Completed
Push — master ( 518ac7...bd154c )
by Frank
01:51 queued 10s
created

SftpAdapter   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 611
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.89%

Importance

Changes 0
Metric Value
wmc 81
lcom 1
cbo 4
dl 0
loc 611
ccs 183
cts 197
cp 0.9289
rs 1.989
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 32 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 LogicException;
12
use phpseclib\Crypt\RSA;
13
use phpseclib\Net\SFTP;
14
use phpseclib\System\SSH\Agent;
15
use RuntimeException;
16
17
class SftpAdapter extends AbstractFtpAdapter
18
{
19
    use StreamedCopyTrait;
20
21
    /**
22
     * @var SFTP
23
     */
24
    protected $connection;
25
26
    /**
27
     * @var int
28
     */
29
    protected $port = 22;
30
31
    /**
32
     * @var string
33
     */
34
    protected $hostFingerprint;
35
36
    /**
37
     * @var string
38
     */
39
    protected $privateKey;
40
41
    /**
42
     * @var bool
43
     */
44
    protected $useAgent = false;
45
46
    /**
47
     * @var Agent
48
     */
49
    private $agent;
50
51
    /**
52
     * @var array
53
     */
54
    protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'passphrase', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
55
56
    /**
57
     * @var array
58
     */
59
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
60
61
    /**
62
     * @var int
63
     */
64
    protected $directoryPerm = 0744;
65
66
    /**
67
     * @var string
68
     */
69
    private $passphrase;
70
71
    /**
72
     * Prefix a path.
73
     *
74
     * @param string $path
75
     *
76
     * @return string
77
     */
78 9
    protected function prefix($path)
79
    {
80 9
        return $this->root.ltrim($path, $this->separator);
81
    }
82
83
    /**
84
     * Set the finger print of the public key of the host you are connecting to.
85
     *
86
     * If the key does not match the server identification, the connection will
87
     * be aborted.
88
     *
89
     * @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
90
     *
91
     * @return $this
92
     */
93 9
    public function setHostFingerprint($fingerprint)
94
    {
95 9
        $this->hostFingerprint = $fingerprint;
96
97 9
        return $this;
98
    }
99
100
    /**
101
     * Set the private key (string or path to local file).
102
     *
103
     * @param string $key
104
     *
105
     * @return $this
106
     */
107 12
    public function setPrivateKey($key)
108
    {
109 12
        $this->privateKey = $key;
110
111 12
        return $this;
112
    }
113
114
    /**
115
     * Set the passphrase for the privatekey.
116
     *
117
     * @param string $passphrase
118
     *
119
     * @return $this
120
     */
121 6
    public function setPassphrase($passphrase)
122
    {
123 6
        $this->passphrase = $passphrase;
124
125 6
        return $this;
126
    }
127
128
    /**
129
     * @param boolean $useAgent
130
     *
131
     * @return $this
132
     */
133
    public function setUseAgent($useAgent)
134
    {
135
        $this->useAgent = (bool) $useAgent;
136
137
        return $this;
138
    }
139
140
    /**
141
     * @param Agent $agent
142
     *
143
     * @return $this
144
     */
145
    public function setAgent(Agent $agent)
146
    {
147
        $this->agent = $agent;
148
149
        return $this;
150
    }
151
152
    /**
153
     * Set permissions for new directory
154
     *
155
     * @param int $directoryPerm
156
     *
157
     * @return $this
158
     */
159 6
    public function setDirectoryPerm($directoryPerm)
160 1
    {
161 6
        $this->directoryPerm = $directoryPerm;
162
163 6
        return $this;
164
    }
165
166
    /**
167
     * Get permissions for new directory
168
     *
169
     * @return int
170
     */
171 3
    public function getDirectoryPerm()
172
    {
173 3
        return $this->directoryPerm;
174
    }
175
176
    /**
177
     * Inject the SFTP instance.
178
     *
179
     * @param SFTP $connection
180
     *
181
     * @return $this
182
     */
183 36
    public function setNetSftpConnection(SFTP $connection)
184
    {
185 36
        $this->connection = $connection;
186
187 36
        return $this;
188
    }
189
190
    /**
191
     * Connect.
192
     */
193 27
    public function connect()
194
    {
195 27
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
196 27
        $this->connection->disableStatCache();
197 27
        $this->login();
198 18
        $this->setConnectionRoot();
199 15
    }
200
201
    /**
202
     * Login.
203
     *
204
     * @throws LogicException
205
     */
206 27
    protected function login()
207
    {
208 27
        if ($this->hostFingerprint) {
209 9
            $publicKey = $this->connection->getServerPublicHostKey();
210
211 9
            if ($publicKey === false) {
212 3
                throw new LogicException('Could not connect to server to verify public key.');
213
            }
214
215 6
            $actualFingerprint = $this->getHexFingerprintFromSshPublicKey($publicKey);
216
217 6
            if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) {
218 3
                throw new LogicException('The authenticity of host '.$this->host.' can\'t be established.');
219
            }
220 1
        }
221
222 21
        $authentication = $this->getAuthentication();
223
224
225 21
        if (! $this->connection->login($this->getUsername(), $authentication)) {
226
227
            //try double authentication, key is already given so now give password
228 4
            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...
229 6
                || ! $this->connection->login($this->getUsername(), $this->getPassword())) {
230 3
                throw new LogicException('Could not login with username: '.$this->getUsername().', host: '.$this->host);
231
            }
232 1
        }
