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