Completed
Push — master ( bd154c...3a6b48 )
by Frank
01:36 queued 11s
created

SftpAdapter   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 612
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 93.55%

Importance

Changes 0
Metric Value
wmc 81
lcom 1
cbo 6
dl 0
loc 612
ccs 174
cts 186
cp 0.9355
rs 1.988
c 0
b 0
f 0

38 Methods

Rating   Name   Duplication   Size   Complexity  
A prefix() 0 4 1
A setHostFingerprint() 0 6 1
A setPrivateKey() 0 6 1
A setPassphrase() 0 6 1
A setUseAgent() 0 6 1
A setAgent() 0 6 1
A setDirectoryPerm() 0 6 1
A getDirectoryPerm() 0 4 1
A setNetSftpConnection() 0 6 1
A connect() 0 7 2
B login() 0 31 8
A getHexFingerprintFromSshPublicKey() 0 5 1
A setConnectionRoot() 0 13 3
A getAuthentication() 0 12 3
A getPrivateKey() 0 16 4
A getPassphrase() 0 8 2
A getAgent() 0 8 2
B listDirectoryContents() 0 28 8
A normalizeListingObject() 0 16 5
A disconnect() 0 4 1
A write() 0 8 2
A writeStream() 0 8 2
A upload() 0 16 4
A read() 0 10 2
A readStream() 0 14 2
A update() 0 4 1
A updateStream() 0 4 1
A delete() 0 6 1
A rename() 0 6 1
A deleteDir() 0 6 1
A has() 0 4 1
A getMetadata() 0 15 4
A getTimestamp() 0 4 1
A getMimetype() 0 10 2
A createDir() 0 10 2
A getVisibility() 0 4 1
A setVisibility() 0 12 2
A isConnected() 0 8 3

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace League\Flysystem\Sftp;
4
5
use InvalidArgumentException;
6
use League\Flysystem\Adapter\AbstractFtpAdapter;
7
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
8
use League\Flysystem\AdapterInterface;
9
use League\Flysystem\Config;
10
use League\Flysystem\Util;
11
use phpseclib\Crypt\RSA;
12
use phpseclib\Net\SFTP;
13
use phpseclib\System\SSH\Agent;
14
15
class SftpAdapter extends AbstractFtpAdapter
16
{
17
    use StreamedCopyTrait;
18
19
    /**
20
     * @var SFTP
21
     */
22
    protected $connection;
23
24
    /**
25
     * @var int
26
     */
27
    protected $port = 22;
28
29
    /**
30
     * @var string
31
     */
32
    protected $hostFingerprint;
33
34
    /**
35
     * @var string
36
     */
37
    protected $privateKey;
38
39
    /**
40
     * @var bool
41
     */
42
    protected $useAgent = false;
43
44
    /**
45
     * @var Agent
46
     */
47
    private $agent;
48
49
    /**
50
     * @var array
51
     */
52
    protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'passphrase', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
53
54
    /**
55
     * @var array
56
     */
57
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
58
59
    /**
60
     * @var int
61
     */
62
    protected $directoryPerm = 0744;
63
64
    /**
65
     * @var string
66
     */
67
    private $passphrase;
68
69
    /**
70
     * Prefix a path.
71
     *
72
     * @param string $path
73
     *
74
     * @return string
75
     */
76 9
    protected function prefix($path)
77
    {
78 9
        return $this->root.ltrim($path, $this->separator);
79
    }
80
81
    /**
82
     * Set the finger print of the public key of the host you are connecting to.
83
     *
84
     * If the key does not match the server identification, the connection will
85
     * be aborted.
86
     *
87
     * @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
88
     *
89
     * @return $this
90
     */
91 9
    public function setHostFingerprint($fingerprint)
92
    {
93 9
        $this->hostFingerprint = $fingerprint;
94
95 9
        return $this;
96
    }
97
98
    /**
99
     * Set the private key (string or path to local file).
100
     *
101
     * @param string $key
102
     *
103
     * @return $this
104
     */
105 12
    public function setPrivateKey($key)
106
    {
107 12
        $this->privateKey = $key;
108
109 12
        return $this;
110
    }
111
112
    /**
113
     * Set the passphrase for the privatekey.
114
     *
115
     * @param string $passphrase
116
     *
117
     * @return $this
118
     */
119 6
    public function setPassphrase($passphrase)
120
    {
121 6
        $this->passphrase = $passphrase;
122
123 6
        return $this;
124
    }
125
126
    /**
127
     * @param boolean $useAgent
128
     *
129
     * @return $this
130
     */
131
    public function setUseAgent($useAgent)
132
    {
133
        $this->useAgent = (bool) $useAgent;
134
135
        return $this;
136
    }
137
138
    /**
139
     * @param Agent $agent
140
     *
141
     * @return $this
142
     */
143
    public function setAgent(Agent $agent)
144
    {
145
        $this->agent = $agent;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Set permissions for new directory
152
     *
153
     * @param int $directoryPerm
154
     *
155
     * @return $this
156
     */
157 6
    public function setDirectoryPerm($directoryPerm)
158
    {
159 6
        $this->directoryPerm = $directoryPerm;
160
161 6
        return $this;
162
    }
163
164
    /**
165
     * Get permissions for new directory
166
     *
167
     * @return int
168
     */
169 3
    public function getDirectoryPerm()
170
    {
171 3
        return $this->directoryPerm;
172
    }
173
174
    /**
175
     * Inject the SFTP instance.
176
     *
177
     * @param SFTP $connection
178
     *
179
     * @return $this
180
     */
181 36
    public function setNetSftpConnection(SFTP $connection)
182
    {
183 36
        $this->connection = $connection;
184
185 36
        return $this;
186
    }
187
188
    /**
189
     * Connect.
190
     */
191 27
    public function connect()
192
    {
193 27
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
194 27
        $this->connection->disableStatCache();
195 27
        $this->login();
196 18
        $this->setConnectionRoot();
197 15
    }
198
199
    /**
200
     * Login.
201
     *
202
     * @throws ConnectionErrorException
203
     */
204 27
    protected function login()
205
    {
206 27
        if ($this->hostFingerprint) {
207 9
            $publicKey = $this->connection->getServerPublicHostKey();
208
209 9
            if ($publicKey === false) {
210 3
                throw new ConnectionErrorException('Could not connect to server to verify public key.');
211
            }
212
213 6
            $actualFingerprint = $this->getHexFingerprintFromSshPublicKey($publicKey);
214
215 6
            if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) {
216 3
                throw new ConnectionErrorException('The authenticity of host '.$this->host.' can\'t be established.');
217
            }
218
        }
219
220 21
        $authentication = $this->getAuthentication();
221
222
223 21
        if (! $this->connection->login($this->getUsername(), $authentication)) {
224
            // try double authentication, key is already given so now give password
225 6
            if (!$authentication instanceof RSA
0 ignored issues
show
Bug introduced by
The class phpseclib\Crypt\RSA does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
226 6
                || ! $this->connection->login($this->getUsername(), $this->getPassword())) {
227 3
                throw new ConnectionErrorException('Could not login with username: '.$this->getUsername().', host: '.$this->host);
228
            }
229
        }
230
231 18
        if ($authentication instanceof Agent) {
0 ignored issues
show
Bug introduced by
The class phpseclib\System\SSH\Agent does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
232
            $authentication->startSSHForwarding($this->connection);
233
        }
234 18
    }
235
236
    /**
237
     * Convert the SSH RSA public key into a hex formatted fingerprint.
238
     *
239
     * @param string $publickey
240
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
241
     */
242 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
243
    {
244 6
        $content = explode(' ', $publickey, 3);
245 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
246
    }
247
248
    /**
249
     * Set the connection root.
250
     *
251
     * @throws InvalidRootException
252
     */
253 18
    protected function setConnectionRoot()
254
    {
255 18
        $root = $this->getRoot();
256
257 18
        if (! $root) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $root of type string|null is loosely compared to false; 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...
258 12
            return;
259
        }
260
261 6
        if (! $this->connection->chdir($root)) {
262 3
            throw new InvalidRootException('Root is invalid or does not exist: '.$root);
263
        }
264 3
        $this->root = $this->connection->pwd() . $this->separator;
265 3
    }
