Completed
Push — master ( 1cb1c1...b2ea65 )
by El
05:50
created

Filesystem   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 364
Duplicated Lines 5.49 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 97.67%

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 2
dl 20
loc 364
ccs 126
cts 129
cp 0.9767
rs 8.4864
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstance() 0 16 4
A create() 0 11 3
A read() 0 18 4
B delete() 0 24 6
A exists() 0 4 1
A createComment() 0 12 3
B readComments() 0 31 4
A existsComment() 0 7 1
C _getExpiredPastes() 0 57 11
B _init() 20 20 6
A _dataid2path() 0 5 1
A _dataid2discussionpath() 0 5 1
A _isFirstLevelDir() 0 5 2
A _isSecondLevelDir() 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 Filesystem 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 Filesystem, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * PrivateBin
4
 *
5
 * a zero-knowledge paste bin
6
 *
7
 * @link      https://github.com/PrivateBin/PrivateBin
8
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10
 * @version   1.1
11
 */
12
13
namespace PrivateBin\Data;
14
15
use Exception;
16
use PrivateBin\Json;
17
use PrivateBin\Model\Paste;
18
19
/**
20
 * Filesystem
21
 *
22
 * Model for filesystem data access, implemented as a singleton.
23
 */
24
class Filesystem extends AbstractData
25
{
26
    /**
27
     * directory where data is stored
28
     *
29
     * @access private
30
     * @static
31
     * @var string
32
     */
33
    private static $_dir = 'data/';
34
35
    /**
36
     * get instance of singleton
37
     *
38
     * @access public
39
     * @static
40
     * @param  array $options
41
     * @return Filesystem
42
     */
43 60
    public static function getInstance($options = null)
44
    {
45
        // if needed initialize the singleton
46 60
        if (!(self::$_instance instanceof self)) {
47 54
            self::$_instance = new self;
48
        }
49
        // if given update the data directory
50
        if (
51 60
            is_array($options) &&
52 60
            array_key_exists('dir', $options)
53
        ) {
54 60
            self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR;
55 60
            self::_init();
56
        }
57 60
        return self::$_instance;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return self::$_instance; (PrivateBin\Data\Filesystem) is incompatible with the return type of the parent method PrivateBin\Data\AbstractData::getInstance of type PrivateBin\Data\privatebin_abstract|null.

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...
58
    }
59
60
    /**
61
     * Create a paste.
62
     *
63
     * @access public
64
     * @param  string $pasteid
65
     * @param  array  $paste
66
     * @throws Exception
67
     * @return bool
68
     */
69 44
    public function create($pasteid, $paste)
70
    {
71 44
        $storagedir = self::_dataid2path($pasteid);
72 44
        if (is_file($storagedir . $pasteid)) {
73 2
            return false;
74
        }
75 44
        if (!is_dir($storagedir)) {
76 44
            mkdir($storagedir, 0700, true);
77
        }
78 44
        return (bool) file_put_contents($storagedir . $pasteid, Json::encode($paste));
79
    }
80
81
    /**
82
     * Read a paste.
83
     *
84
     * @access public
85
     * @param  string $pasteid
86
     * @return stdClass|false
87
     */
88 31
    public function read($pasteid)
89
    {
90 31
        if (!$this->exists($pasteid)) {
91 1
            return false;
92
        }
93 31
        $paste = json_decode(
94 31
            file_get_contents(self::_dataid2path($pasteid) . $pasteid)
95
        );
96 31
        if (property_exists($paste->meta, 'attachment')) {
97 3
            $paste->attachment = $paste->meta->attachment;
98 3
            unset($paste->meta->attachment);
99 3
            if (property_exists($paste->meta, 'attachmentname')) {
100 3
                $paste->attachmentname = $paste->meta->attachmentname;
101 3
                unset($paste->meta->attachmentname);
102
            }
103
        }
104 31
        return $paste;
105
    }
106
107
    /**
108
     * Delete a paste and its discussion.
109
     *
110
     * @access public
111
     * @param  string $pasteid
112
     * @return void
113
     */
114 13
    public function delete($pasteid)
115
    {
116 13
        $pastedir = self::_dataid2path($pasteid);
117 13
        if (is_dir($pastedir)) {
118
            // Delete the paste itself.
119 10
            if (is_file($pastedir . $pasteid)) {
120 10
                unlink($pastedir . $pasteid);
121
            }
122
123
            // Delete discussion if it exists.
124 10
            $discdir = self::_dataid2discussionpath($pasteid);
125 10
            if (is_dir($discdir)) {
126
                // Delete all files in discussion directory
127 1
                $dir = dir($discdir);
128 1
                while (false !== ($filename = $dir->read())) {
129 1
                    if (is_file($discdir . $filename)) {
130 1
                        unlink($discdir . $filename);
131
                    }
132
                }
133 1
                $dir->close();
134 1
                rmdir($discdir);
135
            }
136
        }
137 13
    }
138
139
    /**
140
     * Test if a paste exists.
141
     *
142
     * @access public
143
     * @param  string $pasteid
144
     * @return bool
145
     */
146 58
    public function exists($pasteid)
147
    {
148 58
        return is_file(self::_dataid2path($pasteid) . $pasteid);
149
    }
150
151
    /**
152
     * Create a comment in a paste.
153
     *
154
     * @access public
155
     * @param  string $pasteid
156
     * @param  string $parentid
157
     * @param  string $commentid
158
     * @param  array  $comment
159
     * @throws Exception
160
     * @return bool
161
     */
162 4
    public function createComment($pasteid, $parentid, $commentid, $comment)
163
    {
164 4
        $storagedir = self::_dataid2discussionpath($pasteid);
165 4
        $filename   = $pasteid . '.' . $commentid . '.' . $parentid;
166 4
        if (is_file($storagedir . $filename)) {
167 1
            return false;
168
        }
169 4
        if (!is_dir($storagedir)) {
170 4
            mkdir($storagedir, 0700, true);
171
        }
172 4
        return (bool) file_put_contents($storagedir . $filename, Json::encode($comment));
173
    }
174
175
    /**
176
     * Read all comments of paste.
177
     *
178
     * @access public
179
     * @param  string $pasteid
180
     * @return array
181
     */
182 17
    public function readComments($pasteid)
183
    {
184 17
        $comments = array();
185 17
        $discdir  = self::_dataid2discussionpath($pasteid);
186 17
        if (is_dir($discdir)) {
187
            // Delete all files in discussion directory
188 2
            $dir = dir($discdir);
189 2
            while (false !== ($filename = $dir->read())) {
190
                // Filename is in the form pasteid.commentid.parentid:
191
                // - pasteid is the paste this reply belongs to.
192
                // - commentid is the comment identifier itself.
193
                // - parentid is the comment this comment replies to (It can be pasteid)
194 2
                if (is_file($discdir . $filename)) {
195 2
                    $comment = json_decode(file_get_contents($discdir . $filename));
196 2
                    $items   = explode('.', $filename);
197
                    // Add some meta information not contained in file.
198 2
                    $comment->id       = $items[1];
199 2
                    $comment->parentid = $items[2];
200
201
                    // Store in array
202 2
                    $key            = $this->getOpenSlot($comments, (int) $comment->meta->postdate);
203 2
                    $comments[$key] = $comment;
204
                }
205
            }
206 2
            $dir->close();
207
208
            // Sort comments by date, oldest first.
209 2
            ksort($comments);
210
        }
211 17
        return $comments;
212
    }
213
214
    /**
215
     * Test if a comment exists.
216
     *
217
     * @access public
218
     * @param  string $pasteid
219
     * @param  string $parentid
220
     * @param  string $commentid
221
     * @return bool
222
     */
223 7
    public function existsComment($pasteid, $parentid, $commentid)
224
    {
225 7
        return is_file(
226 7
            self::_dataid2discussionpath($pasteid) .
227 7
            $pasteid . '.' . $commentid . '.' . $parentid
228
        );
229
    }
230
231
    /**
232
     * Returns up to batch size number of paste ids that have expired
233
     *
234
     * @access private
235
     * @param  int $batchsize
236
     * @return array
237
     */
238 1
    protected function _getExpiredPastes($batchsize)
239
    {
240 1
        $pastes     = array();
241 1
        $firstLevel = array_filter(
242 1
            scandir(self::$_dir),
243 1
            'self::_isFirstLevelDir'
244
        );
245 1
        if (count($firstLevel) > 0) {
246
            // try at most 10 times the $batchsize pastes before giving up
247 1
            for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) {
248 1
                $firstKey    = array_rand($firstLevel);
249 1
                $secondLevel = array_filter(
250 1
                    scandir(self::$_dir . $firstLevel[$firstKey]),
251 1
                    'self::_isSecondLevelDir'
252
                );
253
254
                // skip this folder in the next checks if it is empty
255 1
                if (count($secondLevel) == 0) {
256 1
                    unset($firstLevel[$firstKey]);
257 1
                    continue;
258
                }
259
260 1
                $secondKey = array_rand($secondLevel);
261 1
                $path      = self::$_dir . $firstLevel[$firstKey] .
262 1
                    DIRECTORY_SEPARATOR . $secondLevel[$secondKey];
263 1
                if (!is_dir($path)) {
264
                    continue;
265
                }
266 1
                $thirdLevel = array_filter(
267
                    scandir($path),
268 1
                    'PrivateBin\\Model\\Paste::isValidId'
269
                );
270 1
                if (count($thirdLevel) == 0) {
271
                    continue;
272
                }
273 1
                $thirdKey = array_rand($thirdLevel);
274 1
                $pasteid  = $thirdLevel[$thirdKey];
275 1
                if (in_array($pasteid, $pastes)) {
276 1
                    continue;
277
                }
278
279 1
                if ($this->exists($pasteid)) {
280 1
                    $data = $this->read($pasteid);
281
                    if (
282 1
                        property_exists($data->meta, 'expire_date') &&
283 1
                        $data->meta->expire_date < time()
284
                    ) {
285 1
                        $pastes[] = $pasteid;
286 1
                        if (count($pastes) >= $batchsize) {
287
                            break;
288
                        }
289
                    }
290
                }
291
            }
292
        }
293 1
        return $pastes;
294
    }
