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

Fix token decoding during wp-env startup. #220

Merged
merged 10 commits into from
Dec 30, 2024
Merged

Conversation

alecgeatches
Copy link
Contributor

@alecgeatches alecgeatches commented Nov 27, 2024

This PR fixes an bug that can cause wp-env startup to fail when the remote_data_blocks_config option has been set previously and the WordPress version managed by wp-env changes. To reproduce:

  1. Set up .wp-env.override.json with config values.

  2. Run wp-env start.

  3. Change the version of WordPress in .wp-env.json, e.g. "core": "WordPress/WordPress#6.7"

  4. Run wp-env start again.

  5. See token decoding errors in wp-env start, and see that the site failed to start:

    $ wp-env start --xdebug
    ✖ Error while running docker compose command.
    WordPress is already installed.
    
    # ...
    
    Container 4def32faa5199ca2ba3ba70ddee94dde-tests-mysql-1  Running
    Container 4def32faa5199ca2ba3ba70ddee94dde-tests-wordpress-1  Running
    [25-Nov-2024 17:07:12 UTC] PHP Fatal error:  Uncaught TypeError: json_decode(): Argument #1 ($json) must be of type string, bool given in /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php:162
    Stack trace:
    #0 /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php(162): json_decode(false, true)
    #1 /var/www/html/wp-includes/class-wp-hook.php(326): RemoteDataBlocks\PluginSettings\PluginSettings::decrypt_option('...')
    #2 /var/www/html/wp-includes/plugin.php(205): WP_Hook->apply_filters('...', Array)
    #3 /var/www/html/wp-includes/option.php(247): apply_filters('option_remote_d...', '...', 'remote_data_blo...')
    #4 /var/www/html/wp-content/plugins/remote-data-blocks/inc/WpdbStorage/DataSourceCrud.php(69): get_option('remote_data_blo...', Array)
    #5 /var/www/html/wp-content/plugins/remote-data-blocks/inc/WpdbStorage/DataSourceCrud.php(73): RemoteDataBlocks\WpdbStorage\DataSourceCrud::get_config()
    #6 /var/www/html/wp-content/plugins/remote-data-blocks/inc/Integrations/Airtable/AirtableIntegration.php(10): RemoteDataBlocks\WpdbStorage\DataSourceCrud::get_data_sources('airtable')
    #7 /var/www/html/wp-content/plugins/remote-data-blocks/remote-data-blocks.php(47): RemoteDataBlocks\Integrations\Airtable\AirtableIntegration::init()
    #8 /var/www/html/wp-settings.php(526): include_once('/var/www/html/w...')
    #9 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1375): require('/var/www/html/w...')
    #10 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1294): WP_CLI\Runner->load_wordpress()
    #11 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.php(28): WP_CLI\Runner->start()
    #12 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/bootstrap.php(83): WP_CLI\Bootstrap\LaunchRunner->process(Object(WP_CLI\Bootstrap\BootstrapState))
    #13 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/wp-cli.php(32): WP_CLI\bootstrap()
    #14 phar:///usr/local/bin/wp/php/boot-phar.php(20): include('phar:///usr/loc...')
    #15 /usr/local/bin/wp(4): include('phar:///usr/loc...')
    #16 {main}
      thrown in /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php on line 162
    Error: There has been a critical error on this website.Learn more about troubleshooting WordPress. There has been a critical error on this website.
    

The fix involved uncovering a few layers of stack traces related to wp-env initialization, load order, and type checks. I'll add some more context about the changes in the code below.

@alecgeatches alecgeatches self-assigned this Nov 27, 2024

