Completed
Push — master ( 4f070d...a1881f )
by El
03:48
created

lib/Data/Filesystem.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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