Skip to content

Commit

Permalink
[Blueprints] Prevent plugin activation error if plugin redirects duri…
Browse files Browse the repository at this point in the history
…ng activation or produces an output (#2066)
  • Loading branch information
bgrgicak authored and brandonpayton committed Dec 31, 2024
1 parent 2962af0 commit 8faadbe
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 42 deletions.
155 changes: 133 additions & 22 deletions packages/playground/blueprints/src/lib/steps/activate-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,83 @@ describe('Blueprint step activatePlugin()', () => {
php = await handler.getPrimaryPhp();
});

it('should activate the plugin', async () => {
const docroot = php.documentRoot;
it('should activate a plugin file located in the plugins directory', async () => {
const docroot = handler.documentRoot;
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
);
await activatePlugin(php, {
pluginPath: 'test-plugin.php',
});

const response = await php.run({
code: `<?php
require_once '/wordpress/wp-load.php';
require_once ${phpVar(docroot)}. "/wp-admin/includes/plugin.php" ;
echo is_plugin_active('test-plugin.php') ? 'true' : 'false';
`,
});
expect(response.text).toBe('true');
await expect(
activatePlugin(php, {
pluginPath: 'test-plugin.php',
})
).resolves.not.toThrow();
});

it('should activate a plugin file located in a subdirectory of the plugins directory', async () => {
const docroot = handler.documentRoot;
const pluginDir = `${docroot}/wp-content/plugins/test-plugin`;
php.mkdir(pluginDir);
php.writeFile(
`${pluginDir}/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
);

await expect(
activatePlugin(php, {
pluginPath: `test-plugin/test-plugin.php`,
})
).resolves.not.toThrow();
});

it('should activate a plugin if a absolute plugin path is provided', async () => {
const docroot = handler.documentRoot;
php.mkdir(`${docroot}/wp-content/plugins/test-plugin`);
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin/index.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
);

await expect(
activatePlugin(php, {
pluginPath: `${docroot}/wp-content/plugins/test-plugin/index.php`,
})
).resolves.not.toThrow();
});

it('should activate a plugin if a absolute plugin directory path is provided', async () => {
const docroot = handler.documentRoot;
php.mkdir(`${docroot}/wp-content/plugins/test-plugin`);
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
);

await expect(
activatePlugin(php, {
pluginPath: `${docroot}/wp-content/plugins/test-plugin`,
})
).resolves.not.toThrow();
});

it('should activate a plugin if a absolute plugin directory path with a trailing slash is provided', async () => {
const docroot = handler.documentRoot;
php.mkdir(`${docroot}/wp-content/plugins/test-plugin`);
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
);

await expect(
activatePlugin(php, {
pluginPath: `${docroot}/wp-content/plugins/test-plugin/`,
})
).resolves.not.toThrow();
});

it('should detect a silent failure in activating the plugin', async () => {
const docroot = php.documentRoot;
const docroot = handler.documentRoot;
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
Expand All @@ -56,18 +111,17 @@ describe('Blueprint step activatePlugin()', () => {
`${docroot}/wp-content/mu-plugins/0-exit.php`,
`<?php exit(0); `
);
expect(
async () =>
await activatePlugin(php, {
pluginPath: 'test-plugin.php',
})
await expect(
activatePlugin(php, {
pluginPath: 'test-plugin.php',
})
).rejects.toThrow(/Plugin test-plugin.php could not be activated/);
});

it('should run the activation hooks as a priviliged user', async () => {
const docroot = php.documentRoot;
it('should run the activation hooks as a privileged user', async () => {
const docroot = handler.documentRoot;
const createdFilePath =
docroot + '/activation-ran-as-a-priviliged-user.txt';
docroot + '/activation-ran-as-a-privileged-user.txt';
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */
Expand All @@ -84,4 +138,61 @@ describe('Blueprint step activatePlugin()', () => {

expect(php.fileExists(createdFilePath)).toBe(true);
});

it('should activate a plugin if it redirects during activation', async () => {
const docroot = handler.documentRoot;
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin.php`,
`<?php
/**
* Plugin Name: Test Plugin
*/
add_action( 'activated_plugin', function( $plugin ) {
if( $plugin == plugin_basename( __FILE__ ) ) {
wp_redirect( admin_url( 'edit.php' ) );
exit();
}
} );
`
);
await expect(
activatePlugin(php, {
pluginPath: 'test-plugin.php',
})
).resolves.not.toThrow();
});

it('should activate a plugin if it produces a output during activation', async () => {
const docroot = handler.documentRoot;
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin.php`,
`<?php
/**
* Plugin Name: Test Plugin
*/
echo 'Hello World';
`
);
await expect(
activatePlugin(php, {
pluginPath: 'test-plugin.php',
})
).resolves.not.toThrow();
});

it('should not throw an error if the plugin is already active', async () => {
const docroot = handler.documentRoot;
php.writeFile(
`${docroot}/wp-content/plugins/test-plugin.php`,
`<?php /**\n * Plugin Name: Test Plugin */`
);
await activatePlugin(php, {
pluginPath: 'test-plugin.php',
});
await expect(
activatePlugin(php, {
pluginPath: 'test-plugin.php',
})
).resolves.not.toThrow();
});
});
117 changes: 97 additions & 20 deletions packages/playground/blueprints/src/lib/steps/activate-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { phpVar } from '@php-wasm/util';
import { StepHandler } from '.';
import { logger } from '@php-wasm/logger';

/**
* @inheritDoc activatePlugin
* @example
Expand Down Expand Up @@ -39,18 +37,18 @@ export const activatePlugin: StepHandler<ActivatePluginStep> = async (
progress?.tracker.setCaption(`Activating ${pluginName || pluginPath}`);

const docroot = await playground.documentRoot;
const result = await playground.run({
const activatePluginResult = await playground.run({
code: `<?php
define( 'WP_ADMIN', true );
require_once( ${phpVar(docroot)}. "/wp-load.php" );
require_once( ${phpVar(docroot)}. "/wp-admin/includes/plugin.php" );
require_once( getenv('DOCROOT') . "/wp-load.php" );
require_once( getenv('DOCROOT') . "/wp-admin/includes/plugin.php" );
// Set current user to admin
wp_set_current_user( get_users(array('role' => 'Administrator') )[0]->ID );
$plugin_path = ${phpVar(pluginPath)};
$plugin_path = getenv('PLUGIN_PATH');
$response = false;
if (!is_dir($plugin_path)) {
if ( ! is_dir( $plugin_path)) {
$response = activate_plugin($plugin_path);
}
Expand All @@ -65,22 +63,101 @@ export const activatePlugin: StepHandler<ActivatePluginStep> = async (
}
}
if ( null === $response ) {
die('Plugin activated successfully');
} else if ( is_wp_error( $response ) ) {
throw new Exception( $response->get_error_message() );
if ( is_wp_error($response) ) {
die( $response->get_error_message() );
} else if ( false === $response ) {
die( "The activatePlugin step wasn't able to find the plugin $plugin_path." );
}
throw new Exception( 'Unable to activate plugin' );
`,
env: {
PLUGIN_PATH: pluginPath,
DOCROOT: docroot,
},
});
if (result.text !== 'Plugin activated successfully') {
logger.debug(result);
throw new Error(
`Plugin ${pluginPath} could not be activated – WordPress exited with no error. ` +
`Sometimes, when $_SERVER or site options are not configured correctly, ` +
`WordPress exits early with a 301 redirect. ` +
`Inspect the "debug" logs in the console for more details`
if (activatePluginResult.text) {
logger.warn(
`Plugin ${pluginPath} activation printed the following bytes: ${activatePluginResult.text}`
);
}

/**
* 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
*
* 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.
*/
const isActiveCheckResult = 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 . '/';
$relative_plugin_path = getenv( 'PLUGIN_PATH' );
if (strpos($relative_plugin_path, $plugin_directory) === 0) {
$relative_plugin_path = substr($relative_plugin_path, strlen($plugin_directory));
}
if ( is_dir( $plugin_directory . $relative_plugin_path ) ) {
$relative_plugin_path = rtrim( $relative_plugin_path, '/' ) . '/';
}
$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' );
}
}
die( ob_get_flush() ?: 'false' );
`,
env: {
DOCROOT: docroot,
PLUGIN_PATH: pluginPath,
},
});

if (isActiveCheckResult.text === 'true') {
// Plugin activation was successful, yay!
return;
}

if (isActiveCheckResult.text !== 'false') {
logger.debug(isActiveCheckResult.text);
}
throw new Error(
`Plugin ${pluginPath} could not be activated – WordPress exited with no error. ` +
`Sometimes, when $_SERVER or site options are not configured correctly, ` +
`WordPress exits early with a 301 redirect. ` +
`Inspect the "debug" logs in the console for more details.`
);
};

0 comments on commit 8faadbe

Please sign in to comment.