PageSourceSearch

https://ouillade.eu/wp-content/plugins/cronos/addons/hermes/hermes-modal.js?ver=2.12.20

js ouillade.eu collected 2026-10-02 11:26:20 UTC 63,898 bytes, 1,784 lines download raw bytes

1/**
2 * Hermes - Frontend Modal Controller
3 *
4 * Added:
5 * - consent version/fingerprint enforcement
6 * - category/vendor cleanup on consent changes
7 * - cross-domain consent token import/export
8 * - contextual consent blocks
9 */
10(function () {
11    'use strict';
12
13    var cfg = window.hermesConfig || {};
14    cfg.categories = cfg.categories || { analytics: false, marketing: false, functional: false };
15    cfg.consentFramework = cfg.consentFramework || 'tcf_v23';
16    var COOKIE_NAME = 'hermes_consent';
17    var UID_COOKIE = 'hermes_uid';
18    var CONSENT_TOKEN_QUERY_KEY = 'hermes_ct';
19    var CONSENT_FRAMEWORK_TCF = 'tcf_v23';
20    var bannerVideoAutoPlayed = false;
21
22    function sendEmbedCommand(iframe, type, cmd) {
23        if (!iframe || !iframe.contentWindow) return;
24        if (type === 'youtube') {
25            var funcMap = { play: 'playVideo', pause: 'pauseVideo', mute: 'mute', unmute: 'unMute' };
26            iframe.contentWindow.postMessage(JSON.stringify({ event: 'command', func: funcMap[cmd] || cmd, args: '' }), '*');
27        } else if (type === 'vimeo') {
28            if (cmd === 'mute') {
29                iframe.contentWindow.postMessage(JSON.stringify({ method: 'setVolume', value: 0 }), 'https://player.vimeo.com');
30            } else if (cmd === 'unmute') {
31                iframe.contentWindow.postMessage(JSON.stringify({ method: 'setVolume', value: 1 }), 'https://player.vimeo.com');
32            } else {
33                iframe.contentWindow.postMessage(JSON.stringify({ method: cmd }), 'https://player.vimeo.com');
34            }
35        }
36    }
37
38    var CATEGORY_KEYS = ['analytics', 'marketing', 'functional'];
39    var CATEGORY_LABELS = {
40        analytics: 'analytiques',
41        marketing: 'marketing',
42        functional: 'fonctionnels'
43    };
44
45    var COOKIE_PATTERNS_BY_CATEGORY = {
46        analytics: [
47            /^_ga/i, /^_gid$/i, /^_gat/i, /^__utm/i, /^_pk_/i, /^mtm_/i,
48            /^_hj/i, /^_hjid$/i, /^_cl/i, /^clid$/i, /^tk_ai$/i, /^tk_qs$/i, /^fpid$/i
49        ],
50        marketing: [
51            /^_fbp$/i, /^_fbc$/i, /^fr$/i, /^_gcl_/i, /^li_/i, /^bcookie$/i,
52            /^usermatchhistory$/i, /^__hstc$/i, /^hubspotutk$/i, /^__hssc$/i, /^__hssrc$/i,
53            /^_ttp$/i, /^_tt_/i, /^tt_pixel_session_index$/i, /^_pinterest_sess$/i,
54            /^_pin_/i, /^_derived_epik$/i
55        ],
56        functional: [
57            /^pll_language$/i, /^wp-wpml_current_language$/i, /^_icl_current_language$/i,
58            /^elementor$/i, /^gf_form_/i
59        ]
60    };
61
62    var STORAGE_PATTERNS_BY_CATEGORY = {
63        analytics: [/ga/i, /matomo/i, /piwik/i, /hotjar/i, /clarity/i],
64        marketing: [/facebook/i, /fbq/i, /tiktok/i, /pinterest/i, /hubspot/i, /linkedin/i],
65        functional: [/wpml/i, /polylang/i, /elementor/i]
66    };
67
68    var PROTECTED_COOKIE_PATTERNS = [/^hermes_/i, /^wordpress_/i, /^wp-/i, /^php/i];
69    var TCF_PURPOSE_MAP = {
70        1: 'essential',
71        2: 'marketing',
72        3: 'marketing',
73        4: 'marketing',
74        5: 'functional',
75        6: 'functional',
76        7: 'analytics',
77        8: 'analytics',
78        9: 'analytics',
79        10: 'analytics'
80    };
81    var tcfListeners = {};
82    var tcfNextListenerId = 1;
83    var tcfState = null;
84    var tcfPostMessageBound = false;
85
86    function $(s) { return document.querySelector(s); }
87    function $$(s) { return document.querySelectorAll(s); }
88
89    function generateUID() {
90        return 'hermes_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 11);
91    }
92
93    function normalizeCookieDomain(domainValue) {
94        var domain = String(domainValue || '').trim().toLowerCase();
95        if (!domain) {
96            return '';
97        }
98        domain = domain.replace(/^https?:\/\//i, '');
99        domain = domain.split('/')[0];
100        domain = domain.replace(/:\d+$/, '');
101        domain = domain.replace(/^\.+/, '').replace(/\.+$/, '');
102        if (!domain) {
103            return '';
104        }
105        if (!/^[a-z0-9.-]+$/.test(domain)) {
106            return '';
107        }
108        if (domain === 'localhost' || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(domain)) {
109            return domain;
110        }
111        if (domain.indexOf('.') === -1) {
112            return '';
113        }
114        return '.' + domain;
115    }
116
117    function getConfiguredCookieDomain() {
118        return normalizeCookieDomain(cfg.cookieDomain || '');
119    }
120
121    function getCookieDomainCandidates() {
122        var domains = [];
123        var seen = {};
124
125        function pushDomain(d) {
126            if (typeof d !== 'string' || seen[d]) {
127                return;
128            }
129            seen[d] = true;
130            domains.push(d);
131        }
132
133        pushDomain('');
134
135        var configured = getConfiguredCookieDomain();
136        if (configured) {
137            pushDomain(configured);
138            pushDomain(configured.replace(/^\./, ''));
139        }
140
141        var host = String(location.hostname || '').toLowerCase();
142        if (host) {
143            pushDomain(host);
144            if (host.indexOf('.') > -1) {
145                var parts = host.split('.');
146                for (var i = 0; i < parts.length - 1; i++) {
147                    var parent = parts.slice(i).join('.');
148                    if (parent.indexOf('.') > -1) {
149                        pushDomain(parent);
150                        pushDomain('.' + parent);
151                    }
152                }
153            }
154        }
155
156        return domains;
157    }
158
159    function safeDecodeURIComponent(raw) {
160        try {
161            return decodeURIComponent(raw);
162        } catch (e) {
163            return raw;
164        }
165    }
166
167    function parseCookieValue(raw) {
168        if (typeof raw !== 'string') {
169            return null;
170        }
171        var decoded = safeDecodeURIComponent(raw);
172        try {
173            return JSON.parse(decoded);
174        } catch (e) {
175            return decoded || null;
176        }
177    }
178
179    function setCookie(name, value, months) {
180        var d = new Date();
181        d.setMonth(d.getMonth() + (months || 13));
182        var secure = location.protocol === 'https:' ? '; Secure' : '';
183        var domain = getConfiguredCookieDomain();
184        var domainPart = domain ? '; domain=' + domain : '';
185        document.cookie = name + '=' + encodeURIComponent(JSON.stringify(value)) + '; expires=' + d.toUTCString() + '; path=/; SameSite=Lax' + secure + domainPart;
186    }
187
188    function getCookie(name) {
189        var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
190        if (!m) {
191            return null;
192        }
193        return parseCookieValue(m[1]);
194    }
195
196    function clearCookieEverywhere(name) {
197        var domains = getCookieDomainCandidates();
198        var paths = ['/', ''];
199        for (var di = 0; di < domains.length; di++) {
200            for (var pi = 0; pi < paths.length; pi++) {
201                var domain = domains[di];
202                var path = paths[pi] || '/';
203                var cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=' + path + '; SameSite=Lax';
204                if (domain) {
205                    cookie += '; domain=' + domain;
206                }
207                document.cookie = cookie;
208            }
209        }
210    }
211
212    function deleteCookie(name) {
213        clearCookieEverywhere(name);
214    }
215
216    // UID d'AUDIENCE (cookie hermes_uid) : sert exclusivement à la mesure
217    // d'audience maison (pageviews/rebond), exemptée de consentement au sens
218    // CNIL sous conditions strictes — dont l'absence de recoupement avec
219    // d'autres traitements. Il ne doit JAMAIS être utilisé pour la preuve de
220    // consentement (voir getProofUid) et n'est posé que si les stats sont
221    // activées (cfg.statsTracking).
222    function getOrCreateUID() {
223        var uid = getCookie(UID_COOKIE);
224        if (typeof uid === 'string' && uid) {
225            return uid;
226        }
227        uid = generateUID();
228        setCookie(UID_COOKIE, uid, cfg.consentDuration || 13);
229        return uid;
230    }
231
232    function statsTrackingEnabled() {
233        return cfg.statsTracking !== false && cfg.statsTracking !== 0 && cfg.statsTracking !== '0';
234    }
235
236    // UID de PREUVE : identifiant distinct, embarqué DANS le payload du cookie
237    // de consentement, généré uniquement au moment d'un acte de consentement.
238    // Découplé de l'UID d'audience (exigence « aucun recoupement » de
239    // l'exemption mesure d'audience CNIL — audit RGPD 23/07/2026).
240    function getProofUid(previousPayload) {
241        if (previousPayload && typeof previousPayload.uid === 'string' && previousPayload.uid) {
242            return previousPayload.uid;
243        }
244        return generateUID();
245    }
246
247    function normalizeChoices(rawChoices) {
248        var source = (rawChoices && typeof rawChoices === 'object') ? rawChoices : {};
249        var normalized = {
250            essential: true,
251            analytics: !!source.analytics,
252            marketing: !!source.marketing,
253            functional: !!source.functional
254        };
255
256        // If a category is disabled in config, keep it false.
257        if (cfg.categories && typeof cfg.categories === 'object') {
258            CATEGORY_KEYS.forEach(function (cat) {
259                if (!cfg.categories[cat]) {
260                    normalized[cat] = false;
261                }
262            });
263        }
264
265        return normalized;
266    }
267
268    function buildCookiePayload(choices) {
269        var normalized = normalizeChoices(choices);
270        normalized.__v = String(cfg.consentVersion || '1');
271        normalized.__fp = String(cfg.consentFingerprint || '');
272        normalized.__ts = Date.now();
273        return normalized;
274    }
275
276    function isConsentOutdated(rawConsent) {
277        if (!rawConsent || typeof rawConsent !== 'object') {
278            return false;
279        }
280        var expectedVersion = String(cfg.consentVersion || '1');
281        var expectedFingerprint = String(cfg.consentFingerprint || '');
282        var currentVersion = String(rawConsent.__v || '');
283        var currentFingerprint = String(rawConsent.__fp || '');
284
285        if (!currentVersion || currentVersion !== expectedVersion) {
286            return true;
287        }
288        if (expectedFingerprint && currentFingerprint !== expectedFingerprint) {
289            return true;
290        }
291        return false;
292    }
293
294    function isTCFMode() {
295        return String(cfg.consentFramework || '') === CONSENT_FRAMEWORK_TCF;
296    }
297
298    function parseVendorMap(rawMap) {
299        var source = rawMap;
300        if (typeof source === 'string' && source) {
301            try {
302                source = JSON.parse(source);
303            } catch (e) {
304                source = {};
305            }
306        }
307        if (!source || typeof source !== 'object') {
308            source = {};
309        }
310
311        var map = { analytics: [], marketing: [], functional: [] };
312        CATEGORY_KEYS.forEach(function (cat) {
313            var arr = source[cat];
314            if (!Array.isArray(arr)) {
315                map[cat] = [];
316                return;
317            }
318            var normalized = [];
319            arr.forEach(function (value) {
320                var n = parseInt(value, 10);
321                if (n > 0) {
322                    normalized.push(n);
323                }
324            });
325            normalized.sort(function (a, b) { return a - b; });
326            map[cat] = normalized.filter(function (value, index) { return normalized.indexOf(value) === index; });
327        });
328        return map;
329    }
330
331    function buildPurposeConsents(choices) {
332        var normalized = normalizeChoices(choices || {});
333        var result = {};
334        for (var purpose = 1; purpose <= 10; purpose++) {
335            var mapped = TCF_PURPOSE_MAP[purpose];
336            if (mapped === 'essential') {
337                result[purpose] = true;
338            } else if (mapped) {
339                result[purpose] = !!normalized[mapped];
340            } else {
341                result[purpose] = false;
342            }
343        }
344        return result;
345    }
346
347    function buildVendorConsents(choices) {
348        var normalized = normalizeChoices(choices || {});
349        var map = parseVendorMap(cfg.tcfVendorMap || {});
350        var vendorConsents = {};
351        var disclosed = {};
352
353        CATEGORY_KEYS.forEach(function (cat) {
354            var ids = map[cat] || [];
355            ids.forEach(function (id) {
356                disclosed[id] = true;
357                vendorConsents[id] = !!normalized[cat];
358            });
359        });
360
361        return {
362            vendorConsents: vendorConsents,
363            disclosedVendors: disclosed
364        };
365    }
366
367    function purposeConsentBitString(purposeConsents) {
368        var out = '';
369        for (var p = 1; p <= 10; p++) {
370            out += purposeConsents[p] ? '1' : '0';
371        }
372        return out;
373    }
374
375    function buildPseudoTcString(choices, purposeConsents, vendorConsents) {
376        var payload = {
377            v: '2.3',
378            ts: Date.now(),
379            // UID de preuve (payload consentement), pas l'UID d'audience.
380            uid: getProofUid(getCookie(COOKIE_NAME) || {}),
381            p: purposeConsents,
382            vc: vendorConsents,
383            c: normalizeChoices(choices || {})
384        };
385        return 'H23.' + b64UrlEncode(JSON.stringify(payload));
386    }
387
388    function buildAdditionalConsentString(disclosedVendors, vendorConsents) {
389        var ids = [];
390        Object.keys(disclosedVendors || {}
390).forEach(function (key) {
391            if (vendorConsents && vendorConsents[key]) {
392                ids.push(parseInt(key, 10));
393            }
394        });
395        ids = ids.filter(function (n) { return n > 0; }).sort(function (a, b) { return a - b; });
396        if (!ids.length) {
397            return '2~';
398        }
399        return '2~' + ids.join('.');
400    }
401
402    function setRawCookie(name, rawValue, months) {
403        var d = new Date();
404        d.setMonth(d.getMonth() + (months || 13));
405        var secure = location.protocol === 'https:' ? '; Secure' : '';
406        var domain = getConfiguredCookieDomain();
407        var domainPart = domain ? '; domain=' + domain : '';
408        document.cookie = name + '=' + encodeURIComponent(String(rawValue || '')) + '; expires=' + d.toUTCString() + '; path=/; SameSite=Lax' + secure + domainPart;
409    }
410
411    function getRawCookie(name) {
412        var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
413        if (!m) {
414            return '';
415        }
416        return safeDecodeURIComponent(m[1] || '');
417    }
418
419    function clearTcfStorage() {
420        deleteCookie('euconsent-v2');
421        deleteCookie('addtl_consent');
422        var keys = ['IABTCF_TCString', 'IABTCF_AddtlConsent', 'IABTCF_PurposeConsents', 'IABTCF_VendorConsents', 'IABTCF_PublisherCC'];
423        keys.forEach(function (key) {
424            try { window.localStorage.removeItem(key); } catch (e) {}
425            try { window.sessionStorage.removeItem(key); } catch (e2) {}
426        });
427        tcfState = null;
428    }
429
430    function storeTcfState(state) {
431        if (!state || typeof state !== 'object') {
432            clearTcfStorage();
433            return;
434        }
435
436        setRawCookie('euconsent-v2', state.tcString, cfg.consentDuration || 13);
437        setRawCookie('addtl_consent', state.addtlConsent || '2~', cfg.consentDuration || 13);
438
439        try { window.localStorage.setItem('IABTCF_TCString', state.tcString); } catch (e3) {}
440        try { window.localStorage.setItem('IABTCF_AddtlConsent', state.addtlConsent || '2~'); } catch (e4) {}
441        try { window.localStorage.setItem('IABTCF_PurposeConsents', purposeConsentBitString(state.purposeConsents || {})); } catch (e5) {}
442        try { window.localStorage.setItem('IABTCF_VendorConsents', JSON.stringify(state.vendorConsents || {})); } catch (e6) {}
443        try { window.localStorage.setItem('IABTCF_PublisherCC', state.publisherCC || 'FR'); } catch (e7) {}
444    }
445
446    function getBaseTcfData() {
447        var hasConsent = !!getCookie(COOKIE_NAME);
448        var rawTc = getRawCookie('euconsent-v2');
449        var rawAc = getRawCookie('addtl_consent');
450        var policyVersion = parseInt(cfg.tcfPolicyVersion, 10) || 4;
451        var cmpId = parseInt(cfg.tcfCmpId, 10) || 0;
452        var cmpVersion = parseInt(cfg.tcfCmpVersion, 10) || 1;
453        var publisherCC = String(cfg.tcfPublisherCC || 'FR').toUpperCase().slice(0, 2) || 'FR';
454
455        if (tcfState && typeof tcfState === 'object') {
456            return {
457                tcString: tcfState.tcString || rawTc,
458                addtlConsent: tcfState.addtlConsent || rawAc || '2~',
459                cmpId: cmpId,
460                cmpVersion: cmpVersion,
461                tcfPolicyVersion: policyVersion,
462                gdprApplies: cfg.tcfGdprApplies !== false,
463                publisherCC: publisherCC,
464                purposeConsents: tcfState.purposeConsents || {},
465                vendorConsents: tcfState.vendorConsents || {},
466                disclosedVendors: tcfState.disclosedVendors || {}
467            };
468        }
469
470        return {
471            tcString: rawTc || '',
472            addtlConsent: rawAc || '2~',
473            cmpId: cmpId,
474            cmpVersion: cmpVersion,
475            tcfPolicyVersion: policyVersion,
476            gdprApplies: cfg.tcfGdprApplies !== false,
477            publisherCC: publisherCC,
478            purposeConsents: {},
479            vendorConsents: {},
480            disclosedVendors: {}
481        };
482    }
483
484    function buildTcfDataForCallback(eventStatus) {
485        var base = getBaseTcfData();
486        return {
487            tcString: base.tcString || '',
488            addtlConsent: base.addtlConsent || '2~',
489            eventStatus: eventStatus || (base.tcString ? 'tcloaded' : 'cmpuishown'),
490            cmpStatus: 'loaded',
491            listenerId: 0,
492            isServiceSpecific: true,
493            useNonStandardTexts: false,
494            gdprApplies: base.gdprApplies,
495            purposeOneTreatment: false,
496            publisherCC: base.publisherCC,
497            cmpId: base.cmpId,
498            cmpVersion: base.cmpVersion,
499            tcfPolicyVersion: base.tcfPolicyVersion,
500            purpose: { consents: base.purposeConsents, legitimateInterests: {} },
501            vendor: { consents: base.vendorConsents, legitimateInterests: {} },
502            specialFeatureOptins: {},
503            publisher: { consents: {}, legitimateInterests: {}, customPurpose: { consents: {}, legitimateInterests: {} } },
504            outOfBand: { allowedVendors: {}, disclosedVendors: base.disclosedVendors }
505        };
506    }
507
508    function emitTcfUpdate(eventStatus) {
509        if (!isTCFMode()) {
510            return;
511        }
512        var tcData = buildTcfDataForCallback(eventStatus || 'useractioncomplete');
513        Object.keys(tcfListeners).forEach(function (key) {
514            var entry = tcfListeners[key];
515            if (!entry || typeof entry.callback !== 'function') {
516                return;
517            }
518            var payload = Object.assign({}, tcData, { listenerId: entry.id });
519            try {
520                entry.callback(payload, true);
521            } catch (e) {
522                // Ignore listener errors from third-party scripts.
523            }
524        });
525    }
526
527    function updateTcfStateFromChoices(choices, eventStatus) {
528        if (!isTCFMode()) {
529            clearTcfStorage();
530            return;
531        }
532
533        var purposeConsents = buildPurposeConsents(choices);
534        var vendors = buildVendorConsents(choices);
535        var tcString = buildPseudoTcString(choices, purposeConsents, vendors.vendorConsents);
536        var addtlConsent = buildAdditionalConsentString(vendors.disclosedVendors, vendors.vendorConsents);
537
538        tcfState = {
539            tcString: tcString,
540            addtlConsent: addtlConsent,
541            purposeConsents: purposeConsents,
542            vendorConsents: vendors.vendorConsents,
543            disclosedVendors: vendors.disclosedVendors,
544            publisherCC: String(cfg.tcfPublisherCC || 'FR').toUpperCase().slice(0, 2) || 'FR'
545        };
546
547        storeTcfState(tcfState);
548        emitTcfUpdate(eventStatus || 'useractioncomplete');
549    }
550
551    function installTcfApi() {
552        if (!isTCFMode()) {
553            return;
554        }
555
556        var hasApi = typeof window.__tcfapi === 'function' && window.__tcfapi.__hermes === true;
557        if (hasApi) {
558            return;
559        }
560
561        // TCF locator iframe for cross-frame discovery.
562        if (!window.frames.__tcfapiLocator) {
563            try {
564                var iframe = document.createElement('iframe');
565                iframe.style.display = 'none';
566                iframe.name = '__tcfapiLocator';
567                document.body.appendChild(iframe);
568            } catch (e) {
569                // ignore
570            }
571        }
572
573        window.__tcfapi = function (command, version, callback, parameter) {
574            var cb = (typeof callback === 'function') ? callback : function () {};
575            var cmd = String(command || '');
576
577            if (cmd === 'ping') {
578                cb({
579                    gdprApplies: cfg.tcfGdprApplies !== false,
580                    cmpLoaded: true,
581                    cmpStatus: 'loaded',
582                    apiVersion: '2.2'
583                }, true);
584                return;
585            }
586
587            if (cmd === 'addEventListener') {
588                var id = tcfNextListenerId++;
589                tcfListeners[id] = { id: id, callback: cb };
590                var tcData = buildTcfDataForCallback(getCookie(COOKIE_NAME) ? 'tcloaded' : 'cmpuishown');
591                tcData.listenerId = id;
592                cb(tcData, true);
593                return;
594            }
595
596            if (cmd === 'removeEventListener') {
597                var listenerId = parseInt(parameter, 10);
598                if (listenerId && tcfListeners[listenerId]) {
599                    delete tcfListeners[listenerId];
600                    cb(true, true);
601                    return;
602                }
603                cb(false, false);
604                return;
605            }
606
607            if (cmd === 'getTCData') {
608                cb(buildTcfDataForCallback(getCookie(COOKIE_NAME) ? 'tcloaded' : 'cmpuishown'), true);
609                return;
610            }
611
612            if (cmd === 'displayConsentUi') {
613                openPreferences(true);
614                cb(true, true);
615                return;
616            }
617
618            cb(null, false);
619        };
620        window.__tcfapi.__hermes = true;
621
622        if (!tcfPostMessageBound) {
623            tcfPostMessageBound = true;
624            window.addEventListener('message', function (event) {
625                var data = event && event.data;
626                if (!data) {
627                    return;
628                }
629
630                var call = null;
631                if (typeof data === 'string') {
632                    try { data = JSON.parse(data); } catch (e) { data = null; }
633                }
634                if (data && data.__tcfapiCall) {
635                    call = data.__tcfapiCall;
636                }
637                if (!call) {
638                    return;
639                }
640
641                window.__tcfapi(call.command, call.version, function (returnValue, success) {
642                    var response = {
643                        __tcfapiReturn: {
644                            returnValue: returnValue,
645                            success: success,
646                            callId: call.callId
647                        }
648                    };
649                    try {
650                        event.source.postMessage(response, '*');
651                    } catch (e2) {
652                        // ignore
653                    }
654                }, call.parameter);
655            });
656        }
657    }
658
659    function b64UrlEncode(input) {
660        try {
661            return btoa(unescape(encodeURIComponent(input))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
662        } catch (e) {
663            return '';
664        }
665    }
666
667    function b64UrlDecode(input) {
668        if (!input) {
669            return '';
670        }
671        try {
672            var normalized = input.replace(/-/g, '+').replace(/_/g, '/');
673            var padding = normalized.length % 4;
674            if (padding) {
675                normalized += new Array(5 - padding).join('=');
676            }
677            return decodeURIComponent(escape(atob(normalized)));
678        } catch (e) {
679            return '';
680        }
681    }
682
683    function simpleHash(str) {
684        var h = 2166136261;
685        for (var i = 0; i < str.length; i++) {
686            h ^= str.charCodeAt(i);
687            h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
688        }
689        return (h >>> 0).toString(16);
690    }
691
692    function buildConsentTokenPayload(choices) {
693        return {
694            // UID de preuve (payload du cookie de consentement), pas l'UID
695            // d'audience : le token de sync transporte un consentement.
696            uid: getProofUid(getCookie(COOKIE_NAME) || {}),
697            ts: Date.now(),
698            v: String(cfg.consentVersion || '1'),
699            fp: String(cfg.consentFingerprint || ''),
700            choices: normalizeChoices(choices),
701            host: String(location.hostname || '')
702        };
703    }
704
705    function signConsentTokenPayload(payload) {
706        var secret = String(cfg.consentSyncToken || '');
707        if (!secret) {
708            return '';
709        }
710        var basis = [
711            String(payload.uid || ''),
712            String(payload.ts || ''),
713            String(payload.v || ''),
714            String(payload.fp || ''),
715            JSON.stringify(payload.choices || {}),
716            secret
717        ].join('|');
718        return simpleHash(basis);
719    }
720
721    function buildConsentSyncToken(choices) {
722        var payload = buildConsentTokenPayload(choices);
723        var sig = signConsentTokenPayload(payload);
724        if (sig) {
725            payload.sig = sig;
726        }
727        return b64UrlEncode(JSON.stringify(payload));
728    }
729
730    function parseConsentSyncToken(token) {
731        var decoded = b64UrlDecode(String(token || ''));
732        if (!decoded) {
733            return null;
734        }
735
736        var payload;
737        try {
738            payload = JSON.parse(decoded);
739        } catch (e) {
740            return null;
741        }
742
743        if (!payload || typeof payload !== 'object' || !payload.choices) {
744            return null;
745        }
746
747        var choices = normalizeChoices(payload.choices);
748        var ts = parseInt(payload.ts, 10) || 0;
749        var now = Date.now();
750        var maxAgeMs = 24 * 60 * 60 * 1000;
751        if (!ts || Math.abs(now - ts) > maxAgeMs) {
752            return null;
753        }
754
755        if (cfg.consentSyncToken) {
756            var expected = signConsentTokenPayload({
757                uid: payload.uid,
758                ts: ts,
759                v: payload.v,
760                fp: payload.fp,
761                choices: choices
762            });
763            if (!payload.sig || payload.sig !== expected) {
764                return null;
765            }
766        }
767
768        return {
769            uid: (typeof payload.uid === 'string' && payload.uid) ? payload.uid : getOrCreateUID(),
770            choices: choices,
771            version: String(payload.v || ''),
772            fingerprint: String(payload.fp || ''),
773            ts: ts
774        };
775    }
776
777    function buildConsentLink(url, choices) {
778        if (!url) {
779            return '';
780        }
781        var token = buildConsentSyncToken(choices || getCookie(COOKIE_NAME) || {});
782        if (!token) {
783            return url;
784        }
785
786        try {
787            var parsed = new URL(url, window.location.href);
788            if (parsed.protocol === 'mailto:' || parsed.protocol === 'tel:') {
789                return url;
790            }
791            parsed.searchParams.set(CONSENT_TOKEN_QUERY_KEY, token);
792            return parsed.toString();
793        } catch (e) {
794            return url;
795        }
796    }
797
798    function decorateSyncLinks() {
799        var links = document.querySelectorAll('a[data-hermes-consent-sync]');
800        if (!links.length) {
801            return;
802        }
803
804        var consent = getCookie(COOKIE_NAME) || {};
805        links.forEach(function (link) {
806            var href = link.getAttribute('href') || '';
807            if (!href || href.charAt(0) === '#') {
808                return;
809            }
810            link.setAttribute('href', buildConsentLink(href, consent));
811        });
812    }
813
814    function importConsentFromUrlToken() {
815        if (!window.URLSearchParams) {
816            return false;
817        }
818
819        var params = new URLSearchParams(window.location.search || '');
820        if (!params.has(CONSENT_TOKEN_QUERY_KEY)) {
821            return false;
822        }
823
824        var raw = params.get(CONSENT_TOKEN_QUERY_KEY);
825        var parsed = parseConsentSyncToken(raw);
826
827        // Clean URL in all cases.
828        params.delete(CONSENT_TOKEN_QUERY_KEY);
829        var cleanQuery = params.toString();
830        var cleanUrl = window.location.pathname + (cleanQuery ? '?' + cleanQuery : '') + window.location.hash;
831        if (window.history && typeof window.history.replaceState === 'function') {
832            window.history.replaceState({}, '', cleanUrl);
833        }
834
835        if (!parsed) {
836            return false;
837        }
838
839        var cookiePayload = buildCookiePayload(parsed.choices);
840        // Keep remote version/fingerprint when provided to avoid immediate re-prompt.
841        if (parsed.version) {
842            cookiePayload.__v = parsed.version;
843        }
844        if (parsed.fingerprint) {
845            cookiePayload.__fp = parsed.fingerprint;
846        }
847        // L'UID de preuve importé vit dans le payload du consentement — plus
848        // jamais dans hermes_uid (réservé à l'audience, découplage CNIL).
849        cookiePayload.uid = (typeof parsed.uid === 'string' && parsed.uid) ? parsed.uid : generateUID();
850
851        setCookie(COOKIE_NAME, cookiePayload, cfg.consentDuration || 13);
852
853        applyConsent(cookiePayload, null, { eventStatus: 'useractioncomplete' });
854        showBadge();
855
856        logConsent(cookiePayload.uid, cookiePayload, 'sync_import');
857        scheduleStayedPing(cookiePayload.uid);
858
859        document.dispatchEvent(new CustomEvent('hermes:consent', {
860            detail: { choices: cookiePayload, action: 'sync_import', source: 'token' }
861        }));
862
863        return true;
864    }
865
866    function clampRgbChannel(value) {
867        var channel = parseInt(value, 10);
868        if (isNaN(channel)) {
869            return 0;
870        }
871        return Math.max(0, Math.min(255, channel));
872    }
873
874    function colorToRgbString(colorValue, fallbackRgb) {
875        var value = String(colorValue || '').trim();
876        if (!value) {
877            return fallbackRgb;
878        }
879
880        var rgbMatch = value.match(/^rgba?\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})/i);
881        if (rgbMatch) {
882            return clampRgbChannel(rgbMatch[1]) + ', ' + clampRgbChannel(rgbMatch[2]) + ', ' + clampRgbChannel(rgbMatch[3]);
883        }
884
885        var hex = value.replace(/^#/, '');
886        if (/^[0-9a-f]{3}$/i.test(hex)) {
887            hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
888        }
889        if (!/^[0-9a-f]{6}$/i.test(hex)) {
890            return fallbackRgb;
891        }
892
893        return parseInt(hex.slice(0, 2), 16) + ', ' + parseInt(hex.slice(2, 4), 16) + ', ' + parseInt(hex.slice(4, 6), 16);
894    }
895
896    function applyStyles() {
897        var r = document.documentElement;
898        var primaryColor = cfg.primaryColor || '#000b19';
899        var secondaryColor = cfg.secondaryColor || '#e8e7ee';
900        var textColor = cfg.textColor || '#8f97a2';
901        var badgeColor = cfg.badgeColor || primaryColor;
902        var badgeBgColor = cfg.badgeBgColor || 'transparent';
903        var categoryBgColor = cfg.categoryBgColor || '#1a1a2e';
904
905        r.style.setProperty('--hermes-primary', primaryColor);
906        r.style.setProperty('--hermes-bg', primaryColor);
907        r.style.setProperty('--hermes-secondary', secondaryColor);
908        r.style.setProperty('--hermes-secondary-rgb', colorToRgbString(secondaryColor, '232, 231, 238'));
909        r.style.setProperty('--hermes-text', textColor);
910        r.style.setProperty('--hermes-text-rgb', colorToRgbString(textColor, '143, 151, 162'));
911        r.style.setProperty('--hermes-bg-category', categoryBgColor);
912        r.style.setProperty('--hermes-accept', cfg.acceptColor || '#d19adf');
913        r.style.setProperty('--hermes-accept-text', cfg.acceptTextColor || '#ffffff');
914        r.style.setProperty('--hermes-reject', cfg.rejectColor || '#1a1a2e');
915        r.style.setProperty('--hermes-reject-text', cfg.rejectTextColor || '#ffffff');
916        r.style.setProperty('--hermes-badge-color', badgeColor);
917        r.style.setProperty('--hermes-badge-bg', badgeBgColor);
918        r.style.setProperty('--hermes-radius', (cfg.borderRadius || 16) + 'px');
919        if (cfg.titleColor) {
920            r.style.setProperty('--hermes-title-color', cfg.titleColor);
921        }
922        if (cfg.descColor) {
923            r.style.setProperty('--hermes-desc-color', cfg.descColor);
924        }
925        if (cfg.titleSize) {
926            r.style.setProperty('--hermes-title-size', cfg.titleSize + 'rem');
927        }
928        if (cfg.descSize) {
929            r.style.setProperty('--hermes-desc-size', cfg.descSize + 'px');
930        }
931        if (cfg.customizeColor) {
932            r.style.setProperty('--hermes-customize-color', cfg.customizeColor);
933        }
934        if (cfg.customizeTextColor) {
935            r.style.setProperty('--hermes-customize-text', cfg.customizeTextColor);
936        }
937        if (cfg.categoryTitleColor) {
938            r.style.setProperty('--hermes-category-title-color', cfg.categoryTitleColor);
939        }
940        if (cfg.categoryDescColor) {
941            r.style.setProperty('--hermes-category-desc-color', cfg.categoryDescColor);
942        }
943        if (cfg.categoryTitleSize) {
944            r.style.setProperty('--hermes-category-title-size', cfg.categoryTitleSize + 'px');
945        }
946        if (cfg.categoryDescSize) {
947            r.style.setProperty('--hermes-category-desc-size', cfg.categoryDescSize + 'px');
948        }
949        if (cfg.saveColor) {
950            r.style.setProperty('--hermes-save-color', cfg.saveColor);
951        }
952        if (cfg.saveTextColor) {
953            r.style.setProperty('--hermes-save-text', cfg.saveTextColor);
954        }
955    }
956
957    function showModal() {
958        var overlay = $('#hermes-overlay');
959        if (!overlay) {
960            console.warn('[Hermes] Modal overlay #hermes-overlay not found in DOM');
961            return;
962        }
963        trackEvent('widget_shown');
964        overlay.style.display = 'flex';
965        requestAnimationFrame(function () {
966            overlay.classList.add('hermes-visible');
967        });
968        document.body.style.overflow = 'hidden';
969        showView('main');
970        var bannerVideo = document.querySelector('#hermes-modal .hermes-banner-media video');
971        if (bannerVideo) {
972            bannerVideo.loop = false;
973            if (!bannerVideo.dataset.hermesInteractiveBound) {
974                bannerVideo.dataset.hermesInteractiveBound = '1';
975                bannerVideo.addEventListener('ended', function () {
976                    bannerVideo.pause();
977                });
978                bannerVideo.addEventListener('click', function (e) {
979                    e.preventDefault();
980                    if (bannerVideo.ended) {
981                        bannerVideo.currentTime = 0;
982                        var replay = bannerVideo.play();
983                        if (replay && typeof replay.catch === 'function') {
984                            replay.catch(function () {});
985                        }
986                        return;
987                    }
988                    if (bannerVideo.paused) {
989                        var playPromise = bannerVideo.play();
990                        if (playPromise && typeof playPromise.catch === 'function') {
991                            playPromise.catch(function () {});
992                        }
993                    } else {
994                        bannerVideo.pause();
995                    }
996                });
997            }
998            if (!bannerVideoAutoPlayed) {
999                bannerVideoAutoPlayed = true;
1000                bannerVideo.currentTime = 0;
1001                var autoPlay = bannerVideo.play();
1002                if (autoPlay && typeof autoPlay.catch === 'function') {
1003                    autoPlay.catch(function () {});
1004                }
1005            }
1006        }
1007        var embedOverlay = document.querySelector('#hermes-modal .hermes-embed-overlay');
1008        var embedIframe  = document.querySelector('#hermes-modal .hermes-embed-iframe');
1009        if (embedOverlay && embedIframe) {
1010            var embedType = embedIframe.dataset.embedType || '';
1011            if (!embedOverlay.dataset.hermesEmbedBound) {
1012                embedOverlay.dataset.hermesEmbedBound = '1';
1013                embedIframe.dataset.hermesEmbedPlaying = '1';
1014                embedOverlay.addEventListener('click', function () {
1015                    var playing = embedIframe.dataset.hermesEmbedPlaying === '1';
1016                    sendEmbedCommand(embedIframe, embedType, playing ? 'pause' : 'play');
1017                    embedIframe.dataset.hermesEmbedPlaying = playing ? '0' : '1';
1018                });
1019            }
1020        }
1021        trapFocus(overlay);
1022    }
1023
1024    function hideModal() {
1025        var overlay = $('#hermes-overlay');
1026        if (!overlay) {
1027            return;
1028        }
1029        overlay.classList.remove('hermes-visible');
1030        setTimeout(function () {
1031            overlay.style.display = 'none';
1032        }, 300);
1033        document.body.style.overflow = '';
1034    }
1035
1036    function showView(v) {
1037        var main = $('#hermes-view-main');
1038        var det = $('#hermes-view-details');
1039        var banner = document.querySelector('.hermes-banner-media');
1040        if (v === 'details') {
1041            if (main) {
1042                main.style.display = 'none';
1043            }
1044            if (det) {
1045                det.style.display = 'block';
1046            }
1047            if (banner) {
1048                banner.style.display = 'none';
1049            }
1050        } else {
1051            if (main) {
1052                main.style.display = 'block';
1053            }
1054            if (det) {
1055                det.style.display = 'none';
1056            }
1057            if (banner) {
1058                banner.style.display = '';
1059            }
1060        }
1061    }
1062
1063    function showBadge() {
1064        var b = $('#hermes-badge');
1065        if (b && cfg.showBadge) {
1066            b.style.display = 'flex';
1067        }
1068    }
1069
1070    function hideBadge() {
1071        var b = $('#hermes-badge');
1072        if (b) {
1073            b.style.display = 'none';
1074        }
1075    }
1076
1077    function trapFocus(el) {
1078        var foc = el.querySelectorAll('button:not([disabled]),[href],input:not([disabled]),select,textarea,[tabindex]:not
1078([tabindex="-1"])');
1079        if (!foc.length) {
1080            return;
1081        }
1082        var first = foc[0];
1083        var last = foc[foc.length - 1];
1084        first.classList.add('hermes-no-ring');
1085        first.focus();
1086        el.addEventListener('keydown', function () {
1087            var nr = el.querySelector('.hermes-no-ring');
1088            if (nr) { nr.classList.remove('hermes-no-ring'); }
1089        }, { once: true });
1090        el.addEventListener('keydown', function (e) {
1091            if (e.key !== 'Tab') {
1092                return;
1093            }
1094            if (e.shiftKey) {
1095                if (document.activeElement === first) {
1096                    e.preventDefault();
1097                    last.focus();
1098                }
1099            } else if (document.activeElement === last) {
1100                e.preventDefault();
1101                first.focus();
1102            }
1103        });
1104    }
1105
1106    function getChoicesFromToggles() {
1107        var choices = { essential: true };
1108        $$('#hermes-view-details input[data-category]').forEach(function (inp) {
1109            var c = inp.getAttribute('data-category');
1110            if (c !== 'essential') {
1111                choices[c] = inp.checked;
1112            }
1113        });
1114        return normalizeChoices(choices);
1115    }
1116
1117    function setTogglesFromChoices(choices) {
1118        var normalized = normalizeChoices(choices);
1119        $$('#hermes-view-details input[data-category]').forEach(function (inp) {
1120            var c = inp.getAttribute('data-category');
1121            if (c !== 'essential' && Object.prototype.hasOwnProperty.call(normalized, c)) {
1122                inp.checked = !!normalized[c];
1123            }
1124        });
1125    }
1126
1127    function isProtectedCookie(name) {
1128        for (var i = 0; i < PROTECTED_COOKIE_PATTERNS.length; i++) {
1129            if (PROTECTED_COOKIE_PATTERNS[i].test(name)) {
1130                return true;
1131            }
1132        }
1133        return false;
1134    }
1135
1136    function matchesAnyPattern(name, patterns) {
1137        if (!patterns || !patterns.length) {
1138            return false;
1139        }
1140        for (var i = 0; i < patterns.length; i++) {
1141            if (patterns[i].test(name)) {
1142                return true;
1143            }
1144        }
1145        return false;
1146    }
1147
1148    function cleanCategoryCookies(category) {
1149        var patterns = COOKIE_PATTERNS_BY_CATEGORY[category] || [];
1150        if (!patterns.length) {
1151            return;
1152        }
1153
1154        document.cookie.split(';').forEach(function (chunk) {
1155            var eq = chunk.indexOf('=');
1156            var name = (eq >= 0 ? chunk.slice(0, eq) : chunk).trim();
1157            if (!name || isProtectedCookie(name)) {
1158                return;
1159            }
1160            if (matchesAnyPattern(name, patterns)) {
1161                deleteCookie(name);
1162            }
1163        });
1164    }
1165
1166    function cleanCategoryStorage(category) {
1167        var patterns = STORAGE_PATTERNS_BY_CATEGORY[category] || [];
1168        if (!patterns.length) {
1169            return;
1170        }
1171
1172        function cleanStore(store) {
1173            if (!store || typeof store.length !== 'number') {
1174                return;
1175            }
1176            for (var i = store.length - 1; i >= 0; i--) {
1177                var key;
1178                try {
1179                    key = store.key(i);
1180                } catch (e) {
1181                    key = null;
1182                }
1183                if (!key) {
1184                    continue;
1185                }
1186                if (matchesAnyPattern(key, patterns)) {
1187                    try {
1188                        store.removeItem(key);
1189                    } catch (e2) {
1190                        // ignore storage access errors
1191                    }
1192                }
1193            }
1194        }
1195
1196        try {
1197            cleanStore(window.localStorage);
1198        } catch (e3) {
1199            // ignore
1200        }
1201        try {
1202            cleanStore(window.sessionStorage);
1203        } catch (e4) {
1204            // ignore
1205        }
1206    }
1207
1208    function cleanCategoryData(category) {
1209        cleanCategoryCookies(category);
1210        cleanCategoryStorage(category);
1211    }
1212
1213    function cleanRevokedConsentData(previousChoices, nextChoices) {
1214        var prev = normalizeChoices(previousChoices || {});
1215        var next = normalizeChoices(nextChoices || {});
1216
1217        CATEGORY_KEYS.forEach(function (cat) {
1218            // Clean when category is now denied, including stale leftovers.
1219            if (!next[cat]) {
1220                cleanCategoryData(cat);
1221                return;
1222            }
1223
1224            // Clean if explicitly revoked.
1225            if (prev[cat] && !next[cat]) {
1226                cleanCategoryData(cat);
1227            }
1228        });
1229    }
1230
1231    function cleanTrackingCookies() {
1232        CATEGORY_KEYS.forEach(function (cat) {
1233            cleanCategoryData(cat);
1234        });
1235    }
1236
1237    function acceptAll() {
1238        var ch = { essential: true };
1239        if (cfg.categories.analytics) {
1240            ch.analytics = true;
1241        }
1242        if (cfg.categories.marketing) {
1243            ch.marketing = true;
1244        }
1245        if (cfg.categories.functional) {
1246            ch.functional = true;
1247        }
1248        saveConsent(ch, 'accept_all');
1249    }
1250
1251    function rejectAll() {
1252        var ch = { essential: true };
1253        if (cfg.categories.analytics) {
1254            ch.analytics = false;
1255        }
1256        if (cfg.categories.marketing) {
1257            ch.marketing = false;
1258        }
1259        if (cfg.categories.functional) {
1260            ch.functional = false;
1261        }
1262        saveConsent(ch, 'reject_all');
1263    }
1264
1265    function saveCustom() {
1266        saveConsent(getChoicesFromToggles(), 'custom');
1267    }
1268
1269    function saveConsent(choices, actionType) {
1270        var previous = getCookie(COOKIE_NAME) || {};
1271        // UID de preuve : réutilisé d'un choix à l'autre (historique cohérent),
1272        // stocké dans le cookie de consentement lui-même — jamais hermes_uid.
1273        var uid = getProofUid(previous);
1274        var payload = buildCookiePayload(choices);
1275        payload.uid = uid;
1276
1277        setCookie(COOKIE_NAME, payload, cfg.consentDuration || 13);
1278        applyConsent(payload, previous, { eventStatus: 'useractioncomplete' });
1279        logConsent(uid, payload, actionType);
1280        scheduleStayedPing(uid);
1281        hideModal();
1282        showBadge();
1283        decorateSyncLinks();
1284        renderContextualBlocks(payload);
1285
1286        document.dispatchEvent(new CustomEvent('hermes:consent', {
1287            detail: { choices: payload, action: actionType }
1288        }));
1289    }
1290
1291    function applyConsent(choices, previousChoices, meta) {
1292        var normalized = normalizeChoices(choices || {});
1293
1294        // Push consent state for tag managers.
1295        window.dataLayer = window.dataLayer || [];
1296        window.dataLayer.push({
1297            event: 'hermes_consent_update',
1298            hermes_consent: {
1299                analytics: normalized.analytics ? true : false,
1300                marketing: normalized.marketing ? true : false,
1301                functional: normalized.functional ? true : false
1302            },
1303            hermes_analytics: normalized.analytics ? 'granted' : 'denied',
1304            hermes_marketing: normalized.marketing ? 'granted' : 'denied',
1305            hermes_functional: normalized.functional ? 'granted' : 'denied'
1306        });
1307
1308        if (typeof gtag === 'function') {
1309            gtag('consent', 'update', {
1310                analytics_storage: normalized.analytics ? 'granted' : 'denied',
1311                ad_storage: normalized.marketing ? 'granted' : 'denied',
1312                ad_user_data: normalized.marketing ? 'granted' : 'denied',
1313                ad_personalization: normalized.marketing ? 'granted' : 'denied',
1314                functionality_storage: normalized.functional ? 'granted' : 'denied',
1315                personalization_storage: normalized.functional ? 'granted' : 'denied'
1316            });
1317        }
1318
1319        // Réactive les scripts bloqués (text/plain) quel que soit le mode de blocage.
1320        // En Consent Mode, seuls les vendors non-Google sont en text/plain (audit RGPD) ;
1321        // GA4/GTM/Ads (sans data-hermes-category) et Matomo (requireConsent) ne sont pas touchés.
1322        activateBlockedScripts(normalized);
1323
1324        if (window._paq) {
1325            if (normalized.analytics) {
1326                window._paq.push(['setConsentGiven']);
1327            } else {
1328                window._paq.push(['forgetConsentGiven']);
1329            }
1330        }
1331
1332        if (isTCFMode()) {
1333            updateTcfStateFromChoices(normalized, meta && meta.eventStatus ? meta.eventStatus : 'useractioncomplete');
1334        } else {
1335            clearTcfStorage();
1336        }
1337
1338        cleanRevokedConsentData(previousChoices || {}, normalized);
1339    }
1340
1341    function activateBlockedScripts(choices) {
1342        $$('script[data-hermes-category]').forEach(function (script) {
1343            var cat = script.getAttribute('data-hermes-category');
1344            if (!choices[cat]) {
1345                return;
1346            }
1347            var ns = document.createElement('script');
1348            ns.type = 'text/javascript';
1349            if (script.textContent) {
1350                ns.textContent = script.textContent;
1351            }
1352            var src = script.getAttribute('data-hermes-src');
1353            if (src) {
1354                ns.src = src;
1355                ns.async = true;
1356            }
1357            script.parentNode.replaceChild(ns, script);
1358        });
1359    }
1360
1361    function logConsent(uid, choices, actionType) {
1362        var payload = {
1363            consent_uid: uid,
1364            categories: choices,
1365            page_url: window.location.href,
1366            action_type: actionType || 'initial',
1367            language: cfg.language || '',
1368            consent_version: String(cfg.consentVersion || '1')
1369        };
1370
1371        if (cfg.restUrl) {
1372            fetch(cfg.restUrl + 'consent', {
1373                method: 'POST',
1374                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.restNonce || '' },
1375                body: JSON.stringify(payload),
1376                credentials: 'same-origin'
1377            }).catch(function () {
1378                ajaxLogConsent(payload);
1379            });
1380        } else {
1381            ajaxLogConsent(payload);
1382        }
1383    }
1384
1385    function ajaxLogConsent(payload) {
1386        var fd = new FormData();
1387        fd.append('action', 'hermes_log_consent');
1388        fd.append('nonce', cfg.nonce);
1389        fd.append('consent_uid', payload.consent_uid);
1390        fd.append('categories', JSON.stringify(payload.categories));
1391        fd.append('page_url', payload.page_url);
1392        fd.append('action_type', payload.action_type);
1393        fd.append('language', payload.language);
1394        fd.append('consent_version', payload.consent_version);
1395
1396        fetch(cfg.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' })
1397            .catch(function (e) { console.warn('Hermes: log failed', e); });
1398    }
1399
1400    function scheduleStayedPing(uid) {
1401        // Ping « resté 30 s » = mesure d'audience (taux de rebond) : gaté sur
1402        // le réglage stats, comme trackEvent.
1403        if (!statsTrackingEnabled()) {
1404            return;
1405        }
1406        var pinged = false;
1407        var pingStart = Date.now();
1408
1409        function doAjaxPing() {
1410            var fd = new FormData();
1411            fd.append('action', 'hermes_stayed_ping');
1412            fd.append('nonce', cfg.nonce);
1413            fd.append('consent_uid', uid);
1414            return fetch(cfg.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' });
1415        }
1416
1417        function doBeaconPing() {
1418            if (navigator.sendBeacon) {
1419                var fd = new FormData();
1420                fd.append('action', 'hermes_stayed_ping');
1421                fd.append('nonce', cfg.nonce);
1422                fd.append('consent_uid', uid);
1423                navigator.sendBeacon(cfg.ajaxUrl, fd);
1424            }
1425        }
1426
1427        function doPing() {
1428            if (pinged) {
1429                return;
1430            }
1431            pinged = true;
1432
1433            if (cfg.restUrl) {
1434                fetch(cfg.restUrl + 'stayed', {
1435                    method: 'POST',
1436                    headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.restNonce || '' },
1437                    body: JSON.stringify({ consent_uid: uid }),
1438                    credentials: 'same-origin'
1439                }).then(function (r) {
1440                    if (!r.ok) {
1441                        throw new Error('REST ' + r.status);
1442                    }
1443                }).catch(function () {
1444                    doAjaxPing().catch(function () {
1445                        doBeaconPing();
1446                    });
1447                });
1448            } else {
1449                doAjaxPing().catch(function () {
1450                    doBeaconPing();
1451                });
1452            }
1453        }
1454
1455        setTimeout(doPing, 5000);
1456
1457        function onLeave() {
1458            if (pinged) {
1459                return;
1460            }
1461            if ((Date.now() - pingStart) >= 5000) {
1462                pinged = true;
1463                doBeaconPing();
1464            }
1465        }
1466
1467        document.addEventListener('visibilitychange', function () {
1468            if (document.visibilityState === 'hidden') {
1469                onLeave();
1470            }
1471        });
1472        window.addEventListener('pagehide', onLeave);
1473    }
1474
1475    var _tracked = {};
1476    function trackEvent(type) {
1477        // Mesure d'audience maison (exemption CNIL sous conditions) :
1478        // désactivable via le réglage « Statistiques Hermès ». Quand OFF,
1479        // aucun cookie hermes_uid n'est posé et aucun événement n'est envoyé.
1480        if (!statsTrackingEnabled()) {
1481            return;
1482        }
1483        if (_tracked[type]) {
1484            return;
1485        }
1486        _tracked[type] = true;
1487        var uid = getOrCreateUID();
1488        var payload = { event_type: type, visitor_uid: uid, page_url: window.location.pathname };
1489        if (cfg.restUrl) {
1490            fetch(cfg.restUrl + 'event', {
1491                method: 'POST',
1492                headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.restNonce },
1493                body: JSON.stringify(payload),
1494                credentials: 'same-origin'
1495            }).catch(function () {
1496                var fd = new FormData();
1497                fd.append('action', 'hermes_track_event');
1498                fd.append('nonce', cfg.nonce);
1499                fd.append('event_type', type);
1500                fd.append('visitor_uid', uid);
1501                fd.append('page_url', window.location.pathname);
1502                fetch(cfg.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' }).catch(function () {});
1503            });
1504        }
1505    }
1506
1507    function openPreferences(showDetails) {
1508        var ex = getCookie(COOKIE_NAME);
1509        if (ex) {
1510            setTogglesFromChoices(ex);
1511        }
1512        showModal();
1513        if (showDetails) {
1514            showView('details');
1515        }
1516        hideBadge();
1517    }
1518
1519    function allowCategoryFromContext(category) {
1520        var ex = getCookie(COOKIE_NAME) || {};
1521        var next = normalizeChoices(ex);
1522        next[category] = true;
1523        saveConsent(next, 'contextual_allow');
1524    }
1525
1526    function renderContextualBlocks(consentChoices) {
1527        var consent = normalizeChoices(consentChoices || getCookie(COOKIE_NAME) || {});
1528        var blocks = $$('[data-hermes-consent-block]');
1529        if (!blocks.length) {
1530            return;
1531        }
1532
1533        blocks.forEach(function (el) {
1534            var category = String(el.getAttribute('data-hermes-consent-block') || '').toLowerCase();
1535            if (CATEGORY_KEYS.indexOf(category) === -1) {
1536                return;
1537            }
1538
1539            var categoryEnabled = !(cfg.categories && cfg.categories[category] === false);
1540            var allowed = categoryEnabled && !!consent[category];
1541
1542            var oldPlaceholder = el.querySelector('.hermes-contextual-placeholder');
1543            if (allowed || !categoryEnabled) {
1544                el.classList.remove('hermes-contextual-locked');
1545                if (oldPlaceholder) {
1546                    oldPlaceholder.remove();
1547                }
1548                return;
1549            }
1550
1551            el.classList.add('hermes-contextual-locked');
1552            if (oldPlaceholder) {
1553                return;
1554            }
1555
1556            var title = el.getAttribute('data-hermes-consent-title') || 'Contenu soumis a consentement';
1557            var desc = el.getAttribute('data-hermes-consent-description') || ('Activez les cookies ' + (CATEGORY_LABELS[category] || category) + ' pour afficher ce contenu.');
1558            var cta = el.getAttribute('data-hermes-consent-cta') || 'Autoriser et afficher';
1559            var action = String(el.getAttribute('data-hermes-consent-action') || 'allow').toLowerCase();
1560
1561            var placeholder = document.createElement('div');
1562            placeholder.className = 'hermes-contextual-placeholder';
1563
1564            var titleEl = document.createElement('strong');
1565            titleEl.className = 'hermes-contextual-title';
1566            titleEl.textContent = title;
1567
1568            var descEl = document.createElement('p');
1569            descEl.className = 'hermes-contextual-desc';
1570            descEl.textContent = desc;
1571
1572            var button = document.createElement('button');
1573            button.type = 'button';
1574            button.className = 'hermes-contextual-btn';
1575            button.textContent = cta;
1576            button.addEventListener('click', function (e) {
1577                e.preventDefault();
1578                e.stopPropagation();
1579                if (action === 'details') {
1580                    openPreferences(true);
1581                } else {
1582                    allowCategoryFromContext(category);
1583                }
1584            });
1585
1586            placeholder.appendChild(titleEl);
1587            placeholder.appendChild(descEl);
1588            placeholder.appendChild(button);
1589
1590            el.appendChild(placeholder);
1591        });
1592    }
1593
1594    function init() {
1595        applyStyles();
1596        if (statsTrackingEnabled()) {
1597            trackEvent('pageview');
1598        } else {
1599            // Stats désactivées : purge du cookie d'audience résiduel chez les
1600            // visiteurs qui l'avaient reçu quand la mesure était active.
1601            deleteCookie(UID_COOKIE);
1602        }
1603        installTcfApi();
1604
1605        importConsentFromUrlToken();
1606
1607        var existing = getCookie(COOKIE_NAME);
1608        var delay = Math.max(0, (parseInt(cfg.popupDelay, 10) || 0)) * 1000;
1609        if (delay < 500) {
1610            delay = 500;
1611        }
1612
1613        if (existing && typeof existing === 'object' && isConsentOutdated(existing)) {
1614            cleanTrackingCookies();
1615            deleteCookie(COOKIE_NAME);
1616            clearTcfStorage();
1617            existing = null;
1618        }
1619
1620        if (existing && typeof existing === 'object') {
1621            window.dataLayer = window.dataLayer || [];
1622            window.dataLayer.push({
1623                event: 'hermes_consent_ready',
1624                hermes_consent: {
1625                    analytics: existing.analytics ? true : false,
1626                    marketing: existing.marketing ? true : false,
1627                    functional: existing.functional ? true : false
1628                },
1629                hermes_analytics: existing.analytics ? 'granted' : 'denied',
1630                hermes_marketing: existing.marketing ? 'granted' : 'denied',
1631                hermes_functional: existing.functional ? 'granted' : 'denied'
1632            });
1633            applyConsent(existing, null, { eventStatus: 'tcloaded' });
1634            showBadge();
1635        } else {
1636            if (isTCFMode()) clearTcfStorage();
1637            setTimeout(showModal, delay);
1638        }
1639
1640        bindEvents();
1641        decorateSyncLinks();
1642        renderContextualBlocks(existing || {});
1643
1644        document.addEventListener('hermes:consent', function (event) {
1645            renderContextualBlocks(event && event.detail ? event.detail.choices : (getCookie(COOKIE_NAME) || {}));
1646            decorateSyncLinks();
1647        });
1648    }
1649
1650    function bindEvents() {
1651        var a1 = $('#hermes-accept-all');
1652        if (a1) {
1653            a1.addEventListener('click', acceptAll);
1654        }
1655
1656        var a2 = $('#hermes-accept-all-detail');
1657        if (a2) {
1658            a2.addEventListener('click', acceptAll);
1659        }
1660
1661        var rj = $('#hermes-reject-all');
1662        if (rj) {
1663            rj.addEventListener('click', rejectAll);
1664        }
1665
1666        var cu = $('#hermes-show-details');
1667        if (cu) {
1668            cu.addEventListener('click', function () { showView('details'); });
1669        }
1670
1671        var bk = $('#hermes-back-main');
1672        if (bk) {
1673            bk.addEventListener('click', function () { showView('main'); });
1674        }
1675
1676        var sv = $('#hermes-save-choices');
1677        if (sv) {
1678            sv.addEventListener('click', saveCustom);
1679        }
1680
1681        var bd = $('#hermes-badge');
1682        if (bd) {
1683            bd.addEventListener('click', function () {
1684                var ex = getCookie(COOKIE_NAME);
1685                if (ex) {
1686                    setTogglesFromChoices(ex);
1687                }
1688                showModal();
1689                showView('details');
1690                hideBadge();
1691            });
1692        }
1693
1694        document.querySelectorAll('.hermes-open-preferences').forEach(function (el) {
1695            el.addEventListener('click', function (e) {
1696                e.preventDefault();
1697                openPreferences(true);
1698            });
1699        });
1700
1701        var muteBtn = document.querySelector('#hermes-modal .hermes-video-mute');
1702        if (muteBtn) {
1703            muteBtn.addEventListener('click', function (e) {
1704                e.stopPropagation();
1705                var iconMuted   = muteBtn.querySelector('.hermes-icon-muted');
1706                var iconUnmuted = muteBtn.querySelector('.hermes-icon-unmuted');
1707                var iframe = document.querySelector('#hermes-modal .hermes-embed-iframe');
1708                if (iframe) {
1709                    var type   = iframe.dataset.embedType || '';
1710                    var muted  = iframe.dataset.hermesEmbedMuted !== '0';
1711                    sendEmbedCommand(iframe, type, muted ? 'unmute' : 'mute');
1712                    iframe.dataset.hermesEmbedMuted = muted ? '0' : '1';
1713                    iconMuted.style.display   = muted ? 'none' : '';
1714                    iconUnmuted.style.display = muted ? '' : 'none';
1715                    muteBtn.title = muted
1716                        ? (cfg.language === 'en' ? 'Mute' : 'Couper le son')
1717                        : (cfg.language === 'en' ? 'Unmute' : 'Activer le son');
1718                    return;
1719                }
1720                var video = document.querySelector('#hermes-modal .hermes-banner-media video');
1721                if (!video) return;
1722                video.muted = !video.muted;
1723                if (video.muted) {
1724                    iconMuted.style.display = '';
1725                    iconUnmuted.style.display = 'none';
1726                    muteBtn.title = cfg.language === 'en' ? 'Unmute' : 'Activer le son';
1727                } else {
1728                    iconMuted.style.display = 'none';
1729                    iconUnmuted.style.display = '';
1730                    muteBtn.title = cfg.language === 'en' ? 'Mute' : 'Couper le son';
1731                }
1732            });
1733        }
1734
1735        document.addEventListener('keydown', function (e) {
1736            if (e.key === 'Escape') {
1737                var ov = $('#hermes-overlay');
1738                if (ov && ov.classList.contains('hermes-visible') && getCookie(COOKIE_NAME)) {
1739                    hideModal();
1740                    showBadge();
1741                }
1742            }
1743        });
1744
1745        var ov = $('#hermes-overlay');
1746        if (ov) {
1747            ov.addEventListener('click', function (e) {
1748                if (e.target === ov && getCookie(COOKIE_NAME)) {
1749                    hideModal();
1750                    showBadge();
1751                }
1752            });
1753        }
1754    }
1755
1756    if (document.readyState === 'loading') {
1757        document.addEventListener('DOMContentLoaded', init);
1758    } else {
1759        init();
1760    }
1761
1762    window.Hermes = {
1763        show: showModal,
1764        hide: hideModal,
1765        acceptAll: acceptAll,
1766        rejectAll: rejectAll,
1767        getConsent: function () { return getCookie(COOKIE_NAME); },
1768        hasConsent: function (cat) { var c = getCookie(COOKIE_NAME); return c ? !!c[cat] : false; },
1769        getLanguage: function () { return cfg.language || ''; },
1770        onConsent: function (cb) { document.addEventListener('hermes:consent', function (e) { cb(e.detail); }); },
1771        resetConsent: function () {
1772            deleteCookie(COOKIE_NAME);
1773            deleteCookie(UID_COOKIE);
1774            clearTcfStorage();
1775            cleanTrackingCookies();
1776            location.reload();
1777        },
1778        getTCData: function () { return buildTcfDataForCallback(getCookie(COOKIE_NAME) ? 'tcloaded' : 'cmpuishown'); },
1779        getTCString: function () { return getRawCookie('euconsent-v2'); },
1780        buildSyncToken: function (choices) { return buildConsentSyncToken(choices || getCookie(COOKIE_NAME) || {}); },
1781        buildConsentLink: function (url, choices) { return buildConsentLink(url, choices || getCookie(COOKIE_NAME) || {}); },
1782        openPreferences: function () { openPreferences(true); }
1783    };
1784})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.