Completed
Push — master ( 4343d7...64e671 )
by Vladimir
10s
created

CurlFtpAdapter::setConnectionRoot()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 8
nc 3
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 string */
26
    protected $protocol = 'ftp';
27
28
    /** @var Curl */
29
    protected $connection;
30
31
    /**
32
     * @var bool
33
     */
34
    protected $isPureFtpd;
35
36
    /**
37
     * Set remote protocol. ftp or ftps.
38
     *
39
     * @param string $protocol
40
     */
41
    public function setProtocol($protocol)
42
    {
43
        $this->protocol = $protocol;
44
    }
45
46
    /**
47
     * Establish a connection.
48
     */
49
    public function connect()
50
    {
51
        $this->connection = new Curl();
52
        $this->connection->setOptions([
53
            CURLOPT_URL => $this->getUrl(),
54
            CURLOPT_USERPWD => $this->getUsername() . ':' . $this->getPassword(),
55
            CURLOPT_SSL_VERIFYPEER => false,
56
            CURLOPT_SSL_VERIFYHOST => false,
57
            CURLOPT_FTP_SSL => CURLFTPSSL_TRY,
58
            CURLOPT_FTPSSLAUTH => CURLFTPAUTH_TLS,
59
            CURLOPT_RETURNTRANSFER => true,
60
        ]);
61
62
        $this->pingConnection();
63
        $this->setConnectionRoot();
64
    }
65
66
    /**
67
     * Close the connection.
68
     */
69
    public function disconnect()
70
    {
71
        if ($this->connection !== null) {
72
            $this->connection = null;
73
        }
74
        $this->isPureFtpd = null;
75
    }
76
77
    /**
78
     * Check if a connection is active.
79
     *
80
     * @return bool
81
     */
82
    public function isConnected()
83
    {
84
        return $this->connection !== null;
85
    }
86
87
    /**
88
     * Write a new file.
89
     *
90
     * @param string $path
91
     * @param string $contents
92
     * @param Config $config Config object
93
     *
94
     * @return array|false false on failure file meta data on success
95
     */
96
    public function write($path, $contents, Config $config)
97
    {
98
        $stream = fopen('php://temp', 'w+b');
99
        fwrite($stream, $contents);
100
        rewind($stream);
101
102
        $result = $this->writeStream($path, $stream, $config);
103
104
        if ($result === false) {
105
            return false;
106
        }
107
108
        $result['contents'] = $contents;
109
        $result['mimetype'] = Util::guessMimeType($path, $contents);
110
111
        return $result;
112
    }
113
114
    /**
115
     * Write a new file using a stream.
116
     *
117
     * @param string   $path
118
     * @param resource $resource
119
     * @param Config   $config Config object
120
     *
121
     * @return array|false false on failure file meta data on success
122
     */
123
    public function writeStream($path, $resource, Config $config)
124
    {
125
        $connection = $this->getConnection();
126
127
        $result = $connection->exec([
128
            CURLOPT_URL => $this->getUrl() . '/' . $path,
129
            CURLOPT_UPLOAD => 1,
130
            CURLOPT_INFILE => $resource,
131
        ]);
132
133
        if ($result === false) {
134
            return false;
135
        }
136
137
        $type = 'file';
138
139
        return compact('type', 'path');
140
    }
141
142
    /**
143
     * Update a file.
144
     *
145
     * @param string $path
146
     * @param string $contents
147
     * @param Config $config Config object
148
     *
149
     * @return array|false false on failure file meta data on success
150
     */
151
    public function update($path, $contents, Config $config)
152
    {
153
        return $this->write($path, $contents, $config);
154
    }
155
156
    /**
157
     * Update a file using a stream.
158
     *
159
     * @param string   $path
160
     * @param resource $resource
161
     * @param Config   $config Config object
162
     *
163
     * @return array|false false on failure file meta data on success
164
     */
165
    public function updateStream($path, $resource, Config $config)
166
    {
167
        return $this->writeStream($path, $resource, $config);
168
    }
169
170
    /**
171
     * Rename a file.
172
     *
173
     * @param string $path
174
     * @param string $newpath
175
     *
176
     * @return bool
177
     */
178 View Code Duplication
    public function rename($path, $newpath)
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...
179
    {
180
        $connection = $this->getConnection();
181
182
        $result = $connection->exec([
183
            CURLOPT_POSTQUOTE => ['RNFR ' . $path, 'RNTO ' . $newpath],
184
        ]);
185
186
        return $result !== false;
187
    }
188
189
    /**
190
     * Copy a file.
191
     *
192
     * @param string $path
193
     * @param string $newpath
194
     *
195
     * @return bool
196
     */
197
    public function copy($path, $newpath)