233
234 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...
235
            $authentication->startSSHForwarding($this->connection);
236
        }
237 18
    }
238
239
    /**
240
     * Convert the SSH RSA public key into a hex formatted fingerprint.
241
     *
242
     * @param string $publickey
243
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
244
     */
245 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
246
    {
247 6
        $content = explode(' ', $publickey, 3);
248 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
249
    }
250
251
    /**
252
     * Set the connection root.
253
     */
254 18
    protected function setConnectionRoot()
255
    {
256 18
        $root = $this->getRoot();
257
258 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...
259 12
            return;
260
        }
261
262 6
        if (! $this->connection->chdir($root)) {
263 3
            throw new RuntimeException('Root is invalid or does not exist: '.$root);
264
        }
265 3
        $this->root = $this->connection->pwd() . $this->separator;
266 3
    }
267
268
    /**
269
     * Get the password, either the private key or a plain text password.
270
     *
271
     * @return Agent|RSA|string
272
     */
273 24
    public function getAuthentication()
274
    {
275 24
        if ($this->useAgent) {
276
            return $this->getAgent();
277
        }
278
279 24
        if ($this->privateKey) {
280 6
            return $this->getPrivateKey();
281
        }
282
283 18
        return $this->getPassword();
284
    }
285
286
    /**
287
     * Get the private key with the password or private key contents.
288
     *
289
     * @return RSA
290
     */
291 12
    public function getPrivateKey()
292
    {
293 12
        if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {
294 3
            $this->privateKey = file_get_contents($this->privateKey);
295 1
        }
296
297 12
        $key = new RSA();
298
299 12
        if ($password = $this->getPassphrase()) {
300 12
            $key->setPassword($password);
301 4
        }
302
303 12
        $key->loadKey($this->privateKey);
304
305 12
        return $key;
306
    }
307
308
    /**
309
     * @return string
310
     */
311 21
    public function getPassphrase()
312
    {
313 21
        if ($this->passphrase === null) {
314
            //Added for backward compatibility
315 15
            return $this->getPassword();
316
        }
317 6
        return $this->passphrase;
318
    }
319
320
    /**
321
     * @return Agent|bool
322
     */
323
    public function getAgent()
324
    {
325
        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...
326
            $this->agent = new Agent();
327
        }
328
329
        return $this->agent;
330
    }
331
332
    /**
333
     * List the contents of a directory.
334
     *
335
     * @param string $directory
336
     * @param bool   $recursive
337
     *
338
     * @return array
339
     */
340 9
    protected function listDirectoryContents($directory, $recursive = true)
341
    {
342 9
        $result = [];
343 9
        $connection = $this->getConnection();
344 9
        $location = $this->prefix($directory);
345 9
        $listing = $connection->rawlist($location);
346
347 9
        if ($listing === false) {
348 3
            return [];
349
        }
350
351 9
        foreach ($listing as $filename => $object) {
352
            // When directory entries have a numeric filename they are changed to int
353 9
            $filename = (string) $filename;
354 9
            if (in_array($filename, ['.', '..'])) {
355 3
                continue;
356
            }
357
358 9
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
359 9
            $result[] = $this->normalizeListingObject($path, $object);
360
361 9
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
362 5
                $result = array_merge($result, $this->listDirectoryContents($path));
363 1
            }
364 3
        }
365
366 9
        return $result;
367
    }
368
369
    /**
370
     * Normalize a listing response.
371
     *
372
     * @param string $path
373
     * @param array  $object
374
     *
375
     * @return array
376
     */
377 9
    protected function normalizeListingObject($path, array $object)
378
    {
379 9
        $permissions = $this->normalizePermissions($object['permissions']);
380 9
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
381
382 9
        $timestamp = $object['mtime'];
383
384 9
        if ($type === 'dir') {
385 9
            return compact('path', 'timestamp', 'type');
386
        }
387
388 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
389 6
        $size = (int) $object['size'];
390
391 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
392
    }
393
394
    /**
395
     * Disconnect.
396
     */
397 18
    public function disconnect()
398
    {
399 18
        $this->connection = null;
400 18
    }
401
402
    /**
403
     * @inheritdoc
404
     */
405 6
    public function write($path, $contents, Config $config)
406
    {
407 6
        if ($this->upload($path, $contents, $config) === false) {
408 6
            return false;
409
        }
410
411 6
        return compact('contents', 'path');
412
    }
