From ae9766b4ecf6bad6189f2c09215bf1ff014fc5a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:41:18 +0000 Subject: [PATCH 1/5] Initial plan From 2cdb92bf05bc52963d7b9b47cd90790338e95743 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:49:53 +0000 Subject: [PATCH 2/5] Add search_replace_unserialize_options hook with allowed_classes=false default Agent-Logs-Url: https://github.com/wp-cli/search-replace-command/sessions/00af441b-87d8-4c9e-83f9-25a04393eb6e Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/search-replace.feature | 109 ++++++++++---------------------- src/WP_CLI/SearchReplacer.php | 18 +++++- 2 files changed, 49 insertions(+), 78 deletions(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index 2bccd606..a7672b4c 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1203,49 +1203,38 @@ 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 option add cereal_isation 'O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}'` + And a hook.php file: """ - [ 'stdClass' ] ]; + } ); """ - When I try `wp eval-file test_php.php` - Then STDOUT should contain: + When I try `wp search-replace bar baz` + Then STDERR should contain: """ - stdClass Object + Warning: Skipping an uninitialized class "stdClass", replacements might not be complete. """ And STDOUT should contain: """ - [current_field] => 1 + Success: Made 0 replacements. """ + + When I run `wp --require=hook.php search-replace bar baz` + Then STDERR should be empty And STDOUT should contain: """ - [field_count] => 2 + Success: Made 1 replacement. """ - @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) + @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;}')"` @@ -1253,36 +1242,27 @@ Feature: Do global search/replace 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. + Warning: Skipping an uninitialized class "mysqli_result", replacements might not be complete. """ And STDOUT should contain: """ - Success: Made 1 replacement. + Success: Made 0 replacements. """ - 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 + Warning: Skipping an uninitialized class "mysqli_result", replacements might not be complete. """ And STDOUT should contain: """ - [field_count] => 2 + Success: Made 0 replacements. """ @require-mysql @require-php-8.1 @@ -1294,36 +1274,11 @@ 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 + Success: Made 0 replacements. """ # See https://github.com/wp-cli/search-replace-command/issues/190 diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 6f3bea9f..1aa78be8 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,17 @@ 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' => false ]` to prevent instantiation + * of arbitrary classes as a hardening measure. Use this hook to allow + * specific classes when needed. + * + * @param array $options Options array for unserialize(). + */ + $this->unserialize_options = \WP_CLI::do_hook( 'search_replace_unserialize_options', [ 'allowed_classes' => false ] ); } /** @@ -141,7 +157,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 From 0e9bf73e95a10f654700fe842c6a280ae1457fc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:54:21 +0000 Subject: [PATCH 3/5] Fix failing tests: correct replacement count and consolidate PHP-version scenarios Agent-Logs-Url: https://github.com/wp-cli/search-replace-command/sessions/59e4a42c-8f86-450d-b2b6-ee493abb4b19 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/search-replace.feature | 37 +++------------------------------ 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index a7672b4c..3e83dfe7 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1233,40 +1233,9 @@ Feature: Do global search/replace Success: Made 1 replacement. """ - @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: Skipping an uninitialized class "mysqli_result", replacements might not be complete. - """ - And STDOUT should contain: - """ - 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 uninitialized class "mysqli_result", replacements might not be complete. - """ - And STDOUT should contain: - """ - Success: Made 0 replacements. - """ + @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;}')"` @@ -1278,7 +1247,7 @@ Feature: Do global search/replace """ And STDOUT should contain: """ - Success: Made 0 replacements. + Success: Made 1 replacement. """ # See https://github.com/wp-cli/search-replace-command/issues/190 From 21f1537c19b5e7f1bfdc550d6b66201789ec3c34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:29:05 +0000 Subject: [PATCH 4/5] Fix test: use unique search term to avoid collisions with WP core content Agent-Logs-Url: https://github.com/wp-cli/search-replace-command/sessions/0832cf35-01cd-463e-8334-064918206a6c Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/search-replace.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index 3e83dfe7..935c81b8 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1207,7 +1207,7 @@ Feature: Do global search/replace Scenario: The search_replace_unserialize_options hook allows overriding allowed_classes for unserialize Given a WP install - And I run `wp option add cereal_isation 'O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}'` + And I run `wp option add cereal_isation 'O:8:"stdClass":1:{s:3:"foo";s:13:"cereal_marker";}'` And a hook.php file: """ Date: Tue, 28 Apr 2026 09:14:50 +0000 Subject: [PATCH 5/5] Fix: allow stdClass by default, use custom class in hook test scenario Agent-Logs-Url: https://github.com/wp-cli/search-replace-command/sessions/599c39d4-de3e-4bc3-a371-0e93ec6c6a78 Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/search-replace.feature | 9 ++++++--- src/WP_CLI/SearchReplacer.php | 9 +++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/features/search-replace.feature b/features/search-replace.feature index 935c81b8..1384aa64 100644 --- a/features/search-replace.feature +++ b/features/search-replace.feature @@ -1207,19 +1207,22 @@ Feature: Do global search/replace Scenario: The search_replace_unserialize_options hook allows overriding allowed_classes for unserialize Given a WP install - And I run `wp option add cereal_isation 'O:8:"stdClass":1:{s:3:"foo";s:13:"cereal_marker";}'` + 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' ] ]; + return [ 'allowed_classes' => [ 'stdClass', 'MyClass' ] ]; } ); """ When I try `wp search-replace cereal_marker cereal_replaced` Then STDERR should contain: """ - Warning: Skipping an uninitialized class "stdClass", replacements might not be complete. + Warning: Skipping an uninitialized class "MyClass", replacements might not be complete. """ And STDOUT should contain: """ diff --git a/src/WP_CLI/SearchReplacer.php b/src/WP_CLI/SearchReplacer.php index 1aa78be8..b6992c35 100644 --- a/src/WP_CLI/SearchReplacer.php +++ b/src/WP_CLI/SearchReplacer.php @@ -103,13 +103,14 @@ public function __construct( $from, $to, $recurse_objects = false, $regex = fals /** * Filter the options passed to unserialize() during search-replace. * - * Defaults to `[ 'allowed_classes' => false ]` to prevent instantiation - * of arbitrary classes as a hardening measure. Use this hook to allow - * specific classes when needed. + * 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' => false ] ); + $this->unserialize_options = \WP_CLI::do_hook( 'search_replace_unserialize_options', [ 'allowed_classes' => [ 'stdClass' ] ] ); } /**