198
    {
199
        $file = $this->read($path);
200
201
        if ($file === false) {
202
            return false;
203
        }
204
205
        return $this->write($newpath, $file['contents'], new Config()) !== false;
206
    }
207
208
    /**
209
     * Delete a file.
210
     *
211
     * @param string $path
212
     *
213
     * @return bool
214
     */
215 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...
216
    {
217
        $connection = $this->getConnection();
218
219
        $result = $connection->exec([
220
            CURLOPT_POSTQUOTE => ['DELE ' . $path],
221
        ]);
222
223
        return $result !== false;
224
    }
225
226
    /**
227
     * Delete a directory.
228
     *
229
     * @param string $dirname
230
     *
231
     * @return bool
232
     */
233 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...
234
    {
235
        $connection = $this->getConnection();
236
237
        $result = $connection->exec([
238
            CURLOPT_POSTQUOTE => ['RMD ' . $dirname],
239
        ]);
240
241
        return $result !== false;
242
    }
243
244
    /**
245
     * Create a directory.
246
     *
247
     * @param string $dirname directory name
248
     * @param Config $config
249
     *
250
     * @return array|false
251
     */
252
    public function createDir($dirname, Config $config)
253
    {
254
        $connection = $this->getConnection();
255
256
        $result = $connection->exec([
257
            CURLOPT_POSTQUOTE => ['MKD ' . $dirname],
258
        ]);
259
260
        if ($result === false) {
261
            return false;
262
        }
263
264
        return ['type' => 'dir', 'path' => $dirname];
265
    }
266
267
    /**
268
     * Set the visibility for a file.
269
     *
270
     * @param string $path
271
     * @param string $visibility
272
     *
273
     * @return array|false file meta data
274
     */
275
    public function setVisibility($path, $visibility)
276
    {
277
        if ($visibility === AdapterInterface::VISIBILITY_PUBLIC) {
278
            $mode = $this->getPermPublic();
279
        } else {
280
            $mode = $this->getPermPrivate();
281
        }
282
283
        $request = sprintf('SITE CHMOD %o %s', $mode, $path);
284
        $response = $this->rawCommand($this->getConnection(), $request);
285
        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...
286
        if ($code !== '200') {
287
            return false;
288
        }
289
290
        return $this->getMetadata($path);
291
    }
292
293
    /**
294
     * Read a file.
295
     *
296
     * @param string $path
297
     *
298
     * @return array|false
299
     */
300
    public function read($path)
301
    {
302
        if (!$object = $this->readStream($path)) {
303
            return false;
304
        }
305
306
        $object['contents'] = stream_get_contents($object['stream']);
307
        fclose($object['stream']);
308
        unset($object['stream']);
309
310
        return $object;
311
    }
312
313
    /**
314
     * Read a file as a stream.
315
     *
316
     * @param string $path
317
     *
318
     * @return array|false
319
     */
320
    public function readStream($path)
321
    {
322
        $stream = fopen('php://temp', 'w+b');
323
324
        $connection = $this->getConnection();
325
326
        $result = $connection->exec([
327
            CURLOPT_URL => $this->getUrl() . '/' . $path,
328
            CURLOPT_FILE => $stream,
329
        ]);
330
331
        if (!$result) {
332
            fclose($stream);
333
334
            return false;
335
        }
336
337
        rewind($stream);
338
339
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
340
    }
341
342
    /**
343
     * Get all the meta data of a file or directory.
344
     *
345
     * @param string $path
346
     *
347
     * @return array|false
348
     */
349
    public function getMetadata($path)
350
    {
351
        if ($path === '') {
352
            return ['type' => 'dir', 'path' => ''];
353
        }
354
355
        $request = rtrim('LIST -A ' . $this->normalizePath($path));
356
357
        $connection = $this->getConnection();
358
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
359
        if ($result === false) {
360
            return false;
361
        }
362
        $listing = $this->normalizeListing(explode(PHP_EOL, $result), '');
363
364
        return current($listing);
365
    }
366
367
    /**
368
     * Get the mimetype of a file.
369
     *
370
     * @param string $path
371
     *
372
     * @return array|false
373
     */
374
    public function getMimetype($path)
375
    {
376
        if (!$metadata = $this->getMetadata($path)) {
377
            return false;
378
        }
379
380
        $metadata['mimetype'] = MimeType::detectByFilename($path);
381
382
        return $metadata;
383
    }
384
385
    /**
386
     * Get the timestamp of a file.
387
     *
388
     * @param string $path
389
     *
390
     * @return array|false
391
     */
392
    public function getTimestamp($path)
