diff options
Diffstat (limited to 'apps/files_external/lib/Lib/Storage/FtpConnection.php')
-rw-r--r-- | apps/files_external/lib/Lib/Storage/FtpConnection.php | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/apps/files_external/lib/Lib/Storage/FtpConnection.php b/apps/files_external/lib/Lib/Storage/FtpConnection.php new file mode 100644 index 00000000000..a064bf9b100 --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/FtpConnection.php @@ -0,0 +1,222 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_External\Lib\Storage; + +/** + * Low level wrapper around the ftp functions that smooths over some difference between servers + */ +class FtpConnection { + private \FTP\Connection $connection; + + public function __construct(bool $secure, string $hostname, int $port, string $username, string $password) { + if ($secure) { + $connection = ftp_ssl_connect($hostname, $port); + } else { + $connection = ftp_connect($hostname, $port); + } + + if ($connection === false) { + throw new \Exception('Failed to connect to ftp'); + } + + if (ftp_login($connection, $username, $password) === false) { + throw new \Exception('Failed to connect to login to ftp'); + } + + ftp_pasv($connection, true); + $this->connection = $connection; + } + + public function __destruct() { + ftp_close($this->connection); + } + + public function setUtf8Mode(): bool { + $response = ftp_raw($this->connection, 'OPTS UTF8 ON'); + return substr($response[0], 0, 3) === '200'; + } + + public function fput(string $path, $handle) { + return @ftp_fput($this->connection, $path, $handle, FTP_BINARY); + } + + public function fget($handle, string $path) { + return @ftp_fget($this->connection, $handle, $path, FTP_BINARY); + } + + public function mkdir(string $path) { + return @ftp_mkdir($this->connection, $path); + } + + public function chdir(string $path) { + return @ftp_chdir($this->connection, $path); + } + + public function delete(string $path) { + return @ftp_delete($this->connection, $path); + } + + public function rmdir(string $path) { + return @ftp_rmdir($this->connection, $path); + } + + public function rename(string $source, string $target) { + return @ftp_rename($this->connection, $source, $target); + } + + public function mdtm(string $path): int { + $result = @ftp_mdtm($this->connection, $path); + + // filezilla doesn't like empty path with mdtm + if ($result === -1 && $path === '') { + $result = @ftp_mdtm($this->connection, '/'); + } + return $result; + } + + public function size(string $path) { + return @ftp_size($this->connection, $path); + } + + public function systype() { + return @ftp_systype($this->connection); + } + + public function nlist(string $path) { + $files = @ftp_nlist($this->connection, $path); + return array_map(function ($name) { + if (str_contains($name, '/')) { + $name = basename($name); + } + return $name; + }, $files); + } + + public function mlsd(string $path) { + $files = @ftp_mlsd($this->connection, $path); + + if ($files !== false) { + return array_map(function ($file) { + if (str_contains($file['name'], '/')) { + $file['name'] = basename($file['name']); + } + return $file; + }, $files); + } else { + // not all servers support mlsd, in those cases we parse the raw list ourselves + $rawList = @ftp_rawlist($this->connection, '-aln ' . $path); + if ($rawList === false) { + return false; + } + return $this->parseRawList($rawList, $path); + } + } + + // rawlist parsing logic is based on the ftp implementation from https://github.com/thephpleague/flysystem + private function parseRawList(array $rawList, string $directory): array { + return array_map(function ($item) use ($directory) { + return $this->parseRawListItem($item, $directory); + }, $rawList); + } + + private function parseRawListItem(string $item, string $directory): array { + $isWindows = preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item); + + return $isWindows ? $this->parseWindowsItem($item, $directory) : $this->parseUnixItem($item, $directory); + } + + private function parseUnixItem(string $item, string $directory): array { + $item = preg_replace('#\s+#', ' ', $item, 7); + + if (count(explode(' ', $item, 9)) !== 9) { + throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + [$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $time, $name] = explode(' ', $item, 9); + if ($name === '.') { + $type = 'cdir'; + } elseif ($name === '..') { + $type = 'pdir'; + } else { + $type = substr($permissions, 0, 1) === 'd' ? 'dir' : 'file'; + } + + $parsedDate = (new \DateTime()) + ->setTimestamp(strtotime("$month $day $time")); + $tomorrow = (new \DateTime())->add(new \DateInterval('P1D')); + + // since the provided date doesn't include the year, we either set it to the correct year + // or when the date would otherwise be in the future (by more then 1 day to account for timezone errors) + // we use last year + if ($parsedDate > $tomorrow) { + $parsedDate = $parsedDate->sub(new \DateInterval('P1Y')); + } + + $formattedDate = $parsedDate + ->format('YmdHis'); + + return [ + 'type' => $type, + 'name' => $name, + 'modify' => $formattedDate, + 'perm' => $this->normalizePermissions($permissions), + 'size' => (int)$size, + ]; + } + + private function normalizePermissions(string $permissions) { + $isDir = substr($permissions, 0, 1) === 'd'; + // remove the type identifier and only use owner permissions + $permissions = substr($permissions, 1, 4); + + // map the string rights to the ftp counterparts + $filePermissionsMap = ['r' => 'r', 'w' => 'fadfw']; + $dirPermissionsMap = ['r' => 'e', 'w' => 'flcdmp']; + + $map = $isDir ? $dirPermissionsMap : $filePermissionsMap; + + return array_reduce(str_split($permissions), function ($ftpPermissions, $permission) use ($map) { + if (isset($map[$permission])) { + $ftpPermissions .= $map[$permission]; + } + return $ftpPermissions; + }, ''); + } + + private function parseWindowsItem(string $item, string $directory): array { + $item = preg_replace('#\s+#', ' ', trim($item), 3); + + if (count(explode(' ', $item, 4)) !== 4) { + throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts."); + } + + [$date, $time, $size, $name] = explode(' ', $item, 4); + + // Check for the correct date/time format + $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i'; + $formattedDate = \DateTime::createFromFormat($format, $date . $time)->format('YmdGis'); + + if ($name === '.') { + $type = 'cdir'; + } elseif ($name === '..') { + $type = 'pdir'; + } else { + $type = ($size === '<DIR>') ? 'dir' : 'file'; + } + + return [ + 'type' => $type, + 'name' => $name, + 'modify' => $formattedDate, + 'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe', + 'size' => (int)$size, + ]; + } +} |