PluginUtil: Add method to get active valid plugins (#48709)

* PluginUtil: Add method to get active valid plugins

When getting a list of active plugins directly from the options table,
you can run into an error if you try to then access one of the plugin
files if it doesn't actually exist. WP Core has a method that verifies
that the files exist before returning the list. But it's marked as a
"private" method, which means it could change and break backcompat.
That seems very unlikely, though, so we're using the method (it was
already in use actually), but putting in some safeguards so we can
detect if a backcompat issue arises.

Fixes #48132

* Modify to accommodate multisite
This commit is contained in:
Corey McKrill 2024-07-11 10:18:33 -07:00 committed by GitHub
parent 79786c5443
commit 4d68cd486e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 108 additions and 14 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Ensure that active plugins shown in the System Status api endpoint actually exist

View File

@ -17,7 +17,7 @@ use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchro
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\{ OrderUtil, PluginUtil };
use Automattic\WooCommerce\Internal\Utilities\PluginInstaller;
defined( 'ABSPATH' ) || exit;
@ -1264,7 +1264,8 @@ class WC_Install {
return;
}
if ( in_array( $legacy_api_plugin, wp_get_active_and_valid_plugins(), true ) ) {
$active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins();
if ( in_array( $legacy_api_plugin, $active_valid_plugins, true ) ) {
return;
}

View File

@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer as Order_DataSynchronizer;
use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil };
use Automattic\WooCommerce\Utilities\{ LoggingUtil, OrderUtil, PluginUtil };
/**
* System status controller class.
@ -1044,16 +1044,11 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
return array();
}
$active_plugins = (array) get_option( 'active_plugins', array() );
if ( is_multisite() ) {
$network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
$active_plugins = array_merge( $active_plugins, $network_activated_plugins );
}
$active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins();
$active_plugins_data = array();
$active_plugins_data = array();
foreach ( $active_plugins as $plugin ) {
$data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
foreach ( $active_valid_plugins as $plugin ) {
$data = get_plugin_data( $plugin );
$active_plugins_data[] = $this->format_plugin_data( $plugin, $data );
}

View File

@ -4,7 +4,7 @@ namespace Automattic\WooCommerce\Internal\Utilities;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\StringUtil;
use Automattic\WooCommerce\Utilities\{ PluginUtil, StringUtil };
/**
* This class allows installing a plugin programmatically.
@ -206,7 +206,14 @@ class PluginInstaller implements RegisterHooksInterface {
* @return bool True if WooCommerce is installed and active in the current blog, false otherwise.
*/
private static function woocommerce_is_active_in_current_site(): bool {
return ! empty( array_filter( wp_get_active_and_valid_plugins(), fn( $plugin ) => substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0 ) );
$active_valid_plugins = wc_get_container()->get( PluginUtil::class )->get_all_active_valid_plugins();
return ! empty(
array_filter(
$active_valid_plugins,
fn( $plugin ) => substr_compare( $plugin, '/woocommerce.php', -strlen( '/woocommerce.php' ) ) === 0
)
);
}
/**

View File

@ -67,6 +67,34 @@ class PluginUtil {
require_once ABSPATH . WPINC . '/plugin.php';
}
/**
* Wrapper for WP's private `wp_get_active_and_valid_plugins` and `wp_get_active_network_plugins` functions.
*
* This combines the results of the two functions to get a list of all plugins that are active within a site.
* It's more useful than just retrieving the option values because it also validates that the plugin files exist.
* This wrapper is also a hedge against backward-incompatible changes since both of the WP methods are marked as
* being "@access private", so if need be we can update our methods here to preserve functionality.
*
* Note that the doc block for `wp_get_active_and_valid_plugins` says it returns "Array of paths to plugin files
* relative to the plugins directory", but it actually returns absolute paths.
*
* @return string[] Array of absolute paths to plugin files.
*/
public function get_all_active_valid_plugins() {
$local = wp_get_active_and_valid_plugins();
if ( is_multisite() ) {
require_once ABSPATH . WPINC . '/ms-load.php';
$network = wp_get_active_network_plugins();
} else {
$network = array();
}
$all = array_merge( $local, $network );
return array_unique( $all );
}
/**
* Get a list with the names of the WordPress plugins that are WooCommerce aware
* (they have a "WC tested up to" header).

View File

@ -32,6 +32,65 @@ class PluginUtilTests extends \WC_Unit_Test_Case {
$this->sut = $this->get_instance_of( PluginUtil::class );
}
/**
* @testdox `get_all_active_valid_plugins` gets the active plugins *that actually exist* and returns them
* as a list of absolute file paths.
*
* The tested function is just a wrapper around two core WP functions that are marked as "private" so this is
* mostly just to ensure that there haven't been any breaking changes to those functions.
*/
public function test_get_all_active_valid_plugins() {
self::touch( WP_PLUGIN_DIR . '/test1/test1.php' );
self::touch( WP_PLUGIN_DIR . '/test2/test2_x.php' );
self::touch( WP_PLUGIN_DIR . '/test3/test3.php' );
$orig_local_plugins = get_option( 'active_plugins' );
$orig_network_plugins = get_site_option( 'active_sitewide_plugins' );
update_option(
'active_plugins',
array(
'test1/test1.php',
'test2/test2.php',
)
);
update_site_option(
'active_sitewide_plugins',
array( 'test3/test3.php' )
);
$active_valid_plugins = $this->sut->get_all_active_valid_plugins();
if ( is_multisite() ) {
$this->assertCount( 2, $active_valid_plugins );
$this->assertContains( WP_PLUGIN_DIR . '/test3/test3.php', $active_valid_plugins );
} else {
$this->assertCount( 1, $active_valid_plugins );
}
$this->assertContains( WP_PLUGIN_DIR . '/test1/test1.php', $active_valid_plugins );
if ( false === $orig_local_plugins ) {
delete_option( 'active_plugins' );
} else {
update_option( 'active_plugins', $orig_local_plugins );
}
if ( false === $orig_network_plugins ) {
delete_site_option( 'active_sitewide_plugins' );
} else {
update_site_option( 'active_sitewide_plugins', $orig_network_plugins );
}
$this->rmdir( WP_PLUGIN_DIR . '/test1' );
$this->rmdir( WP_PLUGIN_DIR . '/test2' );
$this->rmdir( WP_PLUGIN_DIR . '/test3' );
$this->delete_folders( WP_PLUGIN_DIR . '/test1' );
$this->delete_folders( WP_PLUGIN_DIR . '/test2' );
$this->delete_folders( WP_PLUGIN_DIR . '/test3' );
}
/**
* @testdox 'get_woocommerce_aware_plugins' properly gets the names of all the existing WooCommerce aware plugins.
*/