295
296
    /**
297
     * Initialize data store
298
     *
299
     * @access private
300
     * @static
301
     * @return void
302
     */
303 60 View Code Duplication
    private static function _init()
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...
304
    {
305
        // Create storage directory if it does not exist.
306 60
        if (!is_dir(self::$_dir)) {
307 60
            if (!@mkdir(self::$_dir, 0700)) {
308 1
                throw new Exception('unable to create directory ' . self::$_dir, 10);
309
            }
310
        }
311 60
        $file = self::$_dir . DIRECTORY_SEPARATOR . '.htaccess';
312 60
        if (!is_file($file)) {
313 60
            $writtenBytes = @file_put_contents(
314
                $file,
315 60
                'Require all denied' . PHP_EOL,
316 60
                LOCK_EX
317
            );
318 60
            if ($writtenBytes === false || $writtenBytes < 19) {
319 1
                throw new Exception('unable to write to file ' . $file, 11);
320
            }
321
        }
322 60
    }
323
324
    /**
325
     * Convert paste id to storage path.
326
     *
327
     * The idea is to creates subdirectories in order to limit the number of files per directory.
328
     * (A high number of files in a single directory can slow things down.)
329
     * eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
330
     * High-trafic websites may want to deepen the directory structure (like Squid does).
331
     *
332
     * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
333
     *
334
     * @access private
335
     * @static
336
     * @param  string $dataid
337
     * @return string
338
     */
