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