Completed
Push — master ( e1a755...dc4baa )
by Frank
02:36
created

Ftp::isPureFtpdServer()   A

Complexity

Conditions 1
Paths 1

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 7
Code Lines 4

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 1
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
crap 1
1
<?php
2
3
namespace League\Flysystem\Adapter;
4
5
use ErrorException;
6
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
7
use League\Flysystem\AdapterInterface;
8
use League\Flysystem\Config;
9
use League\Flysystem\Util;
10
use League\Flysystem\Util\MimeType;
11
use RuntimeException;
12
13
class Ftp extends AbstractFtpAdapter
14
{
15
    use StreamedCopyTrait;
16
17
    /**
18
     * @var int
19
     */
20
    protected $transferMode = FTP_BINARY;
21
22
    /**
23
     * @var null|bool
24
     */
25
    protected $ignorePassiveAddress = null;
26
27
    /**
28
     * @var bool
29
     */
30
    protected $recurseManually = false;
31
32
    /**
33
     * @var array
34
     */
35
    protected $configurable = [
36
        'host',
37
        'port',
38
        'username',
39
        'password',
40
        'ssl',
41
        'timeout',
42
        'root',
43
        'permPrivate',
44
        'permPublic',
45
        'passive',
46
        'transferMode',
47
        'systemType',
48
        'ignorePassiveAddress',
49
        'recurseManually',
50
    ];
51
52
    /**
53
     * @var bool
54
     */
55
    protected $isPureFtpd;
56
57
    /**
58
     * Set the transfer mode.
59
     *
60
     * @param int $mode
61
     *
62
     * @return $this
63
     */
64 3
    public function setTransferMode($mode)
65
    {
66 3
        $this->transferMode = $mode;
67
68 3
        return $this;
69
    }
70
71
    /**
72
     * Set if Ssl is enabled.
73
     *
74
     * @param bool $ssl
75
     *
76
     * @return $this
77
     */
78 96
    public function setSsl($ssl)
79
    {
80 96
        $this->ssl = (bool) $ssl;
81
82 96
        return $this;
83
    }
84
85
    /**
86
     * Set if passive mode should be used.
87
     *
88
     * @param bool $passive
89
     */
90 78
    public function setPassive($passive = true)
91
    {
92 78
        $this->passive = $passive;
93 78
    }
94
95
    /**
96
     * @param bool $ignorePassiveAddress
97
     */
98 3
    public function setIgnorePassiveAddress($ignorePassiveAddress)
99
    {
100 3
        $this->ignorePassiveAddress = $ignorePassiveAddress;
101 3
    }
102
103
    /**
104
     * @param bool $recurseManually
105
     */
106 66
    public function setRecurseManually($recurseManually)
107
    {
108 66
        $this->recurseManually = $recurseManually;
109 66
    }
110
111
    /**
112
     * Connect to the FTP server.
113
     */
114 90
    public function connect()
115
    {
116 90
        if ($this->ssl) {
117 87
            $this->connection = ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout());
118 87
        } else {
119 3
            $this->connection = ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout());
120
        }
121
122 90
        if ( ! $this->connection) {
123 6
            throw new RuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
124
        }
125
126 84
        $this->login();
127 81
        $this->setConnectionPassiveMode();
128 78
        $this->setConnectionRoot();
129 75
        $this->isPureFtpd = $this->isPureFtpdServer();
130 72
    }
131
132
    /**
133
     * Set the connections to passive mode.
134
     *
135
     * @throws RuntimeException
136
     */
137 81
    protected function setConnectionPassiveMode()
138
    {
139 81
        if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) {
140 3
            ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress);
141 3
        }
142
143 81
        if ( ! ftp_pasv($this->connection, $this->passive)) {
144 3
            throw new RuntimeException(
145 3
                'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort()
146 3
            );
147
        }
148 78
    }
149
150
    /**
151
     * Set the connection root.
152
     */
153 78
    protected function setConnectionRoot()
154
    {
155 78
        $root = $this->getRoot();
156 78
        $connection = $this->connection;
157
158 78
        if (empty($root) === false && ! ftp_chdir($connection, $root)) {
159 3
            throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot());
160
        }
161
162
        // Store absolute path for further reference.
163
        // This is needed when creating directories and
164
        // initial root was a relative path, else the root
165
        // would be relative to the chdir'd path.
166 75
        $this->root = ftp_pwd($connection);
167 75
    }
168
169
    /**
170
     * Login.
171
     *
172
     * @throws RuntimeException
173
     */
174
    protected function login()
175
    {
176
        set_error_handler(function () {});
177 84
        $isLoggedIn = ftp_login(
178 84
            $this->connection,
179 84
            $this->getUsername(),
180 84
            $this->getPassword()
181 84
        );
182 84
        restore_error_handler();
183
184 84
        if ( ! $isLoggedIn) {
185 3
            $this->disconnect();
186 3
            throw new RuntimeException(
187 3
                'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort(
188 3
                ) . ', username: ' . $this->getUsername()
189 3
            );
190
        }
