SftpAdapter::normalizeListingObject()   A
last analyzed

Complexity

Conditions 5
Paths 12

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.4222
c 0
b 0
f 0
cc 5
nc 12
nop 2
crap 5
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 phpseclib\Crypt\RSA;
12
use phpseclib\Net\SFTP;
13
use phpseclib\System\SSH\Agent;
14
15
class SftpAdapter extends AbstractFtpAdapter
16
{
17
    use StreamedCopyTrait;
18
19
    /**
20
     * @var SFTP
21
     */
22
    protected $connection;
23
24
    /**
25
     * @var int
26
     */
27
    protected $port = 22;
28
29
    /**
30
     * @var string
31
     */
32
    protected $hostFingerprint;
33
34
    /**
35
     * @var string
36
     */
37
    protected $privateKey;
38
39
    /**
40
     * @var bool
41
     */
42
    protected $useAgent = false;
43
44
    /**
45
     * @var Agent
46
     */
47
    private $agent;
48
49
    /**
50
     * @var array
51
     */
52
    protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'passphrase', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection'];
53
54
    /**
55
     * @var array
56
     */
57
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
58
59
    /**
60
     * @var int
61
     */
62
    protected $directoryPerm = 0744;
63
64
    /**
65
     * @var string
66
     */
67
    private $passphrase;
68
69
    /**
70
     * Prefix a path.
71
     *
72
     * @param string $path
73
     *
74
     * @return string
75
     */
76 9
    protected function prefix($path)
77
    {
78 9
        return $this->root.ltrim($path, $this->separator);
79
    }
80
81
    /**
82
     * Set the finger print of the public key of the host you are connecting to.
83
     *
84
     * If the key does not match the server identification, the connection will
85
     * be aborted.
86
     *
87
     * @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
88
     *
89
     * @return $this
90
     */
91 9
    public function setHostFingerprint($fingerprint)
92
    {
93 9
        $this->hostFingerprint = $fingerprint;
94
95 9
        return $this;
96
    }
97
98
    /**
99
     * Set the private key (string or path to local file).
100
     *
101
     * @param string $key
102
     *
103
     * @return $this
104
     */
105 12
    public function setPrivateKey($key)
106
    {
107 12
        $this->privateKey = $key;
108
109 12
        return $this;
110
    }
111
112
    /**
113
     * Set the passphrase for the privatekey.
114
     *
115
     * @param string $passphrase
116
     *
117
     * @return $this
118
     */
119 6
    public function setPassphrase($passphrase)
120
    {
121 6
        $this->passphrase = $passphrase;
122
123 6
        return $this;
124
    }
125
126
    /**
127
     * @param boolean $useAgent
128
     *
129
     * @return $this
130
     */
131
    public function setUseAgent($useAgent)
132
    {
133
        $this->useAgent = (bool) $useAgent;
134
135
        return $this;
136
    }
137
138
    /**
139
     * @param Agent $agent
140
     *
141
     * @return $this
142
     */
143
    public function setAgent(Agent $agent)
144
    {
145
        $this->agent = $agent;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Set permissions for new directory
152
     *
153
     * @param int $directoryPerm
154
     *
155
     * @return $this
156
     */
157 6
    public function setDirectoryPerm($directoryPerm)
158
    {
159 6
        $this->directoryPerm = $directoryPerm;
160
161 6
        return $this;
162
    }
163
164
    /**
165
     * Get permissions for new directory
166
     *
167
     * @return int
168
     */
169 3
    public function getDirectoryPerm()
170
    {
171 3
        return $this->directoryPerm;
172
    }
173
174
    /**
175
     * Inject the SFTP instance.
176
     *
177
     * @param SFTP $connection
178
     *
179
     * @return $this
180
     */
181 36
    public function setNetSftpConnection(SFTP $connection)
182
    {
183 36
        $this->connection = $connection;
184
185 36
        return $this;
186
    }
187
188
    /**
189
     * Connect.
190
     */
191 27
    public function connect()
192
    {
193 27
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
194 27
        $this->connection->disableStatCache();
195 27
        $this->login();
196 18
        $this->setConnectionRoot();
197 15
    }
198
199
    /**
200
     * Login.
201
     *
202
     * @throws ConnectionErrorException
203
     */
204 27
    protected function login()
205
    {
206 27
        if ($this->hostFingerprint) {
207 9
            $publicKey = $this->connection->getServerPublicHostKey();
208
209 9
            if ($publicKey === false) {
210 3
                throw new ConnectionErrorException('Could not connect to server to verify public key.');
211
            }
212
213 6
            $actualFingerprint = $this->getHexFingerprintFromSshPublicKey($publicKey);
214
215 6
            if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) {
216 3
                throw new ConnectionErrorException('The authenticity of host '.$this->host.' can\'t be established.');
217
            }
218
        }
219
220 21
        $authentication = $this->getAuthentication();
221
222
223 21
        if ($this->connection->login($this->getUsername(), $authentication)) {
224 15
            goto past_login;
225
        }
226
227
        // try double authentication, key is already given so now give password
228 6
        if ($authentication instanceof RSA && $this->connection->login($this->getUsername(), $this->getPassword())) {
0 ignored issues
show
Bug introduced by
The class phpseclib\Crypt\RSA 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...
229 3
            goto past_login;
230
        }
231
232 3
        throw new ConnectionErrorException('Could not login with username: '.$this->getUsername().', host: '.$this->host);
233
234
        past_login:
235
236 18
        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...
237
            $authentication->startSSHForwarding($this->connection);
238
        }
239 18
    }
