from = $from; $this->to = $to; $this->recurse_objects = $recurse_objects; $this->regex = $regex; $this->regex_flags = $regex_flags; $this->regex_delimiter = $regex_delimiter; $this->regex_limit = $regex_limit; $this->logging = $logging; $this->clear_log_data(); // Get the XDebug nesting level. Will be zero (no limit) if no value is set $this->max_recursion = intval( ini_get( 'xdebug.max_nesting_level' ) ); } /** * Take a serialised array and unserialise it replacing elements as needed and * unserialising any subordinate arrays and performing the replace on those too. * Ignores any serialized objects unless $recurse_objects is set to true. * * @param array|string $data The data to operate on. * @param bool $serialised Does the value of $data need to be unserialized? * * @return array The original array with all elements replaced as needed. */ public function run( $data, $serialised = false ) { return $this->run_recursively( $data, $serialised ); } /** * @param int $recursion_level Current recursion depth within the original data. * @param array $visited_data Data that has been seen in previous recursion iterations. */ private function run_recursively( $data, $serialised, $recursion_level = 0, $visited_data = array() ) { // some unseriliased data cannot be re-serialised eg. SimpleXMLElements try { if ( $this->recurse_objects ) { // If we've reached the maximum recursion level, short circuit if ( 0 !== $this->max_recursion && $recursion_level >= $this->max_recursion ) { return $data; } if ( is_array( $data ) || is_object( $data ) ) { // If we've seen this exact object or array before, short circuit if ( in_array( $data, $visited_data, true ) ) { return $data; // Avoid infinite loops when there's a cycle } // Add this data to the list of $visited_data[] = $data; } } try { // The error suppression operator is not enough in some cases, so we disable // reporting of notices and warnings as well. $error_reporting = error_reporting(); error_reporting( $error_reporting & ~E_NOTICE & ~E_WARNING ); $unserialized = is_string( $data ) ? @unserialize( $data ) : false; error_reporting( $error_reporting ); } catch ( \TypeError $exception ) { // phpcs:ignore PHPCompatibility.Classes.NewClasses.typeerrorFound // This type error is thrown when trying to unserialize a string that does not fit the // type declarations of the properties it is supposed to fill. // This type checking was introduced with PHP 8.1. // See https://github.com/wp-cli/search-replace-command/issues/191 \WP_CLI::warning( sprintf( 'Skipping an inconvertible serialized object: "%s", replacements might not be complete. Reason: %s.', $data, $exception->getMessage() ) ); throw new Exception( $exception->getMessage(), $exception->getCode(), $exception ); } if ( false !== $unserialized ) { $data = $this->run_recursively( $unserialized, true, $recursion_level + 1 ); } elseif ( is_array( $data ) ) { $keys = array_keys( $data ); foreach ( $keys as $key ) { $data[ $key ] = $this->run_recursively( $data[ $key ], false, $recursion_level + 1, $visited_data ); } } elseif ( $this->recurse_objects && ( is_object( $data ) || $data instanceof \__PHP_Incomplete_Class ) ) { if ( $data instanceof \__PHP_Incomplete_Class ) { $array = new ArrayObject( $data ); \WP_CLI::warning( sprintf( 'Skipping an uninitialized class "%s", replacements might not be complete.', $array['__PHP_Incomplete_Class_Name'] ) ); } else { try { foreach ( $data as $key => $value ) { $data->$key = $this->run_recursively( $value, false, $recursion_level + 1, $visited_data ); } } catch ( \Error $exception ) { // phpcs:ignore PHPCompatibility.Classes.NewClasses.errorFound // This error is thrown when the object that was unserialized cannot be iterated upon. // The most notable reason is an empty `mysqli_result` object which is then considered to be "already closed". // See https://github.com/wp-cli/search-replace-command/pull/192#discussion_r1412310179 \WP_CLI::warning( sprintf( 'Skipping an inconvertible serialized object of type "%s", replacements might not be complete. Reason: %s.', is_object( $data ) ? get_class( $data ) : gettype( $data ), $exception->getMessage() ) ); throw new Exception( $exception->getMessage(), $exception->getCode(), $exception ); } } } elseif ( is_string( $data ) ) { if ( $this->logging ) { $old_data = $data; } if ( $this->regex ) { $search_regex = $this->regex_delimiter; $search_regex .= $this->from; $search_regex .= $this->regex_delimiter; $search_regex .= $this->regex_flags; $result = preg_replace( $search_regex, $this->to, $data, $this->regex_limit ); if ( null === $result || PREG_NO_ERROR !== preg_last_error() ) { \WP_CLI::warning( sprintf( 'The provided regular expression threw a PCRE error - %s', $this->preg_error_message( $result ) ) ); } $data = $result; } else { $data = str_replace( $this->from, $this->to, $data ); } if ( $this->logging && $old_data !== $data ) { $this->log_data[] = $old_data; } } if ( $serialised ) { return serialize( $data ); } } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Intentionally empty. } return $data; } /** * Gets existing data saved for this run when logging. * @return array Array of data strings, prior to replacements. */ public function get_log_data() { return $this->log_data; } /** * Clears data stored for logging. */ public function clear_log_data() { $this->log_data = array(); } /** * Get the PCRE error constant name from an error value. * * @param integer $error Error code. * @return string Error constant name. */ private function preg_error_message( $error ) { static $error_names = null; if ( null === $error_names ) { $definitions = get_defined_constants( true ); $pcre_constants = array_key_exists( 'pcre', $definitions ) ? $definitions['pcre'] : array(); $error_names = array_flip( $pcre_constants ); } return isset( $error_names[ $error ] ) ? $error_names[ $error ] : ''; } }