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; 10 11use Exception; 12use Piwik\Container\StaticContainer; 13use Piwik\Plugins\BulkTracking\Tracker\Requests; 14use Piwik\Plugins\PrivacyManager\Config as PrivacyManagerConfig; 15use Piwik\Tracker\Db as TrackerDb; 16use Piwik\Tracker\Db\DbException; 17use Piwik\Tracker\Handler; 18use Piwik\Tracker\Request; 19use Piwik\Tracker\RequestSet; 20use Piwik\Tracker\TrackerConfig; 21use Piwik\Tracker\Visit; 22use Piwik\Plugin\Manager as PluginManager; 23use Psr\Log\LoggerInterface; 24 25/** 26 * Class used by the logging script piwik.php called by the javascript tag. 27 * Handles the visitor and their actions on the website, saves the data in the DB, 28 * saves information in the cookie, etc. 29 * 30 * We try to include as little files as possible (no dependency on 3rd party modules). 31 */ 32class Tracker 33{ 34 /** 35 * @var Db 36 */ 37 private static $db = null; 38 39 // We use hex ID that are 16 chars in length, ie. 64 bits IDs 40 const LENGTH_HEX_ID_STRING = 16; 41 const LENGTH_BINARY_ID = 8; 42 43 public static $initTrackerMode = false; 44 45 private $countOfLoggedRequests = 0; 46 protected $isInstalled = null; 47 48 /** 49 * @var LoggerInterface 50 */ 51 private $logger; 52 53 public function __construct() 54 { 55 $this->logger = StaticContainer::get(LoggerInterface::class); 56 } 57 58 public function isDebugModeEnabled() 59 { 60 return array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS) && $GLOBALS['PIWIK_TRACKER_DEBUG'] === true; 61 } 62 63 public function shouldRecordStatistics() 64 { 65 $record = TrackerConfig::getConfigValue('record_statistics') != 0; 66 67 if (!$record) { 68 $this->logger->debug('Tracking is disabled in the config.ini.php via record_statistics=0'); 69 } 70 71 return $record && $this->isInstalled(); 72 } 73 74 public static function loadTrackerEnvironment() 75 { 76 SettingsServer::setIsTrackerApiRequest(); 77 if (empty($GLOBALS['PIWIK_TRACKER_DEBUG'])) { 78 $GLOBALS['PIWIK_TRACKER_DEBUG'] = self::isDebugEnabled(); 79 } 80 if (!empty($GLOBALS['PIWIK_TRACKER_DEBUG']) && !Common::isPhpCliMode()) { 81 Common::sendHeader('Content-Type: text/plain'); 82 } 83 PluginManager::getInstance()->loadTrackerPlugins(); 84 } 85 86 private function init() 87 { 88 $this->handleFatalErrors(); 89 90 if ($this->isDebugModeEnabled()) { 91 ErrorHandler::registerErrorHandler(); 92 ExceptionHandler::setUp(); 93 94 $this->logger->debug("Debug enabled - Input parameters: {params}", [ 95 'params' => var_export($_GET + $_POST, true), 96 ]); 97 } 98 } 99 100 public function isInstalled() 101 { 102 if (is_null($this->isInstalled)) { 103 $this->isInstalled = SettingsPiwik::isMatomoInstalled(); 104 } 105 106 return $this->isInstalled; 107 } 108 109 public function main(Handler $handler, RequestSet $requestSet) 110 { 111 try { 112 $this->init(); 113 $handler->init($this, $requestSet); 114 115 $this->track($handler, $requestSet); 116 } catch (Exception $e) { 117 StaticContainer::get(LoggerInterface::class)->debug("Tracker encountered an exception: {ex}", [$e]); 118 119 $handler->onException($this, $requestSet, $e); 120 } 121 122 Piwik::postEvent('Tracker.end'); 123 $response = $handler->finish($this, $requestSet); 124 125 $this->disconnectDatabase(); 126 127 return $response; 128 } 129 130 public function track(Handler $handler, RequestSet $requestSet) 131 { 132 if (!$this->shouldRecordStatistics()) { 133 return; 134 } 135 136 $requestSet->initRequestsAndTokenAuth(); 137 138 if ($requestSet->hasRequests()) { 139 $handler->onStartTrackRequests($this, $requestSet); 140 $handler->process($this, $requestSet); 141 $handler->onAllRequestsTracked($this, $requestSet); 142 } 143 } 144 145 /** 146 * @param Request $request 147 * @return array 148 */ 149 public function trackRequest(Request $request) 150 { 151 if ($request->isEmptyRequest()) { 152 $this->logger->debug('The request is empty'); 153 } else { 154 $this->logger->debug('Current datetime: {date}', [ 155 'date' => date("Y-m-d H:i:s", $request->getCurrentTimestamp()), 156 ]); 157 158 $visit = Visit\Factory::make(); 159 $visit->setRequest($request); 160 $visit->handle(); 161 } 162 163 // increment successfully logged request count. make sure to do this after try-catch, 164 // since an excluded visit is considered 'successfully logged' 165 ++$this->countOfLoggedRequests; 166 } 167 168 /** 169 * Used to initialize core Piwik components on a piwik.php request 170 * Eg. when cache is missed and we will be calling some APIs to generate cache 171 */ 172 public static function initCorePiwikInTrackerMode() 173 { 174 if (SettingsServer::isTrackerApiRequest() 175 && self::$initTrackerMode === false 176 ) { 177 self::$initTrackerMode = true; 178 require_once PIWIK_INCLUDE_PATH . '/core/Option.php'; 179 180 Access::getInstance(); 181 Config::getInstance(); 182 183 try { 184 Db::get(); 185 } catch (Exception $e) { 186 Db::createDatabaseObject(); 187 } 188 189 PluginManager::getInstance()->loadCorePluginsDuringTracker(); 190 } 191 } 192 193 public static function restoreTrackerPlugins() 194 { 195 if (SettingsServer::isTrackerApiRequest() && Tracker::$initTrackerMode) { 196 Plugin\Manager::getInstance()->loadTrackerPlugins(); 197 } 198 } 199 200 public function getCountOfLoggedRequests() 201 { 202 return $this->countOfLoggedRequests; 203 } 204 205 public function setCountOfLoggedRequests($numLoggedRequests) 206 { 207 $this->countOfLoggedRequests = $numLoggedRequests; 208 } 209 210 public function hasLoggedRequests() 211 { 212 return 0 !== $this->countOfLoggedRequests; 213 } 214 215 public function isDatabaseConnected() 216 { 217 return !is_null(self::$db); 218 } 219 220 public static function getDatabase() 221 { 222 if (is_null(self::$db)) { 223 try { 224 self::$db = TrackerDb::connectPiwikTrackerDb(); 225 } catch (Exception $e) { 226 $code = $e->getCode(); 227 // Note: PDOException might return a string as code, but we can't use this for DbException 228 throw new DbException($e->getMessage(), is_int($code) ? $code : 0); 229 } 230 } 231 232 return self::$db; 233 } 234 235 protected function disconnectDatabase() 236 { 237 if ($this->isDatabaseConnected()) { // note: I think we do this only for the tests 238 self::$db->disconnect(); 239 self::$db = null; 240 } 241 } 242 243 // for tests 244 public static function disconnectCachedDbConnection() 245 { 246 // code redundancy w/ above is on purpose; above disconnectDatabase depends on method that can potentially be overridden 247 if (!is_null(self::$db)) { 248 self::$db->disconnect(); 249 self::$db = null; 250 } 251 } 252 253 public static function setTestEnvironment($args = null, $requestMethod = null) 254 { 255 if (is_null($args)) { 256 $requests = new Requests(); 257 $args = $requests->getRequestsArrayFromBulkRequest($requests->getRawBulkRequest()); 258 $args = $_GET + $args; 259 } 260 261 if (is_null($requestMethod) && array_key_exists('REQUEST_METHOD', $_SERVER)) { 262 $requestMethod = $_SERVER['REQUEST_METHOD']; 263 } elseif (is_null($requestMethod)) { 264 $requestMethod = 'GET'; 265 } 266 267 // Do not run scheduled tasks during tests 268 if (!defined('DEBUG_FORCE_SCHEDULED_TASKS')) { 269 TrackerConfig::setConfigValue('scheduled_tasks_min_interval', 0); 270 } 271 272 // if nothing found in _GET/_POST and we're doing a POST, assume bulk request. in which case, 273 // we have to bypass authentication 274 if (empty($args) && $requestMethod == 'POST') { 275 TrackerConfig::setConfigValue('tracking_requests_require_authentication', 0); 276 } 277 278 // Tests can force the use of 3rd party cookie for ID visitor 279 if (Common::getRequestVar('forceEnableFingerprintingAcrossWebsites', false, null, $args) == 1) { 280 TrackerConfig::setConfigValue('enable_fingerprinting_across_websites', 1); 281 } 282 283 // Tests can simulate the tracker API maintenance mode 284 if (Common::getRequestVar('forceEnableTrackerMaintenanceMode', false, null, $args) == 1) { 285 TrackerConfig::setConfigValue('record_statistics', 0); 286 } 287 288 // Tests can force the use of 3rd party cookie for ID visitor 289 if (Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) { 290 TrackerConfig::setConfigValue('use_third_party_id_cookie', 1); 291 } 292 293 // Tests using window_look_back_for_visitor 294 if (Common::getRequestVar('forceLargeWindowLookBackForVisitor', false, null, $args) == 1 295 // also look for this in bulk requests (see fake_logs_replay.log) 296 || strpos(json_encode($args, true), '"forceLargeWindowLookBackForVisitor":"1"') !== false 297 ) { 298 TrackerConfig::setConfigValue('window_look_back_for_visitor', 2678400); 299 } 300 301 // Tests can force the enabling of IP anonymization 302 if (Common::getRequestVar('forceIpAnonymization', false, null, $args) == 1) { 303 self::getDatabase(); // make sure db is initialized 304 305 $privacyConfig = new PrivacyManagerConfig(); 306 $privacyConfig->ipAddressMaskLength = 2; 307 308 \Piwik\Plugins\PrivacyManager\IPAnonymizer::activate(); 309 310 \Piwik\Tracker\Cache::deleteTrackerCache(); 311 Filesystem::clearPhpCaches(); 312 } 313 } 314 315 protected function loadTrackerPlugins() 316 { 317 try { 318 $pluginManager = PluginManager::getInstance(); 319 $pluginsTracker = $pluginManager->loadTrackerPlugins(); 320 321 $this->logger->debug("Loading plugins: { {plugins} }", [ 322 'plugins' => implode(", ", $pluginsTracker), 323 ]); 324 } catch (Exception $e) { 325 $this->logger->error('Error loading tracker plugins: {exception}', [ 326 'exception' => $e, 327 ]); 328 } 329 } 330 331 private function handleFatalErrors() 332 { 333 register_shutdown_function(function () { // TODO: add a log here 334 $lastError = error_get_last(); 335 if (!empty($lastError) && $lastError['type'] == E_ERROR) { 336 Common::sendResponseCode(500); 337 } 338 }); 339 } 340 341 private static function isDebugEnabled() 342 { 343 try { 344 $debug = (bool) TrackerConfig::getConfigValue('debug'); 345 if ($debug) { 346 return true; 347 } 348 349 $debugOnDemand = (bool) TrackerConfig::getConfigValue('debug_on_demand'); 350 if ($debugOnDemand) { 351 return (bool) Common::getRequestVar('debug', false); 352 } 353 } catch (Exception $e) { 354 } 355 356 return false; 357 } 358} 359