diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts index 030886a2a8..23e2997fc3 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts @@ -180,6 +180,52 @@ describe('Blueprint step activatePlugin()', () => { ).resolves.not.toThrow(); }); + it('should log noisy activation output and still treat the plugin as active', async () => { + const docroot = handler.documentRoot; + php.writeFile( + `${docroot}/wp-content/plugins/noisy-plugin.php`, + ` { + const docroot = handler.documentRoot; + php.writeFile( + `${docroot}/wp-content/plugins/noisy-plugin.php`, + ` { const docroot = handler.documentRoot; php.writeFile( diff --git a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts index ad3d3a9f98..300c34cfcb 100644 --- a/packages/playground/blueprints/src/lib/steps/activate-plugin.ts +++ b/packages/playground/blueprints/src/lib/steps/activate-plugin.ts @@ -37,6 +37,21 @@ export const activatePlugin: StepHandler = async ( progress?.tracker.setCaption(`Activating ${pluginName || pluginPath}`); const docroot = await playground.documentRoot; + /** + * Instead of checking the plugin activation response, + * check if the plugin is active by looking at the active plugins list. + * + * We have to split the activation and the check into two PHP runs + * because some plugins might redirect during activation, + * which would prevent any output that happens after activation from being returned. + * + * Relying on the plugin activation response is not reliable because if the plugin activation + * produces any output, WordPress will assume it's an activation error and return a WP_Error. + * WordPress will still activate the plugin and load the required page, + * but it will also show the error as a notice in wp-admin. + * See WordPress source code for more details: + * https://github.com/WordPress/wordpress-develop/blob/6.7/src/wp-admin/includes/plugin.php#L733 + */ const activatePluginResult = await playground.run({ code: ` = async ( } /** - * Instead of checking the plugin activation response, - * check if the plugin is active by looking at the active plugins list. + * Instead of trusting the activation response, check the active plugins list. * - * We have to split the activation and the check into two PHP runs - * because some plugins might redirect during activation, - * which would prevent any output that happens after activation from being returned. - * - * Relying on the plugin activation response is not reliable because if the plugin activation - * produces any output, WordPress will assume it's an activation error and return a WP_Error. - * WordPress will still activate the plugin and load the required page, - * but it will also show the error as a notice in wp-admin. - * See WordPress source code for more details: - * https://github.com/WordPress/wordpress-develop/blob/6.7/src/wp-admin/includes/plugin.php#L733 - * - * Because some plugins can create an output, we need to use output buffering - * to ensure the 'true' response is not polluted by other outputs. - * If the plugin activation fails, we will return the buffered output as it might - * contain more information about the failure. + * We try to discard any extra output via output buffering. The output of the script below + * end with `{"success": true}` or `{"success": false}`. Only `{"success": true}` is + * treated as a successful plugin activation. */ - const isActiveCheckResult = await playground.run({ + const activationStatusResult = await playground.run({ code: ` = async ( } $active_plugins = get_option( 'active_plugins' ); - foreach ( $active_plugins as $plugin ) { - if ( substr( $plugin, 0, strlen( $relative_plugin_path ) ) === $relative_plugin_path ) { - ob_end_clean(); - die( 'true' ); - } + if ( ! is_array( $active_plugins ) ) { + $active_plugins = array(); } - die( ob_get_flush() ?: 'false' ); + ob_end_clean(); + + /** + * Use a shutdown function to ensure the activation-related output comes + * last in stdout. + */ + register_shutdown_function( function() use ( $relative_plugin_path, $active_plugins ) { + foreach ( $active_plugins as $plugin ) { + if ( substr( $plugin, 0, strlen( $relative_plugin_path ) ) === $relative_plugin_path ) { + die('{"success": true}'); + break; + } + } + die('{"success": false}'); + }); `, env: { DOCROOT: docroot, @@ -146,14 +143,14 @@ export const activatePlugin: StepHandler = async ( }, }); - if (isActiveCheckResult.text === 'true') { - // Plugin activation was successful, yay! + const rawStatus = (activationStatusResult.text ?? '').trim(); + if (rawStatus.endsWith('{"success": true}')) { return; } - - if (isActiveCheckResult.text !== 'false') { - logger.debug(isActiveCheckResult.text); + if (rawStatus !== '{"success": false}') { + logger.debug(rawStatus); } + throw new Error( `Plugin ${pluginPath} could not be activated – WordPress exited with no error. ` + `Sometimes, when $_SERVER or site options are not configured correctly, ` +