Completed
Pull Request — master (#39)
by
unknown
01:55
created

SftpAdapter::getUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 4
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 6
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 $hostFingerprint;
30
31
    /**
32
     * @var string
33
     */
34
    protected $privatekey;
35
36
    /**
37
     * @var bool
38
     */
39
    protected $useAgent = false;
40
41
    /**
42
     * @var Agent
43
     */
44
    private $agent;
45
46
    /**
47
     * @var array
48
     */
49
    protected $configurable = ['host', 'hostFingerprint', 'port', 'username', 'password', 'useAgent', 'agent', 'timeout', 'root', 'privateKey', 'permPrivate', 'permPublic', 'directoryPerm', 'NetSftpConnection', 'url'];
50
51
    /**
52
     * @var array
53
     */
54
    protected $statMap = ['mtime' => 'timestamp', 'size' => 'size'];
55
56
    /**
57
     * @var int
58
     */
59
    protected $directoryPerm = 0744;
60
61
    /**
62
     * Prefix a path.
63
     *
64
     * @param string $path
65
     *
66
     * @return string
67
     */
68 6
    protected function prefix($path)
69
    {
70 6
        return $this->root.ltrim($path, $this->separator);
71
    }
72
73
    /**
74
     * Set the finger print of the public key of the host you are connecting to.
75
     *
76
     * If the key does not match the server identification, the connection will
77
     * be aborted.
78
     *
79
     * @param string $fingerprint Example: '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
80
     *
81
     * @return $this
82
     */
83 6
    public function setHostFingerprint($fingerprint)
84
    {
85 6
        $this->hostFingerprint = $fingerprint;
86
87 6
        return $this;
88
    }
89
90
    /**
91
     * Set the private key (string or path to local file).
92
     *
93
     * @param string $key
94
     *
95
     * @return $this
96
     */
97 9
    public function setPrivateKey($key)
98
    {
99 9
        $this->privatekey = $key;
100
101 9
        return $this;
102
    }
103
104
    /**
105
     * @param boolean $useAgent
106
     *
107
     * @return $this
108
     */
109
    public function setUseAgent($useAgent)
110
    {
111
        $this->useAgent = (bool) $useAgent;
112
113
        return $this;
114
    }
115
116
    /**
117
     * @param Agent $agent
118
     *
119
     * @return $this
120
     */
121
    public function setAgent(Agent $agent)
122
    {
123
        $this->agent = $agent;
124
125
        return $this;
126
    }
127
128
    /**
129
     * Set permissions for new directory
130
     *
131
     * @param int $directoryPerm
132
     *
133
     * @return $this
134
     */
135 6
    public function setDirectoryPerm($directoryPerm)
136
    {
137 6
        $this->directoryPerm = $directoryPerm;
138
139 6
        return $this;
140
    }
141
142
    /**
143
     * Get permissions for new directory
144
     *
145
     * @return int
146
     */
147 3
    public function getDirectoryPerm()
148
    {
149 3
        return $this->directoryPerm;
150
    }
151
152
    /**
153
     * Inject the SFTP instance.
154
     *
155
     * @param SFTP $connection
156
     *
157
     * @return $this
158
     */
159 30
    public function setNetSftpConnection(SFTP $connection)
160 2
    {
161 30
        $this->connection = $connection;
162
163 30
        return $this;
164
    }
165
166
    /**
167
     * Set the url.
168
     *
169
     * @param string $url
170
     *
171
     * @return $this
172
     */
173
    public function setUrl($url)
174
    {
175
        $this->url = $url;
0 ignored issues
show
Bug introduced by
The property url does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
176
177
        return $this;
178
    }
179
180
    /**
181
     * get full url from path if url is set.
182
     *
183
     * @param string path
184
     *
185
     * @return string
186
     */
187
    public function getUrl($path)
188
    {
189
        if (!empty($this->url)) {
190
            return rtrim($this->url,'/').'/'.ltrim($path, '/');
191
        }
192
193
        return $path;
194
    }
195
196
    /**
197
     * Connect.
198
     */
199 21
    public function connect()
200
    {
201 21
        $this->connection = $this->connection ?: new SFTP($this->host, $this->port, $this->timeout);
202 21
        $this->login();
203 15
        $this->setConnectionRoot();
204 12
    }
205
206
    /**
207
     * Login.
208
     *
209
     * @throws LogicException
210
     */
211 21
    protected function login()
212
    {
213 21
        if ($this->hostFingerprint) {
214 6
            $actualFingerprint = $this->getHexFingerprintFromSshPublicKey($this->connection->getServerPublicHostKey());
215
216 6
            if (0 !== strcasecmp($this->hostFingerprint, $actualFingerprint)) {
217 3
                throw new LogicException('The authenticity of host '.$this->host.' can\'t be established.');
218
            }
219 2
        }
220
221 18
        $authentication = $this->getAuthentication();
222
223 18
        if (! $this->connection->login($this->getUsername(), $authentication)) {
224 3
            throw new LogicException('Could not login with username: '.$this->getUsername().', host: '.$this->host);
225
        }
226
227 15
        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...
228
            $authentication->startSSHForwarding($this->connection);
229
        }
230 15
    }
