4435e7ea0a
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>
368 lines
9.3 KiB
TypeScript
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;
|