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