'denied', 'ad_user_data' => 'denied', 'ad_personalization' => 'denied', 'analytics_storage' => 'denied', 'functionality_storage' => 'denied', 'personalization_storage' => 'denied', 'security_storage' => 'granted', ]; } private static function get_raw_consent_cookie() { $raw = filter_input(INPUT_COOKIE, self::COOKIE_NAME, FILTER_UNSAFE_RAW); if (null === $raw || false === $raw) { $raw = $_COOKIE[self::COOKIE_NAME] ?? ''; } if (empty($raw)) { $raw = filter_input(INPUT_COOKIE, self::LEGACY_COOKIE_NAME, FILTER_UNSAFE_RAW); if (null === $raw || false === $raw) { $raw = $_COOKIE[self::LEGACY_COOKIE_NAME] ?? ''; } } if (empty($raw)) { return $raw; } return rawurldecode((string)$raw); } private static function parse_consent_cookie() { $raw = self::get_raw_consent_cookie(); if (empty($raw)) { return null; } $candidates = [ (string)$raw, wp_unslash((string)$raw), rawurldecode((string)$raw), urldecode((string)$raw), ]; foreach ($candidates as $candidate) { $value = trim((string)$candidate); if ($value === '') { continue; } $len = strlen($value); if ($len >= 2) { $first = $value[0]; $last = $value[$len - 1]; if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) { $value = substr($value, 1, -1); } } $data = json_decode($value, true); if (is_array($data)) { return $data; } } return null; } private static function has_consent_cookie() { $data = self::parse_consent_cookie(); if (!is_array($data)) { return false; } $keys = array_keys(self::consent_mode_defaults()); foreach ($keys as $key) { if (isset($data[$key]) && in_array($data[$key], ['granted', 'denied'], true)) { return true; } } if (isset($data['analytics']) && is_bool($data['analytics'])) { return true; } if (isset($data['retargeting']) && is_bool($data['retargeting'])) { return true; } if (isset($data['session']) && is_bool($data['session'])) { return true; } return false; } /** * Ratio de contraste WCAG entre deux couleurs hex (#rrggbb). Sert au garde-fou * de parité des boutons et aux avertissements RGAA de l'écran de réglages. */ public static function contrast_ratio($hex1, $hex2) { $l1 = self::relative_luminance($hex1); $l2 = self::relative_luminance($hex2); $light = max($l1, $l2); $dark = min($l1, $l2); return round(($light + 0.05) / ($dark + 0.05), 2); } private static function relative_luminance($hex) { $hex = ltrim((string)$hex, '#'); if (strlen($hex) !== 6) { return 0.0; } $channels = []; foreach ([0, 2, 4] as $i) { $c = hexdec(substr($hex, $i, 2)) / 255; $channels[] = ($c <= 0.03928) ? ($c / 12.92) : pow(($c + 0.055) / 1.055, 2.4); } return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2]; } public static function init() { add_action('admin_menu', [__CLASS__, 'admin_menu']); add_action('admin_init', [__CLASS__, 'register_settings']); add_action('admin_notices', [__CLASS__, 'policy_url_admin_notice']); add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue_admin_assets']); add_filter('plugin_action_links_' . plugin_basename(CMP410GONE_PLUGIN_FILE), [__CLASS__, 'plugin_action_links']); add_filter('plugin_row_meta', [__CLASS__, 'filter_plugin_row_meta'], 10, 2); add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_head_scripts'], 1); add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_assets']); add_action('wp_footer', [__CLASS__, 'render_banner_markup'], 30); add_action('admin_post_cmp410gone_download_gtm', [__CLASS__, 'download_gtm_container']); add_shortcode(self::SHORTCODE, [__CLASS__, 'shortcode_manage_cookies']); } public static function defaults() { return [ 'enable' => 1, 'gtm_id' => '', 'ga4_id' => '', 'consent_wait_for_update_ms' => 500, 'init_datalayer' => 1, 'tracking_mode' => 'hybrid', 'privacy_url' => '', 'cookie_policy_url' => '', 'banner_title' => __('🍪 Paramètres des cookies', '410gone-consent-manager'), 'banner_text' => __('Nous utilisons des cookies essentiels au fonctionnement du site et, si vous l’acceptez, des cookies de mesure d’audience et de reciblage publicitaire. Destinataires : Google Ireland Ltd et Google LLC. Votre choix est conservé 180 jours et modifiable à tout moment.', '410gone-consent-manager'), 'btn_accept' => __('Accepter', '410gone-consent-manager'), 'btn_reject' => __('Refuser', '410gone-consent-manager'), 'btn_reject_all' => __('Tout refuser', '410gone-consent-manager'), 'btn_customize' => __('Personnaliser', '410gone-consent-manager'), 'btn_save' => __('Enregistrer mes choix', '410gone-consent-manager'), 'modal_title' => __('Préférences', '410gone-consent-manager'), // Titres des catégories (visibles côté visiteur, éditables + traduisibles). 'cat_essentials_title' => __('Essentiels', '410gone-consent-manager'), 'cat_analytics_title' => __('Analytics', '410gone-consent-manager'), 'cat_retargeting_title' => __('Reciblage', '410gone-consent-manager'), 'cat_session_title' => __('Enregistrement de session', '410gone-consent-manager'), 'modal_desc_essentials' => __('Nécessaires au fonctionnement du site (toujours actifs).', '410gone-consent-manager'), 'modal_desc_analytics' => __('Mesure d’audience (ex. Google Analytics 4).', '410gone-consent-manager'), 'modal_desc_retargeting' => __('Publicités personnalisées (gérées via GTM).', '410gone-consent-manager'), 'modal_desc_session' => __('Enregistrement de session et cartes de chaleur (ex. Clarity, Hotjar).', '410gone-consent-manager'), // Destinataires nommés + durée de conservation par catégorie (exigence CNIL, brief 1.6). 'cat_essentials_meta' => __('Aucun destinataire tiers · session à 12 mois', '410gone-consent-manager'), 'cat_analytics_meta' => __('Google Analytics 4 — Google Ireland Ltd, Google LLC · 14 mois', '410gone-consent-manager'), 'cat_retargeting_meta' => __('Google Ads — Google Ireland Ltd, Google LLC · 13 mois', '410gone-consent-manager'), 'cat_session_meta' => __('Outil de rejeu de session · durée selon l’outil', '410gone-consent-manager'), // Libellés des liens de politique (visibles côté visiteur). 'privacy_link_label' => __('Politique de confidentialité', '410gone-consent-manager'), 'cookie_link_label' => __('Politique de cookies', '410gone-consent-manager'), // Catégories réellement proposées (brief 1.6.4). Session désactivée par défaut : // aucun outil de rejeu déployé → ne pas demander un consentement sans objet. 'enable_cat_analytics' => 1, 'enable_cat_retargeting' => 1, 'enable_cat_session' => 0, 'background_color' => '#ffffff', 'text_color' => '#0b1621', 'accept_btn_color' => '#00ceff', 'accept_btn_text_color' => '#001018', 'reject_btn_color' => '#0b1621', 'reject_btn_text_color' => '#ffffff', 'customize_btn_color' => '#f5f5f5', 'customize_btn_text_color' => '#111111', // Affichage (brief lot 2 + design 1c). Position: center | footer | bar. 'banner_position' => 'center', // Overlay: blocking (voile bloquant) | veil (voile non bloquant) | none. 'overlay_behavior' => 'blocking', 'overlay_opacity' => 52, 'focus_trap' => 1, // Consent Mode avancé (lot 3). OPT-IN : n'activer qu'avec validation écrite // du client (responsable de traitement sur les pings sans consentement). 'advanced_consent_mode' => 0, 'ttl_days' => 180, 'debug' => 0, 'force_show' => 0, // Déprécié (lot 2.3) : conservé en lecture pour ne pas casser les installs, // remplacé par overlay_behavior. N'est plus utilisé au rendu. 'overlay_mode' => 'inline', ]; } public static function get_settings() { $opts = get_option(self::OPTION_KEY, null); if (!is_array($opts) || empty($opts)) { $legacy = get_option(self::LEGACY_OPTION_KEY, []); $opts = is_array($legacy) ? $legacy : []; } $settings = array_merge(self::defaults(), is_array($opts) ? $opts : []); return self::translate_settings($settings); } public static function plugin_action_links($links) { $settings_url = admin_url('options-general.php?page=' . self::PAGE_SLUG); return array_merge(['settings' => '' . esc_html(__('Settings', '410gone-consent-manager')) . ''], $links); } public static function filter_plugin_row_meta($links, $file) { if ($file !== plugin_basename(CMP410GONE_PLUGIN_FILE)) { return $links; } foreach ($links as $index => $link) { if (strpos($link, 'plugin-install.php') !== false || strpos($link, 'plugin-information') !== false) { unset($links[$index]); } } return $links; } /** * Avertit dans l'admin lorsque les URLs de politique sont absentes, identiques * ou pointent vers la même page (source d'incohérence juridique constatée à * l'audit P3). N'apparaît que sur la page de réglages du plugin. */ public static function policy_url_admin_notice() { if (!current_user_can('manage_options')) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || $screen->id !== 'settings_page_' . self::PAGE_SLUG) { return; } $s = self::get_settings(); $privacy = trim((string)($s['privacy_url'] ?? '')); $cookies = trim((string)($s['cookie_policy_url'] ?? '')); $messages = []; if ($privacy === '') { $messages[] = __('The privacy policy URL is empty. It is required for a compliant consent banner.', '410gone-consent-manager'); } if ($privacy !== '' && $cookies !== '' && untrailingslashit($privacy) === untrailingslashit($cookies)) { $messages[] = __('The privacy policy and cookie policy URLs are identical. Point them to distinct, coherent pages.', '410gone-consent-manager'); } if (empty($messages)) { return; } echo '
410Gone Consent Manager — '; echo esc_html(implode(' ', $messages)); echo '
410gone · options-general.php?page=
' . esc_html__('Colors, banner position, blocking overlay and proposed categories. The live preview is on the right.', '410gone-consent-manager') . '
'; } public static function section_labels() { echo '' . esc_html__('Adjust texts and links. Labels remain compatible with Polylang/WPML and custom filters.', '410gone-consent-manager') . '
'; } public static function section_tracking() { echo '' . esc_html__('GTM can load early, but Consent Mode v2 starts as', '410gone-consent-manager') . ' denied ' . esc_html__('and updates based on the user choice.', '410gone-consent-manager') . '
WP Rocket: ' . esc_html__('exclude', '410gone-consent-manager') . ' cmp.js ' . esc_html__('from “Delay JS execution”.', '410gone-consent-manager') . '
' . esc_html__('Options for debugging, forced tests, and consent retention.', '410gone-consent-manager') . '
'; } private static function render_preview_block($settings, $context) { $style = sprintf( '--cmp410-accept-bg:%1$s;--cmp410-accept-fg:%2$s;--cmp410-reject-bg:%7$s;--cmp410-reject-fg:%8$s;--cmp410-customize-bg:%3$s;--cmp410-customize-fg:%4$s;--cmp410-background:%5$s;--cmp410-foreground:%6$s;', esc_attr($settings['accept_btn_color']), esc_attr($settings['accept_btn_text_color']), esc_attr($settings['customize_btn_color']), esc_attr($settings['customize_btn_text_color']), esc_attr($settings['background_color']), esc_attr($settings['text_color']), esc_attr($settings['reject_btn_color']), esc_attr($settings['reject_btn_text_color']) ); ?>
'NOT_SET'] : ['consentStatus' => 'NEEDED', 'consentType' => ['type' => 'LIST', 'list' => [['type' => 'TEMPLATE', 'value' => 'analytics_storage']]]]; $tags[] = [ 'accountId' => '0', 'containerId' => '0', 'tagId' => '1', 'name' => 'Google tag - GA4', 'type' => 'googtag', 'parameter' => [['type' => 'TEMPLATE', 'key' => 'tagId', 'value' => $ga4]], 'firingTriggerId' => [$all_pages], 'tagFiringOption' => 'ONCE_PER_EVENT', 'monitoringMetadata' => ['type' => 'MAP'], 'consentSettings' => $ga4_consent, ]; // Conversion Linker (All Pages, url_passthrough off). $tags[] = [ 'accountId' => '0', 'containerId' => '0', 'tagId' => '2', 'name' => 'Conversion Linker', 'type' => 'gclidw', 'parameter' => [ ['type' => 'BOOLEAN', 'key' => 'enableCrossDomain', 'value' => 'false'], ['type' => 'BOOLEAN', 'key' => 'enableUrlPassthrough', 'value' => 'false'], ['type' => 'BOOLEAN', 'key' => 'enableCookieOverrides', 'value' => 'false'], ], 'firingTriggerId' => [$all_pages], 'tagFiringOption' => 'ONCE_PER_EVENT', 'monitoringMetadata' => ['type' => 'MAP'], 'consentSettings' => ['consentStatus' => 'NOT_SET'], ]; // Événement canonique (toujours présent). $triggers[] = self::gtm_custom_event_trigger('10', 'CMP - consent update', []); // Catégories activées → variable dataLayer + déclencheur "accordé". $cats = [ 'analytics' => ['on' => (int)$s['enable_cat_analytics'], 'var' => 'cmp410gone_analytics', 'vid' => '1', 'tid' => '11', 'label' => 'CMP - mesure accordee'], 'retargeting' => ['on' => (int)$s['enable_cat_retargeting'], 'var' => 'cmp410gone_retargeting', 'vid' => '2', 'tid' => '12', 'label' => 'CMP - reciblage accorde'], 'session' => ['on' => (int)$s['enable_cat_session'], 'var' => 'cmp410gone_session', 'vid' => '3', 'tid' => '13', 'label' => 'CMP - enregistrement session accorde'], ]; foreach ($cats as $cat) { if (!$cat['on']) { continue; } $variables[] = self::gtm_dlv($cat['vid'], $cat['var']); $triggers[] = self::gtm_custom_event_trigger($cat['tid'], $cat['label'], [['var' => $cat['var'], 'val' => 'true']]); } // Mesure de la page courante dès la validation du consentement (GA4 temps réel). // Le tag GA4 page_view ne part QUE sur un choix frais (cmp410gone_consent_changed) // et analytics accordé → visible immédiatement pour un nouveau visiteur, sans // double comptage pour les récurrents (dont la page vue de chargement compte déjà). if ((int)$s['enable_cat_analytics']) { $variables[] = self::gtm_dlv('4', 'cmp410gone_consent_changed'); $triggers[] = self::gtm_custom_event_trigger('14', 'CMP - page vue post-consentement', [ ['var' => 'cmp410gone_analytics', 'val' => 'true'], ['var' => 'cmp410gone_consent_changed', 'val' => 'true'], ]); $tags[] = [ 'accountId' => '0', 'containerId' => '0', 'tagId' => '4', 'name' => 'GA4 - page_view (post-consentement)', 'type' => 'gaawe', 'parameter' => [ ['type' => 'BOOLEAN', 'key' => 'sendEcommerceData', 'value' => 'false'], ['type' => 'TEMPLATE', 'key' => 'eventName', 'value' => 'page_view'], ['type' => 'TEMPLATE', 'key' => 'measurementIdOverride', 'value' => $ga4], ], 'firingTriggerId' => ['14'], 'tagFiringOption' => 'ONCE_PER_EVENT', 'monitoringMetadata' => ['type' => 'MAP'], 'consentSettings' => ['consentStatus' => 'NOT_SET'], ]; } // Exemple de tag non-Google (en pause) gaté sur le reciblage, si la catégorie existe. if ((int)$s['enable_cat_retargeting']) { $tags[] = [ 'accountId' => '0', 'containerId' => '0', 'tagId' => '3', 'name' => 'Exemple - tag reciblage non-Google (EN PAUSE)', 'type' => 'html', 'notes' => 'Modele a dupliquer pour chaque pixel non-Google (Meta, LinkedIn, Taboola...). Collez votre code, gardez ce declencheur et le consentement ad_storage + ad_personalization, puis retirez la pause.', 'parameter' => [ ['type' => 'TEMPLATE', 'key' => 'html', 'value' => ""], ['type' => 'BOOLEAN', 'key' => 'supportDocumentWrite', 'value' => 'false'], ], 'firingTriggerId' => ['12'], 'tagFiringOption' => 'ONCE_PER_EVENT', 'monitoringMetadata' => ['type' => 'MAP'], 'paused' => true, 'consentSettings' => ['consentStatus' => 'NEEDED', 'consentType' => ['type' => 'LIST', 'list' => [ ['type' => 'TEMPLATE', 'value' => 'ad_storage'], ['type' => 'TEMPLATE', 'value' => 'ad_personalization'], ]]], ]; } $built_in = []; foreach (['PAGE_URL' => 'Page URL', 'PAGE_HOSTNAME' => 'Page Hostname', 'PAGE_PATH' => 'Page Path', 'REFERRER' => 'Referrer', 'EVENT' => 'Event'] as $type => $name) { $built_in[] = ['accountId' => '0', 'containerId' => '0', 'type' => $type, 'name' => $name]; } return [ 'exportFormatVersion' => 2, 'exportTime' => gmdate('Y-m-d H:i:s'), 'containerVersion' => [ 'path' => 'accounts/0/containers/0/versions/0', 'accountId' => '0', 'containerId' => '0', 'containerVersionId' => '0', 'container' => [ 'path' => 'accounts/0/containers/0', 'accountId' => '0', 'containerId' => '0', 'name' => '410Gone CMP - Consent Mode v2', 'publicId' => $public_id, 'usageContext' => ['WEB'], 'features' => [ 'supportUserPermissions' => true, 'supportEnvironments' => true, 'supportWorkspaces' => true, 'supportGtagConfigs' => false, 'supportBuiltInVariables' => true, 'supportClients' => false, 'supportFolders' => true, 'supportTags' => true, 'supportTemplates' => true, 'supportTriggers' => true, 'supportVariables' => true, 'supportVersions' => true, 'supportZones' => true, 'supportTransformations' => false, ], 'tagIds' => [$public_id], ], 'tag' => $tags, 'trigger' => $triggers, 'variable' => $variables, 'builtInVariable' => $built_in, ], ]; } private static function gtm_custom_event_trigger($id, $name, $filters = []) { $trigger = [ 'accountId' => '0', 'containerId' => '0', 'triggerId' => $id, 'name' => $name, 'type' => 'CUSTOM_EVENT', 'customEventFilter' => [[ 'type' => 'EQUALS', 'parameter' => [ ['type' => 'TEMPLATE', 'key' => 'arg0', 'value' => '{{_event}}'], ['type' => 'TEMPLATE', 'key' => 'arg1', 'value' => 'cmp410gone_consent_update'], ], ]], ]; if (!empty($filters)) { $trigger['filter'] = []; foreach ($filters as $f) { $trigger['filter'][] = [ 'type' => 'EQUALS', 'parameter' => [ ['type' => 'TEMPLATE', 'key' => 'arg0', 'value' => '{{' . $f['var'] . '}}'], ['type' => 'TEMPLATE', 'key' => 'arg1', 'value' => $f['val']], ], ]; } } return $trigger; } private static function gtm_dlv($id, $name) { return [ 'accountId' => '0', 'containerId' => '0', 'variableId' => $id, 'name' => $name, 'type' => 'v', 'parameter' => [ ['type' => 'INTEGER', 'key' => 'dataLayerVersion', 'value' => '2'], ['type' => 'BOOLEAN', 'key' => 'setDefaultValue', 'value' => 'true'], ['type' => 'TEMPLATE', 'key' => 'defaultValue', 'value' => 'false'], ['type' => 'TEMPLATE', 'key' => 'name', 'value' => $name], ], ]; } public static function download_gtm_container() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Insufficient permissions.', '410gone-consent-manager')); } check_admin_referer('cmp410gone_download_gtm'); $container = self::generate_gtm_container(self::get_settings()); $json = wp_json_encode($container, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); nocache_headers(); header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename=GTM-410gone-consent-mode.json'); echo $json; exit; } public static function enqueue_head_scripts() { $s = self::get_settings(); $tracking_mode = $s['tracking_mode'] ?? 'hybrid'; $advanced = (int)$s['advanced_consent_mode']; // Le consent default (denied) doit être posé le plus tôt possible dans TOUS // les modes, y compris gtm/hybrid : c'est la garantie « rien avant le choix » // du Consent Mode v2. L'option init_datalayer permet de le désactiver // (conflits de cache éventuels), auquel cas GTM doit poser le default. $init_datalayer = (int)$s['init_datalayer'] && in_array($tracking_mode, ['all_inclusive', 'hybrid', 'gtm'], true); if ($init_datalayer) { $wait_ms = max(0, min(5000, (int)$s['consent_wait_for_update_ms'])); wp_register_script('cmp410gone-consent-default', '', [], null, false); $cookie_name = self::COOKIE_NAME; $legacy_cookie = self::LEGACY_COOKIE_NAME; $gtm_id = esc_js($s['gtm_id']); $ga4_id = esc_js($s['ga4_id']); // Default des 7 signaux dérivé d'une source unique pour éviter toute dérive. $default_lines = ''; foreach (self::consent_mode_defaults() as $key => $value) { $default_lines .= " {$key}:'{$value}',\n"; } // Consent Mode avancé (lot 3.3) : ads_data_redaction retire les identifiants // de clic des pings quand ad_storage est denied ; url_passthrough reste false. $advanced_lines = $advanced ? " ads_data_redaction:true,\n url_passthrough:false,\n" : ''; $base = "window.dataLayer = window.dataLayer || [];\n" . "window.gtag = window.gtag || function(){dataLayer.push(arguments);};\n" . "gtag('consent','default',{\n" . $default_lines . $advanced_lines . " wait_for_update:{$wait_ms}\n" . "});\n"; // Lecture du cookie commune à tous les modes (lot 3.2) : indispensable pour // qu'un visiteur consentant qui revient soit mesuré dès le , sans // attendre cmp.js (footer, DOMContentLoaded, souvent au-delà des 500 ms). $read_fn = "function readCmpCookie(){\n" . " var match=document.cookie.match(/(?:^|;\\s*)(" . preg_quote($cookie_name, '/') . "|" . preg_quote($legacy_cookie, '/') . ")=([^;]+)/);\n" . " if(!match){return null;}\n" . " try{return JSON.parse(decodeURIComponent(match[2]));}catch(e){return null;}\n" . "}\n"; $consent_update = " gtag('consent','update',{\n" . " ad_storage:consent.ad_storage||'denied',\n" . " ad_user_data:consent.ad_user_data||'denied',\n" . " ad_personalization:consent.ad_personalization||'denied',\n" . " analytics_storage:'granted',\n" . " functionality_storage:consent.functionality_storage||'denied',\n" . " personalization_storage:consent.personalization_storage||'denied',\n" . " security_storage:consent.security_storage||'granted'\n" . " });\n"; if ($tracking_mode === 'all_inclusive') { $inline = "(function(){\n" . $base . $read_fn . "var consent=readCmpCookie();\n" . "if(consent && consent.analytics_storage==='granted'){\n" . $consent_update . " if('{$ga4_id}'){\n" . " gtag('js', new Date());\n" . " gtag('config', '{$ga4_id}');\n" . " var ga=document.createElement('script');ga.async=true;ga.src='https://www.googletagmanager.com/gtag/js?id={$ga4_id}';document.head.appendChild(ga);\n" . " window.CMP410GONE_gtagLoaded=true;\n" . " }\n" // Alias historique déprécié + garde partagée avec cmp.js (audit P2-2) : // évite le double ga4_after_consent entre chemin A (head) et chemin B (footer). . " window.CMP410GONE_ga4Fired=true;\n" . " window.dataLayer.push({event:'ga4_after_consent'});\n" . " if('{$gtm_id}'){\n" . " (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','{$gtm_id}');\n" . " window.CMP410GONE_gtmLoaded=true;\n" . " }\n" . "}\n" . "})();"; } else { // hybrid / gtm (tous modes) : on pose le default denied PUIS, pour un // visiteur qui a déjà un cookie, on lit ce cookie et on émet le consent // update dès le — sans injecter de script (GTM/gtm.js s'en chargent // selon le mode). Ainsi le GA4 config de GTM se déclenche avec le bon // consentement dès le chargement, et sa page vue de la page courante n'est // pas dégradée (sinon la MAJ n'arrive que via cmp.js en footer, après les // 500 ms de wait_for_update). Voir aussi le tag "page_view post-consent" // côté GTM pour le NOUVEAU visiteur qui accepte sur la page. $inline = "(function(){\n" . $base . $read_fn . "var consent=readCmpCookie();\n" . "if(consent && consent.analytics_storage==='granted'){\n" . $consent_update . "}\n" . "})();"; } wp_add_inline_script('cmp410gone-consent-default', $inline); wp_enqueue_script('cmp410gone-consent-default'); } if (!(int)$s['enable'] || empty($s['gtm_id']) || $tracking_mode !== 'gtm') { return; } // La dépendance n'est ajoutée que si le handle a réellement été enregistré // (init_datalayer activé), sinon WordPress signale une dépendance manquante. $gtm_deps = wp_script_is('cmp410gone-consent-default', 'registered') ? ['cmp410gone-consent-default'] : []; $gtm_src = 'https://www.googletagmanager.com/gtm.js?id=' . rawurlencode($s['gtm_id']); wp_register_script('cmp410gone-gtm', $gtm_src, $gtm_deps, null, false); wp_enqueue_script('cmp410gone-gtm'); } public static function enqueue_assets() { $s = self::get_settings(); if (!(int)$s['enable']) { return; } $base = plugin_dir_path(CMP410GONE_PLUGIN_FILE); $url = plugin_dir_url(CMP410GONE_PLUGIN_FILE); $css_ver = file_exists($base . 'assets/cmp.css') ? (string)filemtime($base . 'assets/cmp.css') : '1.0.0'; $js_ver = file_exists($base . 'assets/cmp.js') ? (string)filemtime($base . 'assets/cmp.js') : '1.0.0'; wp_enqueue_style(self::SCRIPT_HANDLE, $url . 'assets/cmp.css', [], $css_ver); // L'overlay n'est plus piloté par des variables PHP (lot 2.3) : il suit l'état // du wrap en CSS pur. On n'injecte ici que les couleurs configurables. $inline = sprintf( ':root{--cmp410-accept-bg:%s;--cmp410-accept-fg:%s;--cmp410-reject-bg:%s;--cmp410-reject-fg:%s;--cmp410-customize-bg:%s;--cmp410-customize-fg:%s;--cmp410-surface:%s;--cmp410-text:%s;--cmp410-background:%s;--cmp410-foreground:%s;}', esc_html($s['accept_btn_color']), esc_html($s['accept_btn_text_color']), esc_html($s['reject_btn_color']), esc_html($s['reject_btn_text_color']), esc_html($s['customize_btn_color']), esc_html($s['customize_btn_text_color']), esc_html($s['background_color']), esc_html($s['text_color']), esc_html($s['background_color']), esc_html($s['text_color']) ); wp_add_inline_style(self::SCRIPT_HANDLE, $inline); wp_enqueue_script(self::SCRIPT_HANDLE, $url . 'assets/cmp.js', [], $js_ver, true); wp_localize_script(self::SCRIPT_HANDLE, 'CMP410GONE', [ 'cookieName' => self::COOKIE_NAME, 'legacyCookieName' => self::LEGACY_COOKIE_NAME, 'ttlDays' => (int)$s['ttl_days'], 'privacyUrl' => esc_url_raw($s['privacy_url']), 'cookiePolicyUrl' => esc_url_raw($s['cookie_policy_url']), 'debug' => (bool)$s['debug'], 'forceShow' => (bool)$s['force_show'], 'bannerPosition' => $s['banner_position'], 'overlayBehavior' => $s['overlay_behavior'], 'focusTrap' => (bool)$s['focus_trap'], 'trackingMode' => $s['tracking_mode'] ?? 'hybrid', 'advancedConsentMode' => (bool)$s['advanced_consent_mode'], 'gtmId' => $s['gtm_id'], 'ga4Id' => $s['ga4_id'], 'version' => defined('CMP410GONE_VERSION') ? CMP410GONE_VERSION : '', 'purposesVersion' => defined('CMP410GONE_PURPOSES_VERSION') ? (int)CMP410GONE_PURPOSES_VERSION : 1, 'logEndpoint' => esc_url_raw(rest_url(CMP410GONE_Log::REST_NAMESPACE . '/' . CMP410GONE_Log::REST_ROUTE)), ]); } public static function render_banner_markup() { static $done = false; if ($done) { return; } $done = true; $s = self::get_settings(); if (!(int)$s['enable']) { return; } // Lot 4 : le HTML est toujours servi « visible » (aria-hidden="false") ; seul // le JS masque le bandeau pour qui a déjà un cookie. Robuste au cache page. $privacy = !empty($s['privacy_url']) ? esc_url($s['privacy_url']) : ''; $cookies = !empty($s['cookie_policy_url']) ? esc_url($s['cookie_policy_url']) : ''; $opacity = max(20, min(70, (int)$s['overlay_opacity'])) / 100; $ttl = (int)$s['ttl_days']; $render_links = function () use ($privacy, $cookies, $s) { if ($privacy) { printf( '%2$s', esc_url($privacy), esc_html($s['privacy_link_label']) ); } if ($cookies) { echo '·'; printf( '%2$s', esc_url($cookies), esc_html($s['cookie_link_label']) ); } }; ?>