Static.php 17 KB

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