PageSourceSearch

https://www.co2-sachverstaendiger.de/js/consent-manager.js

js co2-sachverstaendiger.de collected 2026-10-02 17:59:13 UTC 12,827 bytes, 337 lines download raw bytes

1(function () {
2  'use strict';
3
4  const STORAGE_KEY = 'consent_state_v1';
5  // Bump whenever a service is added to or removed from a category, or a category's purpose text
6  // changes: a decision stored under another version is void and the banner is shown again.
7  const CONSENT_VERSION = '2026-09-16';
8  const CATEGORIES = ['necessary', 'functional', 'analytics', 'marketing'];
9  const OPTIONAL_CATEGORIES = ['functional', 'analytics', 'marketing'];
10  // A stored decision is re-requested after 12 months (retention period stated in the privacy notice).
11  const CONSENT_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1000;
12  // First-party cookies set on this site's domain by the services of each category. Cookies that
13  // providers set on their own domains (e.g. _GRECAPTCHA on google.com) cannot be removed from here.
14  const CATEGORY_COOKIE_PREFIXES = {
15    functional: [],
16    analytics: ['_ga', '_gid', '_gat', '_clck', '_clsk'],
17    marketing: ['_lfa']
18  };
19
20  function getLang() {
21    const lang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase();
22    return lang === 'de' ? 'de' : 'en';
23  }
24
25  function getUiText(lang) {
26    if (lang === 'de') {
27      return {
28        title: 'Cookie-Einstellungen',
29        description: 'Bitte wählen Sie, welche optionalen Dienste erlaubt sind. Sie werden erst nach Ihrer Einwilligung geladen; die Einwilligung können Sie jederzeit über „Cookie-Einstellungen“ im Footer widerrufen. Google Search Console und Microsoft/Bing Webmaster Tools setzen keine Cookies und sind daher hier nicht aufgeführt.',
30        necessary: 'Technisch notwendig (immer aktiv): Sitzungs-Cookie zum Schutz der Formulare und Speicherung dieser Auswahl',
31        functional: 'Funktional: zusätzlicher Spam-Schutz der Kontaktformulare (Google reCAPTCHA); Datenübermittlung in die USA möglich',
32        analytics: 'Analyse: Reichweitenmessung und Sitzungsaufzeichnung (Google Analytics 4, Microsoft Clarity); Datenübermittlung in die USA möglich',
33        marketing: 'Marketing: Erkennung besuchender Unternehmen (Leadfeeder; technische Verarbeitung durch Quant Weave Capital GmbH); Datenübermittlung in die USA möglich',
34        privacyLink: 'Datenschutzerklärung',
35        privacyHref: 'Datenschutz.php',
36        imprintLink: 'Impressum',
37        imprintHref: 'Impressum.php',
38        rejectAll: 'Alle ablehnen',
39        acceptAll: 'Alle akzeptieren',
40        save: 'Auswahl speichern'
41      };
42    }
43
44    return {
45      title: 'Cookie settings',
46      description: 'Choose which optional services are allowed. They load only after you consent; you can withdraw consent at any time via "Cookie settings" in the footer. Google Search Console and Microsoft/Bing Webmaster Tools set no cookies and are therefore not listed here.',
47      necessary: 'Strictly necessary (always on): session cookie protecting the forms, and storage of this choice',
48      functional: 'Functional: additional spam protection for the contact forms (Google reCAPTCHA); data may be transferred to the USA',
49      analytics: 'Analytics: audience measurement and session recording (Google Analytics 4, Microsoft Clarity); data may be transferred to the USA',
50      marketing: 'Marketing: identification of visiting companies (Leadfeeder; technical processing by Quant Weave Capital GmbH); data may be transferred to the USA',
51      privacyLink: 'Privacy Notice',
52      privacyHref: 'PrivacyNotice.php',
53      imprintLink: 'Imprint',
54      imprintHref: 'Imprint.php',
55      rejectAll: 'Reject all',
56      acceptAll: 'Accept all',
57      save: 'Save preferences'
58    };
59  }
60
61  function nowIso() {
62    return new Date().toISOString();
63  }
64
65  function safeParseJson(raw) {
66    if (!raw) return null;
67    try {
68      return JSON.parse(raw);
69    } catch {
70      return null;
71    }
72  }
73
74  function readConsent() {
75    const parsed = safeParseJson(localStorage.getItem(STORAGE_KEY));
76    if (!parsed || !parsed.categories) return null;
77    // A decision made for a different set of categories, or older than the retention period, is void.
78    if (parsed.version !== CONSENT_VERSION) return null;
79    const decidedAt = Date.parse(parsed.ts);
80    if (Number.isNaN(decidedAt) || Date.now() - decidedAt > CONSENT_MAX_AGE_MS) return null;
81    return parsed;
82  }
83
84  function normalizeCategories(input) {
85    return {
86      necessary: true,
87      functional: !!input.functional,
88      analytics: !!input.analytics,
89      marketing: !!input.marketing
90    };
91  }
92
93  function writeConsent(categories, source) {
94    const record = {
95      ts: nowIso(),
96      version: CONSENT_VERSION,
97      source: source || 'banner',
98      gpc: navigator.globalPrivacyControl === true,
99      categories: normalizeCategories(categories)
100    };
101    localStorage.setItem(STORAGE_KEY, JSON.stringify(record));
102    return record;
103  }
104
105  function hasConsent(category) {
106    if (CATEGORIES.indexOf(category) === -1) return false;
107    const consent = readConsent();
108    return !!(consent && consent.categories && consent.categories[category] === true);
109  }
110
111  function dispatchConsentEvent() {
112    const event = new CustomEvent('consent:updated', { detail: readConsent() });
113    window.dispatchEvent(event);
114  }
115
116  function loadDeferredScripts() {
117    const nodes = document.querySelectorAll('script[type="text/plain"][data-consent-category]:not([data-consent-loaded="true"])');
118    nodes.forEach(function (node) {
119      const category = node.getAttribute('data-consent-category');
120      if (!hasConsent(category)) return;
121
122      const script = document.createElement('script');
123      script.defer = true;
124
125      if (node.dataset.src) script.src = node.dataset.src;
126      if (node.textContent && node.textContent.trim()) script.text = node.textContent;
127
128      if (node.dataset.referrerpolicy) script.referrerPolicy = node.dataset.referrerpolicy;
129      if (node.dataset.crossorigin) script.crossOrigin = node.dataset.crossorigin;
130
131      document.head.appendChild(script);
132      node.setAttribute('data-consent-loaded', 'true');
133    });
134  }
135
136  function removeBanner() {
137    const banner = document.getElementById('cookie-consent-banner');
138    if (banner) banner.remove();
139  }
140
141  function withdrawnCategories(previous, next) {
142    if (!previous) return [];
143    return OPTIONAL_CATEGORIES.filter(function (category) {
144      return previous.categories[category] === true && !next[category];
145    });
146  }
147
148  function expireCookie(name) {
149    // Cookies may have been set for the exact host or for any parent domain, so try each.
150    const expired = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
151    document.cookie = expired;
152    const parts = location.hostname.split('.');
153    for (let i = 0; i < parts.length - 1; i++) {
154      document.cookie = expired + '; domain=.' + parts.slice(i).join('.');
155    }
156  }
157
158  function clearCategoryCookies(categories) {
159    const prefixes = categories.reduce(function (list, category) {
160      return list.concat(CATEGORY_COOKIE_PREFIXES[category] || []);
161    }, []);
162    if (prefixes.length === 0) return;
163
164    document.cookie.split(';').forEach(function (entry) {
165      const name = entry.split('=')[0].trim();
166      if (prefixes.some(function (prefix) { return name.indexOf(prefix) === 0; })) {
167        expireCookie(name);
168      }
169    });
170  }
171
172  function applyDecision(categories, source) {
173    const withdrawn = withdrawnCategories(readConsent(), categories);
174    writeConsent(categories, source);
175    removeBanner();
176
177    if (withdrawn.length > 0) {
178      // Services already running on this page cannot be switched off; a reload starts the page
179      // without them, since loadDeferredScripts() then skips the withdrawn categories.
180      clearCategoryCookies(withdrawn);
181      location.reload();
182      return;
183    }
184
185    loadDeferredScripts();
186    dispatchConsentEvent();
187  }
188
189  function buildToggle(name, label, checked, disabled) {
190    const wrapper = document.createElement('label');
191    wrapper.className = 'consent-banner__toggle';
192    wrapper.htmlFor = 'consent-' + name;
193
194    const input = document.createElement('input');
195    input.type = 'checkbox';
196    input.id = 'consent-' + name;
197    input.name = name;
198    input.checked = !!checked;
199    input.disabled = !!disabled;
200
201    const text = document.createElement('span');
202    text.textContent = label;
203
204    wrapper.appendChild(input);
205    wrapper.appendChild(text);
206    return wrapper;
207  }
208
209  function buildLink(href, label) {
210    const link = document.createElement('a');
211    link.href = href;
212    link.textContent = label;
213    return link;
214  }
215
216  function showBanner() {
217    if (document.getElementById('cookie-consent-banner')) return;
218
219    const lang = getLang();
220    const t = getUiText(lang);
221
222    const banner = document.createElement('section');
223    banner.id = 'cookie-consent-banner';
224    banner.className = 'consent-banner';
225    banner.setAttribute('aria-label', t.title);
226    banner.setAttribute('aria-live', 'polite');
227
228    const title = document.createElement('h2');
229    title.className = 'consent-banner__title';
230    title.textContent = t.title;
231
232    const description = document.createElement('p');
233    description.className = 'consent-banner__description';
234    description.textContent = t.description;
235
236    const form = document.createElement('form');
237    form.className = 'consent-banner__form';
238    form.id = 'cookie-consent-form';
239
240    const saved = readConsent();
241    const current = saved ? saved.categories : {};
242
243    // Reject all and accept all sit above the category list, so both stay on the first screen
244    // of the banner even on small phones; the per-category save follows the toggles.
245    const quickActions = document.createElement('div');
246    quickActions.className = 'consent-banner__actions';
247    const actions = document.createElement('div');
248    actions.className = 'consent-banner__actions';
249
250    const rejectAll = document.createElement('button');
251    rejectAll.type = 'button';
252    rejectAll.id = 'consent-reject-all';
253    rejectAll.textContent = t.rejectAll;
254
255    const acceptAll = document.createElement('button');
256    acceptAll.type = 'button';
257    acceptAll.id = 'consent-accept-all';
258    acceptAll.textContent = t.acceptAll;
259
260    const save = document.createElement('button');
261    save.type = 'submit';
262    save.id = 'consent-save';
263    save.textContent = t.save;
264
265    quickActions.appendChild(rejectAll);
266    quickActions.appendChild(acceptAll);
267    actions.appendChild(save);
268
269    form.appendChild(quickActions);
270    form.appendChild(buildToggle('necessary', t.necessary, true, true));
271    form.appendChild(buildToggle('functional', t.functional, !!current.functional, false));
272    form.appendChild(buildToggle('analytics', t.analytics, !!current.analytics, false));
273    form.appendChild(buildToggle('marketing', t.marketing, !!current.marketing, false));
274    form.appendChild(actions);
275
276    const legal = document.createElement('p');
277    legal.className = 'consent-banner__legal';
278    legal.appendChild(buildLink(t.privacyHref, t.privacyLink));
279    legal.appendChild(document.createTextNode(' | '));
280    legal.appendChild(buildLink(t.imprintHref, t.imprintLink));
281
282    banner.appendChild(title);
283    banner.appendChild(description);
284    banner.appendChild(form);
285    banner.appendChild(legal);
286    // First in the DOM so keyboard and screen-reader users reach the decision before the page
287    // content; the banner is still positioned at the bottom of the viewport by CSS.
288    document.body.prepend(banner);
289
290    rejectAll.addEventListener('click', function () {
291      applyDecision({ functional: false, analytics: false, marketing: false }, 'reject_all');
292    });
293
294    acceptAll.addEventListener('click', function () {
295      applyDecision({ functional: true, analytics: true, marketing: true }, 'accept_all');
296    });
297
298    form.addEventListener('submit', function (event) {
299      event.preventDefault();
300      applyDecision({
301        functional: !!form.querySelector('[name="functional"]').checked,
302        analytics: !!form.querySelector('[name="analytics"]').checked,
303        marketing: !!form.querySelector('[name="marketing"]').checked
304      }, 'custom');
305    });
306  }
307
308  function openSettings(event) {
309    if (event) event.preventDefault();
310    showBanner();
311  }
312
313  function bindFooterLinks() {
314    document.querySelectorAll('[data-open-cookie-settings="true"]').forEach(function (link) {
315      link.addEventListener('click', openSettings);
316    });
317  }
318
319  function init() {
320    const consent = readConsent();
321    const gpcEnabled = navigator.globalPrivacyControl === true;
322
323    if (!consent) {
324      if (gpcEnabled) {
325        applyDecision({ functional: false, analytics: false, marketing: false }, 'gpc');
326      } else {
327        showBanner();
328      }
329    } else {
330      loadDeferredScripts();
331    }
332
333    bindFooterLinks();
334  }
335
336  document.addEventListener('DOMContentLoaded', init);
337})();

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.