231
232
    /**
233
     * Convert the SSH RSA public key into a hex formatted fingerprint.
234
     *
235
     * @param string $publickey
236
     * @return string Hex formatted fingerprint, e.g. '88:76:75:96:c1:26:7c:dd:9f:87:50:db:ac:c4:a8:7c'.
237
     */
238 6
    private function getHexFingerprintFromSshPublicKey ($publickey)
239
    {
240 6
        $content = explode(' ', $publickey, 3);
241 6
        return implode(':', str_split(md5(base64_decode($content[1])), 2));
242
    }
243
244
    /**
245
     * Set the connection root.
246
     */
247 15
    protected function setConnectionRoot()
248
    {
249 15
        $root = $this->getRoot();
250
251 15
        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...
252 9
            return;
253
        }
254
255 6
        if (! $this->connection->chdir($root)) {
256 3
            throw new RuntimeException('Root is invalid or does not exist: '.$root);
257
        }
258 3
        $this->root = $this->connection->pwd() . $this->separator;
259 3
    }
260
261
    /**
262
     * Get the password, either the private key or a plain text password.
263
     *
264
     * @return Agent|RSA|string
265
     */
266 21
    public function getAuthentication()
267
    {
268 21
        if ($this->useAgent) {
269
            return $this->getAgent();
270
        }
271
272 21
        if ($this->privatekey) {
273 3
            return $this->getPrivateKey();
274
        }
275
276 18
        return $this->getPassword();
277
    }
278
279
    /**
280
     * Get the private get with the password or private key contents.
281
     *
282
     * @return RSA
283
     */
284 9
    public function getPrivateKey()
285
    {
286 9
        if (@is_file($this->privatekey)) {
287 3
            $this->privatekey = file_get_contents($this->privatekey);
288 2
        }
289
290 9
        $key = new RSA();
291
292 9
        if ($password = $this->getPassword()) {
293 9
            $key->setPassword($password);
294 6
        }
295
296 9
        $key->loadKey($this->privatekey);
297
298 9
        return $key;
299
    }
300
301
    /**
302
     * @return Agent|bool
303
     */
304
    public function getAgent()
305
    {
306
        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...
307
            $this->agent = new Agent();
308
        }
309
310
        return $this->agent;
311
    }
312
313
    /**
314
     * List the contents of a directory.
315
     *
316
     * @param string $directory
317
     * @param bool   $recursive
318
     *
319
     * @return array
320
     */
321 6
    protected function listDirectoryContents($directory, $recursive = true)
322
    {
323 6
        $result = [];
324 6
        $connection = $this->getConnection();
325 6
        $location = $this->prefix($directory);
326 6
        $listing = $connection->rawlist($location);
327
328 6
        if ($listing === false) {
329 3
            return [];
330
        }
331
332 6
        foreach ($listing as $filename => $object) {
333 6
            if (in_array($filename, ['.', '..'])) {
334 3
                continue;
335
            }
336
337 6
            $path = empty($directory) ? $filename : ($directory.'/'.$filename);
338 6
            $result[] = $this->normalizeListingObject($path, $object);
339
340 6
            if ($recursive && isset($object['type']) && $object['type'] === NET_SFTP_TYPE_DIRECTORY) {
341 4
                $result = array_merge($result, $this->listDirectoryContents($path));
342 2
            }
343 4
        }
344
345 6
        return $result;
346
    }
347
348
    /**
349
     * Normalize a listing response.
350
     *
351
     * @param string $path
352
     * @param array  $object
353
     *
354
     * @return array
355
     */
356 6
    protected function normalizeListingObject($path, array $object)
357
    {
358 6
        $permissions = $this->normalizePermissions($object['permissions']);
359 6
        $type = isset($object['type']) && ($object['type'] === 2) ?  'dir' : 'file';
360
361 6
        $timestamp = $object['mtime'];
362
363 6
        if ($type === 'dir') {
364 6
            return compact('path', 'timestamp', 'type');
365
        }
366
367 6
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
368 6
        $size = (int) $object['size'];
369
370 6
        return compact('path', 'timestamp', 'type', 'visibility', 'size');
371
    }
372
373
    /**
374
     * Disconnect.
375
     */
376 15
    public function disconnect()
377
    {
378 15
        $this->connection = null;
379 15
    }
380
381
    /**
382
     * @inheritdoc
383
     */
384 6
    public function write($path, $contents, Config $config)
385
    {
386 6
        if ($this->upload($path, $contents, $config) === false) {
387 6
            return false;
388
        }
389
390 6
        return compact('contents', 'visibility', 'path');
391
    }
392
393
    /**
394
     * @inheritdoc
395
     */
396 6
    public function writeStream($path, $resource, Config $config)
397
    {
398 6
        if ($this->upload($path, $resource, $config) === false) {
399 6
            return false;
400
        }
401
402 6
        return compact('visibility', 'path');
403
    }
