Passed
Push — master ( 5024f2...d6ff5d )
by Robin
14:15 queued 12s
created

FtpConnection::fput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2020 Robin Appelman <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\Files_External\Lib\Storage;
25
26
/**
27
 * Low level wrapper around the ftp functions that smooths over some difference between servers
28
 */
29
class FtpConnection {
30
	/** @var resource|\FTP\Connection */
0 ignored issues
show
Bug introduced by
The type FTP\Connection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
	private $connection;
32
33
	public function __construct(bool $secure, string $hostname, int $port, string $username, string $password) {
34
		if ($secure) {
35
			$connection = ftp_ssl_connect($hostname, $port);
36
		} else {
37
			$connection = ftp_connect($hostname, $port);
38
		}
39
40
		if ($connection === false) {
41
			throw new \Exception("Failed to connect to ftp");
42
		}
43
44
		if (ftp_login($connection, $username, $password) === false) {
45
			throw new \Exception("Failed to connect to login to ftp");
46
		}
47
48
		ftp_pasv($connection, true);
49
		$this->connection = $connection;
50
	}
51
52
	public function __destruct() {
53
		if ($this->connection) {
54
			ftp_close($this->connection);
55
		}
56
		$this->connection = null;
57
	}
58
59
	public function setUtf8Mode(): bool {
60
		$response = ftp_raw($this->connection, "OPTS UTF8 ON");
61
		return substr($response[0], 0, 3) === '200';
62
	}
63
64
	public function fput(string $path, $handle) {
65
		return @ftp_fput($this->connection, $path, $handle, FTP_BINARY);
66
	}
67
68
	public function fget($handle, string $path) {
69
		return @ftp_fget($this->connection, $handle, $path, FTP_BINARY);
70
	}
71
72
	public function mkdir(string $path) {
73
		return @ftp_mkdir($this->connection, $path);
74
	}
75
76
	public function chdir(string $path) {
77
		return @ftp_chdir($this->connection, $path);
78
	}
79
80
	public function delete(string $path) {
81
		return @ftp_delete($this->connection, $path);
82
	}
83
84
	public function rmdir(string $path) {
85
		return @ftp_rmdir($this->connection, $path);
86
	}
87
88
	public function rename(string $source, string $target) {
89
		return @ftp_rename($this->connection, $source, $target);
90
	}
91
92
	public function mdtm(string $path): int {
93
		$result = @ftp_mdtm($this->connection, $path);
94
95
		// filezilla doesn't like empty path with mdtm
96
		if ($result === -1 && $path === "") {
97
			$result = @ftp_mdtm($this->connection, "/");
98
		}
99
		return $result;
100
	}
101
102
	public function size(string $path) {
103
		return @ftp_size($this->connection, $path);
104
	}
105
106
	public function systype() {
107
		return @ftp_systype($this->connection);
108
	}
109
110
	public function nlist(string $path) {
111
		$files = @ftp_nlist($this->connection, $path);
112
		return array_map(function ($name) {
113
			if (strpos($name, '/') !== false) {
114
				$name = basename($name);
115
			}
116
			return $name;
117
		}, $files);
118
	}
119
120
	public function mlsd(string $path) {
121
		$files = @ftp_mlsd($this->connection, $path);
122
123
		if ($files !== false) {
124
			return array_map(function ($file) {
125
				if (strpos($file['name'], '/') !== false) {
126
					$file['name'] = basename($file['name']);
127
				}
128
				return $file;
129
			}, $files);
130
		} else {
131
			// not all servers support mlsd, in those cases we parse the raw list ourselves
132
			$rawList = @ftp_rawlist($this->connection, '-aln ' . $path);
133
			if ($rawList === false) {
134
				return false;
135
			}
136
			return $this->parseRawList($rawList, $path);
137
		}
138
	}
139
140
	// rawlist parsing logic is based on the ftp implementation from https://github.com/thephpleague/flysystem
141
	private function parseRawList(array $rawList, string $directory): array {
142
		return array_map(function ($item) use ($directory) {
143
			return $this->parseRawListItem($item, $directory);
144
		}, $rawList);
145
	}
146
147
	private function parseRawListItem(string $item, string $directory): array {
148
		$isWindows = preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item);
149
150
		return $isWindows ? $this->parseWindowsItem($item, $directory) : $this->parseUnixItem($item, $directory);
151
	}
