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