Config
4 months ago
Db
3 months ago
Handler
2 years ago
Visit
1 month ago
Action.php
3 months ago
ActionPageview.php
2 years ago
BotRequest.php
3 months ago
BotRequestProcessor.php
1 month ago
Cache.php
6 months ago
Db.php
1 year ago
Failures.php
6 months ago
FingerprintSalt.php
1 year ago
GoalManager.php
1 month ago
Handler.php
2 years ago
IgnoreCookie.php
1 year ago
LogTable.php
1 year ago
Model.php
6 months ago
PageUrl.php
2 weeks ago
Request.php
1 month ago
RequestHandlerTrait.php
4 months ago
RequestProcessor.php
1 month ago
RequestSet.php
6 months ago
Response.php
3 months ago
ScheduledTasksRunner.php
1 year ago
Settings.php
3 months ago
TableLogAction.php
6 months ago
TrackerCodeGenerator.php
1 year ago
TrackerConfig.php
1 month ago
Visit.php
3 months ago
VisitExcluded.php
3 months ago
VisitInterface.php
3 months ago
Visitor.php
1 month ago
VisitorNotFoundInDb.php
1 month ago
VisitorRecognizer.php
1 year ago
VisitExcluded.php
324 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 Piwik\Cache as PiwikCache; |
| 12 | use Piwik\Common; |
| 13 | use Piwik\Container\StaticContainer; |
| 14 | use Piwik\DeviceDetector\DeviceDetectorFactory; |
| 15 | use Piwik\Exception\UnexpectedWebsiteFoundException; |
| 16 | use Matomo\Network\IP; |
| 17 | use Piwik\Piwik; |
| 18 | use Piwik\Plugins\SitesManager\SiteUrls; |
| 19 | use Piwik\Tracker\Visit\ReferrerSpamFilter; |
| 20 | use Piwik\Config; |
| 21 | /** |
| 22 | * This class contains the logic to exclude some visitors from being tracked as per user settings |
| 23 | */ |
| 24 | class VisitExcluded |
| 25 | { |
| 26 | /** |
| 27 | * @var ReferrerSpamFilter |
| 28 | */ |
| 29 | private $spamFilter; |
| 30 | private $siteCache = array(); |
| 31 | public $request; |
| 32 | public $idSite; |
| 33 | public $userAgent; |
| 34 | public $ip; |
| 35 | public function __construct(\Piwik\Tracker\Request $request) |
| 36 | { |
| 37 | $this->spamFilter = new ReferrerSpamFilter(); |
| 38 | $this->request = $request; |
| 39 | try { |
| 40 | $this->idSite = $request->getIdSite(); |
| 41 | } catch (UnexpectedWebsiteFoundException $e) { |
| 42 | // most checks will still work on a global scope and we still want to be able to test if this is a valid |
| 43 | // visit or not |
| 44 | $this->idSite = 0; |
| 45 | } |
| 46 | $userAgent = $request->getUserAgent(); |
| 47 | $this->userAgent = Common::unsanitizeInputValue($userAgent); |
| 48 | $this->ip = $request->getIp(); |
| 49 | } |
| 50 | /** |
| 51 | * Test if the current visitor is excluded from the statistics. |
| 52 | * |
| 53 | * Plugins can for example exclude visitors based on the |
| 54 | * - IP |
| 55 | * - If a given cookie is found |
| 56 | * |
| 57 | * @return bool True if the visit must not be saved, false otherwise |
| 58 | */ |
| 59 | public function isExcluded() |
| 60 | { |
| 61 | $excluded = \false; |
| 62 | if ($this->isNonHumanBot()) { |
| 63 | Common::printDebug('Search bot detected, visit excluded'); |
| 64 | $excluded = \true; |
| 65 | } |
| 66 | /* |
| 67 | * Requests built with piwik.js will contain a rec=1 parameter. This is used as |
| 68 | * an indication that the request is made by a JS enabled device. By default, Piwik |
| 69 | * doesn't track non-JS visitors. |
| 70 | */ |
| 71 | if (!$excluded) { |
| 72 | $toRecord = $this->request->getParam($parameterForceRecord = 'rec'); |
| 73 | if (!$toRecord) { |
| 74 | Common::printDebug(@$_SERVER['REQUEST_METHOD'] . ' parameter ' . $parameterForceRecord . ' not found in URL, request excluded'); |
| 75 | $excluded = \true; |
| 76 | Common::printDebug("'{$parameterForceRecord}' parameter not found."); |
| 77 | } |
| 78 | } |
| 79 | /** |
| 80 | * Triggered on every tracking request. |
| 81 | * |
| 82 | * This event can be used to tell the Tracker not to record this particular action or visit. |
| 83 | * |
| 84 | * @param bool &$excluded Whether the request should be excluded or not. Initialized |
| 85 | * to `false`. Event subscribers should set it to `true` in |
| 86 | * order to exclude the request. |
| 87 | * @param Request $request The request object which contains all of the request's information |
| 88 | * |
| 89 | */ |
| 90 | Piwik::postEvent('Tracker.isExcludedVisit', array(&$excluded, $this->request)); |
| 91 | /* |
| 92 | * Following exclude operations happen after the hook. |
| 93 | * These are of higher priority and should not be overwritten by plugins. |
| 94 | */ |
| 95 | // Checking if in config some requests are excluded |
| 96 | if (!$excluded) { |
| 97 | $excluded = $this->request->isRequestExcluded(); |
| 98 | if ($excluded) { |
| 99 | Common::printDebug("Request is excluded."); |
| 100 | } |
| 101 | } |
| 102 | // Checking if the Piwik ignore cookie is set |
| 103 | if (!$excluded) { |
| 104 | $excluded = $this->isIgnoreCookieFound(); |
| 105 | if ($excluded) { |
| 106 | Common::printDebug("Ignore cookie found."); |
| 107 | } |
| 108 | } |
| 109 | // Checking for excluded IPs |
| 110 | if (!$excluded) { |
| 111 | $excluded = $this->isVisitorIpExcluded(); |
| 112 | if ($excluded) { |
| 113 | Common::printDebug("IP excluded."); |
| 114 | } |
| 115 | } |
| 116 | // Check if user agent should be excluded |
| 117 | if (!$excluded) { |
| 118 | $excluded = $this->isUserAgentExcluded(); |
| 119 | if ($excluded) { |
| 120 | Common::printDebug("User agent excluded."); |
| 121 | } |
| 122 | } |
| 123 | // Check if Referrer URL is a known spam |
| 124 | $generalConfig = Config::getInstance()->Tracker; |
| 125 | if ($generalConfig['enable_spam_filter']) { |
| 126 | if (!$excluded) { |
| 127 | $excluded = $this->isReferrerSpamExcluded(); |
| 128 | if ($excluded) { |
| 129 | Common::printDebug("Referrer URL is listed as spam."); |
| 130 | } |
| 131 | } |
| 132 | } else { |
| 133 | Common::printDebug("Spam list is disabled."); |
| 134 | } |
| 135 | // Check if request URL is excluded |
| 136 | if (!$excluded) { |
| 137 | $excluded = $this->isUrlExcluded(); |
| 138 | if ($excluded) { |
| 139 | Common::printDebug("Unknown URL is not allowed to track."); |
| 140 | } |
| 141 | } |
| 142 | if (!$excluded) { |
| 143 | if ($this->isPrefetchDetected()) { |
| 144 | $excluded = \true; |
| 145 | Common::printDebug("Prefetch request detected, not a real visit so we Ignore this visit/pageview"); |
| 146 | } |
| 147 | } |
| 148 | if ($excluded) { |
| 149 | Common::printDebug("Visitor excluded."); |
| 150 | return \true; |
| 151 | } |
| 152 | return \false; |
| 153 | } |
| 154 | protected function isPrefetchDetected() |
| 155 | { |
| 156 | return isset($_SERVER["HTTP_X_PURPOSE"]) && in_array($_SERVER["HTTP_X_PURPOSE"], array("preview", "instant")) || isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == "prefetch"; |
| 157 | } |
| 158 | /** |
| 159 | * Live/Bing/MSN bot and Googlebot are evolving to detect cloaked websites. |
| 160 | * As a result, these sophisticated bots exhibit characteristics of |
| 161 | * browsers (cookies enabled, executing JavaScript, etc). |
| 162 | * |
| 163 | * @see \DeviceDetector\Parser\Bot |
| 164 | * |
| 165 | * @return boolean |
| 166 | */ |
| 167 | protected function isNonHumanBot() |
| 168 | { |
| 169 | $allowBots = $this->request->getParam('bots'); |
| 170 | $deviceDetector = StaticContainer::get(DeviceDetectorFactory::class)->makeInstance($this->userAgent, $this->request->getClientHints()); |
| 171 | return !$allowBots && ($deviceDetector->isBot() || $this->isIpInRange()); |
| 172 | } |
| 173 | private function isIpInRange() |
| 174 | { |
| 175 | $cache = PiwikCache::getTransientCache(); |
| 176 | $ip = IP::fromBinaryIP($this->ip); |
| 177 | $key = 'VisitExcludedIsIpInRange' . $ip->toString(); |
| 178 | if ($cache->contains($key)) { |
| 179 | $isInRanges = $cache->fetch($key); |
| 180 | } else { |
| 181 | if ($this->isChromeDataSaverUsed($ip)) { |
| 182 | $isInRanges = \false; |
| 183 | } else { |
| 184 | $isInRanges = $ip->isInRanges($this->getBotIpRanges()); |
| 185 | } |
| 186 | $cache->save($key, $isInRanges); |
| 187 | } |
| 188 | return $isInRanges; |
| 189 | } |
| 190 | public function isChromeDataSaverUsed(IP $ip) |
| 191 | { |
| 192 | // see https://github.com/piwik/piwik/issues/7733 |
| 193 | return !empty($_SERVER['HTTP_VIA']) && \false !== strpos(strtolower($_SERVER['HTTP_VIA']), 'chrome-compression-proxy') && $ip->isInRanges($this->getGoogleBotIpRanges()); |
| 194 | } |
| 195 | protected function getBotIpRanges() |
| 196 | { |
| 197 | return array_merge($this->getGoogleBotIpRanges(), array( |
| 198 | // Live/Bing/MSN |
| 199 | '64.4.0.0/18', |
| 200 | '65.52.0.0/14', |
| 201 | '157.54.0.0/15', |
| 202 | '157.56.0.0/14', |
| 203 | '157.60.0.0/16', |
| 204 | '207.46.0.0/16', |
| 205 | '207.68.128.0/18', |
| 206 | '207.68.192.0/20', |
| 207 | '131.253.26.0/20', |
| 208 | '131.253.24.0/20', |
| 209 | // Yahoo |
| 210 | '72.30.198.0/20', |
| 211 | '72.30.196.0/20', |
| 212 | '98.137.207.0/20', |
| 213 | // Chinese bot hammering websites |
| 214 | '1.202.218.8', |
| 215 | )); |
| 216 | } |
| 217 | private function getGoogleBotIpRanges() |
| 218 | { |
| 219 | return array('216.239.32.0/19', '64.233.160.0/19', '66.249.80.0/20', '72.14.192.0/18', '209.85.128.0/17', '66.102.0.0/20', '74.125.0.0/16', '64.18.0.0/20', '207.126.144.0/20', '173.194.0.0/16'); |
| 220 | } |
| 221 | /** |
| 222 | * Looks for the ignore cookie that users can set in the Piwik admin screen. |
| 223 | * @return bool |
| 224 | */ |
| 225 | protected function isIgnoreCookieFound() |
| 226 | { |
| 227 | if (\Piwik\Tracker\IgnoreCookie::isIgnoreCookieFound()) { |
| 228 | Common::printDebug('Matomo ignore cookie was found, visit not tracked.'); |
| 229 | return \true; |
| 230 | } |
| 231 | return \false; |
| 232 | } |
| 233 | /** |
| 234 | * Checks if the visitor ip is in the excluded list |
| 235 | * |
| 236 | * @return bool |
| 237 | */ |
| 238 | protected function isVisitorIpExcluded() |
| 239 | { |
| 240 | $excludedIps = $this->getAttributes('excluded_ips', 'global_excluded_ips'); |
| 241 | if (!empty($excludedIps)) { |
| 242 | $ip = IP::fromBinaryIP($this->ip); |
| 243 | if ($ip->isInRanges($excludedIps)) { |
| 244 | Common::printDebug('Visitor IP ' . $ip->toString() . ' is excluded from being tracked'); |
| 245 | return \true; |
| 246 | } |
| 247 | } |
| 248 | return \false; |
| 249 | } |
| 250 | private function getAttributes($siteAttribute, $globalAttribute) |
| 251 | { |
| 252 | if (!isset($this->siteCache[$this->idSite])) { |
| 253 | $this->siteCache[$this->idSite] = array(); |
| 254 | } |
| 255 | try { |
| 256 | if (empty($this->siteCache[$this->idSite])) { |
| 257 | $this->siteCache[$this->idSite] = \Piwik\Tracker\Cache::getCacheWebsiteAttributes($this->idSite); |
| 258 | } |
| 259 | if (isset($this->siteCache[$this->idSite][$siteAttribute])) { |
| 260 | return $this->siteCache[$this->idSite][$siteAttribute]; |
| 261 | } |
| 262 | } catch (UnexpectedWebsiteFoundException $e) { |
| 263 | $cached = \Piwik\Tracker\Cache::getCacheGeneral(); |
| 264 | if ($globalAttribute && isset($cached[$globalAttribute])) { |
| 265 | return $cached[$globalAttribute]; |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | /** |
| 270 | * Checks if request URL is excluded |
| 271 | * @return bool |
| 272 | */ |
| 273 | protected function isUrlExcluded() |
| 274 | { |
| 275 | $excludedUrls = $this->getAttributes('exclude_unknown_urls', null); |
| 276 | $siteUrls = $this->getAttributes('urls', null); |
| 277 | if (!empty($excludedUrls) && !empty($siteUrls)) { |
| 278 | $url = $this->request->getParam('url'); |
| 279 | $parsedUrl = parse_url($url); |
| 280 | $trackingUrl = new SiteUrls(); |
| 281 | $urls = $trackingUrl->groupUrlsByHost(array($this->idSite => $siteUrls)); |
| 282 | $idSites = $trackingUrl->getIdSitesMatchingUrl($parsedUrl, $urls); |
| 283 | $isUrlExcluded = !isset($idSites) || !in_array($this->idSite, $idSites); |
| 284 | return $isUrlExcluded; |
| 285 | } |
| 286 | return \false; |
| 287 | } |
| 288 | /** |
| 289 | * Returns true if the specified user agent should be excluded for the current site or not. |
| 290 | * |
| 291 | * Visits whose user agent string contains one of the excluded_user_agents strings for the |
| 292 | * site being tracked (or one of the global strings) will be excluded. Regular expressions |
| 293 | * are also supported. |
| 294 | * |
| 295 | * @internal param string $this ->userAgent The user agent string. |
| 296 | */ |
| 297 | protected function isUserAgentExcluded() : bool |
| 298 | { |
| 299 | $excludedAgents = $this->getAttributes('excluded_user_agents', 'global_excluded_user_agents'); |
| 300 | if (!empty($excludedAgents)) { |
| 301 | foreach ($excludedAgents as $excludedUserAgent) { |
| 302 | // if the excluded user agent string part is in this visit's user agent, this visit should be excluded |
| 303 | if (stripos($this->userAgent, $excludedUserAgent) !== \false) { |
| 304 | return \true; |
| 305 | } |
| 306 | // if the string is a valid regex, and the user agent matches, this visit should be excluded |
| 307 | if (@preg_match($excludedUserAgent, '') !== \false && preg_match($excludedUserAgent, $this->userAgent) === 1) { |
| 308 | return \true; |
| 309 | } |
| 310 | } |
| 311 | } |
| 312 | return \false; |
| 313 | } |
| 314 | /** |
| 315 | * Returns true if the Referrer is a known spammer. |
| 316 | * |
| 317 | * @return bool |
| 318 | */ |
| 319 | protected function isReferrerSpamExcluded() |
| 320 | { |
| 321 | return $this->spamFilter->isSpam($this->request); |
| 322 | } |
| 323 | } |
| 324 |