Completed
Pull Request — master (#56)
by
unknown
02:50
created

SftpAdapter   C

Complexity

Total Complexity 76

Size/Duplication

Total Lines 573
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.39%

Importance

Changes 0
Metric Value
wmc 76
lcom 1
cbo 4
dl 0
loc 573
ccs 170
cts 184
cp 0.9239
rs 5.488
c 0
b 0
f 0

37 Methods

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