Completed
Pull Request — master (#414)
by Albin
02:59
created

Ftp::__construct()   C

Complexity

Conditions 10
Paths 257

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 1
Metric Value
c 3
b 1
f 1
dl 0
loc 17
rs 6
cc 10
eloc 13
nc 257
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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,
0 ignored issues
show
Coding Style introduced by
The first item in a multi-line implements list must be on the line following the implements keyword
Loading history...
15
                     FileFactory,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 21 found
Loading history...
16
                     ListKeysAware
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 21 found
Loading history...
17
{
18
    protected $connection = null;
19
    protected $directory;
20
    protected $host;
21
    protected $port;
22
    protected $username;
23
    protected $password;
24
    protected $passive;
25
    protected $create;
26
    protected $mode;
27
    protected $ssl;
28
    protected $fileData = array();
29
    protected $utf8;
30
31
    /**
32
     * @param string $directory The directory to use in the ftp server
33
     * @param string $host      The host of the ftp server
34
     * @param array  $options   The options like port, username, password, passive, create, mode
35
     */
36
    public function __construct($directory, $host, $options = array())
37
    {
38
        if (!extension_loaded('ftp')) {
39
            throw new \RuntimeException('Unable to use Gaufrette\Adapter\Ftp as the FTP extension is not available.');
40
        }
41
42
        $this->directory = (string) $directory;
43
        $this->host = $host;
44
        $this->port = isset($options['port']) ? $options['port'] : 21;
45
        $this->username = isset($options['username']) ? $options['username'] : null;
46
        $this->password = isset($options['password']) ? $options['password'] : null;
47
        $this->passive = isset($options['passive']) ? $options['passive'] : false;
48
        $this->create = isset($options['create']) ? $options['create'] : false;
49
        $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY;
50
        $this->ssl = isset($options['ssl']) ? $options['ssl'] : false;
51
        $this->utf8 = isset($options['utf8']) ? $options['utf8'] : false;
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function read($key)
58
    {
59
        $this->ensureDirectoryExists($this->directory, $this->create);
60
61
        $temp = fopen('php://temp', 'r+');
62
63
        if (!ftp_fget($this->getConnection(), $temp, $this->computePath($key), $this->mode)) {
64
            return false;
65
        }
66
67
        rewind($temp);
68
        $contents = stream_get_contents($temp);
69
        fclose($temp);
70
71
        return $contents;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function write($key, $content)
78
    {
79
        $this->ensureDirectoryExists($this->directory, $this->create);
80
81
        $path = $this->computePath($key);
82
        $directory = str_replace('\\', '/', \Gaufrette\Util\Path::dirname($path));
83
84
        $this->ensureDirectoryExists($directory, true);
85
86
        $temp = fopen('php://temp', 'r+');
87
        $size = fwrite($temp, $content);
88
        rewind($temp);
89
90
        if (!ftp_fput($this->getConnection(), $path, $temp, $this->mode)) {
91
            fclose($temp);
92
93
            return false;
94
        }
95
96
        fclose($temp);
97
98
        return $size;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function rename($sourceKey, $targetKey)
105
    {
106
        $this->ensureDirectoryExists($this->directory, $this->create);
107
108
        $sourcePath = $this->computePath($sourceKey);
109
        $targetPath = $this->computePath($targetKey);
110
111
        $this->ensureDirectoryExists(str_replace('\\', '/', \Gaufrette\Util\Path::dirname($targetPath)), true);
112
113
        return ftp_rename($this->getConnection(), $sourcePath, $targetPath);
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119
    public function exists($key)
120
    {
121
        $this->ensureDirectoryExists($this->directory, $this->create);
122
123
        $file  = $this->computePath($key);
124
        $lines = ftp_rawlist($this->getConnection(), '-al ' . str_replace('\\', '/', \Gaufrette\Util\Path::dirname($file)));
125
126
        if (false === $lines) {
127
            return false;
128
        }
129
130
        $pattern = '{(?<!->) '.preg_quote(basename($file)).'( -> |$)}m';
131
        foreach ($lines as $line) {
132
            if (preg_match($pattern, $line)) {
133
                return true;
134
            }
135
        }
136
137
        return false;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function keys()
144
    {
145
        $this->ensureDirectoryExists($this->directory, $this->create);
146
147
        $keys = $this->fetchKeys();
148
149
        return $keys['keys'];
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function listKeys($prefix = '')
156
    {
157
        $this->ensureDirectoryExists($this->directory, $this->create);
158
159
        preg_match('/(.*?)[^\/]*$/', $prefix, $match);
160
        $directory = rtrim($match[1], '/');
161
162
        $keys = $this->fetchKeys($directory, false);
163
164
        if ($directory === $prefix) {
165
            return $keys;
166
        }
167
168
        $filteredKeys = array();
169 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...
170
            $filteredKeys[$hash] = array();
171
            foreach ($keys[$hash] as $key) {
172
                if (0 === strpos($key, $prefix)) {
173
                    $filteredKeys[$hash][] = $key;
174
                }
175
            }
176
        }
177
178
        return $filteredKeys;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function mtime($key)
185
    {
186
        $this->ensureDirectoryExists($this->directory, $this->create);
187
188
        $mtime = ftp_mdtm($this->getConnection(), $this->computePath($key));
189
190
        // the server does not support this function
191
        if (-1 === $mtime) {
192
            throw new \RuntimeException('Server does not support ftp_mdtm function.');
193
        }
194
195
        return $mtime;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function delete($key)
202
    {
203
        $this->ensureDirectoryExists($this->directory, $this->create);
204
205
        if ($this->isDirectory($key)) {
206
            return ftp_rmdir($this->getConnection(), $this->computePath($key));
207
        }
208
209
        return ftp_delete($this->getConnection(), $this->computePath($key));
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function isDirectory($key)
216
    {
217
        $this->ensureDirectoryExists($this->directory, $this->create);
218
219
        return $this->isDir($this->computePath($key));
220
    }
221
222
    /**
223
     * Lists files from the specified directory. If a pattern is
224
     * specified, it only returns files matching it.
225
     *
226
     * @param string $directory The path of the directory to list from
227
     *
228
     * @return array An array of keys and dirs
229
     */
230
    public function listDirectory($directory = '')
231
    {
232
        $this->ensureDirectoryExists($this->directory, $this->create);
233
234
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
235
236
        $items = $this->parseRawlist(
237
            ftp_rawlist($this->getConnection(), '-al '.$this->directory.$directory) ?: array()
238
        );
239
240
        $fileData = $dirs = array();
241
        foreach ($items as $itemData) {
242
            if ('..' === $itemData['name'] || '.' === $itemData['name']) {
243
                continue;
244
            }
245
246
            $item = array(
247
                'name' => $itemData['name'],
248
                'path' => trim(($directory ? $directory.'/' : '').$itemData['name'], '/'),
249
                'time' => $itemData['time'],
250
                'size' => $itemData['size'],
251
            );
252
253
            if ('-' === substr($itemData['perms'], 0, 1)) {
254
                $fileData[$item['path']] = $item;
255
            } elseif ('d' === substr($itemData['perms'], 0, 1)) {
256
                $dirs[] = $item['path'];
257
            }
258
        }
259
260
        $this->fileData = array_merge($fileData, $this->fileData);
261
262
        return array(
263
           'keys' => array_keys($fileData),
264
           'dirs' => $dirs,
265
        );
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function createFile($key, Filesystem $filesystem)
272
    {
273
        $this->ensureDirectoryExists($this->directory, $this->create);
274
275
        $file = new File($key, $filesystem);
276
277
        if (!array_key_exists($key, $this->fileData)) {
278
            $directory = \Gaufrette\Util\Path::dirname($key) == '.' ? '' : str_replace('\\', '/', \Gaufrette\Util\Path::dirname($key));
279
            $this->listDirectory($directory);
280
        }
281
282
        if (isset($this->fileData[$key])) {
283
            $fileData = $this->fileData[$key];
284
285
            $file->setName($fileData['name']);
286
            $file->setSize($fileData['size']);
287
        }
288
289
        return $file;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $file; (Gaufrette\File) is incompatible with the return type declared by the interface Gaufrette\Adapter\FileFactory::createFile of type Gaufrette\Adapter\File.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
290
    }
291
292
    /**
293
     * Ensures the specified directory exists. If it does not, and the create
294
     * parameter is set to TRUE, it tries to create it.
295
     *
296
     * @param string $directory
297
     * @param bool   $create    Whether to create the directory if it does not
298
     *                          exist
299
     *
300
     * @throws RuntimeException if the directory does not exist and could not
301
     *                          be created
302
     */
303
    protected function ensureDirectoryExists($directory, $create = false)
304
    {
305
        if (!$this->isDir($directory)) {
306
            if (!$create) {
307
                throw new \RuntimeException(sprintf('The directory \'%s\' does not exist.', $directory));
308
            }
309
310
            $this->createDirectory($directory);
311
        }
312
    }
313
314
    /**
315
     * Creates the specified directory and its parent directories.
316
     *
317
     * @param string $directory Directory to create
318
     *
319
     * @throws RuntimeException if the directory could not be created
320
     */
321
    protected function createDirectory($directory)
322
    {
323
        // create parent directory if needed
324
        $parent = str_replace('\\', '/', \Gaufrette\Util\Path::dirname($directory));
325
        if (!$this->isDir($parent)) {
326
            $this->createDirectory($parent);
327
        }
328
329
        // create the specified directory
330
        $created = ftp_mkdir($this->getConnection(), $directory);
331
        if (false === $created) {
332
            throw new \RuntimeException(sprintf('Could not create the \'%s\' directory.', $directory));
333
        }
334
    }
335
336
    /**
337
     * @param string $directory - full directory path
338
     *
339
     * @return bool
340
     */
341
    private function isDir($directory)
342
    {
343
        if ('/' === $directory) {
344
            return true;
345
        }
346
347
        if (!@ftp_chdir($this->getConnection(), $directory)) {
348
            return false;
349
        }
350
351
        // change directory again to return in the base directory
352
        ftp_chdir($this->getConnection(), $this->directory);
353
354
        return true;
355
    }
356
357
    private function fetchKeys($directory = '', $onlyKeys = true)
358
    {
359
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
360
361
        $lines = ftp_rawlist($this->getConnection(), '-alR '.$this->directory.$directory);
362
363
        if (false === $lines) {
364
            return array('keys' => array(), 'dirs' => array());
365
        }
366
367
        $regexDir = '/'.preg_quote($this->directory.$directory, '/').'\/?(.+):$/u';
368
        $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/';
369
370
        $prevLine = null;
371
        $directories = array();
372
        $keys = array('keys' => array(), 'dirs' => array());
373
374
        foreach ((array) $lines as $line) {
375
            if ('' === $prevLine && preg_match($regexDir, $line, $match)) {
376
                $directory = $match[1];
377
                unset($directories[$directory]);
378 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...
379
                    $keys = array(
380
                        'keys' => array_merge($keys['keys'], $keys['dirs']),
381
                        'dirs' => array(),
382
                    );
383
                }
384
            } elseif (preg_match($regexItem, $line, $tokens)) {
385
                $name = $tokens[3];
386
387
                if ('.' === $name || '..' === $name) {
388
                    continue;
389
                }
390
391
                $path = ltrim($directory.'/'.$name, '/');
392
393
                if ('d' === $tokens[1] || '<dir>' === $tokens[2]) {
394
                    $keys['dirs'][] = $path;
395
                    $directories[$path] = true;
396
                } else {
397
                    $keys['keys'][] = $path;
398
                }
399
            }
400
            $prevLine = $line;
401
        }
402
403 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...
404
            $keys = array(
405
                'keys' => array_merge($keys['keys'], $keys['dirs']),
406
                'dirs' => array(),
407
            );
408
        }
409
410
        foreach (array_keys($directories) as $directory) {
411
            $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys));
412
        }
413
414
        return $keys;
415
    }
416
417
    /**
418
     * Parses the given raw list.
419
     *
420
     * @param array $rawlist
421
     *
422
     * @return array
423
     */
424
    private function parseRawlist(array $rawlist)
425
    {
426
        $parsed = array();
427
        foreach ($rawlist as $line) {
428
            $infos = preg_split("/[\s]+/", $line, 9);
429
430
            if ($this->isLinuxListing($infos)) {
431
                $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7].' 00:00') : (date('Y').' '.$infos[7]);
432
                if ('total' !== $infos[0]) {
433
                    $parsed[] = array(
434
                        'perms' => $infos[0],
435
                        'num' => $infos[1],
436
                        'size' => $infos[4],
437
                        'time' => strtotime($infos[5].' '.$infos[6].'. '.$infos[7]),
438
                        'name' => $infos[8],
439
                    );
440
                }
441
            } else {
442
                $isDir = (boolean) ('<dir>' === $infos[2]);
443
                $parsed[] = array(
444
                    'perms' => $isDir ? 'd' : '-',
445
                    'num' => '',
446
                    'size' => $isDir ? '' : $infos[2],
447
                    'time' => strtotime($infos[0].' '.$infos[1]),
448
                    'name' => $infos[3],
449
                );
450
            }
451
        }
452
453
        return $parsed;
454
    }
455
456
    /**
457
     * Computes the path for the given key.
458
     *
459
     * @param string $key
460
     */
461
    private function computePath($key)
462
    {
463
        return rtrim($this->directory, '/').'/'.$key;
464
    }
465
466
    /**
467
     * Indicates whether the adapter has an open ftp connection.
468
     *
469
     * @return bool
470
     */
471
    private function isConnected()
472
    {
473
        return is_resource($this->connection);
474
    }
475
476
    /**
477
     * Returns an opened ftp connection resource. If the connection is not
478
     * already opened, it open it before.
479
     *
480
     * @return resource The ftp connection
481
     */
482
    private function getConnection()
483
    {
484
        if (!$this->isConnected()) {
485
            $this->connect();
486
        }
487
488
        return $this->connection;
489
    }
490
491
    /**
492
     * Opens the adapter's ftp connection.
493
     *
494
     * @throws RuntimeException if could not connect
495
     */
496
    private function connect()
497
    {
498
        // open ftp connection
499
        if (!$this->ssl) {
500
            $this->connection = ftp_connect($this->host, $this->port);
501
        } else {
502
            if (function_exists('ftp_ssl_connect')) {
503
                $this->connection = ftp_ssl_connect($this->host, $this->port);
504
            } else {
505
                throw new \RuntimeException('This Server Has No SSL-FTP Available.');
506
            }
507
        }
508
        if (!$this->connection) {
509
            throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port));
510
        }
511
512
        $username = $this->username ?: 'anonymous';
513
        $password = $this->password ?: '';
514
515
        // login ftp user
516
        if (!@ftp_login($this->connection, $username, $password)) {
517
            $this->close();
518
            throw new \RuntimeException(sprintf('Could not login as %s.', $username));
519
        }
520
521
        // switch to passive mode if needed
522
        if ($this->passive && !ftp_pasv($this->connection, true)) {
523
            $this->close();
524
            throw new \RuntimeException('Could not turn passive mode on.');
525
        }
526
527
        // enable utf8 mode if configured
528
        if($this->utf8 == true) {
529
            ftp_raw($this->connection, "OPTS UTF8 ON");
530
        }
531
532
        // ensure the adapter's directory exists
533
        if ('/' !== $this->directory) {
534
            try {
535
                $this->ensureDirectoryExists($this->directory, $this->create);
536
            } catch (\RuntimeException $e) {
537
                $this->close();
538
                throw $e;
539
            }
540
541
            // change the current directory for the adapter's directory
542
            if (!ftp_chdir($this->connection, $this->directory)) {
543
                $this->close();
544
                throw new \RuntimeException(sprintf('Could not change current directory for the \'%s\' directory.', $this->directory));
545
            }
546
        }
547
    }
548
549
    /**
550
     * Closes the adapter's ftp connection.
551
     */
552
    private function close()
553
    {
554
        if ($this->isConnected()) {
555
            ftp_close($this->connection);
556
        }
557
    }
558
559
    private function isLinuxListing($info)
560
    {
561
        return count($info) >= 9;
562
    }
563
}
564