Completed
Push — composer-installed ( 5832b4 )
by Ilia
08:49
created

Ftp   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 541
Duplicated Lines 3.7 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 20
loc 541
rs 2
c 0
b 0
f 0
wmc 88
lcom 1
cbo 1

23 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 12 8
A read() 0 16 2
A write() 0 23 2
A rename() 0 11 1
A exists() 0 20 4
A keys() 0 8 1
A listKeys() 8 25 5
A mtime() 0 13 2
A delete() 0 10 2
A isDirectory() 0 6 1
B listDirectory() 0 38 8
A createFile() 0 20 4
A ensureDirectoryExists() 0 10 3
A createDirectory() 0 14 3
A isDir() 0 15 3
C fetchKeys() 12 59 13
B parseRawlist() 0 31 7
A computePath() 0 4 1
A isConnected() 0 4 1
A getConnection() 0 8 2
C connect() 0 47 12
A close() 0 6 2
A isLinuxListing() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Ftp often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Ftp, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Gaufrette\File;
7
use Gaufrette\Filesystem;
8
use Gaufrette\Exception;
9
10
/**
11
 * Ftp adapter
12
 *
13
 * @package Gaufrette
14
 * @author  Antoine Hérault <[email protected]>
15
 */
16
class Ftp implements Adapter,
17
                     FileFactory,
18
                     ListKeysAware
