API
3 years ago
Access
3 years ago
Application
4 years ago
Archive
3 years ago
ArchiveProcessor
3 years ago
Archiver
5 years ago
AssetManager
3 years ago
Auth
3 years ago
Category
5 years ago
Changes
4 years ago
CliMulti
4 years ago
Columns
3 years ago
Concurrency
3 years ago
Config
4 years ago
Container
4 years ago
CronArchive
3 years ago
DataAccess
3 years ago
DataFiles
5 years ago
DataTable
3 years ago
Db
3 years ago
DeviceDetector
3 years ago
Email
5 years ago
Exception
3 years ago
Http
4 years ago
Intl
4 years ago
Mail
3 years ago
Measurable
5 years ago
Menu
3 years ago
Metrics
4 years ago
Notification
4 years ago
Period
4 years ago
Plugin
3 years ago
ProfessionalServices
4 years ago
Report
5 years ago
ReportRenderer
3 years ago
Scheduler
4 years ago
Segment
3 years ago
Session
4 years ago
Settings
3 years ago
Tracker
3 years ago
Translation
4 years ago
UpdateCheck
5 years ago
Updater
3 years ago
Updates
3 years ago
Validators
4 years ago
View
4 years ago
ViewDataTable
3 years ago
Visualization
4 years ago
Widget
5 years ago
.htaccess
6 years ago
Access.php
4 years ago
Archive.php
4 years ago
ArchiveProcessor.php
4 years ago
AssetManager.php
4 years ago
Auth.php
5 years ago
AuthResult.php
5 years ago
BaseFactory.php
5 years ago
Cache.php
5 years ago
CacheId.php
5 years ago
CliMulti.php
4 years ago
Common.php
3 years ago
Config.php
3 years ago
Console.php
3 years ago
Context.php
5 years ago
Cookie.php
3 years ago
CronArchive.php
3 years ago
DataArray.php
4 years ago
DataTable.php
3 years ago
Date.php
4 years ago
Db.php
4 years ago
DbHelper.php
3 years ago
Development.php
5 years ago
ErrorHandler.php
5 years ago
EventDispatcher.php
3 years ago
ExceptionHandler.php
3 years ago
FileIntegrity.php
5 years ago
Filechecks.php
4 years ago
Filesystem.php
3 years ago
FrontController.php
3 years ago
Http.php
3 years ago
IP.php
4 years ago
Log.php
4 years ago
LogDeleter.php
4 years ago
Mail.php
4 years ago
Metrics.php
3 years ago
NoAccessException.php
5 years ago
Nonce.php
3 years ago
Notification.php
5 years ago
NumberFormatter.php
4 years ago
Option.php
4 years ago
Period.php
5 years ago
Piwik.php
3 years ago
Plugin.php
4 years ago
Profiler.php
4 years ago
ProxyHeaders.php
5 years ago
ProxyHttp.php
3 years ago
QuickForm2.php
5 years ago
RankingQuery.php
3 years ago
ReportRenderer.php
4 years ago
Segment.php
3 years ago
Sequence.php
5 years ago
Session.php
3 years ago
SettingsPiwik.php
3 years ago
SettingsServer.php
5 years ago
Singleton.php
5 years ago
Site.php
3 years ago
SiteContentDetector.php
3 years ago
SupportedBrowser.php
3 years ago
TCPDF.php
5 years ago
Theme.php
5 years ago
Timer.php
5 years ago
Tracker.php
4 years ago
Twig.php
4 years ago
Unzip.php
5 years ago
UpdateCheck.php
5 years ago
Updater.php
4 years ago
UpdaterErrorException.php
5 years ago
Updates.php
5 years ago
Url.php
3 years ago
UrlHelper.php
3 years ago
Version.php
3 years ago
View.php
3 years ago
bootstrap.php
3 years ago
dispatch.php
5 years ago
testMinimumPhpVersion.php
3 years ago
SiteContentDetector.php
434 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Matomo - free/libre analytics platform |
| 5 | * |
| 6 | * @link https://matomo.org |
| 7 | * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 8 | * |
| 9 | */ |
| 10 | |
| 11 | namespace Piwik; |
| 12 | |
| 13 | use Matomo\Cache\Lazy; |
| 14 | use Piwik\Config\GeneralConfig; |
| 15 | |
| 16 | /** |
| 17 | * This class provides detection functions for specific content on a site. It can be used to easily detect the |
| 18 | * presence of known third party code. |
| 19 | * |
| 20 | * Note: Calling the detect() method will create a HTTP request to the site to retrieve data, only the main site URL |
| 21 | * will be checked |
| 22 | * |
| 23 | * Usage: |
| 24 | * |
| 25 | * $contentDetector = new SiteContentDetector(); |
| 26 | * $contentDetector->detectContent([SiteContentDetector::GA3]); |
| 27 | * if ($contentDetector->ga3) { |
| 28 | * // site is using GA3 |
| 29 | * } |
| 30 | * |
| 31 | */ |
| 32 | class SiteContentDetector |
| 33 | { |
| 34 | // Content types |
| 35 | const ALL_CONTENT = 1; |
| 36 | const CONSENT_MANAGER = 2; |
| 37 | const GA3 = 3; |
| 38 | const GA4 = 4; |
| 39 | const GTM = 5; |
| 40 | |
| 41 | // Detection detail |
| 42 | public $consentManagerId; // Id of the detected consent manager, eg. 'osano' |
| 43 | public $consentManagerName; // Display name of the detected consent manager, eg. 'Osano' |
| 44 | public $consentManagerUrl; // Url for the configuration guide for the detected consent manager |
| 45 | public $isConnected = false; // True if the detected consent manager is already connected with Matomo |
| 46 | public $ga3; // True if GA3 was detected on the site |
| 47 | public $ga4; // True if GA4 was detected on the site |
| 48 | public $gtm; // True if GTM was detected on the site |
| 49 | |
| 50 | private $siteData; |
| 51 | |
| 52 | /** @var Lazy */ |
| 53 | private $cache; |
| 54 | |
| 55 | public function __construct(?Lazy $cache = null) |
| 56 | { |
| 57 | if ($cache === null) { |
| 58 | $this->cache = Cache::getLazyCache(); |
| 59 | } else { |
| 60 | $this->cache = $cache; |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Reset the detection properties |
| 66 | * |
| 67 | * @return void |
| 68 | */ |
| 69 | private function resetDetectionProperties(): void |
| 70 | { |
| 71 | $this->consentManagerId = null; |
| 72 | $this->consentManagerUrl = null; |
| 73 | $this->consentManagerName = null; |
| 74 | $this->isConnected = false; |
| 75 | $this->ga3 = false; |
| 76 | $this->ga4 = false; |
| 77 | $this->gtm = false; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * This will query the site and populate the class properties with |
| 82 | * the details of the detected content |
| 83 | * |
| 84 | * @param array $detectContent Array of content type for which to check, defaults to all, limiting this list |
| 85 | * will speed up the detection check |
| 86 | * @param ?int $idSite Override the site ID, will use the site from the current request if null |
| 87 | * @param string|null $siteData String containing the site data to search, if blank then data will be retrieved |
| 88 | * from the current request site via an http request |
| 89 | * @param int $timeOut How long to wait for the site to response, defaults to 5 seconds |
| 90 | * @return void |
| 91 | */ |
| 92 | public function detectContent(array $detectContent = [SiteContentDetector::ALL_CONTENT], |
| 93 | ?int $idSite = null, ?string $siteData = null, int $timeOut = 5): void |
| 94 | { |
| 95 | $this->resetDetectionProperties(); |
| 96 | |
| 97 | // If site data was passed in, then just run the detection checks against it and return. |
| 98 | if ($siteData) { |
| 99 | $this->siteData = $siteData; |
| 100 | $this->detectionChecks($detectContent); |
| 101 | return; |
| 102 | } |
| 103 | |
| 104 | // Get the site id from the request object if not explicitly passed |
| 105 | if ($idSite === null) { |
| 106 | if (!isset($_REQUEST['idSite'])) { |
| 107 | return; |
| 108 | } |
| 109 | |
| 110 | $idSite = Common::getRequestVar('idSite', null, 'int'); |
| 111 | |
| 112 | if (!$idSite) { |
| 113 | return; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | // Check and load previously cached site content detection data if it exists |
| 118 | $cacheKey = 'SiteContentDetector_' . $idSite; |
| 119 | $requiredProperties = $this->getRequiredProperties($detectContent); |
| 120 | $siteContentDetectionCache = $this->cache->fetch($cacheKey); |
| 121 | |
| 122 | if ($siteContentDetectionCache !== false) { |
| 123 | if ($this->checkCacheHasRequiredProperties($requiredProperties, $siteContentDetectionCache)) { |
| 124 | $this->loadRequiredPropertiesFromCache($requiredProperties, $siteContentDetectionCache); |
| 125 | return; |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | // No cache hit, no passed data, so make a request for the site content |
| 130 | $siteData = $this->requestSiteData($idSite, $timeOut); |
| 131 | |
| 132 | // Abort if still no site data |
| 133 | if ($siteData === null || $siteData === '') { |
| 134 | return; |
| 135 | } |
| 136 | |
| 137 | $this->siteData = $siteData; |
| 138 | |
| 139 | // We now have site data to analyze, so run the detection checks |
| 140 | $this->detectionChecks($detectContent); |
| 141 | |
| 142 | // A request was made to get this data and it isn't currently cached, so write it to the cache now |
| 143 | $cacheLife = (60 * 60 * 24 * 7); |
| 144 | $this->savePropertiesToCache($cacheKey, $requiredProperties, $cacheLife); |
| 145 | } |
| 146 | |
| 147 | /** |
| 148 | * Returns an array of properties required by the detect content array |
| 149 | * |
| 150 | * @param array $detectContent |
| 151 | * |
| 152 | * @return array |
| 153 | */ |
| 154 | private function getRequiredProperties(array $detectContent): array |
| 155 | { |
| 156 | $requiredProperties = []; |
| 157 | |
| 158 | if (in_array(SiteContentDetector::CONSENT_MANAGER, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 159 | $requiredProperties = array_merge($requiredProperties, ['consentManagerId', 'consentManagerName', 'consentManagerUrl', 'isConnected']); |
| 160 | } |
| 161 | |
| 162 | if (in_array(SiteContentDetector::GA3, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 163 | $requiredProperties[] = 'ga3'; |
| 164 | } |
| 165 | |
| 166 | if (in_array(SiteContentDetector::GA4, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 167 | $requiredProperties[] = 'ga4'; |
| 168 | } |
| 169 | |
| 170 | if (in_array(SiteContentDetector::GTM, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 171 | $requiredProperties[] = 'gtm'; |
| 172 | } |
| 173 | |
| 174 | return $requiredProperties; |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * Checks that all required properties are in the cache array |
| 179 | * |
| 180 | * @param array $properties |
| 181 | * @param array $cache |
| 182 | * |
| 183 | * @return bool |
| 184 | */ |
| 185 | private function checkCacheHasRequiredProperties(array $properties, array $cache): bool |
| 186 | { |
| 187 | foreach ($properties as $prop) { |
| 188 | if (!array_key_exists($prop, $cache)) { |
| 189 | return false; |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | return true; |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * Load object properties from the cache array |
| 198 | * |
| 199 | * @param array $properties |
| 200 | * @param array $cache |
| 201 | * |
| 202 | * @return void |
| 203 | */ |
| 204 | private function loadRequiredPropertiesFromCache(array $properties, array $cache): void |
| 205 | { |
| 206 | foreach ($properties as $prop) { |
| 207 | if (!array_key_exists($prop, $cache)) { |
| 208 | continue; |
| 209 | } |
| 210 | |
| 211 | $this->{$prop} = $cache[$prop]; |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Save properties to the cache |
| 217 | * |
| 218 | * @param string $cacheKey |
| 219 | * @param array $properties |
| 220 | * @param int $cacheLife |
| 221 | * |
| 222 | * @return void |
| 223 | */ |
| 224 | private function savePropertiesToCache(string $cacheKey, array $properties, int $cacheLife): void |
| 225 | { |
| 226 | |
| 227 | $cacheData = []; |
| 228 | |
| 229 | // Load any existing cached values |
| 230 | $siteContentDetectionCache = $this->cache->fetch($cacheKey); |
| 231 | |
| 232 | if (is_array($siteContentDetectionCache)) { |
| 233 | $cacheData = $siteContentDetectionCache; |
| 234 | } |
| 235 | |
| 236 | foreach ($properties as $prop) { |
| 237 | $cacheData[$prop] = $this->{$prop}; |
| 238 | } |
| 239 | |
| 240 | $this->cache->save($cacheKey, $cacheData, $cacheLife); |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Run various detection checks for site content |
| 245 | * |
| 246 | * @param array $detectContent Array of detection types used to filter the checks that are run |
| 247 | * |
| 248 | * @return void |
| 249 | */ |
| 250 | private function detectionChecks($detectContent): void |
| 251 | { |
| 252 | if (in_array(SiteContentDetector::CONSENT_MANAGER, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 253 | $this->detectConsentManager(); |
| 254 | } |
| 255 | |
| 256 | if (in_array(SiteContentDetector::GA3, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 257 | $this->detectGA3(); |
| 258 | } |
| 259 | |
| 260 | if (in_array(SiteContentDetector::GA4, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 261 | $this->detectGA4(); |
| 262 | } |
| 263 | |
| 264 | if (in_array(SiteContentDetector::GTM, $detectContent) || in_array(SiteContentDetector::ALL_CONTENT, $detectContent)) { |
| 265 | $this->detectGTM(); |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Retrieve data from the specified site using an HTTP request |
| 271 | * |
| 272 | * @param int $idSite |
| 273 | * @param int $timeOut |
| 274 | * |
| 275 | * @return string|null |
| 276 | */ |
| 277 | private function requestSiteData(int $idSite, int $timeOut): ?string |
| 278 | { |
| 279 | // If internet features are disabled, we don't try to fetch any site content |
| 280 | if (0 === (int) GeneralConfig::getConfigValue('enable_internet_features')) { |
| 281 | return null; |
| 282 | } |
| 283 | |
| 284 | $url = Site::getMainUrlFor($idSite); |
| 285 | |
| 286 | if (!$url) { |
| 287 | return null; |
| 288 | } |
| 289 | |
| 290 | $siteData = null; |
| 291 | |
| 292 | try { |
| 293 | $siteData = Http::sendHttpRequestBy(Http::getTransportMethod(), $url, $timeOut, null, null, |
| 294 | null, 0, false, true); |
| 295 | } catch (\Exception $e) { |
| 296 | } |
| 297 | |
| 298 | return $siteData; |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Detect known consent managers in the site data |
| 303 | * |
| 304 | * Populate this object's properties with the results |
| 305 | * |
| 306 | * @return void |
| 307 | */ |
| 308 | private function detectConsentManager(): void |
| 309 | { |
| 310 | $defs = SiteContentDetector::getConsentManagerDefinitions(); |
| 311 | |
| 312 | if (!$defs) { |
| 313 | return; |
| 314 | } |
| 315 | |
| 316 | foreach ($defs as $consentManagerId => $consentManagerDef) { |
| 317 | foreach ($consentManagerDef['detectStrings'] as $dStr) { |
| 318 | if (strpos($this->siteData, $dStr) !== false && array_key_exists($consentManagerId, $defs)) { |
| 319 | $this->consentManagerId = $consentManagerId; |
| 320 | $this->consentManagerName = $consentManagerDef['name']; |
| 321 | $this->consentManagerUrl = $consentManagerDef['url']; |
| 322 | break 2; |
| 323 | } |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | if (!isset($defs[$this->consentManagerId]['connectedStrings'])) { |
| 328 | return; |
| 329 | } |
| 330 | |
| 331 | // If a consent manager was detected then perform an additional check to see if it has been connected to Matomo |
| 332 | foreach ($defs[$this->consentManagerId]['connectedStrings'] as $cStr) { |
| 333 | if (strpos($this->siteData, $cStr) !== false) { |
| 334 | $this->isConnected = true; |
| 335 | break; |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * Detect GA3 usage from the site data |
| 342 | * |
| 343 | * @return void |
| 344 | */ |
| 345 | private function detectGA3(): void |
| 346 | { |
| 347 | if (strpos($this->siteData, '(i,s,o,g,r,a,m)') !== false) { |
| 348 | $this->ga3 = true; |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | /** |
| 353 | * Detect GA4 usage from the site data |
| 354 | * |
| 355 | * @return void |
| 356 | */ |
| 357 | private function detectGA4(): void |
| 358 | { |
| 359 | if (strpos($this->siteData, 'gtag.js') !== false) { |
| 360 | $this->ga4 = true; |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * Detect GTM usage from the site data |
| 366 | * |
| 367 | * @return void |
| 368 | */ |
| 369 | private function detectGTM(): void |
| 370 | { |
| 371 | if (strpos($this->siteData, 'gtm.js') !== false) { |
| 372 | $this->gtm = true; |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | /** |
| 377 | * Return an array of consent manager definitions which can be used to detect their presence on the site and show |
| 378 | * the associated guide links |
| 379 | * |
| 380 | * @return array[] |
| 381 | */ |
| 382 | public static function getConsentManagerDefinitions(): array |
| 383 | { |
| 384 | return [ |
| 385 | |
| 386 | 'osano' => [ |
| 387 | 'name' => 'Osano', |
| 388 | 'detectStrings' => ['osano.com'], |
| 389 | 'connectedStrings' => ["Osano.cm.addEventListener('osano-cm-consent-changed', (change) => { console.log('cm-change'); consentSet(change); });"], |
| 390 | 'url' => 'https://matomo.org/faq/how-to/using-osano-consent-manager-with-matomo', |
| 391 | ], |
| 392 | |
| 393 | 'cookiebot' => [ |
| 394 | 'name' => 'Cookiebot', |
| 395 | 'detectStrings' => ['cookiebot.com'], |
| 396 | 'connectedStrings' => ["typeof _paq === 'undefined' || typeof Cookiebot === 'undefined'"], |
| 397 | 'url' => 'https://matomo.org/faq/how-to/using-cookiebot-consent-manager-with-matomo', |
| 398 | ], |
| 399 | |
| 400 | 'cookieyes' => [ |
| 401 | 'name' => 'CookieYes', |
| 402 | 'detectStrings' => ['cookieyes.com'], |
| 403 | 'connectedStrings' => ['document.addEventListener("cookieyes_consent_update", function (eventData)'], |
| 404 | 'url' => 'https://matomo.org/faq/how-to/using-cookieyes-consent-manager-with-matomo', |
| 405 | ], |
| 406 | |
| 407 | // Note: tarte au citron pro is configured server side so we cannot tell if it has been connected by |
| 408 | // crawling the website, however setup of Matomo with the pro version is automatic, so displaying the guide |
| 409 | // link for pro isn't necessary. Only the open source version is detected by this definition. |
| 410 | 'tarteaucitron' => [ |
| 411 | 'name' => 'Tarte au Citron', |
| 412 | 'detectStrings' => ['tarteaucitron.js'], |
| 413 | 'connectedStrings' => ['tarteaucitron.user.matomoHost'], |
| 414 | 'url' => 'https://matomo.org/faq/how-to/using-tarte-au-citron-consent-manager-with-matomo', |
| 415 | ], |
| 416 | |
| 417 | 'klaro' => [ |
| 418 | 'name' => 'Klaro', |
| 419 | 'detectStrings' => ['klaro.js', 'kiprotect.com'], |
| 420 | 'connectedStrings' => ['KlaroWatcher()', "title: 'Matomo',"], |
| 421 | 'url' => 'https://matomo.org/faq/how-to/using-klaro-consent-manager-with-matomo', |
| 422 | ], |
| 423 | |
| 424 | 'complianz' => [ |
| 425 | 'name' => 'Complianz', |
| 426 | 'detectStrings' => ['complianz-gdpr'], |
| 427 | 'connectedStrings' => ["if (!cmplz_in_array( 'statistics', consentedCategories )) { |
| 428 | _paq.push(['forgetCookieConsentGiven']);"], |
| 429 | 'url' => 'https://matomo.org/faq/how-to/using-complianz-for-wordpress-consent-manager-with-matomo', |
| 430 | ], |
| 431 | ]; |
| 432 | } |
| 433 | } |
| 434 |