Completed
Push — master ( 866ed9...ae7fb1 )
by Frank
04:06
created

SftpAdapter::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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