From 6163a28e5906f7d4d7d723a67c87124473e08fea Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Wed, 5 Aug 2026 12:41:42 +0530 Subject: [PATCH] Upgrade: Handle broken symlinks during file copies --- .../includes/class-wp-filesystem-direct.php | 11 ++++++-- .../includes/class-wp-filesystem-ftpext.php | 7 +++++ .../class-wp-filesystem-ftpsockets.php | 7 +++++ .../includes/class-wp-filesystem-ssh2.php | 15 +++++++++-- .../filesystem/wpFilesystemDirect/copy.php | 27 +++++++++++++++++++ 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/includes/class-wp-filesystem-direct.php b/src/wp-admin/includes/class-wp-filesystem-direct.php index 33aa14ce47cb9..bb726ba007362 100644 --- a/src/wp-admin/includes/class-wp-filesystem-direct.php +++ b/src/wp-admin/includes/class-wp-filesystem-direct.php @@ -334,6 +334,13 @@ public function copy( $source, $destination, $overwrite = false, $mode = false ) return false; } + if ( $source === $destination ) { + return false; + } + + if ( $overwrite && $this->exists( $destination ) && ! $this->delete( $destination ) ) { + return false; + } $rtval = copy( $source, $destination ); if ( $mode ) { @@ -444,7 +451,7 @@ public function delete( $file, $recursive = false, $type = false ) { * @return bool Whether $path exists or not. */ public function exists( $path ) { - return @file_exists( $path ); + return @file_exists( $path ) || @is_link( $path ); } /** @@ -456,7 +463,7 @@ public function exists( $path ) { * @return bool Whether $file is a file. */ public function is_file( $file ) { - return @is_file( $file ); + return @is_file( $file ) || @is_link( $file ); } /** diff --git a/src/wp-admin/includes/class-wp-filesystem-ftpext.php b/src/wp-admin/includes/class-wp-filesystem-ftpext.php index 844c4e9c5b95b..6aae717d00184 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ftpext.php +++ b/src/wp-admin/includes/class-wp-filesystem-ftpext.php @@ -402,6 +402,13 @@ public function copy( $source, $destination, $overwrite = false, $mode = false ) return false; } + if ( $source === $destination ) { + return false; + } + + if ( $overwrite && $this->exists( $destination ) && ! $this->delete( $destination ) ) { + return false; + } $content = $this->get_contents( $source ); if ( false === $content ) { diff --git a/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php b/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php index 8d8d9ff02d884..f99090f3c5b96 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php +++ b/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php @@ -409,6 +409,13 @@ public function copy( $source, $destination, $overwrite = false, $mode = false ) return false; } + if ( $source === $destination ) { + return false; + } + + if ( $overwrite && $this->exists( $destination ) && ! $this->delete( $destination ) ) { + return false; + } $content = $this->get_contents( $source ); if ( false === $content ) { diff --git a/src/wp-admin/includes/class-wp-filesystem-ssh2.php b/src/wp-admin/includes/class-wp-filesystem-ssh2.php index f86b06ce0c4ce..318effb63d1b9 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ssh2.php +++ b/src/wp-admin/includes/class-wp-filesystem-ssh2.php @@ -547,6 +547,13 @@ public function copy( $source, $destination, $overwrite = false, $mode = false ) return false; } + if ( $source === $destination ) { + return false; + } + + if ( $overwrite && $this->exists( $destination ) && ! $this->delete( $destination ) ) { + return false; + } $content = $this->get_contents( $source ); if ( false === $content ) { @@ -635,7 +642,9 @@ public function delete( $file, $recursive = false, $type = false ) { * @return bool Whether $path exists or not. */ public function exists( $path ) { - return file_exists( $this->sftp_path( $path ) ); + $path = $this->sftp_path( $path ); + + return file_exists( $path ) || is_link( $path ); } /** @@ -647,7 +656,9 @@ public function exists( $path ) { * @return bool Whether $file is a file. */ public function is_file( $file ) { - return is_file( $this->sftp_path( $file ) ); + $file = $this->sftp_path( $file ); + + return is_file( $file ) || is_link( $file ); } /** diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php index d0355c20e5662..7309d07a96e45 100644 --- a/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php @@ -66,7 +66,34 @@ public function test_should_not_overwrite_an_existing_file_when_overwriting_is_d * @ticket 57774 */ public function test_should_not_overwrite_when_overwriting_is_enabled_and_source_and_destination_are_the_same() { + $source = self::$file_structure['test_dir']['path'] . 'a_file_that_exists.txt'; $this->assertFalse( self::$filesystem->copy( $source, $source, true ) ); } + + /** + * Tests that `WP_Filesystem_Direct::copy()` replaces a broken symlink when + * overwriting is enabled. + * + * @ticket 46040 + */ + public function test_should_replace_a_broken_symlink_when_overwriting_is_enabled() { + $source = self::$file_structure['visible_file']['path']; + $destination = self::$file_structure['test_dir']['path'] . '.broken_symlink'; + + if ( ! function_exists( 'symlink' ) ) { + $this->markTestSkipped( 'The environment does not support creating symlinks.' ); + } + + $created = symlink( 'does-not-exist', $destination ); + if ( ! $created ) { + $this->markTestSkipped( 'The environment does not support creating symlinks.' ); + } + + $this->assertTrue( self::$filesystem->exists( $destination ) ); + $this->assertTrue( self::$filesystem->copy( $source, $destination, true ) ); + $this->assertSame( file_get_contents( $source ), file_get_contents( $destination ) ); + + unlink( $destination ); + } }