Completed
Pull Request — master (#617)
by Nicolas
03:29
created

Ftp::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 2
nc 2
nop 3
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
     * @param string $directory - full directory path
357
     *
358
     * @return bool
359
     */
360
    private function isDir($directory)
361
    {
362
        if ('/' === $directory) {
363
            return true;
364
        }
365
366
        if (!@ftp_chdir($this->getConnection(), $directory)) {
367
            return false;
368
        }
369
370
        // change directory again to return in the base directory
371
        ftp_chdir($this->getConnection(), $this->directory);
372
373
        return true;
374
    }
375
376
    private function fetchKeys($directory = '', $onlyKeys = true)
377
    {
378
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
379
380
        $lines = ftp_rawlist($this->getConnection(), '-alR ' . $this->directory . $directory);
381
382
        if (false === $lines) {
383
            return ['keys' => [], 'dirs' => []];
384
        }
385
386
        $regexDir = '/' . preg_quote($this->directory . $directory, '/') . '\/?(.+):$/u';
387
        $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/';
388
389
        $prevLine = null;
390
        $directories = [];
391
        $keys = ['keys' => [], 'dirs' => []];
392
393
        foreach ((array) $lines as $line) {
394
            if ('' === $prevLine && preg_match($regexDir, $line, $match)) {
395
                $directory = $match[1];
396
                unset($directories[$directory]);
397 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...
398
                    $keys = [
399
                        'keys' => array_merge($keys['keys'], $keys['dirs']),
400
                        'dirs' => [],
401
                    ];
402
                }
403
            } elseif (preg_match($regexItem, $line, $tokens)) {
404
                $name = $tokens[3];
405
406
                if ('.' === $name || '..' === $name) {
407
                    continue;
408
                }
409
410
                $path = ltrim($directory . '/' . $name, '/');
411
412
                if ('d' === $tokens[1] || '<dir>' === $tokens[2]) {
413
                    $keys['dirs'][] = $path;
414
                    $directories[$path] = true;
415
                } else {
416
                    $keys['keys'][] = $path;
417
                }
418
            }
419
            $prevLine = $line;
420
        }
421
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
429
        foreach (array_keys($directories) as $directory) {
430
            $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys));
431
        }
432
433
        return $keys;
434
    }
435
436
    /**
437
     * Parses the given raw list.
438
     *
439
     * @param array $rawlist
440
     *
441
     * @return array
442
     */
443
    private function parseRawlist(array $rawlist)
444
    {
445
        $parsed = [];
446
        foreach ($rawlist as $line) {
447
            $infos = preg_split("/[\s]+/", $line, 9);
448
449
            if ($this->isLinuxListing($infos)) {
450
                $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7] . ' 00:00') : (date('Y') . ' ' . $infos[7]);
451
                if ('total' !== $infos[0]) {
452
                    $parsed[] = [
453
                        'perms' => $infos[0],
454
                        'num' => $infos[1],
455
                        'size' => $infos[4],
456
                        'time' => strtotime($infos[5] . ' ' . $infos[6] . '. ' . $infos[7]),
457
                        'name' => $infos[8],
458
                    ];
459
                }
460
            } elseif (count($infos) >= 4) {
461
                $isDir = (boolean) ('<dir>' === $infos[2]);
462
                $parsed[] = [
463
                    'perms' => $isDir ? 'd' : '-',
464
                    'num' => '',
465
                    'size' => $isDir ? '' : $infos[2],
466
                    'time' => strtotime($infos[0] . ' ' . $infos[1]),
467
                    'name' => $infos[3],
468
                ];
469
            }
470
        }
471
472
        return $parsed;
473
    }
474
475
    /**
476
     * Computes the path for the given key.
477
     *
478
     * @param string $key
479
     */
480
    private function computePath($key)
481
    {
482
        return rtrim($this->directory, '/') . '/' . $key;
483
    }
484
485
    /**
486
     * Indicates whether the adapter has an open ftp connection.
487
     *
488
     * @return bool
489
     */
490
    private function isConnected()
491
    {
492
        return is_resource($this->connection);
493
    }
494
495
    /**
496
     * Returns an opened ftp connection resource. If the connection is not
497
     * already opened, it open it before.
498
     *
499
     * @return resource The ftp connection
500
     */
501
    private function getConnection()
502
    {
503
        if (!$this->isConnected()) {
504
            $this->connect();
505
        }
506
507
        return $this->connection;
508
    }
509
510
    /**
511
     * Opens the adapter's ftp connection.
512
     *
513
     * @throws RuntimeException if could not connect
514
     */
515
    private function connect()
516
    {
517
        if ($this->ssl && !function_exists('ftp_ssl_connect')) {
518
            throw new \RuntimeException('This Server Has No SSL-FTP Available.');
519
        }
520
521
        // open ftp connection
522
        if (!$this->ssl) {
523
            $this->connection = ftp_connect($this->host, $this->port, $this->timeout);
524
        } else {
525
            $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout);
526
        }
527
528
        if (!$this->connection) {
529
            throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port));
530
        }
531
532
        if (defined('FTP_USEPASVADDRESS')) {
533
            ftp_set_option($this->connection, FTP_USEPASVADDRESS, false);
534
        }
535
536
        $username = $this->username ?: 'anonymous';
537
        $password = $this->password ?: '';
538
539
        // login ftp user
540
        if (!@ftp_login($this->connection, $username, $password)) {
541
            $this->close();
542
543
            throw new \RuntimeException(sprintf('Could not login as %s.', $username));
544
        }
545
546
        // switch to passive mode if needed
547
        if ($this->passive && !ftp_pasv($this->connection, true)) {
548
            $this->close();
549
550
            throw new \RuntimeException('Could not turn passive mode on.');
551
        }
552
553
        // enable utf8 mode if configured
554
        if ($this->utf8 == true) {
555
            ftp_raw($this->connection, 'OPTS UTF8 ON');
556
        }
557
558
        // ensure the adapter's directory exists
559
        if ('/' !== $this->directory) {
560
            try {
561
                $this->ensureDirectoryExists($this->directory, $this->create);
562
            } catch (\RuntimeException $e) {
563
                $this->close();
564
565
                throw $e;
566
            }
567
568
            // change the current directory for the adapter's directory
569
            if (!ftp_chdir($this->connection, $this->directory)) {
570
                $this->close();
571
572
                throw new \RuntimeException(sprintf('Could not change current directory for the \'%s\' directory.', $this->directory));
573
            }
574
        }
575
    }
576
577
    /**
578
     * Closes the adapter's ftp connection.
579
     */
580
    public function close()
581
    {
582
        if ($this->isConnected()) {
583
            ftp_close($this->connection);
584
        }
585
    }
586
587
    private function isLinuxListing($info)
588
    {
589
        return count($info) >= 9;
590
    }
591
}
592