404
405
    /**
406
     * Upload a file.
407
     *
408
     * @param string          $path
409
     * @param string|resource $contents
410
     * @param Config          $config
411
     * @return bool
412
     */
413 12
    public function upload($path, $contents, Config $config)
414
    {
415 12
        $connection = $this->getConnection();
416 12
        $this->ensureDirectory(Util::dirname($path));
417 12
        $config = Util::ensureConfig($config);
418
419 12
        if (! $connection->put($path, $contents, SFTP::SOURCE_STRING)) {
420 12
            return false;
421
        }
422
423 12
        if ($config && $visibility = $config->get('visibility')) {
424 6
            $this->setVisibility($path, $visibility);
425 4
        }
426
427 12
        return true;
428
    }
429
430
    /**
431
     * @inheritdoc
432
     */
433 6
    public function read($path)
434
    {
435 6
        $connection = $this->getConnection();
436
437 6
        if (($contents = $connection->get($path)) === false) {
438 6
            return false;
439
        }
440
441 6
        return compact('contents');
442
    }
443
444
    /**
445
     * @inheritdoc
446
     */
447 3
    public function readStream($path)
448
    {
449 3
        $stream = tmpfile();
450 3
        $connection = $this->getConnection();
451
452 3
        if ($connection->get($path, $stream) === false) {
453 3
            fclose($stream);
454 3
            return false;
455
        }
456
457 3
        rewind($stream);
458
459 3
        return compact('stream');
460
    }
461
462
    /**
463
     * @inheritdoc
464
     */
465 3
    public function update($path, $contents, Config $config)
466
    {
467 3
        return $this->write($path, $contents, $config);
468
    }
469
470
    /**
471
     * @inheritdoc
472
     */
473 3
    public function updateStream($path, $contents, Config $config)
474
    {
475 3
        return $this->writeStream($path, $contents, $config);
476
    }
477
478
    /**
479
     * @inheritdoc
480
     */
481 3
    public function delete($path)
482
    {
483 3
        $connection = $this->getConnection();
484
485 3
        return $connection->delete($path);
486
    }
487
488
    /**
489
     * @inheritdoc
490
     */
491 3
    public function rename($path, $newpath)
492
    {
493 3
        $connection = $this->getConnection();
494
495 3
        return $connection->rename($path, $newpath);
496
    }
497
498
    /**
499
     * @inheritdoc
500
     */
501 3
    public function deleteDir($dirname)
502
    {
503 3
        $connection = $this->getConnection();
504
505 3
        return $connection->delete($dirname, true);
506
    }
507
508
    /**
509
     * @inheritdoc
510
     */
511 39
    public function has($path)
512
    {
513 39
        return $this->getMetadata($path);
514
    }
515
516
    /**
517
     * @inheritdoc
518
     */
519 48
    public function getMetadata($path)
520 2
    {
521 48
        $connection = $this->getConnection();
522 48
        $info = $connection->stat($path);
523
524 48
        if ($info === false) {
525 12
            return false;
526
        }
527
528 39
        $result = Util::map($info, $this->statMap);
529 39
        $result['type'] = $info['type'] === NET_SFTP_TYPE_DIRECTORY ? 'dir' : 'file';
530 39
        $result['visibility'] = $info['permissions'] & $this->permPublic ? 'public' : 'private';
531
532 39
        return $result;
533
    }
534
535
    /**
536
     * @inheritdoc
537
     */
538 6
    public function getTimestamp($path)
539
    {
540 6
        return $this->getMetadata($path);
541
    }
542
543
    /**
544
     * @inheritdoc
545
     */
546 3
    public function getMimetype($path)
547
    {
548 3
        if (! $data = $this->read($path)) {
549 3
            return false;
550
        }
551
552 3
        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);
553
554 3
        return $data;
555
    }
556
557
    /**
558
     * @inheritdoc
559
     */
560 3
    public function createDir($dirname, Config $config)
561
    {
562 3
        $connection = $this->getConnection();
563
564 3
        if (! $connection->mkdir($dirname, $this->directoryPerm, true)) {
565 3
            return false;
566
        }
567
568 3
        return ['path' => $dirname];
569
    }
570
571
    /**
572
     * @inheritdoc
573
     */
574 6
    public function getVisibility($path)
575
    {
576 6
        return $this->getMetadata($path);
577
    }
578
579
    /**
580
     * @inheritdoc
581
     */
582 12
    public function setVisibility($path, $visibility)
583
    {
584 12
        $visibility = ucfirst($visibility);
585
586 12
        if (! isset($this->{'perm'.$visibility})) {
587 3
            throw new InvalidArgumentException('Unknown visibility: '.$visibility);
588
        }
589
590 9
        $connection = $this->getConnection();
591
592 9
        return $connection->chmod($this->{'perm'.$visibility}, $path);
593
    }
594
595
    /**
596
     * @inheritdoc
597
     */
598 6
    public function isConnected()
599
    {
600 6
        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 3
            return true;
602
        }
603
604 3
        return false;
605
    }
606
}
607