Passed
Push — master ( d778b6...b24dcf )
by Gabor
03:31
created

ServiceAdapter::getFileDate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 10
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 6
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':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
297
                $fileType = 'directory';
298
                break;
299
300
            case 'l':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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