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