AbstractFtpAdapter   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 682
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 98.86%

Importance

Changes 0
Metric Value
wmc 65
lcom 1
cbo 4
dl 0
loc 682
ccs 173
cts 175
cp 0.9886
rs 3.118
c 0
b 0
f 0

42 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getHost() 0 4 1
A setHost() 0 6 1
A setPermPublic() 0 6 1
A setPermPrivate() 0 6 1
A getPort() 0 4 1
A getRoot() 0 4 1
A setPort() 0 6 1
A setRoot() 0 6 1
A getUsername() 0 6 2
A setUsername() 0 6 1
A getPassword() 0 4 1
A setPassword() 0 6 1
A getTimeout() 0 4 1
A setTimeout() 0 6 1
A getSystemType() 0 4 1
A setSystemType() 0 6 1
A setConfig() 0 16 4
listDirectoryContents() 0 1 ?
connect() 0 1 ?
disconnect() 0 1 ?
isConnected() 0 1 ?
A setEnableTimestampsOnUnixListings() 0 6 1
A listContents() 0 4 1
A normalizeListing() 0 17 3
A sortListing() 0 10 1
A normalizeObject() 0 12 4
B normalizeUnixObject() 0 28 6
A normalizeUnixTimestamp() 0 16 2
B normalizeWindowsObject() 0 28 6
A detectSystemType() 0 4 2
A detectType() 0 4 2
A normalizePermissions() 0 20 1
A removeDotDirectories() 0 8 2
A has() 0 4 1
A getSize() 0 4 1
A getVisibility() 0 4 1
A getConnection() 0 12 3
A getPermPublic() 0 4 1
A getPermPrivate() 0 4 1
A __destruct() 0 4 1
A ensureDirectory() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like AbstractFtpAdapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractFtpAdapter, and based on these observations, apply Extract Interface, too.

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