xref: /dokuwiki/inc/cache.php (revision 48c8c96f687ddf5eea6750db55be522391ba4e63)
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    // for wiki pages, check metadata dependencies
221    if (isset($this->page)) {
222      $metadata = p_get_metadata($this->page);
223
224      // check currnent link existence is consistent with cache version
225      // first check the purgefile
226      // - if the cache is more recent that the purgefile we know no links can have been updated
227      if ($this->_time < @filemtime($conf['cachedir'].'/purgefile')) {
228
229#       $links = p_get_metadata($this->page,"relation references");
230        $links = $metadata['relation']['references'];
231
232        if (!empty($links)) {
233          foreach ($links as $id => $exists) {
234            if ($exists != page_exists($id,'',false)) return false;
235          }
236        }
237      }
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