tag is added to the webpage with a Matomo URL as the source, this URL serves the opt-out * content and sets the opt-out cookie for the Matomo URL domain. Translation and styling is done * server side, a third party cookie is set. Not well supported with modern browser third party cookie * restrictions, no longer offered as an option in the UI but the content URL is supported for existing * webpages still using the iFrame code. * * JavaScript : an empty
tag is added to the webpage along with a '; } /** * Return the HTML code to be added to pages for the self-contained opt-out * * @param string $backgroundColor * @param string $fontColor * @param string $fontSize * @param string $fontFamily * @param bool $applyStyling * @param bool $showIntro * * @return string */ public function getOptOutSelfContainedEmbedCode(string $backgroundColor, string $fontColor, string $fontSize, string $fontFamily, bool $applyStyling, bool $showIntro): string { $cookiePath = Common::getRequestVar('cookiePath', '', 'string'); $cookieDomain = Common::getRequestVar('cookieDomain', '', 'string'); $settings = [ 'showIntro' => $showIntro, 'divId' => 'matomo-opt-out', 'useSecureCookies' => true, 'cookiePath' => ($cookiePath !== '' ? $cookiePath : null), 'cookieDomain' => ($cookieDomain !== '' ? $cookieDomain : null), 'cookieSameSite' => Common::getRequestVar('cookieSameSite', 'Lax', 'string'), ]; // Self contained code translations are static and always use the language of the user who generated the embed code $settings = array_merge($settings, $this->getTranslations()); $settingsString = 'var settings = '.json_encode($settings).';'; $styleSheet = $this->optOutStyling($fontSize, $fontColor, $fontFamily, $backgroundColor, true); $code = <<
HTML; return str_replace('window.MatomoConsent = { };', $this->getOptOutCommonJS(), str_replace('style=""', ($applyStyling ? 'style="'.$styleSheet.'"' : ''), str_replace("var settings = {};", $settingsString, $code))); } /** * Generate and return JavaScript to show the opt-out option * * All optOutJS URL params: * backgroundColor * fontColor * fontSize * fontFamily * language (default "auto") Language code for the translations or "auto" to use the browser language * showIntro (default 1) Should the opt-out intro text be shown? * divId (default "matomo-opt-out") The id of the div which will contain the opt-out form * useCookiesIfNoTracker (default 1) Should consent cookies be read/written directly if the tracker can't be found? * useCookiesTimeout (default 10) How long to wait for the tracker to be detected? * useSecureCookies (default 1) Set secure cookies? * cookiePath (default blank) Use this path for consent cookies * cookieDomain (default blank) Use this domain for consent cookies * * @return string */ public function getOptOutJS() : string { $language = Common::getRequestVar('language', 'auto', 'string'); $showIntro = Common::getRequestVar('showIntro', 1, 'int'); $divId = Common::getRequestVar('divId', 'matomo-opt-out', 'string'); $useCookiesIfNoTracker = Common::getRequestVar('useCookiesIfNoTracker', 1, 'int'); $useCookiesTimeout = Common::getRequestVar('useCookiesTimeout', 10, 'int'); $useSecureCookies = Common::getRequestVar('useSecureCookies', 1, 'int'); $cookiePath = Common::getRequestVar('cookiePath', '', 'string'); $cookieDomain = Common::getRequestVar('cookieDomain', '', 'string'); // If the language parameter is 'auto' then use the browser language if ($language === 'auto') { $language = Common::extractLanguageCodeFromBrowserLanguage(Common::getBrowserLanguage(), APILanguagesManager::getInstance()->getAvailableLanguages()); } $settings = [ 'showIntro' => $showIntro, 'divId' => $divId, 'useSecureCookies' => $useSecureCookies, 'cookiePath' => ($cookiePath !== '' ? $cookiePath : null), 'cookieDomain' => ($cookieDomain !== '' ? $cookieDomain : null), 'useCookiesIfNoTracker' => $useCookiesIfNoTracker, 'useCookiesTimeout' => $useCookiesTimeout, ]; // Self contained code translations are static and always use the language of the user who generated the embed code $translations = $this->getTranslations($language); $translations['OptOutErrorNoTracker'] = Piwik::translate('CoreAdminHome_OptOutErrorNoTracker', [], $language); $settings = array_merge($settings, $translations); $settingsString = 'var settings = '.json_encode($settings).';'; $styleSheet = $this->optOutStyling(null, null, null, null, true); /** @lang JavaScript */ $code = <<getOptOutCommonJS(), str_replace('stylecss', $styleSheet, str_replace("var settings = {};", $settingsString, $code))); } /** * Return the shared opt-out JavaScript (used by self-contained and tracker versions) * * @return string */ private function getOptOutCommonJS() : string { /** @lang JavaScript */ return <<'; document.body.insertBefore(warningDiv, document.body.firstChild); console.log(msg); return; } if (!navigator || !navigator.cookieEnabled) { div.innerHTML = errorBlock+settings.OptOutErrorNoCookies+'

'; return; } if (location.protocol !== 'https:') { div.innerHTML = errorBlock+settings.OptOutErrorNotHttps+'

'; return; } if (errorMessage !== null) { div.innerHTML = errorBlock+errorMessage+'

'; return; } var content = ''; if (consent) { if (settings.showIntro) { content += '

'+settings.YouMayOptOut2+' '+settings.YouMayOptOut3+'

'; } if (useTracker) { content += ''; } else { content += ''; } content += ''; } else { if (settings.showIntro) { content += '

'+settings.OptOutComplete+' '+settings.OptOutCompleteBis+'

'; } if (useTracker) { content += ''; } else { content += ''; } content += ''; } div.innerHTML = content; }; window.MatomoConsent = { cookiesDisabled: (!navigator || !navigator.cookieEnabled), CONSENT_COOKIE_NAME: 'mtm_consent', CONSENT_REMOVED_COOKIE_NAME: 'mtm_consent_removed', cookieIsSecure: false, useSecureCookies: true, cookiePath: '', cookieDomain: '', cookieSameSite: 'Lax', init: function(useSecureCookies, cookiePath, cookieDomain, cookieSameSite) { this.useSecureCookies = useSecureCookies; this.cookiePath = cookiePath; this.cookieDomain = cookieDomain; this.cookieSameSite = cookieSameSite; if(useSecureCookies && location.protocol !== 'https:') { console.log('Error with setting useSecureCookies: You cannot use this option on http.'); } else { this.cookieIsSecure = useSecureCookies; } }, hasConsent: function() { var value = this.getCookie(this.CONSENT_COOKIE_NAME); if (this.getCookie(this.CONSENT_REMOVED_COOKIE_NAME) && value) { this.setCookie(this.CONSENT_COOKIE_NAME, '', -129600000); return false; } return (value || value !== 0); }, consentGiven: function() { this.setCookie(this.CONSENT_REMOVED_COOKIE_NAME, '', -129600000); this.setCookie(this.CONSENT_COOKIE_NAME, new Date().getTime(), 946080000000); }, consentRevoked: function() { this.setCookie(this.CONSENT_COOKIE_NAME, '', -129600000); this.setCookie(this.CONSENT_REMOVED_COOKIE_NAME, new Date().getTime(), 946080000000); }, getCookie: function(cookieName) { var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)'), cookieMatch = cookiePattern.exec(document.cookie); return cookieMatch ? window.decodeURIComponent(cookieMatch[2]) : 0; }, setCookie: function(cookieName, value, msToExpire) { var expiryDate = new Date(); expiryDate.setTime((new Date().getTime()) + msToExpire); document.cookie = cookieName + '=' + window.encodeURIComponent(value) + (msToExpire ? ';expires=' + expiryDate.toGMTString() : '') + ';path=' + (this.cookiePath || '/') + (this.cookieDomain ? ';domain=' + this.cookieDomain : '') + (this.cookieIsSecure ? ';secure' : '') + ';SameSite=' + this.cookieSameSite; if ((!msToExpire || msToExpire >= 0) && this.getCookie(cookieName) !== String(value)) { console.log('There was an error setting cookie `' + cookieName + '`. Please check domain and path.'); } } }; JS; } /** * Get translations used by the opt-out popup * * @param string|null $language * * @return array */ private function getTranslations(string $language = null) : array { return [ 'OptOutComplete' => Piwik::translate('CoreAdminHome_OptOutComplete', [], $language), 'OptOutCompleteBis' => Piwik::translate('CoreAdminHome_OptOutCompleteBis', [], $language), 'YouMayOptOut2' => Piwik::translate('CoreAdminHome_YouMayOptOut2', [], $language), 'YouMayOptOut3' => Piwik::translate('CoreAdminHome_YouMayOptOut3', [], $language), 'OptOutErrorNoCookies' => Piwik::translate('CoreAdminHome_OptOutErrorNoCookies', [], $language), 'OptOutErrorNotHttps' => Piwik::translate('CoreAdminHome_OptOutErrorNotHttps', [], $language), 'YouAreNotOptedOut' => Piwik::translate('CoreAdminHome_YouAreNotOptedOut', [], $language), 'UncheckToOptOut' => Piwik::translate('CoreAdminHome_UncheckToOptOut', [], $language), 'YouAreOptedOut' => Piwik::translate('CoreAdminHome_YouAreOptedOut', [], $language), 'CheckToOptIn' => Piwik::translate('CoreAdminHome_CheckToOptIn', [], $language), ]; } /** * Return the content of the iFrame opt out * * @return View * @throws \Exception */ public function getOptOutViewIFrame() { if ($this->view) { return $this->view; } $trackVisits = !IgnoreCookie::isIgnoreCookieFound(); $dntFound = $this->getDoNotTrackHeaderChecker()->isDoNotTrackFound(); $setCookieInNewWindow = Common::getRequestVar('setCookieInNewWindow', false, 'int'); if ($setCookieInNewWindow) { $nonce = Common::getRequestVar('nonce', false); if ($nonce !== false && !Nonce::verifyNonce('Piwik_OptOut', $nonce)) { Nonce::discardNonce('Piwik_OptOut'); $nonce = ''; } $reloadUrl = Url::getCurrentQueryStringWithParametersModified(array( 'showConfirmOnly' => 1, 'setCookieInNewWindow' => 0, 'nonce' => $nonce ? : '' )); } else { $reloadUrl = false; $requestNonce = Common::getRequestVar('nonce', false); if ($requestNonce !== false && Nonce::verifyNonce('Piwik_OptOut', $requestNonce)) { Nonce::discardNonce('Piwik_OptOut'); IgnoreCookie::setIgnoreCookie(); $trackVisits = !$trackVisits; } } $language = Common::getRequestVar('language', ''); $lang = APILanguagesManager::getInstance()->isLanguageAvailable($language) ? $language : LanguagesManager::getLanguageCodeForCurrentUser(); $nonce = Nonce::getNonce('Piwik_OptOut', 3600); $this->addQueryParameters(array( 'module' => 'CoreAdminHome', 'action' => 'optOut', 'language' => $lang, 'setCookieInNewWindow' => 1, 'nonce' => $nonce ), false); if (Common::getRequestVar('applyStyling', 1, 'int')) { $this->addStylesheet($this->optOutStyling()); } $this->view = new View("@CoreAdminHome/optOut"); $this->addJavaScript('plugins/CoreAdminHome/javascripts/optOut.js', false); $this->view->setXFrameOptions('allow'); $this->view->dntFound = $dntFound; $this->view->trackVisits = $trackVisits; $this->view->nonce = $nonce; $this->view->language = $lang; $this->view->showIntro = Common::getRequestVar('showIntro', 1, 'int'); $this->view->showConfirmOnly = Common::getRequestVar('showConfirmOnly', false, 'int'); $this->view->reloadUrl = $reloadUrl; $this->view->javascripts = $this->getJavaScripts(); $this->view->stylesheets = $this->getStylesheets(); $this->view->title = $this->getTitle(); $this->view->queryParameters = $this->getQueryParameters(); return $this->view; } /** * Provide a CSS style sheet based on the chosen opt out style options * * @param string|null $fontSize * @param string|null $fontColor * @param string|null $fontFamily * @param string|null $backgroundColor * @param bool $noBody * * @return string * @throws \Exception */ private function optOutStyling(?string $fontSize = null, ?string $fontColor = null, ?string $fontFamily = null, ?string $backgroundColor = null, bool $noBody = false): string { $cssfontsize = ($fontSize ? : Common::unsanitizeInputValue(Common::getRequestVar('fontSize', false, 'string'))); $cssfontcolour = ($fontColor ? : Common::unsanitizeInputValue(Common::getRequestVar('fontColor', false, 'string'))); $cssfontfamily = ($fontFamily ? : Common::unsanitizeInputValue(Common::getRequestVar('fontFamily', false, 'string'))); $cssbackgroundcolor = ($backgroundColor ? : Common::unsanitizeInputValue(Common::getRequestVar('backgroundColor', false, 'string'))); if (!$noBody) { $cssbody = 'body { '; } else { $cssbody = ''; } $hexstrings = array( 'fontColor' => $cssfontcolour, 'backgroundColor' => $cssbackgroundcolor ); foreach ($hexstrings as $key => $testcase) { if ($testcase && !(ctype_xdigit($testcase) && in_array(strlen($testcase),array(3,6), true))) { throw new \Exception("The URL parameter $key value of '$testcase' is not valid. Expected value is for example 'ffffff' or 'fff'.\n"); } } /** @noinspection RegExpRedundantEscape */ if ($cssfontsize && (preg_match("/^[0-9]+[\.]?[0-9]*(px|pt|em|rem|%)$/", $cssfontsize))) { $cssbody .= 'font-size: ' . $cssfontsize . '; '; } else if ($cssfontsize) { throw new \Exception("The URL parameter fontSize value of '$cssfontsize' is not valid. Expected value is for example '15pt', '1.2em' or '13px'.\n"); } /** @noinspection RegExpRedundantEscape */ if ($cssfontfamily && (preg_match('/^[a-zA-Z0-9-\ ,\'"]+$/', $cssfontfamily))) { $cssbody .= 'font-family: ' . $cssfontfamily . '; '; } else if ($cssfontfamily) { throw new \Exception("The URL parameter fontFamily value of '$cssfontfamily' is not valid. Expected value is for example 'sans-serif' or 'Monaco, monospace'.\n"); } if ($cssfontcolour) { $cssbody .= 'color: #' . $cssfontcolour . '; '; } if ($cssbackgroundcolor) { $cssbody .= 'background-color: #' . $cssbackgroundcolor . '; '; } if (!$noBody) { $cssbody .= '}'; } return $cssbody; } /** * @return DoNotTrackHeaderChecker */ protected function getDoNotTrackHeaderChecker() { return $this->doNotTrackHeaderChecker; } }