Completed
Pull Request — master (#93)
by
unknown
11:39
created

SftpAdapter::addSeparator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
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 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 1
        }
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 3
270 3
        $this->root = $this->addSeparator(
271
            $this->connection->pwd(),
272
            $this->separator
273
        );
274
    }
275
276
    /**
277 24
     * Cap a value with separator where needed.
278
     *
279 24
     * @param string $value
280
     * @param string $separator
281
     * @return string
282
     */
283 24
    protected function addSeparator($value, $separator)
284 6
    {
285
        return substr($value, -1) === $separator ? $value : $value.$separator;
286
    }
287 18
288
    /**
289
     * Get the password, either the private key or a plain text password.
290
     *
291
     * @return Agent|RSA|string
292
     */
293
    public function getAuthentication()
294
    {
295 12
        if ($this->useAgent) {
296
            return $this->getAgent();
297 12
        }
298 3
299 1
        if ($this->privateKey) {
300
            return $this->getPrivateKey();
301 12
        }
302
303 12
        return $this->getPassword();
304 12
    }
305 4
306
    /**
307 12
     * Get the private key with the password or private key contents.
308
     *
309 12
     * @return RSA
310
     */
311
    public function getPrivateKey()
312
    {
313
        if ("---" !== substr($this->privateKey, 0, 3) && is_file($this->privateKey)) {
314
            $this->privateKey = file_get_contents($this->privateKey);
315 21
        }
316
317 21
        $key = new RSA();
318
319 15
        if ($password = $this->getPassphrase()) {
320
            $key->setPassword($password);
321 6
        }
322
323
        $key->loadKey($this->privateKey);
324
325
        return $key;
326
    }
327
328
    /**
329
     * @return string
330
     */
331
    public function getPassphrase()
332
    {
333
        if ($this->passphrase === null) {
334
            //Added for backward compatibility
335
            return $this->getPassword();
336
        }
337
        return $this->passphrase;
338
    }
339
340
    /**
341
     * @return Agent|bool
342
     */
343
    public function getAgent()
344 9
    {
345
        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...
346 9
            $this->agent = new Agent();
347 9
        }
348 9
349 9
        return $this->agent;
350
    }
351 9
352 3
    /**
353
     * List the contents of a directory.
354
     *
355 9
     * @param string $directory
356
     * @param bool   $recursive
357 9
     *
358 9
     * @return array
359 3
     */
360
    protected function listDirectoryContents($directory, $recursive = true)
361
    {
362 9
        $result = [];
363 9
        $connection = $this->getConnection();
364
        $location = $this->prefix($directory);
365 9
        $listing = $connection->rawlist($location);
366 5
367 1
        if ($listing === false) {
368 3
            return [];
369
        }
370 9
371
        foreach ($listing as $filename => $object) {
372
            // When directory entries have a numeric filename they are changed to int
373
            $filename = (string) $filename;
374
            if (in_array($filename, ['.', '..'])) {
375
                continue;
376
            }
377
378
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
379
            $result[] = $this->normalizeListingObject($path, $object);
380
381 9
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
382
                $result = array_merge($result, $this->listDirectoryContents($path));
383 9
            }
384 9
        }
385
386 9
        return $result;
387
    }
388 9
389 9
    /**
390
     * Normalize a listing response.
391
     *
392 6
     * @param string $path
393 6
     * @param array  $object
394
     *
395 6
     * @return array
396
     */
397
    protected function normalizeListingObject($path, array $object)
398
    {
399
        $permissions = $this->normalizePermissions($object['permissions']);
400
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
401 18
402
        $timestamp = $object['mtime'];
403 18
404 18
        if ($type === 'dir') {
405
            return compact('path', 'timestamp', 'type');
406
        }
407
408
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
409 6
        $size = (int) $object['size'];
410
411 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
412 6
    }
413
414
    /**
415 6
     * Disconnect.
416
     */
417
    public function disconnect()
418
    {
419
        $this->connection = null;
420
    }
421 6
422
    /**
423 6
     * @inheritdoc
424 6
     */
425
    public function write($path, $contents, Config $config)
426
    {
427 6
        if ($this->upload($path, $contents, $config) === false) {
428
            return false;
429
        }
430
431
        return compact('contents', 'path');
432
    }
433
434
    /**
435
     * @inheritdoc
436
     */
437
    public function writeStream($path, $resource, Config $config)
438 12
    {
439
        if ($this->upload($path, $resource, $config) === false) {
440 12
            return false;
441 12
        }
442 12
443
        return compact('path');
444 12
    }