240
241
    /**
242
     * Convert the SSH RSA public key into a hex formatted fingerprint.
243
     *
244
     * @param string $publickey
245
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
246
     */
247 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
248
    {
249 6
        $content = explode(' ', $publickey, 3);
250 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
251
    }
252
253
    /**
254
     * Set the connection root.
255
     *
256
     * @throws InvalidRootException
257
     */
258 18
    protected function setConnectionRoot()
259
    {
260 18
        $root = $this->getRoot();
261
262 18
        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...
263 12
            return;
264
        }
265
266 6
        if (! $this->connection->chdir($root)) {
267 3
            throw new InvalidRootException('Root is invalid or does not exist: '.$root);
268
        }
269
270 3
        $this->setRoot($this->connection->pwd());
271 3
    }
272
273
    /**
274
     * Get the password, either the private key or a plain text password.
275
     *
276
     * @return Agent|RSA|string
277
     */
278 24
    public function getAuthentication()
279
    {
280 24
        if ($this->useAgent) {
281
            return $this->getAgent();
282
        }
283
284 24
        if ($this->privateKey) {
285 6
            return $this->getPrivateKey();
286
        }
287
288 18
        return $this->getPassword();
289
    }
290
291
    /**
292
     * Get the private key with the password or private key contents.
293
     *
294
     * @return RSA
295
     */
296 12
    public function getPrivateKey()
297
    {
298 12
        if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {
299 3
            $this->privateKey = file_get_contents($this->privateKey);
300
        }
301
302 12
        $key = new RSA();
303
304 12
        if ($password = $this->getPassphrase()) {
305 12
            $key->setPassword($password);
306
        }
307
308 12
        $key->loadKey($this->privateKey);
309
310 12
        return $key;
311
    }
312
313
    /**
314
     * @return string
315
     */
316 21
    public function getPassphrase()
317
    {
318 21
        if ($this->passphrase === null) {
319
            //Added for backward compatibility
320 15
            return $this->getPassword();
321
        }
322 6
        return $this->passphrase;
323
    }
324
325
    /**
326
     * @return Agent|bool
327
     */
328
    public function getAgent()
329
    {
330
        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...
331
            $this->agent = new Agent();
332
        }
333
334
        return $this->agent;
335
    }
336
337
    /**
338
     * List the contents of a directory.
339
     *
340
     * @param string $directory
341
     * @param bool   $recursive
342
     *
343
     * @return array
344
     */
345 9
    protected function listDirectoryContents($directory, $recursive = true)
346
    {
347 9
        $result = [];
348 9
        $connection = $this->getConnection();
349 9
        $location = $this->prefix($directory);
350 9
        $listing = $connection->rawlist($location);
351
352 9
        if ($listing === false) {
353 3
            return [];
354
        }
355
356 9
        foreach ($listing as $filename => $object) {
357
            // When directory entries have a numeric filename they are changed to int
358 9
            $filename = (string) $filename;
359 9
            if (in_array($filename, ['.', '..'])) {
360 3
                continue;
361
            }
362
363 9
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
364 9
            $result[] = $this->normalizeListingObject($path, $object);
365
366 9
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
367 5
                $result = array_merge($result, $this->listDirectoryContents($path));
368
            }
369
        }
370
371 9
        return $result;
372
    }
373
374
    /**
375
     * Normalize a listing response.
376
     *
377
     * @param string $path
378
     * @param array  $object
379
     *
380
     * @return array
381
     */
382 9
    protected function normalizeListingObject($path, array $object)
383
    {
384 9
        $permissions = $this->normalizePermissions($object['permissions']);
385 9
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
386
387 9
        $timestamp = $object['mtime'];
388
389 9
        if ($type === 'dir') {
390 9
            return compact('path', 'timestamp', 'type');
391
        }
392
393 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
394 6
        $size = (int) $object['size'];
395
396 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
397
    }
398
399
    /**
400
     * Disconnect.
401
     */
402 18
    public function disconnect()
403
    {
404 18
        $this->connection = null;
405 18
    }
406
407
    /**
408
     * @inheritdoc
409
     */
410 6
    public function write($path, $contents, Config $config)
