PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.6.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.6.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
Db 8 months ago Handler 2 years ago Visit 1 year ago Action.php 1 year ago ActionPageview.php 2 years ago Cache.php 8 months ago Db.php 1 year ago Failures.php 8 months ago FingerprintSalt.php 1 year ago GoalManager.php 8 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 8 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 2 years ago Visit.php 1 year ago VisitExcluded.php 1 year ago VisitInterface.php 2 years ago Visitor.php 1 year ago VisitorNotFoundInDb.php 2 years ago VisitorRecognizer.php 1 year ago
Request.php
803 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 StaticContainer::get('Piwik\\Tracker\\Failures')->logFailure(\Piwik\Tracker\Failures::FAILURE_ID_NOT_AUTHENTICATED, $this);
171 }
172 } else {
173 $this->isAuthenticated = \true;
174 Common::printDebug("token_auth authentication not required");
175 }
176 }
177 public static function authenticateSuperUserOrAdminOrWrite(
178 #[\SensitiveParameter]
179 $tokenAuth, $idSite)
180 {
181 if (empty($tokenAuth)) {
182 return \false;
183 }
184 // Now checking the list of admin token_auth cached in the Tracker config file
185 if (!empty($idSite) && $idSite > 0) {
186 $website = \Piwik\Tracker\Cache::getCacheWebsiteAttributes($idSite);
187 $userModel = new \Piwik\Plugins\UsersManager\Model();
188 $tokenAuthHashed = $userModel->hashTokenAuth($tokenAuth);
189 $hashedToken = UsersManager::hashTrackingToken((string) $tokenAuthHashed, $idSite);
190 if (array_key_exists('tracking_token_auth', $website) && in_array($hashedToken, $website['tracking_token_auth'], \true)) {
191 return \true;
192 }
193 }
194 Piwik::postEvent('Request.initAuthenticationObject');
195 /** @var \Piwik\Auth $auth */
196 $auth = StaticContainer::get('Piwik\\Auth');
197 $auth->setTokenAuth($tokenAuth);
198 $auth->setLogin(null);
199 $auth->setPassword(null);
200 $auth->setPasswordHash(null);
201 $access = $auth->authenticate();
202 if (!empty($access) && $access->hasSuperUserAccess()) {
203 return \true;
204 }
205 Common::printDebug("WARNING! token_auth = {$tokenAuth} is not valid, Super User / Admin / Write was NOT authenticated");
206 /**
207 * @ignore
208 * @internal
209 */
210 Piwik::postEvent('Tracker.Request.authenticate.failed');
211 return \false;
212 }
213 public function isRequestExcluded()
214 {
215 $excludedRequests = \Piwik\Tracker\TrackerConfig::getConfigValue('exclude_requests', $this->getIdSiteIfExists());
216 if (!empty($excludedRequests)) {
217 $excludedRequests = explode(',', $excludedRequests);
218 $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}(.*)/';
219 foreach ($excludedRequests as $excludedRequest) {
220 $match = preg_match($pattern, $excludedRequest, $matches);
221 if (!empty($match)) {
222 $leftMember = $matches[1];
223 $operation = $matches[2];
224 if (!isset($matches[3])) {
225 $valueRightMember = '';
226 } else {
227 $valueRightMember = urldecode($matches[3]);
228 }
229 $actual = Common::getRequestVar($leftMember, '', 'string', $this->params);
230 $actual = mb_strtolower($actual);
231 $valueRightMember = mb_strtolower($valueRightMember);
232 switch ($operation) {
233 case SegmentExpression::MATCH_EQUAL:
234 if ($actual === $valueRightMember) {
235 return \true;
236 }
237 break;
238 case SegmentExpression::MATCH_NOT_EQUAL:
239 if ($actual !== $valueRightMember) {
240 return \true;
241 }
242 break;
243 case SegmentExpression::MATCH_CONTAINS:
244 if (stripos($actual, $valueRightMember) !== \false) {
245 return \true;
246 }
247 break;
248 case SegmentExpression::MATCH_DOES_NOT_CONTAIN:
249 if (stripos($actual, $valueRightMember) === \false) {
250 return \true;
251 }
252 break;
253 case SegmentExpression::MATCH_STARTS_WITH:
254 if (stripos($actual, $valueRightMember) === 0) {
255 return \true;
256 }
257 break;
258 case SegmentExpression::MATCH_ENDS_WITH:
259 if (Common::stringEndsWith($actual, $valueRightMember)) {
260 return \true;
261 }
262 break;
263 }
264 }
265 }
266 }
267 return \false;
268 }
269 /**
270 * Returns the language the visitor is viewing.
271 *
272 * @return string browser language code, eg. "en-gb,en;q=0.5"
273 */
274 public function getBrowserLanguage()
275 {
276 $parameterValue = Common::getRequestVar('lang', '', 'string', $this->params);
277 return Common::getBrowserLanguage($parameterValue ?: null);
278 }
279 /**
280 * @return string
281 */
282 public function getLocalTime()
283 {
284 $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));
285 if ($localTimes['h'] < 0 || $localTimes['h'] > 23) {
286 $localTimes['h'] = 0;
287 }
288 if ($localTimes['i'] < 0 || $localTimes['i'] > 59) {
289 $localTimes['i'] = 0;
290 }
291 if ($localTimes['s'] < 0 || $localTimes['s'] > 59) {
292 $localTimes['s'] = 0;
293 }
294 foreach ($localTimes as $k => $time) {
295 if (strlen($time) == 1) {
296 $localTimes[$k] = '0' . $time;
297 }
298 }
299 $localTime = $localTimes['h'] . ':' . $localTimes['i'] . ':' . $localTimes['s'];
300 return $localTime;
301 }
302 /**
303 * Returns the current date in the "Y-m-d" PHP format
304 *
305 * @param string $format
306 * @return string
307 */
308 protected function getCurrentDate($format = "Y-m-d")
309 {
310 return date($format, $this->getCurrentTimestamp());
311 }
312 public function getGoalRevenue($defaultGoalRevenue)
313 {
314 return Common::getRequestVar('revenue', $defaultGoalRevenue, 'float', $this->params);
315 }
316 public function getParam($name)
317 {
318 static $supportedParams = array(
319 // Name => array( defaultValue, type )
320 '_refts' => array(0, 'int'),
321 '_ref' => array('', 'string'),
322 '_rcn' => array('', 'string'),
323 '_rck' => array('', 'string'),
324 'url' => array('', 'string'),
325 'urlref' => array('', 'string'),
326 'res' => array(self::UNKNOWN_RESOLUTION, 'string'),
327 'idgoal' => array(-1, 'int'),
328 'ping' => array(0, 'int'),
329 // other
330 'bots' => array(0, 'int'),
331 'dp' => array(0, 'int'),
332 'rec' => array(0, 'int'),
333 'new_visit' => array(0, 'int'),
334 // Ecommerce
335 'ec_id' => array('', 'string'),
336 'ec_st' => array(\false, 'float'),
337 'ec_tx' => array(\false, 'float'),
338 'ec_sh' => array(\false, 'float'),
339 'ec_dt' => array(\false, 'float'),
340 'ec_items' => array('', 'json'),
341 // ecommerce product/category view
342 '_pkc' => array('', 'string'),
343 '_pks' => array('', 'string'),
344 '_pkn' => array('', 'string'),
345 '_pkp' => array(\false, 'float'),
346 // Events
347 'e_c' => array('', 'string'),
348 'e_a' => array('', 'string'),
349 'e_n' => array('', 'string'),
350 'e_v' => array(\false, 'float'),
351 // some visitor attributes can be overwritten
352 'cip' => array('', 'string'),
353 'cdt' => array('', 'string'),
354 'cdo' => array('', 'int'),
355 'cid' => array('', 'string'),
356 'uid' => array('', 'string'),
357 // Actions / pages
358 'cs' => array('', 'string'),
359 'download' => array('', 'string'),
360 'link' => array('', 'string'),
361 'action_name' => array('', 'string'),
362 'search' => array('', 'string'),
363 'search_cat' => array('', 'string'),
364 'pv_id' => array('', 'string'),
365 'search_count' => array(-1, 'int'),
366 'pf_net' => array(-1, 'int'),
367 'pf_srv' => array(-1, 'int'),
368 'pf_tfr' => array(-1, 'int'),
369 'pf_dm1' => array(-1, 'int'),
370 'pf_dm2' => array(-1, 'int'),
371 'pf_onl' => array(-1, 'int'),
372 // Content
373 'c_p' => array('', 'string'),
374 'c_n' => array('', 'string'),
375 'c_t' => array('', 'string'),
376 'c_i' => array('', 'string'),
377 // custom action request. Recommended when a plugin declares its own action handler/requestprocessor
378 // refs https://github.com/matomo-org/matomo/issues/16569
379 'ca' => array(0, 'int'),
380 );
381 if (isset($this->paramsCache[$name])) {
382 return $this->paramsCache[$name];
383 }
384 if (!isset($supportedParams[$name])) {
385 throw new Exception("Requested parameter {$name} is not a known Tracking API Parameter.");
386 }
387 $paramDefaultValue = $supportedParams[$name][0];
388 $paramType = $supportedParams[$name][1];
389 if ($this->hasParam($name)) {
390 $this->paramsCache[$name] = $this->replaceUnsupportedUtf8Chars(Common::getRequestVar($name, $paramDefaultValue, $paramType, $this->params), $name);
391 } else {
392 $this->paramsCache[$name] = $paramDefaultValue;
393 }
394 return $this->paramsCache[$name];
395 }
396 public function setParam($name, $value)
397 {
398 $this->params[$name] = $value;
399 unset($this->paramsCache[$name]);
400 if ($name === 'cdt') {
401 $this->cdtCache = null;
402 }
403 }
404 public function hasParam($name)
405 {
406 return isset($this->params[$name]);
407 }
408 public function getParams()
409 {
410 return $this->params;
411 }
412 public function getCurrentTimestamp()
413 {
414 if (!isset($this->cdtCache)) {
415 $this->cdtCache = $this->getCustomTimestamp();
416 }
417 if (!empty($this->cdtCache)) {
418 return $this->cdtCache;
419 }
420 return $this->timestamp;
421 }
422 public function setCurrentTimestamp($timestamp)
423 {
424 $this->timestamp = $timestamp;
425 }
426 protected function getCustomTimestamp()
427 {
428 if (!$this->hasParam('cdt') && !$this->hasParam('cdo')) {
429 return \false;
430 }
431 $cdt = $this->getParam('cdt');
432 $cdo = $this->getParam('cdo');
433 if (empty($cdt) && $cdo) {
434 $cdt = $this->timestamp;
435 }
436 if (empty($cdt)) {
437 return \false;
438 }
439 if (!is_numeric($cdt)) {
440 $cdt = strtotime($cdt, $this->timestamp);
441 }
442 if (!empty($cdo)) {
443 $cdt = $cdt - abs($cdo);
444 }
445 if (!$this->isTimestampValid($cdt, $this->timestamp)) {
446 Common::printDebug(sprintf("Datetime %s is not valid", date("Y-m-d H:i:m", $cdt)));
447 return \false;
448 }
449 // If timestamp in the past, token_auth is required
450 $timeFromNow = $this->timestamp - $cdt;
451 $isTimestampRecent = $timeFromNow < $this->customTimestampDoesNotRequireTokenauthWhenNewerThan;
452 if (!$isTimestampRecent) {
453 if (!$this->isAuthenticated()) {
454 $message = sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow);
455 Common::printDebug($message);
456 Common::printDebug("WARN: Tracker API 'cdt' was used with invalid token_auth");
457 throw new InvalidRequestParameterException($message);
458 }
459 }
460 $cache = Tracker\Cache::getCacheGeneral();
461 if (!empty($cache['delete_logs_enable']) && !empty($cache['delete_logs_older_than'])) {
462 $scheduleInterval = $cache['delete_logs_schedule_lowest_interval'];
463 $maxLogAge = $cache['delete_logs_older_than'];
464 $logEntryCutoff = time() - ($maxLogAge + $scheduleInterval) * 60 * 60 * 24;
465 if ($cdt < $logEntryCutoff) {
466 $message = "Custom timestamp is older than the configured 'deleted old raw data' value of {$maxLogAge} days";
467 Common::printDebug($message);
468 throw new InvalidRequestParameterException($message);
469 }
470 }
471 return (int) $cdt;
472 }
473 /**
474 * Returns true if the timestamp is valid ie. timestamp is sometime in the last 10 years and is not in the future.
475 *
476 * @param $time int Timestamp to test
477 * @param $now int Current timestamp
478 * @return bool
479 */
480 protected function isTimestampValid($time, $now = null)
481 {
482 if (empty($now)) {
483 $now = $this->getCurrentTimestamp();
484 }
485 return $time <= $now && $time > $now - 20 * 365 * 86400;
486 }
487 /**
488 * @internal
489 * @ignore
490 */
491 public function getIdSiteUnverified()
492 {
493 $idSite = Common::getRequestVar('idsite', 0, 'int', $this->params);
494 /**
495 * Triggered when obtaining the ID of the site we are tracking a visit for.
496 *
497 * This event can be used to change the site ID so data is tracked for a different
498 * website.
499 *
500 * @param int &$idSite Initialized to the value of the **idsite** query parameter. If a
501 * subscriber sets this variable, the value it uses must be greater
502 * than 0.
503 * @param array $params The entire array of request parameters in the current tracking
504 * request.
505 */
506 Piwik::postEvent('Tracker.Request.getIdSite', array(&$idSite, $this->params));
507 return $idSite;
508 }
509 public function getIdSiteIfExists()
510 {
511 try {
512 return $this->getIdSite();
513 } catch (UnexpectedWebsiteFoundException $ex) {
514 return null;
515 }
516 }
517 public function getIdSite()
518 {
519 if (isset($this->idSiteCache)) {
520 return $this->idSiteCache;
521 }
522 $idSite = $this->getIdSiteUnverified();
523 if ($idSite <= 0) {
524 throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
525 }
526 // check site actually exists, should throw UnexpectedWebsiteFoundException directly
527 $site = \Piwik\Tracker\Cache::getCacheWebsiteAttributes($idSite);
528 if (empty($site)) {
529 // fallback just in case exception wasn't thrown...
530 throw new UnexpectedWebsiteFoundException('Invalid idSite: \'' . $idSite . '\'');
531 }
532 $this->idSiteCache = $idSite;
533 return $idSite;
534 }
535 public function getUserAgent()
536 {
537 $default = \false;
538 if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
539 $default = $_SERVER['HTTP_USER_AGENT'];
540 }
541 return Common::getRequestVar('ua', $default, 'string', $this->params);
542 }
543 public function getClientHints() : array
544 {
545 // use headers as default if no data was send with the tracking request
546 $default = Http::getClientHintsFromServerVariables();
547 $clientHints = Common::getRequestVar('uadata', $default, 'json', $this->params);
548 return is_array($clientHints) ? $clientHints : [];
549 }
550 public function shouldUseThirdPartyCookie()
551 {
552 return \Piwik\Tracker\TrackerConfig::getConfigValue('use_third_party_id_cookie', $this->getIdSiteIfExists());
553 }
554 public function getThirdPartyCookieVisitorId()
555 {
556 $cookie = $this->makeThirdPartyCookieUID();
557 $idVisitor = $cookie->get(0);
558 if ($idVisitor !== \false && strlen($idVisitor) == Tracker::LENGTH_HEX_ID_STRING) {
559 return $idVisitor;
560 }
561 return null;
562 }
563 /**
564 * Update the cookie information.
565 */
566 public function setThirdPartyCookie($idVisitor)
567 {
568 if (!$this->shouldUseThirdPartyCookie()) {
569 return;
570 }
571 if (\Piwik\Tracker\IgnoreCookie::isIgnoreCookieFound()) {
572 return;
573 }
574 $cookie = $this->makeThirdPartyCookieUID();
575 $idVisitor = bin2hex($idVisitor);
576 $cookie->set(0, $idVisitor);
577 if (ProxyHttp::isHttps()) {
578 $cookie->setSecure(\true);
579 $cookie->save('None');
580 } else {
581 $cookie->save('Lax');
582 }
583 Common::printDebug(sprintf("We set the visitor ID to %s in the 3rd party cookie...", $idVisitor));
584 }
585 protected function makeThirdPartyCookieUID()
586 {
587 $cookie = new Cookie($this->getCookieName(), $this->getCookieExpire(), $this->getCookiePath());
588 $domain = $this->getCookieDomain();
589 if (!empty($domain)) {
590 $cookie->setDomain($domain);
591 }
592 Common::printDebug($cookie);
593 return $cookie;
594 }
595 protected function getCookieName()
596 {
597 return \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_name', $this->getIdSiteIfExists());
598 }
599 protected function getCookieExpire()
600 {
601 return $this->getCurrentTimestamp() + \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_expire', $this->getIdSiteIfExists());
602 }
603 protected function getCookiePath()
604 {
605 return \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_path', $this->getIdSiteIfExists());
606 }
607 protected function getCookieDomain()
608 {
609 return \Piwik\Tracker\TrackerConfig::getConfigValue('cookie_domain', $this->getIdSiteIfExists());
610 }
611 /**
612 * Returns the ID from the request in this order:
613 * return from a given User ID,
614 * or from a Tracking API forced Visitor ID,
615 * or from a Visitor ID from 3rd party (optional) cookies,
616 * or from a given Visitor Id from 1st party?
617 *
618 * @throws Exception
619 */
620 public function getVisitorId()
621 {
622 $found = \false;
623 if (\Piwik\Tracker\TrackerConfig::getConfigValue('enable_userid_overwrites_visitorid', $this->getIdSiteIfExists())) {
624 // If User ID is set it takes precedence
625 $userId = $this->getForcedUserId();
626 if ($userId) {
627 $userIdHashed = $this->getUserIdHashed($userId);
628 $idVisitor = $this->truncateIdAsVisitorId($userIdHashed);
629 Common::printDebug("Request will be recorded for this user_id = " . $userId . " (idvisitor = {$idVisitor})");
630 $found = \true;
631 }
632 }
633 // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request?
634 if (!$found) {
635 $idVisitor = $this->getForcedVisitorId();
636 if (!empty($idVisitor)) {
637 if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) {
638 throw new InvalidRequestParameterException("Visitor ID (cid) {$idVisitor} must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long");
639 }
640 Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor);
641 $found = \true;
642 }
643 }
644 $privacyConfig = new \Piwik\Plugins\PrivacyManager\Config();
645 // Only check for cookie values if cookieless tracking is NOT forced
646 if (!$privacyConfig->forceCookielessTracking) {
647 // - If set to use 3rd party cookies for Visit ID, read the cookie
648 if (!$found) {
649 $useThirdPartyCookie = $this->shouldUseThirdPartyCookie();
650 if ($useThirdPartyCookie) {
651 $idVisitor = $this->getThirdPartyCookieVisitorId();
652 if (!empty($idVisitor)) {
653 $found = \true;
654 }
655 }
656 }
657 // If a third party cookie was not found, we default to the first party cookie
658 if (!$found) {
659 $idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
660 $found = strlen($idVisitor) >= Tracker::LENGTH_HEX_ID_STRING;
661 }
662 }
663 if ($found) {
664 return $this->getVisitorIdAsBinary($idVisitor);
665 }
666 return \false;
667 }
668 /**
669 * When creating a third party cookie, we want to ensure that the original value set in this 3rd party cookie
670 * sticks and is not overwritten later.
671 */
672 public function getVisitorIdForThirdPartyCookie()
673 {
674 $found = \false;
675 // For 3rd party cookies, priority is on re-using the existing 3rd party cookie value
676 if (!$found) {
677 $useThirdPartyCookie = $this->shouldUseThirdPartyCookie();
678 if ($useThirdPartyCookie) {
679 $idVisitor = $this->getThirdPartyCookieVisitorId();
680 if (!empty($idVisitor)) {
681 $found = \true;
682 }
683 }
684 }
685 // If a third party cookie was not found, we default to the first party cookie
686 if (!$found) {
687 $idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
688 $found = strlen($idVisitor) >= Tracker::LENGTH_HEX_ID_STRING;
689 }
690 if ($found) {
691 return $this->getVisitorIdAsBinary($idVisitor);
692 }
693 return \false;
694 }
695 public function getIp()
696 {
697 return IPUtils::stringToBinaryIP($this->getIpString());
698 }
699 public function getForcedUserId()
700 {
701 $featureFlagManager = StaticContainer::get(FeatureFlagManager::class);
702 if ($featureFlagManager->isFeatureActive(PrivacyCompliance::class)) {
703 $idSite = $this->getIdSite();
704 $cache = TrackerCache::getCacheWebsiteAttributes($idSite);
705 $cacheKey = UserIdDisabled::class;
706 if (($cache[$cacheKey] ?? \false) === \true) {
707 return \false;
708 }
709 }
710 $userId = $this->getParam('uid');
711 if (strlen($userId) > 0) {
712 return $userId;
713 }
714 return \false;
715 }
716 public function getForcedVisitorId()
717 {
718 return $this->getParam('cid');
719 }
720 public function getPlugins()
721 {
722 static $pluginsInOrder = array('fla', 'java', 'qt', 'realp', 'pdf', 'wma', 'ag', 'cookie');
723 $plugins = array();
724 foreach ($pluginsInOrder as $param) {
725 $plugins[] = Common::getRequestVar($param, 0, 'int', $this->params);
726 }
727 return $plugins;
728 }
729 public function isEmptyRequest()
730 {
731 return $this->isEmptyRequest;
732 }
733 /**
734 * @param $idVisitor
735 * @return string
736 */
737 private function truncateIdAsVisitorId($idVisitor)
738 {
739 return substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING);
740 }
741 /**
742 * Matches implementation of MatomoTracker::getUserIdHashed
743 *
744 * @param $userId
745 * @return string
746 */
747 public function getUserIdHashed($userId)
748 {
749 return substr(sha1($userId), 0, 16);
750 }
751 /**
752 * @return mixed|string
753 * @throws Exception
754 */
755 public function getIpString()
756 {
757 $cip = $this->getParam('cip');
758 if (empty($cip)) {
759 return IP::getIpFromHeader();
760 }
761 if (!$this->isAuthenticated()) {
762 Common::printDebug("WARN: Tracker API 'cip' was used with invalid token_auth");
763 throw new InvalidRequestParameterException("Tracker API 'cip' was used, requires valid token_auth");
764 }
765 return $cip;
766 }
767 /**
768 * Set a request metadata value.
769 *
770 * @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'`
771 * @param string $key
772 * @param mixed $value
773 */
774 public function setMetadata($pluginName, $key, $value)
775 {
776 $this->requestMetadata[$pluginName][$key] = $value;
777 }
778 /**
779 * Get a request metadata value. Returns `null` if none exists.
780 *
781 * @param string $pluginName eg, `'Actions'`, `'Goals'`, `'YourPlugin'`
782 * @param string $key
783 * @return mixed
784 */
785 public function getMetadata($pluginName, $key)
786 {
787 return isset($this->requestMetadata[$pluginName][$key]) ? $this->requestMetadata[$pluginName][$key] : null;
788 }
789 /**
790 * @param $idVisitor
791 * @return bool|string
792 */
793 private function getVisitorIdAsBinary($idVisitor)
794 {
795 $truncated = $this->truncateIdAsVisitorId($idVisitor);
796 $binVisitorId = @Common::hex2bin($truncated);
797 if (!empty($binVisitorId)) {
798 return $binVisitorId;
799 }
800 return \false;
801 }
802 }
803