Files
breakpilot-compliance/consent-sdk/src/core/ScriptBlocker.ts
T
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

368 lines
9.3 KiB
TypeScript

/**
* ScriptBlocker - Blockiert Skripte bis Consent erteilt wird
*
* Verwendet das data-consent Attribut zur Identifikation von
* Skripten, die erst nach Consent geladen werden duerfen.
*
* Beispiel:
* <script data-consent="analytics" data-src="..." type="text/plain"></script>
*/
import type { ConsentConfig, ConsentCategory } from '../types';
/**
* Script-Element mit Consent-Attributen
*/
interface ConsentScript extends HTMLScriptElement {
dataset: DOMStringMap & {
consent?: string;
src?: string;
};
}
/**
* iFrame-Element mit Consent-Attributen
*/
interface ConsentIframe extends HTMLIFrameElement {
dataset: DOMStringMap & {
consent?: string;
src?: string;
};
}
/**
* ScriptBlocker - Verwaltet Script-Blocking
*/
export class ScriptBlocker {
private config: ConsentConfig;
private observer: MutationObserver | null = null;
private enabledCategories: Set<ConsentCategory> = new Set(['essential']);
private processedElements: WeakSet<Element> = new WeakSet();
constructor(config: ConsentConfig) {
this.config = config;
}
/**
* Initialisieren und Observer starten
*/
init(): void {
if (typeof window === 'undefined') {
return;
}
// Bestehende Elemente verarbeiten
this.processExistingElements();
// MutationObserver fuer neue Elemente
this.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
this.processElement(node as Element);
}
}
}
});
this.observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
this.log('ScriptBlocker initialized');
}
/**
* Kategorie aktivieren
*/
enableCategory(category: ConsentCategory): void {
if (this.enabledCategories.has(category)) {
return;
}
this.enabledCategories.add(category);
this.log('Category enabled:', category);
// Blockierte Elemente dieser Kategorie aktivieren
this.activateCategory(category);
}
/**
* Kategorie deaktivieren
*/
disableCategory(category: ConsentCategory): void {
if (category === 'essential') {
// Essential kann nicht deaktiviert werden
return;
}
this.enabledCategories.delete(category);
this.log('Category disabled:', category);
// Hinweis: Bereits geladene Skripte koennen nicht entladen werden
// Page-Reload noetig fuer vollstaendige Deaktivierung
}
/**
* Alle Kategorien blockieren (ausser Essential)
*/
blockAll(): void {
this.enabledCategories.clear();
this.enabledCategories.add('essential');
this.log('All categories blocked');
}
/**
* Pruefen ob Kategorie aktiviert
*/
isCategoryEnabled(category: ConsentCategory): boolean {
return this.enabledCategories.has(category);
}
/**
* Observer stoppen
*/
destroy(): void {
this.observer?.disconnect();
this.observer = null;
this.log('ScriptBlocker destroyed');
}
// ===========================================================================
// Internal Methods
// ===========================================================================
/**
* Bestehende Elemente verarbeiten
*/
private processExistingElements(): void {
// Scripts mit data-consent
const scripts = document.querySelectorAll<ConsentScript>(
'script[data-consent]'
);
scripts.forEach((script) => this.processScript(script));
// iFrames mit data-consent
const iframes = document.querySelectorAll<ConsentIframe>(
'iframe[data-consent]'
);
iframes.forEach((iframe) => this.processIframe(iframe));
this.log(`Processed ${scripts.length} scripts, ${iframes.length} iframes`);
}
/**
* Element verarbeiten
*/
private processElement(element: Element): void {
if (element.tagName === 'SCRIPT') {
this.processScript(element as ConsentScript);
} else if (element.tagName === 'IFRAME') {
this.processIframe(element as ConsentIframe);
}
// Auch Kinder verarbeiten
element
.querySelectorAll<ConsentScript>('script[data-consent]')
.forEach((script) => this.processScript(script));
element
.querySelectorAll<ConsentIframe>('iframe[data-consent]')
.forEach((iframe) => this.processIframe(iframe));
}
/**
* Script-Element verarbeiten
*/
private processScript(script: ConsentScript): void {
if (this.processedElements.has(script)) {
return;
}
const category = script.dataset.consent as ConsentCategory | undefined;
if (!category) {
return;
}
this.processedElements.add(script);
if (this.enabledCategories.has(category)) {
this.activateScript(script);
} else {
this.log(`Script blocked (${category}):`, script.dataset.src || 'inline');
}
}
/**
* iFrame-Element verarbeiten
*/
private processIframe(iframe: ConsentIframe): void {
if (this.processedElements.has(iframe)) {
return;
}
const category = iframe.dataset.consent as ConsentCategory | undefined;
if (!category) {
return;
}
this.processedElements.add(iframe);
if (this.enabledCategories.has(category)) {
this.activateIframe(iframe);
} else {
this.log(`iFrame blocked (${category}):`, iframe.dataset.src);
// Placeholder anzeigen
this.showPlaceholder(iframe, category);
}
}
/**
* Script aktivieren
*/
private activateScript(script: ConsentScript): void {
const src = script.dataset.src;
if (src) {
// Externes Script: neues Element erstellen
const newScript = document.createElement('script');
// Attribute kopieren
for (const attr of script.attributes) {
if (attr.name !== 'type' && attr.name !== 'data-src') {
newScript.setAttribute(attr.name, attr.value);
}
}
newScript.src = src;
newScript.removeAttribute('data-consent');
// Altes Element ersetzen
script.parentNode?.replaceChild(newScript, script);
this.log('External script activated:', src);
} else {
// Inline-Script: type aendern
const newScript = document.createElement('script');
for (const attr of script.attributes) {
if (attr.name !== 'type') {
newScript.setAttribute(attr.name, attr.value);
}
}
newScript.textContent = script.textContent;
newScript.removeAttribute('data-consent');
script.parentNode?.replaceChild(newScript, script);
this.log('Inline script activated');
}
}
/**
* iFrame aktivieren
*/
private activateIframe(iframe: ConsentIframe): void {
const src = iframe.dataset.src;
if (!src) {
return;
}
// Placeholder entfernen falls vorhanden
const placeholder = iframe.parentElement?.querySelector(
'.bp-consent-placeholder'
);
placeholder?.remove();
// src setzen
iframe.src = src;
iframe.removeAttribute('data-src');
iframe.removeAttribute('data-consent');
iframe.style.display = '';
this.log('iFrame activated:', src);
}
/**
* Placeholder fuer blockierten iFrame anzeigen
*/
private showPlaceholder(iframe: ConsentIframe, category: ConsentCategory): void {
// iFrame verstecken
iframe.style.display = 'none';
// Placeholder erstellen
const placeholder = document.createElement('div');
placeholder.className = 'bp-consent-placeholder';
placeholder.setAttribute('data-category', category);
placeholder.innerHTML = `
<div class="bp-consent-placeholder-content">
<p>Dieser Inhalt erfordert Ihre Zustimmung.</p>
<button type="button" class="bp-consent-placeholder-btn">
${this.getCategoryName(category)} aktivieren
</button>
</div>
`;
// Click-Handler
const btn = placeholder.querySelector('button');
btn?.addEventListener('click', () => {
// Event dispatchen damit ConsentManager reagieren kann
window.dispatchEvent(
new CustomEvent('bp-consent-request', {
detail: { category },
})
);
});
// Nach iFrame einfuegen
iframe.parentNode?.insertBefore(placeholder, iframe.nextSibling);
}
/**
* Alle Elemente einer Kategorie aktivieren
*/
private activateCategory(category: ConsentCategory): void {
// Scripts
const scripts = document.querySelectorAll<ConsentScript>(
`script[data-consent="${category}"]`
);
scripts.forEach((script) => this.activateScript(script));
// iFrames
const iframes = document.querySelectorAll<ConsentIframe>(
`iframe[data-consent="${category}"]`
);
iframes.forEach((iframe) => this.activateIframe(iframe));
this.log(
`Activated ${scripts.length} scripts, ${iframes.length} iframes for ${category}`
);
}
/**
* Kategorie-Name fuer UI
*/
private getCategoryName(category: ConsentCategory): string {
const names: Record<ConsentCategory, string> = {
essential: 'Essentielle Cookies',
functional: 'Funktionale Cookies',
analytics: 'Statistik-Cookies',
marketing: 'Marketing-Cookies',
social: 'Social Media-Cookies',
};
return names[category] ?? category;
}
/**
* Debug-Logging
*/
private log(...args: unknown[]): void {
if (this.config.debug) {
console.log('[ScriptBlocker]', ...args);
}
}
}
export default ScriptBlocker;