266
267
    /**
268
     * Get the password, either the private key or a plain text password.
269
     *
270
     * @return Agent|RSA|string
271
     */
272 24
    public function getAuthentication()
273
    {
274 24
        if ($this->useAgent) {
275
            return $this->getAgent();
276
        }
277
278 24
        if ($this->privateKey) {
279 6
            return $this->getPrivateKey();
280
        }
281
282 18
        return $this->getPassword();
283
    }
284
285
    /**
286
     * Get the private key with the password or private key contents.
287
     *
288
     * @return RSA
289
     */
290 12
    public function getPrivateKey()
291
    {
292 12
        if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {
293 3
            $this->privateKey = file_get_contents($this->privateKey);
294
        }
295
296 12
        $key = new RSA();
297
298 12
        if ($password = $this->getPassphrase()) {
299 12
            $key->setPassword($password);
300
        }
301
302 12
        $key->loadKey($this->privateKey);
303
304 12
        return $key;
305
    }
306
307
    /**
308
     * @return string
309
     */
310 21
    public function getPassphrase()
311
    {
312 21
        if ($this->passphrase === null) {
313
            //Added for backward compatibility
314 15
            return $this->getPassword();
315
        }
316 6
        return $this->passphrase;
317
    }
318
319
    /**
320
     * @return Agent|bool
321
     */
