Static.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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-2010 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-2010 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. } else {
  112. $id = $this->_decodeId($id);
  113. }
  114. if (!$this->_verifyPath($id)) {
  115. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  116. }
  117. if ($doNotTestCacheValidity) {
  118. $this->_log("Zend_Cache_Backend_Static::load() : \$doNotTestCacheValidity=true is unsupported by the Static backend");
  119. }
  120. $fileName = basename($id);
  121. if (empty($fileName)) {
  122. $fileName = $this->_options['index_filename'];
  123. }
  124. $pathName = $this->_options['public_dir'] . dirname($id);
  125. $file = rtrim($pathName, '/') . '/' . $fileName . $this->_options['file_extension'];
  126. if (file_exists($file)) {
  127. $content = file_get_contents($file);
  128. return $content;
  129. }
  130. return false;
  131. }
  132. /**
  133. * Test if a cache is available or not (for the given id)
  134. *
  135. * @param string $id cache id
  136. * @return bool
  137. */
  138. public function test($id)
  139. {
  140. $id = $this->_decodeId($id);
  141. if (!$this->_verifyPath($id)) {
  142. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  143. }
  144. $fileName = basename($id);
  145. if (empty($fileName)) {
  146. $fileName = $this->_options['index_filename'];
  147. }
  148. $pathName = $this->_options['public_dir'] . dirname($id);
  149. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  150. if (file_exists($file)) {
  151. return true;
  152. }
  153. return false;
  154. }
  155. /**
  156. * Save some string datas into a cache record
  157. *
  158. * Note : $data is always "string" (serialization is done by the
  159. * core not by the backend)
  160. *
  161. * @param string $data Datas to cache
  162. * @param string $id Cache id
  163. * @param array $tags Array of strings, the cache record will be tagged by each string entry
  164. * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
  165. * @return boolean true if no problem
  166. */
  167. public function save($data, $id, $tags = array(), $specificLifetime = false)
  168. {
  169. clearstatcache();
  170. if (is_null($id) || strlen($id) == 0) {
  171. $id = $this->_detectId();
  172. } else {
  173. $id = $this->_decodeId($id);
  174. }
  175. $fileName = basename($id);
  176. if (empty($fileName)) {
  177. $fileName = $this->_options['index_filename'];
  178. }
  179. $pathName = $this->_options['public_dir'] . dirname($id);
  180. if (!file_exists($pathName)) {
  181. mkdir($pathName, $this->_octdec($this->_options['cache_file_umask']), true);
  182. }
  183. if (is_null($id) || strlen($id) == 0) {
  184. $dataUnserialized = unserialize($data);
  185. $data = $dataUnserialized['data'];
  186. }
  187. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  188. if ($this->_options['file_locking']) {
  189. $result = file_put_contents($file, $data, LOCK_EX);
  190. } else {
  191. $result = file_put_contents($file, $data);
  192. }
  193. @chmod($file, $this->_options['cache_file_umask']);
  194. if (count($tags) > 0) {
  195. if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
  196. $this->_tagged = $tagged;
  197. } elseif (is_null($this->_tagged)) {
  198. $this->_tagged = array();
  199. }
  200. if (!isset($this->_tagged[$id])) {
  201. $this->_tagged[$id] = array();
  202. }
  203. $this->_tagged[$id] = array_unique(array_merge($this->_tagged[$id], $tags));
  204. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  205. }
  206. return (bool) $result;
  207. }
  208. /**
  209. * Remove a cache record
  210. *
  211. * @param string $id Cache id
  212. * @return boolean True if no problem
  213. */
  214. public function remove($id)
  215. {
  216. if (!$this->_verifyPath($id)) {
  217. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  218. }
  219. $fileName = basename($id);
  220. if (empty($fileName)) {
  221. $fileName = $this->_options['index_filename'];
  222. }
  223. $pathName = $this->_options['public_dir'] . dirname($id);
  224. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  225. if (!file_exists($file)) {
  226. return false;
  227. }
  228. return unlink($file);
  229. }
  230. /**
  231. * Remove a cache record recursively for the given directory matching a
  232. * REQUEST_URI based relative path (deletes the actual file matching this
  233. * in addition to the matching directory)
  234. *
  235. * @param string $id Cache id
  236. * @return boolean True if no problem
  237. */
  238. public function removeRecursively($id)
  239. {
  240. if (!$this->_verifyPath($id)) {
  241. Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path');
  242. }
  243. $fileName = basename($id);
  244. if (empty($fileName)) {
  245. $fileName = $this->_options['index_filename'];
  246. }
  247. $pathName = $this->_options['public_dir'] . dirname($id);
  248. $file = $pathName . '/' . $fileName . $this->_options['file_extension'];
  249. $directory = $pathName . '/' . $fileName;
  250. if (file_exists($directory)) {
  251. if (!is_writable($directory)) {
  252. return false;
  253. }
  254. foreach (new DirectoryIterator($directory) as $file) {
  255. if (true === $file->isFile()) {
  256. if (false === unlink($file->getPathName())) {
  257. return false;
  258. }
  259. }
  260. }
  261. rmdir(dirname($path));
  262. }
  263. if (file_exists($file)) {
  264. if (!is_writable($file)) {
  265. return false;
  266. }
  267. return unlink($file);
  268. }
  269. return true;
  270. }
  271. /**
  272. * Clean some cache records
  273. *
  274. * Available modes are :
  275. * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
  276. * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
  277. * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
  278. * ($tags can be an array of strings or a single string)
  279. * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
  280. * ($tags can be an array of strings or a single string)
  281. * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
  282. * ($tags can be an array of strings or a single string)
  283. *
  284. * @param string $mode Clean mode
  285. * @param array $tags Array of tags
  286. * @return boolean true if no problem
  287. */
  288. public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
  289. {
  290. $result = false;
  291. switch ($mode) {
  292. case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  293. case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  294. if (empty($tags)) {
  295. throw new Zend_Exception('Cannot use tag matching modes as no tags were defined');
  296. }
  297. if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) {
  298. $this->_tagged = $tagged;
  299. } elseif (!$this->_tagged) {
  300. return true;
  301. }
  302. foreach ($tags as $tag) {
  303. $urls = array_keys($this->_tagged);
  304. foreach ($urls as $url) {
  305. if (in_array($tag, $this->_tagged[$url])) {
  306. $this->remove($url);
  307. unset($this->_tagged[$url]);
  308. }
  309. }
  310. }
  311. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  312. $result = true;
  313. break;
  314. case Zend_Cache::CLEANING_MODE_ALL:
  315. if (is_null($this->_tagged)) {
  316. $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME);
  317. $this->_tagged = $tagged;
  318. }
  319. if (is_null($this->_tagged) || empty($this->_tagged)) {
  320. return true;
  321. }
  322. $urls = array_keys($this->_tagged);
  323. foreach ($urls as $url) {
  324. $this->remove($url);
  325. unset($this->_tagged[$url]);
  326. }
  327. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  328. $result = true;
  329. break;
  330. case Zend_Cache::CLEANING_MODE_OLD:
  331. $this->_log("Zend_Cache_Backend_Static : Selected Cleaning Mode Currently Unsupported By This Backend");
  332. break;
  333. case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  334. if (empty($tags)) {
  335. throw new Zend_Exception('Cannot use tag matching modes as no tags were defined');
  336. }
  337. if (is_null($this->_tagged)) {
  338. $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME);
  339. $this->_tagged = $tagged;
  340. }
  341. if (is_null($this->_tagged) || empty($this->_tagged)) {
  342. return true;
  343. }
  344. $urls = array_keys($this->_tagged);
  345. foreach ($urls as $url) {
  346. $difference = array_diff($tags, $this->_tagged[$url]);
  347. if (count($tags) == count($difference)) {
  348. $this->remove($url);
  349. unset($this->_tagged[$url]);
  350. }
  351. }
  352. $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME);
  353. $result = true;
  354. break;
  355. default:
  356. Zend_Cache::throwException('Invalid mode for clean() method');
  357. break;
  358. }
  359. return $result;
  360. }
  361. /**
  362. * Set an Inner Cache, used here primarily to store Tags associated
  363. * with caches created by this backend. Note: If Tags are lost, the cache
  364. * should be completely cleaned as the mapping of tags to caches will
  365. * have been irrevocably lost.
  366. *
  367. * @param Zend_Cache_Core
  368. * @return void
  369. */
  370. public function setInnerCache(Zend_Cache_Core $cache)
  371. {
  372. $this->_tagCache = $cache;
  373. $this->_options['tag_cache'] = $cache;
  374. }
  375. /**
  376. * Get the Inner Cache if set
  377. *
  378. * @return Zend_Cache_Core
  379. */
  380. public function getInnerCache()
  381. {
  382. if (is_null($this->_tagCache)) {
  383. Zend_Cache::throwException('An Inner Cache has not been set; use setInnerCache()');
  384. }
  385. return $this->_tagCache;
  386. }
  387. /**
  388. * Verify path exists and is non-empty
  389. *
  390. * @param string $path
  391. * @return bool
  392. */
  393. protected function _verifyPath($path)
  394. {
  395. $path = realpath($path);
  396. $base = realpath($this->_options['public_dir']);
  397. return strncmp($path, $base, strlen($base)) !== 0;
  398. }
  399. /**
  400. * Determine the page to save from the request
  401. *
  402. * @return string
  403. */
  404. protected function _detectId()
  405. {
  406. return $_SERVER['REQUEST_URI'];
  407. }
  408. /**
  409. * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...)
  410. *
  411. * Throw an exception if a problem is found
  412. *
  413. * @param string $string Cache id or tag
  414. * @throws Zend_Cache_Exception
  415. * @return void
  416. * @deprecated Not usable until perhaps ZF 2.0
  417. */
  418. protected static function _validateIdOrTag($string)
  419. {
  420. if (!is_string($string)) {
  421. Zend_Cache::throwException('Invalid id or tag : must be a string');
  422. }
  423. // Internal only checked in Frontend - not here!
  424. if (substr($string, 0, 9) == 'internal-') {
  425. return;
  426. }
  427. // Validation assumes no query string, fragments or scheme included - only the path
  428. if (!preg_match(
  429. '/^(?:\/(?:(?:%[[:xdigit:]]{2}|[A-Za-z0-9-_.!~*\'()\[\]:@&=+$,;])*)?)+$/',
  430. $string
  431. )
  432. ) {
  433. Zend_Cache::throwException("Invalid id or tag '$string' : must be a valid URL path");
  434. }
  435. }
  436. /**
  437. * Detect an octal string and return its octal value for file permission ops
  438. * otherwise return the non-string (assumed octal or decimal int already)
  439. *
  440. * @param $val The potential octal in need of conversion
  441. * @return int
  442. */
  443. protected function _octdec($val)
  444. {
  445. if (decoct(octdec($val)) == $val && is_string($val)) {
  446. return octdec($val);
  447. }
  448. return $val;
  449. }
  450. /**
  451. * Decode a request URI from the provided ID
  452. */
  453. protected function _decodeId($id)
  454. {
  455. return pack('H*', $id);;
  456. }
  457. }