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