diff --git a/phpcs.xml.dist b/phpcs.xml.dist index d0cb7801..f723979b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -59,7 +59,12 @@ */src/WP_CLI/Fetchers/(Plugin|Theme)\.php$ */src/WP_CLI/CommandWithUpgrade\.php$ - */src/WP_CLI/(CommandWith|DestructivePlugin|DestructiveTheme)Upgrader\.php$ + */src/WP_CLI/DestructivePluginUpgrader\.php$ + */src/WP_CLI/DestructiveThemeUpgrader\.php$ + */src/WP_CLI/ValidatingPluginUpgrader\.php$ + */src/WP_CLI/ValidatingThemeUpgrader\.php$ + */src/WP_CLI/PackageValidator\.php$ + */src/WP_CLI/UpgraderWithValidation\.php$ */src/WP_CLI/Parse(Plugin|Theme)NameInput\.php$ diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index ef5b061f..2606aa1c 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -79,7 +79,7 @@ public function __construct() { } protected function get_upgrader_class( $force ) { - return $force ? '\\WP_CLI\\DestructivePluginUpgrader' : 'Plugin_Upgrader'; + return $force ? '\\WP_CLI\\DestructivePluginUpgrader' : '\\WP_CLI\\ValidatingPluginUpgrader'; } /** diff --git a/src/Theme_Command.php b/src/Theme_Command.php index 5ae8459f..5b17f9fb 100644 --- a/src/Theme_Command.php +++ b/src/Theme_Command.php @@ -75,7 +75,7 @@ public function __construct() { } protected function get_upgrader_class( $force ) { - return $force ? '\\WP_CLI\\DestructiveThemeUpgrader' : 'Theme_Upgrader'; + return $force ? '\\WP_CLI\\DestructiveThemeUpgrader' : '\\WP_CLI\\ValidatingThemeUpgrader'; } /** diff --git a/src/WP_CLI/DestructivePluginUpgrader.php b/src/WP_CLI/DestructivePluginUpgrader.php index 5b3e1774..b1df4a39 100644 --- a/src/WP_CLI/DestructivePluginUpgrader.php +++ b/src/WP_CLI/DestructivePluginUpgrader.php @@ -6,6 +6,7 @@ * A plugin upgrader class that clears the destination directory. */ class DestructivePluginUpgrader extends \Plugin_Upgrader { + use UpgraderWithValidation; public function install_package( $args = array() ) { parent::upgrade_strings(); // Needed for the 'remove_old' string. diff --git a/src/WP_CLI/DestructiveThemeUpgrader.php b/src/WP_CLI/DestructiveThemeUpgrader.php index 699d6dc0..9f2e318d 100644 --- a/src/WP_CLI/DestructiveThemeUpgrader.php +++ b/src/WP_CLI/DestructiveThemeUpgrader.php @@ -6,6 +6,7 @@ * A theme upgrader class that clears the destination directory. */ class DestructiveThemeUpgrader extends \Theme_Upgrader { + use UpgraderWithValidation; public function install_package( $args = array() ) { parent::upgrade_strings(); // Needed for the 'remove_old' string. diff --git a/src/WP_CLI/PackageValidator.php b/src/WP_CLI/PackageValidator.php new file mode 100644 index 00000000..847243fa --- /dev/null +++ b/src/WP_CLI/PackageValidator.php @@ -0,0 +1,143 @@ + ' . escapeshellarg( $null_device ) . ' 2>&1', + false, + true + ); + $is_available = ( 0 === $result->return_code ); + } + + return $is_available; + } + + /** + * Validates zip file integrity using the 'unzip -t' command. + * + * @param string $file_path Path to the zip file. + * @return true|\WP_Error True if valid, WP_Error if validation fails. + */ + private static function validate_with_unzip( $file_path ) { + // Suppress output - use platform-appropriate null device. + // Note: Null device path is a hardcoded constant, safe to use in shell commands. + $null_device = '\\' === DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'; + $command = 'unzip -t ' . escapeshellarg( $file_path ) . ' > ' . escapeshellarg( $null_device ) . ' 2>&1'; + + $result = WP_CLI::launch( + $command, + false, + true + ); + + if ( 0 !== $result->return_code ) { + return new \WP_Error( + 'package_corrupted', + 'Package file failed zip integrity check. This usually indicates a corrupted or incomplete download.' + ); + } + + return true; + } + + /** + * Deletes a corrupted package file. + * + * @param string $file_path Path to the file to delete. + * @return bool True if file was deleted or didn't exist, false on failure. + */ + public static function delete_corrupted_file( $file_path ) { + if ( ! file_exists( $file_path ) ) { + return true; + } + + $result = unlink( $file_path ); + + // Log if deletion failed, but don't throw an error. + if ( ! $result ) { + WP_CLI::debug( + sprintf( 'Failed to delete corrupted file: %s', $file_path ), + 'extension-command' + ); + } + + return $result; + } +} diff --git a/src/WP_CLI/UpgraderWithValidation.php b/src/WP_CLI/UpgraderWithValidation.php new file mode 100644 index 00000000..65cb9578 --- /dev/null +++ b/src/WP_CLI/UpgraderWithValidation.php @@ -0,0 +1,70 @@ +get_error_message() + ), + 'extension-command' + ); + + // Delete the corrupted file to prevent it from being reused. + if ( PackageValidator::delete_corrupted_file( $download ) ) { + WP_CLI::debug( + 'Deleted corrupted package file from cache.', + 'extension-command' + ); + } + + // Return a detailed error message. + return new \WP_Error( + 'package_validation_failed', + sprintf( + 'Downloaded package failed validation (%s). The corrupted file has been removed from cache. Please try the command again.', + $validation->get_error_message() + ) + ); + } +} diff --git a/src/WP_CLI/ValidatingPluginUpgrader.php b/src/WP_CLI/ValidatingPluginUpgrader.php new file mode 100644 index 00000000..dd52c248 --- /dev/null +++ b/src/WP_CLI/ValidatingPluginUpgrader.php @@ -0,0 +1,13 @@ +