Completed
Pull Request — master (#56)
by
unknown
13:02
created

SftpAdapter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
556
            $path = implode('/', array_slice($structure, 0, $i + 1));
557 12
558
            if ($this->has($path)) {
559 12
                continue;
560
            }
561 12
562 3
            // do not call mkdir recursive, because it will throw warning for the already existing directories
563
            if (!$connection->mkdir($path, $this->directoryPerm, false)) {
564
                return false;
565 9
            }
566
        }
567 9
568
        return ['path' => $dirname];
569
    }
570
571
    /**
572
     * @inheritdoc
573 75
     */
574
    public function getVisibility($path)
575 75
    {
576 72
        return $this->getMetadata($path);
577
    }
578
579 3
    /**
580
     * @inheritdoc
581
     */
582
    public function setVisibility($path, $visibility)
583
    {
584
        $visibility = ucfirst($visibility);
585
586
        if (! isset($this->{'perm'.$visibility})) {
587
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
588
        }
589
590
        $connection = $this->getConnection();
591
592
        return $connection->chmod($this->{'perm'.$visibility}, $path);
593
    }
594
595
    /**
596
     * @inheritdoc
597
     */
598
    public function isConnected()
599
    {
600
        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...
601
            return true;
602
        }
603
604
        return false;
605
    }
606
}
607