Completed
Push — master ( b8cee3...755ba7 )
by Frank
02:33
created

AbstractFtpAdapter::normalizeUnixTimestamp()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 11
cts 11
cp 1
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 2
1
<?php
2
3
namespace League\Flysystem\Adapter;
4
5
use DateTime;
6
use League\Flysystem\AdapterInterface;
7
use League\Flysystem\Config;
8
use League\Flysystem\NotSupportedException;
9
use League\Flysystem\SafeStorage;
10
use RuntimeException;
11
12
abstract class AbstractFtpAdapter extends AbstractAdapter
13
{
14
    /**
15
     * @var mixed
16
     */
17
    protected $connection;
18
19
    /**
20
     * @var string
21
     */
22
    protected $host;
23
24
    /**
25
     * @var int
26
     */
27
    protected $port = 21;
28
29
    /**
30
     * @var bool
31
     */
32
    protected $ssl = false;
33
34
    /**
35
     * @var int
36
     */
37
    protected $timeout = 90;
38
39
    /**
40
     * @var bool
41
     */
42
    protected $passive = true;
43
44
    /**
45
     * @var string
46
     */
47
    protected $separator = '/';
48
49
    /**
50
     * @var string|null
51
     */
52
    protected $root;
53
54
    /**
55
     * @var int
56
     */
57
    protected $permPublic = 0744;
58
59
    /**
60
     * @var int
61
     */
62
    protected $permPrivate = 0700;
63
64
    /**
65
     * @var array
66
     */
67
    protected $configurable = [];
68
69
    /**
70
     * @var string
71
     */
72
    protected $systemType;
73
74
    /**
75
     * @var SafeStorage
76
     */
77
    protected $safeStorage;
78
79
    /**
80
     * True to enable timestamps for FTP servers that return unix-style listings
81
     *
82
     * @var bool
83
     */
84
    protected $enableTimestampsOnUnixListings = false;
85
86
    /**
87
     * Constructor.
88
     *
89
     * @param array $config
90
     */
91 120
    public function __construct(array $config)
92
    {
93 120
        $this->safeStorage = new SafeStorage();
94 120
        $this->setConfig($config);
95 120
    }
96
97
    /**
98
     * Set the config.
99
     *
100
     * @param array $config
101
     *
102
     * @return $this
103
     */
104 120
    public function setConfig(array $config)
105
    {
106 120
        foreach ($this->configurable as $setting) {
107 120
            if ( ! isset($config[$setting])) {
108 120
                continue;
109
            }
110
111 120
            $method = 'set' . ucfirst($setting);
112
113 120
            if (method_exists($this, $method)) {
114 120
                $this->$method($config[$setting]);
115
            }
116
        }
117
118 120
        return $this;
119
    }
120
121
    /**
122
     * Returns the host.
123
     *
124
     * @return string
125
     */
126 108
    public function getHost()
127
    {
128 108
        return $this->host;
129
    }
130
131
    /**
132
     * Set the host.
133
     *
134
     * @param string $host
135
     *
136
     * @return $this
137
     */
138 120
    public function setHost($host)
139
    {
140 120
        $this->host = $host;
141
142 120
        return $this;
143
    }
144
145
    /**
146
     * Set the public permission value.
147
     *
148
     * @param int $permPublic
149
     *
150
     * @return $this
151
     */
152 99
    public function setPermPublic($permPublic)
153
    {
154 99
        $this->permPublic = $permPublic;
155
156 99
        return $this;
157
    }
158
159
    /**
160
     * Set the private permission value.
161
     *
162
     * @param int $permPrivate
163
     *
164
     * @return $this
165
     */
166 99
    public function setPermPrivate($permPrivate)
167
    {
168 99
        $this->permPrivate = $permPrivate;
169
170 99
        return $this;
171
    }
172
173
    /**
174
     * Returns the ftp port.
175
     *
176
     * @return int
177
     */
178 108
    public function getPort()
179
    {
180 108
        return $this->port;
181
    }
182
183
    /**
184
     * Returns the root folder to work from.
185
     *
186
     * @return string
187
     */
188 93
    public function getRoot()
189
    {
190 93
        return $this->root;
191
    }
192
193
    /**
194
     * Set the ftp port.
195
     *
196
     * @param int|string $port
197
     *
198
     * @return $this
199
     */
200 99
    public function setPort($port)
201
    {
202 99
        $this->port = (int) $port;
203
204 99
        return $this;
205
    }
206
207
    /**
208
     * Set the root folder to work from.
209
     *
210
     * @param string $root
211
     *
212
     * @return $this
213
     */
214 108
    public function setRoot($root)
215
    {
216 108
        $this->root = rtrim($root, '\\/') . $this->separator;
217
218 108
        return $this;
219
    }
220
221
    /**
222
     * Returns the ftp username.
223
     *
224
     * @return string username
225
     */
226 102
    public function getUsername()
227
    {
228 102
        $username = $this->safeStorage->retrieveSafely('username');
229
230 102
        return $username !== null ? $username : 'anonymous';
231
    }
232
233
    /**
234
     * Set ftp username.
235
     *
236
     * @param string $username
237
     *
238
     * @return $this
239
     */
240 99
    public function setUsername($username)
241
    {
242 99
        $this->safeStorage->storeSafely('username', $username);
243
244 99
        return $this;
245
    }
246
247
    /**
248
     * Returns the password.
249
     *
250
     * @return string password
251
     */
252 102
    public function getPassword()
253
    {
254 102
        return $this->safeStorage->retrieveSafely('password');
255
    }
256
257
    /**
258
     * Set the ftp password.
259
     *
260
     * @param string $password
261
     *
262
     * @return $this
263
     */
264 99
    public function setPassword($password)
265
    {
266 99
        $this->safeStorage->storeSafely('password', $password);
267
268 99
        return $this;
269
    }
270
271
    /**
272
     * Returns the amount of seconds before the connection will timeout.
273
     *
274
     * @return int
275
     */
276 108
    public function getTimeout()
277
    {
278 108
        return $this->timeout;
279
    }
280
281
    /**
282
     * Set the amount of seconds before the connection should timeout.
283
     *
284
     * @param int $timeout
285
     *
286
     * @return $this
287
     */
288 99
    public function setTimeout($timeout)
289
    {
290 99
        $this->timeout = (int) $timeout;
291
292 99
        return $this;
293
    }
294
295
    /**
296
     * Return the FTP system type.
297
     *
298
     * @return string
299
     */
300 3
    public function getSystemType()
301
    {
302 3
        return $this->systemType;
303
    }
304
305
    /**
306
     * Set the FTP system type (windows or unix).
307
     *
308
     * @param string $systemType
309
     *
310
     * @return $this
311
     */
312 15
    public function setSystemType($systemType)
313
    {
314 15
        $this->systemType = strtolower($systemType);
315
316 15
        return $this;
317
    }
318
319
    /**
320
     * True to enable timestamps for FTP servers that return unix-style listings
321
     *
322
     * @param bool $bool
323
     * @return $this
324
     */
325 12
    public function setEnableTimestampsOnUnixListings($bool = false) {
326 12
        $this->enableTimestampsOnUnixListings = $bool;
327
328 12
        return $this;
329
    }
330
331
    /**
332
     * @inheritdoc
333
     */
334 39
    public function listContents($directory = '', $recursive = false)
335
    {
336 39
        return $this->listDirectoryContents($directory, $recursive);
337
    }
338
339
    abstract protected function listDirectoryContents($directory, $recursive = false);
340
341
    /**
342
     * Normalize a directory listing.
343
     *
344
     * @param array  $listing
345
     * @param string $prefix
346
     *
347
     * @return array directory listing
348
     */
349 33
    protected function normalizeListing(array $listing, $prefix = '')
350
    {
351 33
        $base = $prefix;
352 33
        $result = [];
353 33
        $listing = $this->removeDotDirectories($listing);
354
355 33
        while ($item = array_shift($listing)) {
356 33
            if (preg_match('#^.*:$#', $item)) {
357 15
                $base = preg_replace('~^\./*|:$~', '', $item);
358 15
                continue;
359
            }
360
361 33
            $result[] = $this->normalizeObject($item, $base);
362
        }
363
364 30
        return $this->sortListing($result);
365
    }
366
367
    /**
368
     * Sort a directory listing.
369
     *
370
     * @param array $result
371
     *
372
     * @return array sorted listing
373
     */
374
    protected function sortListing(array $result)
375
    {
376 30
        $compare = function ($one, $two) {
377 24
            return strnatcmp($one['path'], $two['path']);
378 30
        };
379
380 30
        usort($result, $compare);
381
382 30
        return $result;
383
    }
384
385
    /**
386
     * Normalize a file entry.
387
     *
388
     * @param string $item
389
     * @param string $base
390
     *
391
     * @return array normalized file array
392
     *
393
     * @throws NotSupportedException
394
     */
395 54
    protected function normalizeObject($item, $base)
396
    {
397 54
        $systemType = $this->systemType ?: $this->detectSystemType($item);
398
399 54
        if ($systemType === 'unix') {
400 45
            return $this->normalizeUnixObject($item, $base);
401 9
        } elseif ($systemType === 'windows') {
402 6
            return $this->normalizeWindowsObject($item, $base);
403
        }
404
405 3
        throw NotSupportedException::forFtpSystemType($systemType);
406
    }
407
408
    /**
409
     * Normalize a Unix file entry.
410
     *
411
     * Given $item contains:
412
     *    '-rw-r--r--   1 ftp      ftp           409 Aug 19 09:01 file1.txt'
413
     *
414
     * This function will return:
415
     * [
416
     *   'type' => 'file',
417
     *   'path' => 'file1.txt',
418
     *   'visibility' => 'public',
419
     *   'size' => 409,
420
     *   'timestamp' => 1566205260
421
     * ]
422
     *
423
     * @param string $item
424
     * @param string $base
425
     *
426
     * @return array normalized file array
427
     */
428 45
    protected function normalizeUnixObject($item, $base)
429
    {
430 45
        $item = preg_replace('#\s+#', ' ', trim($item), 7);
431
432 45
        if (count(explode(' ', $item, 9)) !== 9) {
433 3
            throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
434
        }
435
436 42
        list($permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $timeOrYear, $name) = explode(' ', $item, 9);
437 42
        $type = $this->detectType($permissions);
438 42
        $path = $base === '' ? $name : $base . $this->separator . $name;
439
440 42
        if ($type === 'dir') {
441 18
            return compact('type', 'path');
442
        }
443
444 42
        $permissions = $this->normalizePermissions($permissions);
445 42
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
446 42
        $size = (int) $size;
447
448 42
        $result = compact('type', 'path', 'visibility', 'size');
449 42
        if ($this->enableTimestampsOnUnixListings) {
450 9
            $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear);
451 9
            $result += compact('timestamp');
452
        }
453 42
        return $result;
454
    }
