Ftp   F
last analyzed

Complexity

Total Complexity 80

Size/Duplication

Total Lines 560
Duplicated Lines 0.89 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.11%

Importance

Changes 0
Metric Value
wmc 80
lcom 1
cbo 5
dl 5
loc 560
ccs 208
cts 212
cp 0.9811
rs 2
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getMimetype() 0 10 2
A getTimestamp() 0 6 2
A read() 0 12 2
A readStream() 0 14 2
A setVisibility() 0 10 3
A listDirectoryContents() 0 13 5
A disconnect() 0 8 2
A writeStream() 0 16 3
A update() 0 4 1
A updateStream() 0 4 1
A rename() 0 4 1
A delete() 0 4 1
A deleteDir() 0 17 5
A createActualDirectory() 0 17 5
A setTransferMode() 0 6 1
A setSsl() 0 6 1
A setPassive() 0 4 1
A setIgnorePassiveAddress() 0 4 1
A setRecurseManually() 0 4 1
A setUtf8() 0 4 1
A setConnectionRoot() 0 15 3
A connect() 0 18 3
A setUtf8Mode() 0 11 3
A setConnectionPassiveMode() 0 12 4
A login() 0 19 2
A listDirectoryContentsRecursive() 0 15 4
A isConnected() 0 12 4
A isPureFtpdServer() 0 6 1
A ftpRawlist() 0 10 2
A write() 0 17 3
A createDir() 0 19 3
B getMetadata() 5 28 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Ftp 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 Ftp, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace League\Flysystem\Adapter;
4
5
use ErrorException;
6
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
7
use League\Flysystem\AdapterInterface;
8
use League\Flysystem\Config;
9
use League\Flysystem\Util;
10
use League\Flysystem\Util\MimeType;
11
use RuntimeException;
12
13
class Ftp extends AbstractFtpAdapter
14
{
15
    use StreamedCopyTrait;
16
17
    /**
18
     * @var int
19
     */
20
    protected $transferMode = FTP_BINARY;
21
22
    /**
23
     * @var null|bool
24
     */
25
    protected $ignorePassiveAddress = null;
26
27
    /**
28
     * @var bool
29
     */
30
    protected $recurseManually = false;
31
32
    /**
33
     * @var bool
34
     */
35
    protected $utf8 = false;
36
37
    /**
38
     * @var array
39
     */
40
    protected $configurable = [
41
        'host',
42
        'port',
43
        'username',
44
        'password',
45
        'ssl',
46
        'timeout',
47
        'root',
48
        'permPrivate',
49
        'permPublic',
50
        'passive',
51
        'transferMode',
52
        'systemType',
53
        'ignorePassiveAddress',
54
        'recurseManually',
55
        'utf8',
56
        'enableTimestampsOnUnixListings',
57
    ];
58
59
    /**
60
     * @var bool
61
     */
62
    protected $isPureFtpd;
63
64
    /**
65
     * Set the transfer mode.
66
     *
67
     * @param int $mode
68
     *
69
     * @return $this
70
     */
71 3
    public function setTransferMode($mode)
72
    {
73 3
        $this->transferMode = $mode;
74
75 3
        return $this;
76
    }
77
78
    /**
79
     * Set if Ssl is enabled.
80
     *
81
     * @param bool $ssl
82
     *
83
     * @return $this
84
     */
85 123
    public function setSsl($ssl)
86
    {
87 123
        $this->ssl = (bool) $ssl;
88
89 123
        return $this;
90
    }
91
92
    /**
93
     * Set if passive mode should be used.
94
     *
95
     * @param bool $passive
96
     */
97 105
    public function setPassive($passive = true)
98
    {
99 105
        $this->passive = $passive;
100 105
    }
101
102
    /**
103
     * @param bool $ignorePassiveAddress
104
     */
105 3
    public function setIgnorePassiveAddress($ignorePassiveAddress)
106
    {
107 3
        $this->ignorePassiveAddress = $ignorePassiveAddress;
108 3
    }
109
110
    /**
111
     * @param bool $recurseManually
112
     */
113 84
    public function setRecurseManually($recurseManually)
114
    {
115 84
        $this->recurseManually = $recurseManually;
116 84
    }
117
118
    /**
119
     * @param bool $utf8
120
     */
121 6
    public function setUtf8($utf8)
122
    {
123 6
        $this->utf8 = (bool) $utf8;
124 6
    }
125
126
    /**
127
     * Connect to the FTP server.
128
     */
129 114
    public function connect()
130
    {
131 114
        if ($this->ssl) {
132 108
            $this->connection = ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout());
133 36
        } else {
134 6
            $this->connection = ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout());
135
        }