322
    public function getAgent()
323
    {
324
        if ( ! $this->agent instanceof Agent) {
0 ignored issues
show
Bug introduced by
The class phpseclib\System\SSH\Agent does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
325
            $this->agent = new Agent();
326
        }
327
328
        return $this->agent;
329
    }
330
331
    /**
332
     * List the contents of a directory.
333
     *
334
     * @param string $directory
335
     * @param bool   $recursive
336
     *
337
     * @return array
338
     */
339 9
    protected function listDirectoryContents($directory, $recursive = true)
340
    {
341 9
        $result = [];
342 9
        $connection = $this->getConnection();
343 9
        $location = $this->prefix($directory);
344 9
        $listing = $connection->rawlist($location);
345
346 9
        if ($listing === false) {
347 3
            return [];
348
        }
349
350 9
        foreach ($listing as $filename => $object) {
351
            // When directory entries have a numeric filename they are changed to int
352 9
            $filename = (string) $filename;
353 9
            if (in_array($filename, ['.', '..'])) {
354 3
                continue;
355
            }
356
357 9
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
358 9
            $result[] = $this->normalizeListingObject($path, $object);
359
360 9
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
361 7
                $result = array_merge($result, $this->listDirectoryContents($path));
362
            }
363
        }
364
365 9
        return $result;
366
    }
367
368
    /**
369
     * Normalize a listing response.
370
     *
371
     * @param string $path
372
     * @param array  $object
373
     *
374
     * @return array
375
     */
376 9
    protected function normalizeListingObject($path, array $object)
377
    {
378 9
        $permissions = $this->normalizePermissions($object['permissions']);
379 9
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
380
381 9
        $timestamp = $object['mtime'];
382
383 9
        if ($type === 'dir') {
384 9
            return compact('path', 'timestamp', 'type');
385
        }
386
387 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
388 6
        $size = (int) $object['size'];
389
390 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
391
    }
392
393
    /**
394
     * Disconnect.
395
     */
396 18
    public function disconnect()
397
    {
398 18
        $this->connection = null;
399 18
    }
400
401
    /**
402
     * @inheritdoc
403
     */
404 6
    public function write($path, $contents, Config $config)
405
    {
406 6
        if ($this->upload($path, $contents, $config) === false) {
407 6
            return false;
408
        }
409
410 6
        return compact('contents', 'path');
411
    }
412
413
    /**
414
     * @inheritdoc
415
     */
416 6
    public function writeStream($path, $resource, Config $config)
417
    {
418 6
        if ($this->upload($path, $resource, $config) === false) {
419 6
            return false;
420
        }
421
422 6
        return compact('path');
423
    }
424
425
    /**
426
     * Upload a file.
427
     *
428
     * @param string          $path
429
     * @param string|resource $contents
430
     * @param Config          $config
431
     * @return bool
432
     */
433 12
    public function upload($path, $contents, Config $config)
434
    {
435 12
        $connection = $this->getConnection();
436 12
        $this->ensureDirectory(Util::dirname($path));
437 12
        $config = Util::ensureConfig($config);
438
439 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
440 12
            return false;
441
        }
