From be9d00565d464e72ac04bafd68aa2d0d001ee571 Mon Sep 17 00:00:00 2001 From: "Max W." Date: Wed, 9 Apr 2025 01:25:08 +0200 Subject: [PATCH] Advanced third party cookie manager --- classes/GdprScriptDetector.php | 214 ++++++++++++++++++++++++ gdprcookieconsent.php | 133 ++++++++++++++- views/js/gdpr_cookie.js | 280 ++++++++++++++++++++++++++++---- views/js/gdpr_cookie_helper.js | 211 ++++++++++++++++++++++++ views/templates/hook/footer.tpl | 12 +- 5 files changed, 811 insertions(+), 39 deletions(-) create mode 100644 classes/GdprScriptDetector.php create mode 100644 views/js/gdpr_cookie_helper.js diff --git a/classes/GdprScriptDetector.php b/classes/GdprScriptDetector.php new file mode 100644 index 0000000..4ad3036 --- /dev/null +++ b/classes/GdprScriptDetector.php @@ -0,0 +1,214 @@ + [ + '/google-analytics\.com/i', + '/googletagmanager\.com/i', + '/gtag/i', + '/analytics/i', + '/matomo/i', + '/piwik/i', + '/statcounter/i', + '/hotjar/i', + '/clarity\.ms/i', + '/stats/i' + ], + 'marketing' => [ + '/facebook\.net/i', + '/connect\.facebook\.com/i', + '/fbevents\.js/i', + '/doubleclick\.net/i', + '/googlesyndication\.com/i', + '/google.*ads/i', + '/twitter\.com\/widgets/i', + '/platform\.twitter\.com/i', + '/pixel/i', + '/ads/i', + '/adservice/i', + '/criteo/i', + '/pinterest/i', + '/taboola/i', + '/outbrain/i' + ], + 'functional' => [ + '/recaptcha/i', + '/hcaptcha/i', + '/fonts\.googleapis\.com/i', + '/cloudflare/i', + '/unpkg\.com/i', + '/cdn/i', + '/chat/i', + '/live.*/i', + '/support/i', + '/feedback/i', + '/maps\.google/i', + '/disqus/i', + '/zendesk/i', + '/intercom/i' + ] + ]; + + /** + * Check if a script URL or content matches any pattern + * + * @param string $scriptText The script URL or content to check + * @return string The cookie category ('necessary', 'functional', 'analytics', 'marketing') + */ + public static function detectCategory($scriptText) + { + foreach (self::$scriptPatterns as $category => $patterns) { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $scriptText)) { + return $category; + } + } + } + + return 'necessary'; // Default category if no match found + } + + /** + * Get common third-party domains and their categories + * + * @return array Associative array of domains and their categories + */ + public static function getCommonThirdPartyDomains() + { + return [ + // Analytics + 'google-analytics.com' => 'analytics', + 'googletagmanager.com' => 'analytics', + 'analytics.google.com' => 'analytics', + 'matomo.org' => 'analytics', + 'matomo.cloud' => 'analytics', + 'piwik.pro' => 'analytics', + 'statcounter.com' => 'analytics', + 'hotjar.com' => 'analytics', + 'clarity.ms' => 'analytics', + 'mixpanel.com' => 'analytics', + + // Marketing + 'facebook.com' => 'marketing', + 'facebook.net' => 'marketing', + 'fbcdn.net' => 'marketing', + 'doubleclick.net' => 'marketing', + 'googlesyndication.com' => 'marketing', + 'adservice.google.com' => 'marketing', + 'twitter.com' => 'marketing', + 'linkedin.com' => 'marketing', + 'pinterest.com' => 'marketing', + 'ads-twitter.com' => 'marketing', + 'criteo.com' => 'marketing', + 'criteo.net' => 'marketing', + 'taboola.com' => 'marketing', + 'outbrain.com' => 'marketing', + + // Functional + 'google.com/recaptcha' => 'functional', + 'gstatic.com/recaptcha' => 'functional', + 'hcaptcha.com' => 'functional', + 'fonts.googleapis.com' => 'functional', + 'cloudflare.com' => 'functional', + 'cdn.jsdelivr.net' => 'functional', + 'unpkg.com' => 'functional', + 'cdnjs.cloudflare.com' => 'functional', + 'maps.google.com' => 'functional', + 'maps.googleapis.com' => 'functional', + 'disqus.com' => 'functional', + 'zendesk.com' => 'functional', + 'intercom.io' => 'functional' + ]; + } + + /** + * Check if a domain matches any known third-party domain + * + * @param string $domain The domain to check + * @return string|false The cookie category or false if no match + */ + public static function matchDomain($domain) + { + $thirdPartyDomains = self::getCommonThirdPartyDomains(); + + foreach ($thirdPartyDomains as $thirdPartyDomain => $category) { + if (strpos($domain, $thirdPartyDomain) !== false) { + return $category; + } + } + + return false; + } + + /** + * Detect inline script content category + * + * @param string $content The script content to analyze + * @return string The cookie category + */ + public static function detectInlineScriptCategory($content) + { + // Common script signatures in inline scripts + $inlinePatterns = [ + 'analytics' => [ + '/googleAnalytics/i', + '/gtag\s*\(/i', + '/_gaq\s*\./i', + '/ga\s*\(\s*[\'"]create/i', + '/analytics/i', + '/matomo/i', + '/piwik/i', + '/hotjar/i', + '/clarity/i' + ], + 'marketing' => [ + '/fbq\s*\(/i', + '/FB\.init/i', + '/facebook-jssdk/i', + '/twttr\s*\./i', + '/twitter-widgets/i', + '/pintrk/i', + '/adsbygoogle/i', + '/googletag/i', + '/pixel/i', + '/track/i' + ], + 'functional' => [ + '/grecaptcha/i', + '/hcaptcha/i', + '/maps\.googleapis/i', + '/gapi\.load/i', + '/disqus/i', + '/LiveChat/i', + '/intercom/i', + '/zendesk/i' + ] + ]; + + foreach ($inlinePatterns as $category => $patterns) { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + return $category; + } + } + } + + return 'necessary'; + } +} diff --git a/gdprcookieconsent.php b/gdprcookieconsent.php index c14ed38..e314452 100644 --- a/gdprcookieconsent.php +++ b/gdprcookieconsent.php @@ -18,7 +18,7 @@ class GdprCookieConsent extends Module { $this->name = 'gdprcookieconsent'; $this->tab = 'front_office_features'; - $this->version = '1.0.0'; + $this->version = '1.0.1'; $this->author = 'Walzen665'; $this->need_instance = 0; $this->ps_versions_compliancy = [ @@ -53,7 +53,11 @@ class GdprCookieConsent extends Module Configuration::updateValue('GDPR_COOKIE_RETENTION_PERIOD', '365 days') && Configuration::updateValue('GDPR_COOKIE_THIRD_PARTIES', 'Google Analytics, Facebook, etc.') && Configuration::updateValue('GDPR_COOKIE_MANAGE_TEXT', 'Manage Cookie Preferences') && - Configuration::updateValue('GDPR_COOKIE_ONLY_REQUIRED', 1); + Configuration::updateValue('GDPR_COOKIE_ONLY_REQUIRED', 1) && + Configuration::updateValue('GDPR_COOKIE_NECESSARY_DESC', 'Necessary cookies help make a website usable by enabling basic functions like page navigation and access to secure areas. The website cannot function properly without these cookies.') && + Configuration::updateValue('GDPR_COOKIE_FUNCTIONAL_DESC', 'Functional cookies enable a website to remember information that changes the way the website behaves or looks, like your preferred language or the region you are in.') && + Configuration::updateValue('GDPR_COOKIE_ANALYTICS_DESC', 'Analytics cookies help website owners understand how visitors interact with websites by collecting and reporting information anonymously.') && + Configuration::updateValue('GDPR_COOKIE_MARKETING_DESC', 'Marketing cookies are used to track visitors across websites. The intention is to display ads that are relevant and engaging for the individual user.'); } @@ -74,7 +78,11 @@ class GdprCookieConsent extends Module Configuration::deleteByName('GDPR_COOKIE_RETENTION_PERIOD') && Configuration::deleteByName('GDPR_COOKIE_THIRD_PARTIES') && Configuration::deleteByName('GDPR_COOKIE_MANAGE_TEXT') && - Configuration::deleteByName('GDPR_COOKIE_ONLY_REQUIRED'); + Configuration::deleteByName('GDPR_COOKIE_ONLY_REQUIRED') && + Configuration::deleteByName('GDPR_COOKIE_NECESSARY_DESC') && + Configuration::deleteByName('GDPR_COOKIE_FUNCTIONAL_DESC') && + Configuration::deleteByName('GDPR_COOKIE_ANALYTICS_DESC') && + Configuration::deleteByName('GDPR_COOKIE_MARKETING_DESC'); } /** @@ -99,6 +107,10 @@ class GdprCookieConsent extends Module $thirdParties = Tools::getValue('GDPR_COOKIE_THIRD_PARTIES'); $manageText = Tools::getValue('GDPR_COOKIE_MANAGE_TEXT'); $onlyRequired = Tools::getValue('GDPR_COOKIE_ONLY_REQUIRED'); + $necessaryDesc = Tools::getValue('GDPR_COOKIE_NECESSARY_DESC'); + $functionalDesc = Tools::getValue('GDPR_COOKIE_FUNCTIONAL_DESC'); + $analyticsDesc = Tools::getValue('GDPR_COOKIE_ANALYTICS_DESC'); + $marketingDesc = Tools::getValue('GDPR_COOKIE_MARKETING_DESC'); // Update configuration values Configuration::updateValue('GDPR_COOKIE_ENABLED', $enabled); @@ -113,6 +125,10 @@ class GdprCookieConsent extends Module Configuration::updateValue('GDPR_COOKIE_THIRD_PARTIES', $thirdParties); Configuration::updateValue('GDPR_COOKIE_MANAGE_TEXT', $manageText); Configuration::updateValue('GDPR_COOKIE_ONLY_REQUIRED', $onlyRequired); + Configuration::updateValue('GDPR_COOKIE_NECESSARY_DESC', $necessaryDesc); + Configuration::updateValue('GDPR_COOKIE_FUNCTIONAL_DESC', $functionalDesc); + Configuration::updateValue('GDPR_COOKIE_ANALYTICS_DESC', $analyticsDesc); + Configuration::updateValue('GDPR_COOKIE_MARKETING_DESC', $marketingDesc); // Display confirmation $output .= $this->displayConfirmation($this->l('Settings updated')); @@ -238,6 +254,34 @@ class GdprCookieConsent extends Module ] ], ], + [ + 'type' => 'textarea', + 'label' => $this->l('Necessary Cookies Description'), + 'name' => 'GDPR_COOKIE_NECESSARY_DESC', + 'desc' => $this->l('Description for necessary cookies shown in the modal'), + 'required' => true, + ], + [ + 'type' => 'textarea', + 'label' => $this->l('Functional Cookies Description'), + 'name' => 'GDPR_COOKIE_FUNCTIONAL_DESC', + 'desc' => $this->l('Description for functional cookies shown in the modal'), + 'required' => true, + ], + [ + 'type' => 'textarea', + 'label' => $this->l('Analytics Cookies Description'), + 'name' => 'GDPR_COOKIE_ANALYTICS_DESC', + 'desc' => $this->l('Description for analytics cookies shown in the modal'), + 'required' => true, + ], + [ + 'type' => 'textarea', + 'label' => $this->l('Marketing Cookies Description'), + 'name' => 'GDPR_COOKIE_MARKETING_DESC', + 'desc' => $this->l('Description for marketing cookies shown in the modal'), + 'required' => true, + ], ], 'submit' => [ 'title' => $this->l('Save'), @@ -277,6 +321,10 @@ class GdprCookieConsent extends Module $helper->fields_value['GDPR_COOKIE_THIRD_PARTIES'] = Configuration::get('GDPR_COOKIE_THIRD_PARTIES'); $helper->fields_value['GDPR_COOKIE_MANAGE_TEXT'] = Configuration::get('GDPR_COOKIE_MANAGE_TEXT'); $helper->fields_value['GDPR_COOKIE_ONLY_REQUIRED'] = Configuration::get('GDPR_COOKIE_ONLY_REQUIRED'); + $helper->fields_value['GDPR_COOKIE_NECESSARY_DESC'] = Configuration::get('GDPR_COOKIE_NECESSARY_DESC'); + $helper->fields_value['GDPR_COOKIE_FUNCTIONAL_DESC'] = Configuration::get('GDPR_COOKIE_FUNCTIONAL_DESC'); + $helper->fields_value['GDPR_COOKIE_ANALYTICS_DESC'] = Configuration::get('GDPR_COOKIE_ANALYTICS_DESC'); + $helper->fields_value['GDPR_COOKIE_MARKETING_DESC'] = Configuration::get('GDPR_COOKIE_MARKETING_DESC'); return $helper->generateForm([$form]); } @@ -315,7 +363,12 @@ class GdprCookieConsent extends Module $this->context->smarty->assign([ 'gdprCookieManageText' => Configuration::get('GDPR_COOKIE_MANAGE_TEXT', 'Manage Cookies'), ]); - + + // If not in required-only mode, add script for tagging known third-party scripts + if (!Configuration::get('GDPR_COOKIE_ONLY_REQUIRED')) { + $this->tagThirdPartyScripts(); + } + // Return the template content - but don't throw an error if it doesn't exist yet if (file_exists(_PS_MODULE_DIR_ . $this->name . '/views/templates/hook/manage_button.tpl')) { return $this->display(__FILE__, 'views/templates/hook/manage_button.tpl'); @@ -343,8 +396,80 @@ class GdprCookieConsent extends Module 'gdprCookieThirdParties' => Configuration::get('GDPR_COOKIE_THIRD_PARTIES'), 'gdprCookieManageText' => Configuration::get('GDPR_COOKIE_MANAGE_TEXT'), 'gdprCookieOnlyRequired' => Configuration::get('GDPR_COOKIE_ONLY_REQUIRED'), + 'gdprCookieNecessaryDesc' => Configuration::get('GDPR_COOKIE_NECESSARY_DESC'), + 'gdprCookieFunctionalDesc' => Configuration::get('GDPR_COOKIE_FUNCTIONAL_DESC'), + 'gdprCookieAnalyticsDesc' => Configuration::get('GDPR_COOKIE_ANALYTICS_DESC'), + 'gdprCookieMarketingDesc' => Configuration::get('GDPR_COOKIE_MARKETING_DESC'), ]); return $this->display(__FILE__, 'views/templates/hook/footer.tpl'); } + + /** + * Helper function to tag third-party scripts with appropriate consent categories + */ + protected function tagThirdPartyScripts() + { + // Require the script detector class + require_once(dirname(__FILE__).'/classes/GdprScriptDetector.php'); + + // Get PrestaShop's currently registered JS files + $jsFiles = $this->context->controller->js_files; + + // Loop through JS files and add consent attributes based on patterns + foreach ($jsFiles as $key => $jsFile) { + // Use the detector class + $category = GdprScriptDetector::detectCategory($jsFile); + + // Add attribute to the script + $this->context->smarty->assign([ + 'js_' . md5($jsFile) . '_attributes' => 'data-cookieconsent="' . $category . '"' + ]); + } + + // Inject a helper script to tag inline scripts as well + $this->context->controller->registerJavascript( + 'gdpr-cookie-helper', + $this->_path . 'views/js/gdpr_cookie_helper.js', + ['position' => 'head', 'priority' => 1] + ); + } + + /** + * Modifies scripts in the HTML head + */ + public function hookActionHtmlHeadFooter($params) + { + if (!Configuration::get('GDPR_COOKIE_ENABLED') || Configuration::get('GDPR_COOKIE_ONLY_REQUIRED')) { + return; + } + + // Get the current HTML content + $html = $params['html_content']; + + // Modify script tags to add data-cookieconsent attribute + $patterns = [ + // Google Analytics pattern + '/]*)(gtag|googletagmanager|google-analytics)([^>]*)>/' => '', + + // Facebook Pixel pattern + '/]*)(connect\.facebook\.net|fbevents\.js)([^>]*)>/' => '', + + // Generic analytics patterns + '/]*)(analytics|piwik|matomo|stats)([^>]*)>/' => '', + + // Marketing patterns + '/]*)(ads|adsbygoogle|doubleclick|googlesyndication)([^>]*)>/' => '', + + // Functional patterns (more conservative, as these might be necessary) + '/]*)(recaptcha|chat)([^>]*)>/' => '', + ]; + + foreach ($patterns as $pattern => $replacement) { + $html = preg_replace($pattern, $replacement, $html); + } + + // Update the HTML content + $params['html_content'] = $html; + } } diff --git a/views/js/gdpr_cookie.js b/views/js/gdpr_cookie.js index 6f6ee41..7b0aa8a 100644 --- a/views/js/gdpr_cookie.js +++ b/views/js/gdpr_cookie.js @@ -7,6 +7,13 @@ */ document.addEventListener('DOMContentLoaded', function() { + // Store for blocked scripts + const scriptStore = { + functional: [], + analytics: [], + marketing: [] + }; + // Cookie functions function setCookie(name, value, days) { var expires = ''; @@ -39,14 +46,166 @@ document.addEventListener('DOMContentLoaded', function() { // Hide banner and modal hideModal(); document.getElementById('gdpr-cookie-banner').style.display = 'none'; - + // Show manage button showManageButton(); } + // Block scripts based on type + function blockScripts() { + // Find and process script tags with data-cookieconsent attribute + const scripts = document.querySelectorAll('script[data-cookieconsent]'); + + scripts.forEach(script => { + const consentType = script.getAttribute('data-cookieconsent'); + + if (!consentType || consentType === 'necessary') { + // Necessary scripts always run + return; + } + + // Store script information + const scriptData = { + src: script.getAttribute('src'), + content: script.innerHTML, + type: script.getAttribute('type') || 'text/javascript', + async: script.async, + defer: script.defer + }; + + // Add to appropriate store + if (scriptStore[consentType]) { + scriptStore[consentType].push(scriptData); + } + + // Remove the script from DOM + script.parentNode.removeChild(script); + }); + + // Also block iframe embeds with data-cookieconsent attribute + const iframes = document.querySelectorAll('iframe[data-cookieconsent]'); + + iframes.forEach(iframe => { + const consentType = iframe.getAttribute('data-cookieconsent'); + + if (!consentType || consentType === 'necessary') { + // Necessary iframes always load + return; + } + + // Create placeholder + const placeholder = document.createElement('div'); + placeholder.className = 'gdpr-blocked-content-placeholder'; + placeholder.setAttribute('data-cookieconsent', consentType); + placeholder.setAttribute('data-src', iframe.getAttribute('src')); + placeholder.style.width = iframe.width + 'px' || '100%'; + placeholder.style.height = iframe.height + 'px' || '150px'; + placeholder.style.border = '1px dashed #ccc'; + placeholder.style.display = 'flex'; + placeholder.style.alignItems = 'center'; + placeholder.style.justifyContent = 'center'; + placeholder.style.backgroundColor = '#f9f9f9'; + placeholder.style.color = '#666'; + placeholder.innerHTML = `
+

