@@ -1168,7 +1168,7 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
11681168 * Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip...
11691169 * Unpacking the update...
11701170 * Cleaning up files...
1171- * No files found that need cleaning up
1171+ * No old files were removed.
11721172 * Success: WordPress updated successfully.
11731173 *
11741174 * # Update WordPress using zip file.
@@ -1182,7 +1182,9 @@ private static function get_core_checksums( $version, $locale, $insecure ) {
11821182 * Updating to version 3.1 (en_US)...
11831183 * Downloading update from https://wordpress.org/wordpress-3.1.zip...
11841184 * Unpacking the update...
1185- * Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually.
1185+ * Cleaning up files...
1186+ * No old files were removed.
1187+ * Warning: Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up.
11861188 * Success: WordPress updated successfully.
11871189 *
11881190 * @alias upgrade
@@ -1839,16 +1841,16 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
18391841 return ;
18401842 }
18411843
1842- $ old_checksums = self ::get_core_checksums ( $ version_from , $ locale ?: 'en_US ' , $ insecure );
1843- if ( ! is_array ( $ old_checksums ) ) {
1844- WP_CLI ::warning ( "{$ old_checksums } Please cleanup files manually. " );
1845- return ;
1846- }
1844+ // Always clean up files from WordPress core's $_old_files list first
1845+ $ this ->cleanup_old_files ();
18471846
1847+ $ old_checksums = self ::get_core_checksums ( $ version_from , $ locale ?: 'en_US ' , $ insecure );
18481848 $ new_checksums = self ::get_core_checksums ( $ version_to , $ locale ?: 'en_US ' , $ insecure );
1849- if ( ! is_array ( $ new_checksums ) ) {
1850- WP_CLI ::warning ( "{$ new_checksums } Please cleanup files manually. " );
18511849
1850+ $ has_checksums = is_array ( $ old_checksums ) && is_array ( $ new_checksums );
1851+
1852+ if ( ! $ has_checksums ) {
1853+ WP_CLI ::warning ( 'Could not retrieve WordPress core checksums; skipping checksum-based cleanup. Files listed in $_old_files were still cleaned up. ' );
18521854 return ;
18531855 }
18541856
@@ -1944,6 +1946,184 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse
19441946 }
19451947 }
19461948
1949+ /**
1950+ * Clean up old files using WordPress core's $_old_files list.
1951+ *
1952+ * It unconditionally deletes files from the $_old_files global array maintained by WordPress core.
1953+ */
1954+ private function cleanup_old_files () {
1955+ $ old_files = $ this ->get_old_files_list ();
1956+ if ( empty ( $ old_files ) ) {
1957+ WP_CLI ::log ( 'No files found that need cleaning up. ' );
1958+ return ;
1959+ }
1960+
1961+ WP_CLI ::log ( 'Cleaning up files... ' );
1962+
1963+ $ count = $ this ->remove_old_files_from_list ( $ old_files );
1964+
1965+ if ( $ count ) {
1966+ WP_CLI ::log ( number_format ( $ count ) . ' files cleaned up. ' );
1967+ } else {
1968+ WP_CLI ::log ( 'No old files were removed. ' );
1969+ }
1970+ }
1971+
1972+ /**
1973+ * Get the list of old files from WordPress core.
1974+ *
1975+ * @return array Array of old file paths, or empty array if not available.
1976+ */
1977+ private function get_old_files_list () {
1978+ // Include WordPress core's update file to access the $_old_files list
1979+ if ( ! file_exists ( ABSPATH . 'wp-admin/includes/update-core.php ' ) ) {
1980+ WP_CLI ::warning ( 'Could not find update-core.php. Please cleanup files manually. ' );
1981+ return array ();
1982+ }
1983+
1984+ require_once ABSPATH . 'wp-admin/includes/update-core.php ' ;
1985+
1986+ global $ _old_files ;
1987+
1988+ if ( empty ( $ _old_files ) || ! is_array ( $ _old_files ) ) {
1989+ return array ();
1990+ }
1991+
1992+ return $ _old_files ;
1993+ }
1994+
1995+ /**
1996+ * Remove old files from a list.
1997+ *
1998+ * This is a shared helper method that handles the actual removal of files and directories.
1999+ *
2000+ * @param array $files Array of file paths to remove.
2001+ * @return int Number of files/directories successfully removed.
2002+ */
2003+ private function remove_old_files_from_list ( $ files ) {
2004+ $ count = 0 ;
2005+
2006+ $ abspath_realpath = realpath ( ABSPATH );
2007+ if ( false === $ abspath_realpath ) {
2008+ WP_CLI ::debug ( 'Failed to resolve ABSPATH realpath ' , 'core ' );
2009+ return $ count ;
2010+ }
2011+ $ abspath_realpath_trailing = Utils \trailingslashit ( $ abspath_realpath );
2012+
2013+ foreach ( $ files as $ file ) {
2014+ $ file_path = ABSPATH . $ file ;
2015+
2016+ // Skip entries that don't exist and aren't (broken) symlinks.
2017+ if ( ! file_exists ( $ file_path ) && ! is_link ( $ file_path ) ) {
2018+ continue ;
2019+ }
2020+
2021+ // Symlinks: validate and remove without following the link.
2022+ if ( is_link ( $ file_path ) ) {
2023+ $ normalized_path = realpath ( dirname ( $ file_path ) );
2024+ if ( false === $ normalized_path
2025+ || 0 !== strpos ( Utils \trailingslashit ( $ normalized_path ), $ abspath_realpath_trailing )
2026+ ) {
2027+ WP_CLI ::debug ( "Skipping symbolic link outside of ABSPATH: {$ file }" , 'core ' );
2028+ continue ;
2029+ }
2030+ if ( unlink ( $ file_path ) ) {
2031+ WP_CLI ::log ( "Symbolic link removed: {$ file }" );
2032+ ++$ count ;
2033+ } else {
2034+ WP_CLI ::debug ( "Failed to remove symbolic link: {$ file }" , 'core ' );
2035+ }
2036+ continue ;
2037+ }
2038+
2039+ // Regular files/directories: validate real path is within ABSPATH.
2040+ $ file_realpath = realpath ( $ file_path );
2041+ if ( false === $ file_realpath || 0 !== strpos ( Utils \trailingslashit ( $ file_realpath ), $ abspath_realpath_trailing ) ) {
2042+ WP_CLI ::debug ( "Skipping file outside of ABSPATH: {$ file }" , 'core ' );
2043+ continue ;
2044+ }
2045+
2046+ if ( is_dir ( $ file_path ) ) {
2047+ if ( $ this ->remove_directory ( $ file_path , $ abspath_realpath_trailing ) ) {
2048+ WP_CLI ::log ( "Directory removed: {$ file }" );
2049+ ++$ count ;
2050+ } else {
2051+ WP_CLI ::debug ( "Failed to remove directory: {$ file }" , 'core ' );
2052+ }
2053+ } elseif ( unlink ( $ file_path ) ) {
2054+ WP_CLI ::log ( "File removed: {$ file }" );
2055+ ++$ count ;
2056+ } else {
2057+ WP_CLI ::debug ( "Failed to remove file: {$ file }" , 'core ' );
2058+ }
2059+ }
2060+
2061+ return $ count ;
2062+ }
2063+
2064+ /**
2065+ * Recursively remove a directory and its contents.
2066+ *
2067+ * @param string $dir Directory path to remove.
2068+ * @param string $abspath_realpath_trailing Cached ABSPATH realpath with trailing slash for performance.
2069+ * @return bool True on success, false on failure.
2070+ */
2071+ private function remove_directory ( $ dir , $ abspath_realpath_trailing ) {
2072+ $ dir_realpath = realpath ( $ dir );
2073+ if ( false === $ dir_realpath ) {
2074+ WP_CLI ::debug ( "Failed to resolve realpath for directory: {$ dir }" , 'core ' );
2075+ return false ;
2076+ }
2077+ if ( 0 !== strpos ( Utils \trailingslashit ( $ dir_realpath ), $ abspath_realpath_trailing ) ) {
2078+ WP_CLI ::debug ( "Attempted to remove directory outside of ABSPATH: {$ dir_realpath }" , 'core ' );
2079+ return false ;
2080+ }
2081+ if ( ! is_dir ( $ dir ) ) {
2082+ return false ;
2083+ }
2084+
2085+ $ files = new RecursiveIteratorIterator (
2086+ new RecursiveDirectoryIterator ( $ dir , RecursiveDirectoryIterator::SKIP_DOTS ),
2087+ RecursiveIteratorIterator::CHILD_FIRST
2088+ );
2089+
2090+ /** @var \SplFileInfo $fileinfo */
2091+ foreach ( $ files as $ fileinfo ) {
2092+ // Use the symlink's own path (not realpath) to avoid following it outside ABSPATH.
2093+ if ( $ fileinfo ->isLink () ) {
2094+ $ path = $ fileinfo ->getPathname ();
2095+ if ( ! unlink ( $ path ) ) {
2096+ WP_CLI ::debug ( "Failed to remove symbolic link: {$ path }" , 'core ' );
2097+ return false ;
2098+ }
2099+ continue ;
2100+ }
2101+
2102+ $ path = $ fileinfo ->getRealPath ();
2103+ if ( false === $ path || 0 !== strpos ( $ path , $ abspath_realpath_trailing ) ) {
2104+ WP_CLI ::debug ( "Attempted to remove path outside of ABSPATH: {$ path }" , 'core ' );
2105+ return false ;
2106+ }
2107+
2108+ if ( $ fileinfo ->isDir () ) {
2109+ if ( ! rmdir ( $ path ) ) {
2110+ WP_CLI ::debug ( "Failed to remove directory: {$ path }" , 'core ' );
2111+ return false ;
2112+ }
2113+ } elseif ( ! unlink ( $ path ) ) {
2114+ WP_CLI ::debug ( "Failed to remove file: {$ path }" , 'core ' );
2115+ return false ;
2116+ }
2117+ }
2118+
2119+ if ( ! rmdir ( $ dir ) ) {
2120+ WP_CLI ::debug ( "Failed to remove directory: {$ dir }" , 'core ' );
2121+ return false ;
2122+ }
2123+
2124+ return true ;
2125+ }
2126+
19472127 private static function strip_content_dir ( $ zip_file ) {
19482128 $ new_zip_file = Utils \get_temp_dir () . uniqid ( 'wp_ ' ) . '.zip ' ;
19492129 register_shutdown_function (
0 commit comments