136
137 114
        if ( ! $this->connection) {
138 6
            throw new RuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
139
        }
140
141 108
        $this->login();
142 105
        $this->setUtf8Mode();
143 102
        $this->setConnectionPassiveMode();
144 99
        $this->setConnectionRoot();
145 96
        $this->isPureFtpd = $this->isPureFtpdServer();
146 96
    }
147
148
    /**
149
     * Set the connection to UTF-8 mode.
150
     */
151 105
    protected function setUtf8Mode()
152
    {
153 105
        if ($this->utf8) {
154 6
            $response = ftp_raw($this->connection, "OPTS UTF8 ON");
155 6
            if (substr($response[0], 0, 3) !== '200') {
156 3
                throw new RuntimeException(
157 3
                    'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort()
158 1
                );
159
            }
160 1
        }
161 102
    }
162
163
    /**
164
     * Set the connections to passive mode.
165
     *
166
     * @throws RuntimeException
167
     */
168 102
    protected function setConnectionPassiveMode()
169
    {
170 102
        if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) {
171 3
            ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress);
172 1
        }
173
174 102
        if ( ! ftp_pasv($this->connection, $this->passive)) {
175 3
            throw new RuntimeException(
176 3
                'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort()
177 1
            );
178
        }
179 99
    }
180
181
    /**
182
     * Set the connection root.
183
     */
184 99
    protected function setConnectionRoot()
185
    {
186 99
        $root = $this->getRoot();
187 99
        $connection = $this->connection;
188
189 99
        if ($root && ! ftp_chdir($connection, $root)) {
0 ignored issues
show
Bug Best Practice introduced by mhlavac
The expression $root of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
190 3
            throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot());
191
        }
192
193
        // Store absolute path for further reference.
194
        // This is needed when creating directories and
195
        // initial root was a relative path, else the root
196
        // would be relative to the chdir'd path.
197 96
        $this->root = ftp_pwd($connection);
198 96
    }
199
200
    /**
201
     * Login.
202
     *
203
     * @throws RuntimeException
204
     */
205
    protected function login()
206
    {
207 108
        set_error_handler(function () {
208 108
        });
209 108
        $isLoggedIn = ftp_login(
210 108
            $this->connection,
211 108
            $this->getUsername(),
212 108
            $this->getPassword()
213 36
        );
214 108
        restore_error_handler();
215
216 108
        if ( ! $isLoggedIn) {
217 3
            $this->disconnect();
218 3
            throw new RuntimeException(
219 3
                'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort(
220 3
                ) . ', username: ' . $this->getUsername()
221 1
            );
222
        }
223 105
    }
224
225
    /**
226
     * Disconnect from the FTP server.
227
     */
228 126
    public function disconnect()
229
    {
230 126
        if (is_resource($this->connection)) {
231 9
            ftp_close($this->connection);
232 3
        }
233
234 126
        $this->connection = null;
235 126
    }
236
237
    /**
238
     * @inheritdoc
239
     */
240 9
    public function write($path, $contents, Config $config)
241
    {
242 9
        $stream = fopen('php://temp', 'w+b');
243 9
        fwrite($stream, $contents);
244 9
        rewind($stream);
245 9
        $result = $this->writeStream($path, $stream, $config);
246 9
        fclose($stream);
247
248 9
        if ($result === false) {
249 6
            return false;
250
        }
251
252 6
        $result['contents'] = $contents;
253 6
        $result['mimetype'] = $config->get('mimetype') ?: Util::guessMimeType($path, $contents);
254
255 6
        return $result;
256
    }
257
258
    /**
259
     * @inheritdoc
260
     */
261 9
    public function writeStream($path, $resource, Config $config)