455
456
    /**
457
     * Only accurate to the minute (current year), or to the day
458
     *
459
     * Inadequacies in timestamp accuracy are due to limitations of the FTP 'LIST' command
460
     *
461
     * Note: The 'MLSD' command is a machine-readable replacement for 'LIST'
462
     * but many FTP servers do not support it :(
463
     *
464
     * @param string $month e.g. 'Aug'
465
     * @param string $day e.g. '19'
466
     * @param string $timeOrYear e.g. '09:01' OR '2015'
467
     * @return int
468
     */
469 9
    protected function normalizeUnixTimestamp($month, $day, $timeOrYear) {
470 9
        if (is_numeric($timeOrYear)) {
471 9
            $year = $timeOrYear;
472 9
            $hour = '00';
473 9
            $minute = '00';
474 9
            $seconds = '00';
475
        }
476
        else {
477 9
            $year = date('Y');
478 9
            list($hour, $minute) = explode(':', $timeOrYear);
479 9
            $seconds = '00';
480
        }
481 9
        $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:{$seconds}");
482 9
        return $dateTime->getTimestamp();
483
    }
484
485
    /**
486
     * Normalize a Windows/DOS file entry.
487
     *
488
     * @param string $item
489
     * @param string $base
490
     *
491
     * @return array normalized file array
492
     */
