1 <?php
2 
3 namespace dokuwiki\Cache;
4 
5 use dokuwiki\Debug\PropertyDeprecationHelper;
6 use dokuwiki\Extension\Event;
7 
8 /**
9  * Generic handling of caching
10  */
11 class 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