Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
`<?php
/**
* Plugin Name: Noisy Plugin
*/
register_activation_hook( __FILE__, function() {
echo 'Activation says hi';
} );

register_shutdown_function( function() {
echo 'Shutdown chimes in too';
} );
`
);

await expect(
activatePlugin(php, {
pluginPath: 'noisy-plugin.php',
})
).resolves.not.toThrow();
});

it('should throw an error if the plugin was not activated and noisy output is present', async () => {
const docroot = handler.documentRoot;
php.writeFile(
`${docroot}/wp-content/plugins/noisy-plugin.php`,
`<?php
/**
* Plugin Name: Noisy Plugin
*/
register_activation_hook( __FILE__, function() {
throw new Exception( 'Activation failed' );
} );
`
);

await expect(
activatePlugin(php, {
pluginPath: 'noisy-plugin.php',
})
).rejects.toThrow(/Uncaught Exception: Activation failed/);
});

it('should not throw an error if the plugin is already active', async () => {
const docroot = handler.documentRoot;
php.writeFile(
Expand Down
89 changes: 43 additions & 46 deletions packages/playground/blueprints/src/lib/steps/activate-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ export const activatePlugin: StepHandler<ActivatePluginStep> = 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: `<?php
define( 'WP_ADMIN', true );
Expand Down Expand Up @@ -81,47 +96,18 @@ export const activatePlugin: StepHandler<ActivatePluginStep> = 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: `<?php
ob_start();
require_once( getenv( 'DOCROOT' ) . "/wp-load.php" );

/**
* Extracts the relative plugin path from either an absolute or relative plugin path.
*
* Absolute paths starting with plugin directory (e.g., '/wordpress/wp-content/plugins/test-plugin/index.php')
* should be converted to relative paths (e.g., 'test-plugin/index.php')
*
* Directories should finish with a trailing slash to ensure we match the full plugin directory name.
*
* Examples:
* - '/wordpress/wp-content/plugins/test-plugin/index.php' → 'test-plugin/index.php'
* - '/wordpress/wp-content/plugins/test-plugin/' → 'test-plugin/'
* - '/wordpress/wp-content/plugins/test-plugin' → 'test-plugin/'
* - 'test-plugin/index.php' → 'test-plugin/index.php'
* - 'test-plugin/' → 'test-plugin/'
* - 'test-plugin' → 'test-plugin/'
*/
$plugin_directory = WP_PLUGIN_DIR . '/';
$plugin_directory = rtrim( WP_PLUGIN_DIR, '/' ) . '/';
$relative_plugin_path = getenv( 'PLUGIN_PATH' );
if (strpos($relative_plugin_path, $plugin_directory) === 0) {
$relative_plugin_path = substr($relative_plugin_path, strlen($plugin_directory));
Expand All @@ -132,28 +118,39 @@ export const activatePlugin: StepHandler<ActivatePluginStep> = 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,
PLUGIN_PATH: pluginPath,
},
});

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, ` +
Expand Down