Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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 @@ -183,6 +183,40 @@ 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 on lines +210 to +218
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test scenario for verifying a plugin directory with --all flag (lines 232-240) doesn't specify whether version detection succeeds or fails. If the version cannot be auto-detected from the renamed file, the plugin would be skipped with a "Could not retrieve the version" warning, not verified. The test should either verify that version detection succeeds from other PHP files in the directory, or it should expect the version warning and skip message. Consider adding explicit assertions about the version detection outcome.

Copilot uses AI. Check for mistakes.

Scenario: Verify must-use plugin that is a standard plugin moved to mu-plugins
Given a WP install

Expand Down
69 changes: 68 additions & 1 deletion src/Checksum_Plugin_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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 @@ -251,23 +257,84 @@ 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' );
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glob pattern does not escape special glob characters in the plugin directory path. If a plugin directory contains characters like *, ?, [, or ], the glob function could match unintended files or fail unexpectedly. Consider using glob() with the GLOB_BRACE flag or pre-escaping the path with a function that escapes glob metacharacters, or use an alternative approach like scandir() followed by filtering.

Copilot uses AI. Check for mistakes.
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;
}
Comment thread
swissspidy marked this conversation as resolved.

/**
* 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 );
}

// Also scan the filesystem for plugin directories
$plugin_dir = WP_PLUGIN_DIR;
if ( is_dir( $plugin_dir ) && is_readable( $plugin_dir ) ) {
try {
foreach ( new DirectoryIterator( $plugin_dir ) as $fileinfo ) {
if ( $fileinfo->isDot() || ! $fileinfo->isDir() || $fileinfo->isLink() ) {
continue;
}
$dir = $fileinfo->getFilename();
if ( ! in_array( $dir, $names, true ) ) {
$names[] = $dir;
}
}
} catch ( UnexpectedValueException $e ) {
WP_CLI::warning( "Could not scan plugin directory '{$plugin_dir}': " . $e->getMessage() );
}
}
Comment thread
swissspidy marked this conversation as resolved.

return $names;
}

Expand Down
22 changes: 22 additions & 0 deletions src/WP_CLI/Fetchers/UnfilteredPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class UnfilteredPlugin extends Base {
public function get( $name ) {
$name = (string) $name;

// First, check plugins detected by get_plugins()
foreach ( get_plugins() as $file => $_ ) {
if ( "{$name}.php" === $file ||
( $name && $file === $name ) ||
Expand All @@ -36,6 +37,27 @@ 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;

// Resolve real paths to protect against path traversal and symlinks.
$wp_plugin_dir_real = realpath( WP_PLUGIN_DIR );
$plugin_dir_real = realpath( $plugin_dir );

if ( false !== $wp_plugin_dir_real
&& false !== $plugin_dir_real
&& is_dir( $plugin_dir_real )
&& ! is_link( $plugin_dir_real )
&& ( $plugin_dir_real === $wp_plugin_dir_real
|| 0 === strpos( $plugin_dir_real, $wp_plugin_dir_real . DIRECTORY_SEPARATOR ) )
) {
// 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