aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_external/lib/Lib/Storage/FtpConnection.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_external/lib/Lib/Storage/FtpConnection.php')
-rw-r--r--apps/files_external/lib/Lib/Storage/FtpConnection.php222
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,
+ ];
+ }
+}