Completed
Push — master ( 0d273e...c7bc8e )
by Vladimir
01:55
created

CurlFtpAdapter::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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