19
{
20
    protected $connection = null;
21
    protected $directory;
22
    protected $host;
23
    protected $port;
24
    protected $username;
25
    protected $password;
26
    protected $passive;
27
    protected $create;
28
    protected $mode;
29
    protected $ssl;
30
    protected $fileData = array();
31
32
    /**
33
     * Constructor
34
     *
35
     * @param string $directory The directory to use in the ftp server
36
     * @param string $host      The host of the ftp server
37
     * @param array  $options   The options like port, username, password, passive, create, mode
38
     */
39
    public function __construct($directory, $host, $options = array())
40
    {
41
        $this->directory = (string) $directory;
42
        $this->host      = $host;
43
        $this->port      = isset($options['port']) ? $options['port'] : 21;
44
        $this->username  = isset($options['username']) ? $options['username'] : null;
45
        $this->password  = isset($options['password']) ? $options['password'] : null;
46
        $this->passive   = isset($options['passive']) ? $options['passive'] : false;
47
        $this->create    = isset($options['create']) ? $options['create'] : false;
48
        $this->mode      = isset($options['mode']) ? $options['mode'] : FTP_BINARY;
49
        $this->ssl       = isset($options['ssl']) ? $options['ssl'] : false;
50
    }
51
52
    /**
53
     * {@inheritDoc}
54
     */
55
    public function read($key)
56
    {
57
        $this->ensureDirectoryExists($this->directory, $this->create);
58
59
        $temp = fopen('php://temp', 'r+');
60
61
        if (!ftp_fget($this->getConnection(), $temp, $this->computePath($key), $this->mode)) {
62
            return false;
63
        }
64
65
        rewind($temp);
66
        $contents = stream_get_contents($temp);
67
        fclose($temp);
68
69
        return $contents;
70
    }
71
72
    /**
73
     * {@inheritDoc}
74
     */
75
    public function write($key, $content)
76
    {
77
        $this->ensureDirectoryExists($this->directory, $this->create);
78
79
        $path = $this->computePath($key);
80
        $directory = dirname($path);
81
82
        $this->ensureDirectoryExists($directory, true);
83
84
        $temp = fopen('php://temp', 'r+');
85
        $size = fwrite($temp, $content);
86
        rewind($temp);
87
88
        if (!ftp_fput($this->getConnection(), $path, $temp, $this->mode)) {
89
            fclose($temp);
90
91
            return false;
92
        }
93
94
        fclose($temp);
95
96
        return $size;
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     */
102
    public function rename($sourceKey, $targetKey)
103
    {
104
        $this->ensureDirectoryExists($this->directory, $this->create);
105
106
        $sourcePath = $this->computePath($sourceKey);
107
        $targetPath = $this->computePath($targetKey);
108
109
        $this->ensureDirectoryExists(dirname($targetPath), true);
110
111
        return ftp_rename($this->getConnection(), $sourcePath, $targetPath);
112
    }
113
114
    /**
115
     * {@inheritDoc}
116
     */
117
    public function exists($key)
118
    {
119
        $this->ensureDirectoryExists($this->directory, $this->create);
120
121
        $file  = $this->computePath($key);
122
        $lines = ftp_rawlist($this->getConnection(), '-al ' . dirname($file));
123
124
        if (false === $lines) {
125
            return false;
126
        }
127
128
        $pattern = '{(?<!->) '.preg_quote(basename($file)).'( -> |$)}m';
129
        foreach ($lines as $line) {
130
            if (preg_match($pattern, $line)) {
131
                return true;
132
            }
133
        }
134
135
        return false;
136
    }
137
138
    /**
139
     * {@inheritDoc}
140
     */
141
    public function keys()
142
    {
143
        $this->ensureDirectoryExists($this->directory, $this->create);
144
145
        $keys = $this->fetchKeys();
146
147
        return $keys['keys'];
148
    }
149
150
    /**
151
     * {@inheritDoc}
152
     */
153
    public function listKeys($prefix = '')
154
    {
155
        $this->ensureDirectoryExists($this->directory, $this->create);
156
157
        preg_match('/(.*?)[^\/]*$/', $prefix, $match);
158
        $directory = rtrim($match[1], '/');
159
160
        $keys = $this->fetchKeys($directory, false);
161
162
        if ($directory === $prefix) {
163
            return $keys;
164
        }
165
166
        $filteredKeys = array();
167 View Code Duplication
        foreach (array('keys', 'dirs') as $hash) {
168
            $filteredKeys[$hash] = array();
169
            foreach ($keys[$hash] as $key) {
170
                if (0 === strpos($key, $prefix)) {
171
                    $filteredKeys[$hash][] = $key;
172
                }
173
            }
174
        }
175
176
        return $filteredKeys;
177
    }
178
179
    /**
180
     * {@inheritDoc}
181
     */
182
    public function mtime($key)
183
    {
184
        $this->ensureDirectoryExists($this->directory, $this->create);
185
186
        $mtime = ftp_mdtm($this->getConnection(), $this->computePath($key));
187
188
        // the server does not support this function
189
        if (-1 === $mtime) {
190
            throw new \RuntimeException('Server does not support ftp_mdtm function.');
191
        }
192
193
        return $mtime;
194
    }
195
196
    /**
197
     * {@inheritDoc}
198
     */
199
    public function delete($key)
200
    {
201
        $this->ensureDirectoryExists($this->directory, $this->create);
202
203
        if ($this->isDirectory($key)) {
204
            return ftp_rmdir($this->getConnection(), $this->computePath($key));
205
        }
206
207
        return ftp_delete($this->getConnection(), $this->computePath($key));
208
    }
209
210
    /**
211
     * {@inheritDoc}
212
     */
213
    public function isDirectory($key)
214
    {
215
        $this->ensureDirectoryExists($this->directory, $this->create);
216
217
        return $this->isDir($this->computePath($key));
218
    }
219
220
    /**
221
     * Lists files from the specified directory. If a pattern is
222
     * specified, it only returns files matching it.
223
     *
224
     * @param string $directory The path of the directory to list from
225
     *
226
     * @return array An array of keys and dirs
227
     */
228
    public function listDirectory($directory = '')
229
    {
230
        $this->ensureDirectoryExists($this->directory, $this->create);
231
232
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
233
234
        $items = $this->parseRawlist(
235
            ftp_rawlist($this->getConnection(), '-al ' . $this->directory . $directory ) ? : array()
236
        );
237
238
        $fileData = $dirs = array();
239
        foreach ($items as $itemData) {
240
241
            if ('..' === $itemData['name'] || '.' === $itemData['name']) {
242
                continue;
243
            }
244
245
            $item = array(
246
                'name'  => $itemData['name'],
247
                'path'  => trim(($directory ? $directory . '/' : '') . $itemData['name'], '/'),
248
                'time'  => $itemData['time'],
249
                'size'  => $itemData['size'],
250
            );
251
252
            if ('-' === substr($itemData['perms'], 0, 1)) {
253
                $fileData[$item['path']] = $item;
254
            } elseif ('d' === substr($itemData['perms'], 0, 1)) {
255
                $dirs[] = $item['path'];
256
            }
257
        }
258
259
        $this->fileData = array_merge($fileData, $this->fileData);
260
261
        return array(
262
           'keys'   => array_keys($fileData),
263
           'dirs'   => $dirs
264
        );
265
    }
266
267
    /**
268
     * {@inheritDoc}
269
     */
270
    public function createFile($key, Filesystem $filesystem)
271
    {
272
        $this->ensureDirectoryExists($this->directory, $this->create);
273
274
        $file = new File($key, $filesystem);
275
276
        if (!array_key_exists($key, $this->fileData)) {
277
            $directory = dirname($key) == '.' ? '' : dirname($key);
278
            $this->listDirectory($directory);
279
        }
280
281
        if (isset($this->fileData[$key])) {
282
            $fileData = $this->fileData[$key];
283
284
            $file->setName($fileData['name']);
285
            $file->setSize($fileData['size']);
286
        }
287
288
        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...
289
    }
290
291
    /**
292
     * Ensures the specified directory exists. If it does not, and the create
293
     * parameter is set to TRUE, it tries to create it
294
     *
295
     * @param string  $directory
296
     * @param boolean $create    Whether to create the directory if it does not
297
     *                         exist
298
     *
299
     * @throws RuntimeException if the directory does not exist and could not
300
     *                          be created
301
     */
302
    protected function ensureDirectoryExists($directory, $create = false)
303
    {
304
        if (!$this->isDir($directory)) {
305
            if (!$create) {
306
                throw new \RuntimeException(sprintf('The directory \'%s\' does not exist.', $directory));
307
            }
308
309
            $this->createDirectory($directory);
310
        }
311
    }
312
313
    /**
314
     * Creates the specified directory and its parent directories
315
     *
316
     * @param string $directory Directory to create
317
     *
318
     * @throws RuntimeException if the directory could not be created
319
     */
320
    protected function createDirectory($directory)
321
    {
322
        // create parent directory if needed
323
        $parent = dirname($directory);
324
        if (!$this->isDir($parent)) {
325
            $this->createDirectory($parent);
326
        }
327
328
        // create the specified directory
329
        $created = ftp_mkdir($this->getConnection(), $directory);
330
        if (false === $created) {
331
            throw new \RuntimeException(sprintf('Could not create the \'%s\' directory.', $directory));
332
        }
333
    }
334
335
    /**
336
     * @param  string  $directory - full directory path
337
     * @return boolean
338
     */
339
    private function isDir($directory)
340
    {
341
        if ('/' === $directory) {
342
            return true;
343
        }
344
345
        if (!@ftp_chdir($this->getConnection(), $directory)) {
346
            return false;
347
        }
348
349
        // change directory again to return in the base directory
350
        ftp_chdir($this->getConnection(), $this->directory);
351
352
        return true;
353
    }
354
355
    private function fetchKeys($directory = '', $onlyKeys = true)
356
    {
357
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
358
359
        $lines = ftp_rawlist($this->getConnection(), '-alR '. $this->directory . $directory);
360
361
        if (false === $lines) {
362
            return array('keys' => array(), 'dirs' => array());
363
        }
364
365
        $regexDir = '/'.preg_quote($this->directory . $directory, '/').'\/?(.+):$/u';
366
        $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/';
367
368
        $prevLine = null;
369
        $directories = array();
370
        $keys = array('keys' => array(), 'dirs' => array());
371
372
        foreach ((array) $lines as $line) {
373
            if ('' === $prevLine && preg_match($regexDir, $line, $match)) {
374
                $directory = $match[1];
375
                unset($directories[$directory]);
376 View Code Duplication
                if ($onlyKeys) {
377
                    $keys = array(
378
                        'keys' => array_merge($keys['keys'], $keys['dirs']),
379
                        'dirs' => array()
380
                    );
381
                }
382
            } elseif (preg_match($regexItem, $line, $tokens)) {
383
                $name = $tokens[3];
384
385
                if ('.' === $name || '..' === $name) {
386
                    continue;
387
                }
388
389
                $path = ltrim($directory . '/' . $name, '/');
390
391
                if ('d' === $tokens[1] || '<dir>' === $tokens[2]) {
392
                    $keys['dirs'][] = $path;
393
                    $directories[$path] = true;
394
                } else {
395
                    $keys['keys'][] = $path;
396
                }
397
            }
398
            $prevLine = $line;
399
        }
400
401 View Code Duplication
        if ($onlyKeys) {
402
            $keys = array(
403
                'keys' => array_merge($keys['keys'], $keys['dirs']),
404
                'dirs' => array()
405
            );
406
        }
407
408
        foreach (array_keys($directories) as $directory) {
409
            $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys));
410
        }
411
412
        return $keys;
413
    }
