/framework/vendor/zend/Zend/Cache/Backend/Static.php
PHP | 558 lines | 436 code | 22 blank | 100 comment | 40 complexity | 25bca53df6807f260f72c154d5650011 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-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/** 24 * @see Zend_Cache_Backend_Interface 25 */ 26require_once 'Zend/Cache/Backend/Interface.php'; 27 28/** 29 * @see Zend_Cache_Backend 30 */ 31require_once 'Zend/Cache/Backend.php'; 32 33/** 34 * @package Zend_Cache 35 * @subpackage Zend_Cache_Backend 36 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) 37 * @license http://framework.zend.com/license/new-bsd New BSD License 38 */ 39class Zend_Cache_Backend_Static 40 extends Zend_Cache_Backend 41 implements Zend_Cache_Backend_Interface 42{ 43 const INNER_CACHE_NAME = 'zend_cache_backend_static_tagcache'; 44 45 /** 46 * Static backend options 47 * @var array 48 */ 49 protected $_options = array( 50 'public_dir' => null, 51 'sub_dir' => 'html', 52 'file_extension' => '.html', 53 'index_filename' => 'index', 54 'file_locking' => true, 55 'cache_file_umask' => 0600, 56 'cache_directory_umask' => 0700, 57 'debug_header' => false, 58 'tag_cache' => null, 59 'disable_caching' => false 60 ); 61 62 /** 63 * Cache for handling tags 64 * @var Zend_Cache_Core 65 */ 66 protected $_tagCache = null; 67 68 /** 69 * Tagged items 70 * @var array 71 */ 72 protected $_tagged = null; 73 74 /** 75 * Interceptor child method to handle the case where an Inner 76 * Cache object is being set since it's not supported by the 77 * standard backend interface 78 * 79 * @param string $name 80 * @param mixed $value 81 * @return Zend_Cache_Backend_Static 82 */ 83 public function setOption($name, $value) 84 { 85 if ($name == 'tag_cache') { 86 $this->setInnerCache($value); 87 } else { 88 parent::setOption($name, $value); 89 } 90 return $this; 91 } 92 93 /** 94 * Retrieve any option via interception of the parent's statically held 95 * options including the local option for a tag cache. 96 * 97 * @param string $name 98 * @return mixed 99 */ 100 public function getOption($name) 101 { 102 if ($name == 'tag_cache') { 103 return $this->getInnerCache(); 104 } else { 105 if (in_array($name, $this->_options)) { 106 return $this->_options[$name]; 107 } 108 if ($name == 'lifetime') { 109 return parent::getLifetime(); 110 } 111 return null; 112 } 113 } 114 115 /** 116 * Test if a cache is available for the given id and (if yes) return it (false else) 117 * 118 * Note : return value is always "string" (unserialization is done by the core not by the backend) 119 * 120 * @param string $id Cache id 121 * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested 122 * @return string|false cached datas 123 */ 124 public function load($id, $doNotTestCacheValidity = false) 125 { 126 if (empty($id)) { 127 $id = $this->_detectId(); 128 } else { 129 $id = $this->_decodeId($id); 130 } 131 if (!$this->_verifyPath($id)) { 132 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 133 } 134 if ($doNotTestCacheValidity) { 135 $this->_log("Zend_Cache_Backend_Static::load() : \$doNotTestCacheValidity=true is unsupported by the Static backend"); 136 } 137 138 $fileName = basename($id); 139 if (empty($fileName)) { 140 $fileName = $this->_options['index_filename']; 141 } 142 $pathName = $this->_options['public_dir'] . dirname($id); 143 $file = rtrim($pathName, '/') . '/' . $fileName . $this->_options['file_extension']; 144 if (file_exists($file)) { 145 $content = file_get_contents($file); 146 return $content; 147 } 148 149 return false; 150 } 151 152 /** 153 * Test if a cache is available or not (for the given id) 154 * 155 * @param string $id cache id 156 * @return bool 157 */ 158 public function test($id) 159 { 160 $id = $this->_decodeId($id); 161 if (!$this->_verifyPath($id)) { 162 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 163 } 164 165 $fileName = basename($id); 166 if (empty($fileName)) { 167 $fileName = $this->_options['index_filename']; 168 } 169 if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 170 $this->_tagged = $tagged; 171 } elseif (!$this->_tagged) { 172 return false; 173 } 174 $pathName = $this->_options['public_dir'] . dirname($id); 175 176 // Switch extension if needed 177 if (isset($this->_tagged[$id])) { 178 $extension = $this->_tagged[$id]['extension']; 179 } else { 180 $extension = $this->_options['file_extension']; 181 } 182 $file = $pathName . '/' . $fileName . $extension; 183 if (file_exists($file)) { 184 return true; 185 } 186 return false; 187 } 188 189 /** 190 * Save some string datas into a cache record 191 * 192 * Note : $data is always "string" (serialization is done by the 193 * core not by the backend) 194 * 195 * @param string $data Datas to cache 196 * @param string $id Cache id 197 * @param array $tags Array of strings, the cache record will be tagged by each string entry 198 * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) 199 * @return boolean true if no problem 200 */ 201 public function save($data, $id, $tags = array(), $specificLifetime = false) 202 { 203 if ($this->_options['disable_caching']) { 204 return true; 205 } 206 $extension = null; 207 if ($this->_isSerialized($data)) { 208 $data = unserialize($data); 209 $extension = '.' . ltrim($data[1], '.'); 210 $data = $data[0]; 211 } 212 213 clearstatcache(); 214 if (is_null($id) || strlen($id) == 0) { 215 $id = $this->_detectId(); 216 } else { 217 $id = $this->_decodeId($id); 218 } 219 220 $fileName = basename($id); 221 if (empty($fileName)) { 222 $fileName = $this->_options['index_filename']; 223 } 224 225 $pathName = realpath($this->_options['public_dir']) . dirname($id); 226 $this->_createDirectoriesFor($pathName); 227 228 if (is_null($id) || strlen($id) == 0) { 229 $dataUnserialized = unserialize($data); 230 $data = $dataUnserialized['data']; 231 } 232 $ext = $this->_options['file_extension']; 233 if ($extension) $ext = $extension; 234 $file = rtrim($pathName, '/') . '/' . $fileName . $ext; 235 if ($this->_options['file_locking']) { 236 $result = file_put_contents($file, $data, LOCK_EX); 237 } else { 238 $result = file_put_contents($file, $data); 239 } 240 @chmod($file, $this->_octdec($this->_options['cache_file_umask'])); 241 242 if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 243 $this->_tagged = $tagged; 244 } elseif (is_null($this->_tagged)) { 245 $this->_tagged = array(); 246 } 247 if (!isset($this->_tagged[$id])) { 248 $this->_tagged[$id] = array(); 249 } 250 if (!isset($this->_tagged[$id]['tags'])) { 251 $this->_tagged[$id]['tags'] = array(); 252 } 253 $this->_tagged[$id]['tags'] = array_unique(array_merge($this->_tagged[$id]['tags'], $tags)); 254 $this->_tagged[$id]['extension'] = $ext; 255 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 256 return (bool) $result; 257 } 258 259 /** 260 * Recursively create the directories needed to write the static file 261 */ 262 protected function _createDirectoriesFor($path) 263 { 264 $parts = explode('/', $path); 265 $directory = ''; 266 foreach ($parts as $part) { 267 $directory = rtrim($directory, '/') . '/' . $part; 268 if (!is_dir($directory)) { 269 mkdir($directory, $this->_octdec($this->_options['cache_directory_umask'])); 270 } 271 } 272 } 273 274 /** 275 * Detect serialization of data (cannot predict since this is the only way 276 * to obey the interface yet pass in another parameter). 277 * 278 * In future, ZF 2.0, check if we can just avoid the interface restraints. 279 * 280 * This format is the only valid one possible for the class, so it's simple 281 * to just run a regular expression for the starting serialized format. 282 */ 283 protected function _isSerialized($data) 284 { 285 return preg_match("/a:2:\{i:0;s:\d+:\"/", $data); 286 } 287 288 /** 289 * Remove a cache record 290 * 291 * @param string $id Cache id 292 * @return boolean True if no problem 293 */ 294 public function remove($id) 295 { 296 if (!$this->_verifyPath($id)) { 297 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 298 } 299 $fileName = basename($id); 300 if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 301 $this->_tagged = $tagged; 302 } elseif (!$this->_tagged) { 303 return false; 304 } 305 if (isset($this->_tagged[$id])) { 306 $extension = $this->_tagged[$id]['extension']; 307 } else { 308 $extension = $this->_options['file_extension']; 309 } 310 if (empty($fileName)) { 311 $fileName = $this->_options['index_filename']; 312 } 313 $pathName = $this->_options['public_dir'] . dirname($id); 314 $file = realpath($pathName) . '/' . $fileName . $extension; 315 if (!file_exists($file)) { 316 return false; 317 } 318 return unlink($file); 319 } 320 321 /** 322 * Remove a cache record recursively for the given directory matching a 323 * REQUEST_URI based relative path (deletes the actual file matching this 324 * in addition to the matching directory) 325 * 326 * @param string $id Cache id 327 * @return boolean True if no problem 328 */ 329 public function removeRecursively($id) 330 { 331 if (!$this->_verifyPath($id)) { 332 Zend_Cache::throwException('Invalid cache id: does not match expected public_dir path'); 333 } 334 $fileName = basename($id); 335 if (empty($fileName)) { 336 $fileName = $this->_options['index_filename']; 337 } 338 $pathName = $this->_options['public_dir'] . dirname($id); 339 $file = $pathName . '/' . $fileName . $this->_options['file_extension']; 340 $directory = $pathName . '/' . $fileName; 341 if (file_exists($directory)) { 342 if (!is_writable($directory)) { 343 return false; 344 } 345 foreach (new DirectoryIterator($directory) as $file) { 346 if (true === $file->isFile()) { 347 if (false === unlink($file->getPathName())) { 348 return false; 349 } 350 } 351 } 352 rmdir(dirname($path)); 353 } 354 if (file_exists($file)) { 355 if (!is_writable($file)) { 356 return false; 357 } 358 return unlink($file); 359 } 360 return true; 361 } 362 363 /** 364 * Clean some cache records 365 * 366 * Available modes are : 367 * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 368 * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 369 * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 370 * ($tags can be an array of strings or a single string) 371 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 372 * ($tags can be an array of strings or a single string) 373 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 374 * ($tags can be an array of strings or a single string) 375 * 376 * @param string $mode Clean mode 377 * @param array $tags Array of tags 378 * @return boolean true if no problem 379 */ 380 public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 381 { 382 $result = false; 383 switch ($mode) { 384 case Zend_Cache::CLEANING_MODE_MATCHING_TAG: 385 case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: 386 if (empty($tags)) { 387 throw new Zend_Exception('Cannot use tag matching modes as no tags were defined'); 388 } 389 if (is_null($this->_tagged) && $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME)) { 390 $this->_tagged = $tagged; 391 } elseif (!$this->_tagged) { 392 return true; 393 } 394 foreach ($tags as $tag) { 395 $urls = array_keys($this->_tagged); 396 foreach ($urls as $url) { 397 if (isset($this->_tagged[$url]['tags']) && in_array($tag, $this->_tagged[$url]['tags'])) { 398 $this->remove($url); 399 unset($this->_tagged[$url]); 400 } 401 } 402 } 403 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 404 $result = true; 405 break; 406 case Zend_Cache::CLEANING_MODE_ALL: 407 if (is_null($this->_tagged)) { 408 $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME); 409 $this->_tagged = $tagged; 410 } 411 if (is_null($this->_tagged) || empty($this->_tagged)) { 412 return true; 413 } 414 $urls = array_keys($this->_tagged); 415 foreach ($urls as $url) { 416 $this->remove($url); 417 unset($this->_tagged[$url]); 418 } 419 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 420 $result = true; 421 break; 422 case Zend_Cache::CLEANING_MODE_OLD: 423 $this->_log("Zend_Cache_Backend_Static : Selected Cleaning Mode Currently Unsupported By This Backend"); 424 break; 425 case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: 426 if (empty($tags)) { 427 throw new Zend_Exception('Cannot use tag matching modes as no tags were defined'); 428 } 429 if (is_null($this->_tagged)) { 430 $tagged = $this->getInnerCache()->load(self::INNER_CACHE_NAME); 431 $this->_tagged = $tagged; 432 } 433 if (is_null($this->_tagged) || empty($this->_tagged)) { 434 return true; 435 } 436 $urls = array_keys($this->_tagged); 437 foreach ($urls as $url) { 438 $difference = array_diff($tags, $this->_tagged[$url]['tags']); 439 if (count($tags) == count($difference)) { 440 $this->remove($url); 441 unset($this->_tagged[$url]); 442 } 443 } 444 $this->getInnerCache()->save($this->_tagged, self::INNER_CACHE_NAME); 445 $result = true; 446 break; 447 default: 448 Zend_Cache::throwException('Invalid mode for clean() method'); 449 break; 450 } 451 return $result; 452 } 453 454 /** 455 * Set an Inner Cache, used here primarily to store Tags associated 456 * with caches created by this backend. Note: If Tags are lost, the cache 457 * should be completely cleaned as the mapping of tags to caches will 458 * have been irrevocably lost. 459 * 460 * @param Zend_Cache_Core 461 * @return void 462 */ 463 public function setInnerCache(Zend_Cache_Core $cache) 464 { 465 $this->_tagCache = $cache; 466 $this->_options['tag_cache'] = $cache; 467 } 468 469 /** 470 * Get the Inner Cache if set 471 * 472 * @return Zend_Cache_Core 473 */ 474 public function getInnerCache() 475 { 476 if (is_null($this->_tagCache)) { 477 Zend_Cache::throwException('An Inner Cache has not been set; use setInnerCache()'); 478 } 479 return $this->_tagCache; 480 } 481 482 /** 483 * Verify path exists and is non-empty 484 * 485 * @param string $path 486 * @return bool 487 */ 488 protected function _verifyPath($path) 489 { 490 $path = realpath($path); 491 $base = realpath($this->_options['public_dir']); 492 return strncmp($path, $base, strlen($base)) !== 0; 493 } 494 495 /** 496 * Determine the page to save from the request 497 * 498 * @return string 499 */ 500 protected function _detectId() 501 { 502 return $_SERVER['REQUEST_URI']; 503 } 504 505 /** 506 * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) 507 * 508 * Throw an exception if a problem is found 509 * 510 * @param string $string Cache id or tag 511 * @throws Zend_Cache_Exception 512 * @return void 513 * @deprecated Not usable until perhaps ZF 2.0 514 */ 515 protected static function _validateIdOrTag($string) 516 { 517 if (!is_string($string)) { 518 Zend_Cache::throwException('Invalid id or tag : must be a string'); 519 } 520 521 // Internal only checked in Frontend - not here! 522 if (substr($string, 0, 9) == 'internal-') { 523 return; 524 } 525 526 // Validation assumes no query string, fragments or scheme included - only the path 527 if (!preg_match( 528 '/^(?:\/(?:(?:%[[:xdigit:]]{2}|[A-Za-z0-9-_.!~*\'()\[\]:@&=+$,;])*)?)+$/', 529 $string 530 ) 531 ) { 532 Zend_Cache::throwException("Invalid id or tag '$string' : must be a valid URL path"); 533 } 534 } 535 536 /** 537 * Detect an octal string and return its octal value for file permission ops 538 * otherwise return the non-string (assumed octal or decimal int already) 539 * 540 * @param $val The potential octal in need of conversion 541 * @return int 542 */ 543 protected function _octdec($val) 544 { 545 if (decoct(octdec($val)) == $val && is_string($val)) { 546 return octdec($val); 547 } 548 return $val; 549 } 550 551 /** 552 * Decode a request URI from the provided ID 553 */ 554 protected function _decodeId($id) 555 { 556 return pack('H*', $id);; 557 } 558}