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