Completed
Push — master ( e79825...793039 )
by Vladimir
8s
created

CurlFtpAdapter::listDirectoryContents()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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