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