feat: client-seitige erkennung fuer elementor-videos und JS-iframes
- frontend.js erkennt JS-nachgeladene iframes (Slider etc.) per initialem Scan + MutationObserver und ersetzt sie durch den Platzhalter. - Elementor-Video-Widgets: data-settings (youtube_url/vimeo_url) wird gelesen, zu Embed-URL konvertiert, Widget neutralisiert und durch Platzhalter ersetzt. - Service-Daten + i18n werden dafuer ans Frontend lokalisiert (cbConfig). - readme: Erkennung + Grenzen (Elementor-Vorschaubild) dokumentiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const STORAGE_PREFIX = 'cb_consent_';
|
const STORAGE_PREFIX = 'cb_consent_';
|
||||||
|
const CFG = window.cbConfig || { services: [], i18n: {} };
|
||||||
|
|
||||||
/* ───────────────────────── consent storage ───────────────────────── */
|
/* ───────────────────────── consent storage ───────────────────────── */
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
iframe.setAttribute( 'loading', 'lazy' );
|
iframe.setAttribute( 'loading', 'lazy' );
|
||||||
iframe.setAttribute( 'allowfullscreen', '' );
|
iframe.setAttribute( 'allowfullscreen', '' );
|
||||||
iframe.setAttribute( 'referrerpolicy', 'no-referrer-when-downgrade' );
|
iframe.setAttribute( 'referrerpolicy', 'no-referrer-when-downgrade' );
|
||||||
|
iframe.setAttribute( 'data-cb-loaded', '1' ); // mark as ours (observer skips)
|
||||||
|
|
||||||
// Set src last — this is the moment the network request is made.
|
// Set src last — this is the moment the network request is made.
|
||||||
iframe.src = src;
|
iframe.src = src;
|
||||||
@@ -108,11 +110,195 @@
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ─────────────── client-side detection (JS-injected iframes) ─────────────── */
|
||||||
|
|
||||||
|
/** Find a configured service whose match pattern is contained in the URL. */
|
||||||
|
function findService( url ) {
|
||||||
|
if ( ! url ) return null;
|
||||||
|
const list = CFG.services || [];
|
||||||
|
for ( let i = 0; i < list.length; i++ ) {
|
||||||
|
if ( list[ i ].match && url.indexOf( list[ i ].match ) !== -1 ) {
|
||||||
|
return list[ i ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a YouTube/Vimeo watch URL into its embeddable URL. */
|
||||||
|
function toEmbedUrl( url ) {
|
||||||
|
let m = url.match( /(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([\w-]+)/ );
|
||||||
|
if ( m ) return 'https://www.youtube.com/embed/' + m[ 1 ];
|
||||||
|
m = url.match( /(?:player\.)?vimeo\.com\/(?:video\/)?(\d+)/ );
|
||||||
|
if ( m ) return 'https://player.vimeo.com/video/' + m[ 1 ];
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a consent placeholder element (mirrors PHP render_placeholder). */
|
||||||
|
function buildPlaceholder( svc, src, dims ) {
|
||||||
|
const i18n = CFG.i18n || {};
|
||||||
|
dims = dims || {};
|
||||||
|
|
||||||
|
const wrap = document.createElement( 'div' );
|
||||||
|
wrap.className = 'cb-blocker';
|
||||||
|
wrap.setAttribute( 'data-cb-id', svc.id );
|
||||||
|
wrap.dataset.src = src;
|
||||||
|
if ( dims.width ) wrap.dataset.width = dims.width;
|
||||||
|
if ( dims.height ) wrap.dataset.height = dims.height;
|
||||||
|
if ( dims.height && /^\d+$/.test( String( dims.height ) ) ) {
|
||||||
|
wrap.style.minHeight = dims.height + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
const inner = document.createElement( 'div' );
|
||||||
|
inner.className = 'cb-blocker__inner';
|
||||||
|
|
||||||
|
const text = document.createElement( 'p' );
|
||||||
|
text.className = 'cb-blocker__text';
|
||||||
|
if ( svc.placeholder ) {
|
||||||
|
text.textContent = svc.placeholder;
|
||||||
|
} else {
|
||||||
|
const tpl = i18n.defaultText || '%s';
|
||||||
|
const parts = tpl.split( '%s' );
|
||||||
|
text.appendChild( document.createTextNode( parts[ 0 ] || '' ) );
|
||||||
|
const strong = document.createElement( 'strong' );
|
||||||
|
strong.textContent = svc.name;
|
||||||
|
text.appendChild( strong );
|
||||||
|
text.appendChild( document.createTextNode( parts[ 1 ] || '' ) );
|
||||||
|
}
|
||||||
|
inner.appendChild( text );
|
||||||
|
|
||||||
|
const rec = document.createElement( 'p' );
|
||||||
|
rec.className = 'cb-blocker__recipient';
|
||||||
|
const recLabel = document.createElement( 'strong' );
|
||||||
|
recLabel.textContent = ( i18n.recipient || 'Empfänger:' ) + ' ';
|
||||||
|
rec.appendChild( recLabel );
|
||||||
|
rec.appendChild( document.createTextNode( svc.recipient || '' ) );
|
||||||
|
if ( svc.third_country ) {
|
||||||
|
rec.appendChild( document.createTextNode( ' — ' ) );
|
||||||
|
const tc = document.createElement( 'span' );
|
||||||
|
tc.className = 'cb-blocker__third-country';
|
||||||
|
tc.textContent = i18n.thirdCountry || '';
|
||||||
|
rec.appendChild( tc );
|
||||||
|
}
|
||||||
|
inner.appendChild( rec );
|
||||||
|
|
||||||
|
const pur = document.createElement( 'p' );
|
||||||
|
pur.className = 'cb-blocker__purpose';
|
||||||
|
const purLabel = document.createElement( 'strong' );
|
||||||
|
purLabel.textContent = ( i18n.purpose || 'Zweck:' ) + ' ';
|
||||||
|
pur.appendChild( purLabel );
|
||||||
|
pur.appendChild( document.createTextNode( svc.purpose || '' ) );
|
||||||
|
inner.appendChild( pur );
|
||||||
|
|
||||||
|
if ( svc.privacy_url ) {
|
||||||
|
const p = document.createElement( 'p' );
|
||||||
|
const a = document.createElement( 'a' );
|
||||||
|
a.className = 'cb-blocker__privacy-link';
|
||||||
|
a.href = svc.privacy_url;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.rel = 'noopener noreferrer';
|
||||||
|
a.textContent = i18n.privacy || 'Datenschutz';
|
||||||
|
p.appendChild( a );
|
||||||
|
inner.appendChild( p );
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.createElement( 'button' );
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'cb-blocker__button';
|
||||||
|
btn.setAttribute( 'data-cb-id', svc.id );
|
||||||
|
btn.textContent = ( i18n.load || '%s' ).replace( '%s', svc.name );
|
||||||
|
inner.appendChild( btn );
|
||||||
|
|
||||||
|
const label = document.createElement( 'label' );
|
||||||
|
label.className = 'cb-blocker__remember';
|
||||||
|
const cb = document.createElement( 'input' );
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.className = 'cb-blocker__remember-cb';
|
||||||
|
cb.checked = true;
|
||||||
|
label.appendChild( cb );
|
||||||
|
label.appendChild( document.createTextNode( ' ' + ( i18n.remember || '' ) ) );
|
||||||
|
inner.appendChild( label );
|
||||||
|
|
||||||
|
wrap.appendChild( inner );
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replace a freshly seen iframe with a placeholder (or let it load if consented). */
|
||||||
|
function handleIframe( iframe ) {
|
||||||
|
if ( iframe.dataset.cbLoaded ) return; // one we created
|
||||||
|
if ( iframe.closest( '.cb-blocker' ) ) return; // inside a placeholder
|
||||||
|
const rawSrc = iframe.getAttribute( 'src' ) || '';
|
||||||
|
const svc = findService( rawSrc );
|
||||||
|
if ( ! svc ) return;
|
||||||
|
|
||||||
|
if ( hasConsent( svc.id ) ) return; // already consented → leave it
|
||||||
|
|
||||||
|
iframe.setAttribute( 'src', 'about:blank' ); // stop the request ASAP
|
||||||
|
const dims = {
|
||||||
|
width: iframe.getAttribute( 'width' ) || '',
|
||||||
|
height: iframe.getAttribute( 'height' ) || '',
|
||||||
|
};
|
||||||
|
const placeholder = buildPlaceholder( svc, rawSrc, dims );
|
||||||
|
if ( iframe.parentNode ) {
|
||||||
|
iframe.parentNode.replaceChild( placeholder, iframe );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Elementor video widgets store the URL in data-settings and build the
|
||||||
|
* player via JS — handle them before that happens. */
|
||||||
|
function handleElementorVideos() {
|
||||||
|
document.querySelectorAll( '.elementor-widget-video[data-settings]' ).forEach( function ( widget ) {
|
||||||
|
let settings;
|
||||||
|
try {
|
||||||
|
settings = JSON.parse( widget.dataset.settings );
|
||||||
|
} catch ( e ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = settings.youtube_url || settings.vimeo_url || settings.external_url || '';
|
||||||
|
if ( ! raw ) return;
|
||||||
|
|
||||||
|
const embed = toEmbedUrl( raw );
|
||||||
|
const svc = findService( raw ) || findService( embed );
|
||||||
|
if ( ! svc ) return;
|
||||||
|
|
||||||
|
if ( hasConsent( svc.id ) ) return; // consented → let Elementor build it
|
||||||
|
|
||||||
|
// Neutralise Elementor so it won't build the player / load the overlay.
|
||||||
|
widget.removeAttribute( 'data-settings' );
|
||||||
|
const container = widget.querySelector( '.elementor-widget-container' ) || widget;
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.appendChild( buildPlaceholder( svc, embed, {} ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanExistingIframes() {
|
||||||
|
document.querySelectorAll( 'iframe[src]' ).forEach( handleIframe );
|
||||||
|
}
|
||||||
|
|
||||||
|
function startObserver() {
|
||||||
|
if ( typeof MutationObserver === 'undefined' ) return;
|
||||||
|
const obs = new MutationObserver( function ( mutations ) {
|
||||||
|
for ( const m of mutations ) {
|
||||||
|
for ( const node of m.addedNodes ) {
|
||||||
|
if ( node.nodeType !== 1 ) continue;
|
||||||
|
if ( node.tagName === 'IFRAME' ) {
|
||||||
|
handleIframe( node );
|
||||||
|
} else if ( node.querySelectorAll ) {
|
||||||
|
node.querySelectorAll( 'iframe[src]' ).forEach( handleIframe );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
obs.observe( document.documentElement, { childList: true, subtree: true } );
|
||||||
|
}
|
||||||
|
|
||||||
/* ───────────────────────── bootstrap ─────────────────────────────── */
|
/* ───────────────────────── bootstrap ─────────────────────────────── */
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
loadPreConsented();
|
loadPreConsented();
|
||||||
attachButtons();
|
attachButtons();
|
||||||
|
handleElementorVideos();
|
||||||
|
scanExistingIframes();
|
||||||
|
startObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( document.readyState === 'loading' ) {
|
if ( document.readyState === 'loading' ) {
|
||||||
|
|||||||
@@ -45,6 +45,42 @@ class CB_Renderer {
|
|||||||
CB_VERSION,
|
CB_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Service data + i18n for client-side detection (JS-injected iframes such
|
||||||
|
// as Elementor video widgets, sliders, …).
|
||||||
|
wp_localize_script( 'cb-frontend', 'cbConfig', [
|
||||||
|
'services' => self::services_for_js(),
|
||||||
|
'i18n' => [
|
||||||
|
'recipient' => __( 'Empfänger:', 'gdpr-content-blocker' ),
|
||||||
|
'purpose' => __( 'Zweck:', 'gdpr-content-blocker' ),
|
||||||
|
'thirdCountry' => __( '⚠ Datenübermittlung in ein Drittland außerhalb der EU/des EWR', 'gdpr-content-blocker' ),
|
||||||
|
'privacy' => __( 'Datenschutzerklärung des Anbieters', 'gdpr-content-blocker' ),
|
||||||
|
'load' => __( '%s jetzt laden', 'gdpr-content-blocker' ),
|
||||||
|
'remember' => __( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' ),
|
||||||
|
'defaultText' => __( 'Um diesen Inhalt von %s zu laden, ist Ihre Einwilligung erforderlich. Dabei werden personenbezogene Daten (z. B. Ihre IP-Adresse) an den Anbieter übertragen.', 'gdpr-content-blocker' ),
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal, public service data for client-side detection. */
|
||||||
|
private static function services_for_js(): array {
|
||||||
|
$out = [];
|
||||||
|
foreach ( CB_Settings::get_services() as $svc ) {
|
||||||
|
if ( empty( $svc['match_pattern'] ) || ! ( $svc['enabled'] ?? true ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = [
|
||||||
|
'id' => $svc['id'],
|
||||||
|
'name' => $svc['name'],
|
||||||
|
'match' => $svc['match_pattern'],
|
||||||
|
'recipient' => $svc['recipient'],
|
||||||
|
'third_country' => ! empty( $svc['third_country'] ),
|
||||||
|
'purpose' => $svc['purpose'],
|
||||||
|
'privacy_url' => $svc['privacy_url'] ?? '',
|
||||||
|
'placeholder' => $svc['placeholder_text'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -76,10 +76,20 @@ Veröffentlichung (für den Betreiber): Plugin-ZIP per Gitea-Actions (Tag `v*`)
|
|||||||
manuell per curl an den Endpoint `POST /api/v1/releases` des Backends laden. Die
|
manuell per curl an den Endpoint `POST /api/v1/releases` des Backends laden. Die
|
||||||
ZIP muss einen Ordner `gdpr-content-blocker/` auf oberster Ebene enthalten.
|
ZIP muss einen Ordner `gdpr-content-blocker/` auf oberster Ebene enthalten.
|
||||||
|
|
||||||
|
== Erkennung ==
|
||||||
|
|
||||||
|
* Server-seitig: iframes im initialen HTML werden anhand der Erkennungsmuster blockiert.
|
||||||
|
* Client-seitig (automatisch): per JavaScript nachgeladene iframes (z. B. Slider) sowie
|
||||||
|
Elementor-Video-Widgets werden erkannt und durch den Platzhalter ersetzt.
|
||||||
|
|
||||||
== Bekannte Grenzen ==
|
== Bekannte Grenzen ==
|
||||||
|
|
||||||
* Auto-Erkennung greift nur auf iframes im initialen Server-HTML. Durch JavaScript nachgeladene iframes
|
* Client-seitige Erkennung ist „best effort": Bei per JS nachgeladenen iframes kann im
|
||||||
werden nicht automatisch erkannt. Für diese Fälle den manuellen Shortcode verwenden.
|
Einzelfall ein bereits gestarteter Request nicht garantiert verhindert werden. Für
|
||||||
|
maximale Rechtssicherheit den manuellen Shortcode verwenden.
|
||||||
|
* Bei Elementor-Videos mit Bild-Overlay kann das Vorschaubild (z. B. von ytimg.com) als
|
||||||
|
CSS-Hintergrund bereits beim Laden angefragt werden. Empfehlung: in Elementor das
|
||||||
|
Bild-Overlay deaktivieren oder ein eigenes Vorschaubild verwenden.
|
||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user