413
414
    /**
415
     * @inheritdoc
416
     */
417 6
    public function writeStream($path, $resource, Config $config)
418
    {
419 6
        if ($this->upload($path, $resource, $config) === false) {
420 6
            return false;
421
        }
422
423 6
        return compact('path');
424
    }
425
426
    /**
427
     * Upload a file.
428
     *
429
     * @param string          $path
430
     * @param string|resource $contents
431
     * @param Config          $config
432
     * @return bool
433
     */
434 12
    public function upload($path, $contents, Config $config)
435
    {
436 12
        $connection = $this->getConnection();
437 12
        $this->ensureDirectory(Util::dirname($path));
438 12
        $config = Util::ensureConfig($config);
439
440 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
441 12
            return false;
442
        }
443
444 12
        if ($config && $visibility = $config->get('visibility')) {
445 6
            $this->setVisibility($path, $visibility);
446 2
        }
447
448 12
        return true;
449
    }
450
451
    /**
452
     * @inheritdoc
453
     */
454 6
    public function read($path)
455
    {
456 6
        $connection = $this->getConnection();
457
458 6
        if (($contents = $connection->get($path)) === false) {
459 6
            return false;
460
        }
461
462 6
        return compact('contents', 'path');
463
    }
464
465
    /**
466
     * @inheritdoc
467
     */
468 3
    public function readStream($path)
469
    {
470 3
        $stream = tmpfile();
471 3
        $connection = $this->getConnection();
472
473 3
        if ($connection->get($path, $stream) === false) {
474 3
            fclose($stream);
475 3
            return false;
476
        }
477
478 3
        rewind($stream);
479
480 3
        return compact('stream', 'path');
481
    }
482
483
    /**
484
     * @inheritdoc
485
     */
486 3
    public function update($path, $contents, Config $config)
487
    {
488 3
        return $this->write($path, $contents, $config);
489
    }
490
491
    /**
492
     * @inheritdoc
493
     */
494 3
    public function updateStream($path, $contents, Config $config)
495
    {
496 3
        return $this->writeStream($path, $contents, $config);
497
    }
498
499
    /**
500
     * @inheritdoc
501
     */
502 3
    public function delete($path)
503
    {
504 3
        $connection = $this->getConnection();
505
506 3
        return $connection->delete($path);
507
    }
508
509
    /**
510
     * @inheritdoc
511
     */
512 3
    public function rename($path, $newpath)
513
    {
514 3
        $connection = $this->getConnection();
515
516 3
        return $connection->rename($path, $newpath);
517
    }
518
519
    /**
520
     * @inheritdoc
521
     */
522 3
    public function deleteDir($dirname)
523 1
    {
524 3
        $connection = $this->getConnection();
525
526 3
        return $connection->delete($dirname, true);
527
    }
528
529
    /**
530
     * @inheritdoc
531
     */
532 39
    public function has($path)
533
    {
534 39
        return $this->getMetadata($path);
535
    }
536
537
    /**
538
     * @inheritdoc
539
     */
540 48
    public function getMetadata($path)
541
    {
542 48
        $connection = $this->getConnection();
543 48
        $info = $connection->stat($path);
544
545 48
        if ($info === false) {
546 12
            return false;
547
        }
548
549 39
        $result = Util::map($info, $this->statMap);
550 39
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
551 39
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
552
553 39
        return $result;
554
    }
555
556
    /**
557
     * @inheritdoc
558
     */
559 6
    public function getTimestamp($path)
560
    {
561 6
        return $this->getMetadata($path);
562
    }
563
564
    /**
565
     * @inheritdoc
566
     */
567 3
    public function getMimetype($path)
568
    {
569 3
        if (! $data = $this->read($path)) {
570 3
            return false;
571
        }
572
573 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
574
575 3
        return $data;
576
    }
577
578
    /**
579
     * @inheritdoc
580
     */
581 3
    public function createDir($dirname, Config $config)
582
    {
583 3
        $connection = $this->getConnection();
584
585 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
586 3
            return false;
587
        }
588
589 3
        return ['path' => $dirname];
590
    }
591
592
    /**
593
     * @inheritdoc
594
     */
595 6
    public function getVisibility($path)
596
    {
597 6
        return $this->getMetadata($path);
598
    }
599
600
    /**
601
     * @inheritdoc
602
     */
603 12
    public function setVisibility($path, $visibility)
604
    {
605 12
        $visibility = ucfirst($visibility);
606
607 12
        if (! isset($this->{'perm'.$visibility})) {
608 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
609
        }
610
611 9
        $connection = $this->getConnection();
612
613 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
614
    }
615
616
    /**
617
     * @inheritdoc
618
     */
619 6
    public function isConnected()
620
    {
621 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...
622 3
            return true;
623
        }
624
625 3
        return false;
626
    }
627
}
628