From 19ce0cb585e6b57f5ff15b97d20f8ebae44a0ab8 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 26 Jan 2022 17:07:15 +0200 Subject: [PATCH 01/16] add empty root test --- src/Ftp/FtpAdapterTestCase.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Ftp/FtpAdapterTestCase.php b/src/Ftp/FtpAdapterTestCase.php index 3308e98b4..fc58b140f 100644 --- a/src/Ftp/FtpAdapterTestCase.php +++ b/src/Ftp/FtpAdapterTestCase.php @@ -42,6 +42,32 @@ public function resetFunctionMocks(): void reset_function_mocks(); } + /** + * @test + */ + public function using_empty_string_for_root(): void + { + $options = FtpConnectionOptions::fromArray([ + 'host' => 'localhost', + 'port' => 2121, + 'root' => '', + 'username' => 'foo', + 'password' => 'pass', + ]); + + $this->runScenario(function () use ($options) { + $adapter = new FtpAdapter($options); + + $adapter->write('dirname/path.txt', 'contents', new Config([ + Config::OPTION_VISIBILITY => Visibility::PUBLIC, + Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC + ])); + + $this->assertTrue($adapter->fileExists('dirname/path.txt')); + $this->assertSame('contents', $adapter->read('dirname/path.txt')); + }); + } + /** * @test */ From 386bf427cb8c8746b16d15743b8d38999853ca97 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Wed, 26 Jan 2022 22:57:25 +0100 Subject: [PATCH 02/16] Allow empty connection root for FTP connections. --- composer.json | 1 + src/Ftp/FtpAdapter.php | 31 ++++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 073340f39..f60385e1c 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "require-dev": { "ext-zip": "*", "ext-fileinfo": "*", + "ext-ftp": "*", "phpunit/phpunit": "^9.5.11", "phpstan/phpstan": "^0.12.26", "phpseclib/phpseclib": "^2.0", diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index b6fe24edc..dc3584847 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -27,6 +27,9 @@ use League\MimeTypeDetection\MimeTypeDetector; use Throwable; +use function ftp_chdir; +use function ftp_pwd; + class FtpAdapter implements FilesystemAdapter { private const SYSTEM_TYPE_WINDOWS = 'windows'; @@ -77,6 +80,8 @@ class FtpAdapter implements FilesystemAdapter */ private $mimeTypeDetector; + private ?string $rootDirectory = null; + public function __construct( FtpConnectionOptions $connectionOptions, FtpConnectionProvider $connectionProvider = null, @@ -111,6 +116,7 @@ private function connection() start: if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); + $this->rootDirectory = $this->resolveConnectionRoot($this->connection); } if ($this->connectivityChecker->isConnected($this->connection) === false) { @@ -118,7 +124,7 @@ private function connection() goto start; } - ftp_chdir($this->connection, $this->connectionOptions->root()); + ftp_chdir($this->connection, $this->rootDirectory); return $this->connection; } @@ -434,15 +440,15 @@ private function normalizeUnixObject(string $item, string $base): StorageAttribu $isDirectory = $this->listingItemIsDirectory($permissions); $permissions = $this->normalizePermissions($permissions); $path = $base === '' ? $name : rtrim($base, '/') . '/' . $name; - $lastModified = $this->connectionOptions->timestampsOnUnixListingsEnabled() - ? $this->normalizeUnixTimestamp($month, $day, $timeOrYear) - : null; + $lastModified = $this->connectionOptions->timestampsOnUnixListingsEnabled() ? $this->normalizeUnixTimestamp( + $month, + $day, + $timeOrYear + ) : null; if ($isDirectory) { return new DirectoryAttributes( - $path, - $this->visibilityConverter->inverseForDirectory($permissions), - $lastModified + $path, $this->visibilityConverter->inverseForDirectory($permissions), $lastModified ); } @@ -629,4 +635,15 @@ public function directoryExists(string $path): bool return @ftp_chdir($connection, $path) === true; } + + private function resolveConnectionRoot($connection): string + { + $root = $this->connectionOptions->root(); + + if ($root !== '') { + ftp_chdir($connection, $root); + } + + return ftp_pwd($connection); + } } From c9c365fe38a0003ee90fc28272c4c360a40fcc0e Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Thu, 27 Jan 2022 08:11:28 +0100 Subject: [PATCH 03/16] Immediately return connection after creating it. --- src/Ftp/FtpAdapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index dc3584847..16995af8d 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -117,6 +117,8 @@ private function connection() if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); $this->rootDirectory = $this->resolveConnectionRoot($this->connection); + + return $this->connection; } if ($this->connectivityChecker->isConnected($this->connection) === false) { From 939f054cde169bcac6af525e5bce4bf288e3cfc3 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Thu, 27 Jan 2022 09:05:02 +0100 Subject: [PATCH 04/16] Add release changelog for upcoming release --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0836c3e8e..0df138655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## UNRELEASED + +### Fixes + +* [FTP] Support relative or empty connection root directories (#1410) + ## 3.0.1 - 2022-01-15 ### Fixes From 9fcebc6b7e48d3d131900a470f78832b2f96ab83 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Thu, 27 Jan 2022 18:56:05 +0100 Subject: [PATCH 05/16] Resolve connection root for path prefixing --- src/Ftp/FtpAdapter.php | 6 +++--- src/Ftp/FtpAdapterTestCase.php | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 16995af8d..67cf60192 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -93,7 +93,8 @@ public function __construct( $this->connectionProvider = $connectionProvider ?: new FtpConnectionProvider(); $this->connectivityChecker = $connectivityChecker ?: new NoopCommandConnectivityChecker(); $this->visibilityConverter = $visibilityConverter ?: new PortableVisibilityConverter(); - $this->prefixer = new PathPrefixer($connectionOptions->root()); + $this->rootDirectory = $this->resolveConnectionRoot($this->connection()); + $this->prefixer = new PathPrefixer($this->rootDirectory); $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector(); } @@ -116,7 +117,6 @@ private function connection() start: if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); - $this->rootDirectory = $this->resolveConnectionRoot($this->connection); return $this->connection; } @@ -593,7 +593,7 @@ private function ensureDirectoryExists(string $dirname, ?string $visibility): vo $connection = $this->connection(); $dirPath = ''; - $parts = explode('/', rtrim($dirname, '/')); + $parts = explode('/', trim($dirname, '/')); $mode = $visibility ? $this->visibilityConverter->forDirectory($visibility) : false; foreach ($parts as $part) { diff --git a/src/Ftp/FtpAdapterTestCase.php b/src/Ftp/FtpAdapterTestCase.php index fc58b140f..0e5438248 100644 --- a/src/Ftp/FtpAdapterTestCase.php +++ b/src/Ftp/FtpAdapterTestCase.php @@ -58,13 +58,11 @@ public function using_empty_string_for_root(): void $this->runScenario(function () use ($options) { $adapter = new FtpAdapter($options); - $adapter->write('dirname/path.txt', 'contents', new Config([ - Config::OPTION_VISIBILITY => Visibility::PUBLIC, - Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC - ])); + $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config()); + $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config()); - $this->assertTrue($adapter->fileExists('dirname/path.txt')); - $this->assertSame('contents', $adapter->read('dirname/path.txt')); + $this->assertTrue($adapter->fileExists('dirname1/dirname2/path.txt')); + $this->assertSame('contents', $adapter->read('dirname1/dirname2/path.txt')); }); } From e280a45c115a4dfb226d9de853294d5036e18531 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 26 Jan 2022 17:07:15 +0200 Subject: [PATCH 06/16] add empty root test --- src/Ftp/FtpAdapterTestCase.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Ftp/FtpAdapterTestCase.php b/src/Ftp/FtpAdapterTestCase.php index 3308e98b4..fc58b140f 100644 --- a/src/Ftp/FtpAdapterTestCase.php +++ b/src/Ftp/FtpAdapterTestCase.php @@ -42,6 +42,32 @@ public function resetFunctionMocks(): void reset_function_mocks(); } + /** + * @test + */ + public function using_empty_string_for_root(): void + { + $options = FtpConnectionOptions::fromArray([ + 'host' => 'localhost', + 'port' => 2121, + 'root' => '', + 'username' => 'foo', + 'password' => 'pass', + ]); + + $this->runScenario(function () use ($options) { + $adapter = new FtpAdapter($options); + + $adapter->write('dirname/path.txt', 'contents', new Config([ + Config::OPTION_VISIBILITY => Visibility::PUBLIC, + Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC + ])); + + $this->assertTrue($adapter->fileExists('dirname/path.txt')); + $this->assertSame('contents', $adapter->read('dirname/path.txt')); + }); + } + /** * @test */ From c222d21f99d03bf1925111a4a696fc401ab9b25d Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Wed, 26 Jan 2022 22:57:25 +0100 Subject: [PATCH 07/16] Allow empty connection root for FTP connections. --- composer.json | 1 + src/Ftp/FtpAdapter.php | 31 ++++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index be532f613..9c244f8c0 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ }, "require-dev": { "ext-fileinfo": "*", + "ext-ftp": "*", "phpunit/phpunit": "^8.5 || ^9.4", "phpstan/phpstan": "^0.12.26", "phpseclib/phpseclib": "^2.0", diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index a32922a5e..304417fb8 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -27,6 +27,9 @@ use League\MimeTypeDetection\MimeTypeDetector; use Throwable; +use function ftp_chdir; +use function ftp_pwd; + class FtpAdapter implements FilesystemAdapter { private const SYSTEM_TYPE_WINDOWS = 'windows'; @@ -77,6 +80,8 @@ class FtpAdapter implements FilesystemAdapter */ private $mimeTypeDetector; + private ?string $rootDirectory = null; + public function __construct( FtpConnectionOptions $connectionOptions, FtpConnectionProvider $connectionProvider = null, @@ -111,6 +116,7 @@ private function connection() start: if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); + $this->rootDirectory = $this->resolveConnectionRoot($this->connection); } if ($this->connectivityChecker->isConnected($this->connection) === false) { @@ -118,7 +124,7 @@ private function connection() goto start; } - ftp_chdir($this->connection, $this->connectionOptions->root()); + ftp_chdir($this->connection, $this->rootDirectory); return $this->connection; } @@ -434,15 +440,15 @@ private function normalizeUnixObject(string $item, string $base): StorageAttribu $isDirectory = $this->listingItemIsDirectory($permissions); $permissions = $this->normalizePermissions($permissions); $path = $base === '' ? $name : rtrim($base, '/') . '/' . $name; - $lastModified = $this->connectionOptions->timestampsOnUnixListingsEnabled() - ? $this->normalizeUnixTimestamp($month, $day, $timeOrYear) - : null; + $lastModified = $this->connectionOptions->timestampsOnUnixListingsEnabled() ? $this->normalizeUnixTimestamp( + $month, + $day, + $timeOrYear + ) : null; if ($isDirectory) { return new DirectoryAttributes( - $path, - $this->visibilityConverter->inverseForDirectory($permissions), - $lastModified + $path, $this->visibilityConverter->inverseForDirectory($permissions), $lastModified ); } @@ -622,4 +628,15 @@ private function hasFtpConnection(): bool { return $this->connection instanceof \FTP\Connection || is_resource($this->connection); } + + private function resolveConnectionRoot($connection): string + { + $root = $this->connectionOptions->root(); + + if ($root !== '') { + ftp_chdir($connection, $root); + } + + return ftp_pwd($connection); + } } From 03ce85ce351af4cab995271230885be316b393b5 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sat, 29 Jan 2022 13:55:17 +0100 Subject: [PATCH 08/16] Down-grade property typing --- src/Ftp/FtpAdapter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 304417fb8..fd5d169a1 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -80,7 +80,10 @@ class FtpAdapter implements FilesystemAdapter */ private $mimeTypeDetector; - private ?string $rootDirectory = null; + /** + * @var null|string + */ + private $rootDirectory = null; public function __construct( FtpConnectionOptions $connectionOptions, From 94fd4315055d962eac225844c82cc894dd07a082 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Thu, 27 Jan 2022 08:11:28 +0100 Subject: [PATCH 09/16] Immediately return connection after creating it. --- src/Ftp/FtpAdapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index fd5d169a1..797ab0c93 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -120,6 +120,8 @@ private function connection() if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); $this->rootDirectory = $this->resolveConnectionRoot($this->connection); + + return $this->connection; } if ($this->connectivityChecker->isConnected($this->connection) === false) { From 025d50043c675d981728002c16e6ff7b3699f994 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Thu, 27 Jan 2022 18:56:05 +0100 Subject: [PATCH 10/16] Resolve connection root for path prefixing --- src/Ftp/FtpAdapter.php | 6 +++--- src/Ftp/FtpAdapterTestCase.php | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 797ab0c93..53386374e 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -96,7 +96,8 @@ public function __construct( $this->connectionProvider = $connectionProvider ?: new FtpConnectionProvider(); $this->connectivityChecker = $connectivityChecker ?: new NoopCommandConnectivityChecker(); $this->visibilityConverter = $visibilityConverter ?: new PortableVisibilityConverter(); - $this->prefixer = new PathPrefixer($connectionOptions->root()); + $this->rootDirectory = $this->resolveConnectionRoot($this->connection()); + $this->prefixer = new PathPrefixer($this->rootDirectory); $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector(); } @@ -119,7 +120,6 @@ private function connection() start: if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); - $this->rootDirectory = $this->resolveConnectionRoot($this->connection); return $this->connection; } @@ -596,7 +596,7 @@ private function ensureDirectoryExists(string $dirname, ?string $visibility): vo $connection = $this->connection(); $dirPath = ''; - $parts = explode('/', rtrim($dirname, '/')); + $parts = explode('/', trim($dirname, '/')); $mode = $visibility ? $this->visibilityConverter->forDirectory($visibility) : false; foreach ($parts as $part) { diff --git a/src/Ftp/FtpAdapterTestCase.php b/src/Ftp/FtpAdapterTestCase.php index fc58b140f..0e5438248 100644 --- a/src/Ftp/FtpAdapterTestCase.php +++ b/src/Ftp/FtpAdapterTestCase.php @@ -58,13 +58,11 @@ public function using_empty_string_for_root(): void $this->runScenario(function () use ($options) { $adapter = new FtpAdapter($options); - $adapter->write('dirname/path.txt', 'contents', new Config([ - Config::OPTION_VISIBILITY => Visibility::PUBLIC, - Config::OPTION_DIRECTORY_VISIBILITY => Visibility::PUBLIC - ])); + $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config()); + $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config()); - $this->assertTrue($adapter->fileExists('dirname/path.txt')); - $this->assertSame('contents', $adapter->read('dirname/path.txt')); + $this->assertTrue($adapter->fileExists('dirname1/dirname2/path.txt')); + $this->assertSame('contents', $adapter->read('dirname1/dirname2/path.txt')); }); } From d4fc101eb31e910e035db62dbf1d040934e895f5 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sat, 29 Jan 2022 13:57:16 +0100 Subject: [PATCH 11/16] Prepare changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da16588a4..aaa27fa82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Version 2.x Changelog +## 2.4.1 - 2022-01-29 + +### Added + +- [FTP] Fix relative connection root handling + ## 2.4.0 - 2022-01-04 ### Added From dc3042dc463dca3537699b55ae05179371dff3d3 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sat, 29 Jan 2022 14:01:34 +0100 Subject: [PATCH 12/16] Added typehint --- src/Ftp/FtpAdapter.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 53386374e..12a2c7d37 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -634,6 +634,9 @@ private function hasFtpConnection(): bool return $this->connection instanceof \FTP\Connection || is_resource($this->connection); } + /** + * @param resource|\FTP\Connection$connection + */ private function resolveConnectionRoot($connection): string { $root = $this->connectionOptions->root(); From 40c7e7335578c531eef68de5b1bd94f6182cab31 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sat, 29 Jan 2022 22:31:43 +0100 Subject: [PATCH 13/16] Ignore missing ftp connection class --- phpstan.neon | 1 + src/Ftp/FtpAdapter.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index c429a819e..fe58406ba 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,6 +16,7 @@ parameters: - src/PhpseclibV2 - src/PhpseclibV3 ignoreErrors: + - '#invalid typehint type FTP\\Connection#' - '#FTP\\Connection not found#' - '#unknown class FTP\\Connection#' - '#Call to function iterator_to_array\(\) on a separate line has no effect\.#' diff --git a/src/Ftp/FtpAdapter.php b/src/Ftp/FtpAdapter.php index 12a2c7d37..7f2549451 100644 --- a/src/Ftp/FtpAdapter.php +++ b/src/Ftp/FtpAdapter.php @@ -635,7 +635,7 @@ private function hasFtpConnection(): bool } /** - * @param resource|\FTP\Connection$connection + * @param resource|\FTP\Connection $connection */ private function resolveConnectionRoot($connection): string { From 88cf6a53c0cf63820ea3b21681576421591dbfad Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sun, 30 Jan 2022 14:27:10 +0100 Subject: [PATCH 14/16] Corrected release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaa27fa82..e853ee536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Version 2.x Changelog -## 2.4.1 - 2022-01-29 +## 2.4.1 - 2022-01-30 ### Added From 21be6aefb1f5beb9ea6a27b86e1d5ed65b655a8d Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sun, 30 Jan 2022 14:36:35 +0100 Subject: [PATCH 15/16] Prepare for release. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8677499..39ade1bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## UNRELEASED +## 3.0.2 - 2022-01-30 ### Fixes From d4eedc1b14d04b09d1df00fcb4c2da7b530ec1e8 Mon Sep 17 00:00:00 2001 From: Frank de Jonge Date: Sun, 30 Jan 2022 14:51:07 +0100 Subject: [PATCH 16/16] Corrected changelog header --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ade1bd6..d588ae7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ ## 2.4.1 - 2022-01-30 -### Added +### Fixed - [FTP] Fix relative connection root handling