From 41efc3a6e1a5bf2e4f5675cfca96e7e153f60116 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:59:57 +0000 Subject: [PATCH 01/15] Initial plan From baee2fa671b1aaf440efdcda5fa1407b8b5440a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:05:35 +0000 Subject: [PATCH 02/15] Add support for cleaning up old files using WordPress $_old_files list Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 103 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index b9bbb2b4..4889a5bf 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1511,15 +1511,13 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse } $old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure ); - if ( ! is_array( $old_checksums ) ) { - WP_CLI::warning( "{$old_checksums} Please cleanup files manually." ); - return; - } - $new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure ); - if ( ! is_array( $new_checksums ) ) { - WP_CLI::warning( "{$new_checksums} Please cleanup files manually." ); + $has_checksums = is_array( $old_checksums ) && is_array( $new_checksums ); + + if ( ! $has_checksums ) { + // When checksums are not available, use WordPress core's $_old_files list + $this->cleanup_old_files(); return; } @@ -1615,6 +1613,97 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse } } + /** + * Clean up old files using WordPress core's $_old_files list. + * + * This method is used when checksums are not available for version comparison. + * It unconditionally deletes files from the $_old_files global array maintained by WordPress core. + */ + private function cleanup_old_files() { + // Include WordPress core's update file to access the $_old_files list + if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { + WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' ); + return; + } + + require_once ABSPATH . 'wp-admin/includes/update-core.php'; + + global $_old_files; + + if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { + WP_CLI::log( 'No files found that need cleaning up.' ); + return; + } + + WP_CLI::log( 'Cleaning up files...' ); + + $count = 0; + foreach ( $_old_files as $file ) { + // wp-content should be considered user data + if ( 0 === stripos( $file, 'wp-content' ) ) { + continue; + } + + $file_path = ABSPATH . $file; + + // Handle both files and directories + if ( file_exists( $file_path ) ) { + if ( is_dir( $file_path ) ) { + // Remove directory recursively + if ( $this->remove_directory( $file_path ) ) { + WP_CLI::log( "Directory removed: {$file}" ); + ++$count; + } + } else { + // Remove file + if ( unlink( $file_path ) ) { + WP_CLI::log( "File removed: {$file}" ); + ++$count; + } + } + } + } + + if ( $count ) { + WP_CLI::log( number_format( $count ) . ' files cleaned up.' ); + } else { + WP_CLI::log( 'No files found that need cleaning up.' ); + } + } + + /** + * Recursively remove a directory and its contents. + * + * @param string $dir Directory path to remove. + * @return bool True on success, false on failure. + */ + private function remove_directory( $dir ) { + if ( ! is_dir( $dir ) ) { + return false; + } + + $items = scandir( $dir ); + if ( false === $items ) { + return false; + } + + foreach ( $items as $item ) { + if ( '.' === $item || '..' === $item ) { + continue; + } + + $path = $dir . '/' . $item; + + if ( is_dir( $path ) ) { + $this->remove_directory( $path ); + } else { + unlink( $path ); + } + } + + return rmdir( $dir ); + } + private static function strip_content_dir( $zip_file ) { $new_zip_file = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.zip'; register_shutdown_function( From 37cfece3052cdd36abcdbf71f2f7cd9abbd94a48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:07:10 +0000 Subject: [PATCH 03/15] Enhance cleanup to use $_old_files even when checksums are available Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Core_Command.php b/src/Core_Command.php index 4889a5bf..56d86933 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1611,6 +1611,10 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse WP_CLI::log( 'No files found that need cleaning up.' ); } } + + // Additionally, clean up files from $_old_files that are not in checksums + // These should be deleted unconditionally as they are known old files + $this->cleanup_old_files_not_in_checksums( $old_checksums, $new_checksums ); } /** @@ -1671,6 +1675,73 @@ private function cleanup_old_files() { } } + /** + * Clean up old files from $_old_files that are not tracked in checksums. + * + * This method is used as a supplement when checksums ARE available. + * It unconditionally deletes files from $_old_files that are not present in either + * the old or new checksums, as these files cannot be verified for modifications. + * + * @param array $old_checksums Old checksums array. + * @param array $new_checksums New checksums array. + */ + private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checksums ) { + // Include WordPress core's update file to access the $_old_files list + if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { + return; + } + + require_once ABSPATH . 'wp-admin/includes/update-core.php'; + + global $_old_files; + + if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { + return; + } + + // Combine all files from both checksum arrays + $all_checksum_files = array_merge( array_keys( $old_checksums ), array_keys( $new_checksums ) ); + $all_checksum_files = array_unique( $all_checksum_files ); + + // Find files in $_old_files that are not in checksums + $files_to_remove = array_diff( $_old_files, $all_checksum_files ); + + if ( empty( $files_to_remove ) ) { + return; + } + + $count = 0; + foreach ( $files_to_remove as $file ) { + // wp-content should be considered user data + if ( 0 === stripos( $file, 'wp-content' ) ) { + continue; + } + + $file_path = ABSPATH . $file; + + // Handle both files and directories + if ( file_exists( $file_path ) ) { + if ( is_dir( $file_path ) ) { + // Remove directory recursively + if ( $this->remove_directory( $file_path ) ) { + WP_CLI::log( "Directory removed: {$file}" ); + ++$count; + } + } else { + // Remove file + if ( unlink( $file_path ) ) { + WP_CLI::log( "File removed: {$file}" ); + ++$count; + } + } + } + } + + if ( $count ) { + WP_CLI::log( number_format( $count ) . ' additional old files cleaned up.' ); + } + } + /** * Recursively remove a directory and its contents. * From 4760835ee6a415b650a6645be6783bc8f6820e69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:09:52 +0000 Subject: [PATCH 04/15] Update tests and documentation to reflect new cleanup behavior Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/core-download.feature | 2 +- src/Core_Command.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/core-download.feature b/features/core-download.feature index 5c7fbb57..06eec016 100644 --- a/features/core-download.feature +++ b/features/core-download.feature @@ -180,7 +180,7 @@ Feature: Download WordPress """ Failed to find WordPress version """ - And STDERR should contain: + And STDERR should not contain: """ Warning: Checksums not available for WordPress nightly/en_US. Please cleanup files manually. """ diff --git a/src/Core_Command.php b/src/Core_Command.php index 56d86933..264c7e7c 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1108,7 +1108,7 @@ private static function get_core_checksums( $version, $locale, $insecure ) { * Updating to version 3.1 (en_US)... * Downloading update from https://wordpress.org/wordpress-3.1.zip... * Unpacking the update... - * Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually. + * Cleaning up files... * Success: WordPress updated successfully. * * @alias upgrade From 5a094a0fece214b9f41619c2ef706fd189b13b77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:13:05 +0000 Subject: [PATCH 05/15] Address code review feedback - improve error handling and reduce duplication Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 82 +++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 264c7e7c..b4cb39ad 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1624,17 +1624,8 @@ private function cleanup_extra_files( $version_from, $version_to, $locale, $inse * It unconditionally deletes files from the $_old_files global array maintained by WordPress core. */ private function cleanup_old_files() { - // Include WordPress core's update file to access the $_old_files list - if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { - WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' ); - return; - } - - require_once ABSPATH . 'wp-admin/includes/update-core.php'; - - global $_old_files; - - if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { + $old_files = $this->get_old_files_list(); + if ( empty( $old_files ) ) { WP_CLI::log( 'No files found that need cleaning up.' ); return; } @@ -1642,7 +1633,7 @@ private function cleanup_old_files() { WP_CLI::log( 'Cleaning up files...' ); $count = 0; - foreach ( $_old_files as $file ) { + foreach ( $old_files as $file ) { // wp-content should be considered user data if ( 0 === stripos( $file, 'wp-content' ) ) { continue; @@ -1657,12 +1648,16 @@ private function cleanup_old_files() { if ( $this->remove_directory( $file_path ) ) { WP_CLI::log( "Directory removed: {$file}" ); ++$count; + } else { + WP_CLI::debug( "Failed to remove directory: {$file}", 'core' ); } } else { // Remove file - if ( unlink( $file_path ) ) { + if ( @unlink( $file_path ) ) { WP_CLI::log( "File removed: {$file}" ); ++$count; + } else { + WP_CLI::debug( "Failed to remove file: {$file}", 'core' ); } } } @@ -1686,16 +1681,8 @@ private function cleanup_old_files() { * @param array $new_checksums New checksums array. */ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checksums ) { - // Include WordPress core's update file to access the $_old_files list - if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { - return; - } - - require_once ABSPATH . 'wp-admin/includes/update-core.php'; - - global $_old_files; - - if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { + $old_files = $this->get_old_files_list(); + if ( empty( $old_files ) ) { return; } @@ -1704,7 +1691,7 @@ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checks $all_checksum_files = array_unique( $all_checksum_files ); // Find files in $_old_files that are not in checksums - $files_to_remove = array_diff( $_old_files, $all_checksum_files ); + $files_to_remove = array_diff( $old_files, $all_checksum_files ); if ( empty( $files_to_remove ) ) { return; @@ -1726,12 +1713,16 @@ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checks if ( $this->remove_directory( $file_path ) ) { WP_CLI::log( "Directory removed: {$file}" ); ++$count; + } else { + WP_CLI::debug( "Failed to remove directory: {$file}", 'core' ); } } else { // Remove file - if ( unlink( $file_path ) ) { + if ( @unlink( $file_path ) ) { WP_CLI::log( "File removed: {$file}" ); ++$count; + } else { + WP_CLI::debug( "Failed to remove file: {$file}", 'core' ); } } } @@ -1742,6 +1733,29 @@ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checks } } + /** + * Get the list of old files from WordPress core. + * + * @return array Array of old file paths, or empty array if not available. + */ + private function get_old_files_list() { + // Include WordPress core's update file to access the $_old_files list + if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { + WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' ); + return array(); + } + + require_once ABSPATH . 'wp-admin/includes/update-core.php'; + + global $_old_files; + + if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { + return array(); + } + + return $_old_files; + } + /** * Recursively remove a directory and its contents. * @@ -1755,6 +1769,7 @@ private function remove_directory( $dir ) { $items = scandir( $dir ); if ( false === $items ) { + WP_CLI::debug( "Failed to scan directory: {$dir}", 'core' ); return false; } @@ -1766,13 +1781,24 @@ private function remove_directory( $dir ) { $path = $dir . '/' . $item; if ( is_dir( $path ) ) { - $this->remove_directory( $path ); + if ( ! $this->remove_directory( $path ) ) { + WP_CLI::debug( "Failed to remove subdirectory: {$path}", 'core' ); + return false; + } } else { - unlink( $path ); + if ( ! @unlink( $path ) ) { + WP_CLI::debug( "Failed to remove file in directory: {$path}", 'core' ); + return false; + } } } - return rmdir( $dir ); + if ( ! @rmdir( $dir ) ) { + WP_CLI::debug( "Failed to remove directory: {$dir}", 'core' ); + return false; + } + + return true; } private static function strip_content_dir( $zip_file ) { From bf5a413d8d1eb39c857d58f18991814e0149367d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:16:21 +0000 Subject: [PATCH 06/15] Remove @ error suppression operators per code review feedback Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index b4cb39ad..e45e65d1 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1653,7 +1653,7 @@ private function cleanup_old_files() { } } else { // Remove file - if ( @unlink( $file_path ) ) { + if ( unlink( $file_path ) ) { WP_CLI::log( "File removed: {$file}" ); ++$count; } else { @@ -1718,7 +1718,7 @@ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checks } } else { // Remove file - if ( @unlink( $file_path ) ) { + if ( unlink( $file_path ) ) { WP_CLI::log( "File removed: {$file}" ); ++$count; } else { @@ -1786,14 +1786,14 @@ private function remove_directory( $dir ) { return false; } } else { - if ( ! @unlink( $path ) ) { + if ( ! unlink( $path ) ) { WP_CLI::debug( "Failed to remove file in directory: {$path}", 'core' ); return false; } } } - if ( ! @rmdir( $dir ) ) { + if ( ! rmdir( $dir ) ) { WP_CLI::debug( "Failed to remove directory: {$dir}", 'core' ); return false; } From fe5a71482c70bf896808995f4ee3833c90cf32c2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 7 Nov 2025 22:18:25 +0100 Subject: [PATCH 07/15] Use directory_separator Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Core_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index e45e65d1..1b69bee1 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1778,7 +1778,7 @@ private function remove_directory( $dir ) { continue; } - $path = $dir . '/' . $item; + $path = $dir . DIRECTORY_SEPARATOR . $item; if ( is_dir( $path ) ) { if ( ! $this->remove_directory( $path ) ) { From 12fd04b7daffff4dd285e417a00f74c8687d2a12 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 7 Nov 2025 22:18:48 +0100 Subject: [PATCH 08/15] Path validation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Core_Command.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Core_Command.php b/src/Core_Command.php index 1b69bee1..f7265eae 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1763,6 +1763,16 @@ private function get_old_files_list() { * @return bool True on success, false on failure. */ private function remove_directory( $dir ) { + $dir_realpath = realpath( $dir ); + $abspath_realpath = realpath( ABSPATH ); + if ( false === $dir_realpath || false === $abspath_realpath ) { + WP_CLI::debug( "Failed to resolve realpath for directory or ABSPATH: {$dir}", 'core' ); + return false; + } + if ( 0 !== strpos( $dir_realpath, $abspath_realpath ) ) { + WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' ); + return false; + } if ( ! is_dir( $dir ) ) { return false; } From d5ffb6dae22c36aca321e6572084fd9da7e0d940 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 7 Nov 2025 22:19:11 +0100 Subject: [PATCH 09/15] Adjust message Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Core_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index f7265eae..7c2e496b 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1666,7 +1666,7 @@ private function cleanup_old_files() { if ( $count ) { WP_CLI::log( number_format( $count ) . ' files cleaned up.' ); } else { - WP_CLI::log( 'No files found that need cleaning up.' ); + WP_CLI::log( 'No old files were removed.' ); } } From 96fcab20cb74f834199b70f921d4d8fbf66dc584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:22:24 +0000 Subject: [PATCH 10/15] Refactor to reduce code duplication and add symlink handling Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 108 ++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 57 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 7c2e496b..83db4869 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1632,36 +1632,7 @@ private function cleanup_old_files() { WP_CLI::log( 'Cleaning up files...' ); - $count = 0; - foreach ( $old_files as $file ) { - // wp-content should be considered user data - if ( 0 === stripos( $file, 'wp-content' ) ) { - continue; - } - - $file_path = ABSPATH . $file; - - // Handle both files and directories - if ( file_exists( $file_path ) ) { - if ( is_dir( $file_path ) ) { - // Remove directory recursively - if ( $this->remove_directory( $file_path ) ) { - WP_CLI::log( "Directory removed: {$file}" ); - ++$count; - } else { - WP_CLI::debug( "Failed to remove directory: {$file}", 'core' ); - } - } else { - // Remove file - if ( unlink( $file_path ) ) { - WP_CLI::log( "File removed: {$file}" ); - ++$count; - } else { - WP_CLI::debug( "Failed to remove file: {$file}", 'core' ); - } - } - } - } + $count = $this->remove_old_files_from_list( $old_files ); if ( $count ) { WP_CLI::log( number_format( $count ) . ' files cleaned up.' ); @@ -1697,8 +1668,47 @@ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checks return; } + $count = $this->remove_old_files_from_list( $files_to_remove ); + + if ( $count ) { + WP_CLI::log( number_format( $count ) . ' additional old files cleaned up.' ); + } + } + + /** + * Get the list of old files from WordPress core. + * + * @return array Array of old file paths, or empty array if not available. + */ + private function get_old_files_list() { + // Include WordPress core's update file to access the $_old_files list + if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { + WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' ); + return array(); + } + + require_once ABSPATH . 'wp-admin/includes/update-core.php'; + + global $_old_files; + + if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { + return array(); + } + + return $_old_files; + } + + /** + * Remove old files from a list. + * + * This is a shared helper method that handles the actual removal of files and directories. + * + * @param array $files Array of file paths to remove. + * @return int Number of files/directories successfully removed. + */ + private function remove_old_files_from_list( $files ) { $count = 0; - foreach ( $files_to_remove as $file ) { + foreach ( $files as $file ) { // wp-content should be considered user data if ( 0 === stripos( $file, 'wp-content' ) ) { continue; @@ -1728,32 +1738,7 @@ private function cleanup_old_files_not_in_checksums( $old_checksums, $new_checks } } - if ( $count ) { - WP_CLI::log( number_format( $count ) . ' additional old files cleaned up.' ); - } - } - - /** - * Get the list of old files from WordPress core. - * - * @return array Array of old file paths, or empty array if not available. - */ - private function get_old_files_list() { - // Include WordPress core's update file to access the $_old_files list - if ( ! file_exists( ABSPATH . 'wp-admin/includes/update-core.php' ) ) { - WP_CLI::warning( 'Could not find update-core.php. Please cleanup files manually.' ); - return array(); - } - - require_once ABSPATH . 'wp-admin/includes/update-core.php'; - - global $_old_files; - - if ( empty( $_old_files ) || ! is_array( $_old_files ) ) { - return array(); - } - - return $_old_files; + return $count; } /** @@ -1790,6 +1775,15 @@ private function remove_directory( $dir ) { $path = $dir . DIRECTORY_SEPARATOR . $item; + // Handle symbolic links before checking if it's a directory + if ( is_link( $path ) ) { + if ( ! unlink( $path ) ) { + WP_CLI::debug( "Failed to remove symbolic link: {$path}", 'core' ); + return false; + } + continue; + } + if ( is_dir( $path ) ) { if ( ! $this->remove_directory( $path ) ) { WP_CLI::debug( "Failed to remove subdirectory: {$path}", 'core' ); From 038d662a63d0dfffa921c6660a099ec46dfceaa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:24:25 +0000 Subject: [PATCH 11/15] Add path validation and improve security checks Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 83db4869..e3fb5779 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1716,6 +1716,16 @@ private function remove_old_files_from_list( $files ) { $file_path = ABSPATH . $file; + // Validate the path is within ABSPATH + $file_realpath = realpath( $file_path ); + if ( false !== $file_realpath ) { + $abspath_realpath = realpath( ABSPATH ); + if ( false === $abspath_realpath || 0 !== strpos( $file_realpath, trailingslashit( $abspath_realpath ) ) ) { + WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' ); + continue; + } + } + // Handle both files and directories if ( file_exists( $file_path ) ) { if ( is_dir( $file_path ) ) { @@ -1754,7 +1764,8 @@ private function remove_directory( $dir ) { WP_CLI::debug( "Failed to resolve realpath for directory or ABSPATH: {$dir}", 'core' ); return false; } - if ( 0 !== strpos( $dir_realpath, $abspath_realpath ) ) { + // Normalize paths with trailing slashes for accurate comparison + if ( 0 !== strpos( trailingslashit( $dir_realpath ), trailingslashit( $abspath_realpath ) ) ) { WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' ); return false; } From 92f204911c15c233b2467dc0ac8f64b47d37c001 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:26:12 +0000 Subject: [PATCH 12/15] Fix path validation logic for better security Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index e3fb5779..0966102e 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1724,6 +1724,10 @@ private function remove_old_files_from_list( $files ) { WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' ); continue; } + } else { + // Skip files with invalid paths + WP_CLI::debug( "Skipping file with invalid path: {$file}", 'core' ); + continue; } // Handle both files and directories @@ -1765,7 +1769,7 @@ private function remove_directory( $dir ) { return false; } // Normalize paths with trailing slashes for accurate comparison - if ( 0 !== strpos( trailingslashit( $dir_realpath ), trailingslashit( $abspath_realpath ) ) ) { + if ( 0 !== strpos( $dir_realpath, trailingslashit( $abspath_realpath ) ) ) { WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' ); return false; } From 0d1bfb37dd7c8c0c0afe731a65473ec9bab3f61b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:27:40 +0000 Subject: [PATCH 13/15] Optimize path validation by caching ABSPATH realpath Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 0966102e..fca0653c 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1708,6 +1708,15 @@ private function get_old_files_list() { */ private function remove_old_files_from_list( $files ) { $count = 0; + + // Cache ABSPATH realpath for performance + $abspath_realpath = realpath( ABSPATH ); + if ( false === $abspath_realpath ) { + WP_CLI::debug( 'Failed to resolve ABSPATH realpath', 'core' ); + return $count; + } + $abspath_realpath_trailing = trailingslashit( $abspath_realpath ); + foreach ( $files as $file ) { // wp-content should be considered user data if ( 0 === stripos( $file, 'wp-content' ) ) { @@ -1719,8 +1728,7 @@ private function remove_old_files_from_list( $files ) { // Validate the path is within ABSPATH $file_realpath = realpath( $file_path ); if ( false !== $file_realpath ) { - $abspath_realpath = realpath( ABSPATH ); - if ( false === $abspath_realpath || 0 !== strpos( $file_realpath, trailingslashit( $abspath_realpath ) ) ) { + if ( 0 !== strpos( $file_realpath, $abspath_realpath_trailing ) ) { WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' ); continue; } From 6b30e73f3902564ce7426a57c6260997d2c008cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:31:26 +0000 Subject: [PATCH 14/15] Clean up code style - remove trailing whitespace and simplify logic Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Core_Command.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index fca0653c..9d532e58 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1708,7 +1708,7 @@ private function get_old_files_list() { */ private function remove_old_files_from_list( $files ) { $count = 0; - + // Cache ABSPATH realpath for performance $abspath_realpath = realpath( ABSPATH ); if ( false === $abspath_realpath ) { @@ -1716,7 +1716,7 @@ private function remove_old_files_from_list( $files ) { return $count; } $abspath_realpath_trailing = trailingslashit( $abspath_realpath ); - + foreach ( $files as $file ) { // wp-content should be considered user data if ( 0 === stripos( $file, 'wp-content' ) ) { @@ -1727,17 +1727,17 @@ private function remove_old_files_from_list( $files ) { // Validate the path is within ABSPATH $file_realpath = realpath( $file_path ); - if ( false !== $file_realpath ) { - if ( 0 !== strpos( $file_realpath, $abspath_realpath_trailing ) ) { - WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' ); - continue; - } - } else { + if ( false === $file_realpath ) { // Skip files with invalid paths WP_CLI::debug( "Skipping file with invalid path: {$file}", 'core' ); continue; } + if ( 0 !== strpos( $file_realpath, $abspath_realpath_trailing ) ) { + WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' ); + continue; + } + // Handle both files and directories if ( file_exists( $file_path ) ) { if ( is_dir( $file_path ) ) { From 831e197f6a2ba037ce2c978170c38132324c0602 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 10 Nov 2025 15:54:29 +0100 Subject: [PATCH 15/15] Lint & test fixes --- src/Core_Command.php | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 9d532e58..84dca1f8 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -1715,7 +1715,7 @@ private function remove_old_files_from_list( $files ) { WP_CLI::debug( 'Failed to resolve ABSPATH realpath', 'core' ); return $count; } - $abspath_realpath_trailing = trailingslashit( $abspath_realpath ); + $abspath_realpath_trailing = Utils\trailingslashit( $abspath_realpath ); foreach ( $files as $file ) { // wp-content should be considered user data @@ -1748,14 +1748,11 @@ private function remove_old_files_from_list( $files ) { } else { WP_CLI::debug( "Failed to remove directory: {$file}", 'core' ); } + } elseif ( unlink( $file_path ) ) { + WP_CLI::log( "File removed: {$file}" ); + ++$count; } else { - // Remove file - if ( unlink( $file_path ) ) { - WP_CLI::log( "File removed: {$file}" ); - ++$count; - } else { - WP_CLI::debug( "Failed to remove file: {$file}", 'core' ); - } + WP_CLI::debug( "Failed to remove file: {$file}", 'core' ); } } } @@ -1770,14 +1767,14 @@ private function remove_old_files_from_list( $files ) { * @return bool True on success, false on failure. */ private function remove_directory( $dir ) { - $dir_realpath = realpath( $dir ); + $dir_realpath = realpath( $dir ); $abspath_realpath = realpath( ABSPATH ); if ( false === $dir_realpath || false === $abspath_realpath ) { WP_CLI::debug( "Failed to resolve realpath for directory or ABSPATH: {$dir}", 'core' ); return false; } // Normalize paths with trailing slashes for accurate comparison - if ( 0 !== strpos( $dir_realpath, trailingslashit( $abspath_realpath ) ) ) { + if ( 0 !== strpos( $dir_realpath, Utils\trailingslashit( $abspath_realpath ) ) ) { WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' ); return false; } @@ -1812,11 +1809,9 @@ private function remove_directory( $dir ) { WP_CLI::debug( "Failed to remove subdirectory: {$path}", 'core' ); return false; } - } else { - if ( ! unlink( $path ) ) { + } elseif ( ! unlink( $path ) ) { WP_CLI::debug( "Failed to remove file in directory: {$path}", 'core' ); return false; - } } }