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