Completed
Push — develop ( f11ef2...d41b65 )
by David
06:01 queued 11s
created

Ftp::listDirectoryContents()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 2
dl 0
loc 11
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace League\Flysystem\Adapter;
4
5
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
6
use League\Flysystem\AdapterInterface;
7
use League\Flysystem\Config;
8
use League\Flysystem\ConnectionErrorException;
9
use League\Flysystem\ConnectionRuntimeException;
10
use League\Flysystem\InvalidRootException;
11
use League\Flysystem\Util;
12
use League\Flysystem\Util\MimeType;
13
14
class Ftp extends AbstractFtpAdapter
15
{
16
    use StreamedCopyTrait;
17
18
    /**
19
     * @var int
20
     */
21
    protected $transferMode = FTP_BINARY;
22
23
    /**
24
     * @var null|bool
25
     */
26
    protected $ignorePassiveAddress = null;
27
28
    /**
29
     * @var bool
30
     */
31
    protected $recurseManually = false;
32
33
    /**
34
     * @var bool
35
     */
36
    protected $utf8 = false;
37
38
    /**
39
     * @var array
40
     */
41
    protected $configurable = [
42
        'host',
43
        'port',
44
        'username',
45
        'password',
46
        'ssl',
47
        'timeout',
48
        'root',
49
        'permPrivate',
50
        'permPublic',
51
        'passive',
52
        'transferMode',
53
        'systemType',
54
        'ignorePassiveAddress',
55
        'recurseManually',
56
        'utf8',
57
        'enableTimestampsOnUnixListings',
58
    ];
59
60
    /**
61
     * @var bool
62
     */
63
    protected $isPureFtpd;
64
65
    /**
66
     * Set the transfer mode.
67
     *
68
     * @param int $mode
69
     *
70
     * @return $this
71
     */
72
    public function setTransferMode($mode)
73
    {
74
        $this->transferMode = $mode;
75
76
        return $this;
77
    }
78
79
    /**
80
     * Set if Ssl is enabled.
81
     *
82
     * @param bool $ssl
83
     *
84
     * @return $this
85
     */
86
    public function setSsl($ssl)
87
    {
88
        $this->ssl = (bool) $ssl;
89
90
        return $this;
91
    }
92
93
    /**
94
     * Set if passive mode should be used.
95
     *
96
     * @param bool $passive
97
     */
98
    public function setPassive($passive = true)
99
    {
100
        $this->passive = $passive;
101
    }
102
103
    /**
104
     * @param bool $ignorePassiveAddress
105
     */
106
    public function setIgnorePassiveAddress($ignorePassiveAddress)
107
    {
108
        $this->ignorePassiveAddress = $ignorePassiveAddress;
109
    }
110
111
    /**
112
     * @param bool $recurseManually
113
     */
114
    public function setRecurseManually($recurseManually)
115
    {
116
        $this->recurseManually = $recurseManually;
117
    }
118
119
    /**
120
     * @param bool $utf8
121
     */
122
    public function setUtf8($utf8)
123
    {
124
        $this->utf8 = (bool) $utf8;
125
    }
126
127
    /**
128
     * Connect to the FTP server.
129
     */
130
    public function connect()
131
    {
132
        $tries = 3;
133
        start_connecting:
134
135
        if ($this->ssl) {
136
            $this->connection = @ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout());
137
        } else {
138
            $this->connection = @ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout());
139
        }
140
141
        if ( ! $this->connection) {
142
            $tries--;
143
144
            if ($tries > 0) goto start_connecting;
145
146
            throw new ConnectionRuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
147
        }
148
149
        $this->login();
150
        $this->setUtf8Mode();
151
        $this->setConnectionPassiveMode();
152
        $this->setConnectionRoot();
153
        $this->isPureFtpd = $this->isPureFtpdServer();
154
    }
155
156
    /**
157
     * Set the connection to UTF-8 mode.
158
     */
159
    protected function setUtf8Mode()
160
    {
161
        if ($this->utf8) {
162
            $response = ftp_raw($this->connection, "OPTS UTF8 ON");
163
            if (substr($response[0], 0, 3) !== '200') {
164
                throw new ConnectionRuntimeException(
165
                    'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort()
166
                );
167
            }
168
        }
169
    }
170
171
    /**
172
     * Set the connections to passive mode.
173
     *
174
     * @throws ConnectionRuntimeException
175
     */
176
    protected function setConnectionPassiveMode()
177
    {
178
        if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) {
179
            ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress);
180
        }
181
182
        if ( ! ftp_pasv($this->connection, $this->passive)) {
183
            throw new ConnectionRuntimeException(
184
                'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort()
185
            );
186
        }
187
    }
188
189
    /**
190
     * Set the connection root.
191
     */
192
    protected function setConnectionRoot()
