From 2223c1093aea8886368133d4cd90a07cefb6df50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:20:15 +0000 Subject: [PATCH 01/11] Initial plan From 5953cabd8991b5749dab7bb149f3a5a0ab440934 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:29:33 +0000 Subject: [PATCH 02/11] Add must-use plugin checksum verification support Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/checksum-plugin.feature | 75 ++++++++++++++ src/Checksum_Plugin_Command.php | 171 +++++++++++++++++++++++++++++-- 2 files changed, 240 insertions(+), 6 deletions(-) diff --git a/features/checksum-plugin.feature b/features/checksum-plugin.feature index ea6cb19e..285689a9 100644 --- a/features/checksum-plugin.feature +++ b/features/checksum-plugin.feature @@ -204,3 +204,78 @@ Feature: Validate checksums for WordPress plugins """ Verified 1 of 1 plugins. """ + + Scenario: Verify must-use plugin that is a standard plugin moved to mu-plugins + Given a WP install + + When I run `wp plugin install duplicate-post --version=3.2.1` + Then STDOUT should not be empty + + # Move duplicate-post to mu-plugins folder + When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` + Then STDERR should be empty + + When I run `wp plugin verify-checksums --all` + Then STDOUT should contain: + """ + Verified 1 of 1 plugins. + """ + + Scenario: Exclude must-use plugins from verification + Given a WP install + + When I run `wp plugin install duplicate-post --version=3.2.1` + Then STDOUT should not be empty + + # Move duplicate-post to mu-plugins folder + When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` + Then STDERR should be empty + + When I run `wp plugin verify-checksums --all --exclude-mu-plugins` + Then STDOUT should contain: + """ + Verified 0 of 0 plugins. + """ + + Scenario: Modified must-use plugin doesn't verify + Given a WP install + + When I run `wp plugin install duplicate-post --version=3.2.1` + Then STDOUT should not be empty + + # Move duplicate-post to mu-plugins folder + When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` + Then STDERR should be empty + + Given "Duplicate Post" replaced with "Different Name" in the wp-content/mu-plugins/duplicate-post/duplicate-post.php file + + When I try `wp plugin verify-checksums --all --format=json` + Then STDOUT should contain: + """ + "plugin_name":"duplicate-post","file":"duplicate-post.php","message":"Checksum does not match" + """ + And STDERR should contain: + """ + Error: No plugins verified (1 failed). + """ + + Scenario: Single-file must-use plugin without checksums shows warning + Given a WP install + And a wp-content/mu-plugins/custom-mu-plugin.php file: + """ + &1` + Then STDOUT should contain: + """ + Warning: Must-use plugin 'custom-mu-plugin.php' appears to be a custom file or loader plugin and cannot be verified. + """ + And STDOUT should contain: + """ + Verified 0 of 1 plugins (1 skipped). + """ diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 64509d42..820dc639 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -62,6 +62,9 @@ class Checksum_Plugin_Command extends Checksum_Base_Command { * [--exclude=] * : Comma separated list of plugin names that should be excluded from verifying. * + * [--exclude-mu-plugins] + * : Exclude must-use plugins from verification. + * * ## EXAMPLES * * # Verify the checksums of all installed plugins @@ -74,11 +77,12 @@ class Checksum_Plugin_Command extends Checksum_Base_Command { */ public function __invoke( $args, $assoc_args ) { - $fetcher = new Fetchers\UnfilteredPlugin(); - $all = Utils\get_flag_value( $assoc_args, 'all', false ); - $strict = Utils\get_flag_value( $assoc_args, 'strict', false ); - $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); - $plugins = $fetcher->get_many( $all ? $this->get_all_plugin_names() : $args ); + $fetcher = new Fetchers\UnfilteredPlugin(); + $all = Utils\get_flag_value( $assoc_args, 'all', false ); + $strict = Utils\get_flag_value( $assoc_args, 'strict', false ); + $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); + $exclude_mu = Utils\get_flag_value( $assoc_args, 'exclude-mu-plugins', false ); + $plugins = $fetcher->get_many( $all ? $this->get_all_plugin_names() : $args ); /** * @var string $exclude @@ -149,6 +153,22 @@ public function __invoke( $args, $assoc_args ) { } } + // Process must-use plugins if not excluded. + if ( ! $exclude_mu ) { + $mu_plugins = $this->get_mu_plugin_list(); + + foreach ( $mu_plugins as $mu_file => $mu_plugin ) { + $plugin_name = $this->get_plugin_slug_from_header( $mu_file, $mu_plugin ); + + if ( in_array( $plugin_name, $exclude_list, true ) ) { + ++$skips; + continue; + } + + $this->verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_arg, $insecure, $strict, $skips ); + } + } + if ( ! empty( $this->errors ) ) { $formatter = new Formatter( $assoc_args, @@ -157,7 +177,13 @@ public function __invoke( $args, $assoc_args ) { $formatter->display_items( $this->errors ); } - $total = count( $plugins ); + $mu_plugin_count = 0; + if ( ! $exclude_mu ) { + $mu_plugins = $this->get_mu_plugin_list(); + $mu_plugin_count = count( $mu_plugins ); + } + + $total = count( $plugins ) + $mu_plugin_count; $failures = count( array_unique( array_column( $this->errors, 'plugin_name' ) ) ); $successes = $total - $failures - $skips; @@ -361,4 +387,137 @@ private function get_soft_change_files() { private function is_soft_change_file( $file ) { return in_array( strtolower( $file ), $this->get_soft_change_files(), true ); } + + /** + * Gets the list of all must-use plugins. + * + * @return array Array of MU plugins with their file path as key. + */ + private function get_mu_plugin_list() { + if ( ! function_exists( 'get_mu_plugins' ) ) { + return array(); + } + + return get_mu_plugins(); + } + + /** + * Extracts the plugin slug from the plugin header data. + * + * For MU plugins that are actually standard plugins moved to mu-plugins folder, + * we try to extract the plugin slug to look up checksums. + * + * @param string $mu_file Path to the MU plugin file. + * @param array $mu_plugin Plugin header data. + * + * @return string Plugin slug. + */ + private function get_plugin_slug_from_header( $mu_file, $mu_plugin ) { + // If it's in a subdirectory, use the directory name as slug. + if ( false !== strpos( $mu_file, '/' ) ) { + return dirname( $mu_file ); + } + + // For single files, extract the slug from the filename. + return basename( $mu_file, '.php' ); + } + + /** + * Verifies a must-use plugin against WordPress.org checksums. + * + * @param string $mu_file Path to the MU plugin file. + * @param array $mu_plugin Plugin header data. + * @param string $plugin_name Plugin slug/name. + * @param string $version_arg Version to verify against (if specified). + * @param bool $insecure Whether to allow insecure connections. + * @param bool $strict Whether to check soft change files. + * @param int &$skips Reference to skip counter. + */ + private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_arg, $insecure, $strict, &$skips ) { + $is_single_file = false === strpos( $mu_file, '/' ); + + // Get version from the plugin header. + $version = ''; + if ( ! empty( $version_arg ) ) { + $version = $version_arg; + } elseif ( ! empty( $mu_plugin['Version'] ) ) { + $version = $mu_plugin['Version']; + } + + if ( empty( $version ) ) { + WP_CLI::warning( "Could not retrieve the version for must-use plugin {$plugin_name}, skipping." ); + ++$skips; + return; + } + + $wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] ); + + try { + /** + * @var array|false $checksums + */ + $checksums = $wp_org_api->get_plugin_checksums( $plugin_name, $version ); + } catch ( Exception $exception ) { + // If it's a single file or we can't get checksums, warn the user. + if ( $is_single_file ) { + WP_CLI::warning( "Must-use plugin '{$mu_file}' appears to be a custom file or loader plugin and cannot be verified." ); + } else { + WP_CLI::warning( $exception->getMessage() ); + } + ++$skips; + return; + } + + if ( false === $checksums ) { + if ( $is_single_file ) { + WP_CLI::warning( "Must-use plugin '{$mu_file}' appears to be a custom file or loader plugin and cannot be verified." ); + } else { + WP_CLI::warning( "Could not retrieve the checksums for version {$version} of must-use plugin {$plugin_name}, skipping." ); + } + ++$skips; + return; + } + + $files = $this->get_mu_plugin_files( $mu_file ); + + foreach ( $files as $file ) { + if ( ! array_key_exists( $file, $checksums ) ) { + $this->add_error( $plugin_name, $file, 'File was added' ); + continue; + } + + if ( ! $strict && $this->is_soft_change_file( $file ) ) { + continue; + } + + $absolute_path = WPMU_PLUGIN_DIR . '/' . dirname( $mu_file ) . '/' . $file; + if ( false === strpos( $mu_file, '/' ) ) { + // Single file plugin. + $absolute_path = WPMU_PLUGIN_DIR . '/' . $file; + } + + $result = $this->check_file_checksum( $absolute_path, $checksums[ $file ] ); + if ( true !== $result ) { + $this->add_error( $plugin_name, $file, is_string( $result ) ? $result : 'Checksum does not match' ); + } + } + } + + /** + * Gets the list of files that are part of the given MU plugin. + * + * @param string $mu_file Path to the main MU plugin file. + * + * @return array Array of files with their relative paths. + */ + private function get_mu_plugin_files( $mu_file ) { + // If it's a single file in the root of mu-plugins, return just that file. + if ( false === strpos( $mu_file, '/' ) ) { + return (array) basename( $mu_file ); + } + + // If it's in a subdirectory, get all files in that directory. + $folder = WPMU_PLUGIN_DIR . '/' . dirname( $mu_file ); + return $this->get_files( trailingslashit( $folder ) ); + } } From 5a70d976e1e5aa79f4bc1ef98f7f142e946be8e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:32:28 +0000 Subject: [PATCH 03/11] Fix array return type in get_mu_plugin_files Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 820dc639..640161e0 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -513,7 +513,7 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ private function get_mu_plugin_files( $mu_file ) { // If it's a single file in the root of mu-plugins, return just that file. if ( false === strpos( $mu_file, '/' ) ) { - return (array) basename( $mu_file ); + return array( basename( $mu_file ) ); } // If it's in a subdirectory, get all files in that directory. From d56013e49bc2fea2d66916379252eec7581055a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:36:02 +0000 Subject: [PATCH 04/11] Optimize MU plugin processing - avoid duplicate calls and simplify logic Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 640161e0..ac97b6d2 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -154,6 +154,7 @@ public function __invoke( $args, $assoc_args ) { } // Process must-use plugins if not excluded. + $mu_plugins = array(); if ( ! $exclude_mu ) { $mu_plugins = $this->get_mu_plugin_list(); @@ -177,13 +178,7 @@ public function __invoke( $args, $assoc_args ) { $formatter->display_items( $this->errors ); } - $mu_plugin_count = 0; - if ( ! $exclude_mu ) { - $mu_plugins = $this->get_mu_plugin_list(); - $mu_plugin_count = count( $mu_plugins ); - } - - $total = count( $plugins ) + $mu_plugin_count; + $total = count( $plugins ) + count( $mu_plugins ); $failures = count( array_unique( array_column( $this->errors, 'plugin_name' ) ) ); $successes = $total - $failures - $skips; @@ -490,11 +485,9 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ continue; } - $absolute_path = WPMU_PLUGIN_DIR . '/' . dirname( $mu_file ) . '/' . $file; - if ( false === strpos( $mu_file, '/' ) ) { - // Single file plugin. - $absolute_path = WPMU_PLUGIN_DIR . '/' . $file; - } + $absolute_path = $is_single_file + ? WPMU_PLUGIN_DIR . '/' . $file + : WPMU_PLUGIN_DIR . '/' . dirname( $mu_file ) . '/' . $file; $result = $this->check_file_checksum( $absolute_path, $checksums[ $file ] ); if ( true !== $result ) { From b56dd04a5ea012930aa96c439e9cf37e93967bd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:25:50 +0000 Subject: [PATCH 05/11] Address review comments - refactor and fix path bug - Remove get_mu_plugin_list() wrapper, use get_mu_plugins() directly - Rename get_plugin_slug_from_header() to get_plugin_slug_from_path() and remove unused parameter - Extract version retrieval into get_plugin_version_for_verification() method - Fix critical bug: create check_mu_file_checksum() to properly handle WPMU_PLUGIN_DIR paths Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 91 +++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index ac97b6d2..956b807e 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -156,10 +156,10 @@ public function __invoke( $args, $assoc_args ) { // Process must-use plugins if not excluded. $mu_plugins = array(); if ( ! $exclude_mu ) { - $mu_plugins = $this->get_mu_plugin_list(); + $mu_plugins = get_mu_plugins(); foreach ( $mu_plugins as $mu_file => $mu_plugin ) { - $plugin_name = $this->get_plugin_slug_from_header( $mu_file, $mu_plugin ); + $plugin_name = $this->get_plugin_slug_from_path( $mu_file ); if ( in_array( $plugin_name, $exclude_list, true ) ) { ++$skips; @@ -309,6 +309,33 @@ private function check_file_checksum( $path, $checksums ) { return in_array( $md5, (array) $checksums['md5'], true ); } + /** + * Checks the integrity of a single MU plugin file by comparing it to the + * officially provided checksum. + * + * @param string $path Relative path to the MU plugin file to check the + * integrity of. + * @param array $checksums Array of provided checksums to compare against. + * + * @return bool|string + */ + private function check_mu_file_checksum( $path, $checksums ) { + if ( $this->supports_sha256() + && array_key_exists( 'sha256', $checksums ) + ) { + $sha256 = $this->get_sha256( WPMU_PLUGIN_DIR . '/' . $path ); + return in_array( $sha256, (array) $checksums['sha256'], true ); + } + + if ( ! array_key_exists( 'md5', $checksums ) ) { + return 'No matching checksum algorithm found'; + } + + $md5 = $this->get_md5( WPMU_PLUGIN_DIR . '/' . $path ); + + return in_array( $md5, (array) $checksums['md5'], true ); + } + /** * Checks whether the current environment supports 256-bit SHA-2. * @@ -384,37 +411,43 @@ private function is_soft_change_file( $file ) { } /** - * Gets the list of all must-use plugins. + * Extracts the plugin slug from the plugin file path. + * + * For MU plugins that are actually standard plugins moved to mu-plugins folder, + * we extract the plugin slug from the file path to look up checksums. * - * @return array Array of MU plugins with their file path as key. + * @param string $plugin_file Path to the plugin file. + * + * @return string Plugin slug. */ - private function get_mu_plugin_list() { - if ( ! function_exists( 'get_mu_plugins' ) ) { - return array(); + private function get_plugin_slug_from_path( $plugin_file ) { + // If it's in a subdirectory, use the directory name as slug. + if ( false !== strpos( $plugin_file, '/' ) ) { + return dirname( $plugin_file ); } - return get_mu_plugins(); + // For single files, extract the slug from the filename. + return basename( $plugin_file, '.php' ); } /** - * Extracts the plugin slug from the plugin header data. + * Gets the version for a plugin from its header data or the version argument. * - * For MU plugins that are actually standard plugins moved to mu-plugins folder, - * we try to extract the plugin slug to look up checksums. + * @param string $version_arg Version argument from command line. + * @param array $plugin_data Plugin header data. * - * @param string $mu_file Path to the MU plugin file. - * @param array $mu_plugin Plugin header data. - * - * @return string Plugin slug. + * @return string|false Plugin version, or false if not found. */ - private function get_plugin_slug_from_header( $mu_file, $mu_plugin ) { - // If it's in a subdirectory, use the directory name as slug. - if ( false !== strpos( $mu_file, '/' ) ) { - return dirname( $mu_file ); + private function get_plugin_version_for_verification( $version_arg, $plugin_data ) { + if ( ! empty( $version_arg ) ) { + return $version_arg; } - // For single files, extract the slug from the filename. - return basename( $mu_file, '.php' ); + if ( ! empty( $plugin_data['Version'] ) ) { + return $plugin_data['Version']; + } + + return false; } /** @@ -432,14 +465,9 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ $is_single_file = false === strpos( $mu_file, '/' ); // Get version from the plugin header. - $version = ''; - if ( ! empty( $version_arg ) ) { - $version = $version_arg; - } elseif ( ! empty( $mu_plugin['Version'] ) ) { - $version = $mu_plugin['Version']; - } + $version = $this->get_plugin_version_for_verification( $version_arg, $mu_plugin ); - if ( empty( $version ) ) { + if ( false === $version ) { WP_CLI::warning( "Could not retrieve the version for must-use plugin {$plugin_name}, skipping." ); ++$skips; return; @@ -485,11 +513,10 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ continue; } - $absolute_path = $is_single_file - ? WPMU_PLUGIN_DIR . '/' . $file - : WPMU_PLUGIN_DIR . '/' . dirname( $mu_file ) . '/' . $file; + // Build the relative path for MU plugins. + $relative_path = $is_single_file ? $file : dirname( $mu_file ) . '/' . $file; - $result = $this->check_file_checksum( $absolute_path, $checksums[ $file ] ); + $result = $this->check_mu_file_checksum( $relative_path, $checksums[ $file ] ); if ( true !== $result ) { $this->add_error( $plugin_name, $file, is_string( $result ) ? $result : 'Checksum does not match' ); } From 0f5e8b3b8ec42639058da3e5577551af7acc4882 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:03:39 +0000 Subject: [PATCH 06/11] Eliminate code duplication by refactoring checksum methods - Add optional base_dir parameter to check_file_checksum() and get_absolute_path() - Remove duplicate check_mu_file_checksum() method - Update verify_mu_plugin() to use unified check_file_checksum() with WPMU_PLUGIN_DIR - Reduces code duplication and improves maintainability Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 46 +++++++++------------------------ 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 956b807e..4b12e068 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -289,14 +289,15 @@ private function get_plugin_files( $path ) { * @param string $path Relative path to the plugin file to check the * integrity of. * @param array $checksums Array of provided checksums to compare against. + * @param string $base_dir Optional. Base directory for the plugin. Defaults to WP_PLUGIN_DIR. * * @return bool|string */ - private function check_file_checksum( $path, $checksums ) { + private function check_file_checksum( $path, $checksums, $base_dir = null ) { if ( $this->supports_sha256() && array_key_exists( 'sha256', $checksums ) ) { - $sha256 = $this->get_sha256( $this->get_absolute_path( $path ) ); + $sha256 = $this->get_sha256( $this->get_absolute_path( $path, $base_dir ) ); return in_array( $sha256, (array) $checksums['sha256'], true ); } @@ -304,34 +305,7 @@ private function check_file_checksum( $path, $checksums ) { return 'No matching checksum algorithm found'; } - $md5 = $this->get_md5( $this->get_absolute_path( $path ) ); - - return in_array( $md5, (array) $checksums['md5'], true ); - } - - /** - * Checks the integrity of a single MU plugin file by comparing it to the - * officially provided checksum. - * - * @param string $path Relative path to the MU plugin file to check the - * integrity of. - * @param array $checksums Array of provided checksums to compare against. - * - * @return bool|string - */ - private function check_mu_file_checksum( $path, $checksums ) { - if ( $this->supports_sha256() - && array_key_exists( 'sha256', $checksums ) - ) { - $sha256 = $this->get_sha256( WPMU_PLUGIN_DIR . '/' . $path ); - return in_array( $sha256, (array) $checksums['sha256'], true ); - } - - if ( ! array_key_exists( 'md5', $checksums ) ) { - return 'No matching checksum algorithm found'; - } - - $md5 = $this->get_md5( WPMU_PLUGIN_DIR . '/' . $path ); + $md5 = $this->get_md5( $this->get_absolute_path( $path, $base_dir ) ); return in_array( $md5, (array) $checksums['md5'], true ); } @@ -375,12 +349,16 @@ private function get_md5( $filepath ) { /** * Gets the absolute path to a relative plugin file. * - * @param string $path Relative path to get the absolute path for. + * @param string $path Relative path to get the absolute path for. + * @param string $base_dir Optional. Base directory to prepend. Defaults to WP_PLUGIN_DIR. * * @return string */ - private function get_absolute_path( $path ) { - return WP_PLUGIN_DIR . '/' . $path; + private function get_absolute_path( $path, $base_dir = null ) { + if ( null === $base_dir ) { + $base_dir = WP_PLUGIN_DIR; + } + return $base_dir . '/' . $path; } /** @@ -516,7 +494,7 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ // Build the relative path for MU plugins. $relative_path = $is_single_file ? $file : dirname( $mu_file ) . '/' . $file; - $result = $this->check_mu_file_checksum( $relative_path, $checksums[ $file ] ); + $result = $this->check_file_checksum( $relative_path, $checksums[ $file ], WPMU_PLUGIN_DIR ); if ( true !== $result ) { $this->add_error( $plugin_name, $file, is_string( $result ) ? $result : 'Checksum does not match' ); } From c808afcfcdf9912238fd0e045c73fad2e886f7ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:47:13 +0000 Subject: [PATCH 07/11] Refactor error handling to eliminate duplication in verify_mu_plugin - Throw exception when checksums retrieval returns false - Centralize error handling in single catch block - Remove duplicate logic for single-file MU plugin warnings - Improves code maintainability Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 4b12e068..42069652 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -458,6 +458,9 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ * @var array|false $checksums */ $checksums = $wp_org_api->get_plugin_checksums( $plugin_name, $version ); + if ( false === $checksums ) { + throw new Exception( "Could not retrieve the checksums for version {$version} of must-use plugin {$plugin_name}, skipping." ); + } } catch ( Exception $exception ) { // If it's a single file or we can't get checksums, warn the user. if ( $is_single_file ) { @@ -469,16 +472,6 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ return; } - if ( false === $checksums ) { - if ( $is_single_file ) { - WP_CLI::warning( "Must-use plugin '{$mu_file}' appears to be a custom file or loader plugin and cannot be verified." ); - } else { - WP_CLI::warning( "Could not retrieve the checksums for version {$version} of must-use plugin {$plugin_name}, skipping." ); - } - ++$skips; - return; - } - $files = $this->get_mu_plugin_files( $mu_file ); foreach ( $files as $file ) { From 1ad3502f7a2d34849da103420a402f429e41db2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:49:47 +0000 Subject: [PATCH 08/11] Remove redundant basename() call in get_mu_plugin_files When strpos() returns false, $mu_file has no '/' character, meaning it's already just a filename without a directory path. The basename() call is redundant in this case. Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 42069652..164ee218 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -504,7 +504,7 @@ private function verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_ private function get_mu_plugin_files( $mu_file ) { // If it's a single file in the root of mu-plugins, return just that file. if ( false === strpos( $mu_file, '/' ) ) { - return array( basename( $mu_file ) ); + return array( $mu_file ); } // If it's in a subdirectory, get all files in that directory. From 5c60595ddf255b4d1e242b5ef3ba0d64cc6cfbd3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 11 Jan 2026 21:22:43 +0100 Subject: [PATCH 09/11] some test tweaks --- features/checksum-plugin.feature | 33 +++++++++++--------------------- src/Checksum_Plugin_Command.php | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/features/checksum-plugin.feature b/features/checksum-plugin.feature index 285689a9..b36486d5 100644 --- a/features/checksum-plugin.feature +++ b/features/checksum-plugin.feature @@ -167,10 +167,7 @@ Feature: Validate checksums for WordPress plugins """ When I try `wp plugin verify-checksums --all --exclude=akismet` - Then STDOUT should contain: - """ - Verified 0 of 1 plugins (1 skipped). - """ + Then STDOUT should match /^Success: Verified 0 of \d plugins \(\d skipped\)\.$/ Scenario: Plugin is verified when the --exclude argument isn't included Given a WP install @@ -189,10 +186,7 @@ Feature: Validate checksums for WordPress plugins """ When I try `wp plugin verify-checksums --all` - Then STDOUT should contain: - """ - Verified 1 of 1 plugins. - """ + Then STDOUT should match /^Success: Verified 1 of \d plugins/ # Hello Dolly was moved from a single file to a directory in WordPress 6.9 @less-than-wp-6.9 @@ -208,18 +202,16 @@ Feature: Validate checksums for WordPress plugins Scenario: Verify must-use plugin that is a standard plugin moved to mu-plugins Given a WP install - When I run `wp plugin install duplicate-post --version=3.2.1` + When I run `wp plugin delete --all` + + And I run `wp plugin install duplicate-post --version=3.2.1` Then STDOUT should not be empty - # Move duplicate-post to mu-plugins folder When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` Then STDERR should be empty When I run `wp plugin verify-checksums --all` - Then STDOUT should contain: - """ - Verified 1 of 1 plugins. - """ + Then STDOUT should match /^Success: Verified 1 of \d plugins/ Scenario: Exclude must-use plugins from verification Given a WP install @@ -227,14 +219,15 @@ Feature: Validate checksums for WordPress plugins When I run `wp plugin install duplicate-post --version=3.2.1` Then STDOUT should not be empty - # Move duplicate-post to mu-plugins folder When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` Then STDERR should be empty - When I run `wp plugin verify-checksums --all --exclude-mu-plugins` + When I run `wp plugin delete --all` + + And I run `wp plugin verify-checksums --all --exclude-mu-plugins` Then STDOUT should contain: """ - Verified 0 of 0 plugins. + Plugin already verified. """ Scenario: Modified must-use plugin doesn't verify @@ -243,7 +236,6 @@ Feature: Validate checksums for WordPress plugins When I run `wp plugin install duplicate-post --version=3.2.1` Then STDOUT should not be empty - # Move duplicate-post to mu-plugins folder When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` Then STDERR should be empty @@ -275,7 +267,4 @@ Feature: Validate checksums for WordPress plugins """ Warning: Must-use plugin 'custom-mu-plugin.php' appears to be a custom file or loader plugin and cannot be verified. """ - And STDOUT should contain: - """ - Verified 0 of 1 plugins (1 skipped). - """ + And STDOUT should match /Success: Verified 2 of \d plugins \(\d skipped\)\.$/ diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 164ee218..640fb395 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -153,6 +153,8 @@ public function __invoke( $args, $assoc_args ) { } } + $total = count( $plugins ); + // Process must-use plugins if not excluded. $mu_plugins = array(); if ( ! $exclude_mu ) { @@ -161,6 +163,14 @@ public function __invoke( $args, $assoc_args ) { foreach ( $mu_plugins as $mu_file => $mu_plugin ) { $plugin_name = $this->get_plugin_slug_from_path( $mu_file ); + if ( ! empty( $args ) ) { + if ( ! in_array( $plugin_name, $args, true ) ) { + continue; + } else { + ++$total; + } + } + if ( in_array( $plugin_name, $exclude_list, true ) ) { ++$skips; continue; @@ -178,7 +188,10 @@ public function __invoke( $args, $assoc_args ) { $formatter->display_items( $this->errors ); } - $total = count( $plugins ) + count( $mu_plugins ); + if ( $all ) { + $total += count( $mu_plugins ); + } + $failures = count( array_unique( array_column( $this->errors, 'plugin_name' ) ) ); $successes = $total - $failures - $skips; From f2d0e80fa2c87942dc66acd186a49e88d52aded0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:29:44 +0000 Subject: [PATCH 10/11] Use get_plugins() to retrieve all MU plugins including subdirectories Changed from get_mu_plugins() to get_plugins('/../mu-plugins') to include both single files in the base mu-plugins directory and plugins in subdirectories. get_mu_plugins() only returns single files in the base directory. Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Checksum_Plugin_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 640fb395..fd1ee464 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -158,7 +158,7 @@ public function __invoke( $args, $assoc_args ) { // Process must-use plugins if not excluded. $mu_plugins = array(); if ( ! $exclude_mu ) { - $mu_plugins = get_mu_plugins(); + $mu_plugins = get_plugins( '/../mu-plugins' ); foreach ( $mu_plugins as $mu_file => $mu_plugin ) { $plugin_name = $this->get_plugin_slug_from_path( $mu_file ); From bc1752a25bc6dc7e10dd52e96d50fdf321c2a756 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 11 Jan 2026 21:54:59 +0100 Subject: [PATCH 11/11] some fixes --- features/checksum-plugin.feature | 19 ++++++++++--------- src/Checksum_Plugin_Command.php | 31 +++++++++++++------------------ 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/features/checksum-plugin.feature b/features/checksum-plugin.feature index b36486d5..b69ab0dc 100644 --- a/features/checksum-plugin.feature +++ b/features/checksum-plugin.feature @@ -167,7 +167,7 @@ Feature: Validate checksums for WordPress plugins """ When I try `wp plugin verify-checksums --all --exclude=akismet` - Then STDOUT should match /^Success: Verified 0 of \d plugins \(\d skipped\)\.$/ + Then STDOUT should match /^Success: Verified \d of \d plugins \(\d skipped\)\./ Scenario: Plugin is verified when the --exclude argument isn't included Given a WP install @@ -186,7 +186,7 @@ Feature: Validate checksums for WordPress plugins """ When I try `wp plugin verify-checksums --all` - Then STDOUT should match /^Success: Verified 1 of \d plugins/ + Then STDOUT should match /^Success: Verified \d of \d plugins/ # Hello Dolly was moved from a single file to a directory in WordPress 6.9 @less-than-wp-6.9 @@ -210,8 +210,12 @@ Feature: Validate checksums for WordPress plugins When I run `mv wp-content/plugins/duplicate-post wp-content/mu-plugins/` Then STDERR should be empty - When I run `wp plugin verify-checksums --all` - Then STDOUT should match /^Success: Verified 1 of \d plugins/ + When I try `wp plugin verify-checksums --all` + Then STDOUT should match /Success: Verified \d of \d plugins/ + And STDERR should not contain: + """ + duplicate-post + """ Scenario: Exclude must-use plugins from verification Given a WP install @@ -246,10 +250,7 @@ Feature: Validate checksums for WordPress plugins """ "plugin_name":"duplicate-post","file":"duplicate-post.php","message":"Checksum does not match" """ - And STDERR should contain: - """ - Error: No plugins verified (1 failed). - """ + And STDERR should match /Error: Only verified \d of \d plugins/ Scenario: Single-file must-use plugin without checksums shows warning Given a WP install @@ -267,4 +268,4 @@ Feature: Validate checksums for WordPress plugins """ Warning: Must-use plugin 'custom-mu-plugin.php' appears to be a custom file or loader plugin and cannot be verified. """ - And STDOUT should match /Success: Verified 2 of \d plugins \(\d skipped\)\.$/ + And STDOUT should match /Success: Verified \d of \d plugins \(\d skipped\)\./ diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index fd1ee464..32a2f119 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -83,6 +83,7 @@ public function __invoke( $args, $assoc_args ) { $insecure = Utils\get_flag_value( $assoc_args, 'insecure', false ); $exclude_mu = Utils\get_flag_value( $assoc_args, 'exclude-mu-plugins', false ); $plugins = $fetcher->get_many( $all ? $this->get_all_plugin_names() : $args ); + $mu_plugins = ! $exclude_mu ? array_merge( get_mu_plugins(), get_plugins( '/../' . basename( WPMU_PLUGIN_DIR ) ) ) : []; /** * @var string $exclude @@ -155,29 +156,23 @@ public function __invoke( $args, $assoc_args ) { $total = count( $plugins ); - // Process must-use plugins if not excluded. - $mu_plugins = array(); - if ( ! $exclude_mu ) { - $mu_plugins = get_plugins( '/../mu-plugins' ); + foreach ( $mu_plugins as $mu_file => $mu_plugin ) { + $plugin_name = $this->get_plugin_slug_from_path( $mu_file ); - foreach ( $mu_plugins as $mu_file => $mu_plugin ) { - $plugin_name = $this->get_plugin_slug_from_path( $mu_file ); - - if ( ! empty( $args ) ) { - if ( ! in_array( $plugin_name, $args, true ) ) { - continue; - } else { - ++$total; - } - } - - if ( in_array( $plugin_name, $exclude_list, true ) ) { - ++$skips; + if ( ! empty( $args ) ) { + if ( ! in_array( $plugin_name, $args, true ) ) { continue; + } else { + ++$total; } + } - $this->verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_arg, $insecure, $strict, $skips ); + if ( in_array( $plugin_name, $exclude_list, true ) ) { + ++$skips; + continue; } + + $this->verify_mu_plugin( $mu_file, $mu_plugin, $plugin_name, $version_arg, $insecure, $strict, $skips ); } if ( ! empty( $this->errors ) ) {