1// cookieconsent-config.js 2 3import * as CookieConsent from "/assets/js/cookieconsent.esm.js"; 4 5/** 6 * Per-site URLs - customize before deploy (see README section 10). 7 */ 8const SITE_CONFIG = { 9 privacyPolicyUrl: "/privacy-policy", 10 contactUrl: "https://www.etxluxurylimos.com/contact-us/", 11}; 12 13const CAT_NECESSARY = "necessary"; 14const CAT_ANALYTICS = "analytics"; 15const CAT_ADVERTISEMENT = "advertisement"; 16const CAT_FUNCTIONALITY = "functionality"; 17const CAT_SECURITY = "security"; 18 19const SERVICE_AD_STORAGE = "ad_storage"; 20const SERVICE_AD_USER_DATA = "ad_user_data"; 21const SERVICE_AD_PERSONALIZATION = "ad_personalization"; 22const SERVICE_ANALYTICS_STORAGE = "analytics_storage"; 23const SERVICE_FUNCTIONALITY_STORAGE = "functionality_storage"; 24const SERVICE_PERSONALIZATION_STORAGE = "personalization_storage"; 25const SERVICE_SECURITY_STORAGE = "security_storage"; 26 27const im = iframemanager(); 28 29/** 30 * Convert legacy embed iframes pasted in CMS content to IframeManager placeholders. 31 */ 32/** 33 * Shared embed runtime helpers for IframeManager placeholder conversion. 34 * Included by the consent config module and the head embed guard. 35 */ 36function getYoutubeVideoId(url) { 37 if (!url) { 38 return null; 39 } 40 41 try { 42 const parsed = new URL(url, window.location.origin); 43 const host = parsed.hostname.replace(/^www\./, ""); 44 45 if (host === "youtu.be") { 46 return parsed.pathname.slice(1).split("/")[0] || null; 47 } 48 49 if (host.includes("youtube.com") || host.includes("youtube-nocookie.com")) { 50 if (parsed.pathname.startsWith("/embed/")) { 51 return parsed.pathname.split("/")[2] || null; 52 } 53 54 return parsed.searchParams.get("v"); 55 } 56 } catch (error) { 57 return null; 58 } 59 60 return null; 61} 62 63function getVimeoVideoId(url) { 64 if (!url) { 65 return null; 66 } 67 68 try { 69 const parsed = new URL(url, window.location.origin); 70 const host = parsed.hostname.replace(/^www\./, ""); 71 72 if (host === "player.vimeo.com" && parsed.pathname.startsWith("/video/")) { 73 return parsed.pathname.split("/")[2] || null; 74 } 75 76 if (host === "vimeo.com") { 77 return parsed.pathname.split("/").filter(Boolean)[0] || null; 78 } 79 } catch (error) { 80 return null; 81 } 82 83 return null; 84} 85 86function getGoogleMapsEmbedId(url) { 87 if (!url) { 88 return null; 89 } 90 91 try { 92 const parsed = new URL(url, window.location.origin); 93 const host = parsed.hostname.replace(/^www\./, ""); 94 if (host === "google.com" && parsed.pathname.startsWith("/maps/")) { 95 const suffix = `${parsed.pathname.slice("/maps/".length)}${parsed.search}`.replace(/^\//, ""); 96 return suffix || null; 97 } 98 if (host === "maps.google.com") { 99 const suffix = `${parsed.pathname.replace(/^\//, "")}${parsed.search}`; 100 return suffix || null; 101 } 102 } catch (error) { 103 return null; 104 } 105 106 return null; 107} 108 109function getSpotifyEmbedId(url) { 110 if (!url) { 111 return null; 112 } 113 114 try { 115 const parsed = new URL(url, window.location.origin); 116 const host = parsed.hostname.replace(/^www\./, ""); 117 118 if (!host.includes("spotify.com") || !parsed.pathname.includes("/embed/")) { 119 return null; 120 } 121 122 const suffix = `${parsed.pathname.split("/embed/")[1] || ""}${parsed.search}`.replace(/^\//, ""); 123 return suffix || null; 124 } catch (error) { 125 return null; 126 } 127} 128 129function getSoundCloudEmbedId(url) { 130 if (!url) { 131 return null; 132 } 133 134 try { 135 const parsed = new URL(url, window.location.origin); 136 const host = parsed.hostname.replace(/^www\./, ""); 137 138 if (!host.includes("soundcloud.com") || !parsed.pathname.includes("/player")) { 139 return null; 140 } 141 142 return parsed.search ? parsed.search.slice(1) : null; 143 } catch (error) { 144 return null; 145 } 146} 147 148function getInvestingWidgetEmbedId(url) { 149 if (!url) { 150 return null; 151 } 152 153 try { 154 const parsed = new URL(url, window.location.origin); 155 const host = parsed.hostname.replace(/^www\./, ""); 156 157 if (!/(?:^|\.)forexprostools\.com$|(?:^|\.)investing\.com$/i.test(host)) { 158 return null; 159 } 160 161 return parsed.href; 162 } catch (error) { 163 return null; 164 } 165} 166 167function getFacebookEmbedId(url) { 168 if (!url) { 169 return null; 170 } 171 172 try { 173 const parsed = new URL(url, window.location.origin); 174 const host = parsed.hostname.replace(/^www\./, ""); 175 176 if (!host.includes("facebook.com") || !parsed.pathname.includes("/plugins/")) { 177 return null; 178 } 179 180 const suffix = `${parsed.pathname.split("/plugins/")[1] || ""}${parsed.search}`.replace(/^\//, ""); 181 return suffix || null; 182 } catch (error) { 183 return null; 184 } 185} 186 187function getCalendlyEmbedId(url) { 188 if (!url) { 189 return null; 190 } 191 192 try { 193 const parsed = new URL(url, window.location.origin); 194 const host = parsed.hostname.replace(/^www\./, ""); 195 196 if (!host.includes("calendly.com")) { 197 return null; 198 } 199 200 const suffix = `${parsed.pathname.replace(/^\//, "")}${parsed.search}`.replace(/\/$/, ""); 201 return suffix || null; 202 } catch (error) { 203 return null; 204 } 205} 206 207function normalizeGoogleMapsPlaceholders() { 208 document.querySelectorAll('[data-service="googlemaps"]').forEach((el) => { 209 const id = el.getAttribute("data-id"); 210 if (id && !id.includes("?") && !id.includes("/")) { 211 el.setAttribute("data-id", `embed?pb=${id}`); 212 } 213 }); 214} 215 216const KNOWN_EMBED_RATIOS = new Set([ 217 "1:1", 218 "2:1", 219 "3:2", 220 "5:2", 221 "4:3", 222 "16:9", 223 "16:10", 224 "20:9", 225 "21:9", 226 "9:16", 227 "9:20", 228]); 229 230function gcd(a, b) { 231 return b ? gcd(b, a % b) : a; 232} 233 234function normalizeCssSize(value) { 235 if (!value) { 236 return value; 237 } 238 return /^\d+$/.test(value) ? `${value}px` : value; 239} 240 241function parsePixelSize(value) { 242 if (!value) { 243 return null; 244 } 245 if (/^\d+$/.test(value)) { 246 return parseInt(value, 10); 247 } 248 const match = /^(\d+(?:\.\d+)?)px$/i.exec(value.trim()); 249 return match ? Math.round(parseFloat(match[1])) : null; 250} 251 252function isPercentageSize(value) { 253 return Boolean(value && String(value).trim().endsWith("%")); 254} 255 256function ratioForPixels(widthPx, heightPx) { 257 const divisor = gcd(widthPx, heightPx); 258 const key = `${widthPx / divisor}:${heightPx / divisor}`; 259 return KNOWN_EMBED_RATIOS.has(key) ? key : null; 260} 261 262function extractIframeDimensions(iframe) { 263 let width = iframe.getAttribute("width"); 264 let height = iframe.getAttribute("height"); 265 const style = iframe.getAttribute("style") || ""; 266 267 style.split(";").forEach((chunk) => { 268 if (!chunk.includes(":")) { 269 return; 270 } 271 const [rawKey, rawValue] = chunk.split(":");
272 const key = rawKey.trim().toLowerCase(); 273 const value = rawValue.trim(); 274 if (key === "width" && !width) { 275 width = value; 276 } 277 if (key === "height" && !height) { 278 height = value; 279 } 280 }); 281 282 return { width, height }; 283} 284 285function applyDimensionsToPlaceholder(placeholder, { width, height }) { 286 if (!width && !height) { 287 placeholder.setAttribute("data-autoscale", ""); 288 return; 289 } 290 291 const styleParts = []; 292 if (width) { 293 styleParts.push(`width:${normalizeCssSize(width)}`); 294 } 295 if (height) { 296 styleParts.push(`height:${normalizeCssSize(height)}`); 297 } 298 styleParts.push("margin:0 auto"); 299 placeholder.setAttribute("style", styleParts.join(";")); 300 301 const widthPx = parsePixelSize(width); 302 const heightPx = parsePixelSize(height); 303 if (widthPx && heightPx) { 304 const ratio = ratioForPixels(widthPx, heightPx); 305 if (ratio) { 306 placeholder.setAttribute("data-ratio", ratio); 307 return; 308 } 309 placeholder.classList.add("im-fixed-size"); 310 return; 311 } 312 313 if (isPercentageSize(height)) { 314 placeholder.setAttribute("style", "position:absolute;top:0;left:0;width:100%;height:100%"); 315 placeholder.setAttribute("data-autoscale", ""); 316 return; 317 } 318 319 if (height || (width && !String(width).includes("%"))) { 320 placeholder.classList.add("im-fixed-size"); 321 return; 322 } 323 324 placeholder.setAttribute("data-autoscale", ""); 325} 326 327function getEmbedQueryParams(url) { 328 if (!url) { 329 return ""; 330 } 331 332 try { 333 return new URL(url, window.location.origin).searchParams.toString(); 334 } catch (error) { 335 return ""; 336 } 337} 338 339function getLegacyEmbedTarget(iframe) { 340 const src = iframe.getAttribute("src") 341 || iframe.getAttribute("data-src") 342 || iframe.getAttribute("data-cc-blocked-embed-src") 343 || ""; 344 345 if (!src) { 346 return null; 347 } 348 349 const youtubeId = getYoutubeVideoId(src); 350 351 if (youtubeId) { 352 return { 353 service: "youtube", 354 id: youtubeId, 355 params: getEmbedQueryParams(src), 356 }; 357 } 358 359 const vimeoVideoId = getVimeoVideoId(src); 360 361 if (vimeoVideoId) { 362 return { 363 service: "vimeo", 364 id: vimeoVideoId, 365 params: getEmbedQueryParams(src), 366 }; 367 } 368 369 const mapsId = src.includes("google.com/maps") || src.includes("maps.google") 370 ? getGoogleMapsEmbedId(src) 371 : null; 372 373 if (mapsId) { 374 return { 375 service: "googlemaps", 376 id: mapsId, 377 params: "", 378 }; 379 } 380 381 if (/spotify\.com\/embed/i.test(src)) { 382 const spotifyId = getSpotifyEmbedId(src); 383 if (spotifyId) { 384 return { 385 service: "spotify", 386 id: spotifyId, 387 params: "", 388 }; 389 } 390 } 391 392 if (/soundcloud\.com\/player/i.test(src)) { 393 const soundcloudId = getSoundCloudEmbedId(src); 394 if (soundcloudId) { 395 return { 396 service: "soundcloud", 397 id: soundcloudId, 398 params: "", 399 }; 400 } 401 } 402 403 if (/facebook\.com\/plugins/i.test(src)) { 404 const facebookId = getFacebookEmbedId(src); 405 if (facebookId) { 406 return { 407 service: "facebook_social", 408 id: facebookId, 409 params: "", 410 }; 411 } 412 } 413 414 if (/forexprostools\.com|investing\.com/i.test(src)) { 415 const investingId = getInvestingWidgetEmbedId(src); 416 if (investingId) { 417 return { 418 service: "investing_widget", 419 id: investingId, 420 params: "", 421 }; 422 } 423 } 424 425 if (/calendly\.com/i.test(src)) { 426 const calendlyId = getCalendlyEmbedId(src); 427 if (calendlyId) { 428 return { 429 service: "calendly", 430 id: calendlyId, 431 params: "", 432 }; 433 } 434 } 435 436 return null; 437} 438 439function copyIframeAttributesToPlaceholder(iframe, placeholder) { 440 const passthroughAttributes = ["title", "loading", "referrerpolicy"]; 441 442 passthroughAttributes.forEach((attribute) => { 443 const value = iframe.getAttribute(attribute); 444 445 if (!value) { 446 return; 447 } 448 449 if (attribute === "title") { 450 placeholder.setAttribute("data-title", value); 451 return; 452 } 453 454 placeholder.setAttribute(`data-iframe-${attribute}`, value); 455 }); 456 457 iframe.getAttributeNames().forEach((attribute) => { 458 if (!attribute.startsWith("data-iframe-")) { 459 return; 460 } 461 462 placeholder.setAttribute(attribute, iframe.getAttribute(attribute)); 463 }); 464} 465function migrateLegacyCmsEmbeds() { 466 document 467 .querySelectorAll("iframe[src], iframe[data-src], iframe[data-cc-blocked-embed-src]") 468 .forEach((iframe) => { 469 if (iframe.closest("[data-service]")) { 470 return; 471 } 472 473 const target = getLegacyEmbedTarget(iframe); 474 475 if (!target) { 476 return; 477 } 478 479 const placeholder = document.createElement("div"); 480 placeholder.setAttribute("data-service", target.service); 481 placeholder.setAttribute("data-id", target.id); 482 483 applyDimensionsToPlaceholder(placeholder, extractIframeDimensions(iframe)); 484 485 if (target.params) { 486 placeholder.setAttribute("data-params", target.params); 487 } 488 489 copyIframeAttributesToPlaceholder(iframe, placeholder); 490 iframe.replaceWith(placeholder); 491 }); 492} 493 494migrateLegacyCmsEmbeds(); 495normalizeGoogleMapsPlaceholders(); 496 497/** 498 * Update gtag consent according to the users choices made in CookieConsent UI 499 */ 500function updateGtagConsent() { 501 if (typeof gtag !== "function") return; 502 503 gtag("consent", "update", { 504 [SERVICE_ANALYTICS_STORAGE]: CookieConsent.acceptedService( 505 SERVICE_ANALYTICS_STORAGE, 506 CAT_ANALYTICS, 507 ) 508 ? "granted" 509 : "denied", 510 [SERVICE_AD_STORAGE]: CookieConsent.acceptedService( 511 SERVICE_AD_STORAGE, 512 CAT_ADVERTISEMENT, 513 ) 514 ? "granted" 515 : "denied", 516 [SERVICE_AD_USER_DATA]: CookieConsent.acceptedService( 517 SERVICE_AD_USER_DATA, 518 CAT_ADVERTISEMENT, 519 ) 520 ? "granted" 521 : "denied", 522 [SERVICE_AD_PERSONALIZATION]: CookieConsent.acceptedService( 523 SERVICE_AD_PERSONALIZATION, 524 CAT_ADVERTISEMENT, 525 ) 526 ? "granted" 527 : "denied", 528 [SERVICE_FUNCTIONALITY_STORAGE]: CookieConsent.acceptedService( 529 SERVICE_FUNCTIONALITY_STORAGE, 530 CAT_FUNCTIONALITY, 531 ) 532 ? "granted" 533 : "denied", 534 [SERVICE_PERSONALIZATION_STORAGE]: CookieConsent.acceptedService( 535 SERVICE_PERSONALIZATION_STORAGE, 536 CAT_FUNCTIONALITY, 537 ) 538 ? "granted" 539 : "denied", 540 [SERVICE_SECURITY_STORAGE]: CookieConsent.acceptedService( 541 SERVICE_SECURITY_STORAGE, 542 CAT_SECURITY, 543 ) 544 ? "granted" 545 : "denied", 546 }); 547} 548 549function updateWidgetPlaceholders() { 550 document.querySelectorAll("[data-cc-placeholder]").forEach((el) => { 551 const category = el.getAttribute("data-cc-placeholder"); 552 const service = el.getAttribute("data-cc-service"); 553 554 if (service === "elfsight" || service === "yext_analytics" || service === "weatherwidget" || service === "tradingview_widget" || service === "iprospector" || service === "paragon_mls" || service === "googlemaps_js" || service === "inksoft" || service === "appfolio") { 555 return; 556 } 557 558 let accepted = false;
559 if (service) { 560 accepted = CookieConsent.acceptedService(service, category); 561 } else if (category) { 562 accepted = CookieConsent.acceptedCategory(category); 563 } 564 565 el.toggleAttribute("hidden", accepted); 566 }); 567} 568function isImGooglemapsGranted() { 569 if (!widgetConsentGranted("googlemaps", CAT_FUNCTIONALITY)) return false; 570 if (!/\/mls(?:\/|$)/.test(location.pathname || "")) return true; 571 if (CookieConsent.acceptedCategory(CAT_FUNCTIONALITY)) return true; 572 return CookieConsent.acceptedService("paragon_mls", CAT_FUNCTIONALITY); 573} 574 575function syncIframeManagerConsent() { 576 const sync = (service, category) => { 577 if (widgetConsentGranted(service, category)) { 578 im.acceptService(service); 579 } else { 580 im.rejectService(service); 581 } 582 }; 583 sync("youtube", CAT_FUNCTIONALITY); 584 sync("vimeo", CAT_FUNCTIONALITY); 585 if (isImGooglemapsGranted()) { 586 im.acceptService("googlemaps"); 587 } else { 588 im.rejectService("googlemaps"); 589 } 590 sync("spotify", CAT_FUNCTIONALITY); 591 sync("soundcloud", CAT_FUNCTIONALITY); 592 sync("facebook_social", CAT_FUNCTIONALITY); 593 sync("calendly", CAT_FUNCTIONALITY); 594} 595 596// widget-consent-common:v1 597function widgetConsentGranted(serviceId, category) { 598 return CookieConsent.acceptedService(serviceId, category) 599 || CookieConsent.acceptedCategory(category); 600} 601 602 603// recaptcha-consent:v22 604const RECAPTCHA_CONSENT_VERSION = "v18"; 605let recaptchaScriptPromise = null; 606let recaptchaScriptSrcLoaded = null; 607let recaptchaPreferExplicit = false; 608let recaptchaConfigError = false; 609 610function markRecaptchaConfigError() { 611 recaptchaConfigError = true; 612} 613 614function whenRecaptchaReady() { 615 if (!window.grecaptcha?.ready) { 616 return Promise.resolve(); 617 } 618 return new Promise((resolve) => grecaptcha.ready(resolve)); 619} 620 621function isValidRecaptchaSitekey(key) { 622 return typeof key === "string" && /^6L[\w-]{30,}$/.test(key); 623} 624 625function getRecaptchaForm(preferred) { 626 if (preferred && preferred.tagName === "FORM") { 627 return preferred; 628 } 629 return document.getElementById("theForm") 630 || document.getElementById("ContactForm") 631 || document.querySelector("form.grid-form") 632 || document.querySelector("form"); 633} 634 635function extractRecaptchaSitekeyFromScripts(root) { 636 const scope = root && root.querySelectorAll ? root : document; 637 for (const script of scope.querySelectorAll( 638 'script[src*="recaptcha/api.js"], script[src*="recaptcha/enterprise.js"], script[data-cc-blocked-recaptcha-src*="recaptcha/"]' 639 )) { 640 const src = script.getAttribute("src") 641 || script.getAttribute("data-cc-blocked-recaptcha-src") 642 || ""; 643 const match = src.match(/[?&]render=([^&]+)/); 644 const candidate = match?.[1] ? decodeURIComponent(match[1]) : null; 645 if (isValidRecaptchaSitekey(candidate)) { 646 return candidate; 647 } 648 } 649 return null; 650} 651 652function extractRecaptchaSitekeyFromInline() { 653 for (const script of document.scripts) { 654 const body = script.textContent || ""; 655 const match = body.match(/grecaptcha\.execute\s*\(\s*['"](6L[^'"]+)['"]/i); 656 if (match?.[1] && isValidRecaptchaSitekey(match[1])) { 657 return match[1]; 658 } 659 } 660 return null; 661} 662 663function metadataSitekeyFromScope(root) { 664 if (!root || !root.querySelector) { 665 return null; 666 } 667 const candidate = root.querySelector( 668 ".cookie-captcha-metadata[data-recaptcha-sitekey]" 669 )?.getAttribute("data-recaptcha-sitekey") 670 || root.querySelector("[data-recaptcha-sitekey]")?.getAttribute("data-recaptcha-sitekey") 671 || root.querySelector(".g-recaptcha")?.getAttribute("data-sitekey") 672 || null; 673 return isValidRecaptchaSitekey(candidate) ? candidate : null; 674} 675 676function getRecaptchaSitekey(form) { 677 const targetForm = getRecaptchaForm(form); 678 const fromFormBlocked = targetForm && extractRecaptchaSitekeyFromScripts(targetForm); 679 const fromBlocked = fromFormBlocked || extractRecaptchaSitekeyFromScripts(document); 680 const fromInline = extractRecaptchaSitekeyFromInline(); 681 const fromFormMetadata = metadataSitekeyFromScope(targetForm); 682 const fromAncestor = targetForm?.closest("[data-recaptcha-sitekey]") 683 ?.getAttribute("data-recaptcha-sitekey") || null; 684 const fromValidAncestor = isValidRecaptchaSitekey(fromAncestor) ? fromAncestor : null;
685 const fromGlobal = metadataSitekeyFromScope(document); 686 return fromBlocked || fromInline || fromFormMetadata || fromValidAncestor || fromGlobal || null; 687} 688 689function getRecaptchaVersion(form) { 690 if (recaptchaPreferExplicit) { 691 return "v2"; 692 } 693 const targetForm = getRecaptchaForm(form); 694 // GroupM7 contact forms use buttonClick + grecaptcha.execute (v3), not checkbox widgets. 695 if (formUsesButtonClickSubmit(targetForm)) { 696 return "v3"; 697 } 698 if (targetForm?.querySelector(".g-recaptcha") || document.querySelector(".g-recaptcha")) { 699 return "v2"; 700 } 701 const explicit = targetForm?.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version") 702 || document.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version"); 703 if (explicit) { 704 return explicit; 705 } 706 const scope = targetForm || document; 707 if (scope.querySelector("#g-recaptcha-response") || scope.querySelector("[name='g-recaptcha-response']")) { 708 return "v3"; 709 } 710 return "v2"; 711} 712 713function recaptchaScriptSrc(options = {}) { 714 const forceExplicit = Boolean(options.forceExplicit) || recaptchaPreferExplicit; 715 if (forceExplicit) { 716 return "https://www.google.com/recaptcha/api.js?render=explicit"; 717 } 718 const sitekey = getRecaptchaSitekey(options.form); 719 const version = getRecaptchaVersion(options.form); 720 if (version === "v3" && sitekey) { 721 return `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(sitekey)}`; 722 } 723 return "https://www.google.com/recaptcha/api.js?render=explicit"; 724} 725 726function getRecaptchaAction(form) { 727 const targetForm = getRecaptchaForm(form); 728 return targetForm?.querySelector("[data-recaptcha-action]")?.getAttribute("data-recaptcha-action") 729 || document.querySelector("[data-recaptcha-action]")?.getAttribute("data-recaptcha-action") 730 || "contact"; 731} 732 733function purgeStaleRecaptchaScripts() { 734 document.querySelectorAll("script[src*='google.com/recaptcha/api.js']").forEach((script) => { 735 script.remove(); 736 }); 737 if (window.grecaptcha && recaptchaScriptSrcLoaded !== recaptchaScriptSrc()) { 738 delete window.grecaptcha; 739 recaptchaScriptPromise = null; 740 recaptchaScriptSrcLoaded = null; 741 } 742} 743 744function loadRecaptchaScript(options = {}) { 745 const bypassConsent = Boolean(options.bypassConsent); 746 const forceExplicit = Boolean(options.forceExplicit); 747 const form = getRecaptchaForm(options.form); 748 if (!bypassConsent && !isRecaptchaAccepted()) { 749 return Promise.reject(new Error("reCAPTCHA consent required")); 750 } 751 const src = recaptchaScriptSrc({ forceExplicit, form }); 752 if (getRecaptchaVersion(form) === "v3" && !getRecaptchaSitekey(form)) { 753 return Promise.reject(new Error("reCAPTCHA site key missing")); 754 } 755 purgeStaleRecaptchaScripts(); 756 if (window.grecaptcha?.ready && recaptchaScriptSrcLoaded === src) { 757 return whenRecaptchaReady(); 758 } 759 if (recaptchaScriptPromise && recaptchaScriptSrcLoaded === src) { 760 return recaptchaScriptPromise; 761 } 762 recaptchaScriptPromise = new Promise((resolve, reject) => { 763 const script = document.createElement("script"); 764 script.setAttribute("data-cookie-recaptcha-loader", "1"); 765 script.async = true; 766 script.defer = true; 767 script.src = src; 768 script.onload = () => { 769 recaptchaScriptSrcLoaded = src; 770 whenRecaptchaReady().then(resolve, reject); 771 }; 772 script.onerror = () => { 773 if (!forceExplicit && /[?&]render=/.test(src) && !src.includes("render=explicit")) { 774 recaptchaScriptPromise = null; 775 recaptchaScriptSrcLoaded = null; 776 markRecaptchaConfigError(); 777 script.remove(); 778 reject(new Error("reCAPTCHA configuration error")); 779 return; 780 } 781 reject(new Error("reCAPTCHA failed to load")); 782 }; 783 document.head.appendChild(script); 784 }); 785 return recaptchaScriptPromise; 786} 787 788function renderRecaptchaWidgets() { 789 return whenRecaptchaReady().then(() => { 790 if (!window.grecaptcha?.render) { 791 return; 792 } 793 document.querySelectorAll(".g-recaptcha:not([data-recaptcha-ready])").forEach((el) => { 794 const sitekey = el.getAttribute("data-sitekey"); 795 if (!sitekey) { 796 return; 797 }
798 try { 799 grecaptcha.render(el, { sitekey }); 800 el.setAttribute("data-recaptcha-ready", "1"); 801 } catch (error) { 802 /* widget may already be rendered */ 803 } 804 }); 805 }); 806} 807 808async function executeRecaptchaV3(form) { 809 const targetForm = getRecaptchaForm(form); 810 const sitekey = getRecaptchaSitekey(targetForm); 811 if (!sitekey || !window.grecaptcha?.execute) { 812 return false; 813 } 814 await whenRecaptchaReady(); 815 const action = getRecaptchaAction(targetForm); 816 try { 817 const token = await grecaptcha.execute(sitekey, { action }); 818 setRecaptchaResponseToken(token, targetForm); 819 return Boolean(token); 820 } catch (error) { 821 markRecaptchaConfigError(); 822 return false; 823 } 824} 825 826function setRecaptchaResponseToken(token, form) { 827 const targetForm = getRecaptchaForm(form); 828 let responseEl = (targetForm && targetForm.querySelector("[name='g-recaptcha-response']")) 829 || document.getElementById("g-recaptcha-response") 830 || document.querySelector("[name='g-recaptcha-response']"); 831 if (!responseEl && targetForm) { 832 responseEl = document.createElement("input"); 833 responseEl.type = "hidden"; 834 responseEl.name = "g-recaptcha-response"; 835 responseEl.id = "g-recaptcha-response"; 836 targetForm.appendChild(responseEl); 837 } 838 if (responseEl) { 839 responseEl.value = token; 840 } 841 const dupEl = targetForm && targetForm.querySelector("[name='g_recaptcha_response']"); 842 if (dupEl) { 843 dupEl.value = token; 844 } 845 return responseEl; 846} 847 848function ensureV2RecaptchaWidget(form) { 849 const targetForm = getRecaptchaForm(form); 850 if (!targetForm || targetForm.querySelector(".g-recaptcha")) { 851 return false; 852 } 853 const sitekey = getRecaptchaSitekey(targetForm); 854 if (!sitekey) { 855 return false; 856 } 857 const div = document.createElement("div"); 858 div.className = "g-recaptcha"; 859 div.setAttribute("data-sitekey", sitekey); 860 const submit = targetForm.querySelector( 861 'button:not([type]), button[type="submit"], input[type="submit"]' 862 ); 863 if (submit) { 864 submit.insertAdjacentElement("beforebegin", div); 865 } else { 866 targetForm.appendChild(div); 867 } 868 document.querySelectorAll("[data-recaptcha-version]").forEach((el) => { 869 el.setAttribute("data-recaptcha-version", "v2"); 870 }); 871 return true; 872} 873 874function v2WidgetAwaitingCheckbox(form) { 875 const targetForm = form || document.getElementById("ContactForm") || document.querySelector("form"); 876 if (!targetForm) { 877 return false; 878 } 879 const widget = targetForm.querySelector(".g-recaptcha"); 880 return Boolean(widget && widget.getAttribute("data-recaptcha-ready") === "1"); 881} 882 883async function activateRecaptcha(options = {}) { 884 const bypassConsent = Boolean(options.bypassConsent); 885 if (!bypassConsent && !isRecaptchaAccepted()) { 886 return false; 887 } 888 const form = getRecaptchaForm(options.form); 889 try { 890 await loadRecaptchaScript({ bypassConsent, form }); 891 } catch (error) { 892 return false; 893 } 894 if (getRecaptchaVersion(form) === "v3") { 895 document.querySelectorAll(".g-recaptcha").forEach((el) => { 896 el.hidden = true; 897 }); 898 const executed = await executeRecaptchaV3(form); 899 if (executed) { 900 return true; 901 } 902 if (recaptchaConfigError) { 903 return false; 904 } 905 ensureV2RecaptchaWidget(form); 906 await renderRecaptchaWidgets(); 907 return true; 908 } 909 ensureV2RecaptchaWidget(form); 910 await renderRecaptchaWidgets(); 911 return true; 912} 913 914function isRecaptchaAccepted() { 915 return CookieConsent.acceptedService("google_recaptcha", CAT_SECURITY); 916} 917 918function acceptRecaptchaConsent() { 919 const prefs = CookieConsent.getUserPreferences(); 920 const categories = [...new Set([ 921 CAT_NECESSARY, 922 ...(prefs.acceptedCategories || []), 923 CAT_SECURITY, 924 ])]; 925 CookieConsent.acceptCategory(categories); 926 CookieConsent.acceptService("google_recaptcha", CAT_SECURITY); 927} 928 929function updateCaptchaConsent() { 930 const accepted = isRecaptchaAccepted(); 931 document.querySelectorAll(".g-recaptcha").forEach((el) => { 932 if (accepted && el.hasAttribute("data-recaptcha-ready")) { 933 el.setAttribute("data-recaptcha-ready", "1"); 934 } else if (!accepted) { 935 el.removeAttribute("data-recaptcha-ready"); 936 } 937 }); 938} 939 940async function enableRecaptcha(event) { 941 if (event?.preventDefault) { 942 event.preventDefault(); 943 event.stopPropagation(); 944 } 945 acceptRecaptchaConsent(); 946 try { 947 const ok = await activateRecaptcha({ bypassConsent: true }); 948 updateCaptchaConsent(); 949 return ok; 950 } catch (error) { 951 updateCaptchaConsent(); 952 return false;
953 } 954} 955 956async function preloadRecaptchaOnConsent() { 957 if (!pageHasRecaptchaMarkup()) { 958 return false; 959 } 960 try { 961 const ok = await activateRecaptcha({ bypassConsent: true }); 962 updateCaptchaConsent(); 963 return ok; 964 } catch (error) { 965 return false; 966 } 967} 968 969window.enableRecaptcha = enableRecaptcha; 970window.activateRecaptcha = activateRecaptcha; 971window.isRecaptchaAccepted = isRecaptchaAccepted; 972window.preloadRecaptchaOnConsent = preloadRecaptchaOnConsent; 973 974function ensureCaptchaMetadata() { 975 if (document.querySelector("[data-cc-captcha]")) { 976 return false; 977 } 978 const form = getRecaptchaForm(); 979 const sitekey = getRecaptchaSitekey(form); 980 const version = 981 form?.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version") 982 || document.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version") 983 || (form?.querySelector(".g-recaptcha") || document.querySelector(".g-recaptcha") ? "v2" : "v3"); 984 if (!form || !sitekey) { 985 return false; 986 } 987 const block = document.createElement("div"); 988 block.className = "cookie-captcha-metadata"; 989 block.setAttribute("data-cc-captcha", "security"); 990 block.setAttribute("data-cc-service", "google_recaptcha"); 991 block.setAttribute("data-recaptcha-sitekey", sitekey); 992 block.setAttribute("data-recaptcha-version", version); 993 block.setAttribute("hidden", ""); 994 block.setAttribute("aria-hidden", "true"); 995 const anchor = document.getElementById("g-recaptcha-response") 996 || document.querySelector("[name='g-recaptcha-response']"); 997 if (anchor?.parentNode) { 998 anchor.parentNode.insertBefore(block, anchor); 999 } else { 1000 form.appendChild(block); 1001 } 1002 return true; 1003} 1004 1005function findCaptchaProtectedForms() { 1006 const forms = []; 1007 const contact = document.getElementById("ContactForm"); 1008 if (contact && formHasRecaptcha(contact)) { 1009 forms.push(contact); 1010 } 1011 document.querySelectorAll("form").forEach((form) => { 1012 if (form === contact || forms.includes(form)) { 1013 return; 1014 } 1015 if (formHasRecaptcha(form)) { 1016 forms.push(form); 1017 } 1018 }); 1019 return forms; 1020} 1021 1022function findExternalButtonClickTrigger(form) { 1023 if (!form || !formHasRecaptcha(form)) { 1024 return null; 1025 } 1026 const external = document.getElementById("submitButton"); 1027 if (external && /buttonClick/.test(external.getAttribute("onclick") || "")) { 1028 return external; 1029 } 1030 return null; 1031} 1032 1033function findSubmitNoticeAnchor(form) { 1034 if (!form) { 1035 return null; 1036 } 1037 const inForm = form.querySelector("#submitButton") 1038 || form.querySelector('[onclick*="buttonClick"]') 1039 || form.querySelector( 1040 'button:not([type]), button[type="submit"], input[type="submit"]' 1041 ); 1042 if (inForm) { 1043 return inForm; 1044 } 1045 return findExternalButtonClickTrigger(form); 1046} 1047 1048function submitAnchorHasNotice(submit) { 1049 return Boolean( 1050 submit?.nextElementSibling?.classList?.contains("cookie-captcha-submit-notice") 1051 ); 1052} 1053 1054function formHasSubmitNotice(form) { 1055 if (form.querySelector(".cookie-captcha-submit-notice")) { 1056 return true; 1057 } 1058 return submitAnchorHasNotice(findSubmitNoticeAnchor(form)); 1059} 1060 1061function ensureCaptchaSubmitNotice() { 1062 const captchaForms = findCaptchaProtectedForms(); 1063 if (!captchaForms.length) { 1064 return false; 1065 } 1066 let added = false; 1067 captchaForms.forEach((form) => { 1068 if (formHasSubmitNotice(form)) { 1069 return; 1070 } 1071 const submit = findSubmitNoticeAnchor(form); 1072 if (!submit) { 1073 return; 1074 } 1075 const notice = document.createElement("p"); 1076 notice.className = "cookie-captcha-submit-notice"; 1077 notice.textContent = "Submitting this form gives consent to security verification (reCAPTCHA)."; 1078 submit.insertAdjacentElement("afterend", notice); 1079 added = true; 1080 }); 1081 return added; 1082} 1083 1084function isRecaptchaPostField(name) { 1085 return /recaptcha/i.test(name || ""); 1086} 1087 1088function captchaActivationFailedMessage() { 1089 if (recaptchaConfigError) { 1090 return "Security verification is not available on this site. Please contact the site owner."; 1091 } 1092 if (!getRecaptchaSitekey()) { 1093 return "Security verification is not configured on this form. Enable Security cookies in Cookie Settings, then try again."; 1094 } 1095 return "Security verification failed to load. Enable Security cookies in Cookie Settings, then try again."; 1096} 1097 1098function captchaIncompleteMessage() { 1099 if (getRecaptchaVersion() === "v2") { 1100 return "Please complete the security checkbox, then submit again."; 1101 } 1102 return captchaActivationFailedMessage(); 1103} 1104 1105function formUsesContactVerifyFlow(form) { 1106 const targetForm = getRecaptchaForm(form); 1107 return Boolean(targetForm && targetForm.id === "ContactForm"); 1108} 1109 1110function setHiddenFormField(form, name, value) { 1111 if (!form) { 1112 return null; 1113 } 1114 let field = form.querySelector(`[name="${name}"]`); 1115 if (!field) { 1116 field = document.createElement("input"); 1117 field.type = "hidden"; 1118 field.name = name; 1119 form.appendChild(field); 1120 } 1121 field.value = value; 1122 return field; 1123} 1124 1125async function verifyRecaptchaBeforeContactSubmit(token, submitUrl, form) { 1126 const verifyBody = new URLSearchParams(); 1127 verifyBody.set("cookie_recaptcha_verify", "1"); 1128 verifyBody.set("g-recaptcha-response", token); 1129 verifyBody.set("cookie_recaptcha_version", getRecaptchaVersion(form)); 1130 const response = await fetch(submitUrl, { 1131 method: "POST", 1132 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 1133 body: verifyBody.toString(), 1134 credentials: "same-origin", 1135 redirect: "manual", 1136 }); 1137 return response.status === 204 1138 || response.headers.get("X-Cookie-Recaptcha-Verified") === "1"; 1139} 1140 1141async function submitContactFormWithRecaptcha(preferredForm) { 1142 const form = getRecaptchaForm(preferredForm); 1143 let tokenEl = (form && form.querySelector("[name='g-recaptcha-response']")) 1144 || document.getElementById("g-recaptcha-response") 1145 || document.querySelector("[name='g-recaptcha-response']"); 1146 const tokenNeedsRefresh = !tokenEl || !tokenEl.value || tokenEl.value.length < 100; 1147 if (tokenNeedsRefresh) { 1148 let activated = false;
1149 if (isRecaptchaAccepted()) { 1150 try { 1151 activated = await activateRecaptcha({ form }); 1152 } catch (error) { 1153 activated = false; 1154 } 1155 } else { 1156 activated = await enableRecaptcha(); 1157 } 1158 if (!activated) { 1159 alert(captchaActivationFailedMessage()); 1160 return; 1161 } 1162 tokenEl = (form && form.querySelector("[name='g-recaptcha-response']")) 1163 || document.getElementById("g-recaptcha-response") 1164 || document.querySelector("[name='g-recaptcha-response']"); 1165 if (getRecaptchaVersion(form) === "v2" && (!tokenEl || !tokenEl.value)) { 1166 if (v2WidgetAwaitingCheckbox(form)) { 1167 const widget = form?.querySelector(".g-recaptcha"); 1168 if (widget) { 1169 widget.scrollIntoView({ behavior: "smooth", block: "nearest" }); 1170 } 1171 return; 1172 } 1173 } 1174 } 1175 let submitUrl = form ? (form.action || window.location.href) : window.location.href; 1176 if (form) { 1177 try { 1178 const parsed = new URL(submitUrl, window.location.href); 1179 if (!parsed.searchParams.has("submitted")) { 1180 parsed.searchParams.set("submitted", ""); 1181 } 1182 submitUrl = parsed.href; 1183 form.action = submitUrl; 1184 } catch (error) { 1185 if (!submitUrl.includes("submitted")) { 1186 submitUrl += (submitUrl.includes("?") ? "&" : "?") + "submitted"; 1187 form.action = submitUrl; 1188 } 1189 } 1190 } 1191 if (!tokenEl || !tokenEl.value) { 1192 alert(captchaIncompleteMessage()); 1193 return; 1194 } 1195 if (!form) { 1196 return; 1197 } 1198 if (!formUsesContactVerifyFlow(form)) { 1199 setHiddenFormField(form, "cookie_recaptcha_version", getRecaptchaVersion(form)); 1200 form.submit(); 1201 return; 1202 } 1203 const verified = await verifyRecaptchaBeforeContactSubmit(tokenEl.value, submitUrl, form); 1204 if (!verified) { 1205 alert("Security verification was rejected by the server. Please try submitting again."); 1206 return; 1207 } 1208 const params = new URLSearchParams(); 1209 if (form.elements) { 1210 for (const el of form.elements) { 1211 if (!el.name || el.disabled) { 1212 continue; 1213 } 1214 if (isRecaptchaPostField(el.name)) { 1215 continue; 1216 } 1217 if (el.type === "submit" || el.type === "button" || el.type === "file") { 1218 continue; 1219 } 1220 if ((el.type === "checkbox" || el.type === "radio") && !el.checked) { 1221 continue; 1222 } 1223 params.set(el.name, el.value); 1224 } 1225 } 1226 // Session-miss fallback: server may re-verify token; version stays in PHP session from verify POST. 1227 params.set("g-recaptcha-response", tokenEl.value); 1228 const postForm = document.createElement("form"); 1229 postForm.method = "POST"; 1230 postForm.action = submitUrl; 1231 postForm.style.display = "none"; 1232 params.forEach((value, key) => { 1233 const input = document.createElement("input"); 1234 input.type = "hidden"; 1235 input.name = key; 1236 input.value = value; 1237 postForm.appendChild(input); 1238 }); 1239 document.body.appendChild(postForm); 1240 postForm.submit(); 1241} 1242 1243function usesLegacyRecaptchaButtonClick() { 1244 return typeof window.buttonClick === "function" && /grecaptcha/.test(window.buttonClick.toString()); 1245} 1246 1247function ensureConsentGatedButtonClick() { 1248 if (!pageHasRecaptchaMarkup()) { 1249 return false; 1250 } 1251 const captchaForms = findCaptchaProtectedForms(); 1252 const hasCaptchaButtonClick = captchaForms.some( 1253 (form) => form.querySelector('[onclick*="buttonClick"]') || findExternalButtonClickTrigger(form) 1254 ); 1255 const alreadyPatched = 1256 typeof window.buttonClick === "function" 1257 && /enableRecaptcha|activateRecaptcha|submitContactFormWithRecaptcha/.test(window.buttonClick.toString()); 1258 const needsPatch = usesLegacyRecaptchaButtonClick() 1259 || (!alreadyPatched && hasCaptchaButtonClick); 1260 if (!needsPatch) { 1261 return false; 1262 } 1263 window.buttonClick = async function buttonClick() { 1264 const formEl = getRecaptchaForm(); 1265 if (typeof checkRequired === "function" && formEl) { 1266 const missing = checkRequired(formEl); 1267 if (missing && missing.length) { 1268 return false; 1269 } 1270 } 1271 await submitContactFormWithRecaptcha(formEl); 1272 }; 1273 return true; 1274} 1275 1276function usesLegacyInitiateCaptcha() { 1277 return typeof window.initiateCaptcha === "function" 1278 && /grecaptcha/.test(window.initiateCaptcha.toString()); 1279} 1280 1281function ensureConsentGatedInitiateCaptcha() { 1282 if (!pageHasRecaptchaMarkup()) { 1283 return false;
1284 } 1285 const alreadyPatched = 1286 typeof window.initiateCaptcha === "function" 1287 && /submitContactFormWithRecaptcha/.test(window.initiateCaptcha.toString()); 1288 const needsPatch = usesLegacyInitiateCaptcha() 1289 || (!alreadyPatched && document.querySelector('[onclick*="initiateCaptcha"]')); 1290 if (!needsPatch) { 1291 return false; 1292 } 1293 window.initiateCaptcha = async function initiateCaptcha() { 1294 const formEl = getRecaptchaForm(); 1295 await submitContactFormWithRecaptcha(formEl); 1296 }; 1297 return true; 1298} 1299 1300function formHasRecaptcha(form) { 1301 if (!form) { 1302 return false; 1303 } 1304 return Boolean( 1305 form.querySelector(".g-recaptcha") 1306 || form.querySelector("[data-cc-captcha]") 1307 || form.querySelector("[name='g-recaptcha-response']") 1308 ); 1309} 1310 1311function formUsesButtonClickSubmit(form) { 1312 if (!form) { 1313 return false; 1314 } 1315 if (form.querySelector('[onclick*="buttonClick"]')) { 1316 return true; 1317 } 1318 if (findExternalButtonClickTrigger(form)) { 1319 return true; 1320 } 1321 return usesLegacyRecaptchaButtonClick(); 1322} 1323 1324function ensureConsentGatedFormSubmit() { 1325 if (!pageHasRecaptchaMarkup()) { 1326 return false; 1327 } 1328 let gated = false; 1329 document.querySelectorAll("form").forEach((form) => { 1330 if (form.dataset.cookieRecaptchaSubmitGate) { 1331 return; 1332 } 1333 if (!formHasRecaptcha(form)) { 1334 return; 1335 } 1336 if (formUsesButtonClickSubmit(form)) { 1337 return; 1338 } 1339 form.dataset.cookieRecaptchaSubmitGate = "1"; 1340 form.addEventListener("submit", async (event) => { 1341 event.preventDefault(); 1342 event.stopPropagation(); 1343 await submitContactFormWithRecaptcha(form); 1344 }); 1345 gated = true; 1346 }); 1347 return gated; 1348} 1349 1350function pageHasRecaptchaMarkup() { 1351 return Boolean( 1352 document.querySelector("[data-cc-captcha]") 1353 || document.getElementById("g-recaptcha-response") 1354 || document.querySelector("[name='g-recaptcha-response']") 1355 || document.querySelector(".g-recaptcha") 1356 || document.querySelector(".cookie-captcha-submit-notice") 1357 ); 1358} 1359 1360function neutralizeLegacyRecaptchaScripts() { 1361 const hasMarkup = pageHasRecaptchaMarkup(); 1362 const scripts = document.querySelectorAll( 1363 'script[src*="recaptcha/api.js"], script[src*="recaptcha/enterprise.js"]' 1364 ); 1365 const inlineScripts = [...document.scripts].filter((script) => 1366 /grecaptcha\.(?:render|execute|ready)/.test(script.textContent || "") 1367 ); 1368 if (!hasMarkup) { 1369 return; 1370 } 1371 scripts.forEach((script) => { 1372 const type = (script.getAttribute("type") || "").toLowerCase(); 1373 if (type === "text/plain" || script.getAttribute("data-cookie-recaptcha-loader")) { 1374 return; 1375 } 1376 const src = script.getAttribute("src"); 1377 if (src) { 1378 script.setAttribute("data-cc-blocked-recaptcha-src", src); 1379 script.removeAttribute("src"); 1380 } 1381 script.type = "text/plain"; 1382 }); 1383 inlineScripts.forEach((script) => { 1384 const type = (script.getAttribute("type") || "").toLowerCase(); 1385 if (type === "text/plain") { 1386 return; 1387 } 1388 if (/function\s+buttonClick\s*\(/.test(script.textContent || "")) { 1389 return; 1390 } 1391 if (/function\s+initiateCaptcha\s*\(/.test(script.textContent || "")) { 1392 return; 1393 } 1394 script.remove(); 1395 }); 1396 if (window.grecaptcha && !isRecaptchaAccepted()) { 1397 delete window.grecaptcha; 1398 recaptchaScriptPromise = null; 1399 recaptchaScriptSrcLoaded = null; 1400 } 1401} 1402 1403function onConsentChange(userInitiated = false) { 1404 updateGtagConsent(); 1405 updateWidgetPlaceholders(); 1406 syncIframeManagerConsent(); 1407 if (!userInitiated) { 1408 neutralizeLegacyRecaptchaScripts(); 1409 return; 1410 } 1411 neutralizeLegacyRecaptchaScripts(); 1412} 1413 1414 1415im.run({ 1416 currLang: "en", 1417 1418 onChange: ({ changedServices, eventSource }) => { 1419 if (eventSource.type !== "click" || eventSource.action !== "accept") return; 1420 1421 const serviceCategory = { 1422 youtube: CAT_FUNCTIONALITY, 1423 vimeo: CAT_FUNCTIONALITY, 1424 googlemaps: CAT_FUNCTIONALITY, 1425 spotify: CAT_FUNCTIONALITY, 1426 soundcloud: CAT_FUNCTIONALITY, 1427 facebook_social: CAT_FUNCTIONALITY, 1428 investing_widget: CAT_ADVERTISEMENT, 1429 calendly: CAT_FUNCTIONALITY, 1430 }; 1431 1432 const byCategory = {}; 1433 for (const svc of changedServices) { 1434 const cat = serviceCategory[svc] || CAT_FUNCTIONALITY; 1435 (byCategory[cat] ||= []).push(svc); 1436 } 1437 1438 for (const [category, services] of Object.entries(byCategory)) { 1439 const prefs = CookieConsent.getUserPreferences(); 1440 CookieConsent.acceptCategory([ 1441 ...new Set([CAT_NECESSARY, ...(prefs.acceptedCategories || []), category]), 1442 ]); 1443 CookieConsent.acceptService( 1444 [...new Set([...(prefs.acceptedServices[category] || []), ...services])], 1445 category, 1446 ); 1447 } 1448 }, 1449 1450 services: { 1451 youtube: { 1452 embedUrl: "https://www.youtube-nocookie.com/embed/{data-id}", 1453 iframe: { 1454 allow: 1455 "accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen;", 1456 loading: "lazy", 1457 frameborder: "0", 1458 }, 1459 languages: { 1460 en: { 1461 notice: 1462 'This content is hosted by a third party. By showing the external
1462content you accept the <a rel="noreferrer noopener" href="https://www.youtube.com/t/terms" target="_blank">terms and conditions</a> of youtube.com.', 1463 loadAllBtn: "Load video", 1464 }, 1465 }, 1466 }, 1467 1468 vimeo: { 1469 embedUrl: "https://player.vimeo.com/video/{data-id}", 1470 iframe: { 1471 allow: "fullscreen; picture-in-picture;", 1472 loading: "lazy", 1473 frameborder: "0", 1474 }, 1475 languages: { 1476 en: { 1477 notice: 1478 'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://vimeo.com/terms" target="_blank">terms and conditions</a> of vimeo.com.', 1479 loadAllBtn: "Load video", 1480 }, 1481 }, 1482 }, 1483 1484 googlemaps: { 1485 embedUrl: "https://www.google.com/maps/{data-id}", 1486 iframe: { 1487 allow: "picture-in-picture; fullscreen;", 1488 loading: "lazy", 1489 referrerpolicy: "no-referrer-when-downgrade", 1490 frameborder: "0", 1491 }, 1492 languages: { 1493 en: { 1494 notice: 1495 'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://cloud.google.com/maps-platform/terms" target="_blank">terms and conditions</a> of Google Maps.', 1496 loadAllBtn: "Load map", 1497 }, 1498 }, 1499 }, 1500 1501 spotify: { 1502 embedUrl: "https://open.spotify.com/embed/{data-id}", 1503 iframe: { 1504 allow: "autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture;", 1505 loading: "lazy", 1506 frameborder: "0", 1507 }, 1508 languages: { 1509 en: { 1510 notice: 1511 'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://www.spotify.com/legal/end-user-agreement/" target="_blank">terms and conditions</a> of Spotify.', 1512 loadAllBtn: "Load player", 1513 }, 1514 }, 1515 }, 1516 1517 soundcloud: { 1518 embedUrl: "https://w.soundcloud.com/player/?{data-id}", 1519 iframe: { 1520 allow: "autoplay", 1521 loading: "lazy", 1522 frameborder: "0", 1523 }, 1524 languages: { 1525 en: { 1526 notice: 1527 'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://soundcloud.com/terms-of-use" target="_blank">terms and conditions</a> of SoundCloud.', 1528 loadAllBtn: "Load player", 1529 }, 1530 }, 1531 }, 1532 1533 facebook_social: { 1534 embedUrl: "https://www.facebook.com/plugins/{data-id}", 1535 iframe: { 1536 allow: "autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share", 1537 scrolling: "no", 1538 loading: "lazy", 1539 frameborder: "0", 1540 }, 1541 languages: { 1542 en: { 1543 notice: 1544 'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://www.facebook.com/legal/terms" target="_blank">terms and conditions</a> of Facebook.', 1545 loadAllBtn: "Load feed", 1546 }, 1547 }, 1548 }, 1549 1550 investing_widget: { 1551 embedUrl: "{data-id}", 1552 iframe: { 1553 scrolling: "no", 1554 loading: "lazy", 1555 frameborder: "0", 1556 allowtransparency: "true", 1557 }, 1558 languages: { 1559 en: { 1560 notice: 1561 'This content is hosted by Investing.com. By showing the external content you accept their <a rel="noreferrer noopener" href="https://www.investing.com/about-us/terms-and-conditions" target="_blank">terms and conditions</a>.', 1562 loadAllBtn: "Load widget", 1563 }, 1564 }, 1565 }, 1566 1567 calendly: { 1568 embedUrl: "https://calendly.com/{data-id}", 1569 iframe: { 1570 allow: "fullscreen; clipboard-write", 1571 loading: "lazy", 1572 frameborder: "0", 1573 }, 1574 languages: { 1575 en: { 1576 notice: 1577 'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://calendly.com/legal" target="_blank">terms and conditions</a> of Calendly.', 1578 loadAllBtn: "Load scheduler", 1579 }, 1580 }, 1581 }, 1582 }, 1583}); 1584 1585CookieConsent.run({ 1586 revision: 4, 1587 1588 page_scripts: true, 1589
1590 cookie: { 1591 secure: window.location.protocol === "https:", 1592 }, 1593 1594 guiOptions: { 1595 consentModal: { 1596 layout: "box", 1597 position: "bottom right", 1598 }, 1599 preferencesModal: { 1600 layout: "box", 1601 }, 1602 }, 1603 1604 onFirstConsent: () => { 1605 onConsentChange(true); 1606 }, 1607 onConsent: () => { 1608 onConsentChange(false); 1609 }, 1610 onChange: () => { 1611 onConsentChange(true); 1612 }, 1613 1614 categories: { 1615 [CAT_NECESSARY]: { 1616 enabled: true, 1617 readOnly: true, 1618 services: { 1619 fontawesome: { 1620 label: 1621 "Loads Font Awesome icon fonts essential to site design.", 1622 }, 1623 self_hosted_typography: { 1624 label: 1625 "Typography (Google Fonts) is self-hosted on our servers; no data is sent to Google for font delivery.", 1626 }, 1627 }, 1628 }, 1629 [CAT_ANALYTICS]: { 1630 autoClear: { 1631 cookies: [ 1632 { 1633 name: /^_ga/, 1634 }, 1635 { 1636 name: "_gid", 1637 }, 1638 ], 1639 }, 1640 services: { 1641 [SERVICE_ANALYTICS_STORAGE]: { 1642 label: 1643 "Enables storage (such as cookies) related to analytics e.g. visit duration.", 1644 }, 1645 }, 1646 }, 1647 [CAT_FUNCTIONALITY]: { 1648 services: { 1649 [SERVICE_FUNCTIONALITY_STORAGE]: { 1650 label: 1651 "Enables storage related to site functionality such as language preferences.", 1652 }, 1653 youtube: { 1654 label: 1655 "Embeds YouTube videos, which may set cookies for playback and preferences.", 1656 onAccept: () => im.acceptService("youtube"), 1657 onReject: () => im.rejectService("youtube"), 1658 }, 1659 vimeo: { 1660 label: 1661 "Embeds Vimeo videos, which may set cookies for playback and preferences.", 1662 onAccept: () => im.acceptService("vimeo"), 1663 onReject: () => im.rejectService("vimeo"), 1664 }, 1665 googlemaps: { 1666 label: "Embeds an interactive Google Maps view of our location.", 1667 onAccept: () => im.acceptService("googlemaps"), 1668 onReject: () => { 1669 im.rejectService("googlemaps"); 1670 document.querySelectorAll('iframe[src*="google.com/maps"], iframe[data-src*="google.com/maps"]').forEach((el) => el.remove()); 1671 document.querySelectorAll('[data-service="googlemaps"]').forEach((el) => { 1672 if (el.querySelector("iframe")) { 1673 el.innerHTML = ""; 1674 } 1675 }); 1676 }, 1677 }, 1678 spotify: { 1679 label: 1680 "Embeds Spotify players, which may set cookies for playback and preferences.", 1681 onAccept: () => im.acceptService("spotify"), 1682 onReject: () => { 1683 im.rejectService("spotify"); 1684 document.querySelectorAll('iframe[src*="spotify.com/embed"], iframe[data-src*="spotify.com/embed"]').forEach((el) => el.remove()); 1685 document.querySelectorAll('[data-service="spotify"]').forEach((el) => { 1686 if (el.querySelector("iframe")) { 1687 el.innerHTML = ""; 1688 } 1689 }); 1690 }, 1691 }, 1692 soundcloud: { 1693 label: 1694 "Embeds SoundCloud players, which may set cookies for playback and preferences.", 1695 onAccept: () => im.acceptService("soundcloud"), 1696 onReject: () => { 1697 im.rejectService("soundcloud"); 1698 document.querySelectorAll('iframe[src*="soundcloud.com/player"], iframe[data-src*="soundcloud.com/player"]').forEach((el) => el.remove()); 1699 document.querySelectorAll('[data-service="soundcloud"]').forEach((el) => { 1700 if (el.querySelector("iframe")) { 1701 el.innerHTML = ""; 1702 } 1703 }); 1704 }, 1705 }, 1706 facebook_social: { 1707 label: 1708 "Enables Facebook social plugins such as like buttons and embedded posts.", 1709 onAccept: () => im.acceptService("facebook_social"), 1710 onReject: () => { 1711 im.rejectService("facebook_social"); 1712 document.querySelectorAll('iframe[src*="facebook.com/plugins"], iframe[data-src*="facebook.com/plugins"]').forEach((el) => el.remove()); 1713 document.querySelectorAll('[data-service="facebook_social"]').forEach((el) => { 1714 if (el.querySelector("iframe")) { 1715 el.innerHTML = ""; 1716 } 1717 }); 1718 }, 1719 }, 1720 calendly: { 1721 label: 1722 "Embeds Calendly scheduling, which may set cookies for appointment booking.", 1723 onAccept: () => im.acceptService("calendly"), 1724 onReject: () => { 1725 im.rejectService("calendly"); 1726 document.querySelectorAll('iframe[src*="calendly.com"], iframe[data-src*="calendly.com"]').forEach((el) => el.remove()); 1727 document.querySelectorAll('[data-service="calendly"]').forEach((el) => { 1728 if (el.querySelector("iframe")) { 1729 el.innerHTML = ""; 1730 } 1731 }); 1732 }, 1733 }, 1734 }, 1735 }, 1736 [CAT_SECURITY]: { 1737 services: { 1738 google_recaptcha: { 1739 label: 1740 "Loads Google reCAPTCHA to protect forms from spam and abuse.", 1741 onAccept: () => { 1742 updateCaptchaConsent(); 1743 preloadRecaptchaOnConsent(); 1744 }, 1745 }, 1746 }, 1747 }, 1748 }, 1749 1750 language: { 1751 default: "en", 1752 translations: { 1753 en: { 1754 consentModal: { 1755 title: "We use cookies", 1756 description: 1757 "We use cookies for specific <strong>purposes</strong> - essential operation, analytics and functionality. Optional cookies may involve <strong>third parties</strong> such as Google; you can <strong>withdraw</strong> consent anytime via Manage preferences or Cookie Settings.", 1758 acceptAllBtn: "Accept all", 1759 acceptNecessaryBtn: "Reject all", 1760 showPreferencesBtn: "Manage preferences", 1761 footer: ` 1762 <a href="${SITE_CONFIG.privacyPolicyUrl}">Privacy Policy</a> 1763 <a href="${SITE_CONFIG.contactUrl}">Contact</a> 1764 `, 1765 }, 1766 preferencesModal: { 1767 title: "Manage cookie preferences", 1768 acceptAllBtn: "Accept all", 1769 acceptNecessaryBtn: "Reject all", 1770 savePreferencesBtn: "Accept current selection", 1771 closeIconLabel: "Close modal", 1772 sections: [ 1773 { 1774 title: "Cookie usage", 1775 description: 1776 "We use cookies for specific purposes: essential site operation, analytics, and enhanced functionality. When you enable optional categories, data may be processed by third-parties such as Google, and embedded content providers. Self-hosted typography, Font Awesome load typography essential to our site design. See our privacy policy for full details.", 1777 }, 1778 { 1779 title: "Strictly necessary cookies", 1780 description: 1781 "These cookies and services are essential for the proper functioning of the website, for example for user authentication. Typography (Google Fonts) is self-hosted on our servers and does not send data to Google. Font Awesome loads icon fonts essential to site design and is treated as strictly necessary.", 1782 linkedCategory: CAT_NECESSARY, 1783 }, 1784 { 1785 title: "Analytics", 1786 description: 1787 "Cookies used for analytics help collect data that allows services to understand how users interact with a particular service. Data may be shared with third-parties such as Google Analytics.", 1788 linkedCategory: CAT_ANALYTICS, 1789 cookieTable: { 1790 headers: { 1791 name: "Name", 1792 domain: "Service", 1793 description: "Description", 1794 expiration: "Expiration", 1795 }, 1796 body: [ 1797 { 1798 name: "_ga", 1799 domain: "Google Analytics", 1800 description: 1801 "Distinguishes unique users via a randomly generated client ID.", 1802 expiration: "2 years", 1803 }, 1804 { 1805 name: "_ga_M3GFFYS5D0", 1806 domain: "Google Analytics 4", 1807 description: 1808 "Persists session state for a GA4 property.", 1809 expiration: "2 years", 1810 }, 1811 { 1812 name: "_ga_PYXQQYK45B", 1813 domain: "Google Analytics 4", 1814 description: 1815 "Persists session state for a GA4 property.", 1816 expiration: "2 years", 1817 }, 1818 { 1819 name: "_gat_gtag_UA_215523884_8", 1820 domain: "Google Analytics", 1821 description: 1822 "Throttles request rate to Google Analytics.", 1823 expiration: "1 minute", 1824 }, 1825 { 1826 name: "_gid", 1827 domain: "Google Analytics", 1828 description: 1829 "Distinguishes users for the current day.", 1830 expiration: "24 hours", 1831 }, 1832 ], 1833 }, 1834 }, 1835 { 1836 title: "Functionality", 1837 description: 1838 "Cookies used for functionality enable enhanced features such as embedded YouTube, Vimeo, Google Maps, Spotify, and SoundCloud content. These third-parties may set their own cookies when enabled.", 1839 linkedCategory: CAT_FUNCTIONALITY, 1840 }, 1841 { 1842 title: "Security", 1843 description: 1844 "Security verification (Google reCAPTCHA) loads only when you enable it for protected forms.", 1845 linkedCategory: CAT_SECURITY, 1846 }, 1847 { 1848 title: "More information", 1849 description: `Read our <a href="${SITE_CONFIG.privacyPolicyUrl}">Privacy Policy</a> for full details on purposes, third-parties, and data retention. You may withdraw or change your consent at any time using Manage preferences above or the Cookie Settings link in our site footer. For other queries, <a href="${SITE_CONFIG.contactUrl}">contact us</a>.`, 1850 }, 1851 ], 1852 }, 1853 }, 1854 }, 1855 }, 1856}); 1857 1858window.showCookiePreferences = () => CookieConsent.showPreferences(); 1859updateWidgetPlaceholders(); 1860neutralizeLegacyRecaptchaScripts();
1861ensureCaptchaMetadata(); 1862ensureCaptchaSubmitNotice(); 1863ensureConsentGatedButtonClick(); 1864ensureConsentGatedInitiateCaptcha(); 1865ensureConsentGatedFormSubmit(); 1866updateCaptchaConsent(); 1867window.enableRecaptcha = enableRecaptcha; 1868window.activateRecaptcha = activateRecaptcha; 1869window.isRecaptchaAccepted = isRecaptchaAccepted; 1870window.submitContactFormWithRecaptcha = submitContactFormWithRecaptcha; 1871window.ensureConsentGatedButtonClick = ensureConsentGatedButtonClick; 1872window.ensureConsentGatedInitiateCaptcha = ensureConsentGatedInitiateCaptcha; 1873window.ensureConsentGatedFormSubmit = ensureConsentGatedFormSubmit; 1874new MutationObserver(() => { 1875 neutralizeLegacyRecaptchaScripts(); 1876 ensureCaptchaMetadata(); 1877 ensureCaptchaSubmitNotice(); 1878 ensureConsentGatedButtonClick(); 1879 ensureConsentGatedInitiateCaptcha(); 1880 ensureConsentGatedFormSubmit(); 1881}).observe( 1882 document.documentElement, 1883 { childList: true, subtree: true } 1884); 1885 1886function bootstrapCookieConsentFromIm() { 1887 const serviceCategory = { 1888 youtube: CAT_FUNCTIONALITY, 1889 vimeo: CAT_FUNCTIONALITY, 1890 googlemaps: CAT_FUNCTIONALITY, 1891 spotify: CAT_FUNCTIONALITY, 1892 soundcloud: CAT_FUNCTIONALITY, 1893 facebook_social: CAT_FUNCTIONALITY, 1894 investing_widget: CAT_ADVERTISEMENT, 1895 calendly: CAT_FUNCTIONALITY, 1896 }; 1897 const imAccepted = im.getState().acceptedServices || []; 1898 for (const svc of imAccepted) { 1899 const category = serviceCategory[svc] || CAT_FUNCTIONALITY; 1900 if (widgetConsentGranted(svc, category)) continue; 1901 const prefs = CookieConsent.getUserPreferences(); 1902 CookieConsent.acceptCategory([ 1903 ...new Set([CAT_NECESSARY, ...(prefs.acceptedCategories || []), category]), 1904 ]); 1905 CookieConsent.acceptService( 1906 [...new Set([...(prefs.acceptedServices[category] || []), svc])], 1907 category, 1908 ); 1909 } 1910} 1911bootstrapCookieConsentFromIm(); 1912syncIframeManagerConsent();
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.