442
443 12
        if ($config && $visibility = $config->get('visibility')) {
444 6
            $this->setVisibility($path, $visibility);
445
        }
446
447 12
        return true;
448
    }
449
450
    /**
451
     * @inheritdoc
452
     */
453 6
    public function read($path)
454
    {
455 6
        $connection = $this->getConnection();
456
457 6
        if (($contents = $connection->get($path)) === false) {
458 6
            return false;
459
        }
460
461 6
        return compact('contents', 'path');
462
    }
463
464
    /**
465
     * @inheritdoc
466
     */
467 3
    public function readStream($path)
468
    {
469 3
        $stream = tmpfile();
470 3
        $connection = $this->getConnection();
471
472 3
        if ($connection->get($path, $stream) === false) {
473 3
            fclose($stream);
474 3
            return false;
475
        }
476
477 3
        rewind($stream);
478
479 3
        return compact('stream', 'path');
480
    }
481
482
    /**
483
     * @inheritdoc
484
     */
485 3
    public function update($path, $contents, Config $config)
486
    {
487 3
        return $this->write($path, $contents, $config);
488
    }
489
490
    /**
491
     * @inheritdoc
492
     */
493 3
    public function updateStream($path, $contents, Config $config)
494
    {
495 3
        return $this->writeStream($path, $contents, $config);
496
    }
497
498
    /**
499
     * @inheritdoc
500
     */
501 3
    public function delete($path)
502
    {
503 3
        $connection = $this->getConnection();
504
505 3
        return $connection->delete($path);
506
    }
507
508
    /**
509
     * @inheritdoc
510
     */
511 3
    public function rename($path, $newpath)
512
    {
513 3
        $connection = $this->getConnection();
514
515 3
        return $connection->rename($path, $newpath);
516
    }
517
518
    /**
519
     * @inheritdoc
520
     */
521 3
    public function deleteDir($dirname)
522
    {
523 3
        $connection = $this->getConnection();
524
525 3
        return $connection->delete($dirname, true);
526
    }
527
528
    /**
529
     * @inheritdoc
530
     */
531 39
    public function has($path)
532
    {
533 39
        return $this->getMetadata($path);
534
    }
535
536
    /**
537
     * @inheritdoc
538
     */
539 48
    public function getMetadata($path)
540
    {
541 48
        $connection = $this->getConnection();
542 48
        $info = $connection->stat($path);
543
544 48
        if ($info === false) {
545 12
            return false;
546
        }
547
548 39
        $result = Util::map($info, $this->statMap);
549 39
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
550 39
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
551
552 39
        return $result;
553
    }
554
555
    /**
556
     * @inheritdoc
557
     */
558 6
    public function getTimestamp($path)
559
    {
560 6
        return $this->getMetadata($path);
561
    }
562
563
    /**
564
     * @inheritdoc
565
     */
566 3
    public function getMimetype($path)
567
    {
568 3
        if (! $data = $this->read($path)) {
569 3
            return false;
570
        }
571
572 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
573
574 3
        return $data;
575
    }
576
577
    /**
578
     * @inheritdoc
579
     */
580 3
    public function createDir($dirname, Config $config)
581
    {
582 3
        $connection = $this->getConnection();
583
584 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
585 3
            return false;
586
        }
587
588 3
        return ['path' => $dirname];
589
    }
590
591
    /**
592
     * @inheritdoc
593
     */
594 6
    public function getVisibility($path)
595
    {
596 6
        return $this->getMetadata($path);
597
    }
598
599
    /**
600
     * @inheritdoc
601
     */
602 12
    public function setVisibility($path, $visibility)
603
    {
604 12
        $visibility = ucfirst($visibility);
605
606 12
        if (! isset($this->{'perm'.$visibility})) {
607 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
608
        }
609
610 9
        $connection = $this->getConnection();
611
612 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
613
    }
614
615
    /**
616
     * @inheritdoc
617
     */
618 6
    public function isConnected()
619
    {
620 6
        if ($this->connection instanceof SFTP && $this->connection->isConnected()) {
0 ignored issues
show
Bug introduced by
The class phpseclib\Net\SFTP does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
621 3
            return true;
622
        }
623
624 3
        return false;
625
    }
626
}
627