493 6
    protected function normalizeWindowsObject($item, $base)
494
    {
495 6
        $item = preg_replace('#\s+#', ' ', trim($item), 3);
496
497 6
        if (count(explode(' ', $item, 4)) !== 4) {
498 3
            throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
499
        }
500
501 3
        list($date, $time, $size, $name) = explode(' ', $item, 4);
502 3
        $path = $base === '' ? $name : $base . $this->separator . $name;
503
504
        // Check for the correct date/time format
505 3
        $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
506 3
        $dt = DateTime::createFromFormat($format, $date . $time);
507 3
        $timestamp = $dt ? $dt->getTimestamp() : (int) strtotime("$date $time");
508
509 3
        if ($size === '<DIR>') {
510 3
            $type = 'dir';
511
512 3
            return compact('type', 'path', 'timestamp');
513
        }
514
515 3
        $type = 'file';
516 3
        $visibility = AdapterInterface::VISIBILITY_PUBLIC;
517 3
        $size = (int) $size;
518
519 3
        return compact('type', 'path', 'visibility', 'size', 'timestamp');
520
    }
521
522
    /**
523
     * Get the system type from a listing item.
524
     *
525
     * @param string $item
526
     *
527
     * @return string the system type
528
     */
529 45
    protected function detectSystemType($item)