152
153
	private function parseUnixItem(string $item, string $directory): array {
0 ignored issues
show
Unused Code introduced by
The parameter $directory is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

153
	private function parseUnixItem(string $item, /** @scrutinizer ignore-unused */ string $directory): array {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
154
		$item = preg_replace('#\s+#', ' ', $item, 7);
155
156
		if (count(explode(' ', $item, 9)) !== 9) {
157
			throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
158
		}
159
160
		[$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $time, $name] = explode(' ', $item, 9);
161
		if ($name === '.') {
162
			$type = 'cdir';
163
		} elseif ($name === '..') {
164
			$type = 'pdir';
165
		} else {
166
			$type = substr($permissions, 0, 1) === 'd' ? 'dir' : 'file';
167
		}
168
169
		$parsedDate = (new \DateTime())
170
			->setTimestamp(strtotime("$month $day $time"));
171
		$tomorrow = (new \DateTime())->add(new \DateInterval("P1D"));
172
173
		// since the provided date doesn't include the year, we either set it to the correct year
174
		// or when the date would otherwise be in the future (by more then 1 day to account for timezone errors)
175
		// we use last year
176
		if ($parsedDate > $tomorrow) {
177
			$parsedDate = $parsedDate->sub(new \DateInterval("P1Y"));
178
		}
179
180
		$formattedDate = $parsedDate
181
			->format('YmdHis');
182
183
		return [
184
			'type' => $type,
185
			'name' => $name,
186
			'modify' => $formattedDate,
187
			'perm' => $this->normalizePermissions($permissions),
188
			'size' => (int)$size,
189
		];
190
	}
191
192
	private function normalizePermissions(string $permissions) {
193
		$isDir = substr($permissions, 0, 1) === 'd';
194
		// remove the type identifier and only use owner permissions
195
		$permissions = substr($permissions, 1, 4);
196
197
		// map the string rights to the ftp counterparts
198
		$filePermissionsMap = ['r' => 'r', 'w' => 'fadfw'];
199
		$dirPermissionsMap = ['r' => 'e', 'w' => 'flcdmp'];
200
201
		$map = $isDir ? $dirPermissionsMap : $filePermissionsMap;
202
203
		return array_reduce(str_split($permissions), function ($ftpPermissions, $permission) use ($map) {
204
			if (isset($map[$permission])) {
205
				$ftpPermissions .= $map[$permission];
206
			}
207
			return $ftpPermissions;
208
		}, '');
209
	}
210
211
	private function parseWindowsItem(string $item, string $directory): array {
0 ignored issues
show
Unused Code introduced by
The parameter $directory is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

211
	private function parseWindowsItem(string $item, /** @scrutinizer ignore-unused */ string $directory): array {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
212
		$item = preg_replace('#\s+#', ' ', trim($item), 3);
213
214
		if (count(explode(' ', $item, 4)) !== 4) {
215
			throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
216
		}
217
218
		[$date, $time, $size, $name] = explode(' ', $item, 4);
219
220
		// Check for the correct date/time format
221
		$format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
222
		$formattedDate = \DateTime::createFromFormat($format, $date . $time)->format('YmdGis');
223
224
		if ($name === '.') {
225
			$type = 'cdir';
226
		} elseif ($name === '..') {
227
			$type = 'pdir';
228
		} else {
229
			$type = ($size === '<DIR>') ? 'dir' : 'file';
230
		}
231
232
		return [
233
			'type' => $type,
234
			'name' => $name,
235
			'modify' => $formattedDate,
236
			'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe',
237
			'size' => (int)$size,
238
		];
239
	}
240
}
241