Completed
Pull Request — master (#24)
by Maxime
02:50
created

SftpAdapter   C

Complexity

Total Complexity 64

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 98.1%

Importance

Changes 14
Bugs 4 Features 4
Metric Value
wmc 64
c 14
b 4
f 4
lcom 1
cbo 4
dl 0
loc 475
ccs 155
cts 158
cp 0.981
rs 5.8364

31 Methods

Rating   Name   Duplication   Size   Complexity  
A prefix() 0 4 1
A setPrivateKey() 0 6 1
A setDirectoryPerm() 0 6 1
A getDirectoryPerm() 0 4 1
A setNetSftpConnection() 0 6 1
A connect() 0 6 2
A login() 0 11 3
A setConnectionRoot() 0 13 3
A getPassword() 0 12 3
A getPrivateKey() 0 16 3
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 $privatekey;
30
31
    /**
32
     * @var bool
33
     */
34
    protected $agent;
35
36
    /**
37
     * @var array
38
     */
39
    protected $configurable = ['host', 'port', 'username', 'password', 'agent', 'timeout', 'root', 'privateKey', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
40
41
    /**
42
     * @var array
43
     */
44
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
45
46
    /**
47
     * @var int
48
     */
49
    protected $directoryPerm = 0744;
50
51
    /**
52
     * Prefix a path.
53
     *
54
     * @param string $path
55
     *
56
     * @return string
57
     */
58 6
    protected function prefix($path)
59
    {
60 6
        return $this->root.ltrim($path, $this->separator);
61
    }
62
63
    /**
64
     * Set the private key (string or path to local file).
65
     *
66
     * @param string $key
67
     *
68
     * @return $this
69
     */
70 9
    public function setPrivateKey($key)
71
    {
72 9
        $this->privatekey = $key;
73
74 9
        return $this;
75
    }
76
77
    /**
78
     * Set permissions for new directory
79
     *
80
     * @param int $directoryPerm
81
     *
82
     * @return $this
83
     */
84 6
    public function setDirectoryPerm($directoryPerm)
85
    {
86 6
        $this->directoryPerm = $directoryPerm;
87
88 6
        return $this;
89
    }
90
91
    /**
92
     * Get permissions for new directory
93
     *
94
     * @return int
95
     */
96 3
    public function getDirectoryPerm()
97
    {
98 3
        return $this->directoryPerm;
99
    }
100
101
    /**
102
     * Inject the SFTP instance.
103
     *
104
     * @param SFTP $connection
105
     *
106
     * @return $this
107
     */
108 21
    public function setNetSftpConnection(SFTP $connection)
109
    {
110 21
        $this->connection = $connection;
111
112 21
        return $this;
113
    }
114
115
    /**
116
     * Connect.
117
     */
118 12
    public function connect()
119
    {
120 12
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
121 12
        $this->login();
122 9
        $this->setConnectionRoot();
123 6
    }
124
125
    /**
126
     * Login.
127
     *
128
     * @throws LogicException
129
     */
130 12
    protected function login()
131
    {
132 12
        $authentication = $this->getPassword();
133 12
        if (! $this->connection->login($this->username, $authentication)) {
134 3
            throw new LogicException('Could not login with username: '.$this->username.', host: '.$this->host);
135
        }
136
137 9
        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...
138
            $authentication->startSSHForwarding($this->connection);
139
        }
140 9
    }
141
142
    /**
143
     * Set the connection root.
144
     */
145 9
    protected function setConnectionRoot()
146
    {
147 9
        $root = $this->getRoot();
148
149 9
        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...
150 3
            return;
151
        }
152
153 6
        if (! $this->connection->chdir($root)) {
154 3
            throw new RuntimeException('Root is invalid or does not exist: '.$root);
155
        }
156 3
        $this->root = $this->connection->pwd() . $this->separator;
157 3
    }
158
159
    /**
160
     * Get the password, either the private key or a plain text password.
161
     *
162
     * @return RSA|string
163
     */
164 15
    public function getPassword()
165
    {
166 15
        if ($this->agent) {
167
            return new Agent();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \phpseclib\System\SSH\Agent(); (phpseclib\System\SSH\Agent) is incompatible with the return type of the parent method League\Flysystem\Adapter...FtpAdapter::getPassword of type string|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
168
        }
169
170 15
        if ($this->privatekey) {
171 3
            return $this->getPrivateKey();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getPrivateKey(); (phpseclib\Crypt\RSA) is incompatible with the return type of the parent method League\Flysystem\Adapter...FtpAdapter::getPassword of type string|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
172
        }
173
174 12
        return $this->password;
175
    }
176
177
    /**
178
     * Get the private get with the password or private key contents.
179
     *
180
     * @return RSA
181
     */
182 9
    public function getPrivateKey()
183
    {
184 9
        if (@is_file($this->privatekey)) {
185 3
            $this->privatekey = file_get_contents($this->privatekey);
186 3
        }
187
188 9
        $key = new RSA();
189
190 9
        if ($this->password) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->password of type string|null is loosely compared to true; 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...
191 9
            $key->setPassword($this->password);
192 9
        }
193
194 9
        $key->loadKey($this->privatekey);
195
196 9
        return $key;
197
    }
198
199
    /**
200
     * List the contents of a directory.
201
     *
202
     * @param string $directory
203
     * @param bool   $recursive
204
     *
205
     * @return array
206
     */
207 6
    protected function listDirectoryContents($directory, $recursive = true)
208
    {
209 6
        $result = [];
210 6
        $connection = $this->getConnection();
211 6
        $location = $this->prefix($directory);
212 6
        $listing = $connection->rawlist($location);
213
214 6
        if ($listing === false) {
215 3
            return [];
216
        }
217
218 6
        foreach ($listing as $filename => $object) {
219 6
            if (in_array($filename, ['.', '..'])) {
220 3
                continue;
221
            }
222
223 6
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
224 6
            $result[] = $this->normalizeListingObject($path, $object);
225
226 6
            if ($recursive && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
227 3
                $result = array_merge($result, $this->listDirectoryContents($path));
228 3
            }
229 6
        }
230
231 6
        return $result;
232
    }
233
234
    /**
235
     * Normalize a listing response.
236
     *
237
     * @param string $path
238
     * @param array  $object
239
     *
240
     * @return array
241
     */
242 6
    protected function normalizeListingObject($path, array $object)
243
    {
244 6
        $permissions = $this->normalizePermissions($object['permissions']);
245 6
        $type = ($object['type'] === 1) ? 'file' : 'dir' ;
246 6
        $timestamp = $object['mtime'];
247
248 6
        if ($type === 'dir') {
249 6
            return compact('path', 'timestamp', 'type');
250
        }
251
252 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
253 6
        $size = (int) $object['size'];
254
255 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
256
    }
257
258
    /**
259
     * Disconnect.
260
     */
261 6
    public function disconnect()
262
    {
263 6
        $this->connection = null;
264 6
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269 6
    public function write($path, $contents, Config $config)
270
    {
271 6
        if ($this->upload($path, $contents, $config) === false) {
272 6
            return false;
273
        }
274
275 6
        return compact('contents', 'visibility', 'path');
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281 6
    public function writeStream($path, $resource, Config $config)
282
    {
283 6
        if ($this->upload($path, $resource, $config) === false) {
284 6
            return false;
285
        }
286
287 6
        return compact('visibility', 'path');
288
    }
289
290
    /**
291
     * Upload a file.
292
     *
293
     * @param string          $path
294
     * @param string|resource $contents
295
     * @param Config          $config
296
     * @return bool
297
     */
298 12
    public function upload($path, $contents, Config $config)
299
    {
300 12
        $connection = $this->getConnection();
301 12
        $this->ensureDirectory(Util::dirname($path));
302 12
        $config = Util::ensureConfig($config);
303
304 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
305 12
            return false;
306
        }
307
308 12
        if ($config && $visibility = $config->get('visibility')) {
309 6
            $this->setVisibility($path, $visibility);
310 6
        }
311
312 12
        return true;
313
    }
314
315
    /**
316
     * @inheritdoc
317
     */
318 6
    public function read($path)
319
    {
320 6
        $connection = $this->getConnection();
321
322 6
        if (($contents = $connection->get($path)) === false) {
323 6
            return false;
324
        }
325
326 6
        return compact('contents');
327
    }
328
329
    /**
330
     * @inheritdoc
331
     */
332 3
    public function readStream($path)
333
    {
334 3
        $stream = tmpfile();
335 3
        $connection = $this->getConnection();
336
337 3
        if ($connection->get($path, $stream) === false) {
338 3
            fclose($stream);
339 3
            return false;
340
        }
341
342 3
        rewind($stream);
343
344 3
        return compact('stream');
345
    }
346
347
    /**
348
     * @inheritdoc
349
     */
350 3
    public function update($path, $contents, Config $config)
351
    {
352 3
        return $this->write($path, $contents, $config);
353
    }
354
355
    /**
356
     * @inheritdoc
357
     */
358 3
    public function updateStream($path, $contents, Config $config)
359
    {
360 3
        return $this->writeStream($path, $contents, $config);
361
    }
362
363
    /**
364
     * @inheritdoc
365
     */
366 3
    public function delete($path)
367
    {
368 3
        $connection = $this->getConnection();
369
370 3
        return $connection->delete($path);
371
    }
372
373
    /**
374
     * @inheritdoc
375
     */
376 3
    public function rename($path, $newpath)
377
    {
378 3
        $connection = $this->getConnection();
379
380 3
        return $connection->rename($path, $newpath);
381
    }
382
383
    /**
384
     * @inheritdoc
385
     */
386 3
    public function deleteDir($dirname)
387
    {
388 3
        $connection = $this->getConnection();
389
390 3
        return $connection->delete($dirname, true);
391
    }
392
393
    /**
394
     * @inheritdoc
395
     */
396 39
    public function has($path)
397
    {
398 39
        return $this->getMetadata($path);
399
    }
400
401
    /**
402
     * @inheritdoc
403
     */
404 48
    public function getMetadata($path)
405
    {
406 48
        $connection = $this->getConnection();
407 48
        $info = $connection->stat($path);
408
409 48
        if ($info === false) {
410 12
            return false;
411
        }
412
413 39
        $result = Util::map($info, $this->statMap);
414 39
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
415 39
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
416
417 39
        return $result;
418
    }
419
420
    /**
421
     * @inheritdoc
422
     */
423 6
    public function getTimestamp($path)
424
    {
425 6
        return $this->getMetadata($path);
426
    }
427
428
    /**
429
     * @inheritdoc
430
     */
431 3
    public function getMimetype($path)
432
    {
433 3
        if (! $data = $this->read($path)) {
434 3
            return false;
435
        }
436
437 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
438
439 3
        return $data;
440
    }
441
442
    /**
443
     * @inheritdoc
444
     */
445 3
    public function createDir($dirname, Config $config)
446
    {
447 3
        $connection = $this->getConnection();
448
449 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
450 3
            return false;
451
        }
452
453 3
        return ['path' => $dirname];
454
    }
455
456
    /**
457
     * @inheritdoc
458
     */
459 6
    public function getVisibility($path)
460
    {
461 6
        return $this->getMetadata($path);
462
    }
463
464
    /**
465
     * @inheritdoc
466
     */
467 12
    public function setVisibility($path, $visibility)
468
    {
469 12
        $visibility = ucfirst($visibility);
470
471 12
        if (! isset($this->{'perm'.$visibility})) {
472 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
473
        }
474
475 9
        $connection = $this->getConnection();
476
477 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
478
    }
479
480
    /**
481
     * @inheritdoc
482
     */
483 6
    public function isConnected()
484
    {
485 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...
486 3
            return true;
487
        }
488
489 3
        return false;
490
    }
491
}
492