530
    {
531 45
        return preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item) ? 'windows' : 'unix';
532
    }
533
534
    /**
535
     * Get the file type from the permissions.
536
     *
537
     * @param string $permissions
538
     *
539
     * @return string file type
540
     */
541 42
    protected function detectType($permissions)
542
    {
543 42
        return substr($permissions, 0, 1) === 'd' ? 'dir' : 'file';
544
    }
545
546
    /**
547
     * Normalize a permissions string.
548
     *
549
     * @param string $permissions
550
     *
551
     * @return int
552
     */
553 42
    protected function normalizePermissions($permissions)
554
    {
555
        // remove the type identifier
556 42
        $permissions = substr($permissions, 1);
557
558
        // map the string rights to the numeric counterparts
559 42
        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
560 42
        $permissions = strtr($permissions, $map);
561
562
        // split up the permission groups
563 42
        $parts = str_split($permissions, 3);
564
565
        // convert the groups
566 42
        $mapper = function ($part) {
567 42
            return array_sum(str_split($part));
568 42
        };
569
570
        // converts to decimal number
571 42
        return octdec(implode('', array_map($mapper, $parts)));
572
    }
573
574
    /**
575
     * Filter out dot-directories.
576
     *
577
     * @param array $list
578
     *
579
     * @return array
580
     */
581
    public function removeDotDirectories(array $list)
582
    {
583 33
        $filter = function ($line) {
584 33
            return $line !== '' && ! preg_match('#.* \.(\.)?$|^total#', $line);
585 33
        };
586
587 33
        return array_filter($list, $filter);
588
    }
589
590
    /**
591
     * @inheritdoc
592
     */
593 9
    public function has($path)
594
    {
595 9
        return $this->getMetadata($path);
596
    }
597
598
    /**
599
     * @inheritdoc
600
     */
601 6
    public function getSize($path)
602
    {
603 6
        return $this->getMetadata($path);
604
    }
605
606
    /**
607
     * @inheritdoc
608
     */
609 6
    public function getVisibility($path)
610
    {
611 6
        return $this->getMetadata($path);
612
    }
613
614
    /**
615
     * Ensure a directory exists.
616
     *
617
     * @param string $dirname
618
     */
619 9
    public function ensureDirectory($dirname)
620
    {
621 9
        $dirname = (string) $dirname;
622
623 9
        if ($dirname !== '' && ! $this->has($dirname)) {
624 3
            $this->createDir($dirname, new Config());
625
        }
626 9
    }
627
628
    /**
629
     * @return mixed
630
     */
631 75
    public function getConnection()
632
    {
633 75
        $tries = 0;
634
635 75
        while ( ! $this->isConnected() && $tries < 3) {
636 75
            $tries++;
637 75
            $this->disconnect();
638 75
            $this->connect();
639
        }
640
641 75
        return $this->connection;
642
    }
643
644
    /**
645
     * Get the public permission value.
646
     *
647
     * @return int
648
     */
649 6
    public function getPermPublic()
650
    {
651 6
        return $this->permPublic;
652
    }
653
654
    /**
655
     * Get the private permission value.
656
     *
657
     * @return int
658
     */
659 6
    public function getPermPrivate()
660
    {
661 6
        return $this->permPrivate;
662
    }
663
664
    /**
665
     * Disconnect on destruction.
666
     */
667 120
    public function __destruct()
668
    {
669 120
        $this->disconnect();
670 120
    }
671
672
    /**
673
     * Establish a connection.
674
     */
675
    abstract public function connect();
676
677
    /**
678
     * Close the connection.
679
     */
680
    abstract public function disconnect();
681
682
    /**
683
     * Check if a connection is active.
684
     *
685
     * @return bool
686
     */
687
    abstract public function isConnected();
688
}
689