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