Completed
Push — master ( d64c7f...d0ede0 )
by Nicolas
11s
created

src/Gaufrette/Adapter/Ftp.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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