Completed
Push — master ( 6de68d...83f122 )
by Vladimir
02:45 queued 49s
created

CurlFtpAdapter::setVisibility()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
500
501
        $response = $this->rawCommand('HELP');
502
        $response = end($response);
503
504
        return stripos($response, 'Pure-FTPd') !== false;
505
    }
506
507
    /**
508
     * Sends an arbitrary command to an FTP server
509
     * 
510
     * @param  string $command The command to execute
511
     * @return array Returns the server's response as an array of strings
512
     */
513
    protected function rawCommand($command)
514
    {
515
        $response = '';
516
        $callback = function ($ch, $string) use (&$response) {
517
            $response .= $string;
518
            return strlen($string);
519
        };
520
        $connection = $this->getConnection();
521
        $connection->exec([
522
            CURLOPT_CUSTOMREQUEST => $command,
523
            CURLOPT_HEADERFUNCTION => $callback,
524
        ]);
525
        return explode(PHP_EOL, trim($response));
526
    }
527
528
    protected function getUrl()
529
    {
530
        return $this->protocol.'://'.$this->getHost().':'.$this->getPort();
531
    }
532
}
533