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