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