414
415
    /**
416
     * Parses the given raw list
417
     *
418
     * @param array $rawlist
419
     *
420
     * @return array
421
     */
422
    private function parseRawlist(array $rawlist)
423
    {
424
        $parsed = array();
425
        foreach ($rawlist as $line) {
426
            $infos = preg_split("/[\s]+/", $line, 9);
427
428
            if ($this->isLinuxListing($infos)) {
429
                $infos[7] = (strrpos($infos[7], ':') != 2 ) ? ($infos[7] . ' 00:00') : (date('Y') . ' ' . $infos[7]);
430
                if ('total' !== $infos[0]) {
431
                    $parsed[] = array(
432
                        'perms' => $infos[0],
433
                        'num'   => $infos[1],
434
                        'size'  => $infos[4],
435
                        'time'  => strtotime($infos[5] . ' ' . $infos[6] . '. ' . $infos[7]),
436
                        'name'  => $infos[8]
437
                    );
438
                }
439
            } else {
440
                $isDir = (boolean) ('<dir>' === $infos[2]);
441
                $parsed[] = array(
442
                    'perms' => $isDir ? 'd' : '-',
443
                    'num'   => '',
444
                    'size'  => $isDir ? '' : $infos[2],
445
                    'time'  => strtotime($infos[0] . ' ' . $infos[1]),
446
                    'name'  => $infos[3]
447
                );
448
            }
449
        }
450
451
        return $parsed;
452
    }
