Completed
Push — master ( 3a6b48...ed7014 )
by Frank
01:27
created

SftpAdapter   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 617
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 92.89%

Importance

Changes 0
Metric Value
wmc 81
lcom 1
cbo 6
dl 0
loc 617
ccs 183
cts 197
cp 0.9289
rs 1.983
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 connect() 0 7 2
A setNetSftpConnection() 0 6 1
B login() 0 36 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 1
    }
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 1
        }
219
220 21
        $authentication = $this->getAuthentication();
221
222
223 21
        if ($this->connection->login($this->getUsername(), $authentication)) {
224 15
            goto past_login;
225
        }
226
227
        // try double authentication, key is already given so now give password
228 6
        if ($authentication instanceof RSA && $this->connection->login($this->getUsername(), $this->getPassword())) {
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 3
            goto past_login;
230
        }
231
232 3
        throw new ConnectionErrorException('Could not login with username: '.$this->getUsername().', host: '.$this->host);
233
234
        past_login:
235
236 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...
237
            $authentication->startSSHForwarding($this->connection);
238
        }
239 18
    }
240
241
    /**
242
     * Convert the SSH RSA public key into a hex formatted fingerprint.
243
     *
244
     * @param string $publickey
245
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
246
     */
247 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
248
    {
249 6
        $content = explode(' ', $publickey, 3);
250 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
251
    }
252
253
    /**
254
     * Set the connection root.
255
     *
256
     * @throws InvalidRootException
257
     */
258 18
    protected function setConnectionRoot()
259
    {
260 18
        $root = $this->getRoot();
261
262 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...
263 12
            return;
264
        }
265
266 6
        if (! $this->connection->chdir($root)) {
267 3
            throw new InvalidRootException('Root is invalid or does not exist: '.$root);
268
        }
269 3
        $this->root = $this->connection->pwd() . $this->separator;
270 3
    }
271
272
    /**
273
     * Get the password, either the private key or a plain text password.
274
     *
275
     * @return Agent|RSA|string
276
     */
277 24
    public function getAuthentication()
278
    {
279 24
        if ($this->useAgent) {
280
            return $this->getAgent();
281
        }
282
283 24
        if ($this->privateKey) {
284 6
            return $this->getPrivateKey();
285
        }
286
287 18
        return $this->getPassword();
288
    }
289
290
    /**
291
     * Get the private key with the password or private key contents.
292
     *
293
     * @return RSA
294
     */
295 12
    public function getPrivateKey()
296
    {
297 12
        if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {
298 3
            $this->privateKey = file_get_contents($this->privateKey);
299 1
        }
300
301 12
        $key = new RSA();
302
303 12
        if ($password = $this->getPassphrase()) {
304 12
            $key->setPassword($password);
305 4
        }
306
307 12
        $key->loadKey($this->privateKey);
308
309 12
        return $key;
310
    }
311
312
    /**
313
     * @return string
314
     */
315 21
    public function getPassphrase()
316
    {
317 21
        if ($this->passphrase === null) {
318
            //Added for backward compatibility
319 15
            return $this->getPassword();
320
        }
321 6
        return $this->passphrase;
322
    }
323
324
    /**
325
     * @return Agent|bool
326
     */
327
    public function getAgent()
328
    {
329
        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...
330
            $this->agent = new Agent();
331
        }
332
333
        return $this->agent;
334
    }
335
336
    /**
337
     * List the contents of a directory.
338
     *
339
     * @param string $directory
340
     * @param bool   $recursive
341
     *
342
     * @return array
343
     */
344 9
    protected function listDirectoryContents($directory, $recursive = true)
345
    {
346 9
        $result = [];
347 9
        $connection = $this->getConnection();
348 9
        $location = $this->prefix($directory);
349 9
        $listing = $connection->rawlist($location);
350
351 9
        if ($listing === false) {
352 3
            return [];
353
        }
354
355 9
        foreach ($listing as $filename => $object) {
356
            // When directory entries have a numeric filename they are changed to int
357 9
            $filename = (string) $filename;
358 9
            if (in_array($filename, ['.', '..'])) {
359 3
                continue;
360
            }
361
362 9
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
363 9
            $result[] = $this->normalizeListingObject($path, $object);
364
365 9
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
366 5
                $result = array_merge($result, $this->listDirectoryContents($path));
367 1
            }
368 3
        }
369
370 9
        return $result;
371
    }
372
373
    /**
374
     * Normalize a listing response.
375
     *
376
     * @param string $path
377
     * @param array  $object
378
     *
379
     * @return array
380
     */
381 9
    protected function normalizeListingObject($path, array $object)
382
    {
383 9
        $permissions = $this->normalizePermissions($object['permissions']);
384 9
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
385
386 9
        $timestamp = $object['mtime'];
387
388 9
        if ($type === 'dir') {
389 9
            return compact('path', 'timestamp', 'type');
390
        }
391
392 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
393 6
        $size = (int) $object['size'];
394
395 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
396
    }
397
398
    /**
399
     * Disconnect.
400
     */
401 18
    public function disconnect()
402
    {
403 18
        $this->connection = null;
404 18
    }
