PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.5
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.5
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 / SiteContentDetector.php
matomo / app / core Last commit date
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