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