1<?php 2/** 3 * Smarty Internal Plugin 4 * 5 * @package Smarty 6 * @subpackage Cacher 7 */ 8 9/** 10 * Smarty Cache Handler Base for Key/Value Storage Implementations 11 * This class implements the functionality required to use simple key/value stores 12 * for hierarchical cache groups. key/value stores like memcache or APC do not support 13 * wildcards in keys, therefore a cache group cannot be cleared like "a|*" - which 14 * is no problem to filesystem and RDBMS implementations. 15 * This implementation is based on the concept of invalidation. While one specific cache 16 * can be identified and cleared, any range of caches cannot be identified. For this reason 17 * each level of the cache group hierarchy can have its own value in the store. These values 18 * are nothing but microtimes, telling us when a particular cache group was cleared for the 19 * last time. These keys are evaluated for every cache read to determine if the cache has 20 * been invalidated since it was created and should hence be treated as inexistent. 21 * Although deep hierarchies are possible, they are not recommended. Try to keep your 22 * cache groups as shallow as possible. Anything up 3-5 parents should be ok. So 23 * »a|b|c« is a good depth where »a|b|c|d|e|f|g|h|i|j|k« isn't. Try to join correlating 24 * cache groups: if your cache groups look somewhat like »a|b|$page|$items|$whatever« 25 * consider using »a|b|c|$page-$items-$whatever« instead. 26 * 27 * @package Smarty 28 * @subpackage Cacher 29 * @author Rodney Rehm 30 */ 31abstract class Smarty_CacheResource_KeyValueStore extends Smarty_CacheResource 32{ 33 /** 34 * cache for contents 35 * 36 * @var array 37 */ 38 protected $contents = array(); 39 40 /** 41 * cache for timestamps 42 * 43 * @var array 44 */ 45 protected $timestamps = array(); 46 47 /** 48 * populate Cached Object with meta data from Resource 49 * 50 * @param Smarty_Template_Cached $cached cached object 51 * @param Smarty_Internal_Template $_template template object 52 * 53 * @return void 54 */ 55 public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template) 56 { 57 $cached->filepath = $_template->source->uid . '#' . $this->sanitize($cached->source->resource) . '#' . 58 $this->sanitize($cached->cache_id) . '#' . $this->sanitize($cached->compile_id); 59 $this->populateTimestamp($cached); 60 } 61 62 /** 63 * populate Cached Object with timestamp and exists from Resource 64 * 65 * @param Smarty_Template_Cached $cached cached object 66 * 67 * @return void 68 */ 69 public function populateTimestamp(Smarty_Template_Cached $cached) 70 { 71 if (!$this->fetch( 72 $cached->filepath, 73 $cached->source->name, 74 $cached->cache_id, 75 $cached->compile_id, 76 $content, 77 $timestamp, 78 $cached->source->uid 79 ) 80 ) { 81 return; 82 } 83 $cached->content = $content; 84 $cached->timestamp = (int)$timestamp; 85 $cached->exists = !!$cached->timestamp; 86 } 87 88 /** 89 * Read the cached template and process the header 90 * 91 * @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template 92 * @param Smarty_Template_Cached $cached cached object 93 * @param boolean $update flag if called because cache update 94 * 95 * @return boolean true or false if the cached content does not exist 96 */ 97 public function process( 98 Smarty_Internal_Template $_smarty_tpl, 99 Smarty_Template_Cached $cached = null, 100 $update = false 101 ) { 102 if (!$cached) { 103 $cached = $_smarty_tpl->cached; 104 } 105 $content = $cached->content ? $cached->content : null; 106 $timestamp = $cached->timestamp ? $cached->timestamp : null; 107 if ($content === null || !$timestamp) { 108 if (!$this->fetch( 109 $_smarty_tpl->cached->filepath, 110 $_smarty_tpl->source->name, 111 $_smarty_tpl->cache_id, 112 $_smarty_tpl->compile_id, 113 $content, 114 $timestamp, 115 $_smarty_tpl->source->uid 116 ) 117 ) { 118 return false; 119 } 120 } 121 if (isset($content)) { 122 eval('?>' . $content); 123 return true; 124 } 125 return false; 126 } 127 128 /** 129 * Write the rendered template output to cache 130 * 131 * @param Smarty_Internal_Template $_template template object 132 * @param string $content content to cache 133 * 134 * @return boolean success 135 */ 136 public function writeCachedContent(Smarty_Internal_Template $_template, $content) 137 { 138 $this->addMetaTimestamp($content); 139 return $this->write(array($_template->cached->filepath => $content), $_template->cache_lifetime); 140 } 141 142 /** 143 * Read cached template from cache 144 * 145 * @param Smarty_Internal_Template $_template template object 146 * 147 * @return string|false content 148 */ 149 public function readCachedContent(Smarty_Internal_Template $_template) 150 { 151 $content = $_template->cached->content ? $_template->cached->content : null; 152 $timestamp = null; 153 if ($content === null) { 154 if (!$this->fetch( 155 $_template->cached->filepath, 156 $_template->source->name, 157 $_template->cache_id, 158 $_template->compile_id, 159 $content, 160 $timestamp, 161 $_template->source->uid 162 ) 163 ) { 164 return false; 165 } 166 } 167 if (isset($content)) { 168 return $content; 169 } 170 return false; 171 } 172 173 /** 174 * Empty cache 175 * {@internal the $exp_time argument is ignored altogether }} 176 * 177 * @param Smarty $smarty Smarty object 178 * @param integer $exp_time expiration time [being ignored] 179 * 180 * @return integer number of cache files deleted [always -1] 181 * @uses purge() to clear the whole store 182 * @uses invalidate() to mark everything outdated if purge() is inapplicable 183 */ 184 public function clearAll(Smarty $smarty, $exp_time = null) 185 { 186 if (!$this->purge()) { 187 $this->invalidate(null); 188 } 189 return -1; 190 } 191 192 /** 193 * Empty cache for a specific template 194 * {@internal the $exp_time argument is ignored altogether}} 195 * 196 * @param Smarty $smarty Smarty object 197 * @param string $resource_name template name 198 * @param string $cache_id cache id 199 * @param string $compile_id compile id 200 * @param integer $exp_time expiration time [being ignored] 201 * 202 * @return int number of cache files deleted [always -1] 203 * @throws \SmartyException 204 * @uses buildCachedFilepath() to generate the CacheID 205 * @uses invalidate() to mark CacheIDs parent chain as outdated 206 * @uses delete() to remove CacheID from cache 207 */ 208 public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time) 209 { 210 $uid = $this->getTemplateUid($smarty, $resource_name); 211 $cid = $uid . '#' . $this->sanitize($resource_name) . '#' . $this->sanitize($cache_id) . '#' . 212 $this->sanitize($compile_id); 213 $this->delete(array($cid)); 214 $this->invalidate($cid, $resource_name, $cache_id, $compile_id, $uid); 215 return -1; 216 } 217 218 /** 219 * Get template's unique ID 220 * 221 * @param Smarty $smarty Smarty object 222 * @param string $resource_name template name 223 * 224 * @return string filepath of cache file 225 * @throws \SmartyException 226 */ 227 protected function getTemplateUid(Smarty $smarty, $resource_name) 228 { 229 if (isset($resource_name)) { 230 $source = Smarty_Template_Source::load(null, $smarty, $resource_name); 231 if ($source->exists) { 232 return $source->uid; 233 } 234 } 235 return ''; 236 } 237 238 /** 239 * Sanitize CacheID components 240 * 241 * @param string $string CacheID component to sanitize 242 * 243 * @return string sanitized CacheID component 244 */ 245 protected function sanitize($string) 246 { 247 $string = trim($string, '|'); 248 if (!$string) { 249 return ''; 250 } 251 return preg_replace('#[^\w\|]+#S', '_', $string); 252 } 253 254 /** 255 * Fetch and prepare a cache object. 256 * 257 * @param string $cid CacheID to fetch 258 * @param string $resource_name template name 259 * @param string $cache_id cache id 260 * @param string $compile_id compile id 261 * @param string $content cached content 262 * @param integer &$timestamp cached timestamp (epoch) 263 * @param string $resource_uid resource's uid 264 * 265 * @return boolean success 266 */ 267 protected function fetch( 268 $cid, 269 $resource_name = null, 270 $cache_id = null, 271 $compile_id = null, 272 &$content = null, 273 &$timestamp = null, 274 $resource_uid = null 275 ) { 276 $t = $this->read(array($cid)); 277 $content = !empty($t[ $cid ]) ? $t[ $cid ] : null; 278 $timestamp = null; 279 if ($content && ($timestamp = $this->getMetaTimestamp($content))) { 280 $invalidated = 281 $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid); 282 if ($invalidated > $timestamp) { 283 $timestamp = null; 284 $content = null; 285 } 286 } 287 return !!$content; 288 } 289 290 /** 291 * Add current microtime to the beginning of $cache_content 292 * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}} 293 * 294 * @param string &$content the content to be cached 295 */ 296 protected function addMetaTimestamp(&$content) 297 { 298 $mt = explode(' ', microtime()); 299 $ts = pack('NN', $mt[ 1 ], (int)($mt[ 0 ] * 100000000)); 300 $content = $ts . $content; 301 } 302 303 /** 304 * Extract the timestamp the $content was cached 305 * 306 * @param string &$content the cached content 307 * 308 * @return float the microtime the content was cached 309 */ 310 protected function getMetaTimestamp(&$content) 311 { 312 extract(unpack('N1s/N1m/a*content', $content)); 313 /** 314 * @var int $s 315 * @var int $m 316 */ 317 return $s + ($m / 100000000); 318 } 319 320 /** 321 * Invalidate CacheID 322 * 323 * @param string $cid CacheID 324 * @param string $resource_name template name 325 * @param string $cache_id cache id 326 * @param string $compile_id compile id 327 * @param string $resource_uid source's uid 328 * 329 * @return void 330 */ 331 protected function invalidate( 332 $cid = null, 333 $resource_name = null, 334 $cache_id = null, 335 $compile_id = null, 336 $resource_uid = null 337 ) { 338 $now = microtime(true); 339 $key = null; 340 // invalidate everything 341 if (!$resource_name && !$cache_id && !$compile_id) { 342 $key = 'IVK#ALL'; 343 } // invalidate all caches by template 344 else { 345 if ($resource_name && !$cache_id && !$compile_id) { 346 $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name); 347 } // invalidate all caches by cache group 348 else { 349 if (!$resource_name && $cache_id && !$compile_id) { 350 $key = 'IVK#CACHE#' . $this->sanitize($cache_id); 351 } // invalidate all caches by compile id 352 else { 353 if (!$resource_name && !$cache_id && $compile_id) { 354 $key = 'IVK#COMPILE#' . $this->sanitize($compile_id); 355 } // invalidate by combination 356 else { 357 $key = 'IVK#CID#' . $cid; 358 } 359 } 360 } 361 } 362 $this->write(array($key => $now)); 363 } 364 365 /** 366 * Determine the latest timestamp known to the invalidation chain 367 * 368 * @param string $cid CacheID to determine latest invalidation timestamp of 369 * @param string $resource_name template name 370 * @param string $cache_id cache id 371 * @param string $compile_id compile id 372 * @param string $resource_uid source's filepath 373 * 374 * @return float the microtime the CacheID was invalidated 375 */ 376 protected function getLatestInvalidationTimestamp( 377 $cid, 378 $resource_name = null, 379 $cache_id = null, 380 $compile_id = null, 381 $resource_uid = null 382 ) { 383 // abort if there is no CacheID 384 if (false && !$cid) { 385 return 0; 386 } 387 // abort if there are no InvalidationKeys to check 388 if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) { 389 return 0; 390 } 391 // there are no InValidationKeys 392 if (!($values = $this->read($_cid))) { 393 return 0; 394 } 395 // make sure we're dealing with floats 396 $values = array_map('floatval', $values); 397 return max($values); 398 } 399 400 /** 401 * Translate a CacheID into the list of applicable InvalidationKeys. 402 * Splits 'some|chain|into|an|array' into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... ) 403 * 404 * @param string $cid CacheID to translate 405 * @param string $resource_name template name 406 * @param string $cache_id cache id 407 * @param string $compile_id compile id 408 * @param string $resource_uid source's filepath 409 * 410 * @return array list of InvalidationKeys 411 * @uses $invalidationKeyPrefix to prepend to each InvalidationKey 412 */ 413 protected function listInvalidationKeys( 414 $cid, 415 $resource_name = null, 416 $cache_id = null, 417 $compile_id = null, 418 $resource_uid = null 419 ) { 420 $t = array('IVK#ALL'); 421 $_name = $_compile = '#'; 422 if ($resource_name) { 423 $_name .= $resource_uid . '#' . $this->sanitize($resource_name); 424 $t[] = 'IVK#TEMPLATE' . $_name; 425 } 426 if ($compile_id) { 427 $_compile .= $this->sanitize($compile_id); 428 $t[] = 'IVK#COMPILE' . $_compile; 429 } 430 $_name .= '#'; 431 $cid = trim($cache_id, '|'); 432 if (!$cid) { 433 return $t; 434 } 435 $i = 0; 436 while (true) { 437 // determine next delimiter position 438 $i = strpos($cid, '|', $i); 439 // add complete CacheID if there are no more delimiters 440 if ($i === false) { 441 $t[] = 'IVK#CACHE#' . $cid; 442 $t[] = 'IVK#CID' . $_name . $cid . $_compile; 443 $t[] = 'IVK#CID' . $_name . $_compile; 444 break; 445 } 446 $part = substr($cid, 0, $i); 447 // add slice to list 448 $t[] = 'IVK#CACHE#' . $part; 449 $t[] = 'IVK#CID' . $_name . $part . $_compile; 450 // skip past delimiter position 451 $i++; 452 } 453 return $t; 454 } 455 456 /** 457 * Check is cache is locked for this template 458 * 459 * @param Smarty $smarty Smarty object 460 * @param Smarty_Template_Cached $cached cached object 461 * 462 * @return boolean true or false if cache is locked 463 */ 464 public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached) 465 { 466 $key = 'LOCK#' . $cached->filepath; 467 $data = $this->read(array($key)); 468 return $data && time() - $data[ $key ] < $smarty->locking_timeout; 469 } 470 471 /** 472 * Lock cache for this template 473 * 474 * @param Smarty $smarty Smarty object 475 * @param Smarty_Template_Cached $cached cached object 476 * 477 * @return bool|void 478 */ 479 public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached) 480 { 481 $cached->is_locked = true; 482 $key = 'LOCK#' . $cached->filepath; 483 $this->write(array($key => time()), $smarty->locking_timeout); 484 } 485 486 /** 487 * Unlock cache for this template 488 * 489 * @param Smarty $smarty Smarty object 490 * @param Smarty_Template_Cached $cached cached object 491 * 492 * @return bool|void 493 */ 494 public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached) 495 { 496 $cached->is_locked = false; 497 $key = 'LOCK#' . $cached->filepath; 498 $this->delete(array($key)); 499 } 500 501 /** 502 * Read values for a set of keys from cache 503 * 504 * @param array $keys list of keys to fetch 505 * 506 * @return array list of values with the given keys used as indexes 507 */ 508 abstract protected function read(array $keys); 509 510 /** 511 * Save values for a set of keys to cache 512 * 513 * @param array $keys list of values to save 514 * @param int $expire expiration time 515 * 516 * @return boolean true on success, false on failure 517 */ 518 abstract protected function write(array $keys, $expire = null); 519 520 /** 521 * Remove values from cache 522 * 523 * @param array $keys list of keys to delete 524 * 525 * @return boolean true on success, false on failure 526 */ 527 abstract protected function delete(array $keys); 528 529 /** 530 * Remove *all* values from cache 531 * 532 * @return boolean true on success, false on failure 533 */ 534 protected function purge() 535 { 536 return false; 537 } 538} 539