Completed
Push — master ( 15d729...c9a843 )
by Frank
13s
created

SftpAdapter   F

Complexity

Total Complexity 76

Size/Duplication

Total Lines 572
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.43%

Importance

Changes 0
Metric Value
wmc 76
lcom 1
cbo 4
dl 0
loc 572
ccs 171
cts 185
cp 0.9243
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 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 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 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 getTimestamp() 0 4 1

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 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 2
        }
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 2
        }
271
272 9
        $key = new RSA();
273
274 9
        if ($password = $this->getPassword()) {
275 9
            $key->setPassword($password);
276 6
        }
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 6
    protected function listDirectoryContents($directory, $recursive = true)
304
    {
305 6
        $result = [];
306 6
        $connection = $this->getConnection();
307 6
        $location = $this->prefix($directory);
308 6
        $listing = $connection->rawlist($location);
309
310 6
        if ($listing === false) {
311 3
            return [];
312
        }
313
314 6
        foreach ($listing as $filename => $object) {
315 6
            if (in_array($filename, ['.', '..'])) {
316 3
                continue;
317
            }
318
319 6
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
320 6
            $result[] = $this->normalizeListingObject($path, $object);
321
322 6
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
323 4
                $result = array_merge($result, $this->listDirectoryContents($path));
324 2
            }
325 4
        }
326
327 6
        return $result;
328
    }
329
330
    /**
331
     * Normalize a listing response.
332
     *
333
     * @param string $path
334
     * @param array  $object
335
     *
336
     * @return array
337
     */
338 6
    protected function normalizeListingObject($path, array $object)
339
    {
340 6
        $permissions = $this->normalizePermissions($object['permissions']);
341 6
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
342
343 6
        $timestamp = $object['mtime'];
344
345 6
        if ($type === 'dir') {
346 6
            return compact('path', 'timestamp', 'type');
347
        }
348
349 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
350 6
        $size = (int) $object['size'];
351
352 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
353
    }
354
355
    /**
356
     * Disconnect.
357
     */
358 18
    public function disconnect()
359
    {
360 18
        $this->connection = null;
361 18
    }
362
363
    /**
364
     * @inheritdoc
365
     */
366 6
    public function write($path, $contents, Config $config)
367
    {
368 6
        if ($this->upload($path, $contents, $config) === false) {
369 6
            return false;
370
        }
371
372 6
        return compact('contents', 'path');
373
    }
374
375
    /**
376
     * @inheritdoc
377
     */
378 6
    public function writeStream($path, $resource, Config $config)
379
    {
380 6
        if ($this->upload($path, $resource, $config) === false) {
381 6
            return false;
382
        }
383
384 6
        return compact('path');
385
    }
386
387
    /**
388
     * Upload a file.
389
     *
390
     * @param string          $path
391
     * @param string|resource $contents
392
     * @param Config          $config
393
     * @return bool
394
     */
395 12
    public function upload($path, $contents, Config $config)
396
    {
397 12
        $connection = $this->getConnection();
398 12
        $this->ensureDirectory(Util::dirname($path));
399 12
        $config = Util::ensureConfig($config);
400
401 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
402 12
            return false;
403
        }
404
405 12
        if ($config && $visibility = $config->get('visibility')) {
406 6
            $this->setVisibility($path, $visibility);
407 4
        }
408
409 12
        return true;
410
    }
411
412
    /**
413
     * @inheritdoc
414
     */
415 6
    public function read($path)
416
    {
417 6
        $connection = $this->getConnection();
418
419 6
        if (($contents = $connection->get($path)) === false) {
420 6
            return false;
421
        }
422
423 6
        return compact('contents', 'path');
424
    }
425
426
    /**
427
     * @inheritdoc
428
     */
429 3
    public function readStream($path)
430
    {
431 3
        $stream = tmpfile();
432 3
        $connection = $this->getConnection();
433
434 3
        if ($connection->get($path, $stream) === false) {
435 3
            fclose($stream);
436 3
            return false;
437
        }
438
439 3
        rewind($stream);
440
441 3
        return compact('stream', 'path');
442
    }
443
444
    /**
445
     * @inheritdoc
446
     */
447 3
    public function update($path, $contents, Config $config)
448
    {
449 3
        return $this->write($path, $contents, $config);
450
    }
451
452
    /**
453
     * @inheritdoc
454
     */
455 3
    public function updateStream($path, $contents, Config $config)
456
    {
457 3
        return $this->writeStream($path, $contents, $config);
458
    }
459
460
    /**
461
     * @inheritdoc
462
     */
463 3
    public function delete($path)
464
    {
465 3
        $connection = $this->getConnection();
466
467 3
        return $connection->delete($path);
468
    }
469
470
    /**
471
     * @inheritdoc
472
     */
473 3
    public function rename($path, $newpath)
474
    {
475 3
        $connection = $this->getConnection();
476
477 3
        return $connection->rename($path, $newpath);
478
    }
479
480
    /**
481
     * @inheritdoc
482
     */
483 3
    public function deleteDir($dirname)
484
    {
485 3
        $connection = $this->getConnection();
486
487 3
        return $connection->delete($dirname, true);
488
    }
489
490
    /**
491
     * @inheritdoc
492
     */
493 42
    public function has($path)
494
    {
495 42
        return $this->getMetadata($path);
496
    }
497
498
    /**
499
     * @inheritdoc
500
     */
501 50
    public function getMetadata($path)
502
    {
503 50
        $connection = $this->getConnection();
504 50
        $info = $connection->stat($path);
505
506 50
        if ($info === false) {
507 12
            return false;
508
        }
509
510 41
        $result = Util::map($info, $this->statMap);
511 41
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
512 41
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
513
514 42
        return $result;
515
    }
516
517
    /**
518
     * @inheritdoc
519
     */
520 6
    public function getTimestamp($path)
521
    {
522 6
        return $this->getMetadata($path);
523 1
    }
524
525
    /**
526
     * @inheritdoc
527
     */
528 3
    public function getMimetype($path)
529
    {
530 3
        if (! $data = $this->read($path)) {
531 3
            return false;
532
        }
533
534 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
535
536 3
        return $data;
537
    }
538
539
    /**
540
     * @inheritdoc
541
     */
542 3
    public function createDir($dirname, Config $config)
543
    {
544 3
        $connection = $this->getConnection();
545
546 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
547 3
            return false;
548
        }
549
550 3
        return ['path' => $dirname];
551
    }
552
553
    /**
554
     * @inheritdoc
555
     */
556 6
    public function getVisibility($path)
557
    {
558 6
        return $this->getMetadata($path);
559
    }
560
561
    /**
562
     * @inheritdoc
563
     */
564 12
    public function setVisibility($path, $visibility)
565
    {
566 12
        $visibility = ucfirst($visibility);
567
568 12
        if (! isset($this->{'perm'.$visibility})) {
569 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
570
        }
571
572 9
        $connection = $this->getConnection();
573
574 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
575
    }
576
577
    /**
578
     * @inheritdoc
579
     */
580 30
    public function isConnected()
581
    {
582 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...
583 27
            return true;
584
        }
585
586 3
        return false;
587
    }
588
}
589