Completed
Pull Request — master (#11)
by
unknown
01:58
created

CurlFtpAdapter::setUtf8Mode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
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 RuntimeException;
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
        'host',
17
        'port',
18
        'username',
19
        'password',
20
        'root',
21
        'ssl',
22
        'utf8',
23
    ];
24
25
    /** @var Curl */
26
    protected $connection;
27
28
    /** @var bool */
29
    protected $isPureFtpd;
30
31
    /** @var bool */
32
    protected $utf8 = false;
33
34
    /**
35
     * @param bool $utf8
36
     */
37
    public function setUtf8($utf8)
38
    {
39
        $this->utf8 = (bool) $utf8;
40
    }
41
42
    /**
43
     * Establish a connection.
44
     */
45
    public function connect()
46
    {
47
        $this->connection = new Curl();
48
        $this->connection->setOptions([
49
            CURLOPT_URL => $this->getBaseUri(),
50
            CURLOPT_USERPWD => $this->getUsername() . ':' . $this->getPassword(),
51
            CURLOPT_SSL_VERIFYPEER => false,
52
            CURLOPT_SSL_VERIFYHOST => false,
53
            CURLOPT_FTP_SSL => CURLFTPSSL_TRY,
54
            CURLOPT_FTPSSLAUTH => CURLFTPAUTH_TLS,
55
            CURLOPT_RETURNTRANSFER => true,
56
        ]);
57
58
        $this->pingConnection();
59
        $this->setUtf8Mode();
60
        $this->setConnectionRoot();
61
    }
62
63
    /**
64
     * Close the connection.
65
     */
66
    public function disconnect()
67
    {
68
        if ($this->connection !== null) {
69
            $this->connection = null;
70
        }
71
        $this->isPureFtpd = null;
72
    }
73
74
    /**
75
     * Check if a connection is active.
76
     *
77
     * @return bool
78
     */
79
    public function isConnected()
80
    {
81
        return $this->connection !== null;
82
    }
83
84
    /**
85
     * Write a new file.
86
     *
87
     * @param string $path
88
     * @param string $contents
89
     * @param Config $config Config object
90
     *
91
     * @return array|false false on failure file meta data on success
92
     */
93
    public function write($path, $contents, Config $config)
94
    {
95
        $stream = fopen('php://temp', 'w+b');
96
        fwrite($stream, $contents);
97
        rewind($stream);
98
99
        $result = $this->writeStream($path, $stream, $config);
100
101
        if ($result === false) {
102
            return false;
103
        }
104
105
        $result['contents'] = $contents;
106
        $result['mimetype'] = Util::guessMimeType($path, $contents);
107
108
        return $result;
109
    }
110
111
    /**
112
     * Write a new file using a stream.
113
     *
114
     * @param string   $path
115
     * @param resource $resource
116
     * @param Config   $config Config object
117
     *
118
     * @return array|false false on failure file meta data on success
119
     */
120
    public function writeStream($path, $resource, Config $config)
121
    {
122
        $connection = $this->getConnection();
123
124
        $result = $connection->exec([
125
            CURLOPT_URL => $this->getBaseUri() . '/' . $path,
126
            CURLOPT_UPLOAD => 1,
127
            CURLOPT_INFILE => $resource,
128
        ]);
129
130
        if ($result === false) {
131
            return false;
132
        }
133
134
        $type = 'file';
135
136
        return compact('type', 'path');
137
    }
138
139
    /**
140
     * Update a file.
141
     *
142
     * @param string $path
143
     * @param string $contents
144
     * @param Config $config Config object
145
     *
146
     * @return array|false false on failure file meta data on success
147
     */
148
    public function update($path, $contents, Config $config)
149
    {
150
        return $this->write($path, $contents, $config);
151
    }
152
153
    /**
154
     * Update a file using a stream.
155
     *
156
     * @param string   $path
157
     * @param resource $resource
158
     * @param Config   $config Config object
159
     *
160
     * @return array|false false on failure file meta data on success
161
     */
162
    public function updateStream($path, $resource, Config $config)
163
    {
164
        return $this->writeStream($path, $resource, $config);
165
    }
166
167
    /**
168
     * Rename a file.
169
     *
170
     * @param string $path
171
     * @param string $newpath
172
     *
173
     * @return bool
174
     */
175
    public function rename($path, $newpath)
176
    {
177
        $connection = $this->getConnection();
178
179
        $response = $this->rawCommand($connection, 'RNFR ' . $path);
180
        list($code) = explode(' ', end($response), 2);
181
        if ((int) $code !== 350) {
182
            return false;
183
        }
184
185
        $response = $this->rawCommand($connection, 'RNTO ' . $newpath);
186
        list($code) = explode(' ', end($response), 2);
187
188
        return (int) $code === 250;
189
    }
190
191
    /**
192
     * Copy a file.
193
     *
194
     * @param string $path
195
     * @param string $newpath
196
     *
197
     * @return bool
198
     */
199
    public function copy($path, $newpath)
200
    {
201
        $file = $this->read($path);
202
203
        if ($file === false) {
204
            return false;
205
        }
206
207
        return $this->write($newpath, $file['contents'], new Config()) !== false;
208
    }
209
210
    /**
211
     * Delete a file.
212
     *
213
     * @param string $path
214
     *
215
     * @return bool
216
     */
