Completed
Push — master ( b18bbd...769dfa )
by Frank
03:13
created

SftpAdapter   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 559
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.27%

Importance

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