Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6d5287d
Initial plan
Copilot Nov 1, 2025
b255e2a
Implement plugin directory scanning for checksums verification
Copilot Nov 1, 2025
e567bbd
Add security hardening for file operations
Copilot Nov 1, 2025
f1be4c5
Address additional code review feedback
Copilot Nov 1, 2025
9a4be37
Fix code style issues: remove empty elseif and redundant is_array check
Copilot Nov 1, 2025
97a5b26
Use get_file_data() for version detection and remove readme.txt scanning
Copilot Dec 12, 2025
2545381
Remove unnecessary file size check as get_file_data() reads only 8KB
Copilot Dec 12, 2025
51b669e
Merge branch 'main' into copilot/fix-plugin-verification-checks
swissspidy Dec 19, 2025
40f9fbc
Include renamed PHP files in version detection to fix test failure
Copilot Dec 19, 2025
890b94b
Revert to only scanning .php files and update test to pass version ex…
Copilot Dec 19, 2025
e3a4e71
Merge branch 'main' into copilot/fix-plugin-verification-checks
swissspidy Feb 3, 2026
e38a9b6
Update src/WP_CLI/Fetchers/UnfilteredPlugin.php
swissspidy Feb 3, 2026
353e4af
Merge branch 'main' into copilot/fix-plugin-verification-checks
swissspidy Feb 15, 2026
762e4ed
Apply suggestion from @swissspidy
swissspidy Mar 12, 2026
f264c89
Update src/Checksum_Plugin_Command.php
swissspidy Mar 12, 2026
d1d2931
Merge branch 'main' into copilot/fix-plugin-verification-checks
swissspidy Mar 12, 2026
bcbf764
Merge branch 'main' into copilot/fix-plugin-verification-checks
swissspidy Mar 22, 2026
062dd4b
Merge branch 'main' into copilot/fix-plugin-verification-checks
swissspidy May 19, 2026
247b4e4
Replace detect_version_from_directory with WP.org API version fallback
Copilot May 27, 2026
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
"""
Comment thread
swissspidy marked this conversation as resolved.
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 ) {
Comment thread
swissspidy marked this conversation as resolved.
Outdated
$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' );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
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;
}
}
}
}
Comment thread
swissspidy marked this conversation as resolved.

return array_unique( $names );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
}

/**
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 ) ) {
Comment thread
swissspidy marked this conversation as resolved.
Outdated
// 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