xref: /dokuwiki/inc/Cache/Cache.php (revision e1d9dcc8b460b6f029ac9c8d5d3b8d23b6e73402)
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