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.