API
3 months ago
Access
3 months ago
Application
3 months ago
Archive
3 months ago
ArchiveProcessor
3 months ago
Archiver
2 years ago
AssetManager
3 months ago
Auth
6 months ago
Category
6 months ago
Changes
3 months ago
CliMulti
1 year ago
Columns
3 months ago
Concurrency
3 months ago
Config
3 months ago
Container
3 months ago
CronArchive
3 months ago
DataAccess
3 months ago
DataFiles
2 years ago
DataTable
3 months ago
Db
3 months ago
DeviceDetector
1 year ago
Email
2 years ago
Exception
4 months ago
Http
4 months ago
Intl
3 months ago
Log
2 years ago
Mail
1 year ago
Measurable
6 months ago
Menu
3 months ago
Metrics
3 months ago
Notification
6 months ago
Period
3 months ago
Plugin
3 months ago
Policy
3 months ago
ProfessionalServices
1 year ago
Report
1 year ago
ReportRenderer
3 months ago
Request
3 months ago
Scheduler
3 months ago
Segment
3 months ago
Session
3 months ago
Settings
3 months ago
Tracker
3 months ago
Translation
3 months ago
Twig
1 year ago
UpdateCheck
3 months ago
Updater
4 months ago
Updates
3 months ago
Validators
1 year ago
View
6 months ago
ViewDataTable
3 months ago
Visualization
1 year ago
Widget
3 months ago
.htaccess
2 years ago
Access.php
3 months ago
Archive.php
3 months ago
ArchiveProcessor.php
4 months ago
AssetManager.php
3 months ago
Auth.php
6 months ago
AuthResult.php
6 months ago
BaseFactory.php
2 years ago
Cache.php
2 years ago
CacheId.php
4 months ago
CliMulti.php
3 months ago
Common.php
3 months ago
Config.php
3 months ago
Console.php
3 months ago
Context.php
2 years ago
Cookie.php
1 year ago
CronArchive.php
3 months ago
DI.php
3 months ago
DataArray.php
5 months ago
DataTable.php
3 months ago
Date.php
3 months ago
Db.php
3 months ago
DbHelper.php
3 months ago
Development.php
1 year ago
ErrorHandler.php
6 months ago
EventDispatcher.php
1 year ago
ExceptionHandler.php
4 months ago
FileIntegrity.php
3 months ago
Filechecks.php
1 year ago
Filesystem.php
3 months ago
FrontController.php
4 months ago
Http.php
4 months ago
IP.php
1 year ago
Log.php
3 months ago
LogDeleter.php
1 year ago
Mail.php
1 year ago
Metrics.php
3 months ago
NoAccessException.php
2 years ago
Nonce.php
6 months ago
Notification.php
6 months ago
NumberFormatter.php
5 months ago
Option.php
5 months ago
Period.php
3 months ago
Piwik.php
3 months ago
Plugin.php
3 months ago
Process.php
1 year ago
Profiler.php
6 months ago
ProxyHeaders.php
4 months ago
ProxyHttp.php
5 months ago
QuickForm2.php
3 months ago
RankingQuery.php
5 months ago
ReportRenderer.php
3 months ago
Request.php
3 months ago
Segment.php
3 months ago
Sequence.php
6 months ago
Session.php
3 months ago
SettingsPiwik.php
3 months ago
SettingsServer.php
1 year ago
Singleton.php
2 years ago
Site.php
4 months ago
SiteContentDetector.php
3 months ago
SupportedBrowser.php
2 years ago
TCPDF.php
1 year ago
Theme.php
1 year ago
Timer.php
2 years ago
Tracker.php
3 months ago
Twig.php
3 months ago
Unzip.php
1 year ago
UpdateCheck.php
3 months ago
Updater.php
3 months ago
UpdaterErrorException.php
2 years ago
Updates.php
3 months ago
Url.php
3 months ago
UrlHelper.php
3 months ago
Version.php
3 months ago
View.php
3 months ago
bootstrap.php
1 year ago
dispatch.php
2 years ago
testMinimumPhpVersion.php
6 months ago
SiteContentDetector.php
315 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; |
| 10 | |
| 11 | use Matomo\Cache\Lazy; |
| 12 | use Piwik\Config\GeneralConfig; |
| 13 | use Piwik\Container\StaticContainer; |
| 14 | use Piwik\Plugins\SitesManager\SiteContentDetection\ConsentManagerDetectionAbstract; |
| 15 | use Piwik\Plugins\SitesManager\SiteContentDetection\SiteContentDetectionAbstract; |
| 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 `detectContent()` 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([GoogleAnalytics3::getId()]); |
| 27 | * if ($contentDetector->ga3) { |
| 28 | * // site is using GA3 |
| 29 | * } |
| 30 | * |
| 31 | * @api |
| 32 | */ |
| 33 | class SiteContentDetector |
| 34 | { |
| 35 | /** |
| 36 | * @var array<string, array<string, SiteContentDetectionAbstract>> |
| 37 | */ |
| 38 | public $detectedContent = [SiteContentDetectionAbstract::TYPE_TRACKER => [], SiteContentDetectionAbstract::TYPE_CMS => [], SiteContentDetectionAbstract::TYPE_JS_FRAMEWORK => [], SiteContentDetectionAbstract::TYPE_CONSENT_MANAGER => [], SiteContentDetectionAbstract::TYPE_JS_CRASH_ANALYTICS => [], SiteContentDetectionAbstract::TYPE_OTHER => []]; |
| 39 | public $connectedConsentManagers = []; |
| 40 | private $siteResponse = ['data' => '', 'headers' => []]; |
| 41 | /** @var Lazy */ |
| 42 | private $cache; |
| 43 | public function __construct(?Lazy $cache = null) |
| 44 | { |
| 45 | if ($cache === null) { |
| 46 | $this->cache = \Piwik\Cache::getLazyCache(); |
| 47 | } else { |
| 48 | $this->cache = $cache; |
| 49 | } |
| 50 | } |
| 51 | /** |
| 52 | * @return array<string, SiteContentDetectionAbstract[]> |
| 53 | */ |
| 54 | public static function getSiteContentDetectionsByType() : array |
| 55 | { |
| 56 | $instancesByType = []; |
| 57 | $classes = self::getAllSiteContentDetectionClasses(); |
| 58 | foreach ($classes as $className) { |
| 59 | $instancesByType[$className::getContentType()][] = StaticContainer::get($className); |
| 60 | } |
| 61 | return $instancesByType; |
| 62 | } |
| 63 | /** |
| 64 | * Returns the site content detection object with the provided id, or null if it can't be found |
| 65 | * |
| 66 | */ |
| 67 | public function getSiteContentDetectionById(string $id) : ?SiteContentDetectionAbstract |
| 68 | { |
| 69 | $classes = $this->getAllSiteContentDetectionClasses(); |
| 70 | foreach ($classes as $className) { |
| 71 | if ($className::getId() === $id) { |
| 72 | return StaticContainer::get($className); |
| 73 | } |
| 74 | } |
| 75 | return null; |
| 76 | } |
| 77 | /** |
| 78 | * @return string[] |
| 79 | */ |
| 80 | protected static function getAllSiteContentDetectionClasses() : array |
| 81 | { |
| 82 | return \Piwik\Plugin\Manager::getInstance()->findMultipleComponents('SiteContentDetection', SiteContentDetectionAbstract::class); |
| 83 | } |
| 84 | /** |
| 85 | * Reset the detections |
| 86 | * |
| 87 | */ |
| 88 | private function resetDetections() : void |
| 89 | { |
| 90 | $this->detectedContent = [SiteContentDetectionAbstract::TYPE_TRACKER => [], SiteContentDetectionAbstract::TYPE_CMS => [], SiteContentDetectionAbstract::TYPE_JS_FRAMEWORK => [], SiteContentDetectionAbstract::TYPE_CONSENT_MANAGER => [], SiteContentDetectionAbstract::TYPE_JS_CRASH_ANALYTICS => [], SiteContentDetectionAbstract::TYPE_OTHER => []]; |
| 91 | $this->connectedConsentManagers = []; |
| 92 | } |
| 93 | /** |
| 94 | * This will query the site and populate the class properties with |
| 95 | * the details of the detected content |
| 96 | * |
| 97 | * @param array $detectContent Array of content type for which to check, defaults to all, limiting this list |
| 98 | * will speed up the detection check. |
| 99 | * Allowed values are: |
| 100 | * * empty array - to run all detections |
| 101 | * * an array containing ids of detections, e.g. Wordpress::getId() or any of the |
| 102 | * type constants, e.g. SiteContentDetectionAbstract::TYPE_TRACKER |
| 103 | * @param ?int $idSite Override the site ID, will use the site from the current request if null |
| 104 | * @param ?array $siteResponse String containing the site data to search, if blank then data will be retrieved |
| 105 | * from the current request site via an http request |
| 106 | * @param int $timeOut How long to wait for the site to response, defaults to 5 seconds |
| 107 | */ |
| 108 | public function detectContent(array $detectContent = [], ?int $idSite = null, ?array $siteResponse = null, int $timeOut = 5) : void |
| 109 | { |
| 110 | $this->resetDetections(); |
| 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 | // Get the site id from the request object if not explicitly passed |
| 118 | if ($idSite === null) { |
| 119 | $idSite = \Piwik\Request::fromRequest()->getIntegerParameter('idSite', 0); |
| 120 | if (!$idSite) { |
| 121 | return; |
| 122 | } |
| 123 | } |
| 124 | $url = \Piwik\Site::getMainUrlFor($idSite); |
| 125 | // Check and load previously cached site content detection data if it exists |
| 126 | $cacheKey = 'SiteContentDetection_' . md5($url); |
| 127 | $siteContentDetectionCache = $this->cache->fetch($cacheKey); |
| 128 | if ($siteContentDetectionCache !== \false) { |
| 129 | if ($this->checkCacheHasRequiredProperties($detectContent, $siteContentDetectionCache)) { |
| 130 | $this->detectedContent = $siteContentDetectionCache['detectedContent']; |
| 131 | $this->connectedConsentManagers = $siteContentDetectionCache['connectedConsentManagers']; |
| 132 | return; |
| 133 | } |
| 134 | } |
| 135 | // No cache hit, no passed data, so make a request for the site content |
| 136 | $siteResponse = $this->requestSiteResponse($url, $timeOut); |
| 137 | // Abort if still no site data |
| 138 | if (empty($siteResponse['data'])) { |
| 139 | return; |
| 140 | } |
| 141 | $this->siteResponse = $siteResponse; |
| 142 | // We now have site data to analyze, so run the detection checks |
| 143 | $this->detectionChecks($detectContent); |
| 144 | // A request was made to get this data and it isn't currently cached, so write it to the cache now |
| 145 | $cacheLife = 60 * 60 * 24 * 7; |
| 146 | $this->saveToCache($cacheKey, $cacheLife); |
| 147 | } |
| 148 | /** |
| 149 | * Returns if the detection with the provided id was detected or not |
| 150 | * |
| 151 | * Note: self::detectContent needs to be called before. |
| 152 | * |
| 153 | */ |
| 154 | public function wasDetected(string $detectionClassId) : bool |
| 155 | { |
| 156 | foreach ($this->detectedContent as $type => $detectedClassIds) { |
| 157 | if (array_key_exists($detectionClassId, $detectedClassIds)) { |
| 158 | return $detectedClassIds[$detectionClassId] ?? \false; |
| 159 | } |
| 160 | } |
| 161 | return \false; |
| 162 | } |
| 163 | /** |
| 164 | * Returns an array containing ids of all detected detections of the given type |
| 165 | * |
| 166 | * @param int $type One of the SiteContentDetectionAbstract::TYPE_* constants |
| 167 | * @return array |
| 168 | */ |
| 169 | public function getDetectsByType(int $type) : array |
| 170 | { |
| 171 | $detected = []; |
| 172 | foreach ($this->detectedContent[$type] as $objId => $wasDetected) { |
| 173 | if (\true === $wasDetected) { |
| 174 | $detected[] = $objId; |
| 175 | } |
| 176 | } |
| 177 | return $detected; |
| 178 | } |
| 179 | /** |
| 180 | * Checks that all required detections are in the cache array |
| 181 | * |
| 182 | * @param array $detectContent |
| 183 | * @param array $cache |
| 184 | * |
| 185 | */ |
| 186 | private function checkCacheHasRequiredProperties(array $detectContent, array $cache) : bool |
| 187 | { |
| 188 | if (empty($detectContent)) { |
| 189 | foreach (self::getSiteContentDetectionsByType() as $type => $entries) { |
| 190 | foreach ($entries as $entry) { |
| 191 | if (!isset($cache['detectedContent'][$type][$entry::getId()])) { |
| 192 | return \false; |
| 193 | // random detection missing |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | return \true; |
| 198 | } |
| 199 | foreach ($detectContent as $requestedDetection) { |
| 200 | if (is_string($requestedDetection)) { |
| 201 | // specific detection |
| 202 | $detectionObj = $this->getSiteContentDetectionById($requestedDetection); |
| 203 | if (null !== $detectionObj && !isset($cache['detectedContent'][$detectionObj::getContentType()][$detectionObj::getId()])) { |
| 204 | return \false; |
| 205 | // specific detection was run before |
| 206 | } |
| 207 | } elseif (is_int($requestedDetection)) { |
| 208 | // detection type requested |
| 209 | $detectionsByType = self::getSiteContentDetectionsByType(); |
| 210 | if (isset($detectionsByType[$requestedDetection])) { |
| 211 | foreach ($detectionsByType[$requestedDetection] as $detectionObj) { |
| 212 | if (!isset($cache['detectedContent'][$requestedDetection][$detectionObj::getId()])) { |
| 213 | return \false; |
| 214 | // random detection missing |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | return \true; |
| 221 | } |
| 222 | /** |
| 223 | * Save data to the cache |
| 224 | * |
| 225 | * |
| 226 | */ |
| 227 | private function saveToCache(string $cacheKey, int $cacheLife) : void |
| 228 | { |
| 229 | $cacheData = ['detectedContent' => [], 'connectedConsentManagers' => []]; |
| 230 | // Load any existing cached values |
| 231 | $siteContentDetectionCache = $this->cache->fetch($cacheKey); |
| 232 | if (is_array($siteContentDetectionCache)) { |
| 233 | $cacheData = $siteContentDetectionCache; |
| 234 | } |
| 235 | foreach ($this->detectedContent as $type => $detections) { |
| 236 | if (!isset($cacheData['detectedContent'][$type])) { |
| 237 | $cacheData['detectedContent'][$type] = []; |
| 238 | } |
| 239 | foreach ($detections as $detectionId => $wasDetected) { |
| 240 | if (null !== $wasDetected) { |
| 241 | $cacheData['detectedContent'][$type][$detectionId] = $wasDetected; |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 | $cacheData['connectedConsentManagers'] = array_merge($cacheData['connectedConsentManagers'], $this->connectedConsentManagers); |
| 246 | $this->cache->save($cacheKey, $cacheData, $cacheLife); |
| 247 | } |
| 248 | /** |
| 249 | * Run various detection checks for site content |
| 250 | * |
| 251 | * @param array $detectContent Array of detection types used to filter the checks that are run |
| 252 | * |
| 253 | */ |
| 254 | private function detectionChecks(array $detectContent) : void |
| 255 | { |
| 256 | $detections = $this->getSiteContentDetectionsByType(); |
| 257 | foreach ($detections as $type => $typeDetections) { |
| 258 | foreach ($typeDetections as $typeDetection) { |
| 259 | $this->detectedContent[$type][$typeDetection::getId()] = null; |
| 260 | if (in_array($type, $detectContent) || in_array($typeDetection::getId(), $detectContent) || empty($detectContent)) { |
| 261 | $this->detectedContent[$type][$typeDetection::getId()] = \false; |
| 262 | if ($typeDetection->isDetected($this->siteResponse['data'], $this->siteResponse['headers'])) { |
| 263 | if ($typeDetection instanceof ConsentManagerDetectionAbstract && $typeDetection->checkIsConnected($this->siteResponse['data'], $this->siteResponse['headers'])) { |
| 264 | $this->connectedConsentManagers[] = $typeDetection::getId(); |
| 265 | } |
| 266 | $this->detectedContent[$type][$typeDetection::getId()] = \true; |
| 267 | } |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | /** |
| 273 | * Retrieve data from the specified site using an HTTP request |
| 274 | * |
| 275 | * |
| 276 | * @return array |
| 277 | */ |
| 278 | private function requestSiteResponse(string $url, int $timeOut) : array |
| 279 | { |
| 280 | if (!$url) { |
| 281 | return []; |
| 282 | } |
| 283 | // If internet features are disabled, we don't try to fetch any site content |
| 284 | if (0 === (int) GeneralConfig::getConfigValue('enable_internet_features')) { |
| 285 | return []; |
| 286 | } |
| 287 | $siteData = []; |
| 288 | try { |
| 289 | $siteData = \Piwik\Http::sendHttpRequestBy(\Piwik\Http::getTransportMethod(), $url, $timeOut, null, null, null, 0, \false, \true, \false, \true); |
| 290 | } catch (\Exception $e) { |
| 291 | } |
| 292 | return $siteData; |
| 293 | } |
| 294 | /** |
| 295 | * Return an array of consent manager definitions which can be used to detect their presence on the site and show |
| 296 | * the associated guide links |
| 297 | * |
| 298 | * Note: This list is also used to display the known / supported consent managers on the "Ask for Consent" page |
| 299 | * For adding a new consent manager to this page, it needs to be added here. If a consent manager can't be detected |
| 300 | * automatically, simply leave the detections empty. |
| 301 | * |
| 302 | * @return array[] |
| 303 | */ |
| 304 | public static function getKnownConsentManagers() : array |
| 305 | { |
| 306 | $detections = self::getSiteContentDetectionsByType(); |
| 307 | $cmDetections = $detections[SiteContentDetectionAbstract::TYPE_CONSENT_MANAGER]; |
| 308 | $consentManagers = []; |
| 309 | foreach ($cmDetections as $detection) { |
| 310 | $consentManagers[$detection::getId()] = ['name' => $detection::getName(), 'instructionUrl' => $detection::getInstructionUrl()]; |
| 311 | } |
| 312 | return $consentManagers; |
| 313 | } |
| 314 | } |
| 315 |