Completed
Push — master ( 0df756...b94194 )
by Frank
04:04
created

SftpAdapter   C

Complexity

Total Complexity 69

Size/Duplication

Total Lines 527
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 91.91%

Importance

Changes 15
Bugs 4 Features 4
Metric Value
wmc 69
c 15
b 4
f 4
lcom 1
cbo 4
dl 0
loc 527
ccs 159
cts 173
cp 0.9191
rs 5.6445

35 Methods

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