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

v3.5 support for arbitrary paths in WPLoader #726

Merged
merged 1 commit into from
May 21, 2024
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
88 changes: 83 additions & 5 deletions src/Module/WPLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -648,17 +648,17 @@ private function installAndBootstrapInstallation(): void
$skipInstall = ($this->config['skipInstall'] ?? false)
&& !Debug::isEnabled()
&& $this->isWordPressInstalled();
$isMultisite = $this->config['multisite'];
$plugins = (array)$this->config['plugins'];

Dispatcher::dispatch(self::EVENT_BEFORE_INSTALL, $this);

if (!$skipInstall) {
putenv('WP_TESTS_SKIP_INSTALL=0');
$isMultisite = $this->config['multisite'];
$plugins = (array)$this->config['plugins'];

/*
* The bootstrap file will load the `wp-settings.php` one that will load plugins and the theme.
* Hook on the option to get the the active plugins to run the plugins' and theme activation
* Hook on the option to get the active plugins to run the plugins' and theme activation
* in a separate process.
*/
if ($isMultisite) {
Expand All @@ -680,6 +680,8 @@ private function installAndBootstrapInstallation(): void
putenv('WP_TESTS_SKIP_INSTALL=1');
}

$silentPlugins = $this->config['silentlyActivatePlugins'];
$this->includeAllPlugins(array_merge($plugins, $silentPlugins), $isMultisite);
$this->includeCorePHPUniteSuiteBootstrapFile();

Dispatcher::dispatch(self::EVENT_AFTER_INSTALL, $this);
Expand Down Expand Up @@ -1057,7 +1059,17 @@ private function activatePluginsTheme(array $plugins): array
// Flush the cache to force the refetch of the options' value.
wp_cache_delete('alloptions', 'options');

return $plugins;
// Do not include external plugins, it would create issues at this stage.
$pluginsDir = $this->installation->getPluginsDir();

return array_values(
array_filter(
$plugins,
static function (string $plugin) use ($pluginsDir) {
return is_file($pluginsDir . "/$plugin");
}
)
);
}

/**
Expand Down Expand Up @@ -1090,8 +1102,24 @@ private function muActivatePluginsTheme(array $plugins): array
// Flush the cache to force the refetch of the options' value.
wp_cache_delete("1::active_sitewide_plugins", 'site-options');

// Do not include external plugins, it would create issues at this stage.
$pluginsDir = $this->installation->getPluginsDir();
$validPlugins = array_values(
array_filter(
$plugins,
static function (string $plugin) use ($pluginsDir) {
return is_file($pluginsDir . "/$plugin");
}
)
);

// Format for site-wide active plugins is `[ 'plugin-slug/plugin.php' => timestamp ]`.
return array_combine($plugins, array_fill(0, count($plugins), time()));
$validActiveSitewidePlugins = array_combine(
$validPlugins,
array_fill(0, count($validPlugins), time())
);

return $validActiveSitewidePlugins;
}

private function isWordPressInstalled(): bool
Expand All @@ -1106,4 +1134,54 @@ private function isWordPressInstalled(): bool
return false;
}
}

/**
* @param string[] $plugins
* @throws ModuleConfigException
*/
private function includeAllPlugins(array $plugins, bool $isMultisite): void
{
PreloadFilters::addFilter('plugins_loaded', function () use ($plugins, $isMultisite) {
$activePlugins = $isMultisite ? get_site_option('active_sitewide_plugins') : get_option('active_plugins');

if (!is_array($activePlugins)) {
$activePlugins = [];
}

$pluginsDir = $this->installation->getPluginsDir();

foreach ($plugins as $plugin) {
if (!is_file($pluginsDir . "/$plugin")) {
$pluginRealPath = realpath($plugin);

if (!$pluginRealPath) {
throw new ModuleConfigException(
__CLASS__,
"Plugin file $plugin does not exist."
);
}

include_once $pluginRealPath;

// Create a name for the external plugin in the format <directory>/<file.php>.
$plugin = basename(dirname($pluginRealPath)) . '/' . basename($pluginRealPath);
}

if ($isMultisite) {
// Network-activated plugins are stored in the format <plugins_name> => <timestamp>.
$activePlugins[$plugin] = time();
} else {
$activePlugins[] = $plugin;
}
}


// Update the active plugins to include all plugins, external or not.
if ($isMultisite) {
update_site_option('active_sitewide_plugins', $activePlugins);
} else {
update_option('active_plugins', array_values(array_unique($activePlugins)));
}
}, -100000);
}
}
65 changes: 63 additions & 2 deletions src/WordPress/CodeExecution/ActivatePluginAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use lucatume\WPBrowser\WordPress\FileRequests\FileRequest;
use lucatume\WPBrowser\WordPress\InstallationException;
use WP_Error;

