Completed
Push — master ( 36861d...250896 )
by Vladimir
04:55 queued 03:00
created

CurlFtpAdapter::setUtf8()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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