1<?php 2 3namespace dokuwiki\Cache; 4 5/** 6 * Generic handling of caching 7 */ 8class Cache 9{ 10 public $key = ''; // primary identifier for this item 11 public $ext = ''; // file ext for cache data, secondary identifier for this item 12 public $cache = ''; // cache file name 13 public $depends = array(); // array containing cache dependency information, 14 // used by makeDefaultCacheDecision to determine cache validity 15 16 protected $event = ''; // event to be triggered during useCache 17 protected $time; 18 protected $nocache = false; // if set to true, cache will not be used or stored 19 20 /** 21 * @param string $key primary identifier 22 * @param string $ext file extension 23 */ 24 public function __construct($key, $ext) 25 { 26 $this->key = $key; 27 $this->ext = $ext; 28 $this->cache = getCacheName($key, $ext); 29 } 30 31 /** 32 * @deprecated since 2019-02-02 use the respective getters instead! 33 */ 34 public function __get($key) 35 { 36 if ($key === '_event') { 37 trigger_error( 38 '\dokuwiki\Cache\Cache::_event is deprecated since 2019-02-02. Use \dokuwiki\Cache\Cache::getEvent()', 39 E_USER_DEPRECATED 40 ); 41 dbg_deprecated('\dokuwiki\Cache\Cache::getEvent()'); 42 return $this->getEvent(); 43 } 44 45 if ($key === '_time') { 46 trigger_error( 47 '\dokuwiki\Cache\Cache::_time is deprecated since 2019-02-02. Use \dokuwiki\Cache\Cache::getTime()', 48 E_USER_DEPRECATED 49 ); 50 dbg_deprecated('\dokuwiki\Cache\Cache::getTime()'); 51 return $this->getTime(); 52 } 53 return $this->$$key; 54 } 55 56 public function __set($name, $value) 57 { 58 if ($name === '_event') { 59 trigger_error( 60 '\dokuwiki\Cache\Cache::_event is deprecated since 2019-02-02. Use \dokuwiki\Cache\Cache::getEvent()', 61 E_USER_DEPRECATED 62 ); 63 dbg_deprecated('\dokuwiki\Cache\Cache::getEvent()'); 64 $this->setEvent($value); 65 } 66 $this->$$name = $value; 67 } 68 69 public function getTime() 70 { 71 return $this->time; 72 } 73 74 public function getEvent() 75 { 76 return $this->event; 77 } 78 79 public function setEvent($event) 80 { 81 $this->event = $event; 82 } 83 84 /** 85 * public method to determine whether the cache can be used 86 * 87 * to assist in centralisation of event triggering and calculation of cache statistics, 88 * don't override this function override makeDefaultCacheDecision() 89 * 90 * @param array $depends array of cache dependencies, support dependecies: 91 * 'age' => max age of the cache in seconds 92 * 'files' => cache must be younger than mtime of each file 93 * (nb. dependency passes if file doesn't exist) 94 * 95 * @return bool true if cache can be used, false otherwise 96 */ 97 public function useCache($depends = array()) 98 { 99 $this->depends = $depends; 100 $this->addDependencies(); 101 102 if ($this->event) { 103 return $this->stats(trigger_event($this->event, $this, array($this, 'makeDefaultCacheDecision'))); 104 } else { 105 return $this->stats($this->makeDefaultCacheDecision()); 106 } 107 } 108 109 /** 110 * internal method containing cache use decision logic 111 * 112 * this function processes the following keys in the depends array 113 * purge - force a purge on any non empty value 114 * age - expire cache if older than age (seconds) 115 * files - expire cache if any file in this array was updated more recently than the cache 116 * 117 * Note that this function needs to be public as it is used as callback for the event handler 118 * 119 * can be overridden 120 * 121 * @internal This method may only be called by the event handler! Call \dokuwiki\Cache\Cache::useCache instead! 122 * 123 * @return bool see useCache() 124 */ 125 public function makeDefaultCacheDecision() 126 { 127 128 if ($this->nocache) { 129 return false; 130 } // caching turned off 131 if (!empty($this->depends['purge'])) { 132 return false; 133 } // purge requested? 134 if (!($this->time = @filemtime($this->cache))) { 135 return false; 136 } // cache exists? 137 138 // cache too old? 139 if (!empty($this->depends['age']) && ((time() - $this->time) > $this->depends['age'])) { 140 return false; 141 } 142 143 if (!empty($this->depends['files'])) { 144 foreach ($this->depends['files'] as $file) { 145 if ($this->time <= @filemtime($file)) { 146 return false; 147 } // cache older than files it depends on? 148 } 149 } 150 151 return true; 152 } 153 154 /** 155 * add dependencies to the depends array 156 * 157 * this method should only add dependencies, 158 * it should not remove any existing dependencies and 159 * it should only overwrite a dependency when the new value is more stringent than the old 160 */ 161 protected function addDependencies() 162 { 163 global $INPUT; 164 if ($INPUT->has('purge')) { 165 $this->depends['purge'] = true; 166 } // purge requested 167 } 168 169 /** 170 * retrieve the cached data 171 * 172 * @param bool $clean true to clean line endings, false to leave line endings alone 173 * @return string cache contents 174 */ 175 public function retrieveCache($clean = true) 176 { 177 return io_readFile($this->cache, $clean); 178 } 179 180 /** 181 * cache $data 182 * 183 * @param string $data the data to be cached 184 * @return bool true on success, false otherwise 185 */ 186 public function storeCache($data) 187 { 188 if ($this->nocache) { 189 return false; 190 } 191 192 return io_savefile($this->cache, $data); 193 } 194 195 /** 196 * remove any cached data associated with this cache instance 197 */ 198 public function removeCache() 199 { 200 @unlink($this->cache); 201 } 202 203 /** 204 * Record cache hits statistics. 205 * (Only when debugging allowed, to reduce overhead.) 206 * 207 * @param bool $success result of this cache use attempt 208 * @return bool pass-thru $success value 209 */ 210 protected function stats($success) 211 { 212 global $conf; 213 static $stats = null; 214 static $file; 215 216 if (!$conf['allowdebug']) { 217 return $success; 218 } 219 220 if (is_null($stats)) { 221 $file = $conf['cachedir'] . '/cache_stats.txt'; 222 $lines = explode("\n", io_readFile($file)); 223 224 foreach ($lines as $line) { 225 $i = strpos($line, ','); 226 $stats[substr($line, 0, $i)] = $line; 227 } 228 } 229 230 if (isset($stats[$this->ext])) { 231 list($ext, $count, $hits) = explode(',', $stats[$this->ext]); 232 } else { 233 $ext = $this->ext; 234 $count = 0; 235 $hits = 0; 236 } 237 238 $count++; 239 if ($success) { 240 $hits++; 241 } 242 $stats[$this->ext] = "$ext,$count,$hits"; 243 244 io_saveFile($file, join("\n", $stats)); 245 246 return $success; 247 } 248 249 /** 250 * @return bool 251 */ 252 public function isNoCache() 253 { 254 return $this->nocache; 255 } 256} 257