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