From 53505fedec5d81a06994bcd4e75df44cc9e6a6c7 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 24 Jul 2023 10:52:01 -0400 Subject: [PATCH 1/6] Add APCU caching --- README.md | 36 ++++++++++++++++++-- src/class-wp-plugin-loader.php | 61 +++++++++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e52e9f8..34bd347 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WP Plugin Loader -Code-enabled WordPress plugin loading. +Code-enabled WordPress plugin loading package. ## Installation @@ -25,9 +25,15 @@ new WP_Plugin_Loader( [ The plugin loader will load the specified plugins, be it files or folders under `plugins`/`client-mu-plugins`, and mark them as activated on the plugins screen. +You can pass files or plugin folders that the package will attempt to determine +the main plugin file from and load. -Also supports preventing activations of plugins via the plugins screen (useful -to fully lock down the plugins enabled on site); +See [APCu Caching](#apcu-caching) for more information on caching. + +### Preventing Activations + +The package supports preventing activations of plugins via the plugins screen +(useful to fully lock down the plugins enabled on site): ```php use Alley\WP\WP_Plugin_Loader; @@ -35,6 +41,30 @@ use Alley\WP\WP_Plugin_Loader; ( new WP_Plugin_Loader( [ ... ] )->prevent_activations(); ``` +Plugin activations will be prevented on the plugin screen as well as with a +capability check. + +### APCu Caching + +When a plugin is loaded by a folder name the package will attempt to determine +the main plugin file from the folder. This can be a semi-expensive operation +that can be cached with APCu. To enable caching, call `enable_caching()` or +`set_cache_prefix( $prefix )` to specify a custom cache prefix. + +```php +use Alley\WP\WP_Plugin_Loader; + +( new WP_Plugin_Loader( [ ... ] ) )->enable_caching(); +``` + +```php +use Alley\WP\WP_Plugin_Loader; + +( new WP_Plugin_Loader( [ ... ] ) )->set_cache_prefix( 'my-prefix' ); +``` + +Note: caching will only be enabled if APCu is available. + ## Testing Run `composer test` to run tests against PHPUnit and the PHP code in the plugin. diff --git a/src/class-wp-plugin-loader.php b/src/class-wp-plugin-loader.php index 607dd7a..60cf322 100644 --- a/src/class-wp-plugin-loader.php +++ b/src/class-wp-plugin-loader.php @@ -11,6 +11,13 @@ * WordPress Plugin Loader */ class WP_Plugin_Loader { + /** + * Cache prefix for APCu caching. + * + * @var string|null + */ + protected ?string $cache_prefix = null; + /** * Array of loaded plugins. * @@ -49,12 +56,36 @@ public function __construct( public array $plugins = [] ) { /** * Prevent any plugin activations for non-code activated plugins. * - * @todo Harden with a capability check. - * * @param bool $prevent Whether to prevent activations. + * @return static */ - public function prevent_activations( bool $prevent = true ): void { + public function prevent_activations( bool $prevent = true ): static { $this->prevent_activations = $prevent; + + return $this; + } + + /** + * Enable APCu caching for plugin paths. + * + * @return static + */ + public function enable_caching(): static { + return $this->set_cache_prefix( 'wp-plugin-loader-' ); + } + + /** + * Set the cache prefix for APCu caching. + * + * @param string|null $prefix The cache prefix. + * @return static + */ + public function set_cache_prefix( ?string $prefix ): static { + $this->cache_prefix = function_exists( 'apcu_fetch' ) && filter_var( ini_get( 'apc.enabled' ), FILTER_VALIDATE_BOOLEAN ) + ? $prefix + : null; + + return $this; } /** @@ -77,6 +108,23 @@ protected function load_plugins(): void { continue; } elseif ( false === strpos( $plugin, '.php' ) ) { + // Check the APCu cache if we have a prefix set. + if ( $this->cache_prefix ) { + $cached_plugin_path = apcu_fetch( $this->cache_prefix . $plugin ); + + if ( false !== $cached_plugin_path ) { + // Check if the plugin path is valid. If it is, require + // it. Continue either way if the cache was not false. + if ( is_string( $cached_plugin_path ) && ! empty( $cached_plugin_path ) ) { + require_once $cached_plugin_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + + $this->loaded_plugins[] = trim( substr( $cached_plugin_path, strlen( WP_PLUGIN_DIR ) + 1 ), '/' ); + } + + continue; + } + } + // Attempt to locate the plugin by name if it isn't a file. $sanitized_plugin = $this->sanitize_plugin_name( $plugin ); @@ -86,7 +134,7 @@ protected function load_plugins(): void { WP_PLUGIN_DIR . "/$sanitized_plugin.php", ]; - $match = false; + $match = null; foreach ( $paths as $path ) { if ( file_exists( $path ) ) { @@ -101,6 +149,11 @@ protected function load_plugins(): void { // Bail if we found a match. if ( $match ) { + // Cache the plugin path in APCu if we have a prefix set. + if ( $this->cache_prefix ) { + apcu_store( $this->cache_prefix . $plugin, $path ); + } + continue; } } From 10d3dea025c09f0ca582869a88428dad36045214 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 24 Jul 2023 10:54:27 -0400 Subject: [PATCH 2/6] CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1659af5..9004c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `WP Plugin Loader` will be documented in this file. +## 0.1.1 - 2023-07-24 + +- Added APCu caching for plugin folder lookups. + ## 0.1.0 - 2023-07-22 - Initial release From c94033c4b99a0562719114e4576c14d9754eff64 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 24 Jul 2023 11:16:06 -0400 Subject: [PATCH 3/6] Refactoring to properly discover from client-mu-plugins --- src/class-wp-plugin-loader.php | 93 ++++++++++++++++++++++------------ stubs/constants.stub | 1 + 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/class-wp-plugin-loader.php b/src/class-wp-plugin-loader.php index 60cf322..39c0621 100644 --- a/src/class-wp-plugin-loader.php +++ b/src/class-wp-plugin-loader.php @@ -92,22 +92,40 @@ public function set_cache_prefix( ?string $prefix ): static { * Load the requested plugins. */ protected function load_plugins(): void { - $client_mu_plugins = is_dir( WP_CONTENT_DIR . '/client-mu-plugins' ); + $folders = [ + WP_CONTENT_DIR . '/plugins', + ]; + + // Use the client mu-plugins directory if it exists and the constant + // from VIP's mu-plugins is defined. + if ( defined( WPCOM_VIP_CLIENT_MU_PLUGIN_DIR ) ) { + $folders[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR; + } else { + // Add the mu-plugins directory if the client mu-plugins directory + // does not exist. We wouldn't want to attempt to load from VIP's + // mu-plugins directory if the client mu-plugins directory exists. + $folders[] = WP_CONTENT_DIR . '/mu-plugins'; + } + // Loop through each plugin and attempt to load it. foreach ( $this->plugins as $plugin ) { - if ( file_exists( WP_PLUGIN_DIR . "/$plugin" ) && ! is_dir( WP_PLUGIN_DIR . "/$plugin" ) ) { - require_once WP_PLUGIN_DIR . "/$plugin"; - - $this->loaded_plugins[] = trim( $plugin, '/' ); - - continue; - } elseif ( $client_mu_plugins && file_exists( WP_CONTENT_DIR . "/client-mu-plugins/$plugin" ) && ! is_dir( WP_CONTENT_DIR . "/client-mu-plugins/$plugin" ) ) { - $plugin = ltrim( $plugin, '/' ); + // Loop through each possible folder and attempt to load the plugin + // from it. + foreach ( $folders as $folder ) { + if ( file_exists( $folder . "/$plugin" ) && ! is_dir( $folder . "/$plugin" ) ) { + require_once $folder . "/$plugin"; + + // Mark the plugin as loaded if it is in the /plugins directory. + if ( WP_CONTENT_DIR . '/plugins' === $folder ) { + $this->loaded_plugins[] = trim( $plugin, '/' ); + } - require_once WP_CONTENT_DIR . "/client-mu-plugins/$plugin"; + continue 2; + } + } - continue; - } elseif ( false === strpos( $plugin, '.php' ) ) { + // Attempt to locate the plugin by name if it isn't a file. + if ( false === strpos( $plugin, '.php' ) ) { // Check the APCu cache if we have a prefix set. if ( $this->cache_prefix ) { $cached_plugin_path = apcu_fetch( $this->cache_prefix . $plugin ); @@ -134,41 +152,54 @@ protected function load_plugins(): void { WP_PLUGIN_DIR . "/$sanitized_plugin.php", ]; - $match = null; + // Include the client mu-plugins directory if it exists. + if ( defined( WPCOM_VIP_CLIENT_MU_PLUGIN_DIR ) ) { + $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin/$sanitized_plugin.php"; + $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin/plugin.php"; + $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin.php"; + } foreach ( $paths as $path ) { if ( file_exists( $path ) ) { require_once $path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable - $match = true; + // Cache the plugin path in APCu if we have a prefix set. + if ( $this->cache_prefix ) { + apcu_store( $this->cache_prefix . $plugin, $path ); + } + + // Mark the plugin as loaded if it is in the /plugins directory. $this->loaded_plugins[] = trim( substr( $path, strlen( WP_PLUGIN_DIR ) + 1 ), '/' ); - break; - } - } - // Bail if we found a match. - if ( $match ) { - // Cache the plugin path in APCu if we have a prefix set. - if ( $this->cache_prefix ) { - apcu_store( $this->cache_prefix . $plugin, $path ); + continue 2; } - - continue; } } - $error_message = sprintf( 'WP Plugin Loader: Plugin %s not found.', $plugin ); + $this->handle_missing_plugin( $plugin ); + } + } - trigger_error( esc_html( $error_message ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + /** + * Handle a missing plugin. + * + * @todo Change return type to never when 8.1 is required. + * + * @param string $plugin The plugin name passed to the loader. + * @return void + */ + protected function handle_missing_plugin( string $plugin ): void { + $error_message = sprintf( 'WP Plugin Loader: Plugin %s not found.', $plugin ); - if ( extension_loaded( 'newrelic' ) && function_exists( 'newrelic_notice_error' ) ) { - newrelic_notice_error( $error_message ); - } + trigger_error( esc_html( $error_message ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - // Bye bye! - die( esc_html( $error_message ) ); + if ( extension_loaded( 'newrelic' ) && function_exists( 'newrelic_notice_error' ) ) { + newrelic_notice_error( $error_message ); } + + // Bye bye! + die( esc_html( $error_message ) ); } /** diff --git a/stubs/constants.stub b/stubs/constants.stub index 643714e..c03d037 100644 --- a/stubs/constants.stub +++ b/stubs/constants.stub @@ -1,3 +1,4 @@ Date: Mon, 24 Jul 2023 11:17:27 -0400 Subject: [PATCH 4/6] Linting fixes --- src/class-wp-plugin-loader.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/class-wp-plugin-loader.php b/src/class-wp-plugin-loader.php index 39c0621..877d16d 100644 --- a/src/class-wp-plugin-loader.php +++ b/src/class-wp-plugin-loader.php @@ -98,7 +98,7 @@ protected function load_plugins(): void { // Use the client mu-plugins directory if it exists and the constant // from VIP's mu-plugins is defined. - if ( defined( WPCOM_VIP_CLIENT_MU_PLUGIN_DIR ) ) { + if ( defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) ) { $folders[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR; } else { // Add the mu-plugins directory if the client mu-plugins directory @@ -113,7 +113,7 @@ protected function load_plugins(): void { // from it. foreach ( $folders as $folder ) { if ( file_exists( $folder . "/$plugin" ) && ! is_dir( $folder . "/$plugin" ) ) { - require_once $folder . "/$plugin"; + require_once $folder . "/$plugin"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable // Mark the plugin as loaded if it is in the /plugins directory. if ( WP_CONTENT_DIR . '/plugins' === $folder ) { @@ -153,7 +153,7 @@ protected function load_plugins(): void { ]; // Include the client mu-plugins directory if it exists. - if ( defined( WPCOM_VIP_CLIENT_MU_PLUGIN_DIR ) ) { + if ( defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) ) { $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin/$sanitized_plugin.php"; $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin/plugin.php"; $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin.php"; From 63856a80a0e1104741daae24f0ae9ce500f684de Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 24 Jul 2023 11:54:09 -0400 Subject: [PATCH 5/6] Allow the flexibility of client-mu-plugins to be used where it exists --- src/class-wp-plugin-loader.php | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/class-wp-plugin-loader.php b/src/class-wp-plugin-loader.php index 877d16d..cb4795f 100644 --- a/src/class-wp-plugin-loader.php +++ b/src/class-wp-plugin-loader.php @@ -96,14 +96,24 @@ protected function load_plugins(): void { WP_CONTENT_DIR . '/plugins', ]; - // Use the client mu-plugins directory if it exists and the constant - // from VIP's mu-plugins is defined. + $client_mu_plugins_dir = defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) + ? WPCOM_VIP_CLIENT_MU_PLUGIN_DIR + : ( is_dir( WP_CONTENT_DIR . '/client-mu-plugins' ) ? WP_CONTENT_DIR . '/client-mu-plugins' : null ); + + /** + * The client-mu-plugins directory should always be used if the + * directory exists. If the WPCOM_VIP_CLIENT_MU_PLUGIN_DIR constant is + * defined, then we won't add the mu-plugins directory to the list of + * folders. Otherwise, we will add the mu-plugins directory to the list + * of folders. + */ if ( defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) ) { $folders[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR; } else { - // Add the mu-plugins directory if the client mu-plugins directory - // does not exist. We wouldn't want to attempt to load from VIP's - // mu-plugins directory if the client mu-plugins directory exists. + if ( $client_mu_plugins_dir ) { + $folders[] = WP_CONTENT_DIR . '/client-mu-plugins'; + } + $folders[] = WP_CONTENT_DIR . '/mu-plugins'; } @@ -153,10 +163,10 @@ protected function load_plugins(): void { ]; // Include the client mu-plugins directory if it exists. - if ( defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) ) { - $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin/$sanitized_plugin.php"; - $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin/plugin.php"; - $paths[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR . "/$sanitized_plugin.php"; + if ( $client_mu_plugins_dir ) { + $paths[] = "$client_mu_plugins_dir/$sanitized_plugin/$sanitized_plugin.php"; + $paths[] = "$client_mu_plugins_dir/$sanitized_plugin/plugin.php"; + $paths[] = "$client_mu_plugins_dir/$sanitized_plugin.php"; } foreach ( $paths as $path ) { From 77ddeb8bd64bbb29b58a0f8e0622448e77a57757 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 24 Jul 2023 11:56:22 -0400 Subject: [PATCH 6/6] README --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 34bd347..d4219fd 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ the main plugin file from and load. See [APCu Caching](#apcu-caching) for more information on caching. +### Plugin Directories + +Out of the box, the package will attempt to load your plugin from +`wp-content/plugins`. When it is found, the package will attempt to load your +plugin from `wp-content/client-mu-plugins`. For non-WordPress VIP sites, the +plugin will also load plugins from `wp-content/mu-plugins`. + ### Preventing Activations The package supports preventing activations of plugins via the plugins screen @@ -46,19 +53,16 @@ capability check. ### APCu Caching -When a plugin is loaded by a folder name the package will attempt to determine -the main plugin file from the folder. This can be a semi-expensive operation -that can be cached with APCu. To enable caching, call `enable_caching()` or -`set_cache_prefix( $prefix )` to specify a custom cache prefix. +When a plugin is loaded by a directory name the package will attempt to +determine the main plugin file from the directory. This can be a semi-expensive +operation that can be cached with APCu. To enable caching, call +`enable_caching()` or `set_cache_prefix( $prefix )` to specify a custom cache +prefix. ```php use Alley\WP\WP_Plugin_Loader; ( new WP_Plugin_Loader( [ ... ] ) )->enable_caching(); -``` - -```php -use Alley\WP\WP_Plugin_Loader; ( new WP_Plugin_Loader( [ ... ] ) )->set_cache_prefix( 'my-prefix' ); ```