Content blocked due to ${consentType} cookies preferences

+ +
`; + + // Replace iframe with placeholder + iframe.parentNode.replaceChild(placeholder, iframe); + }); + + // Add event listeners to load content buttons + document.querySelectorAll('.gdpr-load-blocked-content').forEach(button => { + button.addEventListener('click', function(e) { + const type = this.getAttribute('data-type'); + const placeholder = this.closest('.gdpr-blocked-content-placeholder'); + + if (placeholder) { + // Get iframe src + const src = placeholder.getAttribute('data-src'); + + // Create iframe + const iframe = document.createElement('iframe'); + iframe.src = src; + iframe.width = placeholder.style.width; + iframe.height = placeholder.style.height; + iframe.frameBorder = '0'; + + // Replace placeholder with iframe + placeholder.parentNode.replaceChild(iframe, placeholder); + + // Update consent for this content type + const preferences = JSON.parse(getCookie('gdpr_cookie_consent') || '{"necessary":true}'); + preferences[type] = true; + setCookie('gdpr_cookie_consent', JSON.stringify(preferences), 365); + + // Update checkboxes in modal if it exists + const checkbox = document.querySelector(`#gdpr-cookie-${type}`); + if (checkbox) { + checkbox.checked = true; + } + } + }); + }); + } + + // Load scripts based on consent + function loadConsentedScripts(preferences) { + // For each script type + Object.keys(scriptStore).forEach(type => { + if (preferences[type]) { + // Load all scripts of this type + scriptStore[type].forEach(scriptData => { + const script = document.createElement('script'); + + if (scriptData.src) { + script.src = scriptData.src; + } + + script.type = scriptData.type; + script.async = scriptData.async; + script.defer = scriptData.defer; + + if (scriptData.content) { + script.innerHTML = scriptData.content; + } + + document.head.appendChild(script); + }); + + // Clear the store for this type + scriptStore[type] = []; + } + }); + + // Also load blocked iframes + document.querySelectorAll('.gdpr-blocked-content-placeholder').forEach(placeholder => { + const type = placeholder.getAttribute('data-cookieconsent'); + + if (preferences[type]) { + // Get iframe src + const src = placeholder.getAttribute('data-src'); + + // Create iframe + const iframe = document.createElement('iframe'); + iframe.src = src; + iframe.width = placeholder.style.width; + iframe.height = placeholder.style.height; + iframe.frameBorder = '0'; + + // Replace placeholder with iframe + placeholder.parentNode.replaceChild(iframe, placeholder); + } + }); + } + // Check if cookie consent is already set var cookieConsent = getCookie('gdpr_cookie_consent'); - + + // Always block non-necessary scripts first + blockScripts(); + if (!cookieConsent) { // Show the cookie banner if consent is not set document.getElementById('gdpr-cookie-banner').style.display = 'block'; @@ -54,7 +213,7 @@ document.addEventListener('DOMContentLoaded', function() { // Apply cookie preferences var consentPreferences = JSON.parse(cookieConsent); applyConsentPreferences(consentPreferences); - + // Show the manage button since consent was already given showManageButton(); } @@ -98,12 +257,12 @@ document.addEventListener('DOMContentLoaded', function() { function showModal() { document.getElementById('gdpr-cookie-banner').style.display = 'none'; document.getElementById('gdpr-cookie-modal').style.display = 'block'; - + // Load saved preferences if they exist var cookieConsent = getCookie('gdpr_cookie_consent'); if (cookieConsent) { var preferences = JSON.parse(cookieConsent); - + // Set checkboxes based on saved preferences document.querySelectorAll('.gdpr-cookie-checkbox').forEach(function(checkbox) { var category = checkbox.getAttribute('data-cookie-category'); @@ -127,7 +286,7 @@ document.addEventListener('DOMContentLoaded', function() { analytics: true, marketing: true }; - + setCookie('gdpr_cookie_consent', JSON.stringify(preferences), 365); applyConsentPreferences(preferences); afterConsent(); // Use the new function for consistent post-consent behavior @@ -140,7 +299,7 @@ document.addEventListener('DOMContentLoaded', function() { analytics: false, marketing: false }; - + setCookie('gdpr_cookie_consent', JSON.stringify(preferences), 365); applyConsentPreferences(preferences); afterConsent(); // Use the new function for consistent post-consent behavior @@ -150,28 +309,28 @@ document.addEventListener('DOMContentLoaded', function() { var preferences = { necessary: true // Necessary cookies are always accepted }; - + // Get selected preferences document.querySelectorAll('.gdpr-cookie-checkbox').forEach(function(checkbox) { var category = checkbox.getAttribute('data-cookie-category'); preferences[category] = checkbox.checked; }); - + setCookie('gdpr_cookie_consent', JSON.stringify(preferences), 365); applyConsentPreferences(preferences); afterConsent(); // Use the new function for consistent post-consent behavior } function applyConsentPreferences(preferences) { - // Example implementation - // You would need to adapt this based on your specific cookie usage - + // Load consented scripts + loadConsentedScripts(preferences); + // Functional cookies if (!preferences.functional) { // Disable functional cookies removeFunctionalCookies(); } - + // Analytics cookies if (!preferences.analytics) { // Disable analytics cookies (like Google Analytics) @@ -180,7 +339,7 @@ document.addEventListener('DOMContentLoaded', function() { // Enable analytics enableAnalytics(); } - + // Marketing cookies if (!preferences.marketing) { // Disable marketing cookies @@ -194,32 +353,43 @@ document.addEventListener('DOMContentLoaded', function() { // Helper functions to implement consent preferences function removeFunctionalCookies() { // This is just an example - implement based on your specific needs - var functionalCookies = ['prefs', 'language', 'theme']; + var functionalCookies = ['prefs', 'language', 'theme', 'user_preferences']; + var domains = [window.location.hostname, '.' + window.location.hostname]; + functionalCookies.forEach(function(cookie) { - document.cookie = cookie + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + domains.forEach(function(domain) { + document.cookie = cookie + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=' + domain; + }); }); } function disableAnalytics() { - // Example: Disable Google Analytics + // Google Analytics window['ga-disable-UA-XXXXXXXX-X'] = true; - + window['ga-disable-G-XXXXXXXX'] = true; + // Remove existing GA cookies - document.cookie = '_ga=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=.' + window.location.hostname; - document.cookie = '_gid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=.' + window.location.hostname; - document.cookie = '_gat=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=.' + window.location.hostname; + var analyticsCookies = ['_ga', '_gid', '_gat', '__utma', '__utmb', '__utmc', '__utmt', '__utmz', '_hjid', '_hjAbsoluteSessionInProgress']; + var domains = [window.location.hostname, '.' + window.location.hostname]; + + analyticsCookies.forEach(function(cookie) { + domains.forEach(function(domain) { + document.cookie = cookie + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=' + domain; + }); + }); } function enableAnalytics() { - // Example: Enable Google Analytics + // Google Analytics window['ga-disable-UA-XXXXXXXX-X'] = false; + window['ga-disable-G-XXXXXXXX'] = false; } function disableMarketing() { - // Example implementation for common marketing cookies - var marketingCookies = ['_fbp', 'fr', 'IDE', 'MUID', 'personalization_id']; - var domains = [window.location.hostname, '.' + window.location.hostname]; - + // Common marketing cookies + var marketingCookies = ['_fbp', 'fr', 'IDE', 'MUID', 'personalization_id', 'VISITOR_INFO1_LIVE', 'YSC', 'NID']; + var domains = [window.location.hostname, '.' + window.location.hostname, '.google.com', '.facebook.com', '.youtube.com']; + marketingCookies.forEach(function(cookie) { domains.forEach(function(domain) { document.cookie = cookie + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Domain=' + domain; @@ -228,8 +398,60 @@ document.addEventListener('DOMContentLoaded', function() { } function enableMarketing() { - // For enabling marketing, we typically don't need to do anything special - // The marketing scripts will set their cookies when they load - // Just ensure the marketing scripts are loaded after checking consent + // For enabling marketing, we load the scripts that were blocked + // This happens in the loadConsentedScripts function } + + // Function to detect which script is associated with each cookie category + function detectThirdPartyScripts() { + // This function would scan the page for known third-party scripts + // and auto-assign data-cookieconsent attributes + const knownScriptPatterns = { + analytics: [ + /google-analytics\.com\/analytics\.js/i, + /googletagmanager\.com\/gtag/i, + /google-analytics\.com\/ga\.js/i, + /hotjar\.com/i, + /analytics/i, + /matomo/i, + /stats/i + ], + marketing: [ + /facebook\.net/i, + /doubleclick\.net/i, + /googlesyndication\.com/i, + /ads/i, + /adservices/i, + /pixel/i, + /track/i + ], + functional: [ + /recaptcha/i, + /fonts\.googleapis\.com/i, + /cloudflare/i, + /cdn/i, + /livechat/i, + /chat/i, + /support/i + ] + }; + + const scripts = document.querySelectorAll('script:not([data-cookieconsent])'); + + scripts.forEach(script => { + if (!script.src) return; // Skip inline scripts + + for (const [category, patterns] of Object.entries(knownScriptPatterns)) { + for (const pattern of patterns) { + if (pattern.test(script.src)) { + script.setAttribute('data-cookieconsent', category); + break; + } + } + } + }); + } + + // Run script detection + detectThirdPartyScripts(); }); \ No newline at end of file diff --git a/views/js/gdpr_cookie_helper.js b/views/js/gdpr_cookie_helper.js new file mode 100644 index 0000000..9d2e58c --- /dev/null +++ b/views/js/gdpr_cookie_helper.js @@ -0,0 +1,211 @@ +/** + * GDPR Cookie Consent Helper for PrestaShop + * + * @author Walzen665 + * @copyright Copyright (c) 2025 + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + */ + +// This script runs early in the page load to tag inline scripts with appropriate consent categories +(function() { + // Known script patterns to identify analytics, marketing and functional scripts + const scriptPatterns = { + analytics: [ + /google[\s\-_]?analytics/i, + /ga\s*\(\s*['"]create/i, + /googletagmanager/i, + /gtag/i, + /\_gaq/i, + /matomo/i, + /piwik/i, + /mixpanel/i, + /hotjar/i, + /clarity/i + ], + marketing: [ + /facebook[\s\-_]?pixel/i, + /fbq\s*\(\s*['"]init/i, + /doubleclick/i, + /adwords/i, + /google[\s\-_]?ad[\s\-_]?services/i, + /google[\s\-_]?tag[\s\-_]?manager/i, + /gtm/i, + /twitter[\s\-_]?pixel/i, + /pinterest[\s\-_]?tag/i + ], + functional: [ + /recaptcha/i, + /chat/i, + /livechat/i, + /support/i, + /feedback/i, + /preference/i, + /usercentrics/i + ] + }; + + // Function to process and tag a script element + function processScript(script) { + // Skip if it already has a consent attribute + if (script.hasAttribute('data-cookieconsent')) { + return; + } + + // Skip our own scripts + if (script.src && script.src.indexOf('gdpr_cookie') !== -1) { + script.setAttribute('data-cookieconsent', 'necessary'); + return; + } + + // Check content for patterns + const content = script.innerHTML || ''; + const src = script.src || ''; + + // Determine script category + let category = 'necessary'; // Default category + + for (const [cat, patterns] of Object.entries(scriptPatterns)) { + for (const pattern of patterns) { + if (pattern.test(content) || pattern.test(src)) { + category = cat; + break; + } + } + + if (category !== 'necessary') { + break; + } + } + + // Tag the script + script.setAttribute('data-cookieconsent', category); + + // For non-necessary scripts, we'll create a duplicate with the correct attribute + // but disabled until consent is given + if (category !== 'necessary') { + const originalScript = script; + const newScript = document.createElement('script'); + + // Copy attributes + Array.from(originalScript.attributes).forEach(attr => { + if (attr.name !== 'data-cookieconsent') { + newScript.setAttribute(attr.name, attr.value); + } + }); + + // Set consent attribute + newScript.setAttribute('data-cookieconsent', category); + + // Copy content if it's an inline script + if (!originalScript.src && originalScript.innerHTML) { + newScript.innerHTML = originalScript.innerHTML; + } + + // Replace the original script + originalScript.parentNode.replaceChild(newScript, originalScript); + + // Prevent execution by removing src and content + newScript.removeAttribute('src'); + newScript.innerHTML = ''; + } + } + + // Process existing scripts + document.querySelectorAll('script').forEach(processScript); + + // Use a MutationObserver to catch dynamically added scripts + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.tagName === 'SCRIPT') { + processScript(node); + } else if (node.querySelectorAll) { + node.querySelectorAll('script').forEach(processScript); + } + }); + }); + }); + + // Start observing the document + observer.observe(document.documentElement, { + childList: true, + subtree: true + }); + + // Also process iframes + function processIframe(iframe) { + // Skip if it already has a consent attribute + if (iframe.hasAttribute('data-cookieconsent')) { + return; + } + + const src = iframe.src || ''; + + // Common third-party iframe sources + const iframePatterns = { + marketing: [ + /youtube/i, + /vimeo/i, + /facebook\.com\/plugins/i, + /twitter\.com\/widgets/i, + /instagram\.com/i, + /pinterest\.com/i, + /doubleclick/i, + /ads/i + ], + analytics: [ + /googletagmanager/i, + /analytics/i + ], + functional: [ + /recaptcha/i, + /maps\.google/i, + /google\.com\/maps/i, + /disqus/i, + /livechat/i, + /chat/i + ] + }; + + // Determine iframe category + let category = 'necessary'; // Default category + + for (const [cat, patterns] of Object.entries(iframePatterns)) { + for (const pattern of patterns) { + if (pattern.test(src)) { + category = cat; + break; + } + } + + if (category !== 'necessary') { + break; + } + } + + // Tag the iframe + iframe.setAttribute('data-cookieconsent', category); + } + + // Process existing iframes + document.querySelectorAll('iframe').forEach(processIframe); + + // Use the observer to catch dynamically added iframes + const iframeObserver = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.tagName === 'IFRAME') { + processIframe(node); + } else if (node.querySelectorAll) { + node.querySelectorAll('iframe').forEach(processIframe); + } + }); + }); + }); + + // Start observing for iframes + iframeObserver.observe(document.documentElement, { + childList: true, + subtree: true + }); +})(); diff --git a/views/templates/hook/footer.tpl b/views/templates/hook/footer.tpl index b838617..dc6d63e 100644 --- a/views/templates/hook/footer.tpl +++ b/views/templates/hook/footer.tpl @@ -13,35 +13,35 @@