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