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