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