Completed
Pull Request — master (#24)
by Maxime
13:12 queued 11:27
created

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