Completed
Pull Request — master (#24)
by Maxime
12:15
created

SftpAdapter::setAgent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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