393
    {
394
        $response = $this->rawCommand($this->getConnection(), 'MDTM ' . $path);
395
        list($code, $time) = explode(' ', end($response), 2);
396
        if ($code !== '213') {
397
            return false;
398
        }
399
400
        $datetime = DateTime::createFromFormat('YmdHis', $time);
401
402
        return ['path' => $path, 'timestamp' => $datetime->getTimestamp()];
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     *
408
     * @param string $directory
409
     */
410
    protected function listDirectoryContents($directory, $recursive = false)
411
    {
412
        if ($recursive === true) {
413
            return $this->listDirectoryContentsRecursive($directory);
414
        }
415
416
        $request = rtrim('LIST -aln ' . $this->normalizePath($directory));
417
418
        $connection = $this->getConnection();
419
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
420
        if ($result === false) {
421
            return false;
422
        }
423
424
        if ($directory === '/') {
425
            $directory = '';
426
        }
427
428
        return $this->normalizeListing(explode(PHP_EOL, $result), $directory);
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     *
434
     * @param string $directory
435
     */
436
    protected function listDirectoryContentsRecursive($directory)
437
    {
438
        $request = rtrim('LIST -aln ' . $this->normalizePath($directory));
439
440
        $connection = $this->getConnection();
441
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
442
443
        $listing = $this->normalizeListing(explode(PHP_EOL, $result), $directory);
444
        $output = [];
445
446
        foreach ($listing as $item) {
447
            if ($item['type'] === 'file') {
448
                $output[] = $item;
449
            } elseif ($item['type'] === 'dir') {
450
                $output = array_merge($output,
451
                    $this->listDirectoryContentsRecursive($item['path']));
452
            }
453
        }
454
455
        return $output;
456
    }
457
458
    /**
459
     * Normalize a permissions string.
460
     *
461
     * @param string $permissions
462
     *
463
     * @return int
464
     */
465
    protected function normalizePermissions($permissions)
466
    {
467
        // remove the type identifier
468
        $permissions = substr($permissions, 1);
469
        // map the string rights to the numeric counterparts
470
        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
471
        $permissions = strtr($permissions, $map);
472
        // split up the permission groups
473
        $parts = str_split($permissions, 3);
474
        // convert the groups
475
        $mapper = function ($part) {
476
            return array_sum(str_split($part));
477
        };
478
479
        // converts to decimal number
480
        return octdec(implode('', array_map($mapper, $parts)));
481
    }
482
483
    /**
484
     * Normalize path depending on server.
485
     *
486
     * @param string $path
487
     *
488
     * @return string
489
     */
490
    protected function normalizePath($path)
491
    {
492
        if (empty($path)) {
493
            return '';
494
        }
495
        $path = Normalizer::normalize($path);
496
497
        if ($this->isPureFtpdServer()) {
498
            $path = str_replace(' ', '\ ', $path);
499
        }
500
501
        $path = str_replace('*', '\\*', $path);
502
503
        return $path;
504
    }
505
506
    /**
507
     * @return bool
508
     */
509
    protected function isPureFtpdServer()
510
    {
511
        if (!isset($this->isPureFtpd)) {
512
            $response = $this->rawCommand($this->getConnection(), 'HELP');
513
            $response = end($response);
514
            $this->isPureFtpd = stripos($response, 'Pure-FTPd') !== false;
515
        }
516
517
        return $this->isPureFtpd;
518
    }
519
520
    /**
521
     * Sends an arbitrary command to an FTP server.
522
     *
523
     * @param  Curl $connection The CURL instance
524
     * @param  string $command The command to execute
525
     *
526
     * @return array Returns the server's response as an array of strings
527
     */
528
    protected function rawCommand($connection, $command)
529
    {
530
        $response = '';
531
        $callback = function ($ch, $string) use (&$response) {
532
            $response .= $string;
533
534
            return strlen($string);
535
        };
536
        $connection->exec([
537
            CURLOPT_CUSTOMREQUEST => $command,
538
            CURLOPT_HEADERFUNCTION => $callback,
539
        ]);
540
541
        return explode(PHP_EOL, trim($response));
542
    }
543
544
    /**
545
     * Set the connection root.
546
     */
547
    protected function setConnectionRoot()
548
    {
549
        $root = $this->getRoot();
550
        if (empty($root)) {
551
            return;
552
        }
553
554
        // We can't use the getConnection, because it will lead to an infinite cycle
555
        $response = $this->rawCommand($this->connection, 'CWD ' . $root);
556
        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...
557
        if ($code !== '250') {
558
            throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot());
559
        }
560
    }
561
562
    /**
563
     * Check the connection is established.
564
     */
565
    protected function pingConnection()
566
    {
567
        // We can't use the getConnection, because it will lead to an infinite cycle
568
        if ($this->connection->exec() === false) {
569
            throw new RuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
570
        }
571
    }
572
573
    protected function getUrl()
574
    {
575
        return $this->protocol . '://' . $this->getHost() . ':' . $this->getPort();
576
    }
577
}
578