Skip to content

Commit

Permalink
Merge pull request #726 from lucatume/v35-arbitrary-paths
Browse files Browse the repository at this point in the history
v3.5 support for arbitrary paths in WPLoader
  • Loading branch information
lucatume authored May 21, 2024
2 parents 040d78e + 6e99d81 commit 320e3d4
Show file tree
Hide file tree
Showing 6 changed files with 450 additions and 11 deletions.
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

0 comments on commit 320e3d4

Please sign in to comment.