PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/library/Zend/Cache/Backend/File.php

https://bitbucket.org/baruffaldi/cms-php-bfcms
PHP | 716 lines | 451 code | 28 blank | 237 comment | 47 complexity | ea6066b669e2ede0da88711b98d4aa50 MD5 | raw file
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Cache
  17. * @subpackage Zend_Cache_Backend
  18. * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. */
  21. /**
  22. * @see Zend_Cache_Backend_Interface
  23. */
  24. require_once 'Zend/Cache/Backend/Interface.php';
  25. /**
  26. * @see Zend_Cache_Backend
  27. */
  28. require_once 'Zend/Cache/Backend.php';
  29. /**
  30. * @package Zend_Cache
  31. * @subpackage Zend_Cache_Backend
  32. * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
  33. * @license http://framework.zend.com/license/new-bsd New BSD License
  34. */
  35. class Zend_Cache_Backend_File extends Zend_Cache_Backend implements Zend_Cache_Backend_Interface
  36. {
  37. /**
  38. * Available options
  39. *
  40. * =====> (string) cache_dir :
  41. * - Directory where to put the cache files
  42. *
  43. * =====> (boolean) file_locking :
  44. * - Enable / disable file_locking
  45. * - Can avoid cache corruption under bad circumstances but it doesn't work on multithread
  46. * webservers and on NFS filesystems for example
  47. *
  48. * =====> (boolean) read_control :
  49. * - Enable / disable read control
  50. * - If enabled, a control key is embeded in cache file and this key is compared with the one
  51. * calculated after the reading.
  52. *
  53. * =====> (string) read_control_type :
  54. * - Type of read control (only if read control is enabled). Available values are :
  55. * 'md5' for a md5 hash control (best but slowest)
  56. * 'crc32' for a crc32 hash control (lightly less safe but faster, better choice)
  57. * 'adler32' for an adler32 hash control (excellent choice too, faster than crc32)
  58. * 'strlen' for a length only test (fastest)
  59. *
  60. * =====> (int) hashed_directory_level :
  61. * - Hashed directory level
  62. * - Set the hashed directory structure level. 0 means "no hashed directory
  63. * structure", 1 means "one level of directory", 2 means "two levels"...
  64. * This option can speed up the cache only when you have many thousands of
  65. * cache file. Only specific benchs can help you to choose the perfect value
  66. * for you. Maybe, 1 or 2 is a good start.
  67. *
  68. * =====> (int) hashed_directory_umask :
  69. * - Umask for hashed directory structure
  70. *
  71. * =====> (string) file_name_prefix :
  72. * - prefix for cache files
  73. * - be really carefull with this option because a too generic value in a system cache dir
  74. * (like /tmp) can cause disasters when cleaning the cache
  75. *
  76. * =====> (int) cache_file_umask :
  77. * - Umask for cache files
  78. *
  79. * =====> (int) metatadatas_array_max_size :
  80. * - max size for the metadatas array (don't change this value unless you
  81. * know what you are doing)
  82. *
  83. * @var array available options
  84. */
  85. protected $_options = array(
  86. 'cache_dir' => null,
  87. 'file_locking' => true,
  88. 'read_control' => true,
  89. 'read_control_type' => 'crc32',
  90. 'hashed_directory_level' => 0,
  91. 'hashed_directory_umask' => 0700,
  92. 'file_name_prefix' => 'zend_cache',
  93. 'cache_file_umask' => 0600,
  94. 'metadatas_array_max_size' => 100
  95. );
  96. /**
  97. * Array of metadatas (each item is an associative array)
  98. *
  99. * @var array
  100. */
  101. private $_metadatasArray = array();
  102. /**
  103. * Constructor
  104. *
  105. * @param array $options associative array of options
  106. * @throws Zend_Cache_Exception
  107. * @return void
  108. */
  109. public function __construct(array $options = array())
  110. {
  111. parent::__construct($options);
  112. if (!is_null($this->_options['cache_dir'])) { // particular case for this option
  113. $this->setCacheDir($this->_options['cache_dir']);
  114. } else {
  115. $this->setCacheDir(self::getTmpDir() . DIRECTORY_SEPARATOR, false);
  116. }
  117. if (isset($this->_options['file_name_prefix'])) { // particular case for this option
  118. if (!preg_match('~^[\w]+$~', $this->_options['file_name_prefix'])) {
  119. Zend_Cache::throwException('Invalid file_name_prefix : must use only [a-zA-A0-9_]');
  120. }
  121. }
  122. if ($this->_options['metadatas_array_max_size'] < 10) {
  123. Zend_Cache::throwException('Invalid metadatas_array_max_size, must be > 10');
  124. }
  125. }
  126. /**
  127. * Set the cache_dir (particular case of setOption() method)
  128. *
  129. * @param string $value
  130. * @param boolean $trailingSeparator If true, add a trailing separator is necessary
  131. * @throws Zend_Cache_Exception
  132. * @return void
  133. */
  134. public function setCacheDir($value, $trailingSeparator = true)
  135. {
  136. if (!is_dir($value)) {
  137. Zend_Cache::throwException('cache_dir must be a directory');
  138. }
  139. if (!is_writable($value)) {
  140. Zend_Cache::throwException('cache_dir is not writable');
  141. }
  142. if ($trailingSeparator) {
  143. // add a trailing DIRECTORY_SEPARATOR if necessary
  144. $value = rtrim(realpath($value), '\\/') . DIRECTORY_SEPARATOR;
  145. }
  146. $this->_options['cache_dir'] = $value;
  147. }
  148. /**
  149. * Test if a cache is available for the given id and (if yes) return it (false else)
  150. *
  151. * @param string $id cache id
  152. * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
  153. * @return string|false cached datas
  154. */
  155. public function load($id, $doNotTestCacheValidity = false)
  156. {
  157. if (!($this->_test($id, $doNotTestCacheValidity))) {
  158. // The cache is not hit !
  159. return false;
  160. }
  161. $metadatas = $this->_getMetadatas($id);
  162. $file = $this->_file($id);
  163. $data = $this->_fileGetContents($file);
  164. if ($this->_options['read_control']) {
  165. $hashData = $this->_hash($data, $this->_options['read_control_type']);
  166. $hashControl = $metadatas['hash'];
  167. if ($hashData != $hashControl) {
  168. // Problem detected by the read control !
  169. $this->_log('Zend_Cache_Backend_File::load() / read_control : stored hash and computed hash do not match');
  170. $this->remove($id);
  171. return false;
  172. }
  173. }
  174. return $data;
  175. }
  176. /**
  177. * Test if a cache is available or not (for the given id)
  178. *
  179. * @param string $id cache id
  180. * @return mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record
  181. */
  182. public function test($id)
  183. {
  184. clearstatcache();
  185. return $this->_test($id, false);
  186. }
  187. /**
  188. * Save some string datas into a cache record
  189. *
  190. * Note : $data is always "string" (serialization is done by the
  191. * core not by the backend)
  192. *
  193. * @param string $data Datas to cache
  194. * @param string $id Cache id
  195. * @param array $tags Array of strings, the cache record will be tagged by each string entry
  196. * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
  197. * @return boolean true if no problem
  198. */
  199. public function save($data, $id, $tags = array(), $specificLifetime = false)
  200. {
  201. clearstatcache();
  202. $file = $this->_file($id);
  203. $path = $this->_path($id);
  204. $firstTry = true;
  205. $result = false;
  206. if ($this->_options['hashed_directory_level'] > 0) {
  207. if (!is_writable($path)) {
  208. // maybe, we just have to build the directory structure
  209. @mkdir($this->_path($id), $this->_options['hashed_directory_umask'], true);
  210. @chmod($this->_path($id), $this->_options['hashed_directory_umask']); // see #ZF-320 (this line is required in some configurations)
  211. }
  212. if (!is_writable($path)) {
  213. return false;
  214. }
  215. }
  216. if ($this->_options['read_control']) {
  217. $hash = $this->_hash($data, $this->_options['read_control_type']);
  218. } else {
  219. $hash = '';
  220. }
  221. $metadatas = array(
  222. 'hash' => $hash,
  223. 'mtime' => time(),
  224. 'expire' => $this->_expireTime($this->getLifetime($specificLifetime)),
  225. 'tags' => $tags
  226. );
  227. $res = $this->_setMetadatas($id, $metadatas);
  228. if (!$res) {
  229. // FIXME : log
  230. return false;
  231. }
  232. $res = $this->_filePutContents($file, $data);
  233. return $res;
  234. }
  235. /**
  236. * Remove a cache record
  237. *
  238. * @param string $id cache id
  239. * @return boolean true if no problem
  240. */
  241. public function remove($id)
  242. {
  243. $file = $this->_file($id);
  244. return ($this->_delMetadatas($id) && $this->_remove($file));
  245. }
  246. /**
  247. * Clean some cache records
  248. *
  249. * Available modes are :
  250. * 'all' (default) => remove all cache entries ($tags is not used)
  251. * 'old' => remove too old cache entries ($tags is not used)
  252. * 'matchingTag' => remove cache entries matching all given tags
  253. * ($tags can be an array of strings or a single string)
  254. * 'notMatchingTag' => remove cache entries not matching one of the given tags
  255. * ($tags can be an array of strings or a single string)
  256. *
  257. * @param string $mode clean mode
  258. * @param tags array $tags array of tags
  259. * @return boolean true if no problem
  260. */
  261. public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
  262. {
  263. // We use this private method to hide the recursive stuff
  264. clearstatcache();
  265. return $this->_clean($this->_options['cache_dir'], $mode, $tags);
  266. }
  267. /**
  268. * PUBLIC METHOD FOR UNIT TESTING ONLY !
  269. *
  270. * Force a cache record to expire
  271. *
  272. * @param string $id cache id
  273. */
  274. public function ___expire($id)
  275. {
  276. $metadatas = $this->_getMetadatas($id);
  277. if ($metadatas) {
  278. $metadatas['expire'] = 1;
  279. $this->_setMetadatas($id, $metadatas);
  280. }
  281. }
  282. /**
  283. * Get a metadatas record
  284. *
  285. * @param string $id Cache id
  286. * @return array|false Associative array of metadatas
  287. */
  288. private function _getMetadatas($id)
  289. {
  290. if (isset($this->_metadatasArray[$id])) {
  291. return $this->_metadatasArray[$id];
  292. } else {
  293. $metadatas = $this->_loadMetadatas($id);
  294. if (!$metadatas) {
  295. return false;
  296. }
  297. $this->_setMetadatas($id, $metadatas, false);
  298. return $metadatas;
  299. }
  300. }
  301. /**
  302. * Set a metadatas record
  303. *
  304. * @param string $id Cache id
  305. * @param array $metadatas Associative array of metadatas
  306. * @param boolean $save optional pass false to disable saving to file
  307. * @return boolean True if no problem
  308. */
  309. private function _setMetadatas($id, $metadatas, $save = true)
  310. {
  311. if (count($this->_metadatasArray) >= $this->_options['metadatas_array_max_size']) {
  312. $n = (int) ($this->_options['metadatas_array_max_size'] / 10);
  313. $this->_metadatasArray = array_slice($this->_metadatasArray, $n);
  314. }
  315. if ($save) {
  316. $result = $this->_saveMetadatas($id, $metadatas);
  317. if (!$result) {
  318. return false;
  319. }
  320. }
  321. $this->_metadatasArray[$id] = $metadatas;
  322. return true;
  323. }
  324. /**
  325. * Drop a metadata record
  326. *
  327. * @param string $id Cache id
  328. * @return boolean True if no problem
  329. */
  330. private function _delMetadatas($id)
  331. {
  332. if (isset($this->_metadatasArray[$id])) {
  333. unset($this->_metadatasArray[$id]);
  334. }
  335. $file = $this->_metadatasFile($id);
  336. return $this->_remove($file);
  337. }
  338. /**
  339. * Clear the metadatas array
  340. *
  341. * @return void
  342. */
  343. private function _cleanMetadatas()
  344. {
  345. $this->_metadatasArray = array();
  346. }
  347. /**
  348. * Load metadatas from disk
  349. *
  350. * @param string $id Cache id
  351. * @return array|false Metadatas associative array
  352. */
  353. private function _loadMetadatas($id)
  354. {
  355. $file = $this->_metadatasFile($id);
  356. $result = $this->_fileGetContents($file);
  357. if (!$result) {
  358. return false;
  359. }
  360. $tmp = @unserialize($result);
  361. return $tmp;
  362. }
  363. /**
  364. * Save metadatas to disk
  365. *
  366. * @param string $id Cache id
  367. * @param array $metadatas Associative array
  368. * @return boolean True if no problem
  369. */
  370. private function _saveMetadatas($id, $metadatas)
  371. {
  372. $file = $this->_metadatasFile($id);
  373. $result = $this->_filePutContents($file, serialize($metadatas));
  374. if (!$result) {
  375. return false;
  376. }
  377. return true;
  378. }
  379. /**
  380. * Make and return a file name (with path) for metadatas
  381. *
  382. * @param string $id Cache id
  383. * @return string Metadatas file name (with path)
  384. */
  385. private function _metadatasFile($id)
  386. {
  387. $path = $this->_path($id);
  388. $fileName = $this->_idToFileName('internal-metadatas---' . $id);
  389. return $path . $fileName;
  390. }
  391. /**
  392. * Check if the given filename is a metadatas one
  393. *
  394. * @param string $fileName File name
  395. * @return boolean True if it's a metadatas one
  396. */
  397. private function _isMetadatasFile($fileName)
  398. {
  399. $id = $this->_fileNameToId($fileName);
  400. if (substr($id, 0, 21) == 'internal-metadatas---') {
  401. return true;
  402. } else {
  403. return false;
  404. }
  405. }
  406. /**
  407. * Remove a file
  408. *
  409. * If we can't remove the file (because of locks or any problem), we will touch
  410. * the file to invalidate it
  411. *
  412. * @param string $file Complete file path
  413. * @return boolean True if ok
  414. */
  415. private function _remove($file)
  416. {
  417. if (!is_file($file)) {
  418. return false;
  419. }
  420. if (!@unlink($file)) {
  421. # we can't remove the file (because of locks or any problem)
  422. $this->_log("Zend_Cache_Backend_File::_remove() : we can't remove $file");
  423. return false;
  424. }
  425. return true;
  426. }
  427. /**
  428. * Clean some cache records (private method used for recursive stuff)
  429. *
  430. * Available modes are :
  431. * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
  432. * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
  433. * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
  434. * ($tags can be an array of strings or a single string)
  435. * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
  436. * ($tags can be an array of strings or a single string)
  437. *
  438. * @param string $dir Directory to clean
  439. * @param string $mode Clean mode
  440. * @param array $tags Array of tags
  441. * @throws Zend_Cache_Exception
  442. * @return boolean True if no problem
  443. */
  444. private function _clean($dir, $mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
  445. {
  446. if (!is_dir($dir)) {
  447. return false;
  448. }
  449. $result = true;
  450. $prefix = $this->_options['file_name_prefix'];
  451. $glob = @glob($dir . $prefix . '--*');
  452. if ($glob === false) {
  453. return true;
  454. }
  455. foreach ($glob as $file) {
  456. if (is_file($file)) {
  457. $fileName = basename($file);
  458. if ($this->_isMetadatasFile($fileName)) {
  459. // in CLEANING_MODE_ALL, we drop anything, even remainings old metadatas files
  460. if ($mode != Zend_Cache::CLEANING_MODE_ALL) {
  461. continue;
  462. }
  463. }
  464. $id = $this->_fileNameToId($fileName);
  465. $metadatas = $this->_getMetadatas($id);
  466. if ($metadatas === FALSE) {
  467. $metadatas = array('expire' => 1, 'tags' => array());
  468. }
  469. switch ($mode) {
  470. case Zend_Cache::CLEANING_MODE_ALL:
  471. $res = $this->remove($id);
  472. if (!$res) {
  473. // in this case only, we accept a problem with the metadatas file drop
  474. $res = $this->_remove($file);
  475. }
  476. $result = $result && $res;
  477. break;
  478. case Zend_Cache::CLEANING_MODE_OLD:
  479. if (time() > $metadatas['expire']) {
  480. $result = ($result) && ($this->remove($id));
  481. }
  482. break;
  483. case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  484. $matching = true;
  485. foreach ($tags as $tag) {
  486. if (!in_array($tag, $metadatas['tags'])) {
  487. $matching = false;
  488. break;
  489. }
  490. }
  491. if ($matching) {
  492. $result = ($result) && ($this->remove($id));
  493. }
  494. break;
  495. case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  496. $matching = false;
  497. foreach ($tags as $tag) {
  498. if (in_array($tag, $metadatas['tags'])) {
  499. $matching = true;
  500. break;
  501. }
  502. }
  503. if (!$matching) {
  504. $result = ($result) && $this->remove($id);
  505. }
  506. break;
  507. default:
  508. Zend_Cache::throwException('Invalid mode for clean() method');
  509. break;
  510. }
  511. }
  512. if ((is_dir($file)) and ($this->_options['hashed_directory_level']>0)) {
  513. // Recursive call
  514. $result = ($result) && ($this->_clean($file . DIRECTORY_SEPARATOR, $mode, $tags));
  515. if ($mode=='all') {
  516. // if mode=='all', we try to drop the structure too
  517. @rmdir($file);
  518. }
  519. }
  520. }
  521. return $result;
  522. }
  523. /**
  524. * Compute & return the expire time
  525. *
  526. * @return int expire time (unix timestamp)
  527. */
  528. private function _expireTime($lifetime)
  529. {
  530. if (is_null($lifetime)) {
  531. return 9999999999;
  532. }
  533. return time() + $lifetime;
  534. }
  535. /**
  536. * Make a control key with the string containing datas
  537. *
  538. * @param string $data Data
  539. * @param string $controlType Type of control 'md5', 'crc32' or 'strlen'
  540. * @throws Zend_Cache_Exception
  541. * @return string Control key
  542. */
  543. private function _hash($data, $controlType)
  544. {
  545. switch ($controlType) {
  546. case 'md5':
  547. return md5($data);
  548. case 'crc32':
  549. return crc32($data);
  550. case 'strlen':
  551. return strlen($data);
  552. case 'adler32':
  553. return hash('adler32', $data);
  554. default:
  555. Zend_Cache::throwException("Incorrect hash function : $controlType");
  556. }
  557. }
  558. /**
  559. * Transform a cache id into a file name and return it
  560. *
  561. * @param string $id Cache id
  562. * @return string File name
  563. */
  564. private function _idToFileName($id)
  565. {
  566. $prefix = $this->_options['file_name_prefix'];
  567. $result = $prefix . '---' . $id;
  568. return $result;
  569. }
  570. /**
  571. * Make and return a file name (with path)
  572. *
  573. * @param string $id Cache id
  574. * @return string File name (with path)
  575. */
  576. private function _file($id)
  577. {
  578. $path = $this->_path($id);
  579. $fileName = $this->_idToFileName($id);
  580. return $path . $fileName;
  581. }
  582. /**
  583. * Return the complete directory path of a filename (including hashedDirectoryStructure)
  584. *
  585. * @param string $id Cache id
  586. * @return string Complete directory path
  587. */
  588. private function _path($id)
  589. {
  590. $root = $this->_options['cache_dir'];
  591. $prefix = $this->_options['file_name_prefix'];
  592. if ($this->_options['hashed_directory_level']>0) {
  593. $hash = hash('adler32', $id);
  594. for ($i=0 ; $i < $this->_options['hashed_directory_level'] ; $i++) {
  595. $root = $root . $prefix . '--' . substr($hash, 0, $i + 1) . DIRECTORY_SEPARATOR;
  596. }
  597. }
  598. return $root;
  599. }
  600. /**
  601. * Test if the given cache id is available (and still valid as a cache record)
  602. *
  603. * @param string $id Cache id
  604. * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
  605. * @return boolean|mixed false (a cache is not available) or "last modified" timestamp (int) of the available cache record
  606. */
  607. private function _test($id, $doNotTestCacheValidity)
  608. {
  609. $metadatas = $this->_getMetadatas($id);
  610. if (!$metadatas) {
  611. return false;
  612. }
  613. if ($doNotTestCacheValidity || (time() <= $metadatas['expire'])) {
  614. return $metadatas['mtime'];
  615. }
  616. return false;
  617. }
  618. /**
  619. * Return the file content of the given file
  620. *
  621. * @param string $file File complete path
  622. * @return string File content (or false if problem)
  623. */
  624. private function _fileGetContents($file)
  625. {
  626. $result = false;
  627. if (!is_file($file)) {
  628. return false;
  629. }
  630. if (function_exists('get_magic_quotes_runtime')) {
  631. $mqr = @get_magic_quotes_runtime();
  632. @set_magic_quotes_runtime(0);
  633. }
  634. $f = @fopen($file, 'rb');
  635. if ($f) {
  636. if ($this->_options['file_locking']) @flock($f, LOCK_SH);
  637. $result = stream_get_contents($f);
  638. if ($this->_options['file_locking']) @flock($f, LOCK_UN);
  639. @fclose($f);
  640. }
  641. if (function_exists('set_magic_quotes_runtime')) {
  642. @set_magic_quotes_runtime($mqr);
  643. }
  644. return $result;
  645. }
  646. /**
  647. * Put the given string into the given file
  648. *
  649. * @param string $file File complete path
  650. * @param string $string String to put in file
  651. * @return boolean true if no problem
  652. */
  653. private function _filePutContents($file, $string)
  654. {
  655. $result = false;
  656. $f = @fopen($file, 'ab+');
  657. if ($f) {
  658. if ($this->_options['file_locking']) @flock($f, LOCK_EX);
  659. fseek($f, 0);
  660. ftruncate($f, 0);
  661. $tmp = @fwrite($f, $string);
  662. if (!($tmp === FALSE)) {
  663. $result = true;
  664. }
  665. @fclose($f);
  666. }
  667. @chmod($file, $this->_options['cache_file_umask']);
  668. return $result;
  669. }
  670. /**
  671. * Transform a file name into cache id and return it
  672. *
  673. * @param string $fileName File name
  674. * @return string Cache id
  675. */
  676. private function _fileNameToId($fileName)
  677. {
  678. $prefix = $this->_options['file_name_prefix'];
  679. return preg_replace('~^' . $prefix . '---(.*)$~', '$1', $fileName);
  680. }
  681. }