405
406
    /**
407
     * @inheritdoc
408
     */
409 6
    public function write($path, $contents, Config $config)
410
    {
411 6
        if ($this->upload($path, $contents, $config) === false) {
412 6
            return false;
413
        }
414
415 6
        return compact('contents', 'path');
416
    }
417
418
    /**
419
     * @inheritdoc
420
     */
421 6
    public function writeStream($path, $resource, Config $config)
422
    {
423 6
        if ($this->upload($path, $resource, $config) === false) {
424 6
            return false;
425
        }
426
427 6
        return compact('path');
428
    }
429
430
    /**
431
     * Upload a file.
432
     *
433
     * @param string          $path
434
     * @param string|resource $contents
435
     * @param Config          $config
436
     * @return bool
437
     */
438 12
    public function upload($path, $contents, Config $config)
439
    {
440 12
        $connection = $this->getConnection();
441 12
        $this->ensureDirectory(Util::dirname($path));
442 12
        $config = Util::ensureConfig($config);
443
444 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
445 12
            return false;
446
        }
447
448 12
        if ($config && $visibility = $config->get('visibility')) {
449 6
            $this->setVisibility($path, $visibility);
450 2
        }
451
452 12
        return true;
453
    }
454
455
    /**
456
     * @inheritdoc
457
     */
458 6
    public function read($path)
459
    {
460 6
        $connection = $this->getConnection();
461
462 6
        if (($contents = $connection->get($path)) === false) {
463 6
            return false;
464
        }
465
466 6
        return compact('contents', 'path');
467
    }
468
469
    /**
470
     * @inheritdoc
471
     */
472 3
    public function readStream($path)
473
    {
474 3
        $stream = tmpfile();
475 3
        $connection = $this->getConnection();
476
477 3
        if ($connection->get($path, $stream) === false) {
478 3
            fclose($stream);
479 3
            return false;
480
        }
481
482 3
        rewind($stream);
483
484 3
        return compact('stream', 'path');
485
    }
486
487
    /**
488
     * @inheritdoc
489
     */
490 3
    public function update($path, $contents, Config $config)
491
    {
492 3
        return $this->write($path, $contents, $config);
493
    }
494
495
    /**
496
     * @inheritdoc
497
     */
498 3
    public function updateStream($path, $contents, Config $config)
499
    {
500 3
        return $this->writeStream($path, $contents, $config);
501
    }
502
503
    /**
504
     * @inheritdoc
505
     */
506 3
    public function delete($path)
507
    {
508 3
        $connection = $this->getConnection();
509
510 3
        return $connection->delete($path);
511
    }
512
513
    /**
514
     * @inheritdoc
515
     */
516 4
    public function rename($path, $newpath)
517
    {
518 4
        $connection = $this->getConnection();
519
520 3
        return $connection->rename($path, $newpath);
521 1
    }
522
523
    /**
524
     * @inheritdoc
525
     */
526 3
    public function deleteDir($dirname)
527
    {
528 3
        $connection = $this->getConnection();
529
530 3
        return $connection->delete($dirname, true);
531
    }
532
533
    /**
534
     * @inheritdoc
535
     */
536 42
    public function has($path)
537
    {
538 42
        return $this->getMetadata($path);
539
    }
540
541
    /**
542
     * @inheritdoc
543
     */
544 50
    public function getMetadata($path)
545
    {
546 50
        $connection = $this->getConnection();
547 50
        $info = $connection->stat($path);
548
549 50
        if ($info === false) {
550 12
            return false;
551
        }
552
553 41
        $result = Util::map($info, $this->statMap);
554 41
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
555 41
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
556
557 41
        return $result;
558
    }
559
560
    /**
561
     * @inheritdoc
562
     */
563 6
    public function getTimestamp($path)
564
    {
565 6
        return $this->getMetadata($path);
566
    }
567
568
    /**
569
     * @inheritdoc
570
     */
571 3
    public function getMimetype($path)
572
    {
573 3
        if (! $data = $this->read($path)) {
574 3
            return false;
575
        }
576
577 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
578
579 3
        return $data;
580
    }
581
582
    /**
583
     * @inheritdoc
584
     */
585 3
    public function createDir($dirname, Config $config)
586
    {
587 3
        $connection = $this->getConnection();
588
589 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
590 3
            return false;
591
        }
592
593 3
        return ['path' => $dirname];
594
    }
595
596
    /**
597
     * @inheritdoc
598
     */
599 6
    public function getVisibility($path)
600
    {
601 6
        return $this->getMetadata($path);
602
    }
603
604
    /**
605
     * @inheritdoc
606
     */
607 12
    public function setVisibility($path, $visibility)
608
    {
609 12
        $visibility = ucfirst($visibility);
610
611 12
        if (! isset($this->{'perm'.$visibility})) {
612 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
613
        }
614
615 9
        $connection = $this->getConnection();
616
617 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
618
    }
619
620
    /**
621
     * @inheritdoc
622
     */
623 31
    public function isConnected()
624
    {
625 31
        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...
626 28
            return true;
627
        }
628
629 3
        return false;
630
    }
631
}
632