Completed
Push — master ( c49024...15d729 )
by Frank
19:52
created

SftpAdapter   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 566
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.35%

Importance

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