262
    {
263 9
        $this->ensureDirectory(Util::dirname($path));
264
265 9
        if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) {
266 6
            return false;
267
        }
268
269 6
        if ($visibility = $config->get('visibility')) {
270 6
            $this->setVisibility($path, $visibility);
271 2
        }
272
273 6
        $type = 'file';
274
275 6
        return compact('type', 'path', 'visibility');
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281 6
    public function update($path, $contents, Config $config)
282
    {
283 6
        return $this->write($path, $contents, $config);
284
    }
285
286
    /**
287
     * @inheritdoc
288
     */
289 3
    public function updateStream($path, $resource, Config $config)
290
    {
291 3
        return $this->writeStream($path, $resource, $config);
292
    }
293
294
    /**
295
     * @inheritdoc
296
     */
297 3
    public function rename($path, $newpath)
298
    {
299 3
        return ftp_rename($this->getConnection(), $path, $newpath);
300
    }
301
302
    /**
303
     * @inheritdoc
304
     */
305 3
    public function delete($path)
306
    {
307 3
        return ftp_delete($this->getConnection(), $path);
308
    }
309
310
    /**
311
     * @inheritdoc
312
     */
313 3
    public function deleteDir($dirname)
314
    {
315 3
        $connection = $this->getConnection();
316 3
        $contents = array_reverse($this->listDirectoryContents($dirname, false));
317
318 3
        foreach ($contents as $object) {
319 3
            if ($object['type'] === 'file') {
320 3
                if ( ! ftp_delete($connection, $object['path'])) {
321 3
                    return false;
322
                }
323 3
            } elseif ( ! $this->deleteDir($object['path'])) {
324 2
                return false;
325
            }
326 1
        }
327
328 3
        return ftp_rmdir($connection, $dirname);
329
    }
330
331
    /**
332
     * @inheritdoc
333
     */
334 3
    public function createDir($dirname, Config $config)
335
    {
336 3
        $connection = $this->getConnection();
337 3
        $directories = explode('/', $dirname);
338
339 3
        foreach ($directories as $directory) {
340 3
            if (false === $this->createActualDirectory($directory, $connection)) {
341 3
                $this->setConnectionRoot();
342
343 3
                return false;
344
            }
345
346 3
            ftp_chdir($connection, $directory);
347 1
        }
348
349
        $this->setConnectionRoot();
350
351
        return ['type' => 'dir', 'path' => $dirname];
352
    }
353
354
    /**
355
     * Create a directory.
356
     *
357
     * @param string   $directory
358
     * @param resource $connection
359
     *
360
     * @return bool
361
     */
362 3
    protected function createActualDirectory($directory, $connection)
363
    {
364
        // List the current directory
365 3
        $listing = ftp_nlist($connection, '.') ?: [];
366
367 3
        foreach ($listing as $key => $item) {
368 3
            if (preg_match('~^\./.*~', $item)) {
369 3
                $listing[$key] = substr($item, 2);
370 1
            }
371 1
        }
372
373 3
        if (in_array($directory, $listing, true)) {
374 3
            return true;
375
        }
376
377 3
        return (boolean) ftp_mkdir($connection, $directory);
378
    }
379
380
    /**
381
     * @inheritdoc
382
     */
383 33
    public function getMetadata($path)
384
    {
385 33
        if ($path === '') {
386 3
            return ['type' => 'dir', 'path' => ''];
387
        }
388
389 30 View Code Duplication
        if (@ftp_chdir($this->getConnection(), $path) === true) {
0 ignored issues
show
Duplication introduced by Frank de Jonge
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
390 3
            $this->setConnectionRoot();
391
392 3
            return ['type' => 'dir', 'path' => $path];
393
        }
394
395 30
        $listing = $this->ftpRawlist('-A', str_replace('*', '\\*', $path));
396
397 30
        if (empty($listing) || in_array('total 0', $listing, true)) {
398 6
            return false;
399
        }
400
401 24
        if (preg_match('/.* not found/', $listing[0])) {
402 6
            return false;
403
        }
404
405 18
        if (preg_match('/^total [0-9]*$/', $listing[0])) {
406 3
            array_shift($listing);
407 1
        }
408
409 18
        return $this->normalizeObject($listing[0], '');
410
    }
411
412
    /**
413
     * @inheritdoc
414
     */
