Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.28% covered (warning)
72.28%
146 / 202
46.67% covered (danger)
46.67%
7 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
file
72.28% covered (warning)
72.28%
146 / 202
46.67% covered (danger)
46.67%
7 / 15
225.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 load
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unload
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 save
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
6.00
 tidy
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
110
 get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 put
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 purge
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 destroy
58.06% covered (warning)
58.06%
18 / 31
0.00% covered (danger)
0.00%
0 / 1
25.46
 _exists
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 sql_save
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 cleanup_invalid_data_global
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 _read
88.89% covered (warning)
88.89%
56 / 63
0.00% covered (danger)
0.00%
0 / 1
22.66
 _write
87.10% covered (warning)
87.10%
27 / 31
0.00% covered (danger)
0.00%
0 / 1
9.17
 clean_varname
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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
14namespace phpbb\cache\driver;
15
16/**
17* ACM File Based Caching
18*/
19class 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}