Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip search and replace on objects that can't deserialize safely #192

Merged
merged 26 commits into from
Dec 19, 2023

Conversation

MarkBerube
Copy link
Contributor

As discussed in the following issue thread: #191

PHP 8.1 made it so class objects deserialized from strings must follow the types defined on the class if it has been loaded prior. Else you will face a PHP fatal TypeError:

PHP Fatal error:  Uncaught TypeError: Cannot assign null to property mysqli_result::$current_field of type int in phar:///usr/local/bin/wp/vendor/wp-cli/search-replace-command/src/WP_CLI/SearchReplacer.php:90

This approach is to catch those TypeErrors when they happen, warn the user about it but then continue the S&R process. The only con to this approach that I can see is that TypeError according to PHP's own docs doesn't exist in PHP 5.6 but running the following test in PHP 5.6:

<?php
$data = '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;}';
$error_reporting = error_reporting();
error_reporting( $error_reporting & ~E_NOTICE & ~E_WARNING );

try {
$unserialized = is_string( $data ) ? @unserialize( $data ) : false;
} catch(TypeError $e) {
	// we don't care about these - quit the fatal err & move on
}

error_reporting( $error_reporting );

shows no issues. Probably because PHP 5.6 isn't reaching the catch() block here.

@MarkBerube MarkBerube requested a review from a team as a code owner November 27, 2023 21:05
Copy link
Member

@danielbachhuber danielbachhuber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, @MarkBerube !

Can you include a Behat test for this change? Here's an introduction: https://make.wordpress.org/cli/handbook/contributions/pull-requests/#functional-tests

@schlessera
Copy link
Member

Confirmed that the unknown TypeError class is no issues under PHP 5.6: https://3v4l.org/bgh1S

@MarkBerube
Copy link
Contributor Author

Behat scenario has been added! During behat testing it looks like I'm not entirely skipping that instance of search & replace if it ends up catching a TypeError. What ends up happening is it will treat $data as a string & only do str_replace() over it. Happy with that interaction, as it means those objects will still be replaced successfully.

features/search-replace.feature Outdated Show resolved Hide resolved
features/search-replace.feature Outdated Show resolved Hide resolved
features/search-replace.feature Outdated Show resolved Hide resolved
src/WP_CLI/SearchReplacer.php Outdated Show resolved Hide resolved
@danielbachhuber
Copy link
Member

During behat testing it looks like I'm not entirely skipping that instance of search & replace if it ends up catching a TypeError. What ends up happening is it will treat $data as a string & only do str_replace() over it. Happy with that interaction, as it means those objects will still be replaced successfully.

Hm, I'm not sure this is quite correct. I think we should skip search/replace entirely if we can't deserialize the object. If we search/replace against a serialized string, we can break the serialization.

@danielbachhuber danielbachhuber changed the title skip deserialization of S&R objects that return TypeErrors Skip search and replace on objects that can't deserialize safely Nov 28, 2023
@MarkBerube
Copy link
Contributor Author

I've made it so that an exception is thrown if you hit a TypeError in serialization that should reach the empty catch() block at the end of the recursive run, so serialization is skipped on these unsafe objects. Just waiting for thoughts on this

Copy link
Member

@danielbachhuber danielbachhuber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple small things. Looks like it's in pretty good shape otherwise.

features/search-replace.feature Show resolved Hide resolved
@@ -107,7 +119,7 @@ private function run_recursively( $data, $serialised, $recursion_level = 0, $vis
)
);
} else {
foreach ( $data as $key => $value ) {
foreach ( (array) $data as $key => $value ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it will have negative unintended consequences and should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no worries, removed with the latest commit!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it did lead to these incompatibilities that I don't have a clear fix for yet: https://github.com/wp-cli/search-replace-command/actions/runs/7053576340/job/19201047750 in these specifically failing PHP versions the sql object ends up like this:

object(mysqli_result)#1 (0) {}

Copy link
Contributor Author

@MarkBerube MarkBerube Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem here is that foreaching directly over an object is not always safe in PHP and these objects like above show why. Objects in PHP don't always allow iteration and functions like is_iterable() check for the instance of \Traversable on objects & aren't exactly reliable.

So instead of casting, I've found this to be the safest way to iterate through these objects:

try {
foreach ( $data as $key => $value ) {
	$data->$key = $this->run_recursively( $value, false, $recursion_level + 1, $visited_data );
}
} catch ( \Error $e ) {
	\WP_CLI::warning(
	sprintf(
	'Skipping an inconvertible serialized object: "%s", replacements might not be complete.',
	$data
	)
);

throw new Exception();

...

currently works in all versions of PHP I've tested, but I'll need to change my behat tests to account for specific php versions reporting this as a warning instead of a fatal error.

edit: actually just due to being warnings, using the try{} block seems successful in PHP 7.2, 7.4, 8.0 with these changes.

@danielbachhuber
Copy link
Member

Thanks again for your work on this, @MarkBerube !

This PR is directionally correct. @schlessera is going to do a deep dive in the coming days to validate the approach, evaluate the nature of the PHP changes, etc. Once he approves, this change will be a candidate for a point release.

@MarkBerube
Copy link
Contributor Author

No problem @danielbachhuber! Some of the behat tests provide a different warning in the older PHP versions. I couldn't get behat to conditionally behave with those warnings. Should I keep trying to fix them? cc @schlessera

@schlessera
Copy link
Member

Should I keep trying to fix them?

@MarkBerube No, don't spend more time on it until I've played around with this more. I'll ping here once I have an update.

@schlessera schlessera merged commit 88c9f23 into wp-cli:main Dec 19, 2023
36 checks passed
@schlessera
Copy link
Member

Thanks for all your work on this, @MarkBerube !

@schlessera schlessera added this to the 2.2.0 milestone Dec 19, 2023
@MarkBerube
Copy link
Contributor Author

Of course @schlessera , happy to help out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants