Completed
Pull Request — master (#17)
by
unknown
01:54
created

SftpAdapter   C

Complexity

Total Complexity 64

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 98.71%

Importance

Changes 13
Bugs 4 Features 4
Metric Value
wmc 64
c 13
b 4
f 4
lcom 1
cbo 4
dl 0
loc 465
ccs 153
cts 155
cp 0.9871
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 6 2
A setConnectionRoot() 0 12 3
A getPassword() 0 8 2
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
B upload() 0 21 6
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 RuntimeException;
15
16
class SftpAdapter extends AbstractFtpAdapter
17
{
18
    use StreamedCopyTrait;
19
20
    /**
21
     * @var int
22
     */
23
    protected $port = 22;
24
25
    /**
26
     * @var string
27
     */
28
    protected $privatekey;
29
30
    /**
31
     * @var array
32
     */
33
    protected $configurable = ['host', 'port', 'username', 'password', 'timeout', 'root', 'privateKey', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
34
35
    /**
36
     * @var array
37
     */
38
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
39
40
    /**
41
     * @var int
42
     */
43
    protected $directoryPerm = 0744;
44
45
    /**
46
     * Prefix a path.
47
     *
48
     * @param string $path
49
     *
50
     * @return string
51
     */
52 6
    protected function prefix($path)
53
    {
54 6
        return $this->root.ltrim($path, $this->separator);
55
    }
56
57
    /**
58
     * Set the private key (string or path to local file).
59
     *
60
     * @param string $key
61
     *
62
     * @return $this
63
     */
64 9
    public function setPrivateKey($key)
65
    {
66 9
        $this->privatekey = $key;
67
68 9
        return $this;
69
    }
70
71
    /**
72
     * Set permissions for new directory
73
     *
74
     * @param int $directoryPerm
75
     *
76
     * @return $this
77
     */
78 6
    public function setDirectoryPerm($directoryPerm)
79
    {
80 6
        $this->directoryPerm = $directoryPerm;
81
82 6
        return $this;
83
    }
84
85
    /**
86
     * Get permissions for new directory
87
     *
88
     * @return int
89
     */
90 3
    public function getDirectoryPerm()
91
    {
92 3
        return $this->directoryPerm;
93
    }
94
95
    /**
96
     * Inject the SFTP instance.
97
     *
98
     * @param SFTP $connection
99
     *
100
     * @return $this
101
     */
102 21
    public function setNetSftpConnection(SFTP $connection)
103
    {
104 21
        $this->connection = $connection;
105
106 21
        return $this;
107
    }
108
109
    /**
110
     * Connect.
111
     */
112 12
    public function connect()
113
    {
114 12
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
115 12
        $this->login();
116 9
        $this->setConnectionRoot();
117 6
    }
118
119
    /**
120
     * Login.
121
     *
122
     * @throws LogicException
123
     */
124 12
    protected function login()
125
    {
126 12
        if (! $this->connection->login($this->username, $this->getPassword())) {
127 3
            throw new LogicException('Could not login with username: '.$this->username.', host: '.$this->host);
128
        }
129 9
    }
130
131
    /**
132
     * Set the connection root.
133
     */
134 9
    protected function setConnectionRoot()
135
    {
136 9
        $root = $this->getRoot();
137
138 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...
139 3
            return;
140
        }
141
142 6
        if (! $this->connection->chdir($root)) {
143 3
            throw new RuntimeException('Root is invalid or does not exist: '.$root);
144
        }
145 3
    }
146
147
    /**
148
     * Get the password, either the private key or a plain text password.
149
     *
150
     * @return RSA|string
151
     */
152 15
    public function getPassword()
153
    {
154 15
        if ($this->privatekey) {
155 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...
156
        }
157
158 12
        return $this->password;
159
    }
160
161
    /**
162
     * Get the private get with the password or private key contents.
163
     *
164
     * @return RSA
165
     */
166 9
    public function getPrivateKey()
167
    {
168 9
        if (is_file($this->privatekey)) {
169 3
            $this->privatekey = file_get_contents($this->privatekey);
170 3
        }
171
172 9
        $key = new RSA();
173
174 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...
175 9
            $key->setPassword($this->password);
176 9
        }
177
178 9
        $key->loadKey($this->privatekey);
179
180 9
        return $key;
181
    }
182
183
    /**
184
     * List the contents of a directory.
185
     *
186
     * @param string $directory
187
     * @param bool   $recursive
188
     *
189
     * @return array
190
     */
191 6
    protected function listDirectoryContents($directory, $recursive = true)
192
    {
193 6
        $result = [];
194 6
        $connection = $this->getConnection();
195 6
        $location = $this->prefix($directory);
196 6
        $listing = $connection->rawlist($location);
197
198 6
        if ($listing === false) {
199 3
            return [];
200
        }
201
202 6
        foreach ($listing as $filename => $object) {
203 6
            if (in_array($filename, ['.', '..'])) {
204 3
                continue;
205
            }
206
207 6
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
208 6
            $result[] = $this->normalizeListingObject($path, $object);
209
210 6
            if ($recursive && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
211 3
                $result = array_merge($result, $this->listDirectoryContents($path));
212 3
            }
213 6
        }
214
215 6
        return $result;
216
    }
217
218
    /**
219
     * Normalize a listing response.
220
     *
221
     * @param string $path
222
     * @param array  $object
223
     *
224
     * @return array
225
     */
226 6
    protected function normalizeListingObject($path, array $object)
227
    {
228 6
        $permissions = $this->normalizePermissions($object['permissions']);
229 6
        $type = ($object['type'] === 1) ? 'file' : 'dir' ;
230 6
        $timestamp = $object['mtime'];
231
232 6
        if ($type === 'dir') {
233 6
            return compact('path', 'timestamp', 'type');
234
        }
235
236 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
237 6
        $size = (int) $object['size'];
238
239 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
240
    }
241
242
    /**
243
     * Disconnect.
244
     */
245 6
    public function disconnect()
246
    {
247 6
        $this->connection = null;
248 6
    }
249
250
    /**
251
     * @inheritdoc
252
     */
253 6
    public function write($path, $contents, Config $config)
254
    {
255 6
        if ($this->upload($path, $contents, $config) === false) {
256 6
            return false;
257
        }
258
259 6
        return compact('contents', 'visibility', 'path');
260
    }
261
262
    /**
263
     * @inheritdoc
264
     */
265 6
    public function writeStream($path, $resource, Config $config)
266
    {
267 6
        if ($this->upload($path, $resource, $config) === false) {
268 6
            return false;
269
        }
270
271 6
        return compact('visibility', 'path');
272
    }
273
274
    /**
275
     * Upload a file.
276
     *
277
     * @param string          $path
278
     * @param string|resource $contents
279
     * @param Config          $config
280
     * @return bool
281
     */
282 12
    public function upload($path, $contents, Config $config)
283
    {
284 12
        $connection = $this->getConnection();
285 12
        $this->ensureDirectory(Util::dirname($path));
286 12
        $config = Util::ensureConfig($config);
287 12
        $mode = SFTP::SOURCE_STRING;
288
289 12
        if ($config && $config->has('mode')) {
290
            $mode = $config->get('mode');
291
        }
292
293 12
        if (! $connection->put($path, $contents, $mode)) {
294 12
            return false;
295
        }
296
297 12
        if ($config && $visibility = $config->get('visibility')) {
298 6
            $this->setVisibility($path, $visibility);
299 6
        }
300
301 12
        return true;
302
    }
303
304
    /**
305
     * @inheritdoc
306
     */
307 6
    public function read($path)
308
    {
309 6
        $connection = $this->getConnection();
310
311 6
        if (($contents = $connection->get($path)) === false) {
312 6
            return false;
313
        }
314
315 6
        return compact('contents');
316
    }
317
318
    /**
319
     * @inheritdoc
320
     */
321 3
    public function readStream($path)
322
    {
323 3
        $stream = tmpfile();
324 3
        $connection = $this->getConnection();
325
326 3
        if ($connection->get($path, $stream) === false) {
327 3
            fclose($stream);
328 3
            return false;
329
        }
330
331 3
        rewind($stream);
332
333 3
        return compact('stream');
334
    }
335
336
    /**
337
     * @inheritdoc
338
     */
339 3
    public function update($path, $contents, Config $config)
340
    {
341 3
        return $this->write($path, $contents, $config);
342
    }
343
344
    /**
345
     * @inheritdoc
346
     */
347 3
    public function updateStream($path, $contents, Config $config)
348
    {
349 3
        return $this->writeStream($path, $contents, $config);
350
    }
351
352
    /**
353
     * @inheritdoc
354
     */
355 3
    public function delete($path)
356
    {
357 3
        $connection = $this->getConnection();
358
359 3
        return $connection->delete($path);
360
    }
361
362
    /**
363
     * @inheritdoc
364
     */
365 3
    public function rename($path, $newpath)
366
    {
367 3
        $connection = $this->getConnection();
368
369 3
        return $connection->rename($path, $newpath);
370
    }
371
372
    /**
373
     * @inheritdoc
374
     */
375 3
    public function deleteDir($dirname)
376
    {
377 3
        $connection = $this->getConnection();
378
379 3
        return $connection->delete($dirname, true);
380
    }
381
382
    /**
383
     * @inheritdoc
384
     */
385 39
    public function has($path)
386
    {
387 39
        return $this->getMetadata($path);
388
    }
389
390
    /**
391
     * @inheritdoc
392
     */
393 48
    public function getMetadata($path)
394
    {
395 48
        $connection = $this->getConnection();
396 48
        $info = $connection->stat($path);
397
398 48
        if ($info === false) {
399 12
            return false;
400
        }
401
402 39
        $result = Util::map($info, $this->statMap);
403 39
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
404 39
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
405
406 39
        return $result;
407
    }
408
409
    /**
410
     * @inheritdoc
411
     */
412 6
    public function getTimestamp($path)
413
    {
414 6
        return $this->getMetadata($path);
415
    }
416
417
    /**
418
     * @inheritdoc
419
     */
420 3
    public function getMimetype($path)
421
    {
422 3
        if (! $data = $this->read($path)) {
423 3
            return false;
424
        }
425
426 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
427
428 3
        return $data;
429
    }
430
431
    /**
432
     * @inheritdoc
433
     */
434 3
    public function createDir($dirname, Config $config)
435
    {
436 3
        $connection = $this->getConnection();
437
438 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
439 3
            return false;
440
        }
441
442 3
        return ['path' => $dirname];
443
    }
444
445
    /**
446
     * @inheritdoc
447
     */
448 6
    public function getVisibility($path)
449
    {
450 6
        return $this->getMetadata($path);
451
    }
452
453
    /**
454
     * @inheritdoc
455
     */
456 12
    public function setVisibility($path, $visibility)
457
    {
458 12
        $visibility = ucfirst($visibility);
459
460 12
        if (! isset($this->{'perm'.$visibility})) {
461 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
462
        }
463
464 9
        $connection = $this->getConnection();
465
466 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
467
    }
468
469
    /**
470
     * @inheritdoc
471
     */
472 6
    public function isConnected()
473
    {
474 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...
475 3
            return true;
476
        }
477
478 3
        return false;
479
    }
480
}
481