411
    {
412 6
        if ($this->upload($path, $contents, $config) === false) {
413 6
            return false;
414
        }
415
416 6
        return compact('contents', 'path');
417
    }
418
419
    /**
420
     * @inheritdoc
421
     */
422 6
    public function writeStream($path, $resource, Config $config)
423
    {
424 6
        if ($this->upload($path, $resource, $config) === false) {
425 6
            return false;
426
        }
427
428 6
        return compact('path');
429
    }
430
431
    /**
432
     * Upload a file.
433
     *
434
     * @param string          $path
435
     * @param string|resource $contents
436
     * @param Config          $config
437
     * @return bool
438
     */
439 12
    public function upload($path, $contents, Config $config)
440
    {
441 12
        $connection = $this->getConnection();
442 12
        $this->ensureDirectory(Util::dirname($path));
443 12
        $config = Util::ensureConfig($config);
444
445 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
446 12
            return false;
447
        }
448
449 12
        if ($config && $visibility = $config->get('visibility')) {
450 6
            $this->setVisibility($path, $visibility);
451
        }
452
453 12
        return true;
454
    }
455
456
    /**
457
     * @inheritdoc
458
     */
459 6
    public function read($path)
460
    {
461 6
        $connection = $this->getConnection();
462
463 6
        if (($contents = $connection->get($path)) === false) {
464 6
            return false;
465
        }
466
467 6
        return compact('contents', 'path');
468
    }
469
470
    /**
471
     * @inheritdoc
472
     */
473 3
    public function readStream($path)
474
    {
475 3
        $stream = tmpfile();
476 3
        $connection = $this->getConnection();
477
478 3
        if ($connection->get($path, $stream) === false) {
479 3
            fclose($stream);
480 3
            return false;
481
        }
482
483 3
        rewind($stream);
484
485 3
        return compact('stream', 'path');
486
    }
487
488
    /**
489
     * @inheritdoc
490
     */
491 3
    public function update($path, $contents, Config $config)
492
    {
493 3
        return $this->write($path, $contents, $config);
494
    }
495
496
    /**
497
     * @inheritdoc
498
     */
499 3
    public function updateStream($path, $contents, Config $config)
500
    {
501 3
        return $this->writeStream($path, $contents, $config);
502
    }
503
504
    /**
505
     * @inheritdoc
506
     */
507 3
    public function delete($path)
508
    {
509 3
        $connection = $this->getConnection();
510
511 3
        return $connection->delete($path);
512
    }
513
514
    /**
515
     * @inheritdoc
516
     */
517 3
    public function rename($path, $newpath)
518
    {
519 3
        $connection = $this->getConnection();
520
521 3
        return $connection->rename($path, $newpath);
522
    }
523
524
    /**
525
     * @inheritdoc
526
     */
527 3
    public function deleteDir($dirname)
528
    {
529 3
        $connection = $this->getConnection();
530
531 3
        return $connection->delete($dirname, true);
532
    }
533
534
    /**
535
     * @inheritdoc
536
     */
537 48
    public function has($path)
538
    {
539 48
        return $this->getMetadata($path);
540
    }
541
542
    /**
543
     * @inheritdoc
544
     */
545 54
    public function getMetadata($path)
546
    {
547 54
        $connection = $this->getConnection();
548 54
        $info = $connection->stat($path);
549
550 54
        if ($info === false) {
551 12
            return false;
552
        }
553
554 45
        $result = Util::map($info, $this->statMap);
555 45
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
556 45
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
557
558 45
        return $result;
559
    }
560
561
    /**
562
     * @inheritdoc
563
     */
564 6
    public function getTimestamp($path)
565
    {
566 6
        return $this->getMetadata($path);
567
    }
568
569
    /**
570
     * @inheritdoc
571
     */
572 3
    public function getMimetype($path)
573
    {
574 3
        if (! $data = $this->read($path)) {
575 3
            return false;
576
        }
577
578 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
579
580 3
        return $data;
581
    }
582
583
    /**
584
     * @inheritdoc
585
     */
586 3
    public function createDir($dirname, Config $config)
587
    {
588 3
        $connection = $this->getConnection();
589
590 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
591 3
            return false;
592
        }
593
594 3
        return ['path' => $dirname];
595
    }
596
597
    /**
598
     * @inheritdoc
599
     */
600 6
    public function getVisibility($path)
601
    {
602 6
        return $this->getMetadata($path);
603
    }
604
605
    /**
606
     * @inheritdoc
607
     */
608 12
    public function setVisibility($path, $visibility)
609
    {
610 12
        $visibility = ucfirst($visibility);
611
612 12
        if (! isset($this->{'perm'.$visibility})) {
613 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
614
        }
615
616 9
        $connection = $this->getConnection();
617
618 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
619
    }
620
621
    /**
622
     * @inheritdoc
623
     */
624 81
    public function isConnected()
625
    {
626 81
        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...
627 78
            return true;
628
        }
629
630 3
        return false;
631
    }
632
}
633