2
0

Static.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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-2009 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. * @version $Id: BlackHole.php 17867 2009-08-28 09:42:11Z yoshida@zend.co.jp $
  21. */
  22. /**
  23. * @see Zend_Cache_Backend_Interface
  24. */
  25. require_once 'Zend/Cache/Backend/Interface.php';
  26. /**
  27. * @see Zend_Cache_Backend
  28. */
  29. require_once 'Zend/Cache/Backend.php';
  30. /**
  31. * @package Zend_Cache
  32. * @subpackage Zend_Cache_Backend
  33. * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
  34. * @license http://framework.zend.com/license/new-bsd New BSD License
  35. */
  36. class Zend_Cache_Backend_Static
  37. extends Zend_Cache_Backend
  38. implements Zend_Cache_Backend_Interface
  39. {
  40. const INNER_CACHE_NAME = 'zend_cache_backend_static_tagcache';
  41. /**
  42. * Static backend options
  43. * @var array
  44. */
  45. protected $_options = array(
  46. 'public_dir' => null,
  47. 'sub_dir' => 'html',
  48. 'file_extension' => '.html',
  49. 'index_filename' => 'index',
  50. 'file_locking' => true,
  51. 'cache_file_umask' => 0600,
  52. 'debug_header' => false,
  53. 'tag_cache' => null,
  54. );
  55. /**
  56. * Cache for handling tags
  57. * @var Zend_Cache_Core
  58. */
  59. protected $_tagCache = null;
  60. /**
  61. * Tagged items
  62. * @var array
  63. */
  64. protected $_tagged = null;
  65. /**
  66. * Interceptor child method to handle the case where an Inner
  67. * Cache object is being set since it's not supported by the
  68. * standard backend interface
  69. *
  70. * @param string $name
  71. * @param mixed $value
  72. * @return Zend_Cache_Backend_Static
  73. */
  74. public function setOption($name, $value)
  75. {
  76. if ($name == 'tag_cache') {
  77. $this->setInnerCache($value);
  78. } else {
  79. parent::setOption($name, $value);
  80. }
  81. return $this;
  82. }
  83. /**
  84. * Retrieve any option via interception of the parent's statically held
  85. * options including the local option for a tag cache.
  86. *
  87. * @param string $name
  88. * @return mixed
  89. */
  90. public function getOption($name)
  91. {
  92. if ($name == 'tag_cache') {
  93. return $this->getInnerCache();
  94. } else {
  95. return parent::getOption($name);
  96. }
  97. }
  98. /**
  99. * Test if a cache is available for the given id and (if yes) return it (false else)
  100. *
  101. * Note : return value is always "string" (unserialization is done by the core not by the backend)
  102. *
  103. * @param string $id Cache id
  104. * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
  105. * @return string|false cached datas
  106. */
  107. public function load($id, $doNotTestCacheValidity = false)
  108. {
  109. if (empty($id)) {
  110. $id = $this->_detectId();
  111. }
  112. if (!$this->_verifyPath($id)) {
  113. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  114. }
  115. if ($doNotTestCacheValidity) {
  116. $this->_log("Zend_Cache_Backend_Static::load() : \$doNotTestCacheValidity=true is unsupported by the Static backend");
  117. }
  118. $fileName = basename($id);
  119. if (empty($fileName)) {
  120. $fileName = $this->_options['index_filename'];
  121. }
  122. $pathName = $this->_options['public_dir'] . dirname($id);
  123. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  124. if (file_exists($file)) {
  125. $content = file_get_contents($file);
  126. return $content;
  127. }
  128. return false;
  129. }
  130. /**
  131. * Test if a cache is available or not (for the given id)
  132. *
  133. * @param string $id cache id
  134. * @return bool
  135. */
  136. public function test($id)
  137. {
  138. if (!$this->_verifyPath($id)) {
  139. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  140. }
  141. $fileName = basename($id);
  142. if (empty($fileName)) {
  143. $fileName = $this->_options['index_filename'];
  144. }
  145. $pathName = $this->_options['public_dir'] . dirname($id);
  146. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  147. if (file_exists($file)) {
  148. return true;
  149. }
  150. return false;
  151. }
  152. /**
  153. * Save some string datas into a cache record
  154. *
  155. * Note : $data is always "string" (serialization is done by the
  156. * core not by the backend)
  157. *
  158. * @param string $data Datas to cache
  159. * @param string $id Cache id
  160. * @param array $tags Array of strings, the cache record will be tagged by each string entry
  161. * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
  162. * @return boolean true if no problem
  163. */
  164. public function save($data, $id, $tags = array(), $specificLifetime = false)
  165. {
  166. clearstatcache();
  167. if (is_null($id) || strlen($id) == 0) {
  168. $id = $this->_detectId();
  169. }
  170. $fileName = basename($id);
  171. if (empty($fileName)) {
  172. $fileName = $this->_options['index_filename'];
  173. }
  174. $pathName = $this->_options['public_dir'] . dirname($id);
  175. if (!file_exists($pathName)) {
  176. mkdir($pathName, $this->_options['cache_file_umask'], true);
  177. }
  178. if (is_null($id) || strlen($id) == 0) {
  179. $dataUnserialized = unserialize($data);
  180. $data = $dataUnserialized['data'];
  181. }
  182. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  183. if ($this->_options['file_locking']) {
  184. $result = file_put_contents($file, $data, LOCK_EX);
  185. } else {
  186. $result = file_put_contents($file, $data);
  187. }
  188. @chmod($file, $this->_options['cache_file_umask']);
  189. if (count($tags) > 0) {
  190. if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
  191. $this->_tagged = $tagged;
  192. } elseif (is_null($this->_tagged)) {
  193. $this->_tagged = array();
  194. }
  195. if (!isset($this->_tagged[$id])) {
  196. $this->_tagged[$id] = array();
  197. }
  198. $this->_tagged[$id] = array_unique(array_merge($this->_tagged[$id], $tags));
  199. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  200. }
  201. return (bool) $result;
  202. }
  203. /**
  204. * Remove a cache record
  205. *
  206. * @param string $id Cache id
  207. * @return boolean True if no problem
  208. */
  209. public function remove($id)
  210. {
  211. if (!$this->_verifyPath($id)) {
  212. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  213. }
  214. $fileName = basename($id);
  215. if (empty($fileName)) {
  216. $fileName = $this->_options['index_filename'];
  217. }
  218. $pathName = $this->_options['public_dir'] . dirname($id);
  219. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  220. if (!file_exists($file)) {
  221. return false;
  222. }
  223. return unlink($file);
  224. }
  225. /**
  226. * Remove a cache record recursively for the given directory matching a
  227. * REQUEST_URI based relative path (deletes the actual file matching this
  228. * in addition to the matching directory)
  229. *
  230. * @param string $id Cache id
  231. * @return boolean True if no problem
  232. */
  233. public function removeRecursively($id)
  234. {
  235. if (!$this->_verifyPath($id)) {
  236. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  237. }
  238. $fileName = basename($id);
  239. if (empty($fileName)) {
  240. $fileName = $this->_options['index_filename'];
  241. }
  242. $pathName = $this->_options['public_dir'] . dirname($id);
  243. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  244. $directory = $pathName . '/' . $fileName;
  245. if (file_exists($directory)) {
  246. if (!is_writable($directory)) {
  247. return false;
  248. }
  249. foreach (new DirectoryIterator($directory) as $file) {
  250. if (true === $file->isFile()) {
  251. if (false === unlink($file->getPathName())) {
  252. return false;
  253. }
  254. }
  255. }
  256. rmdir(dirname($path));
  257. }
  258. if (file_exists($file)) {
  259. if (!is_writable($file)) {
  260. return false;
  261. }
  262. return unlink($file);
  263. }
  264. return true;
  265. }
  266. /**
  267. * Clean some cache records
  268. *
  269. * Available modes are :
  270. * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
  271. * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
  272. * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
  273. * ($tags can be an array of strings or a single string)
  274. * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
  275. * ($tags can be an array of strings or a single string)
  276. * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
  277. * ($tags can be an array of strings or a single string)
  278. *
  279. * @param string $mode Clean mode
  280. * @param array $tags Array of tags
  281. * @return boolean true if no problem
  282. */
  283. public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
  284. {
  285. $result = false;
  286. switch ($mode) {
  287. case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  288. case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  289. if (empty($tags)) {
  290. throw new Zend_Exception('Cannot use tag matching modes as no tags were defined');
  291. }
  292. if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
  293. $this->_tagged = $tagged;
  294. } elseif (!$this->_tagged) {
  295. return true;
  296. }
  297. foreach ($tags as $tag) {
  298. $urls = array_keys($this->_tagged);
  299. foreach ($urls as $url) {
  300. if (in_array($tag, $this->_tagged[$url])) {
  301. $this->remove($url);
  302. unset($this->_tagged[$url]);
  303. }
  304. }
  305. }
  306. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  307. $result = true;
  308. break;
  309. case Zend_Cache::CLEANING_MODE_ALL:
  310. if (is_null($this->_tagged)) {
  311. $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME);
  312. $this->_tagged = $tagged;
  313. }
  314. if (is_null($this->_tagged) || empty($this->_tagged)) {
  315. return true;
  316. }
  317. $urls = array_keys($this->_tagged);
  318. foreach ($urls as $url) {
  319. $this->remove($url);
  320. unset($this->_tagged[$url]);
  321. }
  322. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  323. $result = true;
  324. break;
  325. case Zend_Cache::CLEANING_MODE_OLD:
  326. $this->_log("Zend_Cache_Backend_Static : Selected Cleaning Mode Currently Unsupported By This Backend");
  327. break;
  328. case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  329. if (empty($tags)) {
  330. throw new Zend_Exception('Cannot use tag matching modes as no tags were defined');
  331. }
  332. if (is_null($this->_tagged)) {
  333. $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME);
  334. $this->_tagged = $tagged;
  335. }
  336. if (is_null($this->_tagged) || empty($this->_tagged)) {
  337. return true;
  338. }
  339. $urls = array_keys($this->_tagged);
  340. foreach ($urls as $url) {
  341. $difference = array_diff($tags, $this->_tagged[$url]);
  342. if (count($tags) == count($difference)) {
  343. $this->remove($url);
  344. unset($this->_tagged[$url]);
  345. }
  346. }
  347. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  348. $result = true;
  349. break;
  350. default:
  351. Zend_Cache::throwException('Invalid mode for clean() method');
  352. break;
  353. }
  354. return $result;
  355. }
  356. /**
  357. * Set an Inner Cache, used here primarily to store Tags associated
  358. * with caches created by this backend. Note: If Tags are lost, the cache
  359. * should be completely cleaned as the mapping of tags to caches will
  360. * have been irrevocably lost.
  361. *
  362. * @param Zend_Cache_Core
  363. * @return void
  364. */
  365. public function setInnerCache(Zend_Cache_Core $cache)
  366. {
  367. $this->_tagCache = $cache;
  368. $this->_options['tag_cache'] = $cache;
  369. }
  370. /**
  371. * Get the Inner Cache if set
  372. *
  373. * @return Zend_Cache_Core
  374. */
  375. public function getInnerCache()
  376. {
  377. if (is_null($this->_tagCache)) {
  378. Zend_Cache::throwException('An Inner Cache has not been set; use setInnerCache()');
  379. }
  380. return $this->_tagCache;
  381. }
  382. /**
  383. * Verify path exists and is non-empty
  384. *
  385. * @param string $path
  386. * @return bool
  387. */
  388. protected function _verifyPath($path)
  389. {
  390. $path = realpath($path);
  391. $base = realpath($this->_options['public_dir']);
  392. return strncmp($path, $base, strlen($base)) !== 0;
  393. }
  394. /**
  395. * Determine the page to save from the request
  396. *
  397. * @return string
  398. */
  399. protected function _detectId()
  400. {
  401. return $_SERVER['REQUEST_URI'];
  402. }
  403. /**
  404. * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...)
  405. *
  406. * Throw an exception if a problem is found
  407. *
  408. * @param string $string Cache id or tag
  409. * @throws Zend_Cache_Exception
  410. * @return void
  411. */
  412. protected static function _validateIdOrTag($string)
  413. {
  414. if (!is_string($string)) {
  415. Zend_Cache::throwException('Invalid id or tag : must be a string');
  416. }
  417. // Internal only checked in Frontend - not here!
  418. if (substr($string, 0, 9) == 'internal-') {
  419. return;
  420. }
  421. // Validation assumes no query string, fragments or scheme included - only the path
  422. if (!preg_match(
  423. '/^(?:\/(?:(?:%[[:xdigit:]]{2}|[A-Za-z0-9-_.!~*\'()\[\]:@&=+$,;])*)?)+$/',
  424. $string
  425. )
  426. ) {
  427. Zend_Cache::throwException("Invalid id or tag '$string' : must be a valid URL path");
  428. }
  429. }
  430. }