use function activate_plugin;

class ActivatePluginAction implements CodeExecutionActionInterface
Expand Down Expand Up @@ -38,15 +39,21 @@ public function __construct(
private function activatePlugin(string $plugin, bool $multisite, bool $silent = false): void
{
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$activated = activate_plugin($plugin, '', $multisite, $silent);

if (file_exists(WP_PLUGIN_DIR . '/' . $plugin)) {
$activated = activate_plugin($plugin, '', $multisite, $silent);
} else {
[$activated, $plugin] = $this->activateExternalPlugin($plugin, $multisite, $silent);
}

$activatedString = $multisite ? 'network activated' : 'activated';
$message = "Plugin $plugin could not be $activatedString.";

if ($activated instanceof WP_Error) {
$message = $activated->get_error_message();
$data = $activated->get_error_data();
if ($data && is_string($data)) {
$message .= ": $data";
$message = substr($message, 0, -1) . ": $data";
}
throw new InstallationException(trim($message));
}
Expand All @@ -58,6 +65,60 @@ private function activatePlugin(string $plugin, bool $multisite, bool $silent =
}
}

/**
* @return array{0: bool|WP_Error, 1: string}
*/
private function activateExternalPlugin(
string $plugin,
bool $multisite,
bool $silent = false
): array {
ob_start();
try {
$pluginRealpath = realpath($plugin);

if (!$pluginRealpath) {
return [new \WP_Error('plugin_not_found', "Plugin file $plugin does not exist."), ''];
}

// Get the plugin name in the `plugin/plugin-file.php` format.
$pluginWpName = basename(dirname($pluginRealpath)) . '/' . basename($pluginRealpath);

include_once $pluginRealpath;

if (!$silent) {
do_action('activate_plugin', $pluginWpName, $multisite);
$pluginNameForActivationHook = ltrim($pluginRealpath, '\\/');
do_action("activate_{$pluginNameForActivationHook}", $multisite);
}

$activePlugins = $multisite ? get_site_option('active_sitewide_plugins') : get_option('active_plugins');

if (!is_array($activePlugins)) {
$activePlugins = [];
}

if ($multisite) {
// Network-activated plugins are stored in the format <plugins_name> => <timestamp>.
$activePlugins[$pluginWpName] = time();
update_site_option('active_sitewide_plugins', $activePlugins);
} else {
$activePlugins[] = $pluginWpName;
update_option('active_plugins', $activePlugins);
}
} catch (\Throwable $t) {
return [new \WP_Error('plugin_activation_failed', $t->getMessage()), ''];
}

$output = ob_get_clean();

if ($output) {
return [new \WP_Error('plugin_activation_output', $output), $pluginWpName];
}

return [true, $pluginWpName];
}

public function getClosure(): Closure
{
$request = $this->request;
Expand Down
10 changes: 10 additions & 0 deletions tests/_data/plugins/exploding-plugin/main.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
/** Plugin Name: Exploding Plugin */

function exploding_plugin_main(){}

register_activation_hook( __FILE__, 'exploding_plugin_main_activation' );
function exploding_plugin_main_activation(){
update_option('exploding_plugin_activated', 1);
throw new \RuntimeException('Boom');
}
9 changes: 9 additions & 0 deletions tests/_data/plugins/some-external-plugin/some-plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
/** Plugin Name: Some Plugin */

function some_plugin_main(){}

register_activation_hook( __FILE__, 'some_plugin_activation' );
function some_plugin_activation(){
update_option('some_plugin_activated', 1);
}
2 changes: 0 additions & 2 deletions tests/_support/Traits/LoopIsolation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
namespace lucatume\WPBrowser\Tests\Traits;

use Closure;
use Codeception\Codecept;
use Codeception\Util\Debug;
use lucatume\WPBrowser\Process\Loop;
use lucatume\WPBrowser\Process\ProcessException;
use lucatume\WPBrowser\Process\WorkerException;
use lucatume\WPBrowser\Utils\Codeception;
use lucatume\WPBrowser\Utils\Property;
use ReflectionObject;
use Throwable;
Expand Down
Loading
Loading