'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 '

'; } public static function admin_menu() { add_options_page(__('🍪 410Gone Consent Manager — Settings', '410gone-consent-manager'), __('🍪 410Gone Consent Manager', '410gone-consent-manager'), 'manage_options', self::PAGE_SLUG, [__CLASS__, 'settings_page']); } public static function register_settings() { register_setting('cmp410gone_group', self::OPTION_KEY, [ 'type' => 'array', 'sanitize_callback' => [__CLASS__, 'sanitize_settings'], 'default' => self::defaults(), ]); add_settings_section('cmp410gone_design', __('Design', '410gone-consent-manager'), [__CLASS__, 'section_design'], self::PAGE_SLUG); add_settings_field('enable', __('Enable CMP', '410gone-consent-manager'), [__CLASS__, 'field_enable'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('accept_btn_color', __('Accept button color', '410gone-consent-manager'), [__CLASS__, 'field_accept_btn_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('accept_btn_text_color', __('Accept button text color', '410gone-consent-manager'), [__CLASS__, 'field_accept_btn_text_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('background_color', __('Background color', '410gone-consent-manager'), [__CLASS__, 'field_background_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('text_color', __('Text color', '410gone-consent-manager'), [__CLASS__, 'field_text_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('reject_btn_color', __('Reject button color', '410gone-consent-manager'), [__CLASS__, 'field_reject_btn_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('reject_btn_text_color', __('Reject button text color', '410gone-consent-manager'), [__CLASS__, 'field_reject_btn_text_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('customize_btn_color', __('Customize button color', '410gone-consent-manager'), [__CLASS__, 'field_customize_btn_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('customize_btn_text_color', __('Customize button text color', '410gone-consent-manager'), [__CLASS__, 'field_customize_btn_text_color'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('banner_position', __('Banner position', '410gone-consent-manager'), [__CLASS__, 'field_banner_position'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('overlay', __('Blocking overlay', '410gone-consent-manager'), [__CLASS__, 'field_overlay'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('categories', __('Proposed categories', '410gone-consent-manager'), [__CLASS__, 'field_categories'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_field('focus_trap', __('Keyboard focus trap', '410gone-consent-manager'), [__CLASS__, 'field_focus_trap'], self::PAGE_SLUG, 'cmp410gone_design'); add_settings_section('cmp410gone_labels', __('Labels', '410gone-consent-manager'), [__CLASS__, 'section_labels'], self::PAGE_SLUG); add_settings_field('privacy_url', __('Privacy policy URL', '410gone-consent-manager'), [__CLASS__, 'field_privacy_url'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('cookie_policy_url', __('Cookie policy URL (optional)', '410gone-consent-manager'), [__CLASS__, 'field_cookie_policy_url'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('banner_title', __('Banner title', '410gone-consent-manager'), [__CLASS__, 'field_banner_title'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('banner_text', __('Banner text', '410gone-consent-manager'), [__CLASS__, 'field_banner_text'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('policy_link_labels', __('Policy link labels', '410gone-consent-manager'), [__CLASS__, 'field_policy_link_labels'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('btn_labels', __('Button labels', '410gone-consent-manager'), [__CLASS__, 'field_btn_labels'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('category_titles', __('Category titles', '410gone-consent-manager'), [__CLASS__, 'field_category_titles'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('modal_labels', __('Category subtitles', '410gone-consent-manager'), [__CLASS__, 'field_modal_labels'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_field('category_meta', __('Category recipients & retention', '410gone-consent-manager'), [__CLASS__, 'field_category_meta'], self::PAGE_SLUG, 'cmp410gone_labels'); add_settings_section('cmp410gone_tracking', __('Tracking & configuration', '410gone-consent-manager'), [__CLASS__, 'section_tracking'], self::PAGE_SLUG); add_settings_field('tracking_mode', __('Tracking mode', '410gone-consent-manager'), [__CLASS__, 'field_tracking_mode'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_field('gtm_id', __('GTM container ID', '410gone-consent-manager'), [__CLASS__, 'field_gtm_id'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_field('ga4_id', __('GA4 measurement ID', '410gone-consent-manager'), [__CLASS__, 'field_ga4_id'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_field('consent_wait_for_update_ms', __('Consent wait_for_update (ms)', '410gone-consent-manager'), [__CLASS__, 'field_wait'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_field('init_datalayer', __('Initialize dataLayer', '410gone-consent-manager'), [__CLASS__, 'field_init_datalayer'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_field('advanced_consent_mode', __('Advanced Consent Mode', '410gone-consent-manager'), [__CLASS__, 'field_advanced_consent_mode'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_field('gtm_export', __('GTM container', '410gone-consent-manager'), [__CLASS__, 'field_gtm_export'], self::PAGE_SLUG, 'cmp410gone_tracking'); add_settings_section('cmp410gone_advanced', __('Advanced', '410gone-consent-manager'), [__CLASS__, 'section_advanced'], self::PAGE_SLUG); add_settings_field('ttl_days', __('Consent retention (days)', '410gone-consent-manager'), [__CLASS__, 'field_ttl_days'], self::PAGE_SLUG, 'cmp410gone_advanced'); add_settings_field('debug', __('Debug console', '410gone-consent-manager'), [__CLASS__, 'field_debug'], self::PAGE_SLUG, 'cmp410gone_advanced'); add_settings_field('force_show', __('Force display (testing)', '410gone-consent-manager'), [__CLASS__, 'field_force_show'], self::PAGE_SLUG, 'cmp410gone_advanced'); } public static function sanitize_settings($in) { $d = self::defaults(); $out = []; $out['enable'] = isset($in['enable']) ? (int)!!$in['enable'] : $d['enable']; $out['debug'] = isset($in['debug']) ? (int)!!$in['debug'] : $d['debug']; $out['force_show'] = isset($in['force_show']) ? (int)!!$in['force_show'] : $d['force_show']; $out['init_datalayer'] = isset($in['init_datalayer']) ? (int)!!$in['init_datalayer'] : $d['init_datalayer']; $tracking_mode = isset($in['tracking_mode']) ? sanitize_key((string)$in['tracking_mode']) : $d['tracking_mode']; if (!in_array($tracking_mode, ['all_inclusive', 'gtm', 'hybrid'], true)) { $tracking_mode = $d['tracking_mode']; } $out['tracking_mode'] = $tracking_mode; $gtm = isset($in['gtm_id']) ? strtoupper(trim((string)$in['gtm_id'])) : ''; if ($gtm !== '' && !preg_match('/^GTM-[A-Z0-9]+$/', $gtm)) { $gtm = ''; } $out['gtm_id'] = $gtm; $ga4 = isset($in['ga4_id']) ? strtoupper(trim((string)$in['ga4_id'])) : ''; if ($ga4 !== '' && !preg_match('/^G-[A-Z0-9]+$/', $ga4)) { $ga4 = ''; } $out['ga4_id'] = $ga4; $wait = isset($in['consent_wait_for_update_ms']) ? (int)$in['consent_wait_for_update_ms'] : $d['consent_wait_for_update_ms']; if ($wait < 0) { $wait = 0; } if ($wait > 5000) { $wait = 5000; } $out['consent_wait_for_update_ms'] = $wait; $out['privacy_url'] = isset($in['privacy_url']) ? esc_url_raw((string)$in['privacy_url']) : $d['privacy_url']; $out['cookie_policy_url'] = isset($in['cookie_policy_url']) ? esc_url_raw((string)$in['cookie_policy_url']) : $d['cookie_policy_url']; $out['banner_title'] = isset($in['banner_title']) ? sanitize_text_field((string)$in['banner_title']) : $d['banner_title']; $out['banner_text'] = isset($in['banner_text']) ? wp_kses_post((string)$in['banner_text']) : $d['banner_text']; $out['btn_accept'] = isset($in['btn_accept']) ? sanitize_text_field((string)$in['btn_accept']) : $d['btn_accept']; $out['btn_reject'] = isset($in['btn_reject']) ? sanitize_text_field((string)$in['btn_reject']) : $d['btn_reject']; $out['btn_reject_all'] = isset($in['btn_reject_all']) ? sanitize_text_field((string)$in['btn_reject_all']) : $d['btn_reject_all']; $out['btn_customize'] = isset($in['btn_customize']) ? sanitize_text_field((string)$in['btn_customize']) : $d['btn_customize']; $out['btn_save'] = isset($in['btn_save']) ? sanitize_text_field((string)$in['btn_save']) : $d['btn_save']; $out['modal_title'] = isset($in['modal_title']) ? sanitize_text_field((string)$in['modal_title']) : $d['modal_title']; $out['modal_desc_essentials'] = isset($in['modal_desc_essentials']) ? sanitize_text_field((string)$in['modal_desc_essentials']) : $d['modal_desc_essentials']; $out['modal_desc_analytics'] = isset($in['modal_desc_analytics']) ? sanitize_text_field((string)$in['modal_desc_analytics']) : $d['modal_desc_analytics']; $out['modal_desc_retargeting'] = isset($in['modal_desc_retargeting']) ? sanitize_text_field((string)$in['modal_desc_retargeting']) : $d['modal_desc_retargeting']; $out['modal_desc_session'] = isset($in['modal_desc_session']) ? sanitize_text_field((string)$in['modal_desc_session']) : $d['modal_desc_session']; $out['cat_essentials_title'] = isset($in['cat_essentials_title']) ? sanitize_text_field((string)$in['cat_essentials_title']) : $d['cat_essentials_title']; $out['cat_analytics_title'] = isset($in['cat_analytics_title']) ? sanitize_text_field((string)$in['cat_analytics_title']) : $d['cat_analytics_title']; $out['cat_retargeting_title'] = isset($in['cat_retargeting_title']) ? sanitize_text_field((string)$in['cat_retargeting_title']) : $d['cat_retargeting_title']; $out['cat_session_title'] = isset($in['cat_session_title']) ? sanitize_text_field((string)$in['cat_session_title']) : $d['cat_session_title']; $out['cat_essentials_meta'] = isset($in['cat_essentials_meta']) ? sanitize_text_field((string)$in['cat_essentials_meta']) : $d['cat_essentials_meta']; $out['cat_analytics_meta'] = isset($in['cat_analytics_meta']) ? sanitize_text_field((string)$in['cat_analytics_meta']) : $d['cat_analytics_meta']; $out['cat_retargeting_meta'] = isset($in['cat_retargeting_meta']) ? sanitize_text_field((string)$in['cat_retargeting_meta']) : $d['cat_retargeting_meta']; $out['cat_session_meta'] = isset($in['cat_session_meta']) ? sanitize_text_field((string)$in['cat_session_meta']) : $d['cat_session_meta']; $out['privacy_link_label'] = isset($in['privacy_link_label']) ? sanitize_text_field((string)$in['privacy_link_label']) : $d['privacy_link_label']; $out['cookie_link_label'] = isset($in['cookie_link_label']) ? sanitize_text_field((string)$in['cookie_link_label']) : $d['cookie_link_label']; $out['enable_cat_analytics'] = isset($in['enable_cat_analytics']) ? (int)!!$in['enable_cat_analytics'] : 0; $out['enable_cat_retargeting'] = isset($in['enable_cat_retargeting']) ? (int)!!$in['enable_cat_retargeting'] : 0; $out['enable_cat_session'] = isset($in['enable_cat_session']) ? (int)!!$in['enable_cat_session'] : 0; $bg = isset($in['background_color']) ? trim((string)$in['background_color']) : $d['background_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $bg)) { $bg = $d['background_color']; } $out['background_color'] = $bg; $text = isset($in['text_color']) ? trim((string)$in['text_color']) : $d['text_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $text)) { $text = $d['text_color']; } $out['text_color'] = $text; $col = isset($in['accept_btn_color']) ? trim((string)$in['accept_btn_color']) : $d['accept_btn_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $col)) { $col = $d['accept_btn_color']; } $out['accept_btn_color'] = $col; $tcol = isset($in['accept_btn_text_color']) ? trim((string)$in['accept_btn_text_color']) : $d['accept_btn_text_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $tcol)) { $tcol = $d['accept_btn_text_color']; } $out['accept_btn_text_color'] = $tcol; $rcol = isset($in['reject_btn_color']) ? trim((string)$in['reject_btn_color']) : $d['reject_btn_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $rcol)) { $rcol = $d['reject_btn_color']; } $out['reject_btn_color'] = $rcol; $rtcol = isset($in['reject_btn_text_color']) ? trim((string)$in['reject_btn_text_color']) : $d['reject_btn_text_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $rtcol)) { $rtcol = $d['reject_btn_text_color']; } $out['reject_btn_text_color'] = $rtcol; // Garde-fou de parité (brief 1.2 / design 1c) : le contraste texte/fond du bouton // Refuser ne doit pas être nettement inférieur à celui d'Accepter, sans quoi la // symétrie exigée par la CNIL est rompue par un simple réglage de couleur. $accept_ratio = self::contrast_ratio($out['accept_btn_text_color'], $out['accept_btn_color']); $reject_ratio = self::contrast_ratio($out['reject_btn_text_color'], $out['reject_btn_color']); if ($reject_ratio < 4.5) { add_settings_error(self::OPTION_KEY, 'cmp410gone_reject_contrast', sprintf( /* translators: %s: contrast ratio. */ __('Le contraste du bouton « Refuser » (%s:1) est sous le seuil RGAA de 4,5:1. Choisissez un texte/fond plus contrasté pour préserver la lisibilité et la parité avec « Accepter ».', '410gone-consent-manager'), number_format_i18n($reject_ratio, 1) ), 'warning'); } elseif ($reject_ratio + 0.5 < $accept_ratio) { add_settings_error(self::OPTION_KEY, 'cmp410gone_reject_parity', sprintf( /* translators: 1: reject ratio, 2: accept ratio. */ __('Le bouton « Refuser » (%1$s:1) est nettement moins contrasté que « Accepter » (%2$s:1). La CNIL exige un traitement équivalent : rapprochez les deux contrastes.', '410gone-consent-manager'), number_format_i18n($reject_ratio, 1), number_format_i18n($accept_ratio, 1) ), 'warning'); } $custom_col = isset($in['customize_btn_color']) ? trim((string)$in['customize_btn_color']) : $d['customize_btn_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $custom_col)) { $custom_col = $d['customize_btn_color']; } $out['customize_btn_color'] = $custom_col; $custom_tcol = isset($in['customize_btn_text_color']) ? trim((string)$in['customize_btn_text_color']) : $d['customize_btn_text_color']; if (!preg_match('/^#([A-Fa-f0-9]{6})$/', $custom_tcol)) { $custom_tcol = $d['customize_btn_text_color']; } $out['customize_btn_text_color'] = $custom_tcol; $ttl = isset($in['ttl_days']) ? (int)$in['ttl_days'] : $d['ttl_days']; if ($ttl < 1) { $ttl = 1; } if ($ttl > 3650) { $ttl = 3650; } $out['ttl_days'] = $ttl; // Affichage / comportement (lot 2 + design 1c). $position = isset($in['banner_position']) ? sanitize_key((string)$in['banner_position']) : $d['banner_position']; if (!in_array($position, ['center', 'footer', 'bar'], true)) { $position = $d['banner_position']; } $out['banner_position'] = $position; $overlay_behavior = isset($in['overlay_behavior']) ? sanitize_key((string)$in['overlay_behavior']) : $d['overlay_behavior']; if (!in_array($overlay_behavior, ['blocking', 'veil', 'none'], true)) { $overlay_behavior = $d['overlay_behavior']; } $out['overlay_behavior'] = $overlay_behavior; $opacity = isset($in['overlay_opacity']) ? (int)$in['overlay_opacity'] : $d['overlay_opacity']; $out['overlay_opacity'] = max(20, min(70, $opacity)); $out['focus_trap'] = isset($in['focus_trap']) ? (int)!!$in['focus_trap'] : 0; $out['advanced_consent_mode'] = isset($in['advanced_consent_mode']) ? (int)!!$in['advanced_consent_mode'] : 0; // Déprécié : conservé pour ne pas perdre l'ancienne valeur, plus utilisé au rendu. $overlay_mode = isset($in['overlay_mode']) ? sanitize_key((string)$in['overlay_mode']) : $d['overlay_mode']; if (!in_array($overlay_mode, ['inline', 'js'], true)) { $overlay_mode = $d['overlay_mode']; } $out['overlay_mode'] = $overlay_mode; self::register_translation_strings($out); return $out; } public static function enqueue_admin_assets($hook) { if ($hook !== 'settings_page_' . self::PAGE_SLUG) { return; } $base = plugin_dir_path(CMP410GONE_PLUGIN_FILE); $url = plugin_dir_url(CMP410GONE_PLUGIN_FILE); $css_ver = file_exists($base . 'assets/admin.css') ? (string)filemtime($base . 'assets/admin.css') : '1.0.0'; $js_ver = file_exists($base . 'assets/admin.js') ? (string)filemtime($base . 'assets/admin.js') : '1.0.0'; wp_enqueue_style(self::ADMIN_STYLE_HANDLE, $url . 'assets/admin.css', [], $css_ver); wp_enqueue_script('cmp410gone-admin', $url . 'assets/admin.js', [], $js_ver, true); } public static function settings_page() { if (!current_user_can('manage_options')) { return; } $page = self::PAGE_SLUG; // Fallback in case hooks didn’t register sections (e.g., custom admin flows). if (empty($GLOBALS['wp_settings_sections'][$page])) { self::register_settings(); } $s = self::get_settings(); $tabs = [ 'cmp410gone_design' => '🎨 ' . __('Design', '410gone-consent-manager'), 'cmp410gone_labels' => '✏️ ' . __('Labels', '410gone-consent-manager'), 'cmp410gone_tracking' => '📈 ' . __('Tracking & configuration', '410gone-consent-manager'), 'cmp410gone_advanced' => '🛠️ ' . __('Advanced', '410gone-consent-manager'), ]; $first = true; ?>

410gone · options-general.php?page=

[cmp410gone_manage_cookies label=""]
'; echo '
'; echo '

' . esc_html($section['title']) . '

'; echo '
'; if (!empty($section['callback'])) { echo '
'; call_user_func($section['callback'], $section); echo '
'; } if (!empty($wp_settings_fields[$page][$section_id])) { echo ''; do_settings_fields($page, $section_id); echo ''; } echo ''; } public static function section_design() { echo '

' . 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') . '

'; echo '

WP Rocket: ' . esc_html__('exclude', '410gone-consent-manager') . ' cmp.js ' . esc_html__('from “Delay JS execution”.', '410gone-consent-manager') . '

'; } public static function section_advanced() { echo '

' . 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']) ); ?>





















= 4.5; printf( '%s %s:1', $ok ? '#1a7f37' : '#b32d2e', esc_html($ok ? '✓' : '⚠'), esc_html(number_format_i18n($ratio, 1)) ); } public static function field_background_color() { $s = self::get_settings(); ?>

[__('Centered card', '410gone-consent-manager'), __('Modal at the center of the screen. Highest decision rate.', '410gone-consent-manager')], 'footer' => [__('Bottom banner', '410gone-consent-manager'), __('Current behavior, less intrusive.', '410gone-consent-manager')], 'bar' => [__('Full-width bar', '410gone-consent-manager'), __('Stuck to the bottom, no margins or rounded corners.', '410gone-consent-manager')], ]; echo '
'; foreach ($options as $val => $texts) { printf( '', esc_attr(self::OPTION_KEY), esc_attr($val), checked($pos, $val, false), esc_html($texts[0]), esc_html($texts[1]) ); } echo '
'; } public static function field_overlay() { $s = self::get_settings(); $ov = $s['overlay_behavior']; $options = [ 'blocking' => [__('Active — a choice is required', '410gone-consent-manager'), __('Dark veil until a choice is made. Driven by pure CSS from the banner state: immune to page cache and JS delay.', '410gone-consent-manager')], 'veil' => [__('Non-blocking veil', '410gone-consent-manager'), __('Darkens the page but lets clicks through.', '410gone-consent-manager')], 'none' => [__('No overlay', '410gone-consent-manager'), ''], ]; echo '
'; foreach ($options as $val => $texts) { printf( '', esc_attr(self::OPTION_KEY), esc_attr($val), checked($ov, $val, false), esc_html($texts[0]), $texts[1] !== '' ? '
' . esc_html($texts[1]) . '' : '' ); } printf( '

', esc_html__('Veil opacity (20–70%):', '410gone-consent-manager'), esc_attr(self::OPTION_KEY), (int)$s['overlay_opacity'] ); echo '
'; } public static function field_categories() { $s = self::get_settings(); ?>

in every tracking mode so a returning visitor is measured without degradation.', '410gone-consent-manager'); ?>

'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']) ); } }; ?>
__('Manage my cookies', '410gone-consent-manager')], $atts, self::SHORTCODE); $label = esc_html($atts['label']); return '' . $label . ''; } private static function translate_settings($settings) { $translatable = [ 'banner_title', 'banner_text', 'btn_accept', 'btn_reject', 'btn_reject_all', 'btn_customize', 'btn_save', 'modal_title', 'cat_essentials_meta', 'cat_analytics_meta', 'cat_retargeting_meta', 'cat_session_meta', 'cat_essentials_title', 'cat_analytics_title', 'cat_retargeting_title', 'cat_session_title', 'modal_desc_essentials', 'modal_desc_analytics', 'modal_desc_retargeting', 'modal_desc_session', 'privacy_link_label', 'cookie_link_label', 'privacy_url', 'cookie_policy_url', ]; foreach ($translatable as $key) { if (!isset($settings[$key])) { continue; } $settings[$key] = self::translate_text($key, $settings[$key]); $settings[$key] = apply_filters('cmp410gone_translate_setting', $settings[$key], $key, $settings); } return $settings; } private static function register_translation_strings($settings) { $fields = [ 'banner_title' => __('Banner title', '410gone-consent-manager'), 'banner_text' => __('Banner text', '410gone-consent-manager'), 'btn_accept' => __('Accept button', '410gone-consent-manager'), 'btn_reject' => __('Reject button', '410gone-consent-manager'), 'btn_reject_all' => __('Reject-all button (modal)', '410gone-consent-manager'), 'btn_customize' => __('Customize button', '410gone-consent-manager'), 'btn_save' => __('Save button', '410gone-consent-manager'), 'modal_title' => __('Customization modal title', '410gone-consent-manager'), 'cat_essentials_meta' => __('Essentials recipients/retention', '410gone-consent-manager'), 'cat_analytics_meta' => __('Analytics recipients/retention', '410gone-consent-manager'), 'cat_retargeting_meta' => __('Retargeting recipients/retention', '410gone-consent-manager'), 'cat_session_meta' => __('Session recording recipients/retention', '410gone-consent-manager'), 'cat_essentials_title' => __('Essentials title', '410gone-consent-manager'), 'cat_analytics_title' => __('Analytics title', '410gone-consent-manager'), 'cat_retargeting_title' => __('Retargeting title', '410gone-consent-manager'), 'cat_session_title' => __('Session recording title', '410gone-consent-manager'), 'modal_desc_essentials' => __('Essentials subtitle', '410gone-consent-manager'), 'modal_desc_analytics' => __('Analytics subtitle', '410gone-consent-manager'), 'modal_desc_retargeting' => __('Retargeting subtitle', '410gone-consent-manager'), 'modal_desc_session' => __('Session recording subtitle', '410gone-consent-manager'), 'privacy_link_label' => __('Privacy policy link label', '410gone-consent-manager'), 'cookie_link_label' => __('Cookie policy link label', '410gone-consent-manager'), 'privacy_url' => __('Privacy policy URL', '410gone-consent-manager'), 'cookie_policy_url' => __('Cookie policy URL', '410gone-consent-manager'), ]; foreach ($fields as $key => $label) { if (!isset($settings[$key])) { continue; } if (function_exists('pll_register_string')) { pll_register_string('cmp410gone_' . $key, $settings[$key], 'CMP 410gone', false); } if (has_filter('wpml_register_single_string')) { do_action('wpml_register_single_string', 'CMP 410gone', 'cmp410gone_' . $key, $settings[$key]); } } } private static function translate_text($key, $value) { $translated = $value; if (function_exists('pll__')) { $translated = pll__($translated); } elseif (has_filter('wpml_translate_single_string')) { $translated = apply_filters('wpml_translate_single_string', $translated, 'CMP 410gone', 'cmp410gone_' . $key); } return $translated; } }