1/*! 2 * @author Dominik Kressler 3 * @version 3.11.0 4 * @description control scripts and iframes that cause in gdpr related problems 5 * @dependencies [email protected], [email protected] 6 **/ 7class PrivacyControl { 8 options = {}; 9 #deps = {}; 10 #elms = {}; 11 #interacted = false; 12 #lang = "en"; 13 #loaded = {}; 14 #localStorage = undefined; 15 initializing = true; 16 eventQueue = []; 17 version = "3.11.0"; 18 isDialogVisible = false; 19 static defaultOptions = { 20 badge: true, 21 badgePos: "vert-left", 22 force: false, 23 hideDialog: false, 24 id: "privacy-control", 25 noteDelay: 300, 26 safeSurfer: true, 27 sessionName: "privacy-control", 28 placeholder: "/securewebapps/evercdn/assets/privacyControl/v3/placeholder/index.html", 29 }; 30 static notification = window?.notification; 31 32 constructor(options = {}) { 33 /* prevent multiple instances */ 34 if (window?.pCinit !== undefined) return false; 35 else window.pCinit = true; 36 37 this.options = { 38 ...PrivacyControl.defaultOptions, 39 ...options, 40 }; 41 42 this.#setUpLocalStorage(); 43 44 /** get doc language and set instance language */ 45 this.#lang = 46 this.options?.lang || 47 document.documentElement.lang.split("-")[0].toLowerCase(); 48 /** unknown languages keep their code but alias en translations */ 49 if (pCl10n[this.#lang] === undefined) { 50 pCl10n[this.#lang] = pCl10n.en; 51 if (typeof pCServiceTemplates === "object") { 52 for (const tplKey in pCServiceTemplates) { 53 const tpl = pCServiceTemplates[tplKey]; 54 if ( 55 tpl?.l10n && 56 tpl.l10n[this.#lang] === undefined && 57 tpl.l10n.en 58 ) { 59 tpl.l10n[this.#lang] = tpl.l10n.en; 60 } 61 } 62 } 63 } 64 65 /** did the user already interact? **/ 66 this.#interacted = 67 this.#localStorage?.getItem(this.options.sessionName) !== null; 68 69 /** create safeSurfer if enabled */ 70 if (this.options.safeSurfer) 71 this.#elms.safeSurferBadge = this.#createSafeSurferBadge(); 72 73 this.#elms.dialog = this.#createDialog(); 74 75 PrivacyControl.notification.add({ 76 sessionName: this.options.sessionName, 77 id: this.options.id, 78 message: this.#elms.dialog, 79 saveState: !this.options.force, 80 remove: false, 81 delay: 0, 82 focus: true, 83 role: "dialog", 84 label: this.#getText("badge"), 85 discard: true, 86 }); 87 88 /** event to open dialog from everywhere */ 89 for (let link of document?.querySelectorAll( 90 'a[href$="#privacy-controller"]' 91 )) { 92 ["click", "keydown"].forEach((eventType) => { 93 link.addEventListener( 94 eventType, 95 this.#handleOpenDialogLinks.bind(this) 96 ); 97 }); 98 } 99 100 /* closeSettings is deprecated, so we keep it as alias */ 101 this.closeSettings = this.closeDialog; 102 103 window.dispatchEvent(new CustomEvent("PCInit", { detail: this })); 104 window.privacyControl = this; 105 106 /** complete initialization after current execution context */ 107 queueMicrotask(() => { 108 this.initializing = false; 109 this.#flushEventQueue(); 110 }); 111 } 112 113 #handleOpenDialogLinks(event) { 114 if (PrivacyControl.isEventClickEnterSpace(event)) this.openDialog(); 115 } 116 #handleOpenSettingsLinks(event) { 117 if (PrivacyControl.isEventClickEnterSpace(event)) this.openSettings(); 118 } 119 #handleDirectSetterLink(event) { 120 if (PrivacyControl.isEventClickEnterSpace(event)) { 121 event.preventDefault(); 122 event.stopPropagation(); 123 const key = event.target.dataset.privacyControl; 124 this.set(key, true); 125 /** a link may address a sub consent, forceReload sits on the service */ 126 if (this.#deps[this.#resolveKey(key).parent]?.forceReload === true) 127 window.location.reload(); 128 } 129 } 130 #handleResetButton(event) { 131 if (PrivacyControl.isEventClickEnterSpace(event)) { 132 this.#localStorage.setItem(this.options.sessionName, ""); 133 window.location.reload(); 134 } 135 } 136 #handleSetterInIframe(event) { 137 if (PrivacyControl.isEventClickEnterSpace(event)) { 138 let key = 139 document.activeElement.dataset.iframe || 140 document.activeElement.dataset.pcService; 141 this?.set(key, true); 142 if (this.#deps[key]?.forceReload === true) window.location.reload(); 143 } 144 } 145 146 #setUpLocalStorage() { 147 /** check if localStorage exists and create fallback */ 148 try { 149 window.localStorage.setItem("_pC_test", "1"); 150 window.localStorage.removeItem("_pC_test"); 151 window._storageDisabled = !1; 152 this.#localStorage = window.localStorage; 153 } catch (e) { 154 this.#localStorage = { 155 _items: new Map(), 156 setItem(e, t) { 157 this._items.set(e, t); 158 }, 159 getItem(e) { 160 return this._items.has(e) ? this._items.get(e) : null; 161 }, 162 }; 163 window._storageDisabled = true; 164 165 console.log( 166 "sessionStorage and/or localStorage not available, falling back to in-memory storage: ", 167 e 168 ); 169 } 170 } 171 172 #dispatchOrQueue(eventName, detail = {}) {
173 /** dispatch event immediately or queue if still initializing */ 174 const event = new CustomEvent(eventName, { detail }); 175 176 if (this.initializing) { 177 this.eventQueue.push(event); 178 } else { 179 window.dispatchEvent(event); 180 } 181 } 182 183 #flushEventQueue() { 184 /** dispatch all queued events after initialization */ 185 while (this.eventQueue.length > 0) { 186 const event = this.eventQueue.shift(); 187 window.dispatchEvent(event); 188 } 189 } 190 191 static isEventClickEnterSpace(e) { 192 return ( 193 e.type === "click" || 194 (e.type === "keydown" && (e.key === "Enter" || e.code === "Space")) 195 ); 196 } 197 198 #createSafeSurferBadge() { 199 const badge = this.#customTag({ 200 name: "badge", 201 target: document.body, 202 attrs: { 203 "aria-expanded": "false", 204 "tabindex": "0", 205 "role": "button", 206 "aria-label": this.#getText("safeSurferLabel"), 207 }, 208 innerHTML: `${pCsafeSurfer.icon} <span class="safe-surfer-msg">${this.#getText("safeSurfer")}</span>`, 209 classList: ["safe-surfer-badge"], 210 }); 211 212 ["click", "keydown", "focusout"].forEach((eventType) => { 213 badge.addEventListener(eventType, function (e) { 214 if (PrivacyControl.isEventClickEnterSpace(e)) { 215 e.preventDefault(); 216 e.stopPropagation(); 217 this.classList.toggle("expand"); 218 this.ariaExpanded = 219 this.ariaExpanded === "false" ? "true" : "false"; 220 } 221 if (e.type === "focusout") { 222 e.target.classList.remove("expand"); 223 e.target.ariaExpanded = "false"; 224 } 225 }); 226 }); 227 228 return badge; 229 } 230 231 #getText(key = "", service) { 232 /** get text from language object */ 233 let string = ""; 234 try { 235 if ( 236 service !== undefined && 237 this.options.deps[service].l10n[this.#lang][key] !== undefined 238 ) { 239 string = this.options.deps[service].l10n[this.#lang][key]; 240 } else if ( 241 typeof this.options[key] === "object" && 242 this.options[key][this.#lang] !== undefined 243 ) { 244 string = this.options[key][this.#lang]; 245 } else if (pCl10n[this.#lang][key] !== undefined) { 246 string = pCl10n[this.#lang][key]; 247 } 248 return string; 249 } catch (error) { 250 console.log("Translation string missing for: " + key); 251 return false; 252 } 253 } 254 255 #serviceStackNeedsReload() { 256 let reloadNeeded = false; 257 258 for (let key in this.#deps) { 259 if (this.#deps[key].forceReload === true) reloadNeeded = true; 260 } 261 262 if (reloadNeeded) window.location.reload(); 263 264 return reloadNeeded; 265 } 266 267 #createDialog() { 268 const instance = this; 269 /** create the main pc dialog */ 270 const dialog = this.#customTag({ name: "privacy-controller" }); 271 dialog.role = "form"; 272 dialog.id = "privacy-controller"; 273 274 const header = this.#customTag({ 275 name: "privacy-header", 276 target: dialog, 277 }); 278 header.id = "privacy-header"; 279 header.innerHTML = `<privacy-head role="heading" aria-level="1">${this.#getText("intro")}</privacy-head>`; 280 const buttons = this.#customTag({ 281 name: "privacy-buttons", 282 target: header, 283 }); 284 const buttonOpen = this.#customTag({ 285 name: "privacy-btn", 286 target: buttons, 287 attrs: { 288 tabindex: "0", 289 role: "button", 290 id: "privacy-open", 291 href: "#privacy-controller", 292 }, 293 innerHTML: this.#getText("settings"), 294 }); 295 ["click", "keydown"].forEach((eventType) => { 296 buttonOpen.addEventListener(eventType, function (e) { 297 if (PrivacyControl.isEventClickEnterSpace(e)) 298 instance.openSettings(); 299 }); 300 }); 301 this.#elms.openSettings = buttonOpen; 302 303 const fields = this.#customTag({ 304 name: "privacy-fields", 305 target: dialog, 306 attrs: { 307 "role": "table", 308 "aria-label": this.#getText("edit_settings"), 309 }, 310 }); 311 this.#elms.serviceContainer = fields; 312 const submitButtons = this.#customTag({ 313 name: "privacy-submits", 314 target: dialog, 315 }); 316 const buttonSave = this.#customTag({ 317 name: "privacy-btn", 318 target: submitButtons, 319 attrs: { 320 tabindex: "0", 321 role: "button", 322 id: "privacy-save", 323 }, 324 innerHTML: this.#getText("save"), 325 }); 326 ["click", "keydown"].forEach((eventType) => { 327 buttonSave.addEventListener(eventType, function (e) { 328 if (PrivacyControl.isEventClickEnterSpace(e)) { 329 let services = 330 instance.#elms.serviceContainer.querySelectorAll( 331 'input[type="checkbox"]' 332 ); 333 let forceAReload = false;
334 for (const service of services) { 335 /** a parent with sub consents follows from its sub toggles */ 336 if (service.dataset.pcHasConsents !== undefined) 337 continue; 338 339 const parentKey = 340 service.dataset.pcParent || service.name; 341 if (instance.#deps[parentKey].forceReload === true) 342 forceAReload = true; 343 instance.set(service.name, service.checked); 344 } 345 if (forceAReload) window.location.reload(); 346 instance.closeDialog(e); 347 } 348 }); 349 }); 350 const buttonConfirmAll = this.#customTag({ 351 name: "privacy-btn", 352 target: submitButtons, 353 attrs: { 354 tabindex: "0", 355 role: "button", 356 id: "privacy-confirm", 357 href: "#privacy-controller", 358 }, 359 innerHTML: this.#getText("confirm"), 360 }); 361 ["click", "keydown"].forEach((eventType) => { 362 buttonConfirmAll.addEventListener(eventType, function (e) { 363 if (PrivacyControl.isEventClickEnterSpace(e)) { 364 for (let service in instance.#deps) { 365 instance.set(service, true); 366 } 367 instance.#serviceStackNeedsReload(); 368 instance.closeDialog(e); 369 } 370 }); 371 }); 372 373 const footer = this.#customTag({ 374 name: "privacy-footer", 375 target: dialog, 376 }); 377 const reset = this.#customTag({ 378 name: "privacy-link", 379 target: footer, 380 attrs: { tabindex: "0", role: "link", id: "privacy-reset" }, 381 innerHTML: this.#getText("reset"), 382 classList: ["privacy-reset"], 383 }); 384 ["click", "keydown"].forEach((eventType) => { 385 reset.addEventListener( 386 eventType, 387 this.#handleResetButton.bind(this) 388 ); 389 }); 390 391 const legal_page = this.#getText("legal_page"); 392 if (legal_page && legal_page != "") { 393 const imprint = this.#customTag({ 394 name: "privacy-link", 395 target: footer, 396 attrs: { 397 tabindex: "0", 398 role: "link", 399 href: legal_page, 400 }, 401 innerHTML: this.#getText("legal_notice"), 402 }); 403 ["click", "keydown"].forEach((eventType) => { 404 imprint.addEventListener(eventType, function (e) { 405 if (PrivacyControl.isEventClickEnterSpace(e)) 406 window.location = this.getAttribute("href"); 407 }); 408 }); 409 } 410 const privacy_page = this.#getText("privacy_page"); 411 if (privacy_page && privacy_page != "") { 412 const policy = this.#customTag({ 413 name: "privacy-link", 414 target: footer, 415 attrs: { 416 tabindex: "0", 417 role: "link", 418 href: privacy_page, 419 }, 420 innerHTML: this.#getText("policy"), 421 }); 422 ["click", "keydown"].forEach((eventType) => { 423 policy.addEventListener(eventType, function (e) { 424 if (PrivacyControl.isEventClickEnterSpace(e)) 425 window.location = this.getAttribute("href"); 426 }); 427 }); 428 } 429 430 return dialog; 431 } 432 433 #customTag( 434 props = { 435 name: "", 436 target: null, 437 prepend: false, 438 attrs: {}, 439 innerHTML: null, 440 } 441 ) { 442 const elm = document.createElement(props?.name); 443 elm.classList.add(props?.name); 444 445 if (props?.target) { 446 if (!props?.prepend) props?.target?.appendChild(elm);
447 else props?.target?.prepend(elm); 448 } 449 450 if (props?.attrs && Object.keys(props?.attrs)?.length) { 451 for (let attr in props?.attrs) { 452 elm.setAttribute(attr, props?.attrs[attr]); 453 } 454 } 455 456 if (props?.innerHTML) elm.innerHTML = props?.innerHTML; 457 458 if (props?.classList && props.classList instanceof Array) { 459 for (let className of props?.classList) { 460 elm.classList.add(className); 461 } 462 } 463 464 return elm; 465 } 466 467 /** create the classic pc badge */ 468 #createPCBadge() { 469 const badge = this.#customTag({ 470 name: "badge", 471 target: document.body, 472 attrs: { 473 "role": "button", 474 "tabindex": "0", 475 "aria-controls": this.options.id, 476 }, 477 innerHTML: `<span>${this.#getText("badge")}</span>`, 478 classList: ["privacy-control-badge", this.options.badgePos], 479 }); 480 481 ["click", "keydown"].forEach((eventType) => { 482 badge.addEventListener( 483 eventType, 484 this.#handleOpenSettingsLinks.bind(this) 485 ); 486 }); 487 488 return badge; 489 } 490 491 static #mergeDeps(obj1 = {}, obj2 = {}) { 492 for (let dep in obj2) { 493 if (obj2[dep] === false) delete obj1[dep]; 494 } 495 return obj1; 496 } 497 498 static serviceCallback(callback) { 499 if (typeof callback == "function") { 500 return callback(); 501 } 502 503 return false; 504 } 505 506 #isLegalPage() { 507 let found = 0; 508 const legal = pCl10n[this.#lang].legal_page; 509 const priv = pCl10n[this.#lang].privacy_page; 510 511 if ( 512 legal == window.location.pathname || 513 priv == window.location.pathname 514 ) 515 found++; 516 517 return found; 518 } 519 520 #getStorage() { 521 let storage = this.#localStorage.getItem(this.options.sessionName); 522 return storage === null || storage === "" || storage === false 523 ? {} 524 : JSON.parse(storage); 525 } 526 527 #resolveKey(key = "") { 528 /** split "parent.sub" into its parts, plain service keys stay untouched */ 529 const plain = { key: key, parent: key, sub: null }; 530 531 if (pCServiceTemplates[key]) return plain; 532 533 const separator = key.lastIndexOf("."); 534 if (separator < 1) return plain; 535 536 const parent = key.slice(0, separator); 537 const sub = key.slice(separator + 1); 538 const consents = pCServiceTemplates[parent]?.consents; 539 540 if ( 541 Array.isArray(consents) && 542 consents.some((consent) => consent.key === sub) 543 ) 544 return { key: key, parent: parent, sub: sub }; 545 546 return plain; 547 } 548 549 #consentKeys(parentKey = "") { 550 /** full storage keys of every sub consent of a service */ 551 const consents = pCServiceTemplates[parentKey]?.consents; 552 if (!Array.isArray(consents)) return []; 553 554 return consents.map((consent) => `${parentKey}.${consent.key}`); 555 } 556 557 #reflectCheckbox(key, status) { 558 /** mirror a consent state onto its checkbox and status label */ 559 const checkbox = this.#elms.serviceContainer.querySelector( 560 `input[name="${key}"]` 561 ); 562 563 checkbox.checked = status; 564 checkbox.indeterminate = false; 565 checkbox.nextSibling.innerText = status 566 ? this.#getText("on") 567 : this.#getText("off"); 568 569 return checkbox; 570 } 571 572 #syncConsentGroup(parentKey) { 573 /** the parent toggle mirrors OR() over its sub consents */ 574 const consentKeys = this.#consentKeys(parentKey); 575 if (!consentKeys.length) return false; 576 577 const granted = consentKeys.filter( 578 (consentKey) => 579 this.#elms.serviceContainer.querySelector( 580 `input[name="${consentKey}"]` 581 ).checked 582 ); 583 const checkbox = this.#elms.serviceContainer.querySelector( 584 `input[name="${parentKey}"]` 585 ); 586 587 checkbox.checked = granted.length > 0; 588 checkbox.indeterminate = 589 granted.length > 0 && granted.length < consentKeys.length; 590 591 if (checkbox.indeterminate) 592 checkbox.nextSibling.innerText = this.#getText("on_partial"); 593 else 594 checkbox.nextSibling.innerText = checkbox.checked 595 ? this.#getText("on") 596 : this.#getText("off"); 597 598 return true; 599 } 600 601 #customCells(cell1, cell2, target) { 602 return [ 603 this.#customTag({ 604 name: "privacy-cell", 605 target: target, 606 innerHTML: cell1, 607 }), 608 this.#customTag({ 609 name: "privacy-cell", 610 target: target, 611 innerHTML: cell2, 612 }), 613 ]; 614 } 615 616 #customRow(label = "", target = null) { 617 return this.#customTag({ 618 name: "privacy-row", 619 target: target, 620 attrs: { "aria-label": label, "role": "row" }, 621 classList: ["privacy-row"], 622 }); 623 } 624 625 #setRequiredMode() { 626 this.openDialog(); 627 628 if (this.#isLegalPage() === 0) { 629 const html = document.querySelector("html"); 630 if (html.dataset.privacyControl.indexOf("center") == -1) 631 html.dataset.privacyControl += " center"; 632 if (html.dataset.privacyControl.indexOf("backdrop") == -1) 633 html.dataset.privacyControl += " backdrop"; 634 } 635 } 636 637 #createServiceMarkup(service) { 638 try { 639 const isRequired = 640 typeof service.required != "undefined" && 641 service.required == true; 642 const group = this.#customTag({ 643 name: "privacy-group", 644 target: this.#elms.serviceContainer, 645 prepend: isRequired, 646 attrs: { 647 "role": "rowgroup", 648 "aria-label": this.#getText("service") + " " + service.name, 649 }, 650 }); 651 const id = this.options.sessionName + ":" + service.key; 652 653 if (typeof service.name !== "undefined") { 654 this.#customCells( 655 this.#getText("name"), 656 service.name, 657 this.#customRow( 658 `${this.#getText("service")} ${this.#getText("name")}: ${service.name}`, 659 group 660 ) 661 ); 662 } 663 664 /* this is the first row with the checkbox */
665 const row = this.#customTag({ 666 name: "privacy-row", 667 target: group, 668 attrs: { "aria-label": this.#getText("settings") }, 669 }); 670 const cell1 = this.#customTag({ 671 name: "privacy-cell", 672 target: row, 673 innerHTML: this.#getText("accept"), 674 }); 675 const cell2 = this.#customTag({ 676 name: "privacy-cell", 677 target: row, 678 attrs: { "aria-label": this.#getText("policy") }, 679 }); 680 681 const preChecked = 682 typeof this.get(service.key) === "undefined" && 683 service.softRequired === true 684 ? "checked" 685 : ""; 686 const status = 687 typeof this.get(service.key) === "undefined" && 688 service.softRequired === true 689 ? this.#getText("on") 690 : this.#getText("off"); 691 692 cell2.innerHTML = `<label for="${id}" id="${id}-label" aria-label="${service.name}: ${this.#getText("accept")}"><input ${preChecked} aria-label="${service.name}: ${this.#getText("accept")}" id="${id}" name="${service.key}" type="checkbox" tabindex="0" /><service-status class="service-status" aria-hidden="true">${status}</service-status></label>`; 693 694 /* adjust markup for required services */ 695 if (isRequired) { 696 if (!this.#interacted) this.#setRequiredMode(); 697 698 cell2.innerHTML = `<label for="${id}" id="${id}-label"><input data-disabled disabled tabindex="0" checked type="checkbox" aria-label="${service.name}: ${this.#getText("accept")}" id="${id}" name="${service.key}" /><service-status class="service-status" aria-hidden="true">${this.#getText("on")}</service-status></label>`; 699 700 cell2.querySelector("service-status").innerText = 701 this.#getText("on_forced"); 702 } 703 704 /* sub consents get their own toggles right below the main one */ 705 const consents = Array.isArray(service.consents) 706 ? service.consents 707 : []; 708 if (consents.length) { 709 cell2 710 .querySelector(`[name="${service.key}"]`) 711 .setAttribute("data-pc-has-consents", ""); 712 713 const consentGroup = this.#customTag({ 714 name: "privacy-consents", 715 target: group, 716 attrs: { 717 "role": "rowgroup", 718 "aria-label": `${service.name}: ${this.#getText("purpose")}`, 719 }, 720 }); 721 /* sub toggles of a required service stay locked like their parent */ 722 const consentChecked = isRequired 723 ? "data-disabled disabled checked" 724 : preChecked; 725 const consentStatus = isRequired 726 ? this.#getText("on_forced") 727 : status; 728 729 for (const consent of consents) { 730 const consentKey = `${service.key}.${consent.key}`; 731 const consentId = `${id}.${consent.key}`; 732 const consentL10n = 733 consent.l10n?.[this.#lang] || consent.l10n?.en || {}; 734 const consentName = consentL10n.name || consent.key; 735 const cells = this.#customCells( 736 consentName, 737 "", 738 this.#customRow( 739 `${service.name}: ${consentName}`, 740 consentGroup 741 ) 742 ); 743 744 if (consentL10n.description !== undefined) 745 this.#customTag({ 746 name: "privacy-consent-hint", 747 target: cells[0], 748 innerHTML: consentL10n.description, 749 }); 750 751 cells[1].innerHTML = `<label for="${consentId}" id="${consentId}-label" aria-label="${consentName}: ${this.#getText("accept")}"><input ${consentChecked} aria-label="${consentName}: ${this.#getText("accept")}" id="${consentId}" name="${consentKey}" data-pc-parent="${service.key}" type="checkbox" tabindex="0" /><service-status class="service-status" aria-hidden="true">${consentStatus}</service-status></label>`; 752 } 753 } 754 755 /* the other rows */ 756 757 if (typeof service.provider !== "undefined") { 758 this.#customCells( 759 this.#getText("provider"), 760 service.provider, 761 this.#customRow( 762 `${this.#getText("service")} ${this.#getText("provider")}: ${service.provider}`, 763 group 764 ) 765 ); 766 } 767 if (typeof service.l10n[this.#lang].description !== "undefined") { 768 this.#customCells( 769 this.#getText("purpose"), 770 service.l10n[this.#lang].description, 771 this.#customRow( 772 `${this.#getText("service")} ${this.#getText("purpose")}: ${service.l10n[this.#lang].description}`, 773 group 774 ) 775 ); 776 } 777 if ( 778 typeof service.l10n[this.#lang].requirement !== "undefined" && 779 service.required === true 780 ) { 781 this.#customCells( 782 this.#getText("required"), 783 service.l10n[this.#lang].requirement, 784 this.#customRow( 785 `${this.#getText("service")} ${this.#getText("required")}: ${service.l10n[this.#lang].requirement}`, 786 group 787 ) 788 ); 789 } 790 if (typeof service.l10n[this.#lang].policy !== "undefined") { 791 this.#customCells( 792 this.#getText("policy"), 793 `<a href="${service.l10n[this.#lang].policy}" role="link" target="_blank" title="${this.#getText("policy")}" aria-label="${this.#getText("policy")} ${service.name}">${service.name}</a>`, 794 this.#customRow( 795 `${this.#getText("service")} ${this.#getText("policy")}: Link`, 796 group 797 ) 798 ); 799 } 800 801 /* listen to checkbox to change label */ 802 group 803 .querySelector(`[name="${service.key}"]`) 804 .addEventListener("change", (e) => { 805 e.target.indeterminate = false;
806 e.target.nextSibling.innerText = e.target.checked 807 ? this.#getText("on") 808 : this.#getText("off"); 809 810 /* the parent toggle carries all of its sub consents along */ 811 for (const consent of consents) { 812 const consentBox = group.querySelector( 813 `[name="${service.key}.${consent.key}"]` 814 ); 815 consentBox.checked = e.target.checked; 816 consentBox.nextSibling.innerText = e.target.checked 817 ? this.#getText("on") 818 : this.#getText("off"); 819 } 820 }); 821 822 for (const consent of consents) { 823 group 824 .querySelector(`[name="${service.key}.${consent.key}"]`) 825 .addEventListener("change", (e) => { 826 e.target.nextSibling.innerText = e.target.checked 827 ? this.#getText("on") 828 : this.#getText("off"); 829 830 this.#syncConsentGroup(service.key); 831 }); 832 } 833 834 return group; 835 } catch (error) { 836 console.error( 837 "addService failed, because your service object is missing some values!", 838 error 839 ); 840 return false; 841 } 842 } 843 844 #bindIframeEvents(doc) { 845 const instance = this; 846 const infoOpen = doc.querySelector(".infoOpen"); 847 if (infoOpen) { 848 ["click", "keydown"].forEach((eventType) => { 849 infoOpen.addEventListener(eventType, function (e) { 850 if (PrivacyControl.isEventClickEnterSpace(e)) { 851 doc.querySelector(".infoBox").classList.add("open"); 852 doc.querySelector(".infoOpen").classList.add("hide"); 853 } 854 }); 855 }); 856 } 857 858 const infoClose = doc.querySelector(".infoClose"); 859 ["click", "keydown"].forEach((eventType) => { 860 infoClose.addEventListener(eventType, function (e) { 861 if (PrivacyControl.isEventClickEnterSpace(e)) { 862 doc.querySelector(".infoBox").classList.remove("open"); 863 doc.querySelector(".infoOpen").classList.remove("hide"); 864 } 865 }); 866 }); 867 868 const accept = doc.querySelector(".accept"); 869 ["click", "keydown"].forEach((eventType) => { 870 accept.addEventListener( 871 eventType, 872 this.#handleSetterInIframe.bind(this), 873 false 874 ); 875 }); 876 } 877 878 #loadDep(key, type) { 879 /** this loads the resources of the service */ 880 if (this.#loaded[key] === true) return false; 881 882 /** assets alias for src */ 883 if (!pCServiceTemplates[key].src && pCServiceTemplates[key].assets) 884 pCServiceTemplates[key].src = pCServiceTemplates[key].assets; 885 886 let scriptLoadIsAsync = false; 887 switch (type) { 888 case "script": 889 try { 890 if (pCServiceTemplates[key].src !== undefined) { 891 let srcs = pCServiceTemplates[key].src; 892 if (typeof srcs != "object") { 893 srcs = [srcs]; 894 } 895 896 scriptLoadIsAsync = true; 897 rIt({ 898 "*": { 899 assets: srcs, 900 function: () => { 901 PrivacyControl.serviceCallback( 902 pCServiceTemplates[key].callback 903 ); 904 this.#dispatchOrQueue("PCDepLoad", { 905 key: key, 906 }); 907 }, 908 }, 909 }); 910 } else { 911 const scripts = document.querySelectorAll( 912 `script[data-script="${key}"], script[data-pc-service="${key}"]` 913 ); 914 915 if (scripts.length > 0) { 916 scriptLoadIsAsync = true; 917 for (let i = 0, l = scripts.length; i < l; i++) { 918 if (i == l - 1) { 919 scripts[i].addEventListener("load", () => { 920 PrivacyControl.serviceCallback( 921 pCServiceTemplates[key].callback 922 ); 923 this.#dispatchOrQueue("PCDepLoad", { 924 key: key, 925 }); 926 }); 927 } 928 scripts[i].src = scripts[i].dataset.pcAsset; 929 } 930 } 931 } 932 933 /** check if placeholder exists and hide */ 934 let pCServicePlaceholder = document.querySelectorAll( 935 'iframe[data-pc-service="' + key + '"]' 936 ); 937 for (let elm of pCServicePlaceholder) { 938 elm.classList.add("enabled", "hide"); 939 } 940 } catch (error) { 941 console.log( 942 `Could not load script for ${key}, no src defined.` 943 ); 944 } 945 946 break; 947 case "iframe": 948 const iframes = document.querySelectorAll( 949 `[data-iframe="${key}"], iframe[data-pc-service="${key}"]` 950 ); 951 952 for (let iframe of iframes) { 953 if (iframe.dataset) { 954 let src = iframe.dataset.pcAsset || iframe.dataset.src; 955 956 iframe.src = src; 957 iframe.classList.add("enabled", "pcLoaded"); 958 iframe.classList.remove("pcUnloaded"); 959 iframe.dataset.frameId = Date.now(); 960 } 961 } 962 963 PrivacyControl.serviceCallback( 964 pCServiceTemplates[key].callback 965 ); 966 967 break; 968 case "image": 969 let images = document.querySelectorAll( 970 `img[data-pc-service="${key}"]` 971 ); 972 for (let img of images) { 973 img.src = img.dataset.pcAsset; 974 img.classList.add("pcLoaded"); 975 img.classList.remove("pcUnloaded"); 976 } 977 978 /** check if placeholder exists and hide */ 979 let pCServicePlaceholder = document.querySelectorAll( 980 `iframe[data-pc-service="${key}"]` 981 ); 982 for (let elm of pCServicePlaceholder) { 983 elm.classList.add("enabled", "hide"); 984 } 985 986 PrivacyControl.serviceCallback( 987 pCServiceTemplates[key].callback 988 ); 989 990 break; 991 } 992 993 this.#loaded[key] = true; 994 995 if (type !== "script" || !scriptLoadIsAsync) { 996 this.#dispatchOrQueue("PCDepLoad", { key: key }); 997 } 998 999 return true; 1000 } 1001 1002 #unloadDep(key, type) { 1003 if (this.#loaded[key] === false) return false;
1004 1005 let pCServicePlaceholder = document.querySelectorAll( 1006 `iframe[data-pc-service="${key}"]` 1007 ); 1008 1009 switch (type) { 1010 case "iframe": 1011 let iframes = document.querySelectorAll( 1012 `[data-iframe="${key}"], iframe[data-pc-service="${key}"]` 1013 ); 1014 for (let iframe of iframes) { 1015 if (iframe.dataset) { 1016 iframe.classList.remove("enabled", "pcLoaded"); 1017 iframe.classList.add("pcUnloaded"); 1018 } 1019 } 1020 break; 1021 case "image": 1022 let images = document.querySelectorAll( 1023 `img[data-pc-service="${key}"]` 1024 ); 1025 for (let img of images) { 1026 img.src = ""; 1027 img.classList.remove("pcLoaded"); 1028 img.classList.add("pcUnloaded"); 1029 } 1030 1031 /** check if placeholder exists and show */ 1032 for (let elm of pCServicePlaceholder) { 1033 elm.classList.remove("enabled", "hide"); 1034 } 1035 break; 1036 case "script": 1037 /** check if placeholder exists and show */ 1038 for (let elm of pCServicePlaceholder) { 1039 elm.classList.remove("enabled", "hide"); 1040 } 1041 break; 1042 } 1043 this.iframePlaceholder(key); 1044 this.#loaded[key] = false; 1045 1046 //callback on disable 1047 if ( 1048 pCServiceTemplates[key].callbackOnDisable !== undefined && 1049 typeof pCServiceTemplates[key].callbackOnDisable == "function" 1050 ) { 1051 pCServiceTemplates[key].callbackOnDisable(); 1052 } else if ( 1053 pCServiceTemplates[key].callbackOnDisable !== undefined && 1054 typeof pCServiceTemplates[key].callbackOnDisable == "string" 1055 ) { 1056 window[pCServiceTemplates[key].callbackOnDisable](); 1057 } 1058 1059 this.#dispatchOrQueue("PCDepUnload", { key: key }); 1060 1061 return true; 1062 } 1063 1064 #handleFrameLoad(event) { 1065 const frame = event.target; 1066 if (frame.classList.contains("enabled")) return false; 1067 1068 const key = frame?.dataset?.pcService || frame?.dataset?.iframe; 1069 const doc = frame?.contentDocument || frame?.contentWindow?.document; 1070 const serviceObj = pCServiceTemplates[key]; 1071 1072 let policy = serviceObj.l10n[this.#lang].policy 1073 ? `<a href="${serviceObj.l10n[this.#lang].policy}" title="${this.#getText("policy")} ${serviceObj.name}" target="_blank">${serviceObj.l10n[this.#lang].policy}</a>` 1074 : ""; 1075 let bodyHTML = doc.body.innerHTML; 1076 bodyHTML = bodyHTML.replaceAll("{{policy}}", policy); 1077 bodyHTML = bodyHTML.replaceAll("{{name}}", serviceObj.name); 1078 bodyHTML = bodyHTML.replaceAll( 1079 "{{activate}}", 1080 this.#getText("activate") 1081 ); 1082 bodyHTML = bodyHTML.replaceAll( 1083 "{{msg}}", 1084 serviceObj.l10n[this.#lang].description 1085 ); 1086 bodyHTML = bodyHTML.replaceAll( 1087 "{{provider}}", 1088 this.#getText("provider") 1089 ); 1090 bodyHTML = bodyHTML.replaceAll("{{providerName}}", serviceObj.provider); 1091 bodyHTML = bodyHTML.replaceAll("{{key}}", key); 1092 bodyHTML = bodyHTML.replaceAll( 1093 "{{open_infobox}}", 1094 this.#getText("open_infobox") 1095 ); 1096 bodyHTML = bodyHTML.replaceAll( 1097 "{{close_infobox}}", 1098 this.#getText("close_infobox") 1099 ); 1100 doc.body.innerHTML = bodyHTML; 1101 1102 doc.head.innerHTML = doc.head.innerHTML.replaceAll( 1103 "{{iframe_title}}", 1104 `${this.#getText("settings")}: ${serviceObj.name}` 1105 ); 1106 doc.body.id = key; 1107 doc.body.classList.add("enabled"); 1108 1109 doc.querySelector("html").lang = this.#lang; 1110 1111 if (serviceObj.css) 1112 doc.head.innerHTML += `<link rel="stylesheet" href="${serviceObj.css}"></link>`; 1113 this.#bindIframeEvents(doc); 1114 } 1115 1116 iframePlaceholder(key) { 1117 const elms = document.querySelectorAll( 1118 `iframe[data-pc-service="${key}"]:not(.enabled):not(.src), iframe[data-iframe="${key}"]:not(.enabled):not(.src)` 1119 ); 1120 1121 for (let i = 0; i < elms.length; i++) { 1122 let frame = elms[i]; 1123 if (!frame.dataset.pcListenerBound) { 1124 frame.addEventListener( 1125 "load", 1126 this.#handleFrameLoad.bind(this) 1127 ); 1128 frame.dataset.pcListenerBound = "true"; 1129 } 1130 frame.dataset.lang = document.querySelector("html").lang; 1131 frame.src = `${this.options.placeholder}?key=${key}`; 1132 } 1133 return true; 1134 } 1135 1136 get(key = "") { 1137 /** get status of service from storage */ 1138 let storage = this.#getStorage(); 1139 if (key == "all") return storage; 1140 return storage[key]; 1141 } 1142 1143 set(key = "", status = false) { 1144 /** set a single service or one of its sub consents */ 1145 let storage = this.#getStorage(); 1146 try { 1147 const resolved = this.#resolveKey(key); 1148 const service = pCServiceTemplates[resolved.parent]; 1149 1150 if (!service) 1151 throw new Error( 1152 `Key "${key}" is missing in the PrivacyControl deps configuration object.` 1153 ); 1154 1155 const consentKeys = this.#consentKeys(resolved.parent); 1156 const status_before = storage[resolved.parent]; 1157 1158 storage[key] = status; 1159 this.#reflectCheckbox(key, status); 1160 1161 /** a parent carries its sub consents, a sub consent derives its parent */ 1162 if (resolved.sub === null) { 1163 for (const consentKey of consentKeys) { 1164 storage[consentKey] = status; 1165 this.#reflectCheckbox(consentKey, status); 1166 } 1167 } else { 1168 storage[resolved.parent] = consentKeys.some( 1169 (consentKey) => storage[consentKey] === true 1170 ); 1171 this.#syncConsentGroup(resolved.parent); 1172 } 1173 1174 const parentStatus = storage[resolved.parent]; 1175 let type = service.type; 1176 1177 if (parentStatus) this.#loadDep(resolved.parent, type); 1178 else this.#unloadDep(resolved.parent, type); 1179 1180 this.#localStorage.setItem( 1181 this.options.sessionName, 1182 JSON.stringify(storage) 1183 ); 1184 1185 //reload only on disable, and only if it was granted before 1186 if ( 1187 parentStatus == false && 1188 status_before === true && 1189 service.forceReloadOnDisable !== undefined && 1190 service.forceReloadOnDisable === true 1191 ) { 1192 window.location.reload(); 1193 } 1194 1195 /* hide consent buttons on page, sub consents carry their own links */ 1196 const grantedKeys = []; 1197 if (parentStatus) grantedKeys.push(resolved.parent); 1198 if (status && resolved.sub === null) 1199 grantedKeys.push(...consentKeys); 1200 else if (status) grantedKeys.push(key); 1201 1202 for (const grantedKey of grantedKeys) { 1203 let buttons = document.querySelectorAll(
1204 `a[data-privacy-control="${grantedKey}"]` 1205 ); 1206 for (let button of buttons) 1207 button.classList.add("activated", "granted"); 1208 } 1209 1210 this.#interacted = true; 1211 1212 const detail = { key: key, status: status }; 1213 if (resolved.sub !== null) detail.parent = resolved.parent; 1214 this.#dispatchOrQueue("PCOnSet", detail); 1215 1216 /** consumers listen per key, so derived changes are announced as well */ 1217 if (resolved.sub === null) { 1218 for (const consentKey of consentKeys) 1219 this.#dispatchOrQueue("PCOnSet", { 1220 key: consentKey, 1221 status: status, 1222 parent: resolved.parent, 1223 }); 1224 } else if (parentStatus !== status_before) { 1225 this.#dispatchOrQueue("PCOnSet", { 1226 key: resolved.parent, 1227 status: parentStatus, 1228 }); 1229 } 1230 } catch (error) { 1231 console.error(error); 1232 } 1233 1234 return storage; 1235 } 1236 1237 addService(service) { 1238 /** if safesurfer badge is there remove it */ 1239 if (this.#elms?.safeSurferBadge instanceof HTMLElement) { 1240 this.#elms.safeSurferBadge.remove(); 1241 delete this.#elms.safeSurferBadge; 1242 } 1243 /** if not already done and if badge is wanted create it */ 1244 if ( 1245 this.#elms.pCBadge instanceof HTMLElement == false && 1246 this.options.badge === true 1247 ) { 1248 this.#elms.pCBadge = this.#createPCBadge(); 1249 } 1250 1251 /** show the dialog if not suppressed and if user didn't interact */ 1252 if (!this.isDialogVisible) { 1253 const instance = this; 1254 if (this.options.hideDialog === false && !this.#interacted) { 1255 setTimeout(function () { 1256 instance.openDialog(); 1257 }, this.options.noteDelay); 1258 } 1259 } 1260 1261 /** create markup for service */ 1262 let serviceObj = false; 1263 /** if service is passed by user */ 1264 if ( 1265 typeof service === "object" && 1266 !Array.isArray(service) && 1267 service !== null 1268 ) { 1269 serviceObj = service; 1270 } 1271 /** if service option is string look for service template */ 1272 if ( 1273 typeof service === "string" && 1274 typeof pCServiceTemplates[service] === "object" && 1275 !Array.isArray(pCServiceTemplates[service]) 1276 ) { 1277 serviceObj = pCServiceTemplates[service]; 1278 } 1279 /** alias en translations onto the detected lang for services added at runtime */ 1280 if ( 1281 serviceObj?.l10n && 1282 serviceObj.l10n[this.#lang] === undefined && 1283 serviceObj.l10n.en 1284 ) { 1285 serviceObj.l10n[this.#lang] = serviceObj.l10n.en; 1286 } 1287 this.#createServiceMarkup(serviceObj); 1288 1289 /** add current service to service stack if unknown, else merge with users' passed data */ 1290 if (typeof pCServiceTemplates[service] !== "object") { 1291 pCServiceTemplates[service.key] = serviceObj; 1292 } else { 1293 pCServiceTemplates = PrivacyControl.#mergeDeps( 1294 pCServiceTemplates, 1295 serviceObj 1296 ); 1297 } 1298 1299 /** prepare the rest of the doc, e.g. iframes */ 1300 if (serviceObj && serviceObj.type) { 1301 switch (serviceObj.type) { 1302 case "iframe": 1303 case "image": 1304 case "script": 1305 this.iframePlaceholder(serviceObj.key); 1306 break; 1307 } 1308 } 1309 1310 this.#deps[serviceObj.key] = serviceObj; 1311 1312 /** load service if consent already given */ 1313 const consentKeys = this.#consentKeys(serviceObj.key); 1314 if (consentKeys.length === 0) { 1315 if (this.get(serviceObj.key)) this.set(serviceObj.key, true); 1316 } else if ( 1317 this.get(serviceObj.key) && 1318 consentKeys.every( 1319 (consentKey) => this.get(consentKey) === undefined 1320 ) 1321 ) { 1322 /** stored consent predates the sub consents, carry it over to all */ 1323 this.set(serviceObj.key, true); 1324 } else { 1325 /** restore each sub consent on its own, the parent follows from them */ 1326 for (const consentKey of consentKeys) { 1327 const stored = this.get(consentKey); 1328 if (stored === true) this.set(consentKey, true); 1329 else if (stored === false) 1330 this.#reflectCheckbox(consentKey, false); 1331 } 1332 this.#syncConsentGroup(serviceObj.key); 1333 } 1334 1335 /** link button event to directly enable a service or one of its purposes */ 1336 1337 let links = document.querySelectorAll( 1338 [serviceObj.key, ...consentKeys] 1339 .map((key) =>
1339 `a[data-privacy-control="${key}"]`) 1340 .join(", ") 1341 ); 1342 if (links) { 1343 for (let link of links) { 1344 ["click", "keydown"].forEach((eventType) => { 1345 link.addEventListener( 1346 eventType, 1347 this.#handleDirectSetterLink.bind(this) 1348 ); 1349 }); 1350 } 1351 } 1352 1353 this.#dispatchOrQueue("PCAddService", serviceObj); 1354 } 1355 1356 openDialog() { 1357 this.isDialogVisible = true; 1358 if (this.#elms.pCBadge) { 1359 this.#elms.pCBadge.tabIndex = "-1"; 1360 this.#elms.pCBadge.classList.add("hide"); 1361 } 1362 PrivacyControl.notification.open(this.options.id); 1363 document.body.classList.add("privacy-control-is-open"); 1364 1365 window.dispatchEvent(new CustomEvent("PCDialogOpen", { detail: this })); 1366 } 1367 1368 openSettings(e, focus = true) { 1369 if (e) { 1370 e.preventDefault(); 1371 e.stopPropagation(); 1372 } 1373 this.isDialogVisible = true; 1374 this.openDialog(); 1375 this.#elms.dialog.classList.add("open"); 1376 this.#elms.openSettings.classList.add("hide"); 1377 this.#elms.serviceContainer.setAttribute("tabindex", "0"); 1378 1379 window.dispatchEvent( 1380 new CustomEvent("PCSettingsOpen", { detail: this }) 1381 ); 1382 } 1383 1384 closeDialog(e) { 1385 if (e) { 1386 e.preventDefault(); 1387 e.stopPropagation(); 1388 } 1389 this.isDialogVisible = false; 1390 PrivacyControl.notification.close(this.options.id); 1391 document.body.classList.remove("privacy-control-is-open"); 1392 if (this.#elms.pCBadge) { 1393 this.#elms.pCBadge.classList.remove("hide"); 1394 this.#elms.pCBadge.tabIndex = "0"; 1395 } 1396 1397 window.dispatchEvent( 1398 new CustomEvent("PCDialogClose", { detail: this }) 1399 ); 1400 } 1401 1402 please(status = true) { 1403 Object.keys(this.#deps).forEach((dep) => { 1404 this.set(dep, status); 1405 }); 1406 } 1407}
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.