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