@@ -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