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

Ftp::createConnectionUrl()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 3
nc 4
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,
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
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...
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
    {
363
        $url = $this->ssl ? 'sftp://' : 'ftp://';
364
        $url .= $this->username . ':' . $this->password . '@' . $this->host;
365
        $url .= $this->port ? ':' . $this->port : '';
366
367
        return $url;
368
    }
369
370
    /**
371
     * @param string $directory - full directory path
372
     *
373
     * @return bool
374
     */
375
    private function isDir($directory)
376
    {
377
        if ('/' === $directory) {
378
            return true;
379
        }
380
381
        try {
382
            $chDirResult = false;
383
            $chDirResult = ftp_chdir($this->getConnection(), $this->directory);
384
385
            // change directory again to return in the base directory
386
            ftp_chdir($this->getConnection(), $this->directory);
387
            return $chDirResult;
388
        } catch (\Exception $e) {
389
            // is_dir is only available in passive mode.
390
            // See https://php.net/manual/en/wrappers.ftp.php for more details.
391
            if (!$this->passive) {
392
                throw new \RuntimeException(
393
                    \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),
394
                    $e->getCode(),
395
                    $e
396
                );
397
            }
398
399
            // Build the FTP URL that will be used to check if the path is a directory or not
400
            $url = $this->createConnectionUrl();
401
            return @is_dir($url . $directory);
402
        }
403
404
        return true;
0 ignored issues
show
Unused Code introduced by
return true; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

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