Skip to content

Commit 9d4a3f3

Browse files
authored
Merge branch 'main' into copilot/fix-multisite-db-version-reporting
2 parents 6e3f3eb + 300001f commit 9d4a3f3

File tree

6 files changed

+297
-15
lines changed

6 files changed

+297
-15
lines changed

.gitattributes

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/.actrc export-ignore
2+
/.distignore export-ignore
3+
/.editorconfig export-ignore
4+
/.github export-ignore
5+
/.gitignore export-ignore
6+
/.typos.toml export-ignore
7+
/AGENTS.md export-ignore
8+
/behat.yml export-ignore
9+
/features export-ignore
10+
/phpcs.xml.dist export-ignore
11+
/phpstan.neon.dist export-ignore
12+
/phpunit.xml.dist export-ignore
13+
/tests export-ignore
14+
/wp-cli.yml export-ignore

.github/workflows/copilot-setup-steps.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
- name: Set up PHP environment
2727
if: steps.check_composer_file.outputs.files_exists == 'true'
28-
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2
28+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
2929
with:
3030
php-version: 'latest'
3131
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
@@ -36,7 +36,7 @@ jobs:
3636

3737
- name: Install Composer dependencies & cache dependencies
3838
if: steps.check_composer_file.outputs.files_exists == 'true'
39-
uses: ramsey/composer-install@a35c6ebd3d08125aaf8852dff361e686a1a67947 # v3
39+
uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v3
4040
env:
4141
COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }}
4242
with:

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ update isn't actually running.
419419
Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip...
420420
Unpacking the update...
421421
Cleaning up files...
422-
No files found that need cleaning up
422+
No old files were removed.
423423
Success: WordPress updated successfully.
424424

425425
# Update WordPress using zip file.
@@ -433,7 +433,9 @@ update isn't actually running.
433433
Updating to version 3.1 (en_US)...
434434
Downloading update from https://wordpress.org/wordpress-3.1.zip...
435435
Unpacking the update...
436-
Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually.
436+
Cleaning up files...
437+
No old files were removed.
438+
Warning: Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up.
437439
Success: WordPress updated successfully.
438440

439441

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
@@ -1167,7 +1167,7 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
11671167
* Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip...
11681168
* Unpacking the update...
11691169
* Cleaning up files...
1170-
* No files found that need cleaning up
1170+
* No old files were removed.
11711171
* Success: WordPress updated successfully.
11721172
*
11731173
* # Update WordPress using zip file.
@@ -1181,7 +1181,9 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
11811181
* Updating to version 3.1 (en_US)...
11821182
* Downloading update from https://wordpress.org/wordpress-3.1.zip...
11831183
* Unpacking the update...
1184-
* Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually.
1184+
* Cleaning up files...
1185+
* No old files were removed.
1186+
* Warning: Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up.
11851187
* Success: WordPress updated successfully.
11861188
*
11871189
* @alias upgrade
@@ -1838,16 +1840,16 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
18381840
return;
18391841
}
18401842

1841-
$old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure );
1842-
if ( ! is_array( $old_checksums ) ) {
1843-
WP_CLI::warning( "{$old_checksums} Please cleanup files manually." );
1844-
return;
1845-
}
1843+
// Always clean up files from WordPress core's $_old_files list first
1844+
$this->cleanup_old_files();
18461845

1846+
$old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure );
18471847
$new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure );
1848-
if ( ! is_array( $new_checksums ) ) {
1849-
WP_CLI::warning( "{$new_checksums} Please cleanup files manually." );
18501848

1849+
$has_checksums = is_array( $old_checksums ) && is_array( $new_checksums );
1850+
1851+
if ( ! $has_checksums ) {
1852+
WP_CLI::warning( 'Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up.' );
18511853
return;
18521854
}
18531855

@@ -1943,6 +1945,184 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
19431945
}
19441946
}
19451947

