Completed
Pull Request — master (#607)
by Romain
02:40
created

Ftp::createConnectionUrl()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
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
        $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
        }
384
        catch(Exception $e) {
0 ignored issues
show
Bug introduced by
The class Gaufrette\Adapter\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
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 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 = 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 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...
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