Completed
Pull Request — master (#607)
by Romain
01:50
created

Ftp::isConnected()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Gaufrette\File;
7
use Gaufrette\Filesystem;
8
9
/**
10
 * Ftp adapter.
11
 *
12
 * @author  Antoine Hérault <[email protected]>
13
 */
14
class Ftp implements Adapter, FileFactory, ListKeysAware, SizeCalculator
15
{
16
    protected $connection = null;
17
    protected $directory;
18
    protected $host;
19
    protected $port;
20
    protected $username;
21
    protected $password;
22
    protected $passive;
23
    protected $create;
24
    protected $mode;
25
    protected $ssl;
26
    protected $timeout;
27
    protected $fileData = [];
28
    protected $utf8;
29
30
    /**
31
     * @param string $directory The directory to use in the ftp server
32
     * @param string $host      The host of the ftp server
33
     * @param array  $options   The options like port, username, password, passive, create, mode
34
     */
35
    public function __construct($directory, $host, $options = [])
36
    {
37
        if (!extension_loaded('ftp')) {
38
            throw new \RuntimeException('Unable to use Gaufrette\Adapter\Ftp as the FTP extension is not available.');
39
        }
40
41
        $this->directory = (string) $directory;
42
        $this->host = $host;
43
        $this->port = $options['port'] ?? 21;
44
        $this->username = $options['username'] ?? null;
45
        $this->password = $options['password'] ?? null;
46
        $this->passive = $options['passive'] ?? false;
47
        $this->create = $options['create'] ?? false;
48
        $this->mode = $options['mode'] ?? FTP_BINARY;
49
        $this->ssl = $options['ssl'] ?? false;
50
        $this->timeout = $options['timeout'] ?? 90;
51
        $this->utf8 = $options['utf8'] ?? false;
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function read($key)
58
    {
59
        $this->ensureDirectoryExists($this->directory, $this->create);
60
61
        $temp = fopen('php://temp', 'r+');
62
63
        if (!ftp_fget($this->getConnection(), $temp, $this->computePath($key), $this->mode)) {
64
            return false;
65
        }
66
67
        rewind($temp);
68
        $contents = stream_get_contents($temp);
69
        fclose($temp);
70
71
        return $contents;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function write($key, $content)
78
    {
79
        $this->ensureDirectoryExists($this->directory, $this->create);
80
81
        $path = $this->computePath($key);
82
        $directory = \Gaufrette\Util\Path::dirname($path);
83
84
        $this->ensureDirectoryExists($directory, true);
85
86
        $temp = fopen('php://temp', 'r+');
87
        $size = fwrite($temp, $content);
88
        rewind($temp);
89
90
        if (!ftp_fput($this->getConnection(), $path, $temp, $this->mode)) {
91
            fclose($temp);
92
93
            return false;
94
        }
95
96
        fclose($temp);
97
98
        return $size;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function rename($sourceKey, $targetKey)
105
    {
106
        $this->ensureDirectoryExists($this->directory, $this->create);
107
108
        $sourcePath = $this->computePath($sourceKey);
109
        $targetPath = $this->computePath($targetKey);
110
111
        $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true);
112
113
        return ftp_rename($this->getConnection(), $sourcePath, $targetPath);
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function exists($key)
120
    {
121
        $this->ensureDirectoryExists($this->directory, $this->create);
122
123
        $file = $this->computePath($key);
124
        $lines = ftp_rawlist($this->getConnection(), '-al ' . \Gaufrette\Util\Path::dirname($file));
125
126
        if (false === $lines) {
127
            return false;
128
        }
129
130
        $pattern = '{(?<!->) ' . preg_quote(basename($file)) . '( -> |$)}m';
131
        foreach ($lines as $line) {
132
            if (preg_match($pattern, $line)) {
133
                return true;
134
            }
135
        }
136
137
        return false;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function keys()
144
    {
145
        $this->ensureDirectoryExists($this->directory, $this->create);
146
147
        $keys = $this->fetchKeys();
148
149
        return $keys['keys'];
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function listKeys($prefix = '')
156
    {
157
        $this->ensureDirectoryExists($this->directory, $this->create);
158
159
        preg_match('/(.*?)[^\/]*$/', $prefix, $match);
160
        $directory = rtrim($match[1], '/');
161
162
        $keys = $this->fetchKeys($directory, false);
163
164
        if ($directory === $prefix) {
165
            return $keys;
166
        }
167
168
        $filteredKeys = [];
169 View Code Duplication
        foreach (['keys', 'dirs'] as $hash) {
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...
170
            $filteredKeys[$hash] = [];
171
            foreach ($keys[$hash] as $key) {
172
                if (0 === strpos($key, $prefix)) {
173
                    $filteredKeys[$hash][] = $key;
174
                }
175
            }
176
        }
177
178
        return $filteredKeys;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function mtime($key)
185
    {
186
        $this->ensureDirectoryExists($this->directory, $this->create);
187
188
        $mtime = ftp_mdtm($this->getConnection(), $this->computePath($key));
189
190
        // the server does not support this function
191
        if (-1 === $mtime) {
192
            throw new \RuntimeException('Server does not support ftp_mdtm function.');
193
        }
194
195
        return $mtime;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function delete($key)
202
    {
203
        $this->ensureDirectoryExists($this->directory, $this->create);
204
205
        if ($this->isDirectory($key)) {
206
            return ftp_rmdir($this->getConnection(), $this->computePath($key));
207
        }
208
209
        return ftp_delete($this->getConnection(), $this->computePath($key));
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function isDirectory($key)
216
    {
217
        $this->ensureDirectoryExists($this->directory, $this->create);
218
219
        return $this->isDir($this->computePath($key));
220
    }
221
222
    /**
223
     * Lists files from the specified directory. If a pattern is
224
     * specified, it only returns files matching it.
225
     *
226
     * @param string $directory The path of the directory to list from
227
     *
228
     * @return array An array of keys and dirs
229
     */
230
    public function listDirectory($directory = '')
231
    {
232
        $this->ensureDirectoryExists($this->directory, $this->create);
233
234
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
235
236
        $items = $this->parseRawlist(
237
            ftp_rawlist($this->getConnection(), '-al ' . $this->directory . $directory) ?: []
238
        );
239
240
        $fileData = $dirs = [];
241
        foreach ($items as $itemData) {
242
            if ('..' === $itemData['name'] || '.' === $itemData['name']) {
243
                continue;
244
            }
245
246
            $item = [
247
                'name' => $itemData['name'],
248
                'path' => trim(($directory ? $directory . '/' : '') . $itemData['name'], '/'),
249
                'time' => $itemData['time'],
250
                'size' => $itemData['size'],
251
            ];
252
253
            if ('-' === substr($itemData['perms'], 0, 1)) {
254
                $fileData[$item['path']] = $item;
255
            } elseif ('d' === substr($itemData['perms'], 0, 1)) {
256
                $dirs[] = $item['path'];
257
            }
258
        }
259
260
        $this->fileData = array_merge($fileData, $this->fileData);
261
262
        return [
263
           'keys' => array_keys($fileData),
264
           'dirs' => $dirs,
265
        ];
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function createFile($key, Filesystem $filesystem)
272
    {
273
        $this->ensureDirectoryExists($this->directory, $this->create);
274
275
        $file = new File($key, $filesystem);
276
277
        if (!array_key_exists($key, $this->fileData)) {
278
            $dirname = \Gaufrette\Util\Path::dirname($key);
279
            $directory = $dirname == '.' ? '' : $dirname;
280
            $this->listDirectory($directory);
281
        }
282
283
        if (isset($this->fileData[$key])) {
284
            $fileData = $this->fileData[$key];
285
286
            $file->setName($fileData['name']);
287
            $file->setSize($fileData['size']);
288
        }
289
290
        return $file;
291
    }
292
293
    /**
294
     * @param string $key
295
     *
296
     * @return int
297
     *
298
     * @throws \RuntimeException
299
     */
300
    public function size($key)
301
    {
302
        $this->ensureDirectoryExists($this->directory, $this->create);
303
304
        if (-1 === $size = ftp_size($this->connection, $key)) {
305
            throw new \RuntimeException(sprintf('Unable to fetch the size of "%s".', $key));
306
        }
307
308
        return $size;
309
    }
310
311
    /**
312
     * Ensures the specified directory exists. If it does not, and the create
313
     * parameter is set to TRUE, it tries to create it.
314
     *
315
     * @param string $directory
316
     * @param bool   $create    Whether to create the directory if it does not
317
     *                          exist
318
     *
319
     * @throws RuntimeException if the directory does not exist and could not
320
     *                          be created
321
     */
322
    protected function ensureDirectoryExists($directory, $create = false)
323
    {
324
        if (!$this->isDir($directory)) {
325
            if (!$create) {
326
                throw new \RuntimeException(sprintf('The directory \'%s\' does not exist.', $directory));
327
            }
328
329
            $this->createDirectory($directory);
330
        }
331
    }
332
333
    /**
334
     * Creates the specified directory and its parent directories.
335
     *
336
     * @param string $directory Directory to create
337
     *
338
     * @throws RuntimeException if the directory could not be created
339
     */
340
    protected function createDirectory($directory)
341
    {
342
        // create parent directory if needed
343
        $parent = \Gaufrette\Util\Path::dirname($directory);
344
        if (!$this->isDir($parent)) {
345
            $this->createDirectory($parent);
346
        }
347
348
        // create the specified directory
349
        $created = ftp_mkdir($this->getConnection(), $directory);
350
        if (false === $created) {
351
            throw new \RuntimeException(sprintf('Could not create the \'%s\' directory.', $directory));
352
        }
353
    }
354
355
    /**
356
     * @return string
357
     */
358
    private function createConnectionUrl()
359
    {
360
        $url = $this->ssl ? 'sftp://' : 'ftp://';
361
        $url .= $this->username . ':' . $this->password . '@' . $this->host;
362
        $url .= $this->port ? ':' . $this->port : '';
363
364
        return $url;
365
    }
366
367
    /**
368
     * @param string $directory - full directory path
369
     *
370
     * @return bool
371
     */
372
    private function isDir($directory)
373
    {
374
        if ('/' === $directory) {
375
            return true;
376
        }
377
378
        try {
379
            $chDirResult = ftp_chdir($this->getConnection(), $this->directory);
380
381
            // change directory again to return in the base directory
382
            ftp_chdir($this->getConnection(), $this->directory);
383
            return $chDirResult;
384
        } catch (\Exception $e) {
385
            // is_dir is only available in passive mode.
386
            // See https://php.net/manual/en/wrappers.ftp.php for more details.
387
            if (!$this->passive) {
388
                throw new \RuntimeException(
389
                    \sprintf('Not able to determine whether "%s" is a directory or not. Please try again using a passive FTP connection if your backend supports it, by setting the "passive" option of this adapter to true.', $directory),
390
                    $e->getCode(),
391
                    $e
392
                );
393
            }
394
395
            // Build the FTP URL that will be used to check if the path is a directory or not
396
            $url = $this->createConnectionUrl();
397
            return @is_dir($url . $directory);
398
        }
399
    }
400
401
    private function fetchKeys($directory = '', $onlyKeys = true)
402
    {
403
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
404
405
        $lines = ftp_rawlist($this->getConnection(), '-alR ' . $this->directory . $directory);
406
407
        if (false === $lines) {
408
            return ['keys' => [], 'dirs' => []];
409
        }
410
411
        $regexDir = '/' . preg_quote($this->directory . $directory, '/') . '\/?(.+):$/u';
412
        $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/';
413
414
        $prevLine = null;
415
        $directories = [];
416
        $keys = ['keys' => [], 'dirs' => []];
417
418
        foreach ((array) $lines as $line) {
419
            if ('' === $prevLine && preg_match($regexDir, $line, $match)) {
420
                $directory = $match[1];
421
                unset($directories[$directory]);
422 View Code Duplication
                if ($onlyKeys) {
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...
423
                    $keys = [
424
                        'keys' => array_merge($keys['keys'], $keys['dirs']),
425
                        'dirs' => [],
426
                    ];
427
                }
428
            } elseif (preg_match($regexItem, $line, $tokens)) {
429
                $name = $tokens[3];
430
431
                if ('.' === $name || '..' === $name) {
432
                    continue;
433
                }
434
435
                $path = ltrim($directory . '/' . $name, '/');
436
437
                if ('d' === $tokens[1] || '<dir>' === $tokens[2]) {
438
                    $keys['dirs'][] = $path;
439
                    $directories[$path] = true;
440
                } else {
441
                    $keys['keys'][] = $path;
442
                }
443
            }
444
            $prevLine = $line;
445
        }
446
447 View Code Duplication
        if ($onlyKeys) {
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...
448
            $keys = [
449
                'keys' => array_merge($keys['keys'], $keys['dirs']),
450
                'dirs' => [],
451
            ];
452
        }
453
454
        foreach (array_keys($directories) as $directory) {
455
            $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys));
456
        }
457
458
        return $keys;
459
    }
460
461
    /**
462
     * Parses the given raw list.
463
     *
464
     * @param array $rawlist
465
     *
466
     * @return array
467
     */
468
    private function parseRawlist(array $rawlist)
469
    {
470
        $parsed = [];
471
        foreach ($rawlist as $line) {
472
            $infos = preg_split("/[\s]+/", $line, 9);
473
474
            if ($this->isLinuxListing($infos)) {
475
                $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7] . ' 00:00') : (date('Y') . ' ' . $infos[7]);
476
                if ('total' !== $infos[0]) {
477
                    $parsed[] = [
478
                        'perms' => $infos[0],
479
                        'num' => $infos[1],
480
                        'size' => $infos[4],
481
                        'time' => strtotime($infos[5] . ' ' . $infos[6] . '. ' . $infos[7]),
482
                        'name' => $infos[8],
483
                    ];
484
                }
485
            } elseif (count($infos) >= 4) {
486
                $isDir = (boolean) ('<dir>' === $infos[2]);
487
                $parsed[] = [
488
                    'perms' => $isDir ? 'd' : '-',
489
                    'num' => '',
490
                    'size' => $isDir ? '' : $infos[2],
491
                    'time' => strtotime($infos[0] . ' ' . $infos[1]),
492
                    'name' => $infos[3],
493
                ];
494
            }
495
        }
496
497
        return $parsed;
498
    }
499
500
    /**
501
     * Computes the path for the given key.
502
     *
503
     * @param string $key
504
     */
505
    private function computePath($key)
506
    {
507
        return rtrim($this->directory, '/') . '/' . $key;
508
    }
509
510
    /**
511
     * Indicates whether the adapter has an open ftp connection.
512
     *
513
     * @return bool
514
     */
515
    private function isConnected()
516
    {
517
        return is_resource($this->connection);
518
    }
519
520
    /**
521
     * Returns an opened ftp connection resource. If the connection is not
522
     * already opened, it open it before.
523
     *
524
     * @return resource The ftp connection
525
     */
526
    private function getConnection()
527
    {
528
        if (!$this->isConnected()) {
529
            $this->connect();
530
        }
531
532
        return $this->connection;
533
    }
534
535
    /**
536
     * Opens the adapter's ftp connection.
537
     *
538
     * @throws RuntimeException if could not connect
539
     */
540
    private function connect()
541
    {
542
        if ($this->ssl && !function_exists('ftp_ssl_connect')) {
543
            throw new \RuntimeException('This Server Has No SSL-FTP Available.');
544
        }
545
546
        // open ftp connection
547
        if (!$this->ssl) {
548
            $this->connection = ftp_connect($this->host, $this->port, $this->timeout);
549
        } else {
550
            $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout);
551
        }
552
553
        if (!$this->connection) {
554
            throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port));
555
        }
556
557
        if (defined('FTP_USEPASVADDRESS')) {
558
            ftp_set_option($this->connection, FTP_USEPASVADDRESS, false);
559
        }
560
561
        $username = $this->username ?: 'anonymous';
562
        $password = $this->password ?: '';
563
564
        // login ftp user
565
        if (!@ftp_login($this->connection, $username, $password)) {
566
            $this->close();
567
568
            throw new \RuntimeException(sprintf('Could not login as %s.', $username));
569
        }
570
571
        // switch to passive mode if needed
572
        if ($this->passive && !ftp_pasv($this->connection, true)) {
573
            $this->close();
574
575
            throw new \RuntimeException('Could not turn passive mode on.');
576
        }
577
578
        // enable utf8 mode if configured
579
        if ($this->utf8 == true) {
580
            ftp_raw($this->connection, 'OPTS UTF8 ON');
581
        }
582
583
        // ensure the adapter's directory exists
584
        if ('/' !== $this->directory) {
585
            try {
586
                $this->ensureDirectoryExists($this->directory, $this->create);
587
            } catch (\RuntimeException $e) {
588
                $this->close();
589
590
                throw $e;
591
            }
592
593
            // change the current directory for the adapter's directory
594
            if (!ftp_chdir($this->connection, $this->directory)) {
595
                $this->close();
596
597
                throw new \RuntimeException(sprintf('Could not change current directory for the \'%s\' directory.', $this->directory));
598
            }
599
        }
600
    }
601
602
    /**
603
     * Closes the adapter's ftp connection.
604
     */
605
    public function close()
606
    {
607
        if ($this->isConnected()) {
608
            ftp_close($this->connection);
609
        }
610
    }
611
612
    private function isLinuxListing($info)
613
    {
614
        return count($info) >= 9;
615
    }
616
}
617