Completed
Push — master ( 64e671...de3cb6 )
by Vladimir
02:03
created

CurlFtpAdapter::getBaseUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 2
nop 0
1
<?php
2
3
namespace VladimirYuldashev\Flysystem;
4
5
use DateTime;
6
use Normalizer;
7
use RuntimeException;
8
use League\Flysystem\Util;
9
use League\Flysystem\Config;
10
use League\Flysystem\Util\MimeType;
11
use League\Flysystem\AdapterInterface;
12
use League\Flysystem\Adapter\AbstractFtpAdapter;
13
14
class CurlFtpAdapter extends AbstractFtpAdapter
15
{
16
    protected $configurable = [
17
        'protocol',
18
        'host',
19
        'port',
20
        'username',
21
        'password',
22
        'root',
23
    ];
24
25
    /** @var Curl */
26
    protected $connection;
27
28
    /** @var bool */
29
    protected $isPureFtpd;
30
31
    /**
32
     * Establish a connection.
33
     */
34
    public function connect()
35
    {
36
        $this->connection = new Curl();
37
        $this->connection->setOptions([
38
            CURLOPT_URL => $this->getBaseUri(),
39
            CURLOPT_USERPWD => $this->getUsername() . ':' . $this->getPassword(),
40
            CURLOPT_SSL_VERIFYPEER => false,
41
            CURLOPT_SSL_VERIFYHOST => false,
42
            CURLOPT_FTP_SSL => CURLFTPSSL_TRY,
43
            CURLOPT_FTPSSLAUTH => CURLFTPAUTH_TLS,
44
            CURLOPT_RETURNTRANSFER => true,
45
        ]);
46
47
        $this->pingConnection();
48
        $this->setConnectionRoot();
49
    }
50
51
    /**
52
     * Close the connection.
53
     */
54
    public function disconnect()
55
    {
56
        if ($this->connection !== null) {
57
            $this->connection = null;
58
        }
59
        $this->isPureFtpd = null;
60
    }
61
62
    /**
63
     * Check if a connection is active.
64
     *
65
     * @return bool
66
     */
67
    public function isConnected()
68
    {
69
        return $this->connection !== null;
70
    }
71
72
    /**
73
     * Write a new file.
74
     *
75
     * @param string $path
76
     * @param string $contents
77
     * @param Config $config Config object
78
     *
79
     * @return array|false false on failure file meta data on success
80
     */
81
    public function write($path, $contents, Config $config)
82
    {
83
        $stream = fopen('php://temp', 'w+b');
84
        fwrite($stream, $contents);
85
        rewind($stream);
86
87
        $result = $this->writeStream($path, $stream, $config);
88
89
        if ($result === false) {
90
            return false;
91
        }
92
93
        $result['contents'] = $contents;
94
        $result['mimetype'] = Util::guessMimeType($path, $contents);
95
96
        return $result;
97
    }
98
99
    /**
100
     * Write a new file using a stream.
101
     *
102
     * @param string   $path
103
     * @param resource $resource
104
     * @param Config   $config Config object
105
     *
106
     * @return array|false false on failure file meta data on success
107
     */
108
    public function writeStream($path, $resource, Config $config)
109
    {
110
        $connection = $this->getConnection();
111
112
        $result = $connection->exec([
113
            CURLOPT_URL => $this->getBaseUri() . '/' . $path,
114
            CURLOPT_UPLOAD => 1,
115
            CURLOPT_INFILE => $resource,
116
        ]);
117
118
        if ($result === false) {
119
            return false;
120
        }
121
122
        $type = 'file';
123
124
        return compact('type', 'path');
125
    }
126
127
    /**
128
     * Update a file.
129
     *
130
     * @param string $path
131
     * @param string $contents
132
     * @param Config $config Config object
133
     *
134
     * @return array|false false on failure file meta data on success
135
     */
136
    public function update($path, $contents, Config $config)
137
    {
138
        return $this->write($path, $contents, $config);
139
    }
140
141
    /**
142
     * Update a file using a stream.
143
     *
144
     * @param string   $path
145
     * @param resource $resource
146
     * @param Config   $config Config object
147
     *
148
     * @return array|false false on failure file meta data on success
149
     */
150
    public function updateStream($path, $resource, Config $config)
151
    {
152
        return $this->writeStream($path, $resource, $config);
153
    }
154
155
    /**
156
     * Rename a file.
157
     *
158
     * @param string $path
159
     * @param string $newpath
160
     *
161
     * @return bool
162
     */
163
    public function rename($path, $newpath)
164
    {
165
        $connection = $this->getConnection();
166
167
        $response = $this->rawCommand($connection, 'RNFR ' . $path);
168
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
169
        if ($code !== '350') {
170
            return false;
171
        }
172
173
        $response = $this->rawCommand($connection, 'RNTO ' . $newpath);
174
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
175
        if ($code !== '250') {
176
            return false;
177
        }
178
179
        return true;
180
    }
181
182
    /**
183
     * Copy a file.
184
     *
185
     * @param string $path
186
     * @param string $newpath
187
     *
188
     * @return bool
189
     */
190
    public function copy($path, $newpath)
191
    {
192
        $file = $this->read($path);
193
194
        if ($file === false) {
195
            return false;
196
        }
197
198
        return $this->write($newpath, $file['contents'], new Config()) !== false;
199
    }
200
201
    /**
202
     * Delete a file.
203
     *
204
     * @param string $path
205
     *
206
     * @return bool
207
     */
208 View Code Duplication
    public function delete($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
209
    {
210
        $connection = $this->getConnection();
211
212
        $response = $this->rawCommand($connection, 'DELE ' . $path);
213
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
214
        if ($code !== '250') {
215
            return false;
216
        }
217
218
        return true;
219
    }
220
221
    /**
222
     * Delete a directory.
223
     *
224
     * @param string $dirname
225
     *
226
     * @return bool
227
     */
228 View Code Duplication
    public function deleteDir($dirname)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
229
    {
230
        $connection = $this->getConnection();
231
232
        $response = $this->rawCommand($connection, 'RMD ' . $dirname);
233
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
234
        if ($code !== '250') {
235
            return false;
236
        }
237
238
        return true;
239
    }
240
241
    /**
242
     * Create a directory.
243
     *
244
     * @param string $dirname directory name
245
     * @param Config $config
246
     *
247
     * @return array|false
248
     */
249
    public function createDir($dirname, Config $config)
250
    {
251
        $connection = $this->getConnection();
252
253
        $response = $this->rawCommand($connection, 'MKD ' . $dirname);
254
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
255
        if ($code !== '257') {
256
            return false;
257
        }
258
259
        return ['type' => 'dir', 'path' => $dirname];
260
    }
261
262
    /**
263
     * Set the visibility for a file.
264
     *
265
     * @param string $path
266
     * @param string $visibility
267
     *
268
     * @return array|false file meta data
269
     */
270
    public function setVisibility($path, $visibility)
271
    {
272
        $connection = $this->getConnection();
273
274
        if ($visibility === AdapterInterface::VISIBILITY_PUBLIC) {
275
            $mode = $this->getPermPublic();
276
        } else {
277
            $mode = $this->getPermPrivate();
278
        }
279
280
        $request = sprintf('SITE CHMOD %o %s', $mode, $path);
281
        $response = $this->rawCommand($connection, $request);
282
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
283
        if ($code !== '200') {
284
            return false;
285
        }
286
287
        return $this->getMetadata($path);
288
    }
289
290
    /**
291
     * Read a file.
292
     *
293
     * @param string $path
294
     *
295
     * @return array|false
296
     */
297
    public function read($path)
298
    {
299
        if (!$object = $this->readStream($path)) {
300
            return false;
301
        }
302
303
        $object['contents'] = stream_get_contents($object['stream']);
304
        fclose($object['stream']);
305
        unset($object['stream']);
306
307
        return $object;
308
    }
309
310
    /**
311
     * Read a file as a stream.
312
     *
313
     * @param string $path
314
     *
315
     * @return array|false
316
     */
317
    public function readStream($path)
318
    {
319
        $stream = fopen('php://temp', 'w+b');
320
321
        $connection = $this->getConnection();
322
323
        $result = $connection->exec([
324
            CURLOPT_URL => $this->getBaseUri() . '/' . $path,
325
            CURLOPT_FILE => $stream,
326
        ]);
327
328
        if (!$result) {
329
            fclose($stream);
330
331
            return false;
332
        }
333
334
        rewind($stream);
335
336
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
337
    }
338
339
    /**
340
     * Get all the meta data of a file or directory.
341
     *
342
     * @param string $path
343
     *
344
     * @return array|false
345
     */
346
    public function getMetadata($path)
347
    {
348
        if ($path === '') {
349
            return ['type' => 'dir', 'path' => ''];
350
        }
351
352
        $request = rtrim('LIST -A ' . $this->normalizePath($path));
353
354
        $connection = $this->getConnection();
355
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
356
        if ($result === false) {
357
            return false;
358
        }
359
        $listing = $this->normalizeListing(explode(PHP_EOL, $result), '');
360
361
        return current($listing);
362
    }
363
364
    /**
365
     * Get the mimetype of a file.
366
     *
367
     * @param string $path
368
     *
369
     * @return array|false
370
     */
371
    public function getMimetype($path)
372
    {
373
        if (!$metadata = $this->getMetadata($path)) {
374
            return false;
375
        }
376
377
        $metadata['mimetype'] = MimeType::detectByFilename($path);
378
379
        return $metadata;
380
    }
381
382
    /**
383
     * Get the timestamp of a file.
384
     *
385
     * @param string $path
386
     *
387
     * @return array|false
388
     */
389
    public function getTimestamp($path)
390
    {
391
        $response = $this->rawCommand($this->getConnection(), 'MDTM ' . $path);
392
        list($code, $time) = explode(' ', end($response), 2);
393
        if ($code !== '213') {
394
            return false;
395
        }
396
397
        $datetime = DateTime::createFromFormat('YmdHis', $time);
398
399
        return ['path' => $path, 'timestamp' => $datetime->getTimestamp()];
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     *
405
     * @param string $directory
406
     */
407
    protected function listDirectoryContents($directory, $recursive = false)
408
    {
409
        if ($recursive === true) {
410
            return $this->listDirectoryContentsRecursive($directory);
411
        }
412
413
        $request = rtrim('LIST -aln ' . $this->normalizePath($directory));
414
415
        $connection = $this->getConnection();
416
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
417
        if ($result === false) {
418
            return false;
419
        }
420
421
        if ($directory === '/') {
422
            $directory = '';
423
        }
424
425
        return $this->normalizeListing(explode(PHP_EOL, $result), $directory);
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     *
431
     * @param string $directory
432
     */
433
    protected function listDirectoryContentsRecursive($directory)
434
    {
435
        $request = rtrim('LIST -aln ' . $this->normalizePath($directory));
436
437
        $connection = $this->getConnection();
438
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
439
440
        $listing = $this->normalizeListing(explode(PHP_EOL, $result), $directory);
441
        $output = [];
442
443
        foreach ($listing as $item) {
444
            if ($item['type'] === 'file') {
445
                $output[] = $item;
446
            } elseif ($item['type'] === 'dir') {
447
                $output = array_merge($output,
448
                    $this->listDirectoryContentsRecursive($item['path']));
449
            }
450
        }
451
452
        return $output;
453
    }
454
455
    /**
456
     * Normalize a permissions string.
457
     *
458
     * @param string $permissions
459
     *
460
     * @return int
461
     */
462
    protected function normalizePermissions($permissions)
463
    {
464
        // remove the type identifier
465
        $permissions = substr($permissions, 1);
466
        // map the string rights to the numeric counterparts
467
        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
468
        $permissions = strtr($permissions, $map);
469
        // split up the permission groups
470
        $parts = str_split($permissions, 3);
471
        // convert the groups
472
        $mapper = function ($part) {
473
            return array_sum(str_split($part));
474
        };
475
476
        // converts to decimal number
477
        return octdec(implode('', array_map($mapper, $parts)));
478
    }
479
480
    /**
481
     * Normalize path depending on server.
482
     *
483
     * @param string $path
484
     *
485
     * @return string
486
     */
487
    protected function normalizePath($path)
488
    {
489
        if (empty($path)) {
490
            return '';
491
        }
492
        $path = Normalizer::normalize($path);
493
494
        if ($this->isPureFtpdServer()) {
495
            $path = str_replace(' ', '\ ', $path);
496
        }
497
498
        $path = str_replace('*', '\\*', $path);
499
500
        return $path;
501
    }
502
503
    /**
504
     * @return bool
505
     */
506
    protected function isPureFtpdServer()
507
    {
508
        if (!isset($this->isPureFtpd)) {
509
            $response = $this->rawCommand($this->getConnection(), 'HELP');
510
            $response = end($response);
511
            $this->isPureFtpd = stripos($response, 'Pure-FTPd') !== false;
512
        }
513
514
        return $this->isPureFtpd;
515
    }
516
517
    /**
518
     * Sends an arbitrary command to an FTP server.
519
     *
520
     * @param  Curl $connection The CURL instance
521
     * @param  string $command The command to execute
522
     *
523
     * @return array Returns the server's response as an array of strings
524
     */
525
    protected function rawCommand($connection, $command)
526
    {
527
        $response = '';
528
        $callback = function ($ch, $string) use (&$response) {
529
            $response .= $string;
530
531
            return strlen($string);
532
        };
533
        $connection->exec([
534
            CURLOPT_CUSTOMREQUEST => $command,
535
            CURLOPT_HEADERFUNCTION => $callback,
536
        ]);
537
538
        return explode(PHP_EOL, trim($response));
539
    }
540
541
    /**
542
     * Returns the base url of the connection.
543
     *
544
     * @return string
545
     */
546
    protected function getBaseUri()
547
    {
548
        $protocol = $this->ssl ? 'ftps' : 'ftp';
549
550
        return $protocol . '://' . $this->getHost() . ':' . $this->getPort();
551
    }
552
553
    /**
554
     * Check the connection is established.
555
     */
556
    protected function pingConnection()
557
    {
558
        // We can't use the getConnection, because it will lead to an infinite cycle
559
        if ($this->connection->exec() === false) {
560
            throw new RuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
561
        }
562
    }
563
564
    /**
565
     * Set the connection root.
566
     */
567
    protected function setConnectionRoot()
568
    {
569
        $root = $this->getRoot();
570
        if (empty($root)) {
571
            return;
572
        }
573
574
        // We can't use the getConnection, because it will lead to an infinite cycle
575
        $response = $this->rawCommand($this->connection, 'CWD ' . $root);
576
        list($code, $message) = explode(' ', end($response), 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $message is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
577
        if ($code !== '250') {
578
            throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot());
579
        }
580
    }
581
}
582