PageSourceSearch

https://www.bundesakademie-trossingen.de/_assets/b9a689cb028cb6925…/JavaScript/consent.js?1787736054

js bundesakademie-trossingen.de collected 2026-10-02 04:25:52 UTC 12,802 bytes, 326 lines download raw bytes

1/* ============================================================================
2   w52_consent — GDPR consent banner.
3
4   Reads its configuration (storage key, declaration version, expiry,
5   groups with their opt-in script URLs and cleanup rules) from the banner's
6   data-consent-config attribute; the values originate from the project's
7   YAML declaration (ConsentConfigurationProcessor).
8
9   The choice is stored in localStorage as
10     { "version": <int>, "date": "<ISO>", "groups": { "<key>": true|false } }
11   and expires after the configured number of months or whenever the
12   declaration version changes (re-consent).
13
14   Lifecycle per group
15     grant    Scripts of required groups always run; scripts of optional
16              groups load only after explicit opt-in — strictly in the
17              declared order (the next script is inserted once the previous
18              one has executed). A script that fails to load is retried the
19              next time the choice is applied; the rest of its group is
20              skipped for that run because a dependency is missing.
21     withdraw When a previously granted group is switched off, its declared
22              cookies and storage entries are deleted and the
23              "w52consent:changed" event lists the group in
24              detail.withdrawn so site scripts can remove their embeds.
25              A running script cannot be unloaded, therefore groups with
26              scripts reload the page after the withdrawal has been stored
27              (YAML: reloadOnWithdraw, defaults to true for groups with
28              scripts). Cookies/storage of groups without consent are also
29              swept on every page view in case a service re-created them.
30
31   The banner is a native <dialog> opened with showModal(): the page behind
32   it is inert, focus starts on the heading and returns to the element that
33   opened the banner (e.g. the footer button) when it closes. Esc closes
34   the dialog without storing anything — nothing optional runs until a
35   choice has been made, the banner reappears on the next page view.
36
37   Everything is file-based: no inline scripts, safe with a strict CSP.
38   Without JavaScript nothing is shown and nothing loads.
39
40   Public API for site scripts (services that are not plain scripts,
41   e.g. iframes/embeds gated behind a consent group):
42     window.w52Consent.has('<groupKey>')  -> boolean (required groups: true)
43     window.w52Consent.open()             -> reopen the banner
44   plus a document event fired on every stored/applied decision:
45     document.addEventListener('w52consent:changed', function (e) {
46         e.detail.groups     // { '<groupKey>': true|false }
47         e.detail.withdrawn  // [ '<groupKey>', ... ] switched off just now
48     });
49   Query has() at startup AND listen to the event — script order between
50   this file and site scripts is then irrelevant.
51   ========================================================================= */
52(function () {
53  "use strict";
54
55  var banner = document.getElementById("w52-consent-banner");
56  if (!banner) return;
57
58  var config;
59  try {
60    config = JSON.parse(banner.getAttribute("data-consent-config"));
61  } catch (e) {
62    return;
63  }
64  var expiryMs = (config.expiryMonths || 12) * 30.44 * 24 * 60 * 60 * 1000;
65  var title = document.getElementById("w52-consent-title");
66  var scriptLoads = {};  /* src -> Promise<boolean>; removed again on failure */
67  var groupQueues = {};  /* group key -> Promise; serializes loads per group */
68  var granted = {};      /* group key -> true while its services run in this page view */
69  var opener = null;     /* element that opened the banner; gets focus back */
70
71  /* ---- stored choice --------------------------------------------------- */
72
73  function readChoice() {
74    try {
75      var raw = window.localStorage.getItem(config.storageKey);
76      if (!raw) return null;
77      var choice = JSON.parse(raw);
78      if (!choice || choice.version !== config.version || typeof choice.groups !== "object") return null;
79      var age = Date.now() - Date.parse(choice.date);
80      if (!(age >= 0) || age > expiryMs) return null; /* expired or invalid */
81      return choice;
82    } catch (e) {
83      return null;
84    }
85  }
86
87  function saveChoice(groups) {
88    try {
89      window.localStorage.setItem(config.storageKey, JSON.stringify({
90        version: config.version,
91        date: new Date().toISOString(),
92        groups: groups
93      }));
94      (config.legacyStorageKeys || []).forEach(function (key) {
95        window.localStorage.removeItem(key);
96      });
97    } catch (e) { /* storage blocked: the banner reappears on the next visit */ }
98  }
99
100  /* ---- script loading -------------------------------------------------- */
101
102  function loadScript(src) {
103    if (!scriptLoads[src]) {
104      scriptLoads[src] = new Promise(function (resolve) {
105        var script = document.createElement("script");
106        script.src = src;
107        script.addEventListener("load", function () { resolve(true); });
108        script.addEventListener("error", function () {
109          delete scriptLoads[src]; /* allow a retry on the next applyChoice() */
110          script.remove();
111          resolve(false);
112        });
113        document.head.appendChild(script);
114      });
115    }
116    return scriptLoads[src];
117  }
118
119  /* Scripts of one group run strictly in declaration order: the next one is
120     only inserted after the previous one has executed (dynamically inserted
121     scripts do not keep their order otherwise). Runs for the same group are
122     queued behind each other. */
123  function loadScripts(group) {
124    var scripts = group.scripts || [];
125    groupQueues[group.key] = (groupQueues[group.key] || Promise.resolve()).then(function () {
126      return scripts.reduce(function (chain, src) {
127        return chain.then(function (ok) { return ok ? loadScript(src) : false; });
128      }, Promise.resolve(true));
129    });
130  }
131
132  /* ---- cleanup of withdrawn / non-consented groups ---------------------- */
133
134  /* "name" matches exactly, "prefix*" matches every name starting with prefix */
135  function matches(name, pattern) {
136    return pattern.slice(-1) === "*"
137      ? name.indexOf(pattern.slice(0, -1)) === 0
138      : name === pattern;
139  }
140
141  function cookieNames() {
142    return document.cookie.split(";").map(function (cookie) {
143      return cookie.split("=")[0].trim();
144    }).filter(Boolean);
145  }
146
147  /* A cookie can only be deleted with the domain/path it was set for, which
148     we do not know — so expire it for the host-only variant, every domain
149     level of the current host, and the root as well as the current path. */
150  function expireCookie(name) {
151    var expires = "; expires=Thu, 01 Jan 1970 00:00:00 GMT";
152    var secure = window.location.protocol === "https:" ? "; secure" : "";
153    var paths = ["/"];
154    if (window.location.pathname !== "/") paths.push(window.location.pathname);
155    var parts = window.location.hostname.split(".");
156    var domains = [""];
157    for (var i = 0; i < parts.length - 1; i++) {
158      domains.push("; domain=" + parts.slice(i).join("."));
159    }
160    paths.forEach(function (path) {
161      domains.forEach(function (domain) {
162        document.cookie = name + "=" + expires + "; path=" + path + domain + secure;
163      });
164    });
165  }
166
167  function removeStorageEntries(storage, pattern) {
168    var keys = [];
169    for (var i = 0; i < storage.length; i++) keys.push(storage.key(i));
170    keys.forEach(function (key) {
171      if (key !== config.storageKey && matches(key, pattern)) storage.removeItem(key);
172    });
173  }
174
175  /* Deletes what a group declared: its cookies (exact name or "prefix*")
176     and its localStorage/sessionStorage entries. Cookies a third party set
177     on its own domain are out of reach — such services rely on
178     reloadOnWithdraw (or the site script reacting to the event). */
179  function cleanup(group) {
180    try {
181      var names = cookieNames();
182      (group.cookies || []).forEach(function (pattern) {
183        names.forEach(function (name) {
184          if (matches(name, pattern)) expireCookie(name);
185        });
186      });
187      (group.storageKeys || []).forEach(function (pattern) {
188        removeStorageEntries(window.localStorage, pattern);
189        removeStorageEntries(window.sessionStorage, pattern);
190      });
191    } catch (e) { /* cookie/storage access blocked */ }
192  }
193
194  /* ---- applying a choice ----------------------------------------------- */
195
196  /* Starts newly granted groups, cleans up everything without consent and
197     returns the groups that were running but are now withdrawn. */
198  function applyChoice(groups) {
199    var withdrawn = [];
200    config.groups.forEach(function (group) {
201      var allowed = group.required || !!(groups && groups[group.key]);
202      if (allowed && !granted[group.key]) {
203        granted[group.key] = true;
204        loadScripts(group);
205      } else if (!allowed && granted[group.key]) {
206        delete granted[group.key];
207        withdrawn.push(group);
208      }
209      if (!allowed) cleanup(group);
210    });
211    return withdrawn;
212  }
213
214  function emitChange(groups, withdrawn) {
215    document.dispatchEvent(new CustomEvent("w52consent:changed", {
216      detail: { groups: groups, withdrawn: withdrawn }
217    }));
218  }
219
220  function decide(groups) {
221    saveChoice(groups);
222    closeBanner();
223    var withdrawn = applyChoice(groups);
224    emitChange(groups, withdrawn.map(function (group) { return group.key; }));
225    if (withdrawn.some(function (group) { return group.reloadOnWithdraw; })) {
226      /* A running script cannot be unloaded — start over without it. The
227         choice is already stored, so the banner stays closed. */
228      window.location.reload();
229    }
230  }
231
232  function collectToggles(value) {
233    var groups = {};
234    config.groups.forEach(function (group) {
235      if (group.required) return;
236      if (typeof value === "boolean") {
237        groups[group.key] = value;
238      } else {
239        var toggle = banner.querySelector('[data-consent-toggle="' + group.key + '"]');
240        groups[group.key] = !!(toggle && toggle.checked);
241      }
242    });
243    return groups;
244  }
245
246  /* ---- dialog ---------------------------------------------------------- */
247
248  function openBanner() {
249    if (banner.hasAttribute("open")) return;
250    opener = document.activeElement;
251    var choice = readChoice();
252    config.groups.forEach(function (group) {
253      if (group.required) return;
254      var toggle = banner.querySelector('[data-consent-toggle="' + group.key + '"]');
255      if (toggle) toggle.checked = !!(choice && choice.groups[group.key]);
256    });
257    if (typeof banner.showModal === "function") {
258      banner.showModal(); /* top layer; the page behind becomes inert */
259    } else {
260      banner.setAttribute("open", "");
261    }
262    /* Long content: start on the heading rather than the first button so
263       screen reader users hear title and intro before the choices. */
264    (title || banner).focus();
265  }
266
267  function restoreFocus() {
268    if (opener && opener !== document.body && typeof opener.focus === "function" && document.body.contains(opener)) {
269      opener.focus();
270    }
271    opener = null;
272  }
273
274  function closeBanner() {
275    if (!banner.hasAttribute("open")) return;
276    if (typeof banner.close === "function") {
277      banner.close();
278    } else {
279      banner.removeAttribute("open");
280    }
281    restoreFocus();
282  }
283
284  /* Esc closes a modal dialog natively (cancel -> close) without a decision:
285     nothing is stored, only the focus is handed back. */
286  banner.addEventListener("close", restoreFocus);
287
288  banner.addEventListener("click", function (e) {
289    if (e.target.closest("[data-consent-accept-all]")) {
290      decide(collectToggles(true));
291    } else if (e.target.closest("[data-consent-required-only]")) {
292      decide(collectToggles(false));
293    } else if (e.target.closest("[data-consent-save]")) {
294      decide(collectToggles());
295    }
296  });
297
298  /* Reopen buttons (e.g. footer) are only useful with JavaScript: reveal
299     them here and wire them up. */
300  var openers = document.querySelectorAll("[data-consent-open]");
301  for (var i = 0; i < openers.length; i++) {
302    openers[i].removeAttribute("hidden");
303    openers[i].addEventListener("click", function () { openBanner(); });
304  }
305
306  window.w52Consent = {
307    has: function (groupKey) {
308      for (var i = 0; i < config.groups.length; i++) {
309        if (config.groups[i].key !== groupKey) continue;
310        if (config.groups[i].required) return true;
311        var choice = readChoice();
312        return !!(choice && choice.groups[groupKey]);
313      }
314      return false;
315    },
316    open: function () { openBanner(); }
317  };
318
319  var choice = readChoice();
320  applyChoice(choice ? choice.groups : null); /* required groups run regardless */
321  if (choice) {
322    emitChange(choice.groups, []);
323  } else {
324    openBanner();
325  }
326})();

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.