193
    {
194
        $root = $this->getRoot();
195
        $connection = $this->connection;
196
197
        if ($root && ! ftp_chdir($connection, $root)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $root of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
198
            throw new InvalidRootException('Root is invalid or does not exist: ' . $this->getRoot());
199
        }
200
201
        // Store absolute path for further reference.
202
        // This is needed when creating directories and
203
        // initial root was a relative path, else the root
204
        // would be relative to the chdir'd path.
205
        $this->root = ftp_pwd($connection);
206
    }
207
208
    /**
209
     * Login.
210
     *
211
     * @throws ConnectionRuntimeException
212
     */
213
    protected function login()
214
    {
215
        set_error_handler(function () {
216
        });
217
        $isLoggedIn = ftp_login(
218
            $this->connection,
219
            $this->getUsername(),
220
            $this->getPassword()
221
        );
222
        restore_error_handler();
223
224
        if ( ! $isLoggedIn) {
225
            $this->disconnect();
226
            throw new ConnectionRuntimeException(
227
                'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort(
228
                ) . ', username: ' . $this->getUsername()
229
            );
230
        }
231
    }
232
233
    /**
234
     * Disconnect from the FTP server.
235
     */
236
    public function disconnect()
237
    {
238
        if (is_resource($this->connection)) {
239
            @ftp_close($this->connection);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
240
        }
241
242
        $this->connection = null;
243
    }
244
245
    /**
246
     * @inheritdoc
247
     */
248
    public function write($path, $contents, Config $config)
249
    {
250
        $stream = fopen('php://temp', 'w+b');
251
        fwrite($stream, $contents);
252
        rewind($stream);
253
        $result = $this->writeStream($path, $stream, $config);
254
        fclose($stream);
255
256
        if ($result === false) {
257
            return false;
258
        }
259
260
        $result['contents'] = $contents;
261
        $result['mimetype'] = $config->get('mimetype') ?: Util::guessMimeType($path, $contents);
262
263
        return $result;
264
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269
    public function writeStream($path, $resource, Config $config)
270
    {
271
        $this->ensureDirectory(Util::dirname($path));
272
273
        if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) {
274
            return false;
275
        }
276
277
        if ($visibility = $config->get('visibility')) {
278
            $this->setVisibility($path, $visibility);
279
        }
280
281
        $type = 'file';
282
283
        return compact('type', 'path', 'visibility');
284
    }
285
286
    /**
287
     * @inheritdoc
288
     */
289
    public function update($path, $contents, Config $config)
290
    {
291
        return $this->write($path, $contents, $config);
292
    }
293
294
    /**
295
     * @inheritdoc
296
     */
297
    public function updateStream($path, $resource, Config $config)
298
    {
299
        return $this->writeStream($path, $resource, $config);
300
    }
301
302
    /**
303
     * @inheritdoc
304
     */
305
    public function rename($path, $newpath)
306
    {
307
        return ftp_rename($this->getConnection(), $path, $newpath);
308
    }
309
310
    /**
311
     * @inheritdoc
312
     */
313
    public function delete($path)
314
    {
315
        return ftp_delete($this->getConnection(), $path);
316
    }
317
318
    /**
319
     * @inheritdoc
320
     */
321
    public function deleteDir($dirname)
322
    {
323
        $connection = $this->getConnection();
324
        $contents = array_reverse($this->listDirectoryContents($dirname, false));
325
326
        foreach ($contents as $object) {
327
            if ($object['type'] === 'file') {
328
                if ( ! ftp_delete($connection, $object['path'])) {
329
                    return false;
330
                }
331
            } elseif ( ! $this->deleteDir($object['path'])) {
332
                return false;
333
            }
334
        }
335
336
        return ftp_rmdir($connection, $dirname);
337
    }
338
339
    /**
340
     * @inheritdoc
341
     */
342
    public function createDir($dirname, Config $config)
343
    {
344
        $connection = $this->getConnection();
345
        $directories = explode('/', $dirname);
346
347
        foreach ($directories as $directory) {
348
            if (false === $this->createActualDirectory($directory, $connection)) {
349
                $this->setConnectionRoot();
350
351
                return false;
352
            }
353
354
            ftp_chdir($connection, $directory);
355
        }
356
357
        $this->setConnectionRoot();
358
359
        return ['type' => 'dir', 'path' => $dirname];
360
    }
361
362
    /**
363
     * Create a directory.
364
     *
365
     * @param string   $directory
366
     * @param resource $connection
367
     *
368
     * @return bool
369
     */
370
    protected function createActualDirectory($directory, $connection)
371
    {
372
        // List the current directory
373
        $listing = ftp_nlist($connection, '.') ?: [];
374
375
        foreach ($listing as $key => $item) {
376
            if (preg_match('~^\./.*~', $item)) {
377
                $listing[$key] = substr($item, 2);
378
            }
379
        }
380
381
        if (in_array($directory, $listing, true)) {
382
            return true;
383
        }
384
385
        return (boolean) ftp_mkdir($connection, $directory);
386
    }
