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