Completed
Pull Request — master (#97)
by artem
01:39
created

SftpAdapter::setVisibility()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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