453
454
    /**
455
     * Computes the path for the given key
456
     *
457
     * @param string $key
458
     */
459
    private function computePath($key)
460
    {
461
        return rtrim($this->directory, '/') . '/' . $key;
462
    }
463
464
    /**
465
     * Indicates whether the adapter has an open ftp connection
466
     *
467
     * @return boolean
468
     */
469
    private function isConnected()
470
    {
471
        return is_resource($this->connection);
472
    }
473
474
    /**
475
     * Returns an opened ftp connection resource. If the connection is not
476
     * already opened, it open it before
477
     *
478
     * @return resource The ftp connection
479
     */
480
    private function getConnection()
481
    {
482
        if (!$this->isConnected()) {
483
            $this->connect();
484
        }
485
486
        return $this->connection;
487
    }
488
489
    /**
490
     * Opens the adapter's ftp connection
491
     *
492
     * @throws RuntimeException if could not connect
493
     */
494
    private function connect()
495
    {
496
        // open ftp connection
497
        if (!$this->ssl) {
498
            $this->connection = ftp_connect($this->host, $this->port);
499
        } else {
500
            if(function_exists('ftp_ssl_connect')) {
501
                $this->connection = ftp_ssl_connect($this->host, $this->port);        
502
            } else {
503
                throw new \RuntimeException('This Server Has No SSL-FTP Available.');
504
            }
505
        }
506
        if (!$this->connection) {
507
            throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port));
508
        }
509
510
        $username = $this->username ? : 'anonymous';
511
        $password = $this->password ? : '';
512
513
        // login ftp user
514
        if (!@ftp_login($this->connection, $username, $password)) {
515
            $this->close();
516
            throw new \RuntimeException(sprintf('Could not login as %s.', $username));
517
        }
518
519
        // switch to passive mode if needed
520
        if ($this->passive && !ftp_pasv($this->connection, true)) {
521
            $this->close();
522
            throw new \RuntimeException('Could not turn passive mode on.');
523
        }
524
525
        // ensure the adapter's directory exists
526
        if ('/' !== $this->directory) {
527
            try {
528
                $this->ensureDirectoryExists($this->directory, $this->create);
529
            } catch (\RuntimeException $e) {
530
                $this->close();
531
                throw $e;
532
            }
533
534
            // change the current directory for the adapter's directory
535
            if (!ftp_chdir($this->connection, $this->directory)) {
536
                $this->close();
537
                throw new \RuntimeException(sprintf('Could not change current directory for the \'%s\' directory.', $this->directory));
538
            }
539
        }
540
    }
541
542
    /**
543
     * Closes the adapter's ftp connection
544
     */
545
    private function close()
546
    {
547
        if ($this->isConnected()) {
548
            ftp_close($this->connection);
549
        }
550
    }
551
552
    private function isLinuxListing($info)
553
    {
554
        return count($info) >= 9;
555
    }
556
}
557