387
388
    /**
389
     * @inheritdoc
390
     */
391
    public function getMetadata($path)
392
    {
393
        if ($path === '') {
394
            return ['type' => 'dir', 'path' => ''];
395
        }
396
397 View Code Duplication
        if (@ftp_chdir($this->getConnection(), $path) === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
398
            $this->setConnectionRoot();
399
400
            return ['type' => 'dir', 'path' => $path];
401
        }
402
403
        $listing = $this->ftpRawlist('-A', $path);
404
405
        if (empty($listing) || in_array('total 0', $listing, true)) {
406
            return false;
407
        }
408
409
        if (preg_match('/.* not found/', $listing[0])) {
410
            return false;
411
        }
412
413
        if (preg_match('/^total [0-9]*$/', $listing[0])) {
414
            array_shift($listing);
415
        }
416
417
        return $this->normalizeObject($listing[0], '');
418
    }
419
420
    /**
421
     * @inheritdoc
422
     */
423
    public function getMimetype($path)
424
    {
425
        if ( ! $metadata = $this->getMetadata($path)) {
426
            return false;
427
        }
428
429
        $metadata['mimetype'] = MimeType::detectByFilename($path);
430
431
        return $metadata;
432
    }
433
434
    /**
435
     * @inheritdoc
436
     */
437
    public function getTimestamp($path)
438
    {
439
        $timestamp = ftp_mdtm($this->getConnection(), $path);
440
441
        return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false;
442
    }
443
444
    /**
445
     * @inheritdoc
446
     */
447
    public function read($path)
448
    {
449
        if ( ! $object = $this->readStream($path)) {
450
            return false;
451
        }
452
453
        $object['contents'] = stream_get_contents($object['stream']);
454
        fclose($object['stream']);
455
        unset($object['stream']);
456
457
        return $object;
458
    }
459
460
    /**
461
     * @inheritdoc
462
     */
463
    public function readStream($path)
464
    {
465
        $stream = fopen('php://temp', 'w+b');
466
        $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode);
467
        rewind($stream);
468
469
        if ( ! $result) {
470
            fclose($stream);
471
472
            return false;
473
        }
474
475
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
476
    }
477
478
    /**
479
     * @inheritdoc
480
     */
481
    public function setVisibility($path, $visibility)
482
    {
483
        $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate();
484
485
        if ( ! ftp_chmod($this->getConnection(), $mode, $path)) {
486
            return false;
487
        }
488
489
        return compact('path', 'visibility');
490
    }
491
492
    /**
493
     * @inheritdoc
494
     *
495
     * @param string $directory
496
     */
497
    protected function listDirectoryContents($directory, $recursive = true)
498
    {
499
        if ($recursive && $this->recurseManually) {
500
            return $this->listDirectoryContentsRecursive($directory);
501
        }
502
503
        $options = $recursive ? '-alnR' : '-aln';
504
        $listing = $this->ftpRawlist($options, $directory);
505
506
        return $listing ? $this->normalizeListing($listing, $directory) : [];
507
    }
508
509
    /**
510
     * @inheritdoc
511
     *
512
     * @param string $directory
513
     */
514
    protected function listDirectoryContentsRecursive($directory)
515
    {
516
        $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: [], $directory);
517
        $output = [];
518
519
        foreach ($listing as $item) {
520
            $output[] = $item;
521
            if ($item['type'] !== 'dir') {
522
                continue;
523
            }
524
            $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path']));
525
        }
526
527
        return $output;
528
    }
529
530
    /**
531
     * Check if the connection is open.
532
     *
533
     * @return bool
534
     *
535
     * @throws ConnectionErrorException
536
     */
537
    public function isConnected()
538
    {
539
        return is_resource($this->connection)
540
            && $this->getRawExecResponseCode('NOOP') === 200;
541
    }
542
543
    /**
544
     * @return bool
545
     */
546
    protected function isPureFtpdServer()
547
    {
548
        $response = ftp_raw($this->connection, 'HELP');
549
550
        return stripos(implode(' ', $response), 'Pure-FTPd') !== false;
551
    }
552
553
    /**
554
     * The ftp_rawlist function with optional escaping.
555
     *
556
     * @param string $options
557
     * @param string $path
558
     *
559
     * @return array
560
     */
561
    protected function ftpRawlist($options, $path)
562
    {
563
        $connection = $this->getConnection();
564
565
        if ($this->isPureFtpd) {
566
            $path = str_replace(' ', '\ ', $path);
567
            $this->escapePath($path);
568
        }
569
570
        return ftp_rawlist($connection, $options . ' ' . $path);
571
    }
572
573
    private function getRawExecResponseCode($command)
574
    {
575
        $response = @ftp_raw($this->connection, trim($command));
576
577
        return (int) preg_replace('/\D/', '', implode(' ', $response));
578
    }
579
}
580