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