Completed
Pull Request — master (#24)
by Maxime
12:21 queued 10:03
created

SftpAdapter::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

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 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 3
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
     * 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
     * @param boolean $agent
79
     *
80
     * @return $this
81
     */
82
    public function setAgent($agent)
83
    {
84
        $this->agent = (bool) $agent;
85
86
        return $this;
87
    }
88
89
    /**
90
     * Set permissions for new directory
91
     *
92
     * @param int $directoryPerm
93
     *
94
     * @return $this
95
     */
96 6
    public function setDirectoryPerm($directoryPerm)
97
    {
98 6
        $this->directoryPerm = $directoryPerm;
99
100 6
        return $this;
101
    }
102
103
    /**
104
     * Get permissions for new directory
105
     *
106
     * @return int
107
     */
108 3
    public function getDirectoryPerm()
109
    {
110 3
        return $this->directoryPerm;
111
    }
112
113
    /**
114
     * Inject the SFTP instance.
115
     *
116
     * @param SFTP $connection
117
     *
118
     * @return $this
119
     */
120 21
    public function setNetSftpConnection(SFTP $connection)
121
    {
122 21
        $this->connection = $connection;
123
124 21
        return $this;
125
    }
126
127
    /**
128
     * Connect.
129
     */
130 12
    public function connect()
131
    {
132 12
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
133 12
        $this->login();
134 9
        $this->setConnectionRoot();
135 6
    }
136
137
    /**
138
     * Login.
139
     *
140
     * @throws LogicException
141
     */
142 12
    protected function login()
143
    {
144 12
        $authentication = $this->getPassword();
145 12
        if (! $this->connection->login($this->username, $authentication)) {
146 3
            throw new LogicException('Could not login with username: '.$this->username.', host: '.$this->host);
147
        }
148
149 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...
150
            $authentication->startSSHForwarding($this->connection);
151
        }
152 9
    }
153
154
    /**
155
     * Set the connection root.
156
     */
157 9
    protected function setConnectionRoot()
158
    {
159 9
        $root = $this->getRoot();
160
161 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...
162 3
            return;
163
        }
164
165 6
        if (! $this->connection->chdir($root)) {
166 3
            throw new RuntimeException('Root is invalid or does not exist: '.$root);
167
        }
168 3
        $this->root = $this->connection->pwd() . $this->separator;
169 3
    }
170
171
    /**
172
     * Get the password, either the private key or a plain text password.
173
     *
174
     * @return Agent|RSA|string
175
     */
176 15
    public function getPassword()
177
    {
178 15
        if ($this->agent) {
179
            return $this->getAgent();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getAgent(); (phpseclib\System\SSH\Agent|boolean) 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...
180
        }
181
182 15
        if ($this->privatekey) {
183 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...
184
        }
185
186 12
        return $this->password;
187
    }
188
189
    /**
190
     * Get the private get with the password or private key contents.
191
     *
192
     * @return RSA
193
     */
194 9
    public function getPrivateKey()
195
    {
196 9
        if (@is_file($this->privatekey)) {
197 3
            $this->privatekey = file_get_contents($this->privatekey);
198 2
        }
199
200 9
        $key = new RSA();
201
202 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...
203 9
            $key->setPassword($this->password);
204 6
        }
205
206 9
        $key->loadKey($this->privatekey);
207
208 9
        return $key;
209
    }
210
211
    /**
212
     * @return Agent|bool
213
     */
214
    public function getAgent()
215
    {
216
        if ($this->agent === true) {
217
            $this->agent = new Agent();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \phpseclib\System\SSH\Agent() of type object<phpseclib\System\SSH\Agent> is incompatible with the declared type boolean of property $agent.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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