Completed
Pull Request — master (#38)
by
unknown
05:35
created

SftpAdapter   C

Complexity

Total Complexity 74

Size/Duplication

Total Lines 570
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.27%

Importance

Changes 0
Metric Value
wmc 74
lcom 1
cbo 4
dl 0
loc 570
ccs 167
cts 181
cp 0.9227
rs 5.5244
c 0
b 0
f 0

36 Methods

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