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 60 $this->populateTimestamp($cached); 61 } 62 63 /** 64 * populate Cached Object with timestamp and exists from Resource 65 * 66 * @param Smarty_Template_Cached $cached cached object 67 * 68 * @return void 69 */ 70 public function populateTimestamp(Smarty_Template_Cached $cached) 71 { 72 if (!$this->fetch($cached->filepath, $cached->source->name, $cached->cache_id, $cached->compile_id, $content, 73 $timestamp, $cached->source->uid) 74 ) { 75 return; 76 } 77 $cached->content = $content; 78 $cached->timestamp = (int) $timestamp; 79 $cached->exists = !!$cached->timestamp; 80 } 81 82 /** 83 * Read the cached template and process the header 84 * 85 * @param \Smarty_Internal_Template $_smarty_tpl do not change variable name, is used by compiled template 86 * @param Smarty_Template_Cached $cached cached object 87 * @param boolean $update flag if called because cache update 88 * 89 * @return boolean true or false if the cached content does not exist 90 */ 91 public function process(Smarty_Internal_Template $_smarty_tpl, Smarty_Template_Cached $cached = null, 92 $update = false) 93 { 94 if (!$cached) { 95 $cached = $_smarty_tpl->cached; 96 } 97 $content = $cached->content ? $cached->content : null; 98 $timestamp = $cached->timestamp ? $cached->timestamp : null; 99 if ($content === null || !$timestamp) { 100 if (!$this->fetch($_smarty_tpl->cached->filepath, $_smarty_tpl->source->name, $_smarty_tpl->cache_id, 101 $_smarty_tpl->compile_id, $content, $timestamp, $_smarty_tpl->source->uid) 102 ) { 103 return false; 104 } 105 } 106 if (isset($content)) { 107 eval("?>" . $content); 108 109 return true; 110 } 111 112 return false; 113 } 114 115 /** 116 * Write the rendered template output to cache 117 * 118 * @param Smarty_Internal_Template $_template template object 119 * @param string $content content to cache 120 * 121 * @return boolean success 122 */ 123 public function writeCachedContent(Smarty_Internal_Template $_template, $content) 124 { 125 $this->addMetaTimestamp($content); 126 127 return $this->write(array($_template->cached->filepath => $content), $_template->cache_lifetime); 128 } 129 130 /** 131 * Read cached template from cache 132 * 133 * @param Smarty_Internal_Template $_template template object 134 * 135 * @return string|false content 136 */ 137 public function readCachedContent(Smarty_Internal_Template $_template) 138 { 139 $content = $_template->cached->content ? $_template->cached->content : null; 140 $timestamp = null; 141 if ($content === null) { 142 if (!$this->fetch($_template->cached->filepath, $_template->source->name, $_template->cache_id, 143 $_template->compile_id, $content, $timestamp, $_template->source->uid) 144 ) { 145 return false; 146 } 147 } 148 if (isset($content)) { 149 return $content; 150 } 151 return false; 152 } 153 154 /** 155 * Empty cache 156 * {@internal the $exp_time argument is ignored altogether }} 157 * 158 * @param Smarty $smarty Smarty object 159 * @param integer $exp_time expiration time [being ignored] 160 * 161 * @return integer number of cache files deleted [always -1] 162 * @uses purge() to clear the whole store 163 * @uses invalidate() to mark everything outdated if purge() is inapplicable 164 */ 165 public function clearAll(Smarty $smarty, $exp_time = null) 166 { 167 if (!$this->purge()) { 168 $this->invalidate(null); 169 } 170 return - 1; 171 } 172 173 /** 174 * Empty cache for a specific template 175 * {@internal the $exp_time argument is ignored altogether}} 176 * 177 * @param Smarty $smarty Smarty object 178 * @param string $resource_name template name 179 * @param string $cache_id cache id 180 * @param string $compile_id compile id 181 * @param integer $exp_time expiration time [being ignored] 182 * 183 * @return integer number of cache files deleted [always -1] 184 * @uses buildCachedFilepath() to generate the CacheID 185 * @uses invalidate() to mark CacheIDs parent chain as outdated 186 * @uses delete() to remove CacheID from cache 187 */ 188 public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time) 189 { 190 $uid = $this->getTemplateUid($smarty, $resource_name); 191 $cid = $uid . '#' . $this->sanitize($resource_name) . '#' . $this->sanitize($cache_id) . '#' . 192 $this->sanitize($compile_id); 193 $this->delete(array($cid)); 194 $this->invalidate($cid, $resource_name, $cache_id, $compile_id, $uid); 195 return - 1; 196 } 197 198 /** 199 * Get template's unique ID 200 * 201 * @param Smarty $smarty Smarty object 202 * @param string $resource_name template name 203 * 204 * @return string filepath of cache file 205 * @throws \SmartyException 206 * 207 */ 208 protected function getTemplateUid(Smarty $smarty, $resource_name) 209 { 210 if (isset($resource_name)) { 211 $source = Smarty_Template_Source::load(null, $smarty, $resource_name); 212 if ($source->exists) { 213 return $source->uid; 214 } 215 } 216 return ''; 217 } 218 219 /** 220 * Sanitize CacheID components 221 * 222 * @param string $string CacheID component to sanitize 223 * 224 * @return string sanitized CacheID component 225 */ 226 protected function sanitize($string) 227 { 228 $string = trim($string, '|'); 229 if (!$string) { 230 return ''; 231 } 232 return preg_replace('#[^\w\|]+#S', '_', $string); 233 } 234 235 /** 236 * Fetch and prepare a cache object. 237 * 238 * @param string $cid CacheID to fetch 239 * @param string $resource_name template name 240 * @param string $cache_id cache id 241 * @param string $compile_id compile id 242 * @param string $content cached content 243 * @param integer &$timestamp cached timestamp (epoch) 244 * @param string $resource_uid resource's uid 245 * 246 * @return boolean success 247 */ 248 protected function fetch($cid, $resource_name = null, $cache_id = null, $compile_id = null, &$content = null, 249 &$timestamp = null, $resource_uid = null) 250 { 251 $t = $this->read(array($cid)); 252 $content = !empty($t[ $cid ]) ? $t[ $cid ] : null; 253 $timestamp = null; 254 255 if ($content && ($timestamp = $this->getMetaTimestamp($content))) { 256 $invalidated = 257 $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid); 258 if ($invalidated > $timestamp) { 259 $timestamp = null; 260 $content = null; 261 } 262 } 263 264 return !!$content; 265 } 266 267 /** 268 * Add current microtime to the beginning of $cache_content 269 * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}} 270 * 271 * @param string &$content the content to be cached 272 */ 273 protected function addMetaTimestamp(&$content) 274 { 275 $mt = explode(" ", microtime()); 276 $ts = pack("NN", $mt[ 1 ], (int) ($mt[ 0 ] * 100000000)); 277 $content = $ts . $content; 278 } 279 280 /** 281 * Extract the timestamp the $content was cached 282 * 283 * @param string &$content the cached content 284 * 285 * @return float the microtime the content was cached 286 */ 287 protected function getMetaTimestamp(&$content) 288 { 289 extract(unpack('N1s/N1m/a*content', $content)); 290 /** 291 * @var int $s 292 * @var int $m 293 */ 294 return $s + ($m / 100000000); 295 } 296 297 /** 298 * Invalidate CacheID 299 * 300 * @param string $cid CacheID 301 * @param string $resource_name template name 302 * @param string $cache_id cache id 303 * @param string $compile_id compile id 304 * @param string $resource_uid source's uid 305 * 306 * @return void 307 */ 308 protected function invalidate($cid = null, $resource_name = null, $cache_id = null, $compile_id = null, 309 $resource_uid = null) 310 { 311 $now = microtime(true); 312 $key = null; 313 // invalidate everything 314 if (!$resource_name && !$cache_id && !$compile_id) { 315 $key = 'IVK#ALL'; 316 } // invalidate all caches by template 317 else { 318 if ($resource_name && !$cache_id && !$compile_id) { 319 $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name); 320 } // invalidate all caches by cache group 321 else { 322 if (!$resource_name && $cache_id && !$compile_id) { 323 $key = 'IVK#CACHE#' . $this->sanitize($cache_id); 324 } // invalidate all caches by compile id 325 else { 326 if (!$resource_name && !$cache_id && $compile_id) { 327 $key = 'IVK#COMPILE#' . $this->sanitize($compile_id); 328 } // invalidate by combination 329 else { 330 $key = 'IVK#CID#' . $cid; 331 } 332 } 333 } 334 } 335 $this->write(array($key => $now)); 336 } 337 338 /** 339 * Determine the latest timestamp known to the invalidation chain 340 * 341 * @param string $cid CacheID to determine latest invalidation timestamp of 342 * @param string $resource_name template name 343 * @param string $cache_id cache id 344 * @param string $compile_id compile id 345 * @param string $resource_uid source's filepath 346 * 347 * @return float the microtime the CacheID was invalidated 348 */ 349 protected function getLatestInvalidationTimestamp($cid, $resource_name = null, $cache_id = null, $compile_id = null, 350 $resource_uid = null) 351 { 352 // abort if there is no CacheID 353 if (false && !$cid) { 354 return 0; 355 } 356 // abort if there are no InvalidationKeys to check 357 if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) { 358 return 0; 359 } 360 361 // there are no InValidationKeys 362 if (!($values = $this->read($_cid))) { 363 return 0; 364 } 365 // make sure we're dealing with floats 366 $values = array_map('floatval', $values); 367 368 return max($values); 369 } 370 371 /** 372 * Translate a CacheID into the list of applicable InvalidationKeys. 373 * Splits "some|chain|into|an|array" into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... ) 374 * 375 * @param string $cid CacheID to translate 376 * @param string $resource_name template name 377 * @param string $cache_id cache id 378 * @param string $compile_id compile id 379 * @param string $resource_uid source's filepath 380 * 381 * @return array list of InvalidationKeys 382 * @uses $invalidationKeyPrefix to prepend to each InvalidationKey 383 */ 384 protected function listInvalidationKeys($cid, $resource_name = null, $cache_id = null, $compile_id = null, 385 $resource_uid = null) 386 { 387 $t = array('IVK#ALL'); 388 $_name = $_compile = '#'; 389 if ($resource_name) { 390 $_name .= $resource_uid . '#' . $this->sanitize($resource_name); 391 $t[] = 'IVK#TEMPLATE' . $_name; 392 } 393 if ($compile_id) { 394 $_compile .= $this->sanitize($compile_id); 395 $t[] = 'IVK#COMPILE' . $_compile; 396 } 397 $_name .= '#'; 398 $cid = trim($cache_id, '|'); 399 if (!$cid) { 400 return $t; 401 } 402 $i = 0; 403 while (true) { 404 // determine next delimiter position 405 $i = strpos($cid, '|', $i); 406 // add complete CacheID if there are no more delimiters 407 if ($i === false) { 408 $t[] = 'IVK#CACHE#' . $cid; 409 $t[] = 'IVK#CID' . $_name . $cid . $_compile; 410 $t[] = 'IVK#CID' . $_name . $_compile; 411 break; 412 } 413 $part = substr($cid, 0, $i); 414 // add slice to list 415 $t[] = 'IVK#CACHE#' . $part; 416 $t[] = 'IVK#CID' . $_name . $part . $_compile; 417 // skip past delimiter position 418 $i ++; 419 } 420 421 return $t; 422 } 423 424 /** 425 * Check is cache is locked for this template 426 * 427 * @param Smarty $smarty Smarty object 428 * @param Smarty_Template_Cached $cached cached object 429 * 430 * @return boolean true or false if cache is locked 431 */ 432 public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached) 433 { 434 $key = 'LOCK#' . $cached->filepath; 435 $data = $this->read(array($key)); 436 437 return $data && time() - $data[ $key ] < $smarty->locking_timeout; 438 } 439 440 /** 441 * Lock cache for this template 442 * 443 * @param Smarty $smarty Smarty object 444 * @param Smarty_Template_Cached $cached cached object 445 * 446 * @return bool|void 447 */ 448 public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached) 449 { 450 $cached->is_locked = true; 451 $key = 'LOCK#' . $cached->filepath; 452 $this->write(array($key => time()), $smarty->locking_timeout); 453 } 454 455 /** 456 * Unlock cache for this template 457 * 458 * @param Smarty $smarty Smarty object 459 * @param Smarty_Template_Cached $cached cached object 460 * 461 * @return bool|void 462 */ 463 public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached) 464 { 465 $cached->is_locked = false; 466 $key = 'LOCK#' . $cached->filepath; 467 $this->delete(array($key)); 468 } 469 470 /** 471 * Read values for a set of keys from cache 472 * 473 * @param array $keys list of keys to fetch 474 * 475 * @return array list of values with the given keys used as indexes 476 */ 477 abstract protected function read(array $keys); 478 479 /** 480 * Save values for a set of keys to cache 481 * 482 * @param array $keys list of values to save 483 * @param int $expire expiration time 484 * 485 * @return boolean true on success, false on failure 486 */ 487 abstract protected function write(array $keys, $expire = null); 488 489 /** 490 * Remove values from cache 491 * 492 * @param array $keys list of keys to delete 493 * 494 * @return boolean true on success, false on failure 495 */ 496 abstract protected function delete(array $keys); 497 498 /** 499 * Remove *all* values from cache 500 * 501 * @return boolean true on success, false on failure 502 */ 503 protected function purge() 504 { 505 return false; 506 } 507} 508