Completed
Pull Request — master (#24)
by Maxime
14:20
created

SftpAdapter::createDir()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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