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