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