Completed
Push — master ( aaeeb4...ea6f4d )
by Andrew
03:03
created

Ftp::close()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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