339 58
    private static function _dataid2path($dataid)
340
    {
341 58
        return self::$_dir . substr($dataid, 0, 2) . DIRECTORY_SEPARATOR .
342 58
            substr($dataid, 2, 2) . DIRECTORY_SEPARATOR;
343
    }
344
345
    /**
346
     * Convert paste id to discussion storage path.
347
     *
348
     * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
349
     *
350
     * @access private
351
     * @static
352
     * @param  string $dataid
353
     * @return string
354
     */
355 24
    private static function _dataid2discussionpath($dataid)
356
    {
357 24
        return self::_dataid2path($dataid) . $dataid .
358 24
            '.discussion' . DIRECTORY_SEPARATOR;
359
    }
360
361
    /**
362
     * Check that the given element is a valid first level directory.
363
     *
364
     * @access private
365
     * @static
366
     * @param  string $element
367
     * @return bool
368
     */
369 1
    private static function _isFirstLevelDir($element)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
370
    {
371 1
        return self::_isSecondLevelDir($element) &&
372 1
            is_dir(self::$_dir . DIRECTORY_SEPARATOR . $element);
373
    }
374
375
    /**
376
     * Check that the given element is a valid second level directory.
377
     *
378
     * @access private
379
     * @static
380
     * @param  string $element
381
     * @return bool
382
     */
383 1
    private static function _isSecondLevelDir($element)
384
    {
385 1
        return (bool) preg_match('/^[a-f0-9]{2}$/', $element);
386
    }
387
}
388