Completed
Push — master ( 79962c...9c0d5f )
by Vladimir
05:28
created

CurlFtpAdapter::setSsl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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