1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\Tracker;
10
11use Piwik\Access;
12use Piwik\ArchiveProcessor\Rules;
13use Piwik\Cache as PiwikCache;
14use Piwik\Config;
15use Piwik\Container\StaticContainer;
16use Piwik\Option;
17use Piwik\Piwik;
18use Piwik\Tracker;
19use Psr\Log\LoggerInterface;
20use function DI\object;
21
22/**
23 * Simple cache mechanism used in Tracker to avoid requesting settings from mysql on every request
24 *
25 */
26class Cache
27{
28    /**
29     * {@see self::withDelegatedCacheClears()}
30     * @var bool
31     */
32    private static $delegatingCacheClears;
33
34    /**
35     * {@see self::withDelegatedCacheClears()}
36     * @var array
37     */
38    private static $delegatedClears = [];
39
40    private static $cacheIdGeneral = 'general';
41
42    /**
43     * Public for tests only
44     * @var \Matomo\Cache\Lazy
45     */
46    public static $cache;
47
48    /**
49     * @return \Matomo\Cache\Lazy
50     */
51    private static function getCache()
52    {
53        if (is_null(self::$cache)) {
54            self::$cache = PiwikCache::getLazyCache();
55        }
56
57        return self::$cache;
58    }
59
60    private static function getTtl()
61    {
62        return Config::getInstance()->Tracker['tracker_cache_file_ttl'];
63    }
64
65    /**
66     * Returns array containing data about the website: goals, URLs, etc.
67     *
68     * @param int $idSite
69     * @return array
70     */
71    public static function getCacheWebsiteAttributes($idSite)
72    {
73        if ('all' === $idSite) {
74            return array();
75        }
76
77        $idSite = (int)$idSite;
78        if ($idSite <= 0) {
79            return array();
80        }
81
82        $cache = self::getCache();
83        $cacheId = self::getCacheKeyWebsiteAttributes($idSite);
84        $cacheContent = $cache->fetch($cacheId);
85
86        if (false !== $cacheContent) {
87            return $cacheContent;
88        }
89
90        return self::updateCacheWebsiteAttributes($idSite);
91    }
92
93    private static function getCacheKeyWebsiteAttributes($idSite)
94    {
95        return $idSite;
96    }
97
98    /**
99     * Updates the website specific tracker cache containing data about the website: goals, URLs, etc.
100     *
101     * @param int $idSite
102     *
103     * @return array
104     */
105    public static function updateCacheWebsiteAttributes($idSite)
106    {
107        $cache = self::getCache();
108        $cacheId = self::getCacheKeyWebsiteAttributes($idSite);
109
110        Tracker::initCorePiwikInTrackerMode();
111
112        $content = array();
113        Access::doAsSuperUser(function () use (&$content, $idSite) {
114            /**
115             * Triggered to get the attributes of a site entity that might be used by the
116             * Tracker.
117             *
118             * Plugins add new site attributes for use in other tracking events must
119             * use this event to put those attributes in the Tracker Cache.
120             *
121             * **Example**
122             *
123             *     public function getSiteAttributes($content, $idSite)
124             *     {
125             *         $sql = "SELECT info FROM " . Common::prefixTable('myplugin_extra_site_info') . " WHERE idsite = ?";
126             *         $content['myplugin_site_data'] = Db::fetchOne($sql, array($idSite));
127             *     }
128             *
129             * @param array &$content Array mapping of site attribute names with values.
130             * @param int $idSite The site ID to get attributes for.
131             */
132            Piwik::postEvent('Tracker.Cache.getSiteAttributes', array(&$content, $idSite));
133
134            $logger = StaticContainer::get(LoggerInterface::class);
135            $logger->debug("Website $idSite tracker cache was re-created.");
136        });
137
138        // if nothing is returned from the plugins, we don't save the content
139        // this is not expected: all websites are expected to have at least one URL
140        if (!empty($content)) {
141            $cache->save($cacheId, $content, self::getTtl());
142        }
143
144        Tracker::restoreTrackerPlugins();
145
146        return $content;
147    }
148
149    /**
150     * Clear general (global) cache
151     */
152    public static function clearCacheGeneral()
153    {
154        if (self::$delegatingCacheClears) {
155            self::$delegatedClears[__FUNCTION__] = [__FUNCTION__, []];
156            return;
157        }
158
159        self::getCache()->delete(self::$cacheIdGeneral);
160    }
161
162    /**
163     * Returns contents of general (global) cache.
164     * If the cache file tmp/cache/tracker/general.php does not exist yet, create it
165     *
166     * @return array
167     */
168    public static function getCacheGeneral()
169    {
170        $cache = self::getCache();
171        $cacheContent = $cache->fetch(self::$cacheIdGeneral);
172
173        if (false !== $cacheContent) {
174            return $cacheContent;
175        }
176
177        return self::updateGeneralCache();
178    }
179
180    /**
181     * Updates the contents of the general (global) cache.
182     *
183     * @return array
184     */
185    public static function updateGeneralCache()
186    {
187        Tracker::initCorePiwikInTrackerMode();
188        $cacheContent = array(
189            'isBrowserTriggerEnabled' => Rules::isBrowserTriggerEnabled(),
190            'lastTrackerCronRun' => Option::get('lastTrackerCronRun'),
191        );
192
193        /**
194         * Triggered before the [general tracker cache](/guides/all-about-tracking#the-tracker-cache)
195         * is saved to disk. This event can be used to add extra content to the cache.
196         *
197         * Data that is used during tracking but is expensive to compute/query should be
198         * cached to keep tracking efficient. One example of such data are options
199         * that are stored in the option table. Querying data for each tracking
200         * request means an extra unnecessary database query for each visitor action. Using
201         * a cache solves this problem.
202         *
203         * **Example**
204         *
205         *     public function setTrackerCacheGeneral(&$cacheContent)
206         *     {
207         *         $cacheContent['MyPlugin.myCacheKey'] = Option::get('MyPlugin_myOption');
208         *     }
209         *
210         * @param array &$cacheContent Array of cached data. Each piece of data must be
211         *                             mapped by name.
212         */
213        Piwik::postEvent('Tracker.setTrackerCacheGeneral', array(&$cacheContent));
214        self::setCacheGeneral($cacheContent);
215
216        $logger = StaticContainer::get(LoggerInterface::class);
217        $logger->debug("General tracker cache was re-created.");
218
219        Tracker::restoreTrackerPlugins();
220
221        return $cacheContent;
222    }
223
224    /**
225     * Store data in general (global cache)
226     *
227     * @param mixed $value
228     * @return bool
229     */
230    public static function setCacheGeneral($value)
231    {
232        $cache = self::getCache();
233
234        return $cache->save(self::$cacheIdGeneral, $value, self::getTtl());
235    }
236
237    /**
238     * Regenerate Tracker cache files
239     *
240     * @param array|int $idSites Array of idSites to clear cache for
241     */
242    public static function regenerateCacheWebsiteAttributes($idSites = array())
243    {
244        if (!is_array($idSites)) {
245            $idSites = array($idSites);
246        }
247
248        foreach ($idSites as $idSite) {
249            self::deleteCacheWebsiteAttributes($idSite);
250            self::getCacheWebsiteAttributes($idSite);
251        }
252    }
253
254    /**
255     * Delete existing Tracker cache
256     *
257     * @param string $idSite (website ID of the site to clear cache for
258     */
259    public static function deleteCacheWebsiteAttributes($idSite)
260    {
261        if (self::$delegatingCacheClears) {
262            self::$delegatedClears[__FUNCTION__ . $idSite] = [__FUNCTION__, func_get_args()];
263            return;
264        }
265
266        self::getCache()->delete((int)$idSite);
267    }
268
269    /**
270     * Deletes all Tracker cache files
271     */
272    public static function deleteTrackerCache()
273    {
274        if (self::$delegatingCacheClears) {
275            self::$delegatedClears[__FUNCTION__] = [__FUNCTION__, []];
276            return;
277        }
278
279        self::getCache()->flushAll();
280    }
281
282    /**
283     * Runs `$callback` without clearing any tracker cache, just collecting which delete methods were called.
284     * After `$callback` finishes, we clear caches, but just once per type of delete/clear method collected.
285     *
286     * Use this method if your code will create many cache clears in a short amount of time (eg, if you
287     * are invalidating a lot of archives at once).
288     *
289     * @param $callback
290     */
291    public static function withDelegatedCacheClears($callback)
292    {
293        try {
294            self::$delegatingCacheClears = true;
295            self::$delegatedClears = [];
296
297            return $callback();
298        } finally {
299            self::$delegatingCacheClears = false;
300
301            self::callAllDelegatedClears();
302
303            self::$delegatedClears = [];
304        }
305    }
306
307    private static function callAllDelegatedClears()
308    {
309        foreach (self::$delegatedClears as list($methodName, $params)) {
310            if (!method_exists(self::class, $methodName)) {
311                continue;
312            }
313
314            call_user_func_array([self::class, $methodName], $params);
315        }
316    }
317}
318