Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.28% |
146 / 202 |
|
46.67% |
7 / 15 |
CRAP | |
0.00% |
0 / 1 |
file | |
72.28% |
146 / 202 |
|
46.67% |
7 / 15 |
225.26 | |
0.00% |
0 / 1 |
__construct | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
load | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
unload | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
save | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
6.00 | |||
tidy | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
110 | |||
get | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
put | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
purge | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
destroy | |
58.06% |
18 / 31 |
|
0.00% |
0 / 1 |
25.46 | |||
_exists | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
sql_save | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
3.01 | |||
cleanup_invalid_data_global | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
_read | |
88.89% |
56 / 63 |
|
0.00% |
0 / 1 |
22.66 | |||
_write | |
87.10% |
27 / 31 |
|
0.00% |
0 / 1 |
9.17 | |||
clean_varname | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\cache\driver; |
15 | |
16 | /** |
17 | * ACM File Based Caching |
18 | */ |
19 | class file extends \phpbb\cache\driver\base |
20 | { |
21 | var $var_expires = array(); |
22 | |
23 | /** |
24 | * @var \phpbb\filesystem\filesystem_interface |
25 | */ |
26 | protected $filesystem; |
27 | |
28 | /** |
29 | * Set cache path |
30 | * |
31 | * @param string $cache_dir Define the path to the cache directory (default: $phpbb_root_path . 'cache/') |
32 | */ |
33 | function __construct($cache_dir = null) |
34 | { |
35 | global $phpbb_container; |
36 | |
37 | $this->cache_dir = !is_null($cache_dir) ? $cache_dir : $phpbb_container->getParameter('core.cache_dir'); |
38 | $this->filesystem = new \phpbb\filesystem\filesystem(); |
39 | |
40 | if ($this->filesystem->is_writable(dirname($this->cache_dir)) && !is_dir($this->cache_dir)) |
41 | { |
42 | mkdir($this->cache_dir, 0777, true); |
43 | } |
44 | } |
45 | |
46 | /** |
47 | * {@inheritDoc} |
48 | */ |
49 | function load() |
50 | { |
51 | return $this->_read('data_global'); |
52 | } |
53 | |
54 | /** |
55 | * {@inheritDoc} |
56 | */ |
57 | function unload() |
58 | { |
59 | parent::unload(); |
60 | unset($this->var_expires); |
61 | $this->var_expires = array(); |
62 | } |
63 | |
64 | /** |
65 | * {@inheritDoc} |
66 | */ |
67 | function save() |
68 | { |
69 | if (!$this->is_modified) |
70 | { |
71 | return; |
72 | } |
73 | |
74 | global $phpEx; |
75 | |
76 | if (!$this->_write('data_global')) |
77 | { |
78 | // Now, this occurred how often? ... phew, just tell the user then... |
79 | if (!$this->filesystem->is_writable($this->cache_dir)) |
80 | { |
81 | // We need to use die() here, because else we may encounter an infinite loop (the message handler calls $cache->unload()) |
82 | die('Fatal: ' . $this->cache_dir . ' is NOT writable.'); |
83 | exit; |
84 | } |
85 | |
86 | die('Fatal: Not able to open ' . $this->cache_dir . 'data_global.' . $phpEx); |
87 | exit; |
88 | } |
89 | |
90 | $this->is_modified = false; |
91 | } |
92 | |
93 | /** |
94 | * {@inheritDoc} |
95 | */ |
96 | function tidy() |
97 | { |
98 | global $config, $phpEx; |
99 | |
100 | $dir = @opendir($this->cache_dir); |
101 | |
102 | if (!$dir) |
103 | { |
104 | return; |
105 | } |
106 | |
107 | $time = time(); |
108 | |
109 | while (($entry = readdir($dir)) !== false) |
110 | { |
111 | if (!preg_match('/^(sql_|data_(?!global))/', $entry)) |
112 | { |
113 | continue; |
114 | } |
115 | |
116 | if (!($handle = @fopen($this->cache_dir . $entry, 'rb'))) |
117 | { |
118 | continue; |
119 | } |
120 | |
121 | // Skip the PHP header |
122 | fgets($handle); |
123 | |
124 | // Skip expiration |
125 | $expires = (int) fgets($handle); |
126 | |
127 | fclose($handle); |
128 | |
129 | if ($time >= $expires) |
130 | { |
131 | $this->remove_file($this->cache_dir . $entry); |
132 | } |
133 | } |
134 | closedir($dir); |
135 | |
136 | if (file_exists($this->cache_dir . 'data_global.' . $phpEx)) |
137 | { |
138 | if (!count($this->vars)) |
139 | { |
140 | $this->load(); |
141 | } |
142 | |
143 | foreach ($this->var_expires as $var_name => $expires) |
144 | { |
145 | if ($time >= $expires) |
146 | { |
147 | $this->destroy($var_name); |
148 | } |
149 | } |
150 | } |
151 | |
152 | $config->set('cache_last_gc', time(), false); |
153 | } |
154 | |
155 | /** |
156 | * {@inheritDoc} |
157 | */ |
158 | function get($var_name) |
159 | { |
160 | if ($var_name[0] == '_') |
161 | { |
162 | if (!$this->_exists($var_name)) |
163 | { |
164 | return false; |
165 | } |
166 | |
167 | return $this->_read('data' . $var_name); |
168 | } |
169 | else |
170 | { |
171 | return ($this->_exists($var_name)) ? $this->vars[$var_name] : false; |
172 | } |
173 | } |
174 | |
175 | /** |
176 | * {@inheritDoc} |
177 | */ |
178 | function put($var_name, $var, $ttl = 31536000) |
179 | { |
180 | if ($var_name[0] == '_') |
181 | { |
182 | $this->_write('data' . $var_name, $var, time() + $ttl); |
183 | } |
184 | else |
185 | { |
186 | $this->vars[$var_name] = $var; |
187 | $this->var_expires[$var_name] = time() + $ttl; |
188 | $this->is_modified = true; |
189 | } |
190 | } |
191 | |
192 | /** |
193 | * {@inheritDoc} |
194 | */ |
195 | function purge() |
196 | { |
197 | parent::purge(); |
198 | $this->var_expires = array(); |
199 | } |
200 | |
201 | /** |
202 | * {@inheritDoc} |
203 | */ |
204 | function destroy($var_name, $table = '') |
205 | { |
206 | global $phpEx; |
207 | |
208 | if ($var_name == 'sql' && !empty($table)) |
209 | { |
210 | if (!is_array($table)) |
211 | { |
212 | $table = array($table); |
213 | } |
214 | |
215 | $dir = @opendir($this->cache_dir); |
216 | |
217 | if (!$dir) |
218 | { |
219 | return; |
220 | } |
221 | |
222 | while (($entry = readdir($dir)) !== false) |
223 | { |
224 | if (strpos($entry, 'sql_') !== 0) |
225 | { |
226 | continue; |
227 | } |
228 | |
229 | if (!($handle = @fopen($this->cache_dir . $entry, 'rb'))) |
230 | { |
231 | continue; |
232 | } |
233 | |
234 | // Skip the PHP header |
235 | fgets($handle); |
236 | |
237 | // Skip expiration |
238 | fgets($handle); |
239 | |
240 | // Grab the query, remove the LF |
241 | $query = substr(fgets($handle), 0, -1); |
242 | |
243 | fclose($handle); |
244 | |
245 | foreach ($table as $check_table) |
246 | { |
247 | // Better catch partial table names than no table names. ;) |
248 | if (strpos($query, $check_table) !== false) |
249 | { |
250 | $this->remove_file($this->cache_dir . $entry); |
251 | break; |
252 | } |
253 | } |
254 | } |
255 | closedir($dir); |
256 | |
257 | return; |
258 | } |
259 | |
260 | if (!$this->_exists($var_name)) |
261 | { |
262 | return; |
263 | } |
264 | |
265 | if ($var_name[0] == '_') |
266 | { |
267 | $this->remove_file($this->cache_dir . 'data' . $var_name . ".$phpEx", true); |
268 | } |
269 | else if (isset($this->vars[$var_name])) |
270 | { |
271 | $this->is_modified = true; |
272 | unset($this->vars[$var_name]); |
273 | unset($this->var_expires[$var_name]); |
274 | |
275 | // We save here to let the following cache hits succeed |
276 | $this->save(); |
277 | } |
278 | } |
279 | |
280 | /** |
281 | * {@inheritDoc} |
282 | */ |
283 | function _exists($var_name) |
284 | { |
285 | if ($var_name[0] == '_') |
286 | { |
287 | global $phpEx; |
288 | $var_name = $this->clean_varname($var_name); |
289 | return file_exists($this->cache_dir . 'data' . $var_name . ".$phpEx"); |
290 | } |
291 | else |
292 | { |
293 | if (!count($this->vars)) |
294 | { |
295 | $this->load(); |
296 | } |
297 | |
298 | if (!isset($this->var_expires[$var_name])) |
299 | { |
300 | return false; |
301 | } |
302 | |
303 | return (time() > $this->var_expires[$var_name]) ? false : isset($this->vars[$var_name]); |
304 | } |
305 | } |
306 | |
307 | /** |
308 | * {@inheritDoc} |
309 | */ |
310 | function sql_save(\phpbb\db\driver\driver_interface $db, $query, $query_result, $ttl) |
311 | { |
312 | // Remove extra spaces and tabs |
313 | $query = preg_replace('/[\n\r\s\t]+/', ' ', $query); |
314 | |
315 | $query_id = md5($query); |
316 | $this->sql_rowset[$query_id] = array(); |
317 | $this->sql_row_pointer[$query_id] = 0; |
318 | |
319 | while ($row = $db->sql_fetchrow($query_result)) |
320 | { |
321 | $this->sql_rowset[$query_id][] = $row; |
322 | } |
323 | $db->sql_freeresult($query_result); |
324 | |
325 | if ($this->_write('sql_' . $query_id, $this->sql_rowset[$query_id], $ttl + time(), $query)) |
326 | { |
327 | return $query_id; |
328 | } |
329 | |
330 | return $query_result; |
331 | } |
332 | |
333 | /** |
334 | * Cleanup when loading invalid data global file |
335 | * |
336 | * @param string $file Filename |
337 | * @param resource $handle |
338 | * |
339 | * @return void |
340 | */ |
341 | private function cleanup_invalid_data_global(string $file, $handle): void |
342 | { |
343 | if (is_resource($handle)) |
344 | { |
345 | fclose($handle); |
346 | } |
347 | |
348 | $this->vars = $this->var_expires = []; |
349 | $this->is_modified = false; |
350 | |
351 | $this->remove_file($file); |
352 | } |
353 | |
354 | /** |
355 | * {@inheritDoc} |
356 | */ |
357 | protected function _read(string $var) |
358 | { |
359 | global $phpEx; |
360 | |
361 | $filename = $this->clean_varname($var); |
362 | $file = "{$this->cache_dir}$filename.$phpEx"; |
363 | |
364 | $type = substr($filename, 0, strpos($filename, '_')); |
365 | |
366 | if (!file_exists($file)) |
367 | { |
368 | return false; |
369 | } |
370 | |
371 | if (!($handle = @fopen($file, 'rb'))) |
372 | { |
373 | return false; |
374 | } |
375 | |
376 | // Skip the PHP header |
377 | fgets($handle); |
378 | |
379 | if ($filename == 'data_global') |
380 | { |
381 | $this->vars = $this->var_expires = array(); |
382 | |
383 | $time = time(); |
384 | |
385 | while (($expires = (int) fgets($handle)) && !feof($handle)) |
386 | { |
387 | // Number of bytes of data |
388 | $bytes = substr(fgets($handle), 0, -1); |
389 | |
390 | if (!is_numeric($bytes) || ($bytes = (int) $bytes) === 0) |
391 | { |
392 | $this->cleanup_invalid_data_global($file, $handle); |
393 | |
394 | return false; |
395 | } |
396 | |
397 | if ($time >= $expires) |
398 | { |
399 | fseek($handle, $bytes, SEEK_CUR); |
400 | |
401 | continue; |
402 | } |
403 | |
404 | $var_name = substr(fgets($handle), 0, -1); |
405 | $data_length = $bytes - strlen($var_name); |
406 | |
407 | if ($data_length <= 0) |
408 | { |
409 | $this->cleanup_invalid_data_global($file, $handle); |
410 | |
411 | return false; |
412 | } |
413 | |
414 | // Read the length of bytes that consists of data. |
415 | $data = fread($handle, $data_length); |
416 | $data = @unserialize($data); |
417 | |
418 | // Don't use the data if it was invalid |
419 | if ($data !== false) |
420 | { |
421 | $this->vars[$var_name] = $data; |
422 | $this->var_expires[$var_name] = $expires; |
423 | } |
424 | |
425 | // Absorb the LF |
426 | fgets($handle); |
427 | } |
428 | |
429 | fclose($handle); |
430 | |
431 | $this->is_modified = false; |
432 | |
433 | return true; |
434 | } |
435 | else |
436 | { |
437 | $data = false; |
438 | $line = 0; |
439 | |
440 | while (($buffer = fgets($handle)) && !feof($handle)) |
441 | { |
442 | $buffer = substr($buffer, 0, -1); // Remove the LF |
443 | |
444 | // $buffer is only used to read integers |
445 | // if it is non numeric we have an invalid |
446 | // cache file, which we will now remove. |
447 | if (!is_numeric($buffer)) |
448 | { |
449 | break; |
450 | } |
451 | |
452 | if ($line == 0) |
453 | { |
454 | $expires = (int) $buffer; |
455 | |
456 | if (time() >= $expires) |
457 | { |
458 | break; |
459 | } |
460 | |
461 | if ($type == 'sql') |
462 | { |
463 | // Skip the query |
464 | fgets($handle); |
465 | } |
466 | } |
467 | else if ($line == 1) |
468 | { |
469 | $bytes = (int) $buffer; |
470 | |
471 | // Never should have 0 bytes |
472 | if (!$bytes) |
473 | { |
474 | break; |
475 | } |
476 | |
477 | // Grab the serialized data |
478 | $data = fread($handle, $bytes); |
479 | |
480 | // Read 1 byte, to trigger EOF |
481 | fread($handle, 1); |
482 | |
483 | if (!feof($handle)) |
484 | { |
485 | // Somebody tampered with our data |
486 | $data = false; |
487 | } |
488 | break; |
489 | } |
490 | else |
491 | { |
492 | // Something went wrong |
493 | break; |
494 | } |
495 | $line++; |
496 | } |
497 | fclose($handle); |
498 | |
499 | // unserialize if we got some data |
500 | $data = ($data !== false) ? @unserialize($data) : $data; |
501 | |
502 | if ($data === false) |
503 | { |
504 | $this->remove_file($file); |
505 | return false; |
506 | } |
507 | |
508 | return $data; |
509 | } |
510 | } |
511 | |
512 | /** |
513 | * Write cache data to a specified file |
514 | * |
515 | * 'data_global' is a special case and the generated format is different for this file: |
516 | * <code> |
517 | * <?php exit; ?> |
518 | * (expiration) |
519 | * (length of var and serialised data) |
520 | * (var) |
521 | * (serialised data) |
522 | * ... (repeat) |
523 | * </code> |
524 | * |
525 | * The other files have a similar format: |
526 | * <code> |
527 | * <?php exit; ?> |
528 | * (expiration) |
529 | * (query) [SQL files only] |
530 | * (length of serialised data) |
531 | * (serialised data) |
532 | * </code> |
533 | * |
534 | * @access private |
535 | * @param string $filename Filename to write |
536 | * @param mixed $data Data to store |
537 | * @param int $expires Timestamp when the data expires |
538 | * @param string $query Query when caching SQL queries |
539 | * @return bool True if the file was successfully created, otherwise false |
540 | */ |
541 | function _write($filename, $data = null, $expires = 0, $query = '') |
542 | { |
543 | global $phpEx; |
544 | |
545 | $filename = $this->clean_varname($filename); |
546 | $file = "{$this->cache_dir}$filename.$phpEx"; |
547 | |
548 | $lock = new \phpbb\lock\flock($file); |
549 | $lock->acquire(); |
550 | |
551 | if ($handle = @fopen($file, 'wb')) |
552 | { |
553 | // File header |
554 | fwrite($handle, '<' . '?php exit; ?' . '>'); |
555 | |
556 | if ($filename == 'data_global') |
557 | { |
558 | // Global data is a different format |
559 | foreach ($this->vars as $var => $data) |
560 | { |
561 | if (strpos($var, "\r") !== false || strpos($var, "\n") !== false) |
562 | { |
563 | // CR/LF would cause fgets() to read the cache file incorrectly |
564 | // do not cache test entries, they probably won't be read back |
565 | // the cache keys should really be alphanumeric with a few symbols. |
566 | continue; |
567 | } |
568 | $data = serialize($data); |
569 | |
570 | // Write out the expiration time |
571 | fwrite($handle, "\n" . $this->var_expires[$var] . "\n"); |
572 | |
573 | // Length of the remaining data for this var (ignoring two LF's) |
574 | fwrite($handle, strlen($data . $var) . "\n"); |
575 | fwrite($handle, $var . "\n"); |
576 | fwrite($handle, $data); |
577 | } |
578 | } |
579 | else |
580 | { |
581 | fwrite($handle, "\n" . $expires . "\n"); |
582 | |
583 | if (strpos($filename, 'sql_') === 0) |
584 | { |
585 | fwrite($handle, $query . "\n"); |
586 | } |
587 | $data = serialize($data); |
588 | |
589 | fwrite($handle, strlen($data) . "\n"); |
590 | fwrite($handle, $data); |
591 | } |
592 | |
593 | fclose($handle); |
594 | |
595 | if (function_exists('opcache_invalidate')) |
596 | { |
597 | @opcache_invalidate($file); |
598 | } |
599 | |
600 | try |
601 | { |
602 | $this->filesystem->phpbb_chmod($file, \phpbb\filesystem\filesystem_interface::CHMOD_READ | \phpbb\filesystem\filesystem_interface::CHMOD_WRITE); |
603 | } |
604 | catch (\phpbb\filesystem\exception\filesystem_exception $e) |
605 | { |
606 | // Do nothing |
607 | } |
608 | |
609 | $return_value = true; |
610 | } |
611 | else |
612 | { |
613 | $return_value = false; |
614 | } |
615 | |
616 | $lock->release(); |
617 | |
618 | return $return_value; |
619 | } |
620 | |
621 | /** |
622 | * Replace slashes in the file name |
623 | * |
624 | * @param string $varname name of a cache variable |
625 | * @return string $varname name that is safe to use as a filename |
626 | */ |
627 | protected function clean_varname($varname) |
628 | { |
629 | return str_replace(array('/', '\\'), '-', $varname); |
630 | } |
631 | } |