Files
s4luorth a738841d60 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>
2026-06-07 15:11:48 +02:00

310 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Content Blocker frontend.js
* Click-to-load: the real iframe is CREATED only after the user actively
* consents per service. Consent is stored in localStorage per service id.
*/
( function () {
'use strict';
const STORAGE_PREFIX = 'cb_consent_';
const CFG = window.cbConfig || { services: [], i18n: {} };
/* ───────────────────────── consent storage ───────────────────────── */
function hasConsent( serviceId ) {
try {
return localStorage.getItem( STORAGE_PREFIX + serviceId ) === '1';
} catch ( e ) {
return false;
}
}
function grantConsent( serviceId ) {
try {
localStorage.setItem( STORAGE_PREFIX + serviceId, '1' );
} catch ( e ) {
// localStorage unavailable; allow the load for this session only.
}
}
/* ───────────────────────── iframe loading ────────────────────────── */
/** Replace a .cb-blocker element with the real iframe. src comes from data-src. */
function loadContent( blockerEl ) {
const src = blockerEl.dataset.src;
if ( ! src ) {
blockerEl.remove();
return;
}
const iframe = document.createElement( 'iframe' );
if ( blockerEl.dataset.width ) iframe.width = blockerEl.dataset.width;
if ( blockerEl.dataset.height ) iframe.height = blockerEl.dataset.height;
iframe.setAttribute( 'loading', 'lazy' );
iframe.setAttribute( 'allowfullscreen', '' );
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.
iframe.src = src;
blockerEl.parentNode.replaceChild( iframe, blockerEl );
}
function loadPreConsented() {
document.querySelectorAll( '.cb-blocker[data-cb-id]' ).forEach( function ( el ) {
const id = el.dataset.cbId;
if ( id && hasConsent( id ) ) {
loadContent( el );
}
} );
}
/* ───────────────────────── consent buttons ───────────────────────── */
function attachButtons() {
document.addEventListener( 'click', function ( e ) {
const btn = e.target.closest( '.cb-blocker__button' );
if ( ! btn ) return;
const serviceId = btn.dataset.cbId;
if ( ! serviceId ) return;
const wrapper = btn.closest( '.cb-blocker' );
// "Remember" checkbox (default checked): persist consent only when
// ticked; otherwise load this embed once without storing consent.
const remember = wrapper
? wrapper.querySelector( '.cb-blocker__remember-cb' )
: null;
if ( ! remember || remember.checked ) {
grantConsent( serviceId );
}
if ( wrapper ) {
loadContent( wrapper );
}
} );
}
/* ───────────────────────── revoke (Art. 7 (3)) ───────────────────── */
window.cbRevokeAll = function () {
try {
const keysToRemove = [];
for ( let i = 0; i < localStorage.length; i++ ) {
const key = localStorage.key( i );
if ( key && key.indexOf( STORAGE_PREFIX ) === 0 ) {
keysToRemove.push( key );
}
}
keysToRemove.forEach( function ( k ) {
localStorage.removeItem( k );
} );
} catch ( e ) {
// Silently ignore if localStorage is unavailable.
}
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 ─────────────────────────────── */
function init() {
loadPreConsented();
attachButtons();
handleElementorVideos();
scanExistingIframes();
startObserver();
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
} )();