From 9ef8f594133467bbdfff53aed42bd7312d3b0c8b Mon Sep 17 00:00:00 2001 From: tinect Date: Tue, 26 Dec 2023 22:28:56 +0100 Subject: [PATCH 01/11] feat: add test cases to ensure error when deleting entire storage and deleting directory by delete method instead of deleteDirectory --- .../FilesystemAdapterTestCase.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php index 5361d30f4..c4bad0879 100644 --- a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php +++ b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php @@ -4,6 +4,7 @@ namespace League\Flysystem\AdapterTestUtilities; +use League\Flysystem\UnableToDeleteFile; use const PHP_EOL; use DateInterval; use DateTimeImmutable; @@ -871,4 +872,50 @@ public function cannot_get_checksum_for_directory(): void $adapter->checksum('dir', new Config()); } + + /** + * @test + */ + public function cannot_delete_directory_over_delete(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + + $adapter->write( + 'test/text.txt', + 'contents', + new Config() + ); + + $this->assertTrue($adapter->fileExists('test/text.txt')); + + $this->expectException(UnableToDeleteFile::class); + $adapter->delete('test/'); + + $this->assertTrue($adapter->fileExists('test/text.txt')); + }); + } + + /** + * @test + */ + public function cannot_delete_with_empty_path(): void + { + $this->runScenario(function () { + $adapter = $this->adapter(); + + $adapter->write( + 'test/text.txt', + 'contents', + new Config() + ); + + $this->assertTrue($adapter->fileExists('test/text.txt')); + + $this->expectException(UnableToDeleteFile::class); + $adapter->delete(''); + + $this->assertTrue($adapter->fileExists('test/text.txt')); + }); + } } From b19d5745a7813d47e4da11e3f16abb81507a60d4 Mon Sep 17 00:00:00 2001 From: tinect Date: Tue, 26 Dec 2023 22:50:41 +0100 Subject: [PATCH 02/11] chore: update sabre/dav --- composer.json | 2 +- src/WebDAV/UrlPrefixingClientStub.php | 2 +- src/WebDAV/composer.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c2f8f800a..8f1e3b733 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "google/cloud-storage": "^1.23", "async-aws/s3": "^1.5 || ^2.0", "async-aws/simple-s3": "^1.1 || ^2.0", - "sabre/dav": "^4.3.1" + "sabre/dav": "^4.6.0" }, "conflict": { "async-aws/core": "<1.19.0", diff --git a/src/WebDAV/UrlPrefixingClientStub.php b/src/WebDAV/UrlPrefixingClientStub.php index 970a45686..635b9c5dd 100644 --- a/src/WebDAV/UrlPrefixingClientStub.php +++ b/src/WebDAV/UrlPrefixingClientStub.php @@ -7,7 +7,7 @@ class UrlPrefixingClientStub extends Client { - public function propFind($url, array $properties, $depth = 0) + public function propFind($url, array $properties, $depth = 0): array { $response = parent::propFind($url, $properties, $depth); diff --git a/src/WebDAV/composer.json b/src/WebDAV/composer.json index b1559eb80..da83b9397 100644 --- a/src/WebDAV/composer.json +++ b/src/WebDAV/composer.json @@ -10,7 +10,7 @@ "require": { "php": "^8.0.2", "league/flysystem": "^3.6.0", - "sabre/dav": "^4.3.1" + "sabre/dav": "^4.6.0" }, "license": "MIT", "authors": [ From 220a9483a5915e3bfd54eaf99b20de386a962bbf Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 00:24:47 +0100 Subject: [PATCH 03/11] feat: [FTP] prevent FTP Adapter from deleting directories when using deleteFile --- src/Ftp/FtpAdapter.php | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 079dd010a..49763a06f 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -213,10 +213,25 @@ public function delete(string $path): void */ private function deleteFile(string $path, $connection): void { + if (empty($path)) { + throw UnableToDeleteFile::atLocation($path); + } + + $fileExists = $this->fileExists($path); + + if ($fileExists === false) { + if ($this->directoryExists($path)) { + throw UnableToDeleteFile::atLocation($path); + } + + return; + } + $location = $this->prefixer()->prefixPath($path); + $success = @ftp_delete($connection, $location); - if ($success === false && ftp_size($connection, $location) !== -1) { + if ($success === false && $this->fileExists($path)) { throw UnableToDeleteFile::atLocation($path, 'the file still exists'); } } @@ -275,7 +290,7 @@ private function fetchMetadata(string $path, string $type): FileAttributes $object = @ftp_raw($this->connection(), 'STAT ' . $location); - if (empty($object) || count($object) < 3 || substr($object[1], 0, 5) === "ftpd:") { + if (empty($object) || count($object) < 3 || str_starts_with($object[1], "ftpd:")) { throw UnableToRetrieveMetadata::create($path, $type, error_get_last()['message'] ?? ''); } @@ -450,7 +465,7 @@ private function normalizeUnixObject(string $item, string $base): StorageAttribu private function listingItemIsDirectory(string $permissions): bool { - return substr($permissions, 0, 1) === 'd'; + return str_starts_with($permissions, 'd'); } private function normalizeUnixTimestamp(string $month, string $day, string $timeOrYear): int @@ -459,14 +474,12 @@ private function normalizeUnixTimestamp(string $month, string $day, string $time $year = $timeOrYear; $hour = '00'; $minute = '00'; - $seconds = '00'; } else { $year = date('Y'); [$hour, $minute] = explode(':', $timeOrYear); - $seconds = '00'; } - $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:{$seconds}"); + $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:00"); return $dateTime->getTimestamp(); } @@ -484,7 +497,7 @@ private function normalizePermissions(string $permissions): int $parts = str_split($permissions, 3); // convert the groups - $mapper = function ($part) { + $mapper = static function ($part) { return array_sum(str_split($part)); }; @@ -492,11 +505,6 @@ private function normalizePermissions(string $permissions): int return octdec(implode('', array_map($mapper, $parts))); } - /** - * @inheritdoc - * - * @param string $directory - */ private function listDirectoryContentsRecursive(string $directory): Generator { $location = $this->prefixer()->prefixPath($directory); @@ -583,9 +591,6 @@ private function ensureParentDirectoryExists(string $path, ?string $visibility): $this->ensureDirectoryExists($dirname, $visibility); } - /** - * @param string $dirname - */ private function ensureDirectoryExists(string $dirname, ?string $visibility): void { $connection = $this->connection(); From 5ca08a77dab4a6c834a442596fa2825cf115c087 Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 00:25:07 +0100 Subject: [PATCH 04/11] feat: [FTP] ensure prefixer in method directoryExists --- src/Ftp/FtpAdapter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 49763a06f..3f696c954 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -639,9 +639,10 @@ private function hasFtpConnection(): bool public function directoryExists(string $path): bool { + $location = $this->prefixer()->prefixPath($path); $connection = $this->connection(); - return @ftp_chdir($connection, $path) === true; + return @ftp_chdir($connection, $location) === true; } /** From d76b34045fed391658a0b29716741ebb01497be0 Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 00:36:00 +0100 Subject: [PATCH 05/11] style: [InMemory] update method usages to PHP 8.0 --- src/InMemory/InMemoryFilesystemAdapter.php | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/InMemory/InMemoryFilesystemAdapter.php b/src/InMemory/InMemoryFilesystemAdapter.php index 76de6ad9f..4a6a51d95 100644 --- a/src/InMemory/InMemoryFilesystemAdapter.php +++ b/src/InMemory/InMemoryFilesystemAdapter.php @@ -23,7 +23,7 @@ class InMemoryFilesystemAdapter implements FilesystemAdapter { - const DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST = '______DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST'; + public const DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST = '______DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST'; /** * @var InMemoryFile[] @@ -85,14 +85,14 @@ public function delete(string $path): void unset($this->files[$this->preparePath($path)]); } - public function deleteDirectory(string $prefix): void + public function deleteDirectory(string $path): void { - $prefix = $this->preparePath($prefix); + $prefix = $this->preparePath($path); $prefix = rtrim($prefix, '/') . '/'; - foreach (array_keys($this->files) as $path) { - if (strpos($path, $prefix) === 0) { - unset($this->files[$path]); + foreach (array_keys($this->files) as $filePath) { + if (str_starts_with($filePath, $prefix)) { + unset($this->files[$filePath]); } } } @@ -108,8 +108,8 @@ public function directoryExists(string $path): bool $prefix = $this->preparePath($path); $prefix = rtrim($prefix, '/') . '/'; - foreach (array_keys($this->files) as $path) { - if (strpos($path, $prefix) === 0) { + foreach (array_keys($this->files) as $filePath) { + if (str_starts_with($filePath, $prefix)) { return true; } } @@ -184,9 +184,9 @@ public function listContents(string $path, bool $deep): iterable $prefixLength = strlen($prefix); $listedDirectories = []; - foreach ($this->files as $path => $file) { - if (substr($path, 0, $prefixLength) === $prefix) { - $subPath = substr($path, $prefixLength); + foreach ($this->files as $filePath => $file) { + if (str_starts_with($filePath, $prefix)) { + $subPath = substr($filePath, $prefixLength); $dirname = dirname($subPath); if ($dirname !== '.') { @@ -208,12 +208,12 @@ public function listContents(string $path, bool $deep): iterable } $dummyFilename = self::DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST; - if (substr($path, -strlen($dummyFilename)) === $dummyFilename) { + if (str_ends_with($filePath, $dummyFilename)) { continue; } - if ($deep === true || strpos($subPath, '/') === false) { - yield new FileAttributes(ltrim($path, '/'), $file->fileSize(), $file->visibility(), $file->lastModified(), $file->mimeType()); + if ($deep === true || !str_contains($subPath, '/')) { + yield new FileAttributes(ltrim($filePath, '/'), $file->fileSize(), $file->visibility(), $file->lastModified(), $file->mimeType()); } } } From e89754489bb2641c8b4e8ddf30aa9a54dbf63619 Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 00:40:06 +0100 Subject: [PATCH 06/11] feat: [InMemory] prevent InMemory Adapter from deleting directories when using delete --- src/InMemory/InMemoryFilesystemAdapter.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/InMemory/InMemoryFilesystemAdapter.php b/src/InMemory/InMemoryFilesystemAdapter.php index 4a6a51d95..fe050bb46 100644 --- a/src/InMemory/InMemoryFilesystemAdapter.php +++ b/src/InMemory/InMemoryFilesystemAdapter.php @@ -9,6 +9,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\UnableToCopyFile; +use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; @@ -82,6 +83,10 @@ public function readStream(string $path) public function delete(string $path): void { + if (empty($path) || $this->directoryExists($path)) { + throw UnableToDeleteFile::atLocation($path); + } + unset($this->files[$this->preparePath($path)]); } From cbcde2563c2a547153093930646c12dc7be57050 Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 00:53:10 +0100 Subject: [PATCH 07/11] feat: [Sftp] prevent Sftp Adapter from deleting directories when using delete --- src/PhpseclibV3/SftpAdapter.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/PhpseclibV3/SftpAdapter.php b/src/PhpseclibV3/SftpAdapter.php index b47ed0695..4761b09ab 100644 --- a/src/PhpseclibV3/SftpAdapter.php +++ b/src/PhpseclibV3/SftpAdapter.php @@ -15,6 +15,7 @@ use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; +use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; @@ -176,9 +177,24 @@ public function readStream(string $path) public function delete(string $path): void { + if (empty($path)) { + throw UnableToDeleteFile::atLocation($path); + } + + $fileExists = $this->fileExists($path); + + if ($fileExists === false) { + if ($this->directoryExists($path)) { + throw UnableToDeleteFile::atLocation($path); + } + + return; + } + $location = $this->prefixer->prefixPath($path); $connection = $this->connectionProvider->provideConnection(); - $connection->delete($location); + + $connection->delete($location, false); } public function deleteDirectory(string $path): void From ccf54b9bdb2e7ad115d97354eefab89527a023df Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 01:23:10 +0100 Subject: [PATCH 08/11] feat: [WebDAV] prevent WebDAV Adapter from deleting directories when using delete --- src/WebDAV/WebDAVAdapter.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/WebDAV/WebDAVAdapter.php b/src/WebDAV/WebDAVAdapter.php index cde5a7c39..e97edf7f7 100644 --- a/src/WebDAV/WebDAVAdapter.php +++ b/src/WebDAV/WebDAVAdapter.php @@ -176,6 +176,16 @@ public function readStream(string $path) public function delete(string $path): void { + $fileExists = $this->fileExists($path); + + if ($fileExists === false) { + if ($this->directoryExists($path)) { + throw UnableToDeleteFile::atLocation($path); + } + + return; + } + $location = $this->encodePath($this->prefixer->prefixPath($path)); try { From 2c26cf9759d1ca59195bc1632b28044273835cb2 Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 01:36:43 +0100 Subject: [PATCH 09/11] feat: [ZipArchive] prevent ZipArchive Adapter from deleting directories when using delete --- src/ZipArchive/ZipArchiveAdapter.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ZipArchive/ZipArchiveAdapter.php b/src/ZipArchive/ZipArchiveAdapter.php index c73ca55cc..a31d53797 100644 --- a/src/ZipArchive/ZipArchiveAdapter.php +++ b/src/ZipArchive/ZipArchiveAdapter.php @@ -132,6 +132,20 @@ public function readStream(string $path) public function delete(string $path): void { + if (empty($path) || \str_ends_with($path, '/')) { + throw UnableToDeleteFile::atLocation($path); + } + + $fileExists = $this->fileExists($path); + + if ($fileExists === false) { + if ($this->directoryExists($path)) { + throw UnableToDeleteFile::atLocation($path); + } + + return; + } + $prefixedPath = $this->pathPrefixer->prefixPath($path); $zipArchive = $this->zipArchiveProvider->createZipArchive(); $success = $zipArchive->locateName($prefixedPath) === false || $zipArchive->deleteName($prefixedPath); @@ -155,7 +169,7 @@ public function deleteDirectory(string $path): void $itemPath = $stats['name']; - if (strpos($itemPath, $prefixedPath) !== 0) { + if (!str_starts_with($itemPath, $prefixedPath)) { continue; } From 2b30c73d2dfb4cf71eef4b1a8ceb7d0acd1952aa Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 01:41:09 +0100 Subject: [PATCH 10/11] feat: specify missing type in method propFind of UrlPrefixingClientStub --- src/WebDAV/UrlPrefixingClientStub.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/WebDAV/UrlPrefixingClientStub.php b/src/WebDAV/UrlPrefixingClientStub.php index 635b9c5dd..ffda84c4f 100644 --- a/src/WebDAV/UrlPrefixingClientStub.php +++ b/src/WebDAV/UrlPrefixingClientStub.php @@ -7,6 +7,9 @@ class UrlPrefixingClientStub extends Client { + /** + * @param string $url + */ public function propFind($url, array $properties, $depth = 0): array { $response = parent::propFind($url, $properties, $depth); From 9bbe2c68dad3f02c2c3d529f9200bdcec23e7f9d Mon Sep 17 00:00:00 2001 From: tinect Date: Wed, 27 Dec 2023 01:43:33 +0100 Subject: [PATCH 11/11] style: apply codestyle --- src/AdapterTestUtilities/FilesystemAdapterTestCase.php | 2 +- src/InMemory/InMemoryFilesystemAdapter.php | 3 +-- src/ZipArchive/ZipArchiveAdapter.php | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php index c4bad0879..927874d04 100644 --- a/src/AdapterTestUtilities/FilesystemAdapterTestCase.php +++ b/src/AdapterTestUtilities/FilesystemAdapterTestCase.php @@ -4,7 +4,6 @@ namespace League\Flysystem\AdapterTestUtilities; -use League\Flysystem\UnableToDeleteFile; use const PHP_EOL; use DateInterval; use DateTimeImmutable; @@ -15,6 +14,7 @@ use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToProvideChecksum; use League\Flysystem\UnableToReadFile; diff --git a/src/InMemory/InMemoryFilesystemAdapter.php b/src/InMemory/InMemoryFilesystemAdapter.php index fe050bb46..a7389b6db 100644 --- a/src/InMemory/InMemoryFilesystemAdapter.php +++ b/src/InMemory/InMemoryFilesystemAdapter.php @@ -20,7 +20,6 @@ use function array_keys; use function rtrim; -use function strpos; class InMemoryFilesystemAdapter implements FilesystemAdapter { @@ -217,7 +216,7 @@ public function listContents(string $path, bool $deep): iterable continue; } - if ($deep === true || !str_contains($subPath, '/')) { + if ($deep === true || ! str_contains($subPath, '/')) { yield new FileAttributes(ltrim($filePath, '/'), $file->fileSize(), $file->visibility(), $file->lastModified(), $file->mimeType()); } } diff --git a/src/ZipArchive/ZipArchiveAdapter.php b/src/ZipArchive/ZipArchiveAdapter.php index a31d53797..52114ba37 100644 --- a/src/ZipArchive/ZipArchiveAdapter.php +++ b/src/ZipArchive/ZipArchiveAdapter.php @@ -169,7 +169,7 @@ public function deleteDirectory(string $path): void $itemPath = $stats['name']; - if (!str_starts_with($itemPath, $prefixedPath)) { + if ( ! str_starts_with($itemPath, $prefixedPath)) { continue; }