diff --git a/CHANGELOG.md b/CHANGELOG.md index 0836c3e8e..d588ae7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 3.0.2 - 2022-01-30 + +### Fixes + +* [FTP] Support relative or empty connection root directories (#1410) + ## 3.0.1 - 2022-01-15 ### Fixes @@ -20,6 +26,12 @@ * FilesystemAdapter::directoryExists to check for directory existence * FilesystemAdapter::fileExists to check for file existence +## 2.4.1 - 2022-01-30 + +### Fixed + +- [FTP] Fix relative connection root handling + ## 2.4.0 - 2022-01-04 ### Added diff --git a/composer.json b/composer.json index 6f157dfbf..14aa162ec 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "require-dev": { "ext-zip": "*", "ext-fileinfo": "*", + "ext-ftp": "*", "microsoft/azure-storage-blob": "^1.1", "phpunit/phpunit": "^9.5.11", "phpstan/phpstan": "^0.12.26", 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 b6fe24edc..2fc288c21 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, @@ -88,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(); } @@ -111,6 +117,8 @@ private function connection() start: if ( ! $this->hasFtpConnection()) { $this->connection = $this->connectionProvider->createConnection($this->connectionOptions); + + return $this->connection; } if ($this->connectivityChecker->isConnected($this->connection) === false) { @@ -118,7 +126,7 @@ private function connection() goto start; } - ftp_chdir($this->connection, $this->connectionOptions->root()); + ftp_chdir($this->connection, $this->rootDirectory); return $this->connection; } @@ -434,15 +442,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 ); } @@ -585,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) { @@ -629,4 +637,18 @@ public function directoryExists(string $path): bool return @ftp_chdir($connection, $path) === true; } + + /** + * @param resource|\FTP\Connection $connection + */ + private function resolveConnectionRoot($connection): string + { + $root = $this->connectionOptions->root(); + + if ($root !== '') { + ftp_chdir($connection, $root); + } + + return ftp_pwd($connection); + } } diff --git a/src/Ftp/FtpAdapterTestCase.php b/src/Ftp/FtpAdapterTestCase.php index 3308e98b4..0e5438248 100644 --- a/src/Ftp/FtpAdapterTestCase.php +++ b/src/Ftp/FtpAdapterTestCase.php @@ -42,6 +42,30 @@ 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('dirname1/dirname2/path.txt', 'contents', new Config()); + $adapter->write('dirname1/dirname2/path.txt', 'contents', new Config()); + + $this->assertTrue($adapter->fileExists('dirname1/dirname2/path.txt')); + $this->assertSame('contents', $adapter->read('dirname1/dirname2/path.txt')); + }); + } + /** * @test */