xref: /dokuwiki/inc/cache.php (revision abbfdfefc9bf5549b7f6a6c6697fa4f4426e7922)
1<?php
2/**
3 * Generic class to handle caching
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Chris Smith <chris@jalakai.co.uk>
7 */
8
9if(!defined('DOKU_INC')) die('meh.');
10
11/**
12 * Generic handling of caching
13 */
14class cache {
15    public $key = '';          // primary identifier for this item
16    public $ext = '';          // file ext for cache data, secondary identifier for this item
17    public $cache = '';        // cache file name
18    public $depends = array(); // array containing cache dependency information,
19                               //   used by _useCache to determine cache validity
20
21    var $_event = '';       // event to be triggered during useCache
22    var $_time;
23    var $_nocache = false;  // if set to true, cache will not be used or stored
24
25    /**
26     * @param string $key primary identifier
27     * @param string $ext file extension
28     */
29    public function __construct($key,$ext) {
30        $this->key = $key;
31        $this->ext = $ext;
32        $this->cache = getCacheName($key,$ext);
33    }
34
35    /**
36     * public method to determine whether the cache can be used
37     *
38     * to assist in centralisation of event triggering and calculation of cache statistics,
39     * don't override this function override _useCache()
40     *
41     * @param  array   $depends   array of cache dependencies, support dependecies:
42     *                            'age'   => max age of the cache in seconds
43     *                            'files' => cache must be younger than mtime of each file
44     *                                       (nb. dependency passes if file doesn't exist)
45     *
46     * @return bool    true if cache can be used, false otherwise
47     */
48    public function useCache($depends=array()) {
49        $this->depends = $depends;
50        $this->_addDependencies();
51
52        if ($this->_event) {
53            return $this->_stats(trigger_event($this->_event, $this, array($this,'_useCache')));
54        } else {
55            return $this->_stats($this->_useCache());
56        }
57    }
58
59    /**
60     * private method containing cache use decision logic
61     *
62     * this function processes the following keys in the depends array
63     *   purge - force a purge on any non empty value
64     *   age   - expire cache if older than age (seconds)
65     *   files - expire cache if any file in this array was updated more recently than the cache
66     *
67     * Note that this function needs to be public as it is used as callback for the event handler
68     *
69     * can be overridden
70     *
71     * @return bool               see useCache()
72     */
73    public function _useCache() {
74
75        if ($this->_nocache) return false;                              // caching turned off
76        if (!empty($this->depends['purge'])) return false;              // purge requested?
77        if (!($this->_time = @filemtime($this->cache))) return false;   // cache exists?
78
79        // cache too old?
80        if (!empty($this->depends['age']) && ((time() - $this->_time) > $this->depends['age'])) return false;
81
82        if (!empty($this->depends['files'])) {
83            foreach ($this->depends['files'] as $file) {
84                if ($this->_time <= @filemtime($file)) return false;         // cache older than files it depends on?
85            }
86        }
87
88        return true;
89    }
90
91    /**
92     * add dependencies to the depends array
93     *
94     * this method should only add dependencies,
95     * it should not remove any existing dependencies and
96     * it should only overwrite a dependency when the new value is more stringent than the old
97     */
98    protected function _addDependencies() {
99        global $INPUT;
100        if ($INPUT->has('purge')) $this->depends['purge'] = true;   // purge requested
101    }
102
103    /**
104     * retrieve the cached data
105     *
106     * @param   bool   $clean   true to clean line endings, false to leave line endings alone
107     * @return  string          cache contents
108     */
109    public function retrieveCache($clean=true) {
110        return io_readFile($this->cache, $clean);
111    }
112
113    /**
114     * cache $data
115     *
116     * @param   string $data   the data to be cached
117     * @return  bool           true on success, false otherwise
118     */
119    public function storeCache($data) {
120        if ($this->_nocache) return false;
121
122        return io_savefile($this->cache, $data);
123    }
124
125    /**
126     * remove any cached data associated with this cache instance
127     */
128    public function removeCache() {
129        @unlink($this->cache);
130    }
131
132    /**
133     * Record cache hits statistics.
134     * (Only when debugging allowed, to reduce overhead.)
135     *
136     * @param    bool   $success   result of this cache use attempt
137     * @return   bool              pass-thru $success value
138     */
139    protected function _stats($success) {
140        global $conf;
141        static $stats = null;
142        static $file;
143
144        if (!$conf['allowdebug']) { return $success; }
145
146        if (is_null($stats)) {
147            $file = $conf['cachedir'].'/cache_stats.txt';
148            $lines = explode("\n",io_readFile($file));
149
150            foreach ($lines as $line) {
151                $i = strpos($line,',');
152                $stats[substr($line,0,$i)] = $line;
153            }
154        }
155
156        if (isset($stats[$this->ext])) {
157            list($ext,$count,$hits) = explode(',',$stats[$this->ext]);
158        } else {
159            $ext = $this->ext;
160            $count = 0;
161            $hits = 0;
162        }
163
164        $count++;
165        if ($success) $hits++;
166        $stats[$this->ext] = "$ext,$count,$hits";
167
168        io_saveFile($file,join("\n",$stats));
169
170        return $success;
171    }
172}
173
174/**
175 * Parser caching
176 */
177class cache_parser extends cache {
178
179    public $file = '';       // source file for cache
180    public $mode = '';       // input mode (represents the processing the input file will undergo)
181    public $page = '';
182
183    var $_event = 'PARSER_CACHE_USE';
184
185    /**
186     *
187     * @param string $id page id
188     * @param string $file source file for cache
189     * @param string $mode input mode
190     */
191    public function __construct($id, $file, $mode) {
192        if ($id) $this->page = $id;
193        $this->file = $file;
194        $this->mode = $mode;
195
196        parent::__construct($file.$_SERVER['HTTP_HOST'].$_SERVER['SERVER_PORT'],'.'.$mode);
197    }
198
199    /**
200     * method contains cache use decision logic
201     *
202     * @return bool               see useCache()
203     */
204    public function _useCache() {
205
206        if (!file_exists($this->file)) return false;                   // source exists?
207        return parent::_useCache();
208    }
209
210    protected function _addDependencies() {
211
212        // parser cache file dependencies ...
213        $files = array($this->file,                              // ... source
214                DOKU_INC.'inc/parser/parser.php',                // ... parser
215                DOKU_INC.'inc/parser/handler.php',               // ... handler
216                );
217        $files = array_merge($files, getConfigFiles('main'));    // ... wiki settings
218
219        $this->depends['files'] = !empty($this->depends['files']) ? array_merge($files, $this->depends['files']) : $files;
220        parent::_addDependencies();
221    }
222
223}
224
225/**
226 * Caching of data of renderer
227 */
228class cache_renderer extends cache_parser {
229
230    /**
231     * method contains cache use decision logic
232     *
233     * @return bool               see useCache()
234     */
235    public function _useCache() {
236        global $conf;
237
238        if (!parent::_useCache()) return false;
239
240        if (!isset($this->page)) {
241            return true;
242        }
243
244        if ($this->_time < @filemtime(metaFN($this->page,'.meta'))) return false;         // meta cache older than file it depends on?
245
246        // check current link existence is consistent with cache version
247        // first check the purgefile
248        // - if the cache is more recent than the purgefile we know no links can have been updated
249        if ($this->_time >= @filemtime($conf['cachedir'].'/purgefile')) {
250            return true;
251        }
252
253        // for wiki pages, check metadata dependencies
254        $metadata = p_get_metadata($this->page);
255
256        if (!isset($metadata['relation']['references']) ||
257                empty($metadata['relation']['references'])) {
258            return true;
259        }
260
261        foreach ($metadata['relation']['references'] as $id => $exists) {
262            if ($exists != page_exists($id,'',false)) return false;
263        }
264
265        return true;
266    }
267
268    protected function _addDependencies() {
269        global $conf;
270
271        // default renderer cache file 'age' is dependent on 'cachetime' setting, two special values:
272        //    -1 : do not cache (should not be overridden)
273        //    0  : cache never expires (can be overridden) - no need to set depends['age']
274        if ($conf['cachetime'] == -1) {
275            $this->_nocache = true;
276            return;
277        } elseif ($conf['cachetime'] > 0) {
278            $this->depends['age'] = isset($this->depends['age']) ?
279                min($this->depends['age'],$conf['cachetime']) : $conf['cachetime'];
280        }
281
282        // renderer cache file dependencies ...
283        $files = array(
284                DOKU_INC.'inc/parser/'.$this->mode.'.php',       // ... the renderer
285                );
286
287        // page implies metadata and possibly some other dependencies
288        if (isset($this->page)) {
289
290            $valid = p_get_metadata($this->page, 'date valid');         // for xhtml this will render the metadata if needed
291            if (!empty($valid['age'])) {
292                $this->depends['age'] = isset($this->depends['age']) ?
293                    min($this->depends['age'],$valid['age']) : $valid['age'];
294            }
295        }
296
297        $this->depends['files'] = !empty($this->depends['files']) ? array_merge($files, $this->depends['files']) : $files;
298        parent::_addDependencies();
299    }
300}
301
302/**
303 * Caching of parser instructions
304 */
305class cache_instructions extends cache_parser {
306
307    /**
308     * @param string $id page id
309     * @param string $file source file for cache
310     */
311    public function __construct($id, $file) {
312        parent::__construct($id, $file, 'i');
313    }
314
315    /**
316     * retrieve the cached data
317     *
318     * @param   bool   $clean   true to clean line endings, false to leave line endings alone
319     * @return  array          cache contents
320     */
321    public function retrieveCache($clean=true) {
322        $contents = io_readFile($this->cache, false);
323        return !empty($contents) ? unserialize($contents) : array();
324    }
325
326    /**
327     * cache $instructions
328     *
329     * @param   array $instructions  the instruction to be cached
330     * @return  bool                  true on success, false otherwise
331     */
332    public function storeCache($instructions) {
333        if ($this->_nocache) return false;
334
335        return io_savefile($this->cache,serialize($instructions));
336    }
337}
338