191 81
    }
192
193
    /**
194
     * Disconnect from the FTP server.
195
     */
196 96
    public function disconnect()
197
    {
198 96
        if ($this->isConnected()) {
199 3
            ftp_close($this->connection);
200 3
        }
201
202 96
        $this->connection = null;
203 96
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208 9
    public function write($path, $contents, Config $config)
209
    {
210 9
        $stream = fopen('php://temp', 'w+b');
211 9
        fwrite($stream, $contents);
212 9
        rewind($stream);
213 9
        $result = $this->writeStream($path, $stream, $config);
214 9
        fclose($stream);
215
216 9
        if ($result === false) {
217 6
            return false;
218
        }
219
220 6
        $result['contents'] = $contents;
221 6
        $result['mimetype'] = Util::guessMimeType($path, $contents);
222
223 6
        return $result;
224
    }
225
226
    /**
227
     * @inheritdoc
228
     */
229 9
    public function writeStream($path, $resource, Config $config)
230
    {
231 9
        $this->ensureDirectory(Util::dirname($path));
232
233 9
        if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) {
234 6
            return false;
235
        }
236
237 6
        if ($visibility = $config->get('visibility')) {
238 6
            $this->setVisibility($path, $visibility);
239 6
        }
240
241 6
        $type = 'file';
242
243 6
        return compact('type', 'path', 'visibility');
244
    }
245
246
    /**
247
     * @inheritdoc
248
     */
249 6
    public function update($path, $contents, Config $config)
250
    {
251 6
        return $this->write($path, $contents, $config);
252
    }
253
254
    /**
255
     * @inheritdoc
256
     */
257 3
    public function updateStream($path, $resource, Config $config)
258
    {
259 3
        return $this->writeStream($path, $resource, $config);
260
    }
261
262
    /**
263
     * @inheritdoc
264
     */
265 3
    public function rename($path, $newpath)
266
    {
267 3
        return ftp_rename($this->getConnection(), $path, $newpath);
268
    }
269
270
    /**
271
     * @inheritdoc
272
     */
273 3
    public function delete($path)
274
    {
275 3
        return ftp_delete($this->getConnection(), $path);
276
    }
277
278
    /**
279
     * @inheritdoc
280
     */
281 3
    public function deleteDir($dirname)
282
    {
283 3
        $connection = $this->getConnection();
284 3
        $contents = array_reverse($this->listDirectoryContents($dirname));
285
286 3
        foreach ($contents as $object) {
287 3
            if ($object['type'] === 'file') {
288 3
                if ( ! ftp_delete($connection, $object['path'])) {
289 3
                    return false;
290
                }
291 3
            } elseif ( ! ftp_rmdir($connection, $object['path'])) {
292 3
                return false;
293
            }
294 3
        }
295
296 3
        return ftp_rmdir($connection, $dirname);
297
    }
298
299
    /**
300
     * @inheritdoc
301
     */
302 6
    public function createDir($dirname, Config $config)
303
    {
304 6
        $connection = $this->getConnection();
305 6
        $directories = explode('/', $dirname);
306
307 6
        foreach ($directories as $directory) {
308 6
            if (false === $this->createActualDirectory($directory, $connection)) {
309 3
                $this->setConnectionRoot();
310
311 3
                return false;
312
            }
313
314 6
            ftp_chdir($connection, $directory);
315 6
        }
316
317 3
        $this->setConnectionRoot();
318
319 3
        return ['type' => 'dir', 'path' => $dirname];
320
    }
321
322
    /**
323
     * Create a directory.
324
     *
325
     * @param string   $directory
326
     * @param resource $connection
327
     *
328
     * @return bool
329
     */
330 6
    protected function createActualDirectory($directory, $connection)
331
    {
332
        // List the current directory
333 6
        $listing = ftp_nlist($connection, '.') ?: [];
334
335 6
        foreach ($listing as $key => $item) {
336 6
            if (preg_match('~^\./.*~', $item)) {
337 6
                $listing[$key] = substr($item, 2);
338 6
            }
339 6
        }
340
341 6
        if (in_array($directory, $listing, true)) {
342 3
            return true;
343
        }
344
345 6
        return (boolean) ftp_mkdir($connection, $directory);
346
    }
347
348
    /**
349
     * @inheritdoc
350
     */
351 30
    public function getMetadata($path)
