@@ -62,6 +62,9 @@ class Checksum_Plugin_Command extends Checksum_Base_Command {
6262 * [--exclude=<name>]
6363 * : Comma separated list of plugin names that should be excluded from verifying.
6464 *
65+ * [--exclude-mu-plugins]
66+ * : Exclude must-use plugins from verification.
67+ *
6568 * ## EXAMPLES
6669 *
6770 * # Verify the checksums of all installed plugins
@@ -74,11 +77,13 @@ class Checksum_Plugin_Command extends Checksum_Base_Command {
7477 */
7578 public function __invoke ( $ args , $ assoc_args ) {
7679
77- $ fetcher = new Fetchers \UnfilteredPlugin ();
78- $ all = Utils \get_flag_value ( $ assoc_args , 'all ' , false );
79- $ strict = Utils \get_flag_value ( $ assoc_args , 'strict ' , false );
80- $ insecure = Utils \get_flag_value ( $ assoc_args , 'insecure ' , false );
81- $ plugins = $ fetcher ->get_many ( $ all ? $ this ->get_all_plugin_names () : $ args );
80+ $ fetcher = new Fetchers \UnfilteredPlugin ();
81+ $ all = Utils \get_flag_value ( $ assoc_args , 'all ' , false );
82+ $ strict = Utils \get_flag_value ( $ assoc_args , 'strict ' , false );
83+ $ insecure = Utils \get_flag_value ( $ assoc_args , 'insecure ' , false );
84+ $ exclude_mu = Utils \get_flag_value ( $ assoc_args , 'exclude-mu-plugins ' , false );
85+ $ plugins = $ fetcher ->get_many ( $ all ? $ this ->get_all_plugin_names () : $ args );
86+ $ mu_plugins = ! $ exclude_mu ? array_merge ( get_mu_plugins (), get_plugins ( '/../ ' . basename ( WPMU_PLUGIN_DIR ) ) ) : [];
8287
8388 /**
8489 * @var string $exclude
@@ -155,6 +160,27 @@ public function __invoke( $args, $assoc_args ) {
155160 }
156161 }
157162
163+ $ total = count ( $ plugins );
164+
165+ foreach ( $ mu_plugins as $ mu_file => $ mu_plugin ) {
166+ $ plugin_name = $ this ->get_plugin_slug_from_path ( $ mu_file );
167+
168+ if ( ! empty ( $ args ) ) {
169+ if ( ! in_array ( $ plugin_name , $ args , true ) ) {
170+ continue ;
171+ } else {
172+ ++$ total ;
173+ }
174+ }
175+
176+ if ( in_array ( $ plugin_name , $ exclude_list , true ) ) {
177+ ++$ skips ;
178+ continue ;
179+ }
180+
181+ $ this ->verify_mu_plugin ( $ mu_file , $ mu_plugin , $ plugin_name , $ version_arg , $ insecure , $ strict , $ skips );
182+ }
183+
158184 if ( ! empty ( $ this ->errors ) ) {
159185 $ formatter = new Formatter (
160186 $ assoc_args ,
@@ -163,7 +189,10 @@ public function __invoke( $args, $assoc_args ) {
163189 $ formatter ->display_items ( $ this ->errors );
164190 }
165191
166- $ total = count ( $ plugins );
192+ if ( $ all ) {
193+ $ total += count ( $ mu_plugins );
194+ }
195+
167196 $ failures = count ( array_unique ( array_column ( $ this ->errors , 'plugin_name ' ) ) );
168197 $ successes = $ total - $ failures - $ skips ;
169198
@@ -335,22 +364,23 @@ private function get_plugin_files( $path ) {
335364 * @param string $path Relative path to the plugin file to check the
336365 * integrity of.
337366 * @param array $checksums Array of provided checksums to compare against.
367+ * @param string $base_dir Optional. Base directory for the plugin. Defaults to WP_PLUGIN_DIR.
338368 *
339369 * @return bool|string
340370 */
341- private function check_file_checksum ( $ path , $ checksums ) {
371+ private function check_file_checksum ( $ path , $ checksums, $ base_dir = null ) {
342372 if ( $ this ->supports_sha256 ()
343373 && array_key_exists ( 'sha256 ' , $ checksums )
344374 ) {
345- $ sha256 = $ this ->get_sha256 ( $ this ->get_absolute_path ( $ path ) );
375+ $ sha256 = $ this ->get_sha256 ( $ this ->get_absolute_path ( $ path, $ base_dir ) );
346376 return in_array ( $ sha256 , (array ) $ checksums ['sha256 ' ], true );
347377 }
348378
349379 if ( ! array_key_exists ( 'md5 ' , $ checksums ) ) {
350380 return 'No matching checksum algorithm found ' ;
351381 }
352382
353- $ md5 = $ this ->get_md5 ( $ this ->get_absolute_path ( $ path ) );
383+ $ md5 = $ this ->get_md5 ( $ this ->get_absolute_path ( $ path, $ base_dir ) );
354384
355385 return in_array ( $ md5 , (array ) $ checksums ['md5 ' ], true );
356386 }
@@ -394,12 +424,16 @@ private function get_md5( $filepath ) {
394424 /**
395425 * Gets the absolute path to a relative plugin file.
396426 *
397- * @param string $path Relative path to get the absolute path for.
427+ * @param string $path Relative path to get the absolute path for.
428+ * @param string $base_dir Optional. Base directory to prepend. Defaults to WP_PLUGIN_DIR.
398429 *
399430 * @return string
400431 */
401- private function get_absolute_path ( $ path ) {
402- return WP_PLUGIN_DIR . '/ ' . $ path ;
432+ private function get_absolute_path ( $ path , $ base_dir = null ) {
433+ if ( null === $ base_dir ) {
434+ $ base_dir = WP_PLUGIN_DIR ;
435+ }
436+ return $ base_dir . '/ ' . $ path ;
403437 }
404438
405439 /**
@@ -428,4 +462,128 @@ private function get_soft_change_files() {
428462 private function is_soft_change_file ( $ file ) {
429463 return in_array ( strtolower ( $ file ), $ this ->get_soft_change_files (), true );
430464 }
465+
466+ /**
467+ * Extracts the plugin slug from the plugin file path.
468+ *
469+ * For MU plugins that are actually standard plugins moved to mu-plugins folder,
470+ * we extract the plugin slug from the file path to look up checksums.
471+ *
472+ * @param string $plugin_file Path to the plugin file.
473+ *
474+ * @return string Plugin slug.
475+ */
476+ private function get_plugin_slug_from_path ( $ plugin_file ) {
477+ // If it's in a subdirectory, use the directory name as slug.
478+ if ( false !== strpos ( $ plugin_file , '/ ' ) ) {
479+ return dirname ( $ plugin_file );
480+ }
481+
482+ // For single files, extract the slug from the filename.
483+ return basename ( $ plugin_file , '.php ' );
484+ }
485+
486+ /**
487+ * Gets the version for a plugin from its header data or the version argument.
488+ *
489+ * @param string $version_arg Version argument from command line.
490+ * @param array $plugin_data Plugin header data.
491+ *
492+ * @return string|false Plugin version, or false if not found.
493+ */
494+ private function get_plugin_version_for_verification ( $ version_arg , $ plugin_data ) {
495+ if ( ! empty ( $ version_arg ) ) {
496+ return $ version_arg ;
497+ }
498+
499+ if ( ! empty ( $ plugin_data ['Version ' ] ) ) {
500+ return $ plugin_data ['Version ' ];
501+ }
502+
503+ return false ;
504+ }
505+
506+ /**
507+ * Verifies a must-use plugin against WordPress.org checksums.
508+ *
509+ * @param string $mu_file Path to the MU plugin file.
510+ * @param array $mu_plugin Plugin header data.
511+ * @param string $plugin_name Plugin slug/name.
512+ * @param string $version_arg Version to verify against (if specified).
513+ * @param bool $insecure Whether to allow insecure connections.
514+ * @param bool $strict Whether to check soft change files.
515+ * @param int &$skips Reference to skip counter.
516+ */
517+ private function verify_mu_plugin ( $ mu_file , $ mu_plugin , $ plugin_name , $ version_arg , $ insecure , $ strict , &$ skips ) {
518+ $ is_single_file = false === strpos ( $ mu_file , '/ ' );
519+
520+ // Get version from the plugin header.
521+ $ version = $ this ->get_plugin_version_for_verification ( $ version_arg , $ mu_plugin );
522+
523+ if ( false === $ version ) {
524+ WP_CLI ::warning ( "Could not retrieve the version for must-use plugin {$ plugin_name }, skipping. " );
525+ ++$ skips ;
526+ return ;
527+ }
528+
529+ $ wp_org_api = new WpOrgApi ( [ 'insecure ' => $ insecure ] );
530+
531+ try {
532+ /**
533+ * @var array|false $checksums
534+ */
535+ $ checksums = $ wp_org_api ->get_plugin_checksums ( $ plugin_name , $ version );
536+ if ( false === $ checksums ) {
537+ throw new Exception ( "Could not retrieve the checksums for version {$ version } of must-use plugin {$ plugin_name }, skipping. " );
538+ }
539+ } catch ( Exception $ exception ) {
540+ // If it's a single file or we can't get checksums, warn the user.
541+ if ( $ is_single_file ) {
542+ WP_CLI ::warning ( "Must-use plugin ' {$ mu_file }' appears to be a custom file or loader plugin and cannot be verified. " );
543+ } else {
544+ WP_CLI ::warning ( $ exception ->getMessage () );
545+ }
546+ ++$ skips ;
547+ return ;
548+ }
549+
550+ $ files = $ this ->get_mu_plugin_files ( $ mu_file );
551+
552+ foreach ( $ files as $ file ) {
553+ if ( ! array_key_exists ( $ file , $ checksums ) ) {
554+ $ this ->add_error ( $ plugin_name , $ file , 'File was added ' );
555+ continue ;
556+ }
557+
558+ if ( ! $ strict && $ this ->is_soft_change_file ( $ file ) ) {
559+ continue ;
560+ }
561+
562+ // Build the relative path for MU plugins.
563+ $ relative_path = $ is_single_file ? $ file : dirname ( $ mu_file ) . '/ ' . $ file ;
564+
565+ $ result = $ this ->check_file_checksum ( $ relative_path , $ checksums [ $ file ], WPMU_PLUGIN_DIR );
566+ if ( true !== $ result ) {
567+ $ this ->add_error ( $ plugin_name , $ file , is_string ( $ result ) ? $ result : 'Checksum does not match ' );
568+ }
569+ }
570+ }
571+
572+ /**
573+ * Gets the list of files that are part of the given MU plugin.
574+ *
575+ * @param string $mu_file Path to the main MU plugin file.
576+ *
577+ * @return array<string> Array of files with their relative paths.
578+ */
579+ private function get_mu_plugin_files ( $ mu_file ) {
580+ // If it's a single file in the root of mu-plugins, return just that file.
581+ if ( false === strpos ( $ mu_file , '/ ' ) ) {
582+ return array ( $ mu_file );
583+ }
584+
585+ // If it's in a subdirectory, get all files in that directory.
586+ $ folder = WPMU_PLUGIN_DIR . '/ ' . dirname ( $ mu_file );
587+ return $ this ->get_files ( trailingslashit ( $ folder ) );
588+ }
431589}
0 commit comments