/* WEBklar Analyse w.js - generiert aus client/core.js + client/w.js (node client/build.js), nicht von Hand aendern */ (function () { 'use strict'; // Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten). // Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen. const CONSENT_KEY = 'wk-consent'; const CONSENT_MAX_AGE = 183 * 86400000; function vpClass(width) { const w = Number(width); if (w < 768) return 'm'; if (w < 1024) return 't'; return 'd'; } function cleanPath(href) { let p = '/'; try { p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ } try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ } if (p.charAt(0) !== '/') p = '/' + p; return p.slice(0, 300); } const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, ''); function refHost(referrer, ownHost) { if (!referrer) return ''; try { const u = new URL(String(referrer)); if (u.protocol !== 'http:' && u.protocol !== 'https:') return ''; const h = u.hostname.toLowerCase(); if (!h || stripWww(h) === stripWww(ownHost || '')) return ''; return h; } catch (e) { return ''; } } // Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen. function looksGenerated(s) { if (/\d{4,}/.test(s)) return true; const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t); if (mixed(s, 10) && !/[-_]/.test(s)) return true; if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true; return s.split(/[-_]/).some((p) => mixed(p, 6)); } const safeName = (s) => String(s).replace(/[^\w-]/g, ''); function cssPath(el) { const parts = []; let n = el; while (n && n.tagName && parts.length < 5) { const tag = String(n.tagName).toLowerCase(); let seg = tag; const id = n.id ? safeName(n.id) : ''; if (id && !looksGenerated(id)) { parts.unshift(seg + '#' + id); break; } const raw = n.getAttribute ? n.getAttribute('class') : ''; const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2); if (cls.length) seg += '.' + cls.join('.'); parts.unshift(seg); n = n.parentNode; } let out = parts.join(' > '); while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); } return out.slice(0, 200); } function parseHref(a) { const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || ''; return String(href).trim(); } function linkHost(a) { try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; } } function contactKind(a, ownHost) { const href = parseHref(a); if (!href) return null; const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href); if (!scheme) return null; // relativ, Anker const s = scheme[1].toLowerCase(); if (s === 'tel') return 'tel'; if (s === 'mailto') return 'mail'; if (s === 'whatsapp') return 'wa'; if (s !== 'http' && s !== 'https') return null; const host = linkHost(a); if (!host) return null; if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa'; const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost; return stripWww(host) === stripWww(own || '') ? null : 'out'; } function readConsent(storage, now) { try { const raw = storage.getItem(CONSENT_KEY); if (!raw) return null; const c = JSON.parse(raw); if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null; const age = now - c.t; if (age < 0 || age >= CONSENT_MAX_AGE) return null; return { v: 1, a: c.a, t: c.t }; } catch (e) { return null; } } function contrastOk(hex) { if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false; const lin = (i) => { const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }; const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2); return 1.05 / (L + 0.05) >= 4.5; } function escapeHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites. // Keine Cookies, keine Kennung im Browser. Nur pv ohne Einwilligung; ck/ct/en nur nach Annehmen. function main() { const DOC = document; const WIN = window; const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site') ? DOC.currentScript : DOC.querySelector('script[src$="/a/w.js"]'); if (!SELF) return; const site = String(SELF.getAttribute('data-site') || ''); if (!/^[a-z0-9]{16,40}$/.test(site)) return; const colorAttr = String(SELF.getAttribute('data-color') || ''); const accent = contrastOk(colorAttr) ? colorAttr : '#111111'; const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, ''); const base = src.replace(/[^/]*$/, ''); const endpoint = base + 'e'; // ---- Heatmap-Modus: nichts senden, kein Banner ---- const qs = new URLSearchParams(WIN.location.search); const heatToken = qs.get('__wk_heat'); if (heatToken) { qs.delete('__wk_heat'); const rest = qs.toString(); try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ } const s = DOC.createElement('script'); s.setAttribute('src', base + 'h.js'); s.setAttribute('data-token', heatToken); (DOC.head || DOC.documentElement).appendChild(s); return; } // ---- Senden: Fehler werden verschluckt ---- function send(obj) { try { const body = JSON.stringify(obj); if (navigator.sendBeacon && navigator.sendBeacon(endpoint, body)) return; fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {}); } catch (e) { /* egal */ } } const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } }; const vp = () => vpClass(WIN.innerWidth); const ownHost = () => WIN.location.hostname; // ---- Seitenaufruf (pv): immer, auch ohne Einwilligung ---- let lastPath = null; let pvStart = Date.now(); let maxScroll = 0; let enSent = false; function sendPv(first) { lastPath = cleanPath(WIN.location.pathname); pvStart = Date.now(); maxScroll = 0; enSent = false; send({ t: 'pv', site, path: lastPath, ref: first ? refHost(DOC.referrer, ownHost()) : '', vp: vp() }); } let navTimer = null; function scheduleNav() { if (navTimer) return; navTimer = setTimeout(safe(function () { navTimer = null; if (cleanPath(WIN.location.pathname) !== lastPath) sendPv(false); }), 300); } sendPv(true); try { const orig = WIN.history.pushState; WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; }; WIN.addEventListener('popstate', safe(scheduleNav)); } catch (e) { /* egal */ } // ---- Tracking (nur nach Annehmen erreichbar) ---- let tracking = false; const hosts = []; const scrollPct = () => { const h = DOC.documentElement.scrollHeight || 1; return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100))); }; const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000)); const onClick = safe(function (e) { const t = e.target; if (!t || hosts.indexOf(t) !== -1) return; const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight; const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0); const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0); const path = cleanPath(WIN.location.pathname); send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) }); let a = t; while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode; const kind = a ? contactKind(a, ownHost()) : null; if (kind) { const ev = { t: 'ct', site, path, kind }; if (kind === 'out') ev.host = linkHost(a); send(ev); } }); const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); }); const onLeave = safe(function () { if (enSent) return; enSent = true; send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(), sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) }); }); const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); }); function startTracking() { if (tracking) return; tracking = true; DOC.addEventListener('click', onClick, { capture: true, passive: true }); DOC.addEventListener('visibilitychange', onVis); WIN.addEventListener('pagehide', onLeave); WIN.addEventListener('scroll', onScroll, { passive: true }); } function stopTracking() { tracking = false; DOC.removeEventListener('click', onClick, { capture: true }); DOC.removeEventListener('visibilitychange', onVis); WIN.removeEventListener('pagehide', onLeave); WIN.removeEventListener('scroll', onScroll); } // ---- Einwilligung ---- const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1'; if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking let storage = null; try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ } const stored = readConsent(storage, Date.now()); let bannerHost = null; let linkHostEl = null; function choose(a) { try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ } send({ t: 'bn', site, a }); if (a === 'accept') startTracking(); else stopTracking(); closeBanner(); } function el(tag, attrs, text) { const n = DOC.createElement(tag); for (const k in attrs || {}) n.setAttribute(k, attrs[k]); if (text != null) n.textContent = text; return n; } const CSS = ':host{all:initial}' + '*{box-sizing:border-box}' + '.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' + '.t{margin:0;max-width:720px}' + '.t a{color:#111;text-decoration:underline}' + '.r{display:flex;gap:10px;flex:none}' + 'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' + 'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' + 'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' + '@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}'; function openBanner() { if (bannerHost) return; const prev = DOC.activeElement; bannerHost = DOC.createElement('div'); const st = bannerHost.style; st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647'; hosts.push(bannerHost); const root = bannerHost.attachShadow({ mode: 'open' }); root.appendChild(el('style', {}, CSS)); const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' }); const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der '); p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info')); p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.')); const row = el('div', { class: 'r' }); const no = el('button', { type: 'button' }, 'Ablehnen'); const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen'); no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); })); yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); })); row.appendChild(no); row.appendChild(yes); bar.appendChild(p); bar.appendChild(row); root.appendChild(bar); DOC.body.appendChild(bannerHost); if (linkHostEl) linkHostEl.style.display = 'none'; no.focus(); } function closeBanner() { if (!bannerHost) return; const i = hosts.indexOf(bannerHost); if (i !== -1) hosts.splice(i, 1); bannerHost.remove(); bannerHost = null; if (linkHostEl) linkHostEl.style.display = ''; } function addSettingsLink() { linkHostEl = DOC.createElement('div'); const st = linkHostEl.style; st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646'; hosts.push(linkHostEl); const root = linkHostEl.attachShadow({ mode: 'open' }); root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}')); const b = el('button', { type: 'button' }, 'Cookie-Einstellungen'); b.addEventListener('click', safe(openBanner)); root.appendChild(b); DOC.body.appendChild(linkHostEl); } if (DOC.querySelector('[data-webklar-consent]')) { DOC.addEventListener('click', safe(function (e) { const t = e.target; if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(); } })); } else { addSettingsLink(); } if (stored) { if (stored.a === 'accept') startTracking(); } else { openBanner(); send({ t: 'bn', site, a: 'shown' }); } } try { main(); } catch (e) { /* nie die Seite stoeren */ } })();