Skip to content

Commit 6877861

Browse files
CopilotswissspidyCopilot
authored
Add automatic cleanup of old core files using WordPress $_old_files list (#299)
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascal.birchler@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascalb@google.com>
1 parent 7589212 commit 6877861

File tree

3 files changed

+277
-11
lines changed

3 files changed

+277
-11
lines changed

features/core-download.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Feature: Download WordPress
180180
"""
181181
Failed to find WordPress version
182182
"""
183-
And STDERR should contain:
183+
And STDERR should not contain:
184184
"""
185185
Warning: Checksums not available for WordPress nightly/en_US. Please cleanup files manually.
186186
"""

features/core-update.feature

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Feature: Update WordPress core
2121
"""
2222
Starting update...
2323
Unpacking the update...
24+
Cleaning up files...
25+
No old files were removed.
2426
Success: WordPress updated successfully.
2527
"""
2628

@@ -420,6 +422,90 @@ Feature: Update WordPress core
420422
</div>
421423
"""
422424

425+
Scenario: Old files from $_old_files are cleaned up when upgrading
426+
Given a WP install
427+
428+
When I run `wp core download --version=6.8 --force`
429+
430+
# Create files that should be removed according to 6.9 old_files list
431+
Given a wp-includes/blocks/post-author/editor.css file:
432+
"""
433+
/* Old CSS file */
434+
"""
435+
And a wp-includes/blocks/post-author/editor.min.css file:
436+
"""
437+
/* Old minified CSS */
438+
"""
439+
And a wp-includes/blocks/post-author/editor-rtl.css file:
440+
"""
441+
/* Old RTL CSS */
442+
"""
443+
And a wp-includes/blocks/post-author/editor-rtl.min.css file:
444+
"""
445+
/* Old RTL minified CSS */
446+
"""
447+
And a wp-includes/SimplePie/src/Core.php file:
448+
"""
449+
<?php
450+
// Old SimplePie Core file
451+
"""
452+
And an empty wp-includes/SimplePie/src/Decode directory
453+
454+
When I run `wp core update --version=6.9 --force`
455+
Then STDOUT should contain:
456+
"""
457+
Success: WordPress updated successfully.
458+
"""
459+
And the wp-includes/blocks/post-author/editor.css file should not exist
460+
And the wp-includes/blocks/post-author/editor.min.css file should not exist
461+
And the wp-includes/blocks/post-author/editor-rtl.css file should not exist
462+
And the wp-includes/blocks/post-author/editor-rtl.min.css file should not exist
463+
And the wp-includes/SimplePie/src/Core.php file should not exist
464+
And the wp-includes/SimplePie/src/Decode directory should not exist
465+
466+
@require-php-7.2
467+
Scenario: Old files cleanup works when checksums unavailable
468+
Given a WP install
469+
470+
When I run `wp core download --version=6.8 --force`
471+
Then STDOUT should contain:
472+
"""
473+
Success: WordPress downloaded.
474+
"""
475+
476+
# Create files that exist in the $_old_files list from WordPress 6.9
477+
Given a wp-includes/blocks/post-author/editor.css file:
478+
"""
479+
/* Old CSS file */
480+
"""
481+
And a wp-includes/blocks/post-author/editor.min.css file:
482+
"""
483+
/* Old minified CSS */
484+
"""
485+
486+
# Mock checksum API to return empty response so checksums are unavailable
487+
And that HTTP requests to https://api.wordpress.org/core/checksums/1.0/ will respond with:
488+
"""
489+
HTTP/1.1 200
490+
Content-Type: application/json
491+
492+
{}
493+
"""
494+
495+
When I try `wp core update --version=6.9 --force`
496+
Then STDOUT should contain:
497+
"""
498+
Cleaning up files...
499+
"""
500+
And STDOUT should contain:
501+
"""
502+
Success: WordPress updated successfully.
503+
"""
504+
505+
# Verify files from $_old_files were removed
506+
And the wp-includes/blocks/post-author/editor.css file should not exist
507+
And the wp-includes/blocks/post-author/editor.min.css file should not exist
508+
423509
Scenario: Update WordPress locale without --force when version is the same
424510
Given a WP install
425511
And an empty cache
@@ -485,7 +571,7 @@ Feature: Update WordPress core
485571
"""
486572
Package language: en_US
487573
"""
488-
@require-php-7.0 @require-wp-6.1
574+
@require-wp-6.1
489575
Scenario: Attempting to downgrade without --force shows helpful message
490576
Given a WP install
491577

src/Core_Command.php

Lines changed: 189 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
11231123
* Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip...
11241124
* Unpacking the update...
11251125
* Cleaning up files...
1126-
* No files found that need cleaning up
1126+
* No old files were removed.
11271127
* Success: WordPress updated successfully.
11281128
*
11291129
* # Update WordPress using zip file.
@@ -1137,7 +1137,9 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
11371137
* Updating to version 3.1 (en_US)...
11381138
* Downloading update from https://wordpress.org/wordpress-3.1.zip...
11391139
* Unpacking the update...
1140-
* Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually.
1140+
* Cleaning up files...
1141+
* No old files were removed.
1142+
* Warning: Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up.
11411143
* Success: WordPress updated successfully.
11421144
*
11431145
* @alias upgrade
@@ -1794,16 +1796,16 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
17941796
return;
17951797
}
17961798

1797-
$old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure );
1798-
if ( ! is_array( $old_checksums ) ) {
1799-
WP_CLI::warning( "{$old_checksums} Please cleanup files manually." );
1800-
return;
1801-
}
1799+
// Always clean up files from WordPress core's $_old_files list first
1800+
$this->cleanup_old_files();
18021801

1802+
$old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure );
18031803
$new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure );
1804-
if ( ! is_array( $new_checksums ) ) {
1805-
WP_CLI::warning( "{$new_checksums} Please cleanup files manually." );
18061804

1805+
$has_checksums = is_array( $old_checksums ) && is_array( $new_checksums );
1806+
1807+
if ( ! $has_checksums ) {
1808+
WP_CLI::warning( 'Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up.' );
18071809
return;
18081810
}
18091811

@@ -1899,6 +1901,184 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
18991901
}
19001902
}
19011903

1904+
/**
1905+
* Clean up old files using WordPress core's $_old_files list.
1906+
*
1907+
* It unconditionally deletes files from the $_old_files global array maintained by WordPress core.
1908+
*/
1909+
private function cleanup_old_files() {
1910+
$old_files = $this->get_old_files_list();
1911+
if ( empty( $old_files ) ) {
1912+
WP_CLI::log( 'No files found that need cleaning up.' );
1913+
return;
1914+
}
1915+
1916+
WP_CLI::log( 'Cleaning up files...' );
1917+
1918+
$count = $this->remove_old_files_from_list( $old_files );
1919+
1920+
if ( $count ) {
1921+
WP_CLI::log( number_format( $count ) . ' files cleaned up.' );
1922+
} else {
1923+
WP_CLI::log( 'No old files were removed.' );
1924+
}
1925+
}
1926+
1927+
/**
1928+
* Get the list of old files from WordPress core.
1929+
*
1930+
* @return array Array of old file paths, or empty array if not available.
1931+
*/
1932+
private function get_old_files_list() {
1933+
// Include WordPress core's update file to access the $_old_files list
1934+
if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) {
1935+
WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' );
1936+
return array();
1937+
}
1938+
1939+
require_once ABSPATH . 'wp-admin/includes/update-core.php';
1940+
1941+
global $_old_files;
1942+
1943+
if ( empty( $_old_files ) || ! is_array( $_old_files ) ) {
1944+
return array();
1945+
}
1946+
1947+
return $_old_files;
1948+
}
1949+
1950+
/**
1951+
* Remove old files from a list.
1952+
*
1953+
* This is a shared helper method that handles the actual removal of files and directories.
1954+
*
1955+
* @param array $files Array of file paths to remove.
1956+
* @return int Number of files/directories successfully removed.
1957+
*/
1958+
private function remove_old_files_from_list( $files ) {
1959+
$count = 0;
1960+
1961+
$abspath_realpath = realpath( ABSPATH );
1962+
if ( false === $abspath_realpath ) {
1963+
WP_CLI::debug( 'Failed to resolve ABSPATH realpath', 'core' );
1964+
return $count;
1965+
}
1966+
$abspath_realpath_trailing = Utils\trailingslashit( $abspath_realpath );
1967+
1968+
foreach ( $files as $file ) {
1969+
$file_path = ABSPATH . $file;
1970+
1971+
// Skip entries that don't exist and aren't (broken) symlinks.
1972+
if ( ! file_exists( $file_path ) && ! is_link( $file_path ) ) {
1973+
continue;
1974+
}
1975+
1976+
// Symlinks: validate and remove without following the link.
1977+
if ( is_link( $file_path ) ) {
1978+
$normalized_path = realpath( dirname( $file_path ) );
1979+
if ( false === $normalized_path
1980+
|| 0 !== strpos( Utils\trailingslashit( $normalized_path ), $abspath_realpath_trailing )
1981+
) {
1982+
WP_CLI::debug( "Skipping symbolic link outside of ABSPATH: {$file}", 'core' );
1983+
continue;
1984+
}
1985+
if ( unlink( $file_path ) ) {
1986+
WP_CLI::log( "Symbolic link removed: {$file}" );
1987+
++$count;
1988+
} else {
1989+
WP_CLI::debug( "Failed to remove symbolic link: {$file}", 'core' );
1990+
}
1991+
continue;
1992+
}
1993+
1994+
// Regular files/directories: validate real path is within ABSPATH.
1995+
$file_realpath = realpath( $file_path );
1996+
if ( false === $file_realpath || 0 !== strpos( Utils\trailingslashit( $file_realpath ), $abspath_realpath_trailing ) ) {
1997+
WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' );
1998+
continue;
1999+
}
2000+
2001+
if ( is_dir( $file_path ) ) {
2002+
if ( $this->remove_directory( $file_path, $abspath_realpath_trailing ) ) {
2003+
WP_CLI::log( "Directory removed: {$file}" );
2004+
++$count;
2005+
} else {
2006+
WP_CLI::debug( "Failed to remove directory: {$file}", 'core' );
2007+
}
2008+
} elseif ( unlink( $file_path ) ) {
2009+
WP_CLI::log( "File removed: {$file}" );
2010+
++$count;
2011+
} else {
2012+
WP_CLI::debug( "Failed to remove file: {$file}", 'core' );
2013+
}
2014+
}
2015+
2016+
return $count;
2017+
}
2018+
2019+
/**
2020+
* Recursively remove a directory and its contents.
2021+
*
2022+
* @param string $dir Directory path to remove.
2023+
* @param string $abspath_realpath_trailing Cached ABSPATH realpath with trailing slash for performance.
2024+
* @return bool True on success, false on failure.
2025+
*/
2026+
private function remove_directory( $dir, $abspath_realpath_trailing ) {
2027+
$dir_realpath = realpath( $dir );
2028+
if ( false === $dir_realpath ) {
2029+
WP_CLI::debug( "Failed to resolve realpath for directory: {$dir}", 'core' );
2030+
return false;
2031+
}
2032+
if ( 0 !== strpos( Utils\trailingslashit( $dir_realpath ), $abspath_realpath_trailing ) ) {
2033+
WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' );
2034+
return false;
2035+
}
2036+
if ( ! is_dir( $dir ) ) {
2037+
return false;
2038+
}
2039+
2040+
$files = new RecursiveIteratorIterator(
2041+
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
2042+
RecursiveIteratorIterator::CHILD_FIRST
2043+
);
2044+
2045+
/** @var \SplFileInfo $fileinfo */
2046+
foreach ( $files as $fileinfo ) {
2047+
// Use the symlink's own path (not realpath) to avoid following it outside ABSPATH.
2048+
if ( $fileinfo->isLink() ) {
2049+
$path = $fileinfo->getPathname();
2050+
if ( ! unlink( $path ) ) {
2051+
WP_CLI::debug( "Failed to remove symbolic link: {$path}", 'core' );
2052+
return false;
2053+
}
2054+
continue;
2055+
}
2056+
2057+
$path = $fileinfo->getRealPath();
2058+
if ( false === $path || 0 !== strpos( $path, $abspath_realpath_trailing ) ) {
2059+
WP_CLI::debug( "Attempted to remove path outside of ABSPATH: {$path}", 'core' );
2060+
return false;
2061+
}
2062+
2063+
if ( $fileinfo->isDir() ) {
2064+
if ( ! rmdir( $path ) ) {
2065+
WP_CLI::debug( "Failed to remove directory: {$path}", 'core' );
2066+
return false;
2067+
}
2068+
} elseif ( ! unlink( $path ) ) {
2069+
WP_CLI::debug( "Failed to remove file: {$path}", 'core' );
2070+
return false;
2071+
}
2072+
}
2073+
2074+
if ( ! rmdir( $dir ) ) {
2075+
WP_CLI::debug( "Failed to remove directory: {$dir}", 'core' );
2076+
return false;
2077+
}
2078+
2079+
return true;
2080+
}
2081+
19022082
private static function strip_content_dir( $zip_file ) {
19032083
$new_zip_file = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.zip';
19042084
register_shutdown_function(

0 commit comments

Comments
 (0)