1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* WebHemi. |
4
|
|
|
* |
5
|
|
|
* PHP version 7.1 |
6
|
|
|
* |
7
|
|
|
* @copyright 2012 - 2017 Gixx-web (http://www.gixx-web.com) |
8
|
|
|
* @license https://opensource.org/licenses/MIT The MIT License (MIT) |
9
|
|
|
* |
10
|
|
|
* @link http://www.gixx-web.com |
11
|
|
|
*/ |
12
|
|
|
declare(strict_types = 1); |
13
|
|
|
|
14
|
|
|
namespace WebHemi\Ftp\ServiceAdapter\Base; |
15
|
|
|
|
16
|
|
|
use RuntimeException; |
17
|
|
|
use WebHemi\Configuration\ServiceInterface as ConfigurationInterface; |
18
|
|
|
use WebHemi\Ftp\ServiceInterface; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Class ServiceAdapter. |
22
|
|
|
*/ |
23
|
|
|
class ServiceAdapter implements ServiceInterface |
24
|
|
|
{ |
25
|
|
|
/** @var resource */ |
26
|
|
|
private $connectionId = null; |
27
|
|
|
/** @var string */ |
28
|
|
|
private $localPath = __DIR__; |
29
|
|
|
/** @var array */ |
30
|
|
|
protected $options = []; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* ServiceAdapter constructor. |
34
|
|
|
* |
35
|
|
|
* @param ConfigurationInterface $configuration |
36
|
|
|
*/ |
37
|
|
|
public function __construct(ConfigurationInterface $configuration) |
38
|
|
|
{ |
39
|
|
|
$this->setOptions($configuration->getData('ftp')); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Disconnected by garbage collection. |
44
|
|
|
*/ |
45
|
|
|
public function __destruct() |
46
|
|
|
{ |
47
|
|
|
$this->disconnect(); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Connect and login to remote host. |
52
|
|
|
* |
53
|
|
|
* @return ServiceInterface |
54
|
|
|
*/ |
55
|
|
|
public function connect() : ServiceInterface |
56
|
|
|
{ |
57
|
|
|
if ($this->getOption('secure', true)) { |
58
|
|
|
$this->connectionId = @ftp_ssl_connect($this->getOption('host')); |
59
|
|
|
} else { |
60
|
|
|
$this->connectionId = @ftp_connect($this->getOption('host')); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
if (!$this->connectionId) { |
64
|
|
|
throw new RuntimeException( |
65
|
|
|
sprintf('Cannot establish connection to server: %s', $this->getOption('host')), |
66
|
|
|
1000 |
67
|
|
|
); |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
$loginResult = @ftp_login($this->connectionId, $this->getOption('username'), $this->getOption('password')); |
71
|
|
|
|
72
|
|
|
if (!$loginResult) { |
73
|
|
|
throw new RuntimeException('Cannot connect to remote host: invalid credentials', 1001); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
return $this; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Disconnect from remote host. |
81
|
|
|
* |
82
|
|
|
* @return ServiceInterface |
83
|
|
|
*/ |
84
|
|
|
public function disconnect() : ServiceInterface |
85
|
|
|
{ |
86
|
|
|
if (!empty($this->connectionId)) { |
87
|
|
|
ftp_close($this->connectionId); |
88
|
|
|
$this->connectionId = null; |
89
|
|
|
} |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Sets an option data. |
94
|
|
|
* |
95
|
|
|
* @param string $key |
96
|
|
|
* @param mixed $value |
97
|
|
|
* @return ServiceInterface |
98
|
|
|
*/ |
99
|
|
|
public function setOption(string $key, $value) : ServiceInterface |
100
|
|
|
{ |
101
|
|
|
$this->options[$key] = $value; |
102
|
|
|
return $this; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Sets a group of options. |
107
|
|
|
* |
108
|
|
|
* @param array $options |
109
|
|
|
* @return ServiceInterface |
110
|
|
|
*/ |
111
|
|
|
public function setOptions(array $options) : ServiceInterface |
112
|
|
|
{ |
113
|
|
|
$this->options = array_merge($this->options, $options); |
114
|
|
|
return $this; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Gets a specific option data. |
119
|
|
|
* |
120
|
|
|
* @param string $key |
121
|
|
|
* @param mixed $default |
122
|
|
|
* @return mixed |
123
|
|
|
*/ |
124
|
|
|
public function getOption(string $key, $default = null) |
125
|
|
|
{ |
126
|
|
|
return $this->options[$key] ?? $default; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Toggles connection security level. |
131
|
|
|
* |
132
|
|
|
* @param bool $state |
133
|
|
|
* @return ServiceInterface |
134
|
|
|
*/ |
135
|
|
|
public function setSecureConnection(bool $state) : ServiceInterface |
136
|
|
|
{ |
137
|
|
|
$this->setOption('secure', (bool) $state); |
138
|
|
|
|
139
|
|
|
if (!empty($this->connectionId)) { |
140
|
|
|
ftp_close($this->connectionId); |
141
|
|
|
$this->connectionId = null; |
142
|
|
|
$this->connect(); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
return $this; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Toggles connection passive mode. |
150
|
|
|
* |
151
|
|
|
* @param bool $state |
152
|
|
|
* @return ServiceInterface |
153
|
|
|
*/ |
154
|
|
|
public function setPassiveMode(bool $state) : ServiceInterface |
155
|
|
|
{ |
156
|
|
|
ftp_pasv($this->connectionId, (bool) $state); |
157
|
|
|
return $this; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Sets remote path. |
162
|
|
|
* |
163
|
|
|
* @param string $path |
164
|
|
|
* @return ServiceInterface |
165
|
|
|
*/ |
166
|
|
|
public function setRemotePath(string $path) : ServiceInterface |
167
|
|
|
{ |
168
|
|
|
if (trim($this->getRemotePath(), '/') == trim($path, '/')) { |
169
|
|
|
return $this; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
if (strpos($path, '/') !== 0) { |
173
|
|
|
$path = $this->getRemotePath().$path; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$chdirResult = @ftp_chdir($this->connectionId, $path); |
177
|
|
|
|
178
|
|
|
if (!$chdirResult) { |
179
|
|
|
throw new RuntimeException(sprintf('No such directory on remote host: %s', $path), 1002); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return $this; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Gets remote path. |
187
|
|
|
* |
188
|
|
|
* @return string |
189
|
|
|
*/ |
190
|
|
|
public function getRemotePath() : string |
191
|
|
|
{ |
192
|
|
|
return ftp_pwd($this->connectionId).'/'; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Sets local path. |
197
|
|
|
* |
198
|
|
|
* @param string $path |
199
|
|
|
* @return ServiceInterface |
200
|
|
|
*/ |
201
|
|
|
public function setLocalPath(string $path) : ServiceInterface |
202
|
|
|
{ |
203
|
|
|
// if it's not an absolute path, we take it relative to the current folder |
204
|
|
|
if (strpos($path, '/') !== 0) { |
205
|
|
|
$path = __DIR__.'/'.$path; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
if (!realpath($path) || !is_dir($path)) { |
209
|
|
|
throw new RuntimeException(sprintf('No such directory: %s', $path), 1003); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
if (!is_readable($path)) { |
213
|
|
|
throw new RuntimeException(sprintf('Cannot read directory: %s; Permission denied.', $path), 1004); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
if (!is_writable($path)) { |
217
|
|
|
throw new RuntimeException( |
218
|
|
|
sprintf('Cannot write data into directory: %s; Permission denied.', $path), |
219
|
|
|
1005 |
220
|
|
|
); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
$this->localPath = $path; |
224
|
|
|
|
225
|
|
|
return $this; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Gets local path. |
230
|
|
|
* |
231
|
|
|
* @return string |
232
|
|
|
*/ |
233
|
|
|
public function getLocalPath() : string |
234
|
|
|
{ |
235
|
|
|
return $this->localPath; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Lists remote path. |
240
|
|
|
* |
241
|
|
|
* @param null|string $path |
242
|
|
|
* @param bool|null $changeToDirectory |
243
|
|
|
* @return array |
244
|
|
|
*/ |
245
|
|
|
public function getRemoteFileList(? string $path, ? bool $changeToDirectory) : array |
246
|
|
|
{ |
247
|
|
|
$fileList = []; |
248
|
|
|
|
249
|
|
|
if (!empty($path) && $changeToDirectory) { |
250
|
|
|
$this->setRemotePath($path); |
251
|
|
|
$path = '.'; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$result = @ftp_rawlist($this->connectionId, $path); |
255
|
|
|
|
256
|
|
|
if (!is_array($result)) { |
257
|
|
|
throw new RuntimeException('Cannot retrieve file list', 1006); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
foreach ($result as $fileRawData) { |
261
|
|
|
$fileData = []; |
262
|
|
|
|
263
|
|
|
preg_match( |
264
|
|
|
'/^(?P<rights>(?P<type>(-|d))[^\s]+)\s+(?P<symlinks>\d+)\s+(?P<user>[^\s]+)\s+(?P<group>[^\s]+)\s+' |
265
|
|
|
.'(?P<size>\d+)\s+(?P<date>(?P<month>[^\s]+)\s+(?P<day>[^\s]+)\s+' |
266
|
|
|
.'(?P<time>[^\s]+))\s+(?P<filename>.+)$/', |
267
|
|
|
$fileRawData, |
268
|
|
|
$fileData |
269
|
|
|
); |
270
|
|
|
$fileInfo = pathinfo($fileData['filename']); |
271
|
|
|
|
272
|
|
|
$fileList[] = [ |
273
|
|
|
'type' => $this->getFileType($fileData['type']), |
274
|
|
|
'chmod' => $this->getOctalChmod($fileData['rights']), |
275
|
|
|
'symlinks' => $fileData['symlinks'], |
276
|
|
|
'user' => $fileData['user'], |
277
|
|
|
'group' => $fileData['group'], |
278
|
|
|
'size' => $fileData['size'], |
279
|
|
|
'date' => $this->getFileDate($fileData), |
280
|
|
|
'basename' => $fileInfo['basename'], |
281
|
|
|
'filename' => $fileInfo['filename'], |
282
|
|
|
'extension' => $fileInfo['extension'] ?? '', |
283
|
|
|
]; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
return $fileList; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @param string $fileData |
291
|
|
|
* @return string |
292
|
|
|
*/ |
293
|
|
|
private function getFileType(string $fileData) : string |
294
|
|
|
{ |
295
|
|
|
switch ($fileData) { |
296
|
|
|
case 'd': |
|
|
|
|
297
|
|
|
$fileType = 'directory'; |
298
|
|
|
break; |
299
|
|
|
|
300
|
|
|
case 'l': |
|
|
|
|
301
|
|
|
$fileType = 'symlink'; |
302
|
|
|
break; |
303
|
|
|
|
304
|
|
|
default: |
305
|
|
|
$fileType = 'file'; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $fileType; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* @param array $fileData |
313
|
|
|
* @return string |
314
|
|
|
*/ |
315
|
|
|
private function getFileDate(array $fileData) : string |
316
|
|
|
{ |
317
|
|
|
if (strpos($fileData['time'], ':') !== false) { |
318
|
|
|
$date = $fileData['month'].' '.$fileData['day'].' '.date('Y').' '.$fileData['time']; |
319
|
|
|
} else { |
320
|
|
|
$date = $fileData['date'].' 12:00:00'; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
$time = strtotime($date); |
324
|
|
|
|
325
|
|
|
return date('Y-m-d H:i:s', $time); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Converts file rights string into octal value. |
330
|
|
|
* |
331
|
|
|
* @param string $permissions The UNIX-style permission string, e.g.: 'drwxr-xr-x' |
332
|
|
|
* @return string |
333
|
|
|
*/ |
334
|
|
|
private function getOctalChmod(string $permissions) : string |
335
|
|
|
{ |
336
|
|
|
$mode = 0; |
337
|
|
|
$mapper = [ |
338
|
|
|
0 => [], // type like d as directory, l as link etc. |
339
|
|
|
// Owner |
340
|
|
|
1 => ['r' => 0400], |
341
|
|
|
2 => ['w' => 0200], |
342
|
|
|
3 => [ |
343
|
|
|
'x' => 0100, |
344
|
|
|
's' => 04100, |
345
|
|
|
'S' => 04000 |
346
|
|
|
], |
347
|
|
|
// Group |
348
|
|
|
4 => ['r' => 040], |
349
|
|
|
5 => ['w' => 020], |
350
|
|
|
6 => [ |
351
|
|
|
'x' => 010, |
352
|
|
|
's' => 02010, |
353
|
|
|
'S' => 02000 |
354
|
|
|
], |
355
|
|
|
// World |
356
|
|
|
7 => ['r' => 04], |
357
|
|
|
8 => ['w' => 02], |
358
|
|
|
9 => [ |
359
|
|
|
'x' => 01, |
360
|
|
|
't' => 01001, |
361
|
|
|
'T' => 01000 |
362
|
|
|
], |
363
|
|
|
]; |
364
|
|
|
|
365
|
|
|
for ($i = 1; $i <= 9; $i++) { |
366
|
|
|
$mode += $mapper[$i][$permissions[$i]] ?? 0; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
return (string) $mode; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* Uploads file to remote host. |
374
|
|
|
* |
375
|
|
|
* @see self::setRemotePath |
376
|
|
|
* @see self::setLocalPath |
377
|
|
|
* |
378
|
|
|
* @param string $sourceFileName |
379
|
|
|
* @param string $destinationFileName |
380
|
|
|
* @param int $fileMode |
381
|
|
|
* @return mixed |
382
|
|
|
*/ |
383
|
|
|
public function upload( |
384
|
|
|
string $sourceFileName, |
385
|
|
|
string $destinationFileName, |
386
|
|
|
int $fileMode = self::FILE_MODE_BINARY |
387
|
|
|
) : ServiceInterface { |
388
|
|
|
$this->checkLocalFile($sourceFileName); |
389
|
|
|
$this->checkRemoteFile($destinationFileName); |
390
|
|
|
|
391
|
|
|
if (!file_exists($this->localPath.'/'.$sourceFileName)) { |
392
|
|
|
throw new RuntimeException(sprintf('File not found: %s', $this->localPath.'/'.$sourceFileName), 1007); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
$uploadResult = @ftp_put( |
396
|
|
|
$this->connectionId, |
397
|
|
|
$destinationFileName, |
398
|
|
|
$this->localPath.'/'.$sourceFileName, |
399
|
|
|
$fileMode |
400
|
|
|
); |
401
|
|
|
|
402
|
|
|
if (!$uploadResult) { |
403
|
|
|
throw new RuntimeException(sprintf('There was a problem while uploading file: %s', $sourceFileName), 1008); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
return $this; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Downloads file from remote host. |
411
|
|
|
* |
412
|
|
|
* @see self::setRemotePath |
413
|
|
|
* @see self::setLocalPath |
414
|
|
|
* |
415
|
|
|
* @param string $remoteFileName |
416
|
|
|
* @param string $localFileName |
417
|
|
|
* @param int $fileMode |
418
|
|
|
* @return mixed |
419
|
|
|
*/ |
420
|
|
|
public function download( |
421
|
|
|
string $remoteFileName, |
422
|
|
|
string&$localFileName, |
423
|
|
|
int $fileMode = self::FILE_MODE_BINARY |
424
|
|
|
) : ServiceInterface { |
425
|
|
|
$this->checkRemoteFile($remoteFileName); |
426
|
|
|
$this->checkLocalFile($localFileName, true); |
427
|
|
|
|
428
|
|
|
$downloadResult = @ftp_get( |
429
|
|
|
$this->connectionId, |
430
|
|
|
$this->localPath.'/'.$localFileName, |
431
|
|
|
$remoteFileName, |
432
|
|
|
$fileMode |
433
|
|
|
); |
434
|
|
|
|
435
|
|
|
if (!$downloadResult) { |
436
|
|
|
throw new RuntimeException( |
437
|
|
|
sprintf('There was a problem while downloading file: %s', $remoteFileName), |
438
|
|
|
1010 |
439
|
|
|
); |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return $this; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Checks local file, and generates new unique name if necessary. |
447
|
|
|
* |
448
|
|
|
* @param string $localFileName |
449
|
|
|
* @param bool $forceUnique |
450
|
|
|
* @throws RuntimeException |
451
|
|
|
*/ |
452
|
|
|
private function checkLocalFile(string&$localFileName, bool $forceUnique = false) : void |
453
|
|
|
{ |
454
|
|
|
$pathInfo = pathinfo($localFileName); |
455
|
|
|
|
456
|
|
|
if ($pathInfo['dirname'] != '.') { |
457
|
|
|
$this->setLocalPath($pathInfo['dirname']); |
458
|
|
|
$localFileName = $pathInfo['basename']; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
if (!$forceUnique) { |
462
|
|
|
return; |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
$variant = 0; |
466
|
|
|
|
467
|
|
|
while (file_exists($this->localPath.'/'.$localFileName) && $variant++ < 20) { |
468
|
|
|
$fileNameParts = [ |
469
|
|
|
$pathInfo['filename'], |
470
|
|
|
'('.$variant.')', |
471
|
|
|
$pathInfo['extension'] |
472
|
|
|
]; |
473
|
|
|
|
474
|
|
|
// remove empty parts (e.g.: when there's no extension) |
475
|
|
|
$fileNameParts = array_filter($fileNameParts); |
476
|
|
|
|
477
|
|
|
$localFileName = implode('.', $fileNameParts); |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
if ($variant >= 20) { |
481
|
|
|
throw new RuntimeException( |
482
|
|
|
sprintf('Too many similar files in folder %s, please cleanup first.', $this->localPath), |
483
|
|
|
1009 |
484
|
|
|
); |
485
|
|
|
} |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* Check remote file name. |
490
|
|
|
* |
491
|
|
|
* @param string $remoteFileName |
492
|
|
|
*/ |
493
|
|
|
private function checkRemoteFile(string&$remoteFileName) : void |
494
|
|
|
{ |
495
|
|
|
$pathInfo = pathinfo($remoteFileName); |
496
|
|
|
|
497
|
|
|
if ($pathInfo['dirname'] != '.') { |
498
|
|
|
$this->setRemotePath($pathInfo['dirname']); |
499
|
|
|
$remoteFileName = $pathInfo['basename']; |
500
|
|
|
} |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* Moves file on remote host. |
505
|
|
|
* |
506
|
|
|
* @param string $currentPath |
507
|
|
|
* @param string $newPath |
508
|
|
|
* @return ServiceInterface |
509
|
|
|
*/ |
510
|
|
|
public function moveRemoteFile(string $currentPath, string $newPath) : ServiceInterface |
511
|
|
|
{ |
512
|
|
|
$result = @ftp_rename($this->connectionId, $currentPath, $newPath); |
513
|
|
|
|
514
|
|
|
if (!$result) { |
515
|
|
|
throw new RuntimeException( |
516
|
|
|
sprintf('Unable to move/rename file from %s to %s', $currentPath, $newPath), |
517
|
|
|
1011 |
518
|
|
|
); |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
return $this; |
522
|
|
|
} |
523
|
|
|
|
524
|
|
|
/** |
525
|
|
|
* Deletes file on remote host. |
526
|
|
|
* |
527
|
|
|
* @param string $path |
528
|
|
|
* @return ServiceInterface |
529
|
|
|
*/ |
530
|
|
|
public function deleteRemoteFile(string $path) : ServiceInterface |
531
|
|
|
{ |
532
|
|
|
$result = @ftp_delete($this->connectionId, $path); |
533
|
|
|
|
534
|
|
|
if (!$result) { |
535
|
|
|
throw new RuntimeException(sprintf('Unable to delete file on remote host: %s', $path), 1012); |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
return $this; |
539
|
|
|
} |
540
|
|
|
} |
541
|
|
|
|
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next
break
.There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.