Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions features/checksum-plugin.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
73 changes: 71 additions & 2 deletions src/Checksum_Plugin_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string> 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 );
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/WP_CLI/Fetchers/UnfilteredPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ||
Expand All @@ -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;
}
}
Loading