Completed
Push — master ( be6c8d...b8d478 )
by Frank
24:40 queued 23:38
created

SftpAdapter   C

Complexity

Total Complexity 75

Size/Duplication

Total Lines 565
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 93.02%

Importance

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