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>
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user