Completed
Push — master ( e187cb...30919d )
by Vladimir
02:16
created

CurlFtpAdapter::setVisibility()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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