unresolved = array(); $rewritten = $this->walk( $value, $hint, $map ); return array( 'value' => $rewritten, 'unresolved' => array_values( array_unique( $this->unresolved ) ), ); } /** * Recursive worker. * * @param mixed $value Current node. * @param Reference_Hint $hint Reference hint. * @param Id_Map $map The id map. * @return mixed */ private function walk( $value, Reference_Hint $hint, Id_Map $map ) { if ( is_array( $value ) ) { foreach ( $value as $key => $item ) { $value[ $key ] = $this->walk( $item, $hint, $map ); } return $value; } if ( is_int( $value ) ) { return $this->remap( $value, $hint, $map ); } if ( is_string( $value ) ) { return $this->walk_string( $value, $hint, $map ); } // Objects, floats, booleans and null carry no ids we can safely touch. return $value; } /** * Handle the three string shapes: serialized, JSON, plain numeric. * * @param string $value Current node. * @param Reference_Hint $hint Reference hint. * @param Id_Map $map The id map. * @return string */ private function walk_string( string $value, Reference_Hint $hint, Id_Map $map ): string { if ( is_serialized( $value ) ) { $unserialized = unserialize( $value, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- allowed_classes is false; serialized values are rewritten in place. // A serialized scalar is not worth re-serializing round-trip risk for. if ( is_array( $unserialized ) ) { return maybe_serialize( $this->walk( $unserialized, $hint, $map ) ); } return $value; } $first = substr( ltrim( $value ), 0, 1 ); if ( '[' === $first || '{' === $first ) { $decoded = json_decode( $value, true ); if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) { $encoded = wp_json_encode( $this->walk( $decoded, $hint, $map ) ); if ( is_string( $encoded ) ) { return $encoded; } } return $value; } // A plain numeric string is an id stored as text, which is the common case. if ( '' !== $value && ctype_digit( $value ) ) { return (string) $this->remap( (int) $value, $hint, $map ); } // A comma-separated list of ids, which is how more than one plugin // denormalises a relationship. Only ever reached under a list hint, so // an ordinary comma-separated string is never touched. if ( $hint->is_list() && 1 === preg_match( '/^\d+(,\d+)+$/', $value ) ) { $remapped = array(); foreach ( explode( ',', $value ) as $id ) { $remapped[] = $this->remap( (int) $id, $hint, $map ); } return implode( ',', $remapped ); } return $value; } /** * Translate one id, recording it when the map has no entry. * * @param int $id Source-site id. * @param Reference_Hint $hint Reference hint. * @param Id_Map $map The id map. * @return int */ private function remap( int $id, Reference_Hint $hint, Id_Map $map ): int { if ( $id <= 0 ) { return $id; } $bucket = $hint->map_bucket(); $new = $map->get( $bucket, $id ); if ( null === $new ) { $this->unresolved[] = $id; return $id; } return $new; } }