diff --git a/features/search-replace.feature b/features/search-replace.feature index 2bccd606..1384aa64 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1203,90 +1203,42 @@ Feature: Do global search/replace a:1:{i:0;O:10:"CornFlakes":0:{}} """ - @require-mysql @less-than-php-8.0 - Scenario: Warn and ignore type-hinted objects that have some error in deserialization (PHP < 8.0) - Given a WP install - And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation','O:13:\"mysqli_result\":5:{s:13:\"current_field\";N;s:11:\"field_count\";N;s:7:\"lengths\";N;s:8:\"num_rows\";N;s:4:\"type\";N;}')"` - And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation_2','O:8:\"mysqli_result\":5:{s:13:\"current_field\";i:1;s:11:\"field_count\";i:2;s:7:\"lengths\";a:1:{i:0;s:4:\"blah\";}s:8:\"num_rows\";i:1;s:4:\"type\";i:2;}')"` - - When I try `wp search-replace mysqli_result stdClass` - Then STDERR should contain: - """ - Warning: WP_CLI\SearchReplacer::run_recursively(): Couldn't fetch mysqli_result - """ - And STDOUT should contain: - """ - Success: Made 1 replacement. - """ + @require-mysql + Scenario: The search_replace_unserialize_options hook allows overriding allowed_classes for unserialize - When I run `wp db query "SELECT option_value from wp_options where option_name='cereal_isation_2'" --skip-column-names` - Then STDOUT should contain: - """ - O:8:"stdClass":5:{s:13:"current_field";i:1;s:11:"field_count";i:2;s:7:"lengths";a:1:{i:0;s:4:"blah";}s:8:"num_rows";i:1;s:4:"type";i:2;} - """ - And save STDOUT as {SERIALIZED_RESULT} - And a test_php.php file: + Given a WP install + And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation','O:7:\"MyClass\":1:{s:3:\"foo\";s:13:\"cereal_marker\";}')"` + And a hook.php file: """ - [ 'stdClass', 'MyClass' ] ]; + } ); """ - When I try `wp eval-file test_php.php` - Then STDOUT should contain: - """ - stdClass Object - """ - And STDOUT should contain: + When I try `wp search-replace cereal_marker cereal_replaced` + Then STDERR should contain: """ - [current_field] => 1 + Warning: Skipping an uninitialized class "MyClass", replacements might not be complete. """ And STDOUT should contain: """ - [field_count] => 2 + Success: Made 0 replacements. """ - @require-mysql @require-php-8.0 @less-than-php-8.1 - Scenario: Warn and ignore type-hinted objects that have some error in deserialization (PHP 8.0) - Given a WP install - And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation','O:13:\"mysqli_result\":5:{s:13:\"current_field\";N;s:11:\"field_count\";N;s:7:\"lengths\";N;s:8:\"num_rows\";N;s:4:\"type\";N;}')"` - And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation_2','O:8:\"mysqli_result\":5:{s:13:\"current_field\";i:1;s:11:\"field_count\";i:2;s:7:\"lengths\";a:1:{i:0;s:4:\"blah\";}s:8:\"num_rows\";i:1;s:4:\"type\";i:2;}')"` - - When I try `wp search-replace mysqli_result stdClass` - Then STDERR should contain: - """ - Warning: Skipping an inconvertible serialized object of type "mysqli_result", replacements might not be complete. Reason: mysqli_result object is already closed. - """ + When I run `wp --require=hook.php search-replace cereal_marker cereal_replaced` + Then STDERR should be empty And STDOUT should contain: """ Success: Made 1 replacement. """ - When I run `wp db query "SELECT option_value from wp_options where option_name='cereal_isation_2'" --skip-column-names` - Then STDOUT should contain: - """ - O:8:"stdClass":5:{s:13:"current_field";i:1;s:11:"field_count";i:2;s:7:"lengths";a:1:{i:0;s:4:"blah";}s:8:"num_rows";i:1;s:4:"type";i:2;} - """ - And save STDOUT as {SERIALIZED_RESULT} - And a test_php.php file: - """ - 1 - """ - And STDOUT should contain: - """ - [field_count] => 2 - """ + @require-mysql + Scenario: Warn and ignore type-hinted objects that have some error in deserialization - @require-mysql @require-php-8.1 - Scenario: Warn and ignore type-hinted objects that have some error in deserialization (PHP 8.1+) Given a WP install And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation','O:13:\"mysqli_result\":5:{s:13:\"current_field\";N;s:11:\"field_count\";N;s:7:\"lengths\";N;s:8:\"num_rows\";N;s:4:\"type\";N;}')"` And I run `wp db query "INSERT INTO wp_options (option_name,option_value) VALUES ('cereal_isation_2','O:8:\"mysqli_result\":5:{s:13:\"current_field\";i:1;s:11:\"field_count\";i:2;s:7:\"lengths\";a:1:{i:0;s:4:\"blah\";}s:8:\"num_rows\";i:1;s:4:\"type\";i:2;}')"` @@ -1294,38 +1246,13 @@ Feature: Do global search/replace When I try `wp search-replace mysqli_result stdClass` Then STDERR should contain: """ - Warning: Skipping an inconvertible serialized object: "O:13:"mysqli_result":5:{s:13:"current_field";N;s:11:"field_count";N;s:7:"lengths";N;s:8:"num_rows";N;s:4:"type";N;}", replacements might not be complete. Reason: Cannot assign null to property mysqli_result::$current_field of type int. + Warning: Skipping an uninitialized class "mysqli_result", replacements might not be complete. """ And STDOUT should contain: """ Success: Made 1 replacement. """ - When I run `wp db query "SELECT option_value from wp_options where option_name='cereal_isation_2'" --skip-column-names` - Then STDOUT should contain: - """ - O:8:"stdClass":5:{s:13:"current_field";i:1;s:11:"field_count";i:2;s:7:"lengths";a:1:{i:0;s:4:"blah";}s:8:"num_rows";i:1;s:4:"type";i:2;} - """ - And save STDOUT as {SERIALIZED_RESULT} - And a test_php.php file: - """ - 1 - """ - And STDOUT should contain: - """ - [field_count] => 2 - """ - # See https://github.com/wp-cli/search-replace-command/issues/190 Scenario: Regex search/replace with `--regex-limit=1` option Given a WP install diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 6f3bea9f..b6992c35 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -67,6 +67,11 @@ class SearchReplacer { */ private $max_recursion; + /** + * @var array + */ + private $unserialize_options; + /** * @param string $from String we're looking to replace. * @param string $to What we want it to be replaced with. @@ -94,6 +99,18 @@ public function __construct( $from, $to, $recurse_objects = false, $regex = fals // 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' ) ); + + /** + * Filter the options passed to unserialize() during search-replace. + * + * Defaults to `[ 'allowed_classes' => [ 'stdClass' ] ]` to allow the + * built-in stdClass (used extensively by WordPress, e.g. theme mods) + * while blocking arbitrary user-defined class instantiation. Use this + * hook to allow additional classes when needed. + * + * @param array $options Options array for unserialize(). + */ + $this->unserialize_options = \WP_CLI::do_hook( 'search_replace_unserialize_options', [ 'allowed_classes' => [ 'stdClass' ] ] ); } /** @@ -141,7 +158,7 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis // 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; + $unserialized = is_string( $data ) ? @unserialize( $data, $this->unserialize_options ) : false; error_reporting( $error_reporting ); } catch ( \TypeError $exception ) { // phpcs:ignore PHPCompatibility.Classes.NewClasses.typeerrorFound