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