diff --git a/features/checksum-plugin.feature b/features/checksum-plugin.feature index ea6cb19e..41776542 100644 --- a/features/checksum-plugin.feature +++ b/features/checksum-plugin.feature @@ -204,3 +204,37 @@ Feature: Validate checksums for WordPress plugins """ Verified 1 of 1 plugins. """ + + Scenario: Verifies plugin directory when main file is missing + Given a WP install + + When I run `wp plugin install duplicate-post --version=3.2.1` + Then STDOUT should not be empty + And STDERR should be empty + + When I run `mv wp-content/plugins/duplicate-post/duplicate-post.php wp-content/plugins/duplicate-post/duplicate-post.php.renamed` + Then STDERR should be empty + + When I try `wp plugin verify-checksums duplicate-post --version=3.2.1 --format=json` + Then STDOUT should contain: + """ + "plugin_name":"duplicate-post","file":"duplicate-post.php.renamed","message":"File was added" + """ + And STDERR should contain: + """ + Warning: Plugin duplicate-post main file is missing: duplicate-post/duplicate-post.php + """ + And STDERR should contain: + """ + Error: No plugins verified (1 failed). + """ + + When I try `wp plugin verify-checksums --all --format=json` + Then STDOUT should contain: + """ + "plugin_name":"duplicate-post" + """ + And STDERR should contain: + """ + Warning: Plugin duplicate-post main file is missing: duplicate-post/duplicate-post.php + """ diff --git a/src/Checksum_Plugin_Command.php b/src/Checksum_Plugin_Command.php index 64509d42..dcb46af4 100644 --- a/src/Checksum_Plugin_Command.php +++ b/src/Checksum_Plugin_Command.php @@ -107,6 +107,12 @@ public function __invoke( $args, $assoc_args ) { continue; } + // Check if the main plugin file exists + $main_file_path = WP_PLUGIN_DIR . '/' . $plugin->file; + if ( ! file_exists( $main_file_path ) ) { + WP_CLI::warning( "Plugin {$plugin->name} main file is missing: {$plugin->file}" ); + } + if ( false === $version ) { WP_CLI::warning( "Could not retrieve the version for plugin {$plugin->name}, skipping." ); ++$skips; @@ -222,24 +228,87 @@ private function get_plugin_version( $path ) { } if ( ! array_key_exists( $path, $this->plugins_data ) ) { - return false; + // Try to detect version from any PHP file in the plugin directory + return $this->detect_version_from_directory( dirname( $path ) ); } return $this->plugins_data[ $path ]['Version']; } + /** + * Attempts to detect plugin version from any PHP file in the plugin directory. + * + * This is used as a fallback when the main plugin file is missing or has no valid headers. + * + * @param string $plugin_dir Plugin directory name (relative to WP_PLUGIN_DIR). + * + * @return string|false Detected version, or false if not found. + */ + private function detect_version_from_directory( $plugin_dir ) { + $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_dir; + + // If it's not a directory (single-file plugin), we can't detect version + if ( ! is_dir( $plugin_path ) ) { + return false; + } + + // Try scanning PHP files for Version header using WordPress's get_file_data() + $files = glob( $plugin_path . '/*.php' ); + if ( is_array( $files ) && ! empty( $files ) ) { + foreach ( $files as $file ) { + if ( is_readable( $file ) ) { + $file_data = get_file_data( + $file, + array( 'Version' => 'Version' ) + ); + if ( ! empty( $file_data['Version'] ) ) { + return $file_data['Version']; + } + } + } + } + // If glob() failed (returns false), version will just not be detected from PHP files + + return false; + } + /** * Gets the names of all installed plugins. * + * Includes both plugins detected by get_plugins() and plugin directories + * that exist on the filesystem but may not have valid headers. + * * @return array Names of all installed plugins. */ private function get_all_plugin_names() { $names = array(); + + // Get plugins from get_plugins() (those with valid headers) foreach ( get_plugins() as $file => $details ) { $names[] = Utils\get_plugin_name( $file ); } - return $names; + // Also scan the filesystem for plugin directories + $plugin_dir = WP_PLUGIN_DIR; + if ( is_dir( $plugin_dir ) && is_readable( $plugin_dir ) ) { + $dirs = @scandir( $plugin_dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( false !== $dirs ) { + foreach ( $dirs as $dir ) { + // Skip special directories and files + if ( '.' === $dir || '..' === $dir ) { + continue; + } + + $full_path = $plugin_dir . '/' . $dir; + // Only include real directories, not symlinks or files + if ( is_dir( $full_path ) && ! is_link( $full_path ) && ! in_array( $dir, $names, true ) ) { + $names[] = $dir; + } + } + } + } + + return array_unique( $names ); } /** diff --git a/src/WP_CLI/Fetchers/UnfilteredPlugin.php b/src/WP_CLI/Fetchers/UnfilteredPlugin.php index 2cac264e..83c18d38 100644 --- a/src/WP_CLI/Fetchers/UnfilteredPlugin.php +++ b/src/WP_CLI/Fetchers/UnfilteredPlugin.php @@ -25,6 +25,7 @@ class UnfilteredPlugin extends Base { * @return object|false */ public function get( $name ) { + // First, check plugins detected by get_plugins() foreach ( get_plugins() as $file => $_ ) { if ( "{$name}.php" === $file || ( $name && $file === $name ) || @@ -33,6 +34,16 @@ public function get( $name ) { } } + // If not found, check if a directory with this name exists + // This handles cases where the main plugin file is missing + $plugin_dir = WP_PLUGIN_DIR . '/' . $name; + if ( is_dir( $plugin_dir ) ) { + // Use the conventional main file name, even if it doesn't exist + // The checksum verification will handle missing files appropriately + $file = $name . '/' . $name . '.php'; + return (object) compact( 'name', 'file' ); + } + return false; } }