PageRenderTime 101ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/phpBB/phpbb/cache/driver/file.php

http://github.com/phpbb/phpbb
PHP | 613 lines | 381 code | 103 blank | 129 comment | 68 complexity | 104e67b0908bbc3b9a17d61fa843eb0c MD5 | raw file
Possible License(s): GPL-3.0, AGPL-1.0
  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. namespace phpbb\cache\driver;
  14. /**
  15. * ACM File Based Caching
  16. */
  17. class file extends \phpbb\cache\driver\base
  18. {
  19. var $var_expires = array();
  20. /**
  21. * @var \phpbb\filesystem\filesystem_interface
  22. */
  23. protected $filesystem;
  24. /**
  25. * Set cache path
  26. *
  27. * @param string $cache_dir Define the path to the cache directory (default: $phpbb_root_path . 'cache/')
  28. */
  29. function __construct($cache_dir = null)
  30. {
  31. global $phpbb_container;
  32. $this->cache_dir = !is_null($cache_dir) ? $cache_dir : $phpbb_container->getParameter('core.cache_dir');
  33. $this->filesystem = new \phpbb\filesystem\filesystem();
  34. if (!is_dir($this->cache_dir))
  35. {
  36. @mkdir($this->cache_dir, 0777, true);
  37. }
  38. }
  39. /**
  40. * {@inheritDoc}
  41. */
  42. function load()
  43. {
  44. return $this->_read('data_global');
  45. }
  46. /**
  47. * {@inheritDoc}
  48. */
  49. function unload()
  50. {
  51. parent::unload();
  52. unset($this->var_expires);
  53. $this->var_expires = array();
  54. }
  55. /**
  56. * {@inheritDoc}
  57. */
  58. function save()
  59. {
  60. if (!$this->is_modified)
  61. {
  62. return;
  63. }
  64. global $phpEx;
  65. if (!$this->_write('data_global'))
  66. {
  67. // Now, this occurred how often? ... phew, just tell the user then...
  68. if (!$this->filesystem->is_writable($this->cache_dir))
  69. {
  70. // We need to use die() here, because else we may encounter an infinite loop (the message handler calls $cache->unload())
  71. die('Fatal: ' . $this->cache_dir . ' is NOT writable.');
  72. exit;
  73. }
  74. die('Fatal: Not able to open ' . $this->cache_dir . 'data_global.' . $phpEx);
  75. exit;
  76. }
  77. $this->is_modified = false;
  78. }
  79. /**
  80. * {@inheritDoc}
  81. */
  82. function tidy()
  83. {
  84. global $config, $phpEx;
  85. $dir = @opendir($this->cache_dir);
  86. if (!$dir)
  87. {
  88. return;
  89. }
  90. $time = time();
  91. while (($entry = readdir($dir)) !== false)
  92. {
  93. if (!preg_match('/^(sql_|data_(?!global))/', $entry))
  94. {
  95. continue;
  96. }
  97. if (!($handle = @fopen($this->cache_dir . $entry, 'rb')))
  98. {
  99. continue;
  100. }
  101. // Skip the PHP header
  102. fgets($handle);
  103. // Skip expiration
  104. $expires = (int) fgets($handle);
  105. fclose($handle);
  106. if ($time >= $expires)
  107. {
  108. $this->remove_file($this->cache_dir . $entry);
  109. }
  110. }
  111. closedir($dir);
  112. if (file_exists($this->cache_dir . 'data_global.' . $phpEx))
  113. {
  114. if (!count($this->vars))
  115. {
  116. $this->load();
  117. }
  118. foreach ($this->var_expires as $var_name => $expires)
  119. {
  120. if ($time >= $expires)
  121. {
  122. $this->destroy($var_name);
  123. }
  124. }
  125. }
  126. $config->set('cache_last_gc', time(), false);
  127. }
  128. /**
  129. * {@inheritDoc}
  130. */
  131. function get($var_name)
  132. {
  133. if ($var_name[0] == '_')
  134. {
  135. if (!$this->_exists($var_name))
  136. {
  137. return false;
  138. }
  139. return $this->_read('data' . $var_name);
  140. }
  141. else
  142. {
  143. return ($this->_exists($var_name)) ? $this->vars[$var_name] : false;
  144. }
  145. }
  146. /**
  147. * {@inheritDoc}
  148. */
  149. function put($var_name, $var, $ttl = 31536000)
  150. {
  151. if ($var_name[0] == '_')
  152. {
  153. $this->_write('data' . $var_name, $var, time() + $ttl);
  154. }
  155. else
  156. {
  157. $this->vars[$var_name] = $var;
  158. $this->var_expires[$var_name] = time() + $ttl;
  159. $this->is_modified = true;
  160. }
  161. }
  162. /**
  163. * {@inheritDoc}
  164. */
  165. function purge()
  166. {
  167. parent::purge();
  168. $this->var_expires = array();
  169. }
  170. /**
  171. * {@inheritDoc}
  172. */
  173. function destroy($var_name, $table = '')
  174. {
  175. global $phpEx;
  176. if ($var_name == 'sql' && !empty($table))
  177. {
  178. if (!is_array($table))
  179. {
  180. $table = array($table);
  181. }
  182. $dir = @opendir($this->cache_dir);
  183. if (!$dir)
  184. {
  185. return;
  186. }
  187. while (($entry = readdir($dir)) !== false)
  188. {
  189. if (strpos($entry, 'sql_') !== 0)
  190. {
  191. continue;
  192. }
  193. if (!($handle = @fopen($this->cache_dir . $entry, 'rb')))
  194. {
  195. continue;
  196. }
  197. // Skip the PHP header
  198. fgets($handle);
  199. // Skip expiration
  200. fgets($handle);
  201. // Grab the query, remove the LF
  202. $query = substr(fgets($handle), 0, -1);
  203. fclose($handle);
  204. foreach ($table as $check_table)
  205. {
  206. // Better catch partial table names than no table names. ;)
  207. if (strpos($query, $check_table) !== false)
  208. {
  209. $this->remove_file($this->cache_dir . $entry);
  210. break;
  211. }
  212. }
  213. }
  214. closedir($dir);
  215. return;
  216. }
  217. if (!$this->_exists($var_name))
  218. {
  219. return;
  220. }
  221. if ($var_name[0] == '_')
  222. {
  223. $this->remove_file($this->cache_dir . 'data' . $var_name . ".$phpEx", true);
  224. }
  225. else if (isset($this->vars[$var_name]))
  226. {
  227. $this->is_modified = true;
  228. unset($this->vars[$var_name]);
  229. unset($this->var_expires[$var_name]);
  230. // We save here to let the following cache hits succeed
  231. $this->save();
  232. }
  233. }
  234. /**
  235. * {@inheritDoc}
  236. */
  237. function _exists($var_name)
  238. {
  239. if ($var_name[0] == '_')
  240. {
  241. global $phpEx;
  242. $var_name = $this->clean_varname($var_name);
  243. return file_exists($this->cache_dir . 'data' . $var_name . ".$phpEx");
  244. }
  245. else
  246. {
  247. if (!count($this->vars))
  248. {
  249. $this->load();
  250. }
  251. if (!isset($this->var_expires[$var_name]))
  252. {
  253. return false;
  254. }
  255. return (time() > $this->var_expires[$var_name]) ? false : isset($this->vars[$var_name]);
  256. }
  257. }
  258. /**
  259. * {@inheritDoc}
  260. */
  261. function sql_save(\phpbb\db\driver\driver_interface $db, $query, $query_result, $ttl)
  262. {
  263. // Remove extra spaces and tabs
  264. $query = preg_replace('/[\n\r\s\t]+/', ' ', $query);
  265. $query_id = md5($query);
  266. $this->sql_rowset[$query_id] = array();
  267. $this->sql_row_pointer[$query_id] = 0;
  268. while ($row = $db->sql_fetchrow($query_result))
  269. {
  270. $this->sql_rowset[$query_id][] = $row;
  271. }
  272. $db->sql_freeresult($query_result);
  273. if ($this->_write('sql_' . $query_id, $this->sql_rowset[$query_id], $ttl + time(), $query))
  274. {
  275. return $query_id;
  276. }
  277. return $query_result;
  278. }
  279. /**
  280. * Read cached data from a specified file
  281. *
  282. * @access private
  283. * @param string $filename Filename to write
  284. * @return mixed False if an error was encountered, otherwise the data type of the cached data
  285. */
  286. function _read($filename)
  287. {
  288. global $phpEx;
  289. $filename = $this->clean_varname($filename);
  290. $file = "{$this->cache_dir}$filename.$phpEx";
  291. $type = substr($filename, 0, strpos($filename, '_'));
  292. if (!file_exists($file))
  293. {
  294. return false;
  295. }
  296. if (!($handle = @fopen($file, 'rb')))
  297. {
  298. return false;
  299. }
  300. // Skip the PHP header
  301. fgets($handle);
  302. if ($filename == 'data_global')
  303. {
  304. $this->vars = $this->var_expires = array();
  305. $time = time();
  306. while (($expires = (int) fgets($handle)) && !feof($handle))
  307. {
  308. // Number of bytes of data
  309. $bytes = substr(fgets($handle), 0, -1);
  310. if (!is_numeric($bytes) || ($bytes = (int) $bytes) === 0)
  311. {
  312. // We cannot process the file without a valid number of bytes
  313. // so we discard it
  314. fclose($handle);
  315. $this->vars = $this->var_expires = array();
  316. $this->is_modified = false;
  317. $this->remove_file($file);
  318. return false;
  319. }
  320. if ($time >= $expires)
  321. {
  322. fseek($handle, $bytes, SEEK_CUR);
  323. continue;
  324. }
  325. $var_name = substr(fgets($handle), 0, -1);
  326. // Read the length of bytes that consists of data.
  327. $data = fread($handle, $bytes - strlen($var_name));
  328. $data = @unserialize($data);
  329. // Don't use the data if it was invalid
  330. if ($data !== false)
  331. {
  332. $this->vars[$var_name] = $data;
  333. $this->var_expires[$var_name] = $expires;
  334. }
  335. // Absorb the LF
  336. fgets($handle);
  337. }
  338. fclose($handle);
  339. $this->is_modified = false;
  340. return true;
  341. }
  342. else
  343. {
  344. $data = false;
  345. $line = 0;
  346. while (($buffer = fgets($handle)) && !feof($handle))
  347. {
  348. $buffer = substr($buffer, 0, -1); // Remove the LF
  349. // $buffer is only used to read integers
  350. // if it is non numeric we have an invalid
  351. // cache file, which we will now remove.
  352. if (!is_numeric($buffer))
  353. {
  354. break;
  355. }
  356. if ($line == 0)
  357. {
  358. $expires = (int) $buffer;
  359. if (time() >= $expires)
  360. {
  361. break;
  362. }
  363. if ($type == 'sql')
  364. {
  365. // Skip the query
  366. fgets($handle);
  367. }
  368. }
  369. else if ($line == 1)
  370. {
  371. $bytes = (int) $buffer;
  372. // Never should have 0 bytes
  373. if (!$bytes)
  374. {
  375. break;
  376. }
  377. // Grab the serialized data
  378. $data = fread($handle, $bytes);
  379. // Read 1 byte, to trigger EOF
  380. fread($handle, 1);
  381. if (!feof($handle))
  382. {
  383. // Somebody tampered with our data
  384. $data = false;
  385. }
  386. break;
  387. }
  388. else
  389. {
  390. // Something went wrong
  391. break;
  392. }
  393. $line++;
  394. }
  395. fclose($handle);
  396. // unserialize if we got some data
  397. $data = ($data !== false) ? @unserialize($data) : $data;
  398. if ($data === false)
  399. {
  400. $this->remove_file($file);
  401. return false;
  402. }
  403. return $data;
  404. }
  405. }
  406. /**
  407. * Write cache data to a specified file
  408. *
  409. * 'data_global' is a special case and the generated format is different for this file:
  410. * <code>
  411. * <?php exit; ?>
  412. * (expiration)
  413. * (length of var and serialised data)
  414. * (var)
  415. * (serialised data)
  416. * ... (repeat)
  417. * </code>
  418. *
  419. * The other files have a similar format:
  420. * <code>
  421. * <?php exit; ?>
  422. * (expiration)
  423. * (query) [SQL files only]
  424. * (length of serialised data)
  425. * (serialised data)
  426. * </code>
  427. *
  428. * @access private
  429. * @param string $filename Filename to write
  430. * @param mixed $data Data to store
  431. * @param int $expires Timestamp when the data expires
  432. * @param string $query Query when caching SQL queries
  433. * @return bool True if the file was successfully created, otherwise false
  434. */
  435. function _write($filename, $data = null, $expires = 0, $query = '')
  436. {
  437. global $phpEx;
  438. $filename = $this->clean_varname($filename);
  439. $file = "{$this->cache_dir}$filename.$phpEx";
  440. $lock = new \phpbb\lock\flock($file);
  441. $lock->acquire();
  442. if ($handle = @fopen($file, 'wb'))
  443. {
  444. // File header
  445. fwrite($handle, '<' . '?php exit; ?' . '>');
  446. if ($filename == 'data_global')
  447. {
  448. // Global data is a different format
  449. foreach ($this->vars as $var => $data)
  450. {
  451. if (strpos($var, "\r") !== false || strpos($var, "\n") !== false)
  452. {
  453. // CR/LF would cause fgets() to read the cache file incorrectly
  454. // do not cache test entries, they probably won't be read back
  455. // the cache keys should really be alphanumeric with a few symbols.
  456. continue;
  457. }
  458. $data = serialize($data);
  459. // Write out the expiration time
  460. fwrite($handle, "\n" . $this->var_expires[$var] . "\n");
  461. // Length of the remaining data for this var (ignoring two LF's)
  462. fwrite($handle, strlen($data . $var) . "\n");
  463. fwrite($handle, $var . "\n");
  464. fwrite($handle, $data);
  465. }
  466. }
  467. else
  468. {
  469. fwrite($handle, "\n" . $expires . "\n");
  470. if (strpos($filename, 'sql_') === 0)
  471. {
  472. fwrite($handle, $query . "\n");
  473. }
  474. $data = serialize($data);
  475. fwrite($handle, strlen($data) . "\n");
  476. fwrite($handle, $data);
  477. }
  478. fclose($handle);
  479. if (function_exists('opcache_invalidate'))
  480. {
  481. @opcache_invalidate($file);
  482. }
  483. try
  484. {
  485. $this->filesystem->phpbb_chmod($file, \phpbb\filesystem\filesystem_interface::CHMOD_READ | \phpbb\filesystem\filesystem_interface::CHMOD_WRITE);
  486. }
  487. catch (\phpbb\filesystem\exception\filesystem_exception $e)
  488. {
  489. // Do nothing
  490. }
  491. $return_value = true;
  492. }
  493. else
  494. {
  495. $return_value = false;
  496. }
  497. $lock->release();
  498. return $return_value;
  499. }
  500. /**
  501. * Replace slashes in the file name
  502. *
  503. * @param string $varname name of a cache variable
  504. * @return string $varname name that is safe to use as a filename
  505. */
  506. protected function clean_varname($varname)
  507. {
  508. return str_replace(array('/', '\\'), '-', $varname);
  509. }
  510. }