445 12
446
    /**
447
     * Upload a file.
448 12
     *
449 6
     * @param string          $path
450 2
     * @param string|resource $contents
451
     * @param Config          $config
452 12
     * @return bool
453
     */
454
    public function upload($path, $contents, Config $config)
455
    {
456
        $connection = $this->getConnection();
457
        $this->ensureDirectory(Util::dirname($path));
458 6
        $config = Util::ensureConfig($config);
459
460 6
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
461
            return false;
462 6
        }
463 6
464
        if ($config && $visibility = $config->get('visibility')) {
465
            $this->setVisibility($path, $visibility);
466 6
        }
467
468
        return true;
469
    }
470
471
    /**
472 3
     * @inheritdoc
473
     */
474 3
    public function read($path)
475 3
    {
476
        $connection = $this->getConnection();
477 3
478 3
        if (($contents = $connection->get($path)) === false) {
479 3
            return false;
480
        }
481
482 3
        return compact('contents', 'path');
483
    }
484 3
485
    /**
486
     * @inheritdoc
487
     */
488
    public function readStream($path)
489
    {
490 3
        $stream = tmpfile();
491
        $connection = $this->getConnection();
492 3
493
        if ($connection->get($path, $stream) === false) {
494
            fclose($stream);
495
            return false;
496
        }
497
498 3
        rewind($stream);
499
500 3
        return compact('stream', 'path');
501
    }
502
503
    /**
504
     * @inheritdoc
505
     */
506 3
    public function update($path, $contents, Config $config)
507
    {
508 3
        return $this->write($path, $contents, $config);
509
    }
510 3
511
    /**
512
     * @inheritdoc
513
     */
514
    public function updateStream($path, $contents, Config $config)
515
    {
516 4
        return $this->writeStream($path, $contents, $config);
517
    }
518 3
519
    /**
520 4
     * @inheritdoc
521
     */
522
    public function delete($path)
523
    {
524
        $connection = $this->getConnection();
525
526 3
        return $connection->delete($path);
527
    }
528 3
529
    /**
530 3
     * @inheritdoc
531
     */
532
    public function rename($path, $newpath)
533
    {
534
        $connection = $this->getConnection();
535
536 39
        return $connection->rename($path, $newpath);
537
    }
538 39
539
    /**
540
     * @inheritdoc
541
     */
542
    public function deleteDir($dirname)
543
    {
544 48
        $connection = $this->getConnection();
545
546 48
        return $connection->delete($dirname, true);
547 48
    }
548
549 48
    /**
550 12
     * @inheritdoc
551
     */
552
    public function has($path)
553 39
    {
554 39
        return $this->getMetadata($path);
555 39
    }
556
557 39
    /**
558
     * @inheritdoc
559
     */
560
    public function getMetadata($path)
561
    {
562
        $connection = $this->getConnection();
563 6
        $info = $connection->stat($path);
564
565 6
        if ($info === false) {
566
            return false;
567
        }
568
569
        $result = Util::map($info, $this->statMap);
570
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
571 3
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
572
573 3
        return $result;
574 3
    }
575
576
    /**
577 3
     * @inheritdoc
578
     */
579 3
    public function getTimestamp($path)
580
    {
581
        return $this->getMetadata($path);
582
    }
583
584
    /**
585 3
     * @inheritdoc
586
     */
587 3
    public function getMimetype($path)
588
    {
589 3
        if (! $data = $this->read($path)) {
590 3
            return false;
591
        }
592
593 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
594
595
        return $data;
596
    }
597
598
    /**
599 6
     * @inheritdoc
600
     */
601 6
    public function createDir($dirname, Config $config)
602
    {
603
        $connection = $this->getConnection();
604
605
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
606
            return false;
607 12
        }
608
609 12
        return ['path' => $dirname];
610
    }
611 12
612 3
    /**
613
     * @inheritdoc
614
     */
615 9
    public function getVisibility($path)
616
    {
617 9
        return $this->getMetadata($path);
618
    }
619
620
    /**
621
     * @inheritdoc
622
     */
623 6
    public function setVisibility($path, $visibility)
624
    {
625 6
        $visibility = ucfirst($visibility);
626 3
627
        if (! isset($this->{'perm'.$visibility})) {
628
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
629 3
        }
630
631
        $connection = $this->getConnection();
632
633
        return $connection->chmod($this->{'perm'.$visibility}, $path);
634
    }
635
636
    /**
637
     * @inheritdoc
638
     */
639
    public function isConnected()
640
    {
641
        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...
642
            return true;
643
        }
644
645
        return false;
646
    }
647
}
648