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

Ftp::listKeys()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 25

Duplication

Lines 8
Ratio 32 %

Importance

Changes 0
Metric Value
dl 8
loc 25
rs 9.2088
c 0
b 0
f 0
cc 5
nc 5
nop 1
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,
15
                     FileFactory,
16
                     ListKeysAware,
17
                     SizeCalculator
18
{
19
    protected $connection = null;
20
    protected $directory;
21
    protected $host;
22
    protected $port;
23
    protected $username;
24
    protected $password;
25
    protected $passive;
26
    protected $create;
27
    protected $mode;
28
    protected $ssl;
29
    protected $timeout;
30
    protected $fileData = array();
31
    protected $utf8;
32
33
    /**
34
     * @param string $directory The directory to use in the ftp server
35
     * @param string $host      The host of the ftp server
36
     * @param array  $options   The options like port, username, password, passive, create, mode
37
     */
38
    public function __construct($directory, $host, $options = array())
39
    {
40
        if (!extension_loaded('ftp')) {
41
            throw new \RuntimeException('Unable to use Gaufrette\Adapter\Ftp as the FTP extension is not available.');
42
        }
43
44
        $this->directory = (string) $directory;
45
        $this->host = $host;
46
        $this->port = isset($options['port']) ? $options['port'] : 21;
47
        $this->username = isset($options['username']) ? $options['username'] : null;
48
        $this->password = isset($options['password']) ? $options['password'] : null;
49
        $this->passive = isset($options['passive']) ? $options['passive'] : false;
50
        $this->create = isset($options['create']) ? $options['create'] : false;
51
        $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY;
52
        $this->ssl = isset($options['ssl']) ? $options['ssl'] : false;
53
        $this->timeout = isset($options['timeout']) ? $options['timeout'] : 90;
54
        $this->utf8 = isset($options['utf8']) ? $options['utf8'] : false;
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function read($key)
61
    {
62
        $this->ensureDirectoryExists($this->directory, $this->create);
63
64
        $temp = fopen('php://temp', 'r+');
65
66
        if (!ftp_fget($this->getConnection(), $temp, $this->computePath($key), $this->mode)) {
67
            return false;
68
        }
69
70
        rewind($temp);
71
        $contents = stream_get_contents($temp);
72
        fclose($temp);
73
74
        return $contents;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function write($key, $content)
81
    {
82
        $this->ensureDirectoryExists($this->directory, $this->create);
83
84
        $path = $this->computePath($key);
85
        $directory = \Gaufrette\Util\Path::dirname($path);
86
87
        $this->ensureDirectoryExists($directory, true);
88
89
        $temp = fopen('php://temp', 'r+');
90
        $size = fwrite($temp, $content);
91
        rewind($temp);
92
93
        if (!ftp_fput($this->getConnection(), $path, $temp, $this->mode)) {
94
            fclose($temp);
95
96
            return false;
97
        }
98
99
        fclose($temp);
100
101
        return $size;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function rename($sourceKey, $targetKey)
108
    {
109
        $this->ensureDirectoryExists($this->directory, $this->create);
110
111
        $sourcePath = $this->computePath($sourceKey);
112
        $targetPath = $this->computePath($targetKey);
113
114
        $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true);
115
116
        return ftp_rename($this->getConnection(), $sourcePath, $targetPath);
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function exists($key)
123
    {
124
        $this->ensureDirectoryExists($this->directory, $this->create);
125
126
        $file  = $this->computePath($key);
127
        $lines = ftp_rawlist($this->getConnection(), '-al ' . \Gaufrette\Util\Path::dirname($file));
128
129
        if (false === $lines) {
130
            return false;
131
        }
132
133
        $pattern = '{(?<!->) '.preg_quote(basename($file)).'( -> |$)}m';
134
        foreach ($lines as $line) {
135
            if (preg_match($pattern, $line)) {
136
                return true;
137
            }
138
        }
139
140
        return false;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function keys()
147
    {
148
        $this->ensureDirectoryExists($this->directory, $this->create);
149
150
        $keys = $this->fetchKeys();
151
152
        return $keys['keys'];
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function listKeys($prefix = '')
159
    {
160
        $this->ensureDirectoryExists($this->directory, $this->create);
161
162
        preg_match('/(.*?)[^\/]*$/', $prefix, $match);
163
        $directory = rtrim($match[1], '/');
164
165
        $keys = $this->fetchKeys($directory, false);
166
167
        if ($directory === $prefix) {
168
            return $keys;
169
        }
170
171
        $filteredKeys = array();
172
        foreach (array('keys', 'dirs') as $hash) {
173
            $filteredKeys[$hash] = array();
174
            foreach ($keys[$hash] as $key) {
175
                if (0 === strpos($key, $prefix)) {
176
                    $filteredKeys[$hash][] = $key;
177
                }
178
            }
179
        }
180
181
        return $filteredKeys;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function mtime($key)
188
    {
189
        $this->ensureDirectoryExists($this->directory, $this->create);
190
191
        $mtime = ftp_mdtm($this->getConnection(), $this->computePath($key));
192
193
        // the server does not support this function
194
        if (-1 === $mtime) {
195
            throw new \RuntimeException('Server does not support ftp_mdtm function.');
196
        }
197
198
        return $mtime;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function delete($key)
205
    {
206
        $this->ensureDirectoryExists($this->directory, $this->create);
207
208
        if ($this->isDirectory($key)) {
209
            return ftp_rmdir($this->getConnection(), $this->computePath($key));
210
        }
211
212
        return ftp_delete($this->getConnection(), $this->computePath($key));
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function isDirectory($key)
219
    {
220
        $this->ensureDirectoryExists($this->directory, $this->create);
221
222
        return $this->isDir($this->computePath($key));
223
    }
224
225
    /**
226
     * Lists files from the specified directory. If a pattern is
227
     * specified, it only returns files matching it.
228
     *
229
     * @param string $directory The path of the directory to list from
230
     *
231
     * @return array An array of keys and dirs
232
     */
233
    public function listDirectory($directory = '')
234
    {
235
        $this->ensureDirectoryExists($this->directory, $this->create);
236
237
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
238
239
        $items = $this->parseRawlist(
240
            ftp_rawlist($this->getConnection(), '-al '.$this->directory.$directory) ?: array()
241
        );
242
243
        $fileData = $dirs = array();
244
        foreach ($items as $itemData) {
245
            if ('..' === $itemData['name'] || '.' === $itemData['name']) {
246
                continue;
247
            }
248
249
            $item = array(
250
                'name' => $itemData['name'],
251
                'path' => trim(($directory ? $directory.'/' : '').$itemData['name'], '/'),
252
                'time' => $itemData['time'],
253
                'size' => $itemData['size'],
254
            );
255
256
            if ('-' === substr($itemData['perms'], 0, 1)) {
257
                $fileData[$item['path']] = $item;
258
            } elseif ('d' === substr($itemData['perms'], 0, 1)) {
259
                $dirs[] = $item['path'];
260
            }
261
        }
262
263
        $this->fileData = array_merge($fileData, $this->fileData);
264
265
        return array(
266
           'keys' => array_keys($fileData),
267
           'dirs' => $dirs,
268
        );
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function createFile($key, Filesystem $filesystem)
275
    {
276
        $this->ensureDirectoryExists($this->directory, $this->create);
277
278
        $file = new File($key, $filesystem);
279
280
        if (!array_key_exists($key, $this->fileData)) {
281
            $dirname = \Gaufrette\Util\Path::dirname($key);
282
            $directory = $dirname == '.' ? '' : $dirname;
283
            $this->listDirectory($directory);
284
        }
285
286
        if (isset($this->fileData[$key])) {
287
            $fileData = $this->fileData[$key];
288
289
            $file->setName($fileData['name']);
290
            $file->setSize($fileData['size']);
291
        }
292
293
        return $file;
294
    }
295
296
    /**
297
     * @param string $key
298
     *
299
     * @return int
300
     *
301
     * @throws \RuntimeException
302
     */
303
    public function size($key)
304
    {
305
        $this->ensureDirectoryExists($this->directory, $this->create);
306
307
        if (-1 === $size = ftp_size($this->connection, $key)) {
308
            throw new \RuntimeException(sprintf('Unable to fetch the size of "%s".', $key));
309
        }
310
311
        return $size;
312
    }
313
314
    /**
315
     * Ensures the specified directory exists. If it does not, and the create
316
     * parameter is set to TRUE, it tries to create it.
317
     *
318
     * @param string $directory
319
     * @param bool   $create    Whether to create the directory if it does not
320
     *                          exist
321
     *
322
     * @throws RuntimeException if the directory does not exist and could not
323
     *                          be created
324
     */
325
    protected function ensureDirectoryExists($directory, $create = false)
326
    {
327
        if (!$this->isDir($directory)) {
328
            if (!$create) {
329
                throw new \RuntimeException(sprintf('The directory \'%s\' does not exist.', $directory));
330
            }
331
332
            $this->createDirectory($directory);
333
        }
334
    }
335
336
    /**
337
     * Creates the specified directory and its parent directories.
338
     *
339
     * @param string $directory Directory to create
340
     *
341
     * @throws RuntimeException if the directory could not be created
342
     */
343
    protected function createDirectory($directory)
344
    {
345
        // create parent directory if needed
346
        $parent = \Gaufrette\Util\Path::dirname($directory);
347
        if (!$this->isDir($parent)) {
348
            $this->createDirectory($parent);
349
        }
350
351
        // create the specified directory
352
        $created = ftp_mkdir($this->getConnection(), $directory);
353
        if (false === $created) {
354
            throw new \RuntimeException(sprintf('Could not create the \'%s\' directory.', $directory));
355
        }
356
    }
357
358
    /**
359
     * @return string
360
     */
361
    private function createConnectionUrl() {
362
        $url = $this->ssl ? 'sftp://' : 'ftp://';
363
        $url .= $this->username . ':' . $this->password . '@' . $this->host;
364
        $url .= $this->port ? ':' . $this->port : '';
365
366
        return $url;
367
    }
368
369
    /**
370
     * @param string $directory - full directory path
371
     *
372
     * @return bool
373
     */
374
    private function isDir($directory)
375
    {
376
        if ('/' === $directory) {
377
            return true;
378
        }
379
380
        $chDirResult = false;
381
        try {
382
            $chDirResult = ftp_chdir($this->getConnection(), $directory)
383
        }
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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