415 9
    public function getMimetype($path)
416
    {
417 9
        if ( ! $metadata = $this->getMetadata($path)) {
418 6
            return false;
419
        }
420
421 6
        $metadata['mimetype'] = MimeType::detectByFilename($path);
422
423 6
        return $metadata;
424
    }
425
426
    /**
427
     * @inheritdoc
428
     */
429 12
    public function getTimestamp($path)
430
    {
431 12
        $timestamp = ftp_mdtm($this->getConnection(), $path);
432
433 12
        return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false;
434
    }
435
436
    /**
437
     * @inheritdoc
438
     */
439 6
    public function read($path)
440
    {
441 6
        if ( ! $object = $this->readStream($path)) {
442 3
            return false;
443
        }
444
445 3
        $object['contents'] = stream_get_contents($object['stream']);
446 3
        fclose($object['stream']);
447 3
        unset($object['stream']);
448
449 3
        return $object;
450
    }
451
452
    /**
453
     * @inheritdoc
454
     */
455 6
    public function readStream($path)
456
    {
457 6
        $stream = fopen('php://temp', 'w+b');
458 6
        $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode);
459 6
        rewind($stream);
460
461 6
        if ( ! $result) {
462 3
            fclose($stream);
463
464 3
            return false;
465
        }
466
467 3
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
468
    }
469
470
    /**
471
     * @inheritdoc
472
     */
473 9
    public function setVisibility($path, $visibility)
474
    {
475 9
        $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate();
476
477 9
        if ( ! ftp_chmod($this->getConnection(), $mode, $path)) {
478 6
            return false;
479
        }
480
481 6
        return compact('path', 'visibility');
482
    }
483
484
    /**
485
     * @inheritdoc
486
     *
487
     * @param string $directory
488
     */
489 33
    protected function listDirectoryContents($directory, $recursive = true)
490
    {
491 33
        $directory = str_replace('*', '\\*', $directory);
492
493 33
        if ($recursive && $this->recurseManually) {
494 3
            return $this->listDirectoryContentsRecursive($directory);
495
        }
496
497 30
        $options = $recursive ? '-alnR' : '-aln';
498 30
        $listing = $this->ftpRawlist($options, $directory);
499
500 30
        return $listing ? $this->normalizeListing($listing, $directory) : [];
501
    }
502
503
    /**
504
     * @inheritdoc
505
     *
506
     * @param string $directory
507
     */
508 3
    protected function listDirectoryContentsRecursive($directory)
509
    {
510 3
        $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: [], $directory);
511 3
        $output = [];
512
513 3
        foreach ($listing as $item) {
514 3
            $output[] = $item;
515 3
            if ($item['type'] !== 'dir') {
516 3
                continue;
517
            }
518 3
            $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path']));
519 1
        }
520
521 3
        return $output;
522
    }
523
524
    /**
525
     * Check if the connection is open.
526
     *
527
     * @return bool
528
     *
529
     * @throws ErrorException
530
     */
531 90
    public function isConnected()
532
    {
533
        try {
534 90
            return is_resource($this->connection) && ftp_rawlist($this->connection, $this->getRoot()) !== false;
535 6
        } catch (ErrorException $e) {
536 6
            if (strpos($e->getMessage(), 'ftp_rawlist') === false) {
537 3
                throw $e;
538
            }
539
540 3
            return false;
541
        }
542
    }
543
544
    /**
545
     * @return bool
546
     */
547 96
    protected function isPureFtpdServer()
548
    {
549 96
        $response = ftp_raw($this->connection, 'HELP');
550
551 96
        return stripos(implode(' ', $response), 'Pure-FTPd') !== false;
552
    }
553
554
    /**
555
     * The ftp_rawlist function with optional escaping.
556
     *
557
     * @param string $options
558
     * @param string $path
559
     *
560
     * @return array
561
     */
562 60
    protected function ftpRawlist($options, $path)
563
    {
564 60
        $connection = $this->getConnection();
565
566 60
        if ($this->isPureFtpd) {
567
            $path = str_replace(' ', '\ ', $path);
568
        }
569
570 60
        return ftp_rawlist($connection, $options . ' ' . $path);
571
    }
572
}
573