File indexing completed on 2025-01-19 05:20:57
0001 <?php 0002 /** 0003 * Zend Framework 0004 * 0005 * LICENSE 0006 * 0007 * This source file is subject to the new BSD license that is bundled 0008 * with this package in the file LICENSE.txt. 0009 * It is also available through the world-wide-web at this URL: 0010 * http://framework.zend.com/license/new-bsd 0011 * If you did not receive a copy of the license and are unable to 0012 * obtain it through the world-wide-web, please send an email 0013 * to license@zend.com so we can send you a copy immediately. 0014 * 0015 * @category Zend 0016 * @package Zend_Cache 0017 * @subpackage Zend_Cache_Backend 0018 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0019 * @license http://framework.zend.com/license/new-bsd New BSD License 0020 * @version $Id$ 0021 */ 0022 0023 /** 0024 * @see Zend_Cache_Backend_Interface 0025 */ 0026 // require_once 'Zend/Cache/Backend/Interface.php'; 0027 0028 /** 0029 * @see Zend_Cache_Backend 0030 */ 0031 // require_once 'Zend/Cache/Backend.php'; 0032 0033 /** 0034 * @package Zend_Cache 0035 * @subpackage Zend_Cache_Backend 0036 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0037 * @license http://framework.zend.com/license/new-bsd New BSD License 0038 */ 0039 class Zend_Cache_Backend_Static 0040 extends Zend_Cache_Backend 0041 implements Zend_Cache_Backend_Interface 0042 { 0043 const INNER_CACHE_NAME = 'zend_cache_backend_static_tagcache'; 0044 0045 /** 0046 * Static backend options 0047 * @var array 0048 */ 0049 protected $_options = array( 0050 'public_dir' => null, 0051 'sub_dir' => 'html', 0052 'file_extension' => '.html', 0053 'index_filename' => 'index', 0054 'file_locking' => true, 0055 'cache_file_perm' => 0600, 0056 'cache_directory_perm' => 0700, 0057 'debug_header' => false, 0058 'tag_cache' => null, 0059 'disable_caching' => false 0060 ); 0061 0062 /** 0063 * Cache for handling tags 0064 * @var Zend_Cache_Core 0065 */ 0066 protected $_tagCache = null; 0067 0068 /** 0069 * Tagged items 0070 * @var array 0071 */ 0072 protected $_tagged = null; 0073 0074 /** 0075 * Interceptor child method to handle the case where an Inner 0076 * Cache object is being set since it's not supported by the 0077 * standard backend interface 0078 * 0079 * @param string $name 0080 * @param mixed $value 0081 * @return Zend_Cache_Backend_Static 0082 */ 0083 public function setOption($name, $value) 0084 { 0085 if ($name == 'tag_cache') { 0086 $this->setInnerCache($value); 0087 } else { 0088 // See #ZF-12047 and #GH-91 0089 if ($name == 'cache_file_umask') { 0090 trigger_error( 0091 "'cache_file_umask' is deprecated -> please use 'cache_file_perm' instead", 0092 E_USER_NOTICE 0093 ); 0094 0095 $name = 'cache_file_perm'; 0096 } 0097 if ($name == 'cache_directory_umask') { 0098 trigger_error( 0099 "'cache_directory_umask' is deprecated -> please use 'cache_directory_perm' instead", 0100 E_USER_NOTICE 0101 ); 0102 0103 $name = 'cache_directory_perm'; 0104 } 0105 0106 parent::setOption($name, $value); 0107 } 0108 return $this; 0109 } 0110 0111 /** 0112 * Retrieve any option via interception of the parent's statically held 0113 * options including the local option for a tag cache. 0114 * 0115 * @param string $name 0116 * @return mixed 0117 */ 0118 public function getOption($name) 0119 { 0120 $name = strtolower($name); 0121 0122 if ($name == 'tag_cache') { 0123 return $this->getInnerCache(); 0124 } 0125 0126 return parent::getOption($name); 0127 } 0128 0129 /** 0130 * Test if a cache is available for the given id and (if yes) return it (false else) 0131 * 0132 * Note : return value is always "string" (unserialization is done by the core not by the backend) 0133 * 0134 * @param string $id Cache id 0135 * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested 0136 * @return string|false cached datas 0137 */ 0138 public function load($id, $doNotTestCacheValidity = false) 0139 { 0140 if (($id = (string)$id) === '') { 0141 $id = $this->_detectId(); 0142 } else { 0143 $id = $this->_decodeId($id); 0144 } 0145 if (!$this->_verifyPath($id)) { 0146 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 0147 } 0148 if ($doNotTestCacheValidity) { 0149 $this->_log("Zend_Cache_Backend_Static::load() : \$doNotTestCacheValidity=true is unsupported by the Static backend"); 0150 } 0151 0152 $fileName = basename($id); 0153 if ($fileName === '') { 0154 $fileName = $this->_options['index_filename']; 0155 } 0156 $pathName = $this->_options['public_dir'] . dirname($id); 0157 $file = rtrim($pathName, '/') . '/' . $fileName . $this->_options['file_extension']; 0158 if (file_exists($file)) { 0159 $content = file_get_contents($file); 0160 return $content; 0161 } 0162 0163 return false; 0164 } 0165 0166 /** 0167 * Test if a cache is available or not (for the given id) 0168 * 0169 * @param string $id cache id 0170 * @return bool 0171 */ 0172 public function test($id) 0173 { 0174 $id = $this->_decodeId($id); 0175 if (!$this->_verifyPath($id)) { 0176 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 0177 } 0178 0179 $fileName = basename($id); 0180 if ($fileName === '') { 0181 $fileName = $this->_options['index_filename']; 0182 } 0183 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 0184 $this->_tagged = $tagged; 0185 } elseif (!$this->_tagged) { 0186 return false; 0187 } 0188 $pathName = $this->_options['public_dir'] . dirname($id); 0189 0190 // Switch extension if needed 0191 if (isset($this->_tagged[$id])) { 0192 $extension = $this->_tagged[$id]['extension']; 0193 } else { 0194 $extension = $this->_options['file_extension']; 0195 } 0196 $file = $pathName . '/' . $fileName . $extension; 0197 if (file_exists($file)) { 0198 return true; 0199 } 0200 return false; 0201 } 0202 0203 /** 0204 * Save some string datas into a cache record 0205 * 0206 * Note : $data is always "string" (serialization is done by the 0207 * core not by the backend) 0208 * 0209 * @param string $data Datas to cache 0210 * @param string $id Cache id 0211 * @param array $tags Array of strings, the cache record will be tagged by each string entry 0212 * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) 0213 * @return boolean true if no problem 0214 */ 0215 public function save($data, $id, $tags = array(), $specificLifetime = false) 0216 { 0217 if ($this->_options['disable_caching']) { 0218 return true; 0219 } 0220 $extension = null; 0221 if ($this->_isSerialized($data)) { 0222 $data = unserialize($data); 0223 $extension = '.' . ltrim($data[1], '.'); 0224 $data = $data[0]; 0225 } 0226 0227 clearstatcache(); 0228 if (($id = (string)$id) === '') { 0229 $id = $this->_detectId(); 0230 } else { 0231 $id = $this->_decodeId($id); 0232 } 0233 0234 $fileName = basename($id); 0235 if ($fileName === '') { 0236 $fileName = $this->_options['index_filename']; 0237 } 0238 0239 $pathName = realpath($this->_options['public_dir']) . dirname($id); 0240 $this->_createDirectoriesFor($pathName); 0241 0242 if ($id === null || strlen($id) == 0) { 0243 $dataUnserialized = unserialize($data); 0244 $data = $dataUnserialized['data']; 0245 } 0246 $ext = $this->_options['file_extension']; 0247 if ($extension) $ext = $extension; 0248 $file = rtrim($pathName, '/') . '/' . $fileName . $ext; 0249 if ($this->_options['file_locking']) { 0250 $result = file_put_contents($file, $data, LOCK_EX); 0251 } else { 0252 $result = file_put_contents($file, $data); 0253 } 0254 @chmod($file, $this->_octdec($this->_options['cache_file_perm'])); 0255 0256 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 0257 $this->_tagged = $tagged; 0258 } elseif ($this->_tagged === null) { 0259 $this->_tagged = array(); 0260 } 0261 if (!isset($this->_tagged[$id])) { 0262 $this->_tagged[$id] = array(); 0263 } 0264 if (!isset($this->_tagged[$id]['tags'])) { 0265 $this->_tagged[$id]['tags'] = array(); 0266 } 0267 $this->_tagged[$id]['tags'] = array_unique(array_merge($this->_tagged[$id]['tags'], $tags)); 0268 $this->_tagged[$id]['extension'] = $ext; 0269 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 0270 return (bool) $result; 0271 } 0272 0273 /** 0274 * Recursively create the directories needed to write the static file 0275 */ 0276 protected function _createDirectoriesFor($path) 0277 { 0278 if (!is_dir($path)) { 0279 $oldUmask = umask(0); 0280 if ( !@mkdir($path, $this->_octdec($this->_options['cache_directory_perm']), true)) { 0281 $lastErr = error_get_last(); 0282 umask($oldUmask); 0283 Zend_Cache::throwException("Can't create directory: {$lastErr['message']}"); 0284 } 0285 umask($oldUmask); 0286 } 0287 } 0288 0289 /** 0290 * Detect serialization of data (cannot predict since this is the only way 0291 * to obey the interface yet pass in another parameter). 0292 * 0293 * In future, ZF 2.0, check if we can just avoid the interface restraints. 0294 * 0295 * This format is the only valid one possible for the class, so it's simple 0296 * to just run a regular expression for the starting serialized format. 0297 */ 0298 protected function _isSerialized($data) 0299 { 0300 return preg_match("/a:2:\{i:0;s:\d+:\"/", $data); 0301 } 0302 0303 /** 0304 * Remove a cache record 0305 * 0306 * @param string $id Cache id 0307 * @return boolean True if no problem 0308 */ 0309 public function remove($id) 0310 { 0311 if (!$this->_verifyPath($id)) { 0312 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 0313 } 0314 $fileName = basename($id); 0315 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 0316 $this->_tagged = $tagged; 0317 } elseif (!$this->_tagged) { 0318 return false; 0319 } 0320 if (isset($this->_tagged[$id])) { 0321 $extension = $this->_tagged[$id]['extension']; 0322 } else { 0323 $extension = $this->_options['file_extension']; 0324 } 0325 if ($fileName === '') { 0326 $fileName = $this->_options['index_filename']; 0327 } 0328 $pathName = $this->_options['public_dir'] . dirname($id); 0329 $file = realpath($pathName) . '/' . $fileName . $extension; 0330 if (!file_exists($file)) { 0331 return false; 0332 } 0333 return unlink($file); 0334 } 0335 0336 /** 0337 * Remove a cache record recursively for the given directory matching a 0338 * REQUEST_URI based relative path (deletes the actual file matching this 0339 * in addition to the matching directory) 0340 * 0341 * @param string $id Cache id 0342 * @return boolean True if no problem 0343 */ 0344 public function removeRecursively($id) 0345 { 0346 if (!$this->_verifyPath($id)) { 0347 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 0348 } 0349 $fileName = basename($id); 0350 if ($fileName === '') { 0351 $fileName = $this->_options['index_filename']; 0352 } 0353 $pathName = $this->_options['public_dir'] . dirname($id); 0354 $file = $pathName . '/' . $fileName . $this->_options['file_extension']; 0355 $directory = $pathName . '/' . $fileName; 0356 if (file_exists($directory)) { 0357 if (!is_writable($directory)) { 0358 return false; 0359 } 0360 if (is_dir($directory)) { 0361 foreach (new DirectoryIterator($directory) as $file) { 0362 if (true === $file->isFile()) { 0363 if (false === unlink($file->getPathName())) { 0364 return false; 0365 } 0366 } 0367 } 0368 } 0369 rmdir($directory); 0370 } 0371 if (file_exists($file)) { 0372 if (!is_writable($file)) { 0373 return false; 0374 } 0375 return unlink($file); 0376 } 0377 return true; 0378 } 0379 0380 /** 0381 * Clean some cache records 0382 * 0383 * Available modes are : 0384 * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 0385 * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 0386 * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 0387 * ($tags can be an array of strings or a single string) 0388 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 0389 * ($tags can be an array of strings or a single string) 0390 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 0391 * ($tags can be an array of strings or a single string) 0392 * 0393 * @param string $mode Clean mode 0394 * @param array $tags Array of tags 0395 * @return boolean true if no problem 0396 * @throws Zend_Exception 0397 */ 0398 public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 0399 { 0400 $result = false; 0401 switch ($mode) { 0402 case Zend_Cache::CLEANING_MODE_MATCHING_TAG: 0403 case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: 0404 if (empty($tags)) { 0405 throw new Zend_Exception('Cannot use tag matching modes as no tags were defined'); 0406 } 0407 if ($this->_tagged === null && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 0408 $this->_tagged = $tagged; 0409 } elseif (!$this->_tagged) { 0410 return true; 0411 } 0412 foreach ($tags as $tag) { 0413 $urls = array_keys($this->_tagged); 0414 foreach ($urls as $url) { 0415 if (isset($this->_tagged[$url]['tags']) && in_array($tag, $this->_tagged[$url]['tags'])) { 0416 $this->remove($url); 0417 unset($this->_tagged[$url]); 0418 } 0419 } 0420 } 0421 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 0422 $result = true; 0423 break; 0424 case Zend_Cache::CLEANING_MODE_ALL: 0425 if ($this->_tagged === null) { 0426 $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME); 0427 $this->_tagged = $tagged; 0428 } 0429 if ($this->_tagged === null || empty($this->_tagged)) { 0430 return true; 0431 } 0432 $urls = array_keys($this->_tagged); 0433 foreach ($urls as $url) { 0434 $this->remove($url); 0435 unset($this->_tagged[$url]); 0436 } 0437 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 0438 $result = true; 0439 break; 0440 case Zend_Cache::CLEANING_MODE_OLD: 0441 $this->_log("Zend_Cache_Backend_Static : Selected Cleaning Mode Currently Unsupported By This Backend"); 0442 break; 0443 case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: 0444 if (empty($tags)) { 0445 throw new Zend_Exception('Cannot use tag matching modes as no tags were defined'); 0446 } 0447 if ($this->_tagged === null) { 0448 $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME); 0449 $this->_tagged = $tagged; 0450 } 0451 if ($this->_tagged === null || empty($this->_tagged)) { 0452 return true; 0453 } 0454 $urls = array_keys($this->_tagged); 0455 foreach ($urls as $url) { 0456 $difference = array_diff($tags, $this->_tagged[$url]['tags']); 0457 if (count($tags) == count($difference)) { 0458 $this->remove($url); 0459 unset($this->_tagged[$url]); 0460 } 0461 } 0462 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 0463 $result = true; 0464 break; 0465 default: 0466 Zend_Cache::throwException('Invalid mode for clean() method'); 0467 break; 0468 } 0469 return $result; 0470 } 0471 0472 /** 0473 * Set an Inner Cache, used here primarily to store Tags associated 0474 * with caches created by this backend. Note: If Tags are lost, the cache 0475 * should be completely cleaned as the mapping of tags to caches will 0476 * have been irrevocably lost. 0477 * 0478 * @param Zend_Cache_Core 0479 * @return void 0480 */ 0481 public function setInnerCache(Zend_Cache_Core $cache) 0482 { 0483 $this->_tagCache = $cache; 0484 $this->_options['tag_cache'] = $cache; 0485 } 0486 0487 /** 0488 * Get the Inner Cache if set 0489 * 0490 * @return Zend_Cache_Core 0491 */ 0492 public function getInnerCache() 0493 { 0494 if ($this->_tagCache === null) { 0495 Zend_Cache::throwException('An Inner Cache has not been set; use setInnerCache()'); 0496 } 0497 return $this->_tagCache; 0498 } 0499 0500 /** 0501 * Verify path exists and is non-empty 0502 * 0503 * @param string $path 0504 * @return bool 0505 */ 0506 protected function _verifyPath($path) 0507 { 0508 $path = realpath($path); 0509 $base = realpath($this->_options['public_dir']); 0510 return strncmp($path, $base, strlen($base)) !== 0; 0511 } 0512 0513 /** 0514 * Determine the page to save from the request 0515 * 0516 * @return string 0517 */ 0518 protected function _detectId() 0519 { 0520 return $_SERVER['REQUEST_URI']; 0521 } 0522 0523 /** 0524 * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) 0525 * 0526 * Throw an exception if a problem is found 0527 * 0528 * @param string $string Cache id or tag 0529 * @throws Zend_Cache_Exception 0530 * @return void 0531 * @deprecated Not usable until perhaps ZF 2.0 0532 */ 0533 protected static function _validateIdOrTag($string) 0534 { 0535 if (!is_string($string)) { 0536 Zend_Cache::throwException('Invalid id or tag : must be a string'); 0537 } 0538 0539 // Internal only checked in Frontend - not here! 0540 if (substr($string, 0, 9) == 'internal-') { 0541 return; 0542 } 0543 0544 // Validation assumes no query string, fragments or scheme included - only the path 0545 if (!preg_match( 0546 '/^(?:\/(?:(?:%[[:xdigit:]]{2}|[A-Za-z0-9-_.!~*\'()\[\]:@&=+$,;])*)?)+$/', 0547 $string 0548 ) 0549 ) { 0550 Zend_Cache::throwException("Invalid id or tag '$string' : must be a valid URL path"); 0551 } 0552 } 0553 0554 /** 0555 * Detect an octal string and return its octal value for file permission ops 0556 * otherwise return the non-string (assumed octal or decimal int already) 0557 * 0558 * @param string $val The potential octal in need of conversion 0559 * @return int 0560 */ 0561 protected function _octdec($val) 0562 { 0563 if (is_string($val) && decoct(octdec($val)) == $val) { 0564 return octdec($val); 0565 } 0566 return $val; 0567 } 0568 0569 /** 0570 * Decode a request URI from the provided ID 0571 * 0572 * @param string $id 0573 * @return string 0574 */ 0575 protected function _decodeId($id) 0576 { 0577 return pack('H*', $id); 0578 } 0579 }