217 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...
218
    {
219
        $connection = $this->getConnection();
220
221
        $response = $this->rawCommand($connection, 'DELE ' . $path);
222
        list($code) = explode(' ', end($response), 2);
223
224
        return (int) $code === 250;
225
    }
226
227
    /**
228
     * Delete a directory.
229
     *
230
     * @param string $dirname
231
     *
232
     * @return bool
233
     */
234 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...
235
    {
236
        $connection = $this->getConnection();
237
238
        $response = $this->rawCommand($connection, 'RMD ' . $dirname);
239
        list($code) = explode(' ', end($response), 2);
240
241
        return (int) $code === 250;
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
        $response = $this->rawCommand($connection, 'MKD ' . $dirname);
257
        list($code) = explode(' ', end($response), 2);
258
        if ((int) $code !== 257) {
259
            return false;
260
        }
261
262
        return ['type' => 'dir', 'path' => $dirname];
263
    }
264
265
    /**
266
     * Set the visibility for a file.
267
     *
268
     * @param string $path
269
     * @param string $visibility
270
     *
271
     * @return array|false file meta data
272
     */
273
    public function setVisibility($path, $visibility)
274
    {
275
        $connection = $this->getConnection();
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($connection, $request);
285
        list($code) = explode(' ', end($response), 2);
286
        if ((int) $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->getBaseUri() . '/' . $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, $this->listDirectoryContentsRecursive($item['path']));
451
            }
452
        }
453
454
        return $output;
455
    }
456
457
    /**
458
     * Normalize a permissions string.
459
     *
460
     * @param string $permissions
461
     *
462
     * @return int
463
     */
464
    protected function normalizePermissions($permissions)
465
    {
466
        // remove the type identifier
467
        $permissions = substr($permissions, 1);
468
        // map the string rights to the numeric counterparts
469
        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
470
        $permissions = strtr($permissions, $map);
471
        // split up the permission groups
472
        $parts = str_split($permissions, 3);
473
        // convert the groups
474
        $mapper = function ($part) {
475
            return array_sum(str_split($part));
476
        };
477
478
        // converts to decimal number
479
        return octdec(implode('', array_map($mapper, $parts)));
480
    }
481
482
    /**
483
     * Normalize path depending on server.
484
     *
485
     * @param string $path
486
     *
487
     * @return string
488
     */
489
    protected function normalizePath($path)
490
    {
491
        if (empty($path)) {
492
            return '';
493
        }
494
495
        if ($this->isPureFtpdServer()) {
496
            $path = str_replace(' ', '\ ', $path);
497
        }
498
499
        $path = str_replace('*', '\\*', $path);
500
501
        return $path;
502
    }
503
504
    /**
505
     * @return bool
506
     */
507
    protected function isPureFtpdServer()
508
    {
509
        if ($this->isPureFtpd === null) {
510
            $response = $this->rawCommand($this->getConnection(), 'HELP');
511
            $response = end($response);
512
            $this->isPureFtpd = stripos($response, 'Pure-FTPd') !== false;
513
        }
514
515
        return $this->isPureFtpd;
516
    }
517
518
    /**
519
     * Sends an arbitrary command to an FTP server.
520
     *
521
     * @param  Curl   $connection The CURL instance
522
     * @param  string $command    The command to execute
523
     *
524
     * @return array Returns the server's response as an array of strings
525
     */
526
    protected function rawCommand($connection, $command)
527
    {
528
        $response = '';
529
        $callback = function ($ch, $string) use (&$response) {
530
            $response .= $string;
531
532
            return strlen($string);
533
        };
534
        $connection->exec([
535
            CURLOPT_CUSTOMREQUEST => $command,
536
            CURLOPT_HEADERFUNCTION => $callback,
537
        ]);
538
539
        return explode(PHP_EOL, trim($response));
540
    }
541
542
    /**
543
     * Returns the base url of the connection.
544
     *
545
     * @return string
546
     */
547
    protected function getBaseUri()
548
    {
549
        $protocol = $this->ssl ? 'ftps' : 'ftp';
550
551
        return $protocol . '://' . $this->getHost() . ':' . $this->getPort();
552
    }
553
554
    /**
555
     * Check the connection is established.
556
     */
557
    protected function pingConnection()
558
    {
559
        // We can't use the getConnection, because it will lead to an infinite cycle
560
        if ($this->connection->exec() === false) {
561
            throw new RuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
562
        }
563
    }
564
565
    /**
566
     * Set the connection to UTF-8 mode.
567
     */
568
    protected function setUtf8Mode()
569
    {
570
        if (!$this->utf8) {
571
            return;
572
        }
573
574
        $response = $this->rawCommand($this->connection, 'OPTS UTF8 ON');
575
        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...
576
        if ($code !== '200') {
577
            throw new RuntimeException(
578
                'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort()
579
            );
580
        }
581
    }
582
583
    /**
584
     * Set the connection root.
585
     */
586
    protected function setConnectionRoot()
587
    {
588
        $root = $this->getRoot();
589
        if (empty($root)) {
590
            return;
591
        }
592
593
        // We can't use the getConnection, because it will lead to an infinite cycle
594
        $response = $this->rawCommand($this->connection, 'CWD ' . $root);
595
        list($code) = explode(' ', end($response), 2);
596
        if ((int) $code !== 250) {
597
            throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot());
598
        }
599
    }
600
}
601