Completed
Push — master ( f85e07...518ac7 )
by Frank
01:56
created

SftpAdapter   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 574
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.47%

Importance

Changes 0
Metric Value
wmc 76
lcom 1
cbo 4
dl 0
loc 574
ccs 172
cts 186
cp 0.9247
rs 2.32
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
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 updateStream() 0 4 1
A delete() 0 6 1
A rename() 0 6 1
A connect() 0 7 2
B login() 0 26 6
A getHexFingerprintFromSshPublicKey() 0 5 1
A setConnectionRoot() 0 13 3
A getAuthentication() 0 12 3
A getPrivateKey() 0 16 4
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 deleteDir() 0 6 1
A has() 0 4 1
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
A getMetadata() 0 15 4

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