UNPKG

peertube-plugin-cookie-consent

Version:

GDPR/DSGVO compliant cookie consent banner for PeerTube instances with script blocking functionality

697 lines (693 loc) 22.6 kB
// PeerTube Cookie Consent Plugin - ES Module // node_modules/universal-cookie/esm/index.mjs var cookie = {}; var hasRequiredCookie; function requireCookie() { if (hasRequiredCookie) return cookie; hasRequiredCookie = 1; cookie.parse = parse; cookie.serialize = serialize; var __toString = Object.prototype.toString; var __hasOwnProperty = Object.prototype.hasOwnProperty; var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/; var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i; var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/; function parse(str, opt) { if (typeof str !== "string") { throw new TypeError("argument str must be a string"); } var obj = {}; var len = str.length; if (len < 2) return obj; var dec = opt && opt.decode || decode; var index = 0; var eqIdx = 0; var endIdx = 0; do { eqIdx = str.indexOf("=", index); if (eqIdx === -1) break; endIdx = str.indexOf(";", index); if (endIdx === -1) { endIdx = len; } else if (eqIdx > endIdx) { index = str.lastIndexOf(";", eqIdx - 1) + 1; continue; } var keyStartIdx = startIndex(str, index, eqIdx); var keyEndIdx = endIndex(str, eqIdx, keyStartIdx); var key = str.slice(keyStartIdx, keyEndIdx); if (!__hasOwnProperty.call(obj, key)) { var valStartIdx = startIndex(str, eqIdx + 1, endIdx); var valEndIdx = endIndex(str, endIdx, valStartIdx); if (str.charCodeAt(valStartIdx) === 34 && str.charCodeAt(valEndIdx - 1) === 34) { valStartIdx++; valEndIdx--; } var val = str.slice(valStartIdx, valEndIdx); obj[key] = tryDecode(val, dec); } index = endIdx + 1; } while (index < len); return obj; } function startIndex(str, index, max) { do { var code = str.charCodeAt(index); if (code !== 32 && code !== 9) return index; } while (++index < max); return max; } function endIndex(str, index, min) { while (index > min) { var code = str.charCodeAt(--index); if (code !== 32 && code !== 9) return index + 1; } return min; } function serialize(name, val, opt) { var enc = opt && opt.encode || encodeURIComponent; if (typeof enc !== "function") { throw new TypeError("option encode is invalid"); } if (!cookieNameRegExp.test(name)) { throw new TypeError("argument name is invalid"); } var value = enc(val); if (!cookieValueRegExp.test(value)) { throw new TypeError("argument val is invalid"); } var str = name + "=" + value; if (!opt) return str; if (null != opt.maxAge) { var maxAge = Math.floor(opt.maxAge); if (!isFinite(maxAge)) { throw new TypeError("option maxAge is invalid"); } str += "; Max-Age=" + maxAge; } if (opt.domain) { if (!domainValueRegExp.test(opt.domain)) { throw new TypeError("option domain is invalid"); } str += "; Domain=" + opt.domain; } if (opt.path) { if (!pathValueRegExp.test(opt.path)) { throw new TypeError("option path is invalid"); } str += "; Path=" + opt.path; } if (opt.expires) { var expires = opt.expires; if (!isDate(expires) || isNaN(expires.valueOf())) { throw new TypeError("option expires is invalid"); } str += "; Expires=" + expires.toUTCString(); } if (opt.httpOnly) { str += "; HttpOnly"; } if (opt.secure) { str += "; Secure"; } if (opt.partitioned) { str += "; Partitioned"; } if (opt.priority) { var priority = typeof opt.priority === "string" ? opt.priority.toLowerCase() : opt.priority; switch (priority) { case "low": str += "; Priority=Low"; break; case "medium": str += "; Priority=Medium"; break; case "high": str += "; Priority=High"; break; default: throw new TypeError("option priority is invalid"); } } if (opt.sameSite) { var sameSite = typeof opt.sameSite === "string" ? opt.sameSite.toLowerCase() : opt.sameSite; switch (sameSite) { case true: str += "; SameSite=Strict"; break; case "lax": str += "; SameSite=Lax"; break; case "strict": str += "; SameSite=Strict"; break; case "none": str += "; SameSite=None"; break; default: throw new TypeError("option sameSite is invalid"); } } return str; } function decode(str) { return str.indexOf("%") !== -1 ? decodeURIComponent(str) : str; } function isDate(val) { return __toString.call(val) === "[object Date]"; } function tryDecode(str, decode2) { try { return decode2(str); } catch (e) { return str; } } return cookie; } var cookieExports = requireCookie(); function hasDocumentCookie() { const testingValue = typeof global === "undefined" ? void 0 : global.TEST_HAS_DOCUMENT_COOKIE; if (typeof testingValue === "boolean") { return testingValue; } return typeof document === "object" && typeof document.cookie === "string"; } function parseCookies(cookies2) { if (typeof cookies2 === "string") { return cookieExports.parse(cookies2); } else if (typeof cookies2 === "object" && cookies2 !== null) { return cookies2; } else { return {}; } } function readCookie(value, options = {}) { const cleanValue = cleanupCookieValue(value); if (!options.doNotParse) { try { return JSON.parse(cleanValue); } catch (e) { } } return value; } function cleanupCookieValue(value) { if (value && value[0] === "j" && value[1] === ":") { return value.substr(2); } return value; } var Cookies = class { constructor(cookies2, defaultSetOptions = {}) { this.changeListeners = []; this.HAS_DOCUMENT_COOKIE = false; this.update = () => { if (!this.HAS_DOCUMENT_COOKIE) { return; } const previousCookies = this.cookies; this.cookies = cookieExports.parse(document.cookie); this._checkChanges(previousCookies); }; const domCookies = typeof document === "undefined" ? "" : document.cookie; this.cookies = parseCookies(cookies2 || domCookies); this.defaultSetOptions = defaultSetOptions; this.HAS_DOCUMENT_COOKIE = hasDocumentCookie(); } _emitChange(params) { for (let i = 0; i < this.changeListeners.length; ++i) { this.changeListeners[i](params); } } _checkChanges(previousCookies) { const names = new Set(Object.keys(previousCookies).concat(Object.keys(this.cookies))); names.forEach((name) => { if (previousCookies[name] !== this.cookies[name]) { this._emitChange({ name, value: readCookie(this.cookies[name]) }); } }); } _startPolling() { this.pollingInterval = setInterval(this.update, 300); } _stopPolling() { if (this.pollingInterval) { clearInterval(this.pollingInterval); } } get(name, options = {}) { if (!options.doNotUpdate) { this.update(); } return readCookie(this.cookies[name], options); } getAll(options = {}) { if (!options.doNotUpdate) { this.update(); } const result = {}; for (let name in this.cookies) { result[name] = readCookie(this.cookies[name], options); } return result; } set(name, value, options) { if (options) { options = Object.assign(Object.assign({}, this.defaultSetOptions), options); } else { options = this.defaultSetOptions; } const stringValue = typeof value === "string" ? value : JSON.stringify(value); this.cookies = Object.assign(Object.assign({}, this.cookies), { [name]: stringValue }); if (this.HAS_DOCUMENT_COOKIE) { document.cookie = cookieExports.serialize(name, stringValue, options); } this._emitChange({ name, value, options }); } remove(name, options) { const finalOptions = options = Object.assign(Object.assign(Object.assign({}, this.defaultSetOptions), options), { expires: new Date(1970, 1, 1, 0, 0, 1), maxAge: 0 }); this.cookies = Object.assign({}, this.cookies); delete this.cookies[name]; if (this.HAS_DOCUMENT_COOKIE) { document.cookie = cookieExports.serialize(name, "", finalOptions); } this._emitChange({ name, value: void 0, options }); } addChangeListener(callback) { this.changeListeners.push(callback); if (this.HAS_DOCUMENT_COOKIE && this.changeListeners.length === 1) { if (typeof window === "object" && "cookieStore" in window) { window.cookieStore.addEventListener("change", this.update); } else { this._startPolling(); } } } removeChangeListener(callback) { const idx = this.changeListeners.indexOf(callback); if (idx >= 0) { this.changeListeners.splice(idx, 1); } if (this.HAS_DOCUMENT_COOKIE && this.changeListeners.length === 0) { if (typeof window === "object" && "cookieStore" in window) { window.cookieStore.removeEventListener("change", this.update); } else { this._stopPolling(); } } } }; // client/common-client-plugin.js var cookies = new Cookies(); function getConsentCategories() { return cookies.get("cookieConsentCategories") || null; } function setConsentCategories(categories, days) { cookies.set("cookieConsentCategories", categories, { path: "/", maxAge: (days || 180) * 24 * 60 * 60, secure: location.protocol === "https:", sameSite: "lax" }); } function injectCss(css) { const style = document.createElement("style"); style.innerHTML = css; document.head.appendChild(style); } function injectScripts(scripts, allowedCategories) { allowedCategories = allowedCategories || []; for (let i = 0; i < scripts.length; i++) { const s = scripts[i]; if (!s.category || allowedCategories.indexOf(s.category) !== -1) { const el = document.createElement("script"); el.src = s.src; if (s.async) el.async = true; if (s.defer) el.defer = true; document.head.appendChild(el); } } } function createElementFromMarkdown(markdown) { const container = document.createElement("div"); container.innerHTML = markdown.replace(/\n/g, "<br>").replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); return container; } function safeParse(jsonInput) { try { return JSON.parse(jsonInput); } catch (e) { console.error("[Cookie Consent Plugin] Fehler beim Parsen von Skripten:", e); return []; } } function applyConsent(settings) { const scripts = safeParse(settings.scripts); const categories = getConsentCategories(); if (!categories) return; const allowed = []; const keys = Object.keys(categories); for (let i = 0; i < keys.length; i++) { if (categories[keys[i]] === true) { allowed.push(keys[i]); } } injectScripts(scripts, allowed); } function darkenColor(color, percent) { const num = parseInt(color.replace("#", ""), 16); const amt = Math.round(2.55 * percent); const R = (num >> 16) - amt; const G = (num >> 8 & 255) - amt; const B = (num & 255) - amt; return "#" + (16777216 + (R > 255 ? 255 : R < 0 ? 0 : R) * 65536 + (G > 255 ? 255 : G < 0 ? 0 : G) * 256 + (B > 255 ? 255 : B < 0 ? 0 : B)).toString(16).slice(1); } function showCookieSettings(settings) { const existing = document.getElementById("cookie-settings-modal"); if (existing) existing.remove(); const overlay = document.createElement("div"); overlay.id = "cookie-settings-modal"; overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:99999;display:flex;align-items:center;justify-content:center;"; const borderRadius = parseInt(settings.modalBorderRadius || "12"); const accentColor = settings.modalAccentColor || "#007bff"; const accentHover = darkenColor(accentColor, 15); const modal = document.createElement("div"); modal.style.cssText = `background:#fff;padding:2em;border-radius:${borderRadius}px;max-width:450px;text-align:left;font-family:Arial,sans-serif;color:#333;box-shadow:0 8px 32px rgba(0,0,0,0.3);`; const currentPrefs = getConsentCategories() || { funktional: true, statistik: false, marketing: false }; const modalContent = ` <h2 style="color:#333;margin-top:0;margin-bottom:1.5em;font-size:1.4em;">Cookie-Einstellungen</h2> <style> .cookie-option { display: flex; align-items: center; margin: 15px 0; padding: 8px 0; } .cookie-checkbox { width: 18px; height: 18px; margin-right: 12px; accent-color: ${accentColor}; transform: scale(1.2); } .cookie-label { color: #333 !important; font-size: 16px; font-weight: 500; cursor: pointer; user-select: none; } .cookie-buttons { margin-top: 2em; padding-top: 1em; border-top: 1px solid #eee; } .cookie-btn-primary { background: ${accentColor}; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; margin-right: 12px; transition: background 0.2s; } .cookie-btn-primary:hover { background: ${accentHover}; } .cookie-btn-secondary { background: #6c757d; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s; } .cookie-btn-secondary:hover { background: #545b62; } </style> <form id="cookie-categories-form"> <div class="cookie-option"> <input type="checkbox" class="cookie-checkbox" name="funktional" id="funktional" ${currentPrefs.funktional ? "checked" : ""}> <label class="cookie-label" for="funktional">Funktionalit\xE4t</label> </div> <div class="cookie-option"> <input type="checkbox" class="cookie-checkbox" name="statistik" id="statistik" ${currentPrefs.statistik ? "checked" : ""}> <label class="cookie-label" for="statistik">Statistik</label> </div> <div class="cookie-option"> <input type="checkbox" class="cookie-checkbox" name="marketing" id="marketing" ${currentPrefs.marketing ? "checked" : ""}> <label class="cookie-label" for="marketing">Marketing</label> </div> <div class="cookie-buttons"> <button type="submit" class="cookie-btn-primary">Speichern</button> <button type="button" id="cancelCookieSettings" class="cookie-btn-secondary">Abbrechen</button> </div> </form> `; modal.innerHTML = modalContent; overlay.appendChild(modal); document.body.appendChild(overlay); document.getElementById("cancelCookieSettings").onclick = function() { overlay.remove(); }; document.getElementById("cookie-categories-form").onsubmit = function(e) { e.preventDefault(); const form = new FormData(e.target); const prefs = { funktional: form.has("funktional"), statistik: form.has("statistik"), marketing: form.has("marketing") }; setConsentCategories(prefs, 180); overlay.remove(); location.reload(); }; } function getManageButtonIcon(style) { switch (style) { case "icon-gear": return "\u2699\uFE0F"; case "icon-cookie": return "\u{1F36A}"; case "icon-settings": return "\u{1F4CB}"; default: return "\u2699\uFE0F"; } } function getPositionStyle(position) { switch (position) { case "bottom-left": return "bottom: 20px; left: 20px;"; case "top-right": return "top: 20px; right: 20px;"; case "top-left": return "top: 20px; left: 20px;"; default: return "bottom: 20px; right: 20px;"; } } function addManageButton(settings) { if (document.getElementById("cookie-manage-btn")) return; const buttonStyle = settings.manageButtonStyle || "icon-gear"; const buttonColor = settings.manageButtonColor || "#007bff"; const buttonPosition = settings.manageButtonPosition || "bottom-right"; const buttonColorHover = darkenColor(buttonColor, 15); const manageBtn = document.createElement("button"); manageBtn.id = "cookie-manage-btn"; manageBtn.title = "Cookie-Einstellungen verwalten"; if (buttonStyle.startsWith("icon-")) { manageBtn.innerHTML = getManageButtonIcon(buttonStyle); manageBtn.style.cssText = ` position: fixed; ${getPositionStyle(buttonPosition)} width: 48px; height: 48px; font-size: 18px; z-index: 9999; background: ${buttonColor}; color: white; border: none; border-radius: 50%; cursor: pointer; box-shadow: 0 4px 12px ${buttonColor}40; transition: all 0.3s ease; backdrop-filter: blur(10px); `; } else { manageBtn.textContent = "Cookie-Einstellungen"; const isSmall = buttonStyle === "text-small"; manageBtn.style.cssText = ` position: fixed; ${getPositionStyle(buttonPosition)} font-size: ${isSmall ? "12px" : "14px"}; z-index: 9999; background: ${buttonColor}; color: white; border: none; padding: ${isSmall ? "6px 12px" : "8px 16px"}; border-radius: 4px; cursor: pointer; box-shadow: 0 2px 8px ${buttonColor}40; transition: all 0.3s ease; `; } manageBtn.onmouseenter = function() { this.style.background = buttonColorHover; if (buttonStyle.startsWith("icon-")) { this.style.transform = "scale(1.1)"; } }; manageBtn.onmouseleave = function() { this.style.background = buttonColor; if (buttonStyle.startsWith("icon-")) { this.style.transform = "scale(1)"; } }; manageBtn.onclick = function() { showCookieSettings(settings); }; document.body.appendChild(manageBtn); } function createStyledButton(text, color, onClick) { const button = document.createElement("button"); const hoverColor = darkenColor(color, 15); button.textContent = text; button.style.cssText = ` margin: 0 8px; padding: 10px 20px; background: ${color}; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s; `; button.onmouseenter = function() { this.style.background = hoverColor; }; button.onmouseleave = function() { this.style.background = color; }; button.onclick = onClick; return button; } function register({ registerHook, peertubeHelpers }) { console.log("[Cookie Consent Plugin] Register function called"); if (!registerHook || !peertubeHelpers) { console.error("[Cookie Consent Plugin] Missing required options"); return; } console.log("[Cookie Consent Plugin] Registering hooks..."); registerHook({ target: "action:application.init", handler: function() { console.log("[Cookie Consent Plugin] Application initialized!"); setTimeout(function() { initCookieBanner(peertubeHelpers); }, 1e3); } }); } function initCookieBanner(peertubeHelpers) { console.log("[Cookie Consent Plugin] Initializing cookie banner..."); peertubeHelpers.getSettings().then(function(settings) { console.log("[Cookie Consent Plugin] Settings loaded:", settings); if (!settings || !settings.enableConsentBanner) { console.log("[Cookie Consent Plugin] Banner disabled in settings"); return; } const categories = getConsentCategories(); if (categories) { console.log("[Cookie Consent Plugin] Consent already given:", categories); applyConsent(settings); addManageButton(settings); return; } console.log("[Cookie Consent Plugin] Creating banner..."); if (settings.customCss) { injectCss(settings.customCss); } const bannerBgColor = settings.bannerBackgroundColor || "#000000"; const bannerTextColor = settings.bannerTextColor || "#ffffff"; const wrapper = document.createElement("div"); wrapper.className = "cookie-banner"; wrapper.style.cssText = ` position: fixed; bottom: 0; left: 0; width: 100%; background: ${bannerBgColor}; color: ${bannerTextColor}; padding: 1.5em; text-align: center; z-index: 9999; font-family: Arial, sans-serif; box-shadow: 0 -4px 16px rgba(0,0,0,0.3); `; const content = createElementFromMarkdown(settings.bannerMarkdown || "Diese Website verwendet Cookies."); content.style.cssText = "margin-bottom: 15px; line-height: 1.4; font-size: 15px;"; const btnAcceptAll = createStyledButton( "Alle akzeptieren", settings.buttonAcceptColor || "#28a745", function() { const allCategories = { funktional: true, statistik: true, marketing: true }; setConsentCategories(allCategories, 180); wrapper.remove(); applyConsent(settings); addManageButton(settings); } ); const btnEssenzielle = createStyledButton( "Nur essentielle Cookies", settings.buttonEssentialColor || "#6c757d", function() { const essentialOnly = { funktional: true, statistik: false, marketing: false }; setConsentCategories(essentialOnly, 180); wrapper.remove(); addManageButton(settings); } ); const btnSettings = createStyledButton( "Einstellungen", settings.buttonSettingsColor || "#007bff", function() { showCookieSettings(settings); } ); wrapper.appendChild(content); wrapper.appendChild(btnAcceptAll); wrapper.appendChild(btnEssenzielle); wrapper.appendChild(btnSettings); document.body.appendChild(wrapper); console.log("[Cookie Consent Plugin] Banner displayed successfully!"); }).catch(function(error) { console.error("[Cookie Consent Plugin] Error loading settings:", error); }); } export { register }; /*! Bundled license information: universal-cookie/esm/index.mjs: (*! * cookie * Copyright(c) 2012-2014 Roman Shtylman * Copyright(c) 2015 Douglas Christopher Wilson * MIT Licensed *) */