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