352
    {
353 30
        $connection = $this->getConnection();
354
355 30
        if ($path === '') {
356 3
            return ['type' => 'dir', 'path' => ''];
357
        }
358
359 27
        if (@ftp_chdir($connection, $path) === true) {
360 3
            $this->setConnectionRoot();
361
362 3
            return ['type' => 'dir', 'path' => $path];
363
        }
364
365 27
        $listing = $this->ftpRawlist('-A', str_replace('*', '\\*', $path));
366
367 27
        if (empty($listing)) {
368 3
            return false;
369
        }
370
371 24
        if (preg_match('/.* not found/', $listing[0])) {
372 6
            return false;
373
        }
374
375 18
        if (preg_match('/^total [0-9]*$/', $listing[0])) {
376 3
            array_shift($listing);
377 3
        }
378
379 18
        return $this->normalizeObject($listing[0], '');
380
    }
381
382
    /**
383
     * @inheritdoc
384
     */
385 9
    public function getMimetype($path)
386
    {
387 9
        if ( ! $metadata = $this->getMetadata($path)) {
388 6
            return false;
389
        }
390
391 6
        $metadata['mimetype'] = MimeType::detectByFilename($path);
392
393 6
        return $metadata;
394
    }
395
396
    /**
397
     * @inheritdoc
398
     */
399 12
    public function getTimestamp($path)
400
    {
401 12
        $timestamp = ftp_mdtm($this->getConnection(), $path);
402
403 12
        return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false;
404
    }
405
406
    /**
407
     * @inheritdoc
408
     */
409 6
    public function read($path)
410
    {
411 6
        if ( ! $object = $this->readStream($path)) {
412 3
            return false;
413
        }
414
415 3
        $object['contents'] = stream_get_contents($object['stream']);
416 3
        fclose($object['stream']);
417 3
        unset($object['stream']);
418
419 3
        return $object;
420
    }
421
422
    /**
423
     * @inheritdoc
424
     */
425 6
    public function readStream($path)
426
    {
427 6
        $stream = fopen('php://temp', 'w+b');
428 6
        $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode);
429 6
        rewind($stream);
430
431 6
        if ( ! $result) {
432 3
            fclose($stream);
433
434 3
            return false;
435
        }
436
437 3
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
438
    }
439
440
    /**
441
     * @inheritdoc
442
     */
443 9
    public function setVisibility($path, $visibility)
444
    {
445 9
        $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate();
446
447 9
        if ( ! ftp_chmod($this->getConnection(), $mode, $path)) {
448 6
            return false;
449
        }
450
451 6
        return compact('path', 'visibility');
452
    }
453
454
    /**
455
     * @inheritdoc
456
     *
457
     * @param string $directory
458
     */
459 21
    protected function listDirectoryContents($directory, $recursive = true)
460
    {
461 21
        $directory = str_replace('*', '\\*', $directory);
462
463 21
        if ($recursive && $this->recurseManually) {
464 3
            return $this->listDirectoryContentsRecursive($directory);
465
        }
466
467 18
        $options = $recursive ? '-alnR' : '-aln';
468 18
        $listing = $this->ftpRawlist($options, $directory);
469
470 18
        return $listing ? $this->normalizeListing($listing, $directory) : [];
471
    }
472
473
    /**
474
     * @inheritdoc
475
     *
476
     * @param string $directory
477
     */
478 3
    protected function listDirectoryContentsRecursive($directory)
479
    {
480 3
        $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: []);
481 3
        $output = [];
482
483 3
        foreach ($listing as $directory) {
484 3
            $output[] = $directory;
485 3
            if ($directory['type'] !== 'dir') continue;
486
487 3
            $output = array_merge($output, $this->listDirectoryContentsRecursive($directory['path']));
488 3
        }
489
490 3
        return $output;
491
    }
492
493
    /**
494
     * Check if the connection is open.
495
     *
496
     * @return bool
497
     * @throws ErrorException
498
     */
499 96
    public function isConnected()
500
    {
501
        try {
502 96
            return is_resource($this->connection) && ftp_rawlist($this->connection, '/') !== false;
503 6
        } catch (ErrorException $e) {
504 6
            is_resource($this->connection) && fclose($this->connection);
505 6
            $this->connection = null;
506
507 6
            if (strpos($e->getMessage(), 'ftp_rawlist') === false) {
508 3
                throw $e;
509
            }
510
511 3
            return false;
512
        }
513
    }
514
515
    /**
516
     * @return null|string
517
     */
518 75
    protected function isPureFtpdServer()
519
    {
520 75
        $connection = $this->getConnection();
521 72
        $response = ftp_raw($connection, 'HELP');
522
523 72
        return stripos(implode(' ', $response), 'Pure-FTPd') !== false;
524
    }
525
526
    /**
527
     * The ftp_rawlist function with optional escaping.
528
     *
529
     * @param string $options
530
     * @param string $path
531
     *
532
     * @return array
533
     */
534 45
    protected function ftpRawlist($options, $path)
535
    {
536 45
        if ($this->isPureFtpd) {
537
            $path = str_replace(' ', '\ ', $path);
538
        }
539 45
        return ftp_rawlist($this->getConnection(), $options . ' ' . $path);
540
    }
541
}
542