1948+
/**
1949+
* Clean up old files using WordPress core's $_old_files list.
1950+
*
1951+
* It unconditionally deletes files from the $_old_files global array maintained by WordPress core.
1952+
*/
1953+
private function cleanup_old_files() {
1954+
$old_files = $this->get_old_files_list();
1955+
if ( empty( $old_files ) ) {
1956+
WP_CLI::log( 'No files found that need cleaning up.' );
1957+
return;
1958+
}
1959+
1960+
WP_CLI::log( 'Cleaning up files...' );
1961+
1962+
$count = $this->remove_old_files_from_list( $old_files );
1963+
1964+
if ( $count ) {
1965+
WP_CLI::log( number_format( $count ) . ' files cleaned up.' );
1966+
} else {
1967+
WP_CLI::log( 'No old files were removed.' );
1968+
}
1969+
}
1970+
1971+
/**
1972+
* Get the list of old files from WordPress core.
1973+
*
1974+
* @return array Array of old file paths, or empty array if not available.
1975+
*/
1976+
private function get_old_files_list() {
1977+
// Include WordPress core's update file to access the $_old_files list
1978+
if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) {
1979+
WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' );
1980+
return array();
1981+
}
1982+
1983+
require_once ABSPATH . 'wp-admin/includes/update-core.php';
1984+
1985+
global $_old_files;
1986+
1987+
if ( empty( $_old_files ) || ! is_array( $_old_files ) ) {
1988+
return array();
1989+
}
1990+
1991+
return $_old_files;
1992+
}
1993+
1994+
/**
1995+
* Remove old files from a list.
1996+
*
1997+
* This is a shared helper method that handles the actual removal of files and directories.
1998+
*
1999+
* @param array $files Array of file paths to remove.
2000+
* @return int Number of files/directories successfully removed.
2001+
*/
2002+
private function remove_old_files_from_list( $files ) {
2003+
$count = 0;
2004+
2005+
$abspath_realpath = realpath( ABSPATH );
2006+
if ( false === $abspath_realpath ) {
2007+
WP_CLI::debug( 'Failed to resolve ABSPATH realpath', 'core' );
2008+
return $count;
2009+
}
2010+
$abspath_realpath_trailing = Utils\trailingslashit( $abspath_realpath );
2011+
2012+
foreach ( $files as $file ) {
2013+
$file_path = ABSPATH . $file;
2014+
2015+
// Skip entries that don't exist and aren't (broken) symlinks.
2016+
if ( ! file_exists( $file_path ) && ! is_link( $file_path ) ) {
2017+
continue;
2018+
}
2019+
2020+
// Symlinks: validate and remove without following the link.
2021+
if ( is_link( $file_path ) ) {
2022+
$normalized_path = realpath( dirname( $file_path ) );
2023+
if ( false === $normalized_path
2024+
|| 0 !== strpos( Utils\trailingslashit( $normalized_path ), $abspath_realpath_trailing )
2025+
) {
2026+
WP_CLI::debug( "Skipping symbolic link outside of ABSPATH: {$file}", 'core' );
2027+
continue;
2028+
}
2029+
if ( unlink( $file_path ) ) {
2030+
WP_CLI::log( "Symbolic link removed: {$file}" );
2031+
++$count;
2032+
} else {
2033+
WP_CLI::debug( "Failed to remove symbolic link: {$file}", 'core' );
2034+
}
2035+
continue;
2036+
}
2037+
2038+
// Regular files/directories: validate real path is within ABSPATH.
2039+
$file_realpath = realpath( $file_path );
2040+
if ( false === $file_realpath || 0 !== strpos( Utils\trailingslashit( $file_realpath ), $abspath_realpath_trailing ) ) {
2041+
WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' );
2042+
continue;
2043+
}
2044+
2045+
if ( is_dir( $file_path ) ) {
2046+
if ( $this->remove_directory( $file_path, $abspath_realpath_trailing ) ) {
2047+
WP_CLI::log( "Directory removed: {$file}" );
2048+
++$count;
2049+
} else {
2050+
WP_CLI::debug( "Failed to remove directory: {$file}", 'core' );
2051+
}
2052+
} elseif ( unlink( $file_path ) ) {
2053+
WP_CLI::log( "File removed: {$file}" );
2054+
++$count;
2055+
} else {
2056+
WP_CLI::debug( "Failed to remove file: {$file}", 'core' );
2057+
}
2058+
}
2059+
2060+
return $count;
2061+
}
2062+
2063+
/**
2064+
* Recursively remove a directory and its contents.
2065+
*
2066+
* @param string $dir Directory path to remove.
2067+
* @param string $abspath_realpath_trailing Cached ABSPATH realpath with trailing slash for performance.
2068+
* @return bool True on success, false on failure.
2069+
*/
2070+
private function remove_directory( $dir, $abspath_realpath_trailing ) {
2071+
$dir_realpath = realpath( $dir );
2072+
if ( false === $dir_realpath ) {
2073+
WP_CLI::debug( "Failed to resolve realpath for directory: {$dir}", 'core' );
2074+
return false;
2075+
}
2076+
if ( 0 !== strpos( Utils\trailingslashit( $dir_realpath ), $abspath_realpath_trailing ) ) {
2077+
WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' );
2078+
return false;
2079+
}
2080+
if ( ! is_dir( $dir ) ) {
2081+
return false;
2082+
}
2083+
2084+
$files = new RecursiveIteratorIterator(
2085+
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
2086+
RecursiveIteratorIterator::CHILD_FIRST
2087+
);
2088+
2089+
/** @var \SplFileInfo $fileinfo */
2090+
foreach ( $files as $fileinfo ) {
2091+
// Use the symlink's own path (not realpath) to avoid following it outside ABSPATH.
2092+
if ( $fileinfo->isLink() ) {
2093+
$path = $fileinfo->getPathname();
2094+
if ( ! unlink( $path ) ) {
2095+
WP_CLI::debug( "Failed to remove symbolic link: {$path}", 'core' );
2096+
return false;
2097+
}
2098+
continue;
2099+
}
2100+
2101+
$path = $fileinfo->getRealPath();
2102+
if ( false === $path || 0 !== strpos( $path, $abspath_realpath_trailing ) ) {
2103+
WP_CLI::debug( "Attempted to remove path outside of ABSPATH: {$path}", 'core' );
2104+
return false;
2105+
}
2106+
2107+
if ( $fileinfo->isDir() ) {
2108+
if ( ! rmdir( $path ) ) {
2109+
WP_CLI::debug( "Failed to remove directory: {$path}", 'core' );
2110+
return false;
2111+
}
2112+
} elseif ( ! unlink( $path ) ) {
2113+
WP_CLI::debug( "Failed to remove file: {$path}", 'core' );
2114+
return false;
2115+
}
2116+
}
2117+
2118+
if ( ! rmdir( $dir ) ) {
2119+
WP_CLI::debug( "Failed to remove directory: {$dir}", 'core' );
2120+
return false;
2121+
}
2122+
2123+
return true;
2124+
}
2125+
19462126
private static function strip_content_dir( $zip_file ) {
19472127
$new_zip_file = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.zip';
19482128
register_shutdown_function(

0 commit comments

Comments
 (0)