1<?php
2// +----------------------------------------------------------------------+
3// | PEAR :: Cache                                                        |
4// +----------------------------------------------------------------------+
5// | Copyright (c) 1997-2003 The PHP Group                                |
6// +----------------------------------------------------------------------+
7// | This source file is subject to version 2.0 of the PHP license,       |
8// | that is bundled with this package in the file LICENSE, and is        |
9// | available at through the world-wide-web at                           |
10// | http://www.php.net/license/2_02.txt.                                 |
11// | If you did not receive a copy of the PHP license and are unable to   |
12// | obtain it through the world-wide-web, please send a note to          |
13// | license@php.net so we can mail you a copy immediately.               |
14// +----------------------------------------------------------------------+
15// | Authors: Ulf Wendel <ulf.wendel@phpdoc.de>                           |
16// |          Sebastian Bergmann <sb@sebastian-bergmann.de>               |
17// |          Christian Stocker <chregu@phant.ch>                         |
18// +----------------------------------------------------------------------+
19//
20// $Id: Container.php 293863 2010-01-23 03:46:52Z clockwerx $
21
22require_once 'Cache/Error.php';
23
24/**
25* Common base class of all cache storage container.
26*
27* To speed up things we do a preload you should know about, otherwise it might
28* play you a trick. The Cache controller classes (Cache/Cache, Cache/Output, ...)
29* usually do something like is (isCached($id) && !isExpired($id)) return $container->load($id).
30* if you implement isCached(), isExpired() and load() straight ahead, each of this
31* functions will result in a storage medium (db, file,...) access. This generates too much load.
32* Now, a simple speculative preload should saves time in most cases. Whenever
33* one of the mentioned methods is invoked we preload the cached dataset into class variables.
34* That means that we have only one storage medium access for the sequence
35*  (isCached($id) && !isExpired($id)) return $container->load($id).
36* The bad thing is that the preloaded data might be outdated meanwhile, which is
37* unlikely but for you power users, be warned. If you do not want the preload
38* you should switch it off by setting the class variable $preload to false. Anyway, this is
39* not recommended!
40*
41* @author   Ulf Wendel <ulf.wendel@phpdoc.de>
42* @version  $Id: Container.php 293863 2010-01-23 03:46:52Z clockwerx $
43* @package  Cache
44* @access   public
45* @abstract
46*/
47class Cache_Container
48{
49
50    /**
51    * Flag indicating wheter to preload datasets.
52    *
53    * See the class description for more details.
54    *
55    * @var  boolean
56    */
57    var $preload = true;
58
59    /**
60    * ID of a preloaded dataset
61    *
62    * @var  string
63    */
64    var $id = '';
65
66    /**
67    * Cache group of a preloaded dataset
68    *
69    * @var  string
70    */
71    var $group = '';
72
73    /**
74    * Expiration timestamp of a preloaded dataset.
75    *
76    * @var  integer 0 means never, endless
77    */
78    var $expires = 0;
79
80    /**
81    * Value of a preloaded dataset.
82    *
83    * @var  string
84    */
85    var $cachedata = '';
86
87    /**
88    * Preloaded userdata field.
89    *
90    * @var  string
91    */
92    var $userdata = '';
93
94    /**
95    * Flag indicating that the dataset requested for preloading is unknown.
96    *
97    * @var  boolean
98    */
99    var $unknown = true;
100
101    /**
102    * Encoding mode for cache data: base64 or addslashes() (slash).
103    *
104    * @var  string  base64 or slash
105    */
106    var $encoding_mode = 'base64';
107
108    /**
109    * Highwater mark - maximum space required by all cache entries.
110    *
111    * Whenever the garbage collection runs it checks the amount of space
112    * required by all cache entries. If it's more than n (highwater) bytes
113    * the garbage collection deletes as many entries as necessary to reach the
114    * lowwater mark.
115    *
116    * @var  int
117    * @see  lowwater
118    */
119    var $highwater = 2048000;
120
121
122    /**
123    * Lowwater mark
124    *
125    * @var  int
126    * @see  highwater
127    */
128    var $lowwater = 1536000;
129
130
131    /**
132    * Options that can be set in every derived class using it's constructor.
133    *
134    * @var  array
135    */
136    var $allowed_options = array('encoding_mode', 'highwater', 'lowwater');
137
138
139    /**
140    * Loads a dataset from the cache.
141    *
142    * @param    string  dataset ID
143    * @param    string  cache group
144    * @return   mixed   dataset value or null on failure
145    * @access   public
146    */
147    function load($id, $group)
148    {
149        if ($this->preload) {
150            if ($this->id != $id || $this->group != $group) {
151                $this->preload($id, $group);
152            }
153            return $this->cachedata;
154        }
155
156        $ret = $this->fetch($id, $group);
157        if (PEAR::isError($ret)) {
158            return $ret;
159        }
160
161        list( , $data, ) = $ret;
162        return $data;
163    } // end func load
164
165    /**
166    * Returns the userdata field of a cached data set.
167    *
168    * @param    string  dataset ID
169    * @param    string  cache group
170    * @return   string  userdata
171    * @access   public
172    */
173    function getUserdata($id, $group)
174    {
175        if ($this->preload) {
176            if ($this->id != $id || $this->group != $group) {
177                $this->preload($id, $group);
178            }
179            return $this->userdata;
180        }
181
182        $ret = $this->fetch($id, $group);
183        if (PEAR::isError($ret)) {
184            return $ret;
185        }
186
187        list( , , $userdata) = $ret;
188        return $userdata;
189    } // end func getUserdata
190
191    /**
192    * Checks if a dataset is expired.
193    *
194    * @param    string  dataset ID
195    * @param    string  cache group
196    * @param    integer maximum age timestamp
197    * @return   boolean
198    * @access   public
199    */
200    function isExpired($id, $group, $max_age)
201    {
202        if ($this->preload) {
203            if ($this->id != $id || $this->group != $group) {
204                $this->preload($id, $group);
205            }
206            if ($this->unknown) {
207                return false;
208            }
209        } else {
210            // check if at all it is cached
211            if (!$this->isCached($id, $group)) {
212                return false;
213            }
214            // I'm lazy...
215            $ret = $this->fetch($id, $group);
216            if (PEAR::isError($ret)) {
217                return $ret;
218            }
219
220            list($this->expires, , ) = $ret;
221        }
222
223        // endless
224        if (0 == $this->expires) {
225            return false;
226        }
227        // you feel fine, Ulf?
228        if ($expired  = ($this->expires <= time() || ($max_age && ($this->expires <= $max_age))) ) {
229
230           $this->remove($id, $group);
231           $this->flushPreload();
232        }
233        return $expired;
234    } // end func isExpired
235
236    /**
237    * Checks if a dataset is cached.
238    *
239    * @param    string  dataset ID
240    * @param    string  cache group
241    * @return   boolean
242    */
243    function isCached($id, $group)
244    {
245        if ($this->preload) {
246            if ($this->id != $id || $this->group != $group) {
247                $this->preload($id, $group);
248            }
249            return !($this->unknown);
250        }
251        return $this->idExists($id, $group);
252    } // end func isCached
253
254    //
255    // abstract methods
256    //
257
258    /**
259    * Fetches a dataset from the storage medium.
260    *
261    * @param    string  dataset ID
262    * @param    string  cache group
263    * @return   array   format: [expire date, cached data, user data]
264    * @throws   Cache_Error
265    * @abstract
266    */
267    function fetch($id, $group)
268    {
269        return array(null, null, null);
270    } // end func fetch
271
272    /**
273    * Stores a dataset.
274    *
275    * @param    string  dataset ID
276    * @param    mixed   data to store
277    * @param    mixed   userdefined expire date
278    * @param    string  cache group
279    * @param    string  additional userdefined data
280    * @return   boolean
281    * @throws   Cache_Error
282    * @access   public
283    * @abstract
284    */
285    function save($id, $data, $expire, $group, $userdata)
286    {
287        // QUESTION: Should we update the preload buffer instead?
288        // Don't think so as the sequence save()/load() is unlikely.
289        $this->flushPreload($id, $group);
290        return null;
291    } // end func save
292
293    /**
294    * Removes a dataset.
295    *
296    * @param    string  dataset ID
297    * @param    string  cache group
298    * @return   boolean
299    * @access   public
300    * @abstract
301    */
302    function remove($id, $group)
303    {
304        $this->flushPreload($id, $group);
305        return null;
306    } // end func remove
307
308    /**
309    * Flushes the cache - removes all caches datasets from the cache.
310    *
311    * @param    string      If a cache group is given only the group will be flushed
312    * @return   integer     Number of removed datasets, -1 on failure
313    * @access   public
314    * @abstract
315    */
316    function flush($group)
317    {
318        $this->flushPreload();
319        return null;
320    } // end func flush
321
322    /**
323    * Checks if a dataset exists.
324    *
325    * @param    string  dataset ID
326    * @param    string  cache group
327    * @return   boolean
328    * @access   public
329    * @abstract
330    */
331    function idExists($id, $group)
332    {
333        return null;
334    } // end func idExists
335
336    /**
337    * Starts the garbage collection.
338    *
339    * @param int $gc_maxlifetime The maximum lifetime (seconds) for a cache
340    *                            entry. Implemented by containers,
341    *
342    * @access   public
343    * @abstract
344    */
345    function garbageCollection($gc_maxlifetime)
346    {
347        $this->flushPreload();
348    } // end func garbageCollection
349
350    /**
351    * Does a speculative preload of a dataset
352    *
353    * @param    string  dataset ID
354    * @param    string  cache group
355    * @return   boolean
356    */
357    function preload($id, $group)
358    {
359        // whatever happens, remember the preloaded ID
360        $this->id = $id;
361        $this->group = $group;
362
363        $ret = $this->fetch($id, $group);
364        if (PEAR::isError($ret)) {
365            return $ret;
366        }
367
368        list($this->expires, $this->cachedata, $this->userdata) = $ret;
369        if ($this->expires === null) {
370            // Uuups, unknown ID
371            $this->flushPreload();
372            return false;
373        }
374
375        $this->unknown = false;
376
377        return true;
378    } // end func preload
379
380    /**
381    * Flushes the internal preload buffer.
382    *
383    * save(), remove() and flush() must call this method
384    * to preevent differences between the preloaded values and
385    * the real cache contents.
386    *
387    * @param    string  dataset ID, if left out the preloaded values will be flushed.
388    *                   If given the preloaded values will only be flushed if they are
389    *                   equal to the given id and group
390    * @param    string  cache group
391    * @see  preload()
392    */
393    function flushPreload($id = '', $group = 'default')
394    {
395        if (!$id || ($this->id == $id && $this->group == $group)) {
396            // clear the internal preload values
397            $this->id = '';
398            $this->group = '';
399            $this->cachedata = '';
400            $this->userdata = '';
401            $this->expires = -1;
402            $this->unknown = true;
403        }
404    } // end func flushPreload
405
406    /**
407    * Imports the requested datafields as object variables if allowed
408    *
409    * @param    array   List of fields to be imported as object variables
410    * @param    array   List of allowed datafields
411    */
412    function setOptions($requested, $allowed)
413    {
414        foreach ($allowed as $k => $field) {
415            if (isset($requested[$field])) {
416                $this->$field = $requested[$field];
417            }
418        }
419    } // end func setOptions
420
421    /**
422    * Encodes the data for the storage container.
423    *
424    * @var  mixed data to encode
425    */
426    function encode($data)
427    {
428        if ($this->encoding_mode == 'base64') {
429            return base64_encode(serialize($data));
430        } else {
431            return serialize($data);
432        }
433    } // end func encode
434
435
436    /**
437    * Decodes the data from the storage container.
438    *
439    * @var  mixed
440    */
441    function decode($data)
442    {
443        if ($this->encoding_mode == 'base64') {
444            return unserialize(base64_decode($data));
445        } else {
446            return unserialize($data);
447        }
448    } // end func decode
449
450
451    /**
452    * Translates human readable/relative times in unixtime
453    *
454    * @param  mixed   can be in the following formats:
455    *               human readable          : yyyymmddhhmm[ss]] eg: 20010308095100
456    *               relative in seconds (1) : +xx              eg: +10
457    *               relative in seconds (2) : x <  946681200   eg: 10
458    *               absolute unixtime       : x < 2147483648   eg: 2147483648
459    *               see comments in code for details
460    * @return integer unix timestamp
461    */
462    function getExpiresAbsolute($expires)
463    {
464        if (!$expires) {
465            return 0;
466        }
467        //for api-compatibility, one has not to provide a "+",
468        // if integer is < 946681200 (= Jan 01 2000 00:00:00)
469        if ($expires[0] == '+' || $expires < 946681200) {
470            return(time() + $expires);
471        } elseif ($expires < 100000000000) {
472            //if integer is < 100000000000 (= in 3140 years),
473            // it must be an absolut unixtime
474            // (since the "human readable" definition asks for a higher number)
475            return $expires;
476        } else {
477            // else it's "human readable";
478            $year = substr($expires, 0, 4);
479            $month = substr($expires, 4, 2);
480            $day = substr($expires, 6, 2);
481            $hour = substr($expires, 8, 2);
482            $minute = substr($expires, 10, 2);
483            $second = substr($expires, 12, 2);
484            return mktime($hour, $minute, $second, $month, $day, $year);
485        }
486
487    } // end func getExpireAbsolute
488
489} // end class Container
490?>
491