try {
$decrypted = $decryptor->decrypt( $value );
return json_decode( $decrypted, true );

if ( false === $decrypted ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fixes the original stack trace in the issue description. $decryptor->decrypt() may return false, which causes the original error when json_decode() is called:

PHP Fatal error:  Uncaught TypeError: json_decode(): Argument #1 ($json) must be of type string, bool given in /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php:162
Stack trace:
#0 /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php(162): json_decode(false, true)

Copy link
Contributor Author

@alecgeatches alecgeatches Nov 27, 2024

Choose a reason for hiding this comment

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

From testing, I think the reason this fails is that LOGGED_IN_KEY and LOGGED_IN_SALT have different values during wp-env initial setup and once the site is actually running. I tried adding error_log()s in the relevant code where we use these values and both values changed during initialization of wp-env after a version change and during normal website usage.

Because the values are initially encrypted with the site's real LOGGED_IN_ keys, this process only fails during wp-env initialization to a new WordPress version.


private static function show_decryption_error(): void {
// Check that we have add_settings_error() available. This can be unavailable during wp-env startup.
if ( is_admin() ) {
Copy link
Contributor Author

@alecgeatches alecgeatches Nov 27, 2024

Choose a reason for hiding this comment

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

Even after the fixes above, I was getting this stack trace for using the add_settings_error() function:

PHP Fatal error:  Uncaught Error: Call to undefined function RemoteDataBlocks\PluginSettings\add_settings_error() in /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php:176
Stack trace:
#0 /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php(168): RemoteDataBlocks\PluginSettings\PluginSettings::add_decrypt_error()
#1 /var/www/html/wp-includes/class-wp-hook.php(326): RemoteDataBlocks\PluginSettings\PluginSettings::decrypt_option('r7xSgvl3faIL5qW...')
#2 /var/www/html/wp-includes/plugin.php(205): WP_Hook->apply_filters('r7xSgvl3faIL5qW...', Array)
#3 /var/www/html/wp-includes/option.php(247): apply_filters('option_remote_d...', 'r7xSgvl3faIL5qW...', 'remote_data_blo...')
#4 /var/www/html/wp-content/plugins/remote-data-blocks/inc/WpdbStorage/DataSourceCrud.php(69): get_option('remote_data_blo...', Array)
#5 /var/www/html/wp-content/plugins/remote-data-blocks/inc/WpdbStorage/DataSourceCrud.php(75): RemoteDataBlocks\WpdbStorage\DataSourceCrud::get_config()
#6 /var/www/html/wp-content/plugins/remote-data-blocks/inc/Integrations/Airtable/AirtableIntegration.php(10): RemoteDataBlocks\WpdbStorage\DataSourceCrud::get_data_sources('airtable')
#7 /var/www/html/wp-content/plugins/remote-data-blocks/remote-data-blocks.php(47): RemoteDataBlocks\Integrations\Airtable\AirtableIntegration::init()
#8 /var/www/html/wp-settings.php(526): include_once('/var/www/html/w...')
#9 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1375): require('/var/www/html/w...')
#10 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1294): WP_CLI\Runner->load_wordpress()
#11 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.php(28): WP_CLI\Runner->start()
#12 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/bootstrap.php(83): WP_CLI\Bootstrap\LaunchRunner->process(Object(WP_CLI\Bootstrap\BootstrapState))
#13 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/wp-cli.php(32): WP_CLI\bootstrap()
#14 phar:///usr/local/bin/wp/php/boot-phar.php(20): include('phar:///usr/loc...')
#15 /usr/local/bin/wp(4): include('phar:///usr/loc...')
#16 {main}
  thrown in /var/www/html/wp-content/plugins/remote-data-blocks/inc/PluginSettings/PluginSettings.php on line 176
Error: There has been a critical error on this website.Learn more about troubleshooting WordPress. There has been a critical error on this website.

The root cause is a bit harder to determine. I think WP_CLI is doing something strange with load order, as wp-settings.php is loaded first (which fires the init action) before admin functions are loaded into memory. This does look similar to WordPress' native load order, but I'm only seeing this error during initialization through WP_CLI. I found some similar-sounding load order concerns in WP_CLI issues like wp-cli/wp-cli#6010.

We can get around this error by ensuring is_admin() is true, which means that add_settings_error() will be defined and actually do something.

@@ -154,19 +155,36 @@ public static function encrypt_option( array $new_value, string|array|bool $old_
}
}

public static function decrypt_option( string $value ): array|null {
public static function decrypt_option( string $value ): array {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

decrypt_option() was also changed to return an empty array [] instead of null on error. Returning null causes a runtime type failure in the parent call to get_config(), which expects to return an array.

@chriszarate
Copy link
Member

Running things that might require WordPress initialization on init makes sense, but we have the potential for race conditions, since BlockRegistration also runs on init. So we might need to reintroduce a custom hook that runs after init.

@alecgeatches
Copy link
Contributor Author

@chriszarate @maxschmeling We can also run our important block registration on init with a lower priority (like 5) if we want to ensure it's available before init, or as Chris suggested above we can run it on a custom hook that runs after init. Any thoughts? I think it's best if we can work around WordPress' lifecycle per-component, in whatever organization makes most sense.

@alecgeatches
Copy link
Contributor Author

Actually, it already appears we have BlockRegistration running with a higher priority of 50:

public static function init(): void {
add_action( 'init', [ __CLASS__, 'register_blocks' ], 50, 0 );

This makes sense to me as we'd expect blocks to register during init (priority 10) and anything that may have a race condition can run with a higher priority after like this. Are there other race condition issues we should worry about?

Copy link
Member

@chriszarate chriszarate left a comment

Choose a reason for hiding this comment

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

Fine with this approach, but needs to be done with more classes now

@@ -7,6 +7,10 @@

class AirtableIntegration {
public static function init(): void {
add_action( 'init', [ __CLASS__, 'register_blocks' ] );
Copy link
Member

Choose a reason for hiding this comment

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

Add explicit arity and priority args

@alecgeatches
Copy link
Contributor Author

@maxschmeling We've had some discussion above (#220 (comment) + more) since your prior review. Could you take a look at the current state and let me know if this is good to go? Thank you!

@maxschmeling
Copy link
Contributor

@maxschmeling We've had some discussion above (#220 (comment) + more) since your prior review. Could you take a look at the current state and let me know if this is good to go? Thank you!

Looks great! Very thorough rebuttal 😁

@maxschmeling maxschmeling merged commit 8242f14 into trunk Dec 30, 2024
11 checks passed
@maxschmeling maxschmeling deleted the fix/token-decoding branch December 30, 2024 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants