Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41efc3a
Initial plan
Copilot Nov 7, 2025
baee2fa
Add support for cleaning up old files using WordPress $_old_files list
Copilot Nov 7, 2025
37cfece
Enhance cleanup to use $_old_files even when checksums are available
Copilot Nov 7, 2025
4760835
Update tests and documentation to reflect new cleanup behavior
Copilot Nov 7, 2025
5a094a0
Address code review feedback - improve error handling and reduce dupl…
Copilot Nov 7, 2025
bf5a413
Remove @ error suppression operators per code review feedback
Copilot Nov 7, 2025
fe5a714
Use directory_separator
swissspidy Nov 7, 2025
12fd04b
Path validation
swissspidy Nov 7, 2025
d5ffb6d
Adjust message
swissspidy Nov 7, 2025
96fcab2
Refactor to reduce code duplication and add symlink handling
Copilot Nov 7, 2025
038d662
Add path validation and improve security checks
Copilot Nov 7, 2025
92f2049
Fix path validation logic for better security
Copilot Nov 7, 2025
0d1bfb3
Optimize path validation by caching ABSPATH realpath
Copilot Nov 7, 2025
6b30e73
Clean up code style - remove trailing whitespace and simplify logic
Copilot Nov 7, 2025
831e197
Lint & test fixes
swissspidy Nov 10, 2025
1a5cb90
Merge branch 'main' into copilot/add-delete-old-core-files
swissspidy Dec 7, 2025
0064498
Add comprehensive test coverage for old files cleanup functionality
Copilot Dec 7, 2025
253a873
Apply suggestions from code review
swissspidy Dec 7, 2025
40c7fc5
Remove custom old_files test and improve checksums unavailable test w…
Copilot Dec 8, 2025
8ab689b
Gherkin lint fix
swissspidy Dec 8, 2025
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
2 changes: 1 addition & 1 deletion features/core-download.feature
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Feature: Download WordPress
"""
Failed to find WordPress version
"""
And STDERR should contain:
And STDERR should not contain:
"""
Warning: Checksums not available for WordPress nightly/en_US. Please cleanup files manually.
"""
Expand Down
89 changes: 89 additions & 0 deletions features/core-update.feature
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,92 @@ Feature: Update WordPress core
"""
Success:
"""

@require-php-7.2
Scenario: Old files from $_old_files are cleaned up when upgrading
Given a WP install

When I run `wp core download --version=6.8 --force`
Then STDOUT should contain:
"""
Success: WordPress downloaded.
"""

# Create files that should be removed according to 6.9 old_files list
Given a wp-includes/blocks/post-author/editor.css file:
"""
/* Old CSS file */
"""
And a wp-includes/blocks/post-author/editor.min.css file:
"""
/* Old minified CSS */
"""
And a wp-includes/blocks/post-author/editor-rtl.css file:
"""
/* Old RTL CSS */
"""
And a wp-includes/blocks/post-author/editor-rtl.min.css file:
"""
/* Old RTL minified CSS */
"""
And a wp-includes/SimplePie/src/Core.php file:
"""
<?php
// Old SimplePie Core file
"""
And an empty wp-includes/SimplePie/src/Decode directory

When I run `wp core update --version=6.9 --force`
Then STDOUT should contain:
"""
Success: WordPress updated successfully.
"""
And the wp-includes/blocks/post-author/editor.css file should not exist
And the wp-includes/blocks/post-author/editor.min.css file should not exist
And the wp-includes/blocks/post-author/editor-rtl.css file should not exist
And the wp-includes/blocks/post-author/editor-rtl.min.css file should not exist
And the wp-includes/SimplePie/src/Core.php file should not exist
And the wp-includes/SimplePie/src/Decode directory should not exist

@require-php-7.2
Scenario: Old files cleanup works when checksums unavailable
Given a WP install

When I run `wp core download --version=6.8 --force`
Then STDOUT should contain:
"""
Success: WordPress downloaded.
"""

# Create files that exist in the $_old_files list from WordPress 6.9
Given a wp-includes/blocks/post-author/editor.css file:
"""
/* Old CSS file */
"""
And a wp-includes/blocks/post-author/editor.min.css file:
"""
/* Old minified CSS */
"""

# Mock checksum API to return empty response so checksums are unavailable
And that HTTP requests to https://api.wordpress.org/core/checksums/1.0/ will respond with:
"""
HTTP/1.1 200
Content-Type: application/json

{}
"""

When I try `wp core update --version=6.9 --force`
Then STDOUT should contain:
"""
Cleaning up files...
"""
And STDOUT should contain:
"""
Success: WordPress updated successfully.
"""

# Verify files from $_old_files were removed
And the wp-includes/blocks/post-author/editor.css file should not exist
And the wp-includes/blocks/post-author/editor.min.css file should not exist
224 changes: 216 additions & 8 deletions src/Core_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
* Updating to version 3.1 (en_US)...
* Downloading update from https://wordpress.org/wordpress-3.1.zip...
* Unpacking the update...
* Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually.
* Cleaning up files...
* Success: WordPress updated successfully.
*
* @alias upgrade
Expand Down Expand Up @@ -1511,15 +1511,13 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
}

$old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure );
if ( ! is_array( $old_checksums ) ) {
WP_CLI::warning( "{$old_checksums} Please cleanup files manually." );
return;
}

$new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure );
if ( ! is_array( $new_checksums ) ) {
WP_CLI::warning( "{$new_checksums} Please cleanup files manually." );

$has_checksums = is_array( $old_checksums ) && is_array( $new_checksums );

if ( ! $has_checksums ) {
// When checksums are not available, use WordPress core's $_old_files list
$this->cleanup_old_files();
return;
}

Expand Down Expand Up @@ -1613,6 +1611,216 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
WP_CLI::log( 'No files found that need cleaning up.' );
}
}

// Additionally, clean up files from $_old_files that are not in checksums
// These should be deleted unconditionally as they are known old files
$this->cleanup_old_files_not_in_checksums( $old_checksums, $new_checksums );
}

/**
* Clean up old files using WordPress core's $_old_files list.
*
* This method is used when checksums are not available for version comparison.
* It unconditionally deletes files from the $_old_files global array maintained by WordPress core.
*/
private function cleanup_old_files() {
$old_files = $this->get_old_files_list();
if ( empty( $old_files ) ) {
WP_CLI::log( 'No files found that need cleaning up.' );
return;
}

WP_CLI::log( 'Cleaning up files...' );

$count = $this->remove_old_files_from_list( $old_files );

if ( $count ) {
WP_CLI::log( number_format( $count ) . ' files cleaned up.' );
} else {
WP_CLI::log( 'No old files were removed.' );
}
}

/**
* Clean up old files from $_old_files that are not tracked in checksums.
*
* This method is used as a supplement when checksums ARE available.
* It unconditionally deletes files from $_old_files that are not present in either
* the old or new checksums, as these files cannot be verified for modifications.
*
* @param array $old_checksums Old checksums array.
* @param array $new_checksums New checksums array.
*/
private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checksums ) {
$old_files = $this->get_old_files_list();
if ( empty( $old_files ) ) {
return;
}

// Combine all files from both checksum arrays
$all_checksum_files = array_merge( array_keys( $old_checksums ), array_keys( $new_checksums ) );
$all_checksum_files = array_unique( $all_checksum_files );

// Find files in $_old_files that are not in checksums
$files_to_remove = array_diff( $old_files, $all_checksum_files );

if ( empty( $files_to_remove ) ) {
return;
}

$count = $this->remove_old_files_from_list( $files_to_remove );

if ( $count ) {
WP_CLI::log( number_format( $count ) . ' additional old files cleaned up.' );
}
}

/**
* Get the list of old files from WordPress core.
*
* @return array Array of old file paths, or empty array if not available.
*/
private function get_old_files_list() {
// Include WordPress core's update file to access the $_old_files list
if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) {
WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' );
return array();
}

require_once ABSPATH . 'wp-admin/includes/update-core.php';

global $_old_files;

if ( empty( $_old_files ) || ! is_array( $_old_files ) ) {
return array();
}

return $_old_files;
}

/**
* Remove old files from a list.
*
* This is a shared helper method that handles the actual removal of files and directories.
*
* @param array $files Array of file paths to remove.
* @return int Number of files/directories successfully removed.
*/
private function remove_old_files_from_list( $files ) {
$count = 0;

// Cache ABSPATH realpath for performance
$abspath_realpath = realpath( ABSPATH );
if ( false === $abspath_realpath ) {
WP_CLI::debug( 'Failed to resolve ABSPATH realpath', 'core' );
return $count;
}
$abspath_realpath_trailing = Utils\trailingslashit( $abspath_realpath );

foreach ( $files as $file ) {
// wp-content should be considered user data
if ( 0 === stripos( $file, 'wp-content' ) ) {
continue;
}

$file_path = ABSPATH . $file;

// Validate the path is within ABSPATH
$file_realpath = realpath( $file_path );
if ( false === $file_realpath ) {
// Skip files with invalid paths
WP_CLI::debug( "Skipping file with invalid path: {$file}", 'core' );
continue;
}

if ( 0 !== strpos( $file_realpath, $abspath_realpath_trailing ) ) {
WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' );
continue;
}

// Handle both files and directories
if ( file_exists( $file_path ) ) {
if ( is_dir( $file_path ) ) {
// Remove directory recursively
if ( $this->remove_directory( $file_path ) ) {
WP_CLI::log( "Directory removed: {$file}" );
++$count;
} else {
WP_CLI::debug( "Failed to remove directory: {$file}", 'core' );
}
} elseif ( unlink( $file_path ) ) {
WP_CLI::log( "File removed: {$file}" );
++$count;
} else {
WP_CLI::debug( "Failed to remove file: {$file}", 'core' );
}
}
}

return $count;
}

/**
* Recursively remove a directory and its contents.
*
* @param string $dir Directory path to remove.
* @return bool True on success, false on failure.
*/
private function remove_directory( $dir ) {
$dir_realpath = realpath( $dir );
$abspath_realpath = realpath( ABSPATH );
if ( false === $dir_realpath || false === $abspath_realpath ) {
WP_CLI::debug( "Failed to resolve realpath for directory or ABSPATH: {$dir}", 'core' );
return false;
}
// Normalize paths with trailing slashes for accurate comparison
if ( 0 !== strpos( $dir_realpath, Utils\trailingslashit( $abspath_realpath ) ) ) {
WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' );
return false;
}
if ( ! is_dir( $dir ) ) {
return false;
}

$items = scandir( $dir );
if ( false === $items ) {
WP_CLI::debug( "Failed to scan directory: {$dir}", 'core' );
return false;
}

foreach ( $items as $item ) {
if ( '.' === $item || '..' === $item ) {
continue;
}

$path = $dir . DIRECTORY_SEPARATOR . $item;

// Handle symbolic links before checking if it's a directory
if ( is_link( $path ) ) {
if ( ! unlink( $path ) ) {
WP_CLI::debug( "Failed to remove symbolic link: {$path}", 'core' );
return false;
}
continue;
}

if ( is_dir( $path ) ) {
if ( ! $this->remove_directory( $path ) ) {
WP_CLI::debug( "Failed to remove subdirectory: {$path}", 'core' );
return false;
}
} elseif ( ! unlink( $path ) ) {
WP_CLI::debug( "Failed to remove file in directory: {$path}", 'core' );
return false;
}
}

if ( ! rmdir( $dir ) ) {
WP_CLI::debug( "Failed to remove directory: {$dir}", 'core' );
return false;
}

return true;
}

private static function strip_content_dir( $zip_file ) {
Expand Down