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