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