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(); // 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 = array()) 77 { 78 $this->depends = $depends; 79 $this->addDependencies(); 80 81 if ($this->_event) { 82 return $this->stats(Event::createAndTrigger($this->_event, $this, array($this, 'makeDefaultCacheDecision'))); 83 } else { 84 return $this->stats($this->makeDefaultCacheDecision()); 85 } 86 } 87 88 /** 89 * internal method containing cache use decision logic 90 * 91 * this function processes the following keys in the depends array 92 * purge - force a purge on any non empty value 93 * age - expire cache if older than age (seconds) 94 * files - expire cache if any file in this array was updated more recently than the cache 95 * 96 * Note that this function needs to be public as it is used as callback for the event handler 97 * 98 * can be overridden 99 * 100 * @internal This method may only be called by the event handler! Call \dokuwiki\Cache\Cache::useCache instead! 101 * 102 * @return bool see useCache() 103 */ 104 public function makeDefaultCacheDecision() 105 { 106 107 if ($this->_nocache) { 108 return false; 109 } // caching turned off 110 if (!empty($this->depends['purge'])) { 111 return false; 112 } // purge requested? 113 if (!($this->_time = @filemtime($this->cache))) { 114 return false; 115 } // cache exists? 116 117 // cache too old? 118 if (!empty($this->depends['age']) && ((time() - $this->_time) > $this->depends['age'])) { 119 return false; 120 } 121 122 if (!empty($this->depends['files'])) { 123 foreach ($this->depends['files'] as $file) { 124 if ($this->_time <= @filemtime($file)) { 125 return false; 126 } // cache older than files it depends on? 127 } 128 } 129 130 return true; 131 } 132 133 /** 134 * add dependencies to the depends array 135 * 136 * this method should only add dependencies, 137 * it should not remove any existing dependencies and 138 * it should only overwrite a dependency when the new value is more stringent than the old 139 */ 140 protected function addDependencies() 141 { 142 global $INPUT; 143 if ($INPUT->has('purge')) { 144 $this->depends['purge'] = true; 145 } // purge requested 146 } 147 148 /** 149 * retrieve the cached data 150 * 151 * @param bool $clean true to clean line endings, false to leave line endings alone 152 * @return string cache contents 153 */ 154 public function retrieveCache($clean = true) 155 { 156 return io_readFile($this->cache, $clean); 157 } 158 159 /** 160 * cache $data 161 * 162 * @param string $data the data to be cached 163 * @return bool true on success, false otherwise 164 */ 165 public function storeCache($data) 166 { 167 if ($this->_nocache) { 168 return false; 169 } 170 171 return io_savefile($this->cache, $data); 172 } 173 174 /** 175 * remove any cached data associated with this cache instance 176 */ 177 public function removeCache() 178 { 179 @unlink($this->cache); 180 } 181 182 /** 183 * Record cache hits statistics. 184 * (Only when debugging allowed, to reduce overhead.) 185 * 186 * @param bool $success result of this cache use attempt 187 * @return bool pass-thru $success value 188 */ 189 protected function stats($success) 190 { 191 global $conf; 192 static $stats = null; 193 static $file; 194 195 if (!$conf['allowdebug']) { 196 return $success; 197 } 198 199 if (is_null($stats)) { 200 $file = $conf['cachedir'] . '/cache_stats.txt'; 201 $lines = explode("\n", io_readFile($file)); 202 203 foreach ($lines as $line) { 204 $i = strpos($line, ','); 205 $stats[substr($line, 0, $i)] = $line; 206 } 207 } 208 209 if (isset($stats[$this->ext])) { 210 list($ext, $count, $hits) = explode(',', $stats[$this->ext]); 211 } else { 212 $ext = $this->ext; 213 $count = 0; 214 $hits = 0; 215 } 216 217 $count++; 218 if ($success) { 219 $hits++; 220 } 221 $stats[$this->ext] = "$ext,$count,$hits"; 222 223 io_saveFile($file, join("\n", $stats)); 224 225 return $success; 226 } 227 228 /** 229 * @return bool 230 */ 231 public function isNoCache() 232 { 233 return $this->_nocache; 234 } 235} 236