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.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 | 60 | public static function getInstance($options = null) |
|
34 | { |
||
35 | // if needed initialize the singleton |
||
36 | 60 | if (!(self::$_instance instanceof self)) { |
|
37 | 55 | self::$_instance = new self; |
|
38 | } |
||
39 | // if given update the data directory |
||
40 | if ( |
||
41 | 60 | is_array($options) && |
|
42 | 60 | array_key_exists('dir', $options) |
|
43 | ) { |
||
44 | 60 | DataStore::setPath($options['dir']); |
|
45 | } |
||
46 | 60 | 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 . '.php'; |
|
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 | 33 | public function read($pasteid) |
|
78 | { |
||
79 | 33 | if (!$this->exists($pasteid)) { |
|
80 | 1 | return false; |
|
81 | } |
||
82 | 33 | $paste = DataStore::get(self::_dataid2path($pasteid) . $pasteid . '.php'); |
|
83 | 33 | if (property_exists($paste->meta, 'attachment')) { |
|
84 | 3 | $paste->attachment = $paste->meta->attachment; |
|
85 | 3 | unset($paste->meta->attachment); |
|
86 | 3 | if (property_exists($paste->meta, 'attachmentname')) { |
|
87 | 3 | $paste->attachmentname = $paste->meta->attachmentname; |
|
88 | 3 | unset($paste->meta->attachmentname); |
|
89 | } |
||
90 | } |
||
91 | 33 | return $paste; |
|
0 ignored issues
–
show
|
|||
92 | } |
||
93 | |||
94 | /** |
||
95 | * Delete a paste and its discussion. |
||
96 | * |
||
97 | * @access public |
||
98 | * @param string $pasteid |
||
99 | */ |
||
100 | 14 | public function delete($pasteid) |
|
101 | { |
||
102 | 14 | $pastedir = self::_dataid2path($pasteid); |
|
103 | 14 | if (is_dir($pastedir)) { |
|
104 | // Delete the paste itself. |
||
105 | 11 | if (is_file($pastedir . $pasteid . '.php')) { |
|
106 | 11 | unlink($pastedir . $pasteid . '.php'); |
|
107 | } |
||
108 | |||
109 | // Delete discussion if it exists. |
||
110 | 11 | $discdir = self::_dataid2discussionpath($pasteid); |
|
111 | 11 | if (is_dir($discdir)) { |
|
112 | // Delete all files in discussion directory |
||
113 | 1 | $dir = dir($discdir); |
|
114 | 1 | while (false !== ($filename = $dir->read())) { |
|
115 | 1 | if (is_file($discdir . $filename)) { |
|
116 | 1 | unlink($discdir . $filename); |
|
117 | } |
||
118 | } |
||
119 | 1 | $dir->close(); |
|
120 | 1 | rmdir($discdir); |
|
121 | } |
||
122 | } |
||
123 | 14 | } |
|
124 | |||
125 | /** |
||
126 | * Test if a paste exists. |
||
127 | * |
||
128 | * @access public |
||
129 | * @param string $pasteid |
||
130 | * @return bool |
||
131 | */ |
||
132 | 60 | public function exists($pasteid) |
|
133 | { |
||
134 | 60 | $basePath = self::_dataid2path($pasteid) . $pasteid; |
|
135 | 60 | $pastePath = $basePath . '.php'; |
|
136 | // convert to PHP protected files if needed |
||
137 | 60 | if (is_readable($basePath)) { |
|
138 | 1 | $context = stream_context_create(); |
|
139 | // don't overwrite already converted file |
||
140 | 1 | View Code Duplication | if (!is_file($pastePath)) { |
0 ignored issues
–
show
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...
|
|||
141 | 1 | $handle = fopen($basePath, 'r', false, $context); |
|
142 | 1 | file_put_contents($pastePath, DataStore::PROTECTION_LINE . PHP_EOL); |
|
143 | 1 | file_put_contents($pastePath, $handle, FILE_APPEND); |
|
144 | 1 | fclose($handle); |
|
145 | } |
||
146 | 1 | unlink($basePath); |
|
147 | |||
148 | // convert comments, too |
||
149 | 1 | $discdir = self::_dataid2discussionpath($pasteid); |
|
150 | 1 | if (is_dir($discdir)) { |
|
151 | 1 | $dir = dir($discdir); |
|
152 | 1 | while (false !== ($filename = $dir->read())) { |
|
153 | 1 | if (substr($filename, -4) !== '.php' && strlen($filename) >= 16) { |
|
154 | 1 | $commentFilename = $discdir . $filename . '.php'; |
|
155 | // don't overwrite already converted file |
||
156 | 1 | View Code Duplication | if (!is_file($commentFilename)) { |
0 ignored issues
–
show
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...
|
|||
157 | 1 | $handle = fopen($discdir . $filename, 'r', false, $context); |
|
158 | 1 | file_put_contents($commentFilename, DataStore::PROTECTION_LINE . PHP_EOL); |
|
159 | 1 | file_put_contents($commentFilename, $handle, FILE_APPEND); |
|
160 | 1 | fclose($handle); |
|
161 | } |
||
162 | 1 | unlink($discdir . $filename); |
|
163 | } |
||
164 | } |
||
165 | 1 | $dir->close(); |
|
166 | } |
||
167 | } |
||
168 | 60 | return is_readable($pastePath); |
|
169 | } |
||
170 | |||
171 | /** |
||
172 | * Create a comment in a paste. |
||
173 | * |
||
174 | * @access public |
||
175 | * @param string $pasteid |
||
176 | * @param string $parentid |
||
177 | * @param string $commentid |
||
178 | * @param array $comment |
||
179 | * @return bool |
||
180 | */ |
||
181 | 4 | public function createComment($pasteid, $parentid, $commentid, $comment) |
|
182 | { |
||
183 | 4 | $storagedir = self::_dataid2discussionpath($pasteid); |
|
184 | 4 | $file = $storagedir . $pasteid . '.' . $commentid . '.' . $parentid . '.php'; |
|
185 | 4 | if (is_file($file)) { |
|
186 | 1 | return false; |
|
187 | } |
||
188 | 4 | if (!is_dir($storagedir)) { |
|
189 | 4 | mkdir($storagedir, 0700, true); |
|
190 | } |
||
191 | 4 | return DataStore::store($file, $comment); |
|
192 | } |
||
193 | |||
194 | /** |
||
195 | * Read all comments of paste. |
||
196 | * |
||
197 | * @access public |
||
198 | * @param string $pasteid |
||
199 | * @return array |
||
200 | */ |
||
201 | 19 | public function readComments($pasteid) |
|
202 | { |
||
203 | 19 | $comments = array(); |
|
204 | 19 | $discdir = self::_dataid2discussionpath($pasteid); |
|
205 | 19 | if (is_dir($discdir)) { |
|
206 | 3 | $dir = dir($discdir); |
|
207 | 3 | while (false !== ($filename = $dir->read())) { |
|
208 | // Filename is in the form pasteid.commentid.parentid.php: |
||
209 | // - pasteid is the paste this reply belongs to. |
||
210 | // - commentid is the comment identifier itself. |
||
211 | // - parentid is the comment this comment replies to (It can be pasteid) |
||
212 | 3 | if (is_file($discdir . $filename)) { |
|
213 | 3 | $comment = DataStore::get($discdir . $filename); |
|
214 | 3 | $items = explode('.', $filename); |
|
215 | // Add some meta information not contained in file. |
||
216 | 3 | $comment->id = $items[1]; |
|
217 | 3 | $comment->parentid = $items[2]; |
|
218 | |||
219 | // Store in array |
||
220 | 3 | $key = $this->getOpenSlot($comments, (int) $comment->meta->postdate); |
|
221 | 3 | $comments[$key] = $comment; |
|
222 | } |
||
223 | } |
||
224 | 3 | $dir->close(); |
|
225 | |||
226 | // Sort comments by date, oldest first. |
||
227 | 3 | ksort($comments); |
|
228 | } |
||
229 | 19 | return $comments; |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * Test if a comment exists. |
||
234 | * |
||
235 | * @access public |
||
236 | * @param string $pasteid |
||
237 | * @param string $parentid |
||
238 | * @param string $commentid |
||
239 | * @return bool |
||
240 | */ |
||
241 | 8 | public function existsComment($pasteid, $parentid, $commentid) |
|
242 | { |
||
243 | 8 | return is_file( |
|
244 | 8 | self::_dataid2discussionpath($pasteid) . |
|
245 | 8 | $pasteid . '.' . $commentid . '.' . $parentid . '.php' |
|
246 | ); |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * Returns up to batch size number of paste ids that have expired |
||
251 | * |
||
252 | * @access private |
||
253 | * @param int $batchsize |
||
254 | * @return array |
||
255 | */ |
||
256 | 2 | protected function _getExpiredPastes($batchsize) |
|
257 | { |
||
258 | 2 | $pastes = array(); |
|
259 | 2 | $mainpath = DataStore::getPath(); |
|
260 | 2 | $firstLevel = array_filter( |
|
261 | 2 | scandir($mainpath), |
|
262 | 2 | 'self::_isFirstLevelDir' |
|
263 | ); |
||
264 | 2 | if (count($firstLevel) > 0) { |
|
265 | // try at most 10 times the $batchsize pastes before giving up |
||
266 | 2 | for ($i = 0, $max = $batchsize * 10; $i < $max; ++$i) { |
|
267 | 2 | $firstKey = array_rand($firstLevel); |
|
268 | 2 | $secondLevel = array_filter( |
|
269 | 2 | scandir($mainpath . DIRECTORY_SEPARATOR . $firstLevel[$firstKey]), |
|
270 | 2 | 'self::_isSecondLevelDir' |
|
271 | ); |
||
272 | |||
273 | // skip this folder in the next checks if it is empty |
||
274 | 2 | if (count($secondLevel) == 0) { |
|
275 | 1 | unset($firstLevel[$firstKey]); |
|
276 | 1 | continue; |
|
277 | } |
||
278 | |||
279 | 2 | $secondKey = array_rand($secondLevel); |
|
280 | 2 | $path = $mainpath . DIRECTORY_SEPARATOR . |
|
281 | 2 | $firstLevel[$firstKey] . DIRECTORY_SEPARATOR . |
|
282 | 2 | $secondLevel[$secondKey]; |
|
283 | 2 | if (!is_dir($path)) { |
|
284 | continue; |
||
285 | } |
||
286 | 2 | $thirdLevel = array_filter( |
|
287 | 2 | array_map( |
|
288 | 2 | function ($filename) { |
|
289 | 2 | return strlen($filename) >= 20 ? |
|
290 | 2 | substr($filename, 0, -4) : |
|
291 | 2 | $filename; |
|
292 | 2 | }, |
|
293 | 2 | scandir($path) |
|
294 | ), |
||
295 | 2 | 'PrivateBin\\Model\\Paste::isValidId' |
|
296 | ); |
||
297 | 2 | if (count($thirdLevel) == 0) { |
|
298 | continue; |
||
299 | } |
||
300 | 2 | $thirdKey = array_rand($thirdLevel); |
|
301 | 2 | $pasteid = $thirdLevel[$thirdKey]; |
|
302 | 2 | if (in_array($pasteid, $pastes)) { |
|
303 | 1 | continue; |
|
304 | } |
||
305 | |||
306 | 2 | if ($this->exists($pasteid)) { |
|
307 | 2 | $data = $this->read($pasteid); |
|
308 | if ( |
||
309 | 2 | property_exists($data->meta, 'expire_date') && |
|
310 | 2 | $data->meta->expire_date < time() |
|
311 | ) { |
||
312 | 1 | $pastes[] = $pasteid; |
|
313 | 1 | if (count($pastes) >= $batchsize) { |
|
314 | break; |
||
315 | } |
||
316 | } |
||
317 | } |
||
318 | } |
||
319 | } |
||
320 | 2 | return $pastes; |
|
321 | } |
||
322 | |||
323 | /** |
||
324 | * Convert paste id to storage path. |
||
325 | * |
||
326 | * The idea is to creates subdirectories in order to limit the number of files per directory. |
||
327 | * (A high number of files in a single directory can slow things down.) |
||
328 | * eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8" |
||
329 | * High-trafic websites may want to deepen the directory structure (like Squid does). |
||
330 | * |
||
331 | * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/' |
||
332 | * |
||
333 | * @access private |
||
334 | * @static |
||
335 | * @param string $dataid |
||
336 | * @return string |
||
337 | */ |
||
338 | 60 | private static function _dataid2path($dataid) |
|
339 | { |
||
340 | 60 | return DataStore::getPath( |
|
341 | 60 | substr($dataid, 0, 2) . DIRECTORY_SEPARATOR . |
|
342 | 60 | substr($dataid, 2, 2) . DIRECTORY_SEPARATOR |
|
343 | ); |
||
344 | } |
||
345 | |||
346 | /** |
||
347 | * Convert paste id to discussion storage path. |
||
348 | * |
||
349 | * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/' |
||
350 | * |
||
351 | * @access private |
||
352 | * @static |
||
353 | * @param string $dataid |
||
354 | * @return string |
||
355 | */ |
||
356 | 26 | private static function _dataid2discussionpath($dataid) |
|
357 | { |
||
358 | 26 | return self::_dataid2path($dataid) . $dataid . |
|
359 | 26 | '.discussion' . DIRECTORY_SEPARATOR; |
|
360 | } |
||
361 | |||
362 | /** |
||
363 | * Check that the given element is a valid first level directory. |
||
364 | * |
||
365 | * @access private |
||
366 | * @static |
||
367 | * @param string $element |
||
368 | * @return bool |
||
369 | */ |
||
370 | 2 | private static function _isFirstLevelDir($element) |
|
371 | { |
||
372 | 2 | return self::_isSecondLevelDir($element) && |
|
373 | 2 | is_dir(DataStore::getPath($element)); |
|
374 | } |
||
375 | |||
376 | /** |
||
377 | * Check that the given element is a valid second level directory. |
||
378 | * |
||
379 | * @access private |
||
380 | * @static |
||
381 | * @param string $element |
||
382 | * @return bool |
||
383 | */ |
||
384 | 2 | private static function _isSecondLevelDir($element) |
|
385 | { |
||
386 | 2 | return (bool) preg_match('/^[a-f0-9]{2}$/', $element); |
|
387 | } |
||
388 | } |
||
389 |
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.