PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.7.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.7.0
5.13.0 5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Tracker / Request.php
matomo / app / core / Tracker Last commit date
Config 6 months ago Db 8 months ago Handler 2 years ago Visit 1 year ago Action.php 1 year ago ActionPageview.php 2 years ago BotRequest.php 6 months ago BotRequestProcessor.php 6 months ago Cache.php 8 months ago Db.php 1 year ago Failures.php 8 months ago FingerprintSalt.php 1 year ago GoalManager.php 7 months ago Handler.php 2 years ago IgnoreCookie.php 1 year ago LogTable.php 1 year ago Model.php 8 months ago PageUrl.php 8 months ago Request.php 6 months ago RequestHandlerTrait.php 6 months ago RequestProcessor.php 1 year ago RequestSet.php 8 months ago Response.php 1 year ago ScheduledTasksRunner.php 1 year ago Settings.php 1 year ago TableLogAction.php 8 months ago TrackerCodeGenerator.php 1 year ago TrackerConfig.php 6 months ago Visit.php 6 months ago VisitExcluded.php 6 months ago VisitInterface.php 2 years ago Visitor.php 1 year ago VisitorNotFoundInDb.php 2 years ago VisitorRecognizer.php 1 year ago
Request.php
806 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Tracker;
10
11 use Exception;
12 use Piwik\Request\AuthenticationToken;
13 use Piwik\Common;
14 use Piwik\Container\StaticContainer;
15 use Piwik\Cookie;
16 use Piwik\Exception\InvalidRequestParameterException;
17 use Piwik\Exception\UnexpectedWebsiteFoundException;
18 use Piwik\Http;
19 use Piwik\IP;
20 use Matomo\Network\IPUtils;
21 use Piwik\Piwik;
22 use Piwik\Plugins\UsersManager\UsersManager;
23 use Piwik\ProxyHttp;
24 use Piwik\Segment\SegmentExpression;
25 use Piwik\Tracker;
26 use Piwik\Cache as PiwikCache;
27 use Piwik\Tracker\Cache as TrackerCache;
28 use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
29 use Piwik\Plugins\PrivacyManager\FeatureFlags\PrivacyCompliance;
30 use Piwik\Plugins\UserId\Settings\UserIdDisabled;
31 /**
32 * The Request object holding the http parameters for this tracking request. Use getParam() to fetch a named parameter.
33 *
34 */
35 class Request
36 {
37 private $cdtCache;
38 private $idSiteCache;
39 private $paramsCache = array();
40 /**
41 * @var array
42 */
43 protected $params;
44 protected $rawParams;
45 protected $isAuthenticated = null;
46 private $isEmptyRequest = \false;
47 protected $tokenAuth;
48 protected $timestamp;
49 /**
50 * Stores plugin specific tracking request metadata. RequestProcessors can store
51 * whatever they want in this array, and other RequestProcessors can modify these
52 * values to change tracker behavior.
53 *
54 * @var string[][]
55 */
56 private $requestMetadata = array();
57 public const UNKNOWN_RESOLUTION = 'unknown';
58 private $customTimestampDoesNotRequireTokenauthWhenNewerThan;
59 /**
60 * @param $params
61 * @param string $tokenAuth
62 */
63 public function __construct($params,
64 #[\SensitiveParameter]
65 $tokenAuth = '')
66 {
67 if (!is_array($params)) {
68 $params = array();
69 }
70 $this->params = $params;
71 $this->rawParams = $params;
72 $this->tokenAuth = $tokenAuth;
73 $this->timestamp = time();
74 $this->isEmptyRequest = empty($params);
75 // When the 'url' and referrer url parameter are not given, we might be in the 'Simple Image Tracker' mode.
76 // The URL can default to the Referrer, which will be in this case
77 // the URL of the page containing the Simple Image beacon
78 if (empty($this->params['urlref']) && empty($this->params['url']) && array_key_exists('HTTP_REFERER', $_SERVER)) {
79 $url = $_SERVER['HTTP_REFERER'];
80 if (!empty($url)) {
81 $this->params['url'] = $url;
82 }
83 }
84 // check for 4byte utf8 characters in all tracking params and replace them with � if not support by database
85 $this->params = $this->replaceUnsupportedUtf8Chars($this->params);
86 $this->customTimestampDoesNotRequireTokenauthWhenNewerThan = (int) \Piwik\Tracker\TrackerConfig::getConfigValue('tracking_requests_require_authentication_when_custom_timestamp_newer_than', $this->getIdSiteIfExists());
87 }
88 protected function replaceUnsupportedUtf8Chars($value, $key = \false)
89 {
90 $dbSettings = new \Piwik\Db\Settings();
91 $charset = $dbSettings->getUsedCharset();
92 if ('utf8mb4' === $charset) {
93 return $value;
94 // no need to replace anything if utf8mb4 is supported
95 }
96 if (is_string($value) && preg_match('/[\\x{10000}-\\x{10FFFF}]/u', $value)) {
97 Common::printDebug("Unsupported character detected in {$key}. Replacing with �");
98 return preg_replace('/[\\x{10000}-\\x{10FFFF}]/u', "", $value);
99 }
100 if (is_array($value)) {
101 array_walk_recursive($value, function (&$value, $key) {
102 $value = $this->replaceUnsupportedUtf8Chars($value, $key);
103 });
104 }
105 return $value;
106 }
107 /**
108 * Get the params that were originally passed to the instance. These params do not contain any params that were added
109 * within this object.
110 * @return array
111 */
112 public function getRawParams()
113 {
114 return $this->rawParams;
115 }
116 public function getTokenAuth()
117 {
118 return $this->tokenAuth;
119 }
120 /**
121 * @return bool
122 */
123 public function isAuthenticated()
124 {
125 if (is_null($this->isAuthenticated)) {
126 $this->authenticateTrackingApi($this->tokenAuth);
127 }
128 return $this->isAuthenticated;
129 }
130 /**
131 * This method allows to set custom IP + server time + visitor ID, when using Tracking API.
132 * These two attributes can be only set by the Super User (passing token_auth).
133 */
134 protected function authenticateTrackingApi(
135 #[\SensitiveParameter]
136 $tokenAuth)
137 {
138 $shouldAuthenticate = \Piwik\Tracker\TrackerConfig::getConfigValue('tracking_requests_require_authentication', $this->getIdSiteIfExists());
139 if ($shouldAuthenticate) {
140 try {
141 $idSite = $this->getIdSite();
142 } catch (Exception $e) {
143 Common::printDebug("failed to authenticate: invalid idSite");
144 $this->isAuthenticated = \false;
145 return;
146 }
147 if (empty($tokenAuth) && !empty($this->params)) {
148 $tokenAuth = StaticContainer::get(AuthenticationToken::class)->getAuthToken($this->params);
149 }
150 if (empty($tokenAuth)) {
151 $tokenAuth = StaticContainer::get(AuthenticationToken::class)->getAuthToken();
152 }
153 $cache = PiwikCache::getTransientCache();
154 $cacheKey = 'tracker_request_authentication_' . $idSite . '_' . $tokenAuth;
155 if ($cache->contains($cacheKey)) {
156 Common::printDebug("token_auth is authenticated in cache!");
157 $this->isAuthenticated = $cache->fetch($cacheKey);
158 return;
159 }
160 try {
161 $this->isAuthenticated = self::authenticateSuperUserOrAdminOrWrite($tokenAuth, $idSite);
162 $cache->save($cacheKey, $this->isAuthenticated);
163 } catch (Exception $e) {
164 Common::printDebug("could not authenticate, caught exception: " . $e->getMessage());
165 $this->isAuthenticated = \false;
166 }
167 if ($this->isAuthenticated) {
168 Common::printDebug("token_auth is authenticated!");
169 } else {
170 if (preg_match('/^\\w{28,36}$/', $tokenAuth) || empty($tokenAuth)) {
171 // only log a failure if the token auth looks partial valid or is completely missing
172 StaticContainer::get('Piwik\\Tracker\\Failures')->logFailure(\Piwik\Tracker\Failures::FAILURE_ID_NOT_AUTHENTICATED, $this);
173 }
174 }
175 } else {
176 $this->isAuthenticated = \true;
177 Common::printDebug("token_auth authentication not required");
178 }
179 }
180 public static function authenticateSuperUserOrAdminOrWrite(
181 #[\SensitiveParameter]
182 $tokenAuth, $idSite)
183 {
184 if (empty($tokenAuth)) {
185 return \false;
186 }
187 // Now checking the list of admin token_auth cached in the Tracker config file
188 if (!empty($idSite) && $idSite > 0) {
189 $website = \Piwik\Tracker\Cache::getCacheWebsiteAttributes($idSite);
190 $userModel = new \Piwik\Plugins\UsersManager\Model();
191 $tokenAuthHashed = $userModel->hashTokenAuth($tokenAuth);
192 $hashedToken = UsersManager::hashTrackingToken((string) $tokenAuthHashed, $idSite);
193 if (array_key_exists('tracking_token_auth', $website) && in_array($hashedToken, $website['tracking_token_auth'], \true)) {
194 return \true;
195 }
196 }
197 Piwik::postEvent('Request.initAuthenticationObject');
198 /** @var \Piwik\Auth $auth */
199 $auth = StaticContainer::get('Piwik\\Auth');
200 $auth->setTokenAuth($tokenAuth);
201 $auth->setLogin(null);
202 $auth->setPassword(null);
203 $auth->setPasswordHash(null);
204 $access = $auth->authenticate();
205 if (!empty($access) && $access->hasSuperUserAccess()) {
206 return \true;
207 }
208 Common::printDebug("WARNING! token_auth = {$tokenAuth} is not valid, Super User / Admin / Write was NOT authenticated");
209 /**
210 * @ignore
211 * @internal
212 */
213 Piwik::postEvent('Tracker.Request.authenticate.failed');
214 return \false;
215 }
216 public function isRequestExcluded()
217 {
218 $excludedRequests = \Piwik\Tracker\TrackerConfig::getConfigValue('exclude_requests', $this->getIdSiteIfExists());
219 if (!empty($excludedRequests)) {
220 $excludedRequests = explode(',', $excludedRequests);
221 $pattern = '/^(.+?)(' . SegmentExpression::MATCH_EQUAL . '|' . SegmentExpression::MATCH_NOT_EQUAL . '|' . SegmentExpression::MATCH_CONTAINS . '|' . SegmentExpression::MATCH_DOES_NOT_CONTAIN . '|' . preg_quote(SegmentExpression::MATCH_STARTS_WITH) . '|' . preg_quote(SegmentExpression::MATCH_ENDS_WITH) . '){1}(.*)/';
222 foreach ($excludedRequests as $excludedRequest) {
223 $match = preg_match($pattern, $excludedRequest, $matches);
224 if (!empty($match)) {
225 $leftMember = $matches[1];
226 $operation = $matches[2];
227 if (!isset($matches[3])) {
228 $valueRightMember = '';
229 } else {
230 $valueRightMember = urldecode($matches[3]);
231 }
232 $actual = Common::getRequestVar($leftMember, '', 'string', $this->params);
233 $actual = mb_strtolower($actual);
234 $valueRightMember = mb_strtolower($valueRightMember);
235 switch ($operation) {
236 case SegmentExpression::MATCH_EQUAL:
237 if ($actual === $valueRightMember) {
238 return \true;
239 }
240 break;
241 case SegmentExpression::MATCH_NOT_EQUAL:
242 if ($actual !== $valueRightMember) {
243 return \true;
244 }
245 break;
246 case SegmentExpression::MATCH_CONTAINS:
247 if (stripos($actual, $valueRightMember) !== \false) {
248 return \true;
249 }
250 break;
251 case SegmentExpression::MATCH_DOES_NOT_CONTAIN:
252 if (stripos($actual, $valueRightMember) === \false) {
253 return \true;
254 }
255 break;
256 case SegmentExpression::MATCH_STARTS_WITH:
257 if (stripos($actual, $valueRightMember) === 0) {
258 return \true;
259 }
260 break;
261 case SegmentExpression::MATCH_ENDS_WITH:
262 if (Common::stringEndsWith($actual, $valueRightMember)) {
263 return \true;
264 }
265 break;
266 }
267 }
268 }
269 }
270 return \false;
271 }
272 /**
273 * Returns the language the visitor is viewing.
274 *
275 * @return string browser language code, eg. "en-gb,en;q=0.5"
276 */
277 public function getBrowserLanguage()
278 {
279 $parameterValue = Common::getRequestVar('lang', '', 'string', $this->params);
280 return Common::getBrowserLanguage($parameterValue ?: null);
281 }
282 /**
283 * @return string
284 */
285 public function getLocalTime()
286 {
287 $localTimes = array('h' => (string) Common::getRequestVar('h', $this->getCurrentDate("H"), 'int', $this->params), 'i' => (string) Common::getRequestVar('m', $this->getCurrentDate("i"), 'int', $this->params), 's' => (string) Common::getRequestVar('s', $this->getCurrentDate("s"), 'int', $this->params));
288 if ($localTimes['h'] < 0 || $localTimes['h'] > 23) {
289 $localTimes['h'] = 0;
290 }
291 if ($localTimes['i'] < 0 || $localTimes['i'] > 59) {
292 $localTimes['i'] = 0;
293 }
294 if ($localTimes['s'] < 0 || $localTimes['s'] > 59) {
295 $localTimes['s'] = 0;
296 }
297 foreach ($localTimes as $k => $time) {
298 if (strlen($time) == 1) {
299 $localTimes[$k] = '0' . $time;
300 }
301 }
302 $localTime = $localTimes['h'] . ':' . $localTimes['i'] . ':' . $localTimes['s'];
303 return $localTime;
304 }
305 /**
306 * Returns the current date in the "Y-m-d" PHP format
307 *
308 * @param string $format
309 * @return string
310 */
311 protected function getCurrentDate($format = "Y-m-d")
312 {
313 return date($format, $this->getCurrentTimestamp());
314 }
315 public function getGoalRevenue($defaultGoalRevenue)
316 {
317 return Common::getRequestVar('revenue', $defaultGoalRevenue, 'float', $this->params);
318 }
319 public function getParam($name)
320 {
321 static $supportedParams = array(
322 // Name => array( defaultValue, type )
323 '_refts' => array(0, 'int'),
324 '_ref' => array('', 'string'),
325 '_rcn' => array('', 'string'),
326 '_rck' => array('', 'string'),
327 'url' => array('', 'string'),
328 'urlref' => array('', 'string'),
329 'res' => array(self::UNKNOWN_RESOLUTION, 'string'),
330 'idgoal' => array(-1, 'int'),
331 'ping' => array(0, 'int'),
332 // other
333 'bots' => array(0, 'int'),
334 'dp' => array(0, 'int'),
335 'rec' => array(0, 'int'),
336 'new_visit' => array(0, 'int'),
337 // Ecommerce
338 'ec_id' => array('', 'string'),
339 'ec_st' => array(\false, 'float'),
340 'ec_tx' => array(\false, 'float'),
341 'ec_sh' => array(\false, 'float'),
342 'ec_dt' => array(\false, 'float'),
343 'ec_items' => array('', 'json'),
344 // ecommerce product/category view
345 '_pkc' => array('', 'string'),
346 '_pks' => array('', 'string'),
347 '_pkn' => array('', 'string'),
348 '_pkp' => array(\false, 'float'),
349 // Events
350 'e_c' => array('', 'string'),
351 'e_a' => array('', 'string'),
352 'e_n' => array('', 'string'),
353 'e_v' => array(\false, 'float'),
354 // some visitor attributes can be overwritten
355 'cip' => array('', 'string'),
356 'cdt' => array('', 'string'),
357 'cdo' => array('', 'int'),
358 'cid' => array('', 'string'),
359 'uid' => array('', 'string'),
360 // Actions / pages
361 'cs' => array('', 'string'),
362 'download' => array('', 'string'),
363 'link' => array('', 'string'),
364 'action_name' => array('', 'string'),
365 'search' => array('', 'string'),
366 'search_cat' => array('', 'string'),
367 'pv_id' => array('', 'string'),
368 'search_count' => array(-1, 'int'),
369 'pf_net' => array(-1, 'int'),
370 'pf_srv' => array(-1, 'int'),
371 'pf_tfr' => array(-1, 'int'),
372 'pf_dm1' => array(-1, 'int'),
373 'pf_dm2' => array(-1, 'int'),
374 'pf_onl' => array(-1, 'int'),
375 // Content
376 'c_p' => array('', 'string'),
377 'c_n' => array('', 'string'),
378 'c_t' => array('', 'string'),
379 'c_i' => array('', 'string'),
380 // custom action request. Recommended when a plugin declares its own action handler/requestprocessor
381 // refs https://github.com/matomo-org/matomo/issues/16569
382 'ca' => array(0, 'int'),
383 );
384 if (isset($this->paramsCache[$name])) {
385 return $this->paramsCache[$name];
386 }
387 if (!isset($supportedParams[$name])) {
388 throw new Exception("Requested parameter {$name} is not a known Tracking API Parameter.");
389 }
390 $paramDefaultValue = $supportedParams[$name][0];
391 $paramType = $supportedParams[$name][1];
392 if ($this->hasParam($name)) {
393 $this->paramsCache[$name] = $this->replaceUnsupportedUtf8Chars(Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params), $name);
394 } else {
395 $this->paramsCache[$name] = $paramDefaultValue;
396 }
397 return $this->paramsCache[$name];
398 }
399 public function setParam($name, $value)
400 {
401 $this->params[$name] = $value;
402 unset($this->paramsCache[$name]);
403 if ($name === 'cdt') {
404 $this->cdtCache = null;
405 }
406 }
407 public function hasParam($name)
408 {
409 return isset($this->params[$name]);
410 }
411 public function getParams()
412 {
413 return $this->params;
414 }
415 public function getCurrentTimestamp()
416 {
417 if (!isset($this->cdtCache)) {
418 $this->cdtCache = $this->getCustomTimestamp();
419 }
420 if (!empty($this->cdtCache)) {
421 return $this->cdtCache;
422 }
423 return $this->timestamp;
424 }
425 public function setCurrentTimestamp($timestamp)
426 {
427 $this->timestamp = $timestamp;
428 }
429 protected function getCustomTimestamp()
430 {
431 if (!$this->hasParam('cdt') && !$this->hasParam('cdo')) {
432 return \false;
433 }
434 $cdt = $this->getParam('cdt');
435 $cdo = $this->getParam('cdo');
436 if (empty($cdt) && $cdo) {
437 $cdt = $this->timestamp;
438 }
439 if (empty($cdt)) {
440 return \false;
441 }
442 if (!is_numeric($cdt)) {
443 $cdt = strtotime($cdt, $this->timestamp);
444 }
445 if (!empty($cdo)) {
446 $cdt = $cdt - abs($cdo);
447 }
448 if (!$this->isTimestampValid($cdt, $this->timestamp)) {
449 Common::printDebug(sprintf("Datetime %s is not valid", date("Y-m-d H:i:m", $cdt)));
450 return \false;
451 }
452 // If timestamp in the past, token_auth is required
453 $timeFromNow = $this->timestamp - $cdt;
454 $isTimestampRecent = $timeFromNow < $this->customTimestampDoesNotRequireTokenauthWhenNewerThan;
455 if (!$isTimestampRecent) {
456 if (!$this->isAuthenticated()) {
457 $message = sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow);
458 Common::printDebug($message);
459 Common::printDebug("WARN: Tracker API 'cdt' was used with invalid token_auth");
460 throw new InvalidRequestParameterException($message);
461 }
462 }
463 $cache = Tracker\Cache::getCacheGeneral();
464 if (!empty($cache['delete_logs_enable']) && !empty($cache['delete_logs_older_than'])) {
465 $scheduleInterval = $cache['delete_logs_schedule_lowest_interval'];
466 $maxLogAge = $cache['delete_logs_older_than'];
467 $logEntryCutoff = time() - ($maxLogAge + $scheduleInterval) * 60 * 60 * 24;
468 if ($cdt < $logEntryCutoff) {
469 $message = "Custom timestamp is older than the configured 'deleted old raw data' value of {$maxLogAge} days";
470 Common::printDebug($message);
471 throw new InvalidRequestParameterException($message);
472 }
473 }
474 return (int) $cdt;
475 }
476 /**
477 * Returns true if the timestamp is valid ie. timestamp is sometime in the last 10 years and is not in the future.
478 *
479 * @param $time int Timestamp to test
480 * @param $now int Current timestamp
481 * @return bool
482 */
483 protected function isTimestampValid($time, $now = null)
484 {
485 if (empty($now)) {
486 $now = $this->getCurrentTimestamp();
487 }
488 return $time <= $now && $time > $now - 20 * 365 * 86400;
489 }
490 /**
491 * @internal
492 * @ignore
493 */
494 public function getIdSiteUnverified()
495 {
496 $idSite = Common::getRequestVar('idsite', 0, 'int', $this->params);
497 /**
498 * Triggered when obtaining the ID of the site we are tracking a visit for.
499 *
500 * This event can be used to change the site ID so data is tracked for a different
501 * website.
502 *
503 * @param int &$idSite Initialized to the value of the **idsite** query parameter. If a
504 * subscriber sets this variable, the value it uses must be greater
505 * than 0.
506 * @param array $params The entire array of request parameters in the current tracking
507 * request.
508 */
509 Piwik::postEvent('Tracker.Request.getIdSite', array(&$idSite, $this->params));
510 return $idSite;
511 }
512 public function getIdSiteIfExists()
513 {
514 try {
515 return $this->getIdSite();
516 } catch (UnexpectedWebsiteFoundException $ex) {
517 return null;
518 }
519 }
520 public function getIdSite()
521 {
522 if (isset($this->idSiteCache)) {
523 return $this->idSiteCache;
524 }
525 $idSite = $this->getIdSiteUnverified();
526 if ($idSite <= 0) {
527 throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
528 }
529 // check site actually exists, should throw UnexpectedWebsiteFoundException directly
530 $site = \Piwik\Tracker\Cache::getCacheWebsiteAttributes($idSite);
531 if (empty($site)) {
532 // fallback just in case exception wasn't thrown...
533 throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
534 }
535 $this->idSiteCache = $idSite;
536 return $idSite;
537 }
538 public function getUserAgent()
539 {
540 $default = \false;
541 if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
542 $default = $_SERVER['HTTP_USER_AGENT'];
543 }
544 return Common::getRequestVar('ua', $default, 'string', $this->params);
545 }
546 public function getClientHints() : array
547 {
548 // use headers as default if no data was send with the tracking request
549 $default = Http::getClientHintsFromServerVariables();
550 $clientHints = Common::getRequestVar('uadata', $default, 'json', $this->params);
551 return is_array($clientHints) ? $clientHints : [];
552 }
553 public function shouldUseThirdPartyCookie()
554 {
555 return \Piwik\Tracker\TrackerConfig::getConfigValue('use_third_party_id_cookie', $this->getIdSiteIfExists());
556 }
557 public function getThirdPartyCookieVisitorId()
558 {
559 $cookie = $this->makeThirdPartyCookieUID();
560 $idVisitor = $cookie->get(0);
561 if ($idVisitor !== \false && strlen($idVisitor) == Tracker::LENGTH_HEX_ID_STRING) {
562 return $idVisitor;
563 }
564 return null;
565 }
566 /**
567 * Update the cookie information.
568 */
569 public function setThirdPartyCookie($idVisitor)
570 {
571 if (!$this->shouldUseThirdPartyCookie()) {
572 return;
573 }
574 if (\Piwik\Tracker\IgnoreCookie::isIgnoreCookieFound()) {
575 return;
576 }
577 $cookie = $this->makeThirdPartyCookieUID();
578 $idVisitor = bin2hex($idVisitor);
579 $cookie->set(0, $idVisitor);
580 if (ProxyHttp::isHttps()) {
581 $cookie->setSecure(\true);
582 $cookie->save('None');
583 } else {
584 $cookie->save('Lax');
585 }
586 Common::printDebug(sprintf("We set the visitor ID to %s in the 3rd party cookie...", $idVisitor));
587 }
588 protected function makeThirdPartyCookieUID()
589 {
590 $cookie = new Cookie($this->getCookieName(), $this->getCookieExpire(), $this->getCookiePath());
591 $domain = $this->getCookieDomain();
592 if (!empty($domain)) {
593 $cookie->setDomain($domain);
594 }
595 Common::printDebug($cookie);
596 return $cookie;
597 }
598 protected function getCookieName()
599 {
600 return \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_name', $this->getIdSiteIfExists());
601 }
602 protected function getCookieExpire()
603 {
604 return $this->getCurrentTimestamp() + \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_expire', $this->getIdSiteIfExists());
605 }
606 protected function getCookiePath()
607 {
608 return \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_path', $this->getIdSiteIfExists());
609 }
610 protected function getCookieDomain()
611 {
612 return \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_domain', $this->getIdSiteIfExists());
613 }
614 /**
615 * Returns the ID from the request in this order:
616 * return from a given User ID,
617 * or from a Tracking API forced Visitor ID,
618 * or from a Visitor ID from 3rd party (optional) cookies,
619 * or from a given Visitor Id from 1st party?
620 *
621 * @throws Exception
622 */
623 public function getVisitorId()
624 {
625 $found = \false;
626 if (\Piwik\Tracker\TrackerConfig::getConfigValue('enable_userid_overwrites_visitorid', $this->getIdSiteIfExists())) {
627 // If User ID is set it takes precedence
628 $userId = $this->getForcedUserId();
629 if ($userId) {
630 $userIdHashed = $this->getUserIdHashed($userId);
631 $idVisitor = $this->truncateIdAsVisitorId($userIdHashed);
632 Common::printDebug("Request will be recorded for this user_id = " . $userId . " (idvisitor = {$idVisitor})");
633 $found = \true;
634 }
635 }
636 // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request?
637 if (!$found) {
638 $idVisitor = $this->getForcedVisitorId();
639 if (!empty($idVisitor)) {
640 if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) {
641 throw new InvalidRequestParameterException("Visitor ID (cid) {$idVisitor} must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long");
642 }
643 Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor);
644 $found = \true;
645 }
646 }
647 $privacyConfig = new \Piwik\Plugins\PrivacyManager\Config();
648 // Only check for cookie values if cookieless tracking is NOT forced
649 if (!$privacyConfig->forceCookielessTracking) {
650 // - If set to use 3rd party cookies for Visit ID, read the cookie
651 if (!$found) {
652 $useThirdPartyCookie = $this->shouldUseThirdPartyCookie();
653 if ($useThirdPartyCookie) {
654 $idVisitor = $this->getThirdPartyCookieVisitorId();
655 if (!empty($idVisitor)) {
656 $found = \true;
657 }
658 }
659 }
660 // If a third party cookie was not found, we default to the first party cookie
661 if (!$found) {
662 $idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
663 $found = strlen($idVisitor) >= Tracker::LENGTH_HEX_ID_STRING;
664 }
665 }
666 if ($found) {
667 return $this->getVisitorIdAsBinary($idVisitor);
668 }
669 return \false;
670 }
671 /**
672 * When creating a third party cookie, we want to ensure that the original value set in this 3rd party cookie
673 * sticks and is not overwritten later.
674 */
675 public function getVisitorIdForThirdPartyCookie()
676 {
677 $found = \false;
678 // For 3rd party cookies, priority is on re-using the existing 3rd party cookie value
679 if (!$found) {
680 $useThirdPartyCookie = $this->shouldUseThirdPartyCookie();
681 if ($useThirdPartyCookie) {
682 $idVisitor = $this->getThirdPartyCookieVisitorId();
683 if (!empty($idVisitor)) {
684 $found = \true;
685 }
686 }
687 }
688 // If a third party cookie was not found, we default to the first party cookie
689 if (!$found) {
690 $idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
691 $found = strlen($idVisitor) >= Tracker::LENGTH_HEX_ID_STRING;
692 }
693 if ($found) {
694 return $this->getVisitorIdAsBinary($idVisitor);
695 }
696 return \false;
697 }
698 public function getIp()
699 {
700 return IPUtils::stringToBinaryIP($this->getIpString());
701 }
702 public function getForcedUserId()
703 {
704 $featureFlagManager = StaticContainer::get(FeatureFlagManager::class);
705 if ($featureFlagManager->isFeatureActive(PrivacyCompliance::class)) {
706 $idSite = $this->getIdSite();
707 $cache = TrackerCache::getCacheWebsiteAttributes($idSite);
708 $cacheKey = UserIdDisabled::class;
709 if (($cache[$cacheKey] ?? \false) === \true) {
710 return \false;
711 }
712 }
713 $userId = $this->getParam('uid');
714 if (strlen($userId) > 0) {
715 return $userId;
716 }
717 return \false;
718 }
719 public function getForcedVisitorId()
720 {
721 return $this->getParam('cid');
722 }
723 public function getPlugins()
724 {
725 static $pluginsInOrder = array('fla', 'java', 'qt', 'realp', 'pdf', 'wma', 'ag', 'cookie');
726 $plugins = array();
727 foreach ($pluginsInOrder as $param) {
728 $plugins[] = Common::getRequestVar($param, 0, 'int', $this->params);
729 }
730 return $plugins;
731 }
732 public function isEmptyRequest()
733 {
734 return $this->isEmptyRequest;
735 }
736 /**
737 * @param $idVisitor
738 * @return string
739 */
740 private function truncateIdAsVisitorId($idVisitor)
741 {
742 return substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING);
743 }
744 /**
745 * Matches implementation of MatomoTracker::getUserIdHashed
746 *
747 * @param $userId
748 * @return string
749 */
750 public function getUserIdHashed($userId)
751 {
752 return substr(sha1($userId), 0, 16);
753 }
754 /**
755 * @return mixed|string
756 * @throws Exception
757 */
758 public function getIpString()
759 {
760 $cip = $this->getParam('cip');
761 if (empty($cip)) {
762 return IP::getIpFromHeader();
763 }
764 if (!$this->isAuthenticated()) {
765 Common::printDebug("WARN: Tracker API 'cip' was used with invalid token_auth");
766 throw new InvalidRequestParameterException("Tracker API 'cip' was used, requires valid token_auth");
767 }
768 return $cip;
769 }
770 /**
771 * Set a request metadata value.
772 *
773 * @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'`
774 * @param string $key
775 * @param mixed $value
776 */
777 public function setMetadata($pluginName, $key, $value)
778 {
779 $this->requestMetadata[$pluginName][$key] = $value;
780 }
781 /**
782 * Get a request metadata value. Returns `null` if none exists.
783 *
784 * @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'`
785 * @param string $key
786 * @return mixed
787 */
788 public function getMetadata($pluginName, $key)
789 {
790 return isset($this->requestMetadata[$pluginName][$key]) ? $this->requestMetadata[$pluginName][$key] : null;
791 }
792 /**
793 * @param $idVisitor
794 * @return bool|string
795 */
796 private function getVisitorIdAsBinary($idVisitor)
797 {
798 $truncated = $this->truncateIdAsVisitorId($idVisitor);
799 $binVisitorId = @Common::hex2bin($truncated);
800 if (!empty($binVisitorId)) {
801 return $binVisitorId;
802 }
803 return \false;
804 }
805 }
806