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