Files
GDPR-Content-Blocker/gdpr-content-blocker/assets/frontend.js
s4luorth 1b7d3434bf fix: elementor-youtube-widget (per JS gebaut) zuverlaessig erkennen
Nach dem script-blocking blieb ein per YouTube-iframe_api gebautes
Elementor-video leer: die API wird blockiert, also baut Elementor kein
iframe -> mein iframe-basierter platzhalter griff nicht, und das
data-settings enthielt nur {"video_type":"youtube"} (keine URL).

videoUrlForWidget() findet die video-URL jetzt aus mehreren quellen:
- explizite keys (youtube_url/vimeo_url/...) im data-settings
- beliebige youtube/vimeo-URL im data-settings (regex, escaped slashes)
- bereits server-gerendertes iframe im widget
- YouTube-thumbnail (i.ytimg.com/vi/ID/) bei lazyload/overlay-modus
Greift auch fuer .elementor-widget[data-widget_type^="video"] und ueberspringt
widgets, die der server-autodetect schon zum platzhalter gemacht hat.

Extraktions-logik isoliert getestet (thumbnail, escaped-json, vimeo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:36:31 +02:00

371 lines
13 KiB
JavaScript
Raw 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.
}
// Release any third-party <script> blocked for this service (handled by
// the early head guard, which exposes cbActivateScripts).
if ( typeof window.cbActivateScripts === 'function' ) {
window.cbActivateScripts( serviceId );
}
}
/**
* Public: grant consent for a service from a custom control
* ([content_blocker_consent]) — used for script-only services that have no
* visible embed. Reloads so blocked scripts/embeds load cleanly.
*/
window.cbConsent = function ( serviceId ) {
if ( ! serviceId ) {
return;
}
grantConsent( serviceId );
window.location.reload();
};
/* ───────────────────────── 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 );
}
}
/** Reconstruct a YouTube watch URL from a thumbnail (i.ytimg.com/vi/ID/…). */
function ytIdFromThumb( s ) {
const m = String( s || '' ).match( /(?:i\.ytimg\.com|img\.youtube\.com)\/vi\/([\w-]{6,})\// );
return m ? m[ 1 ] : '';
}
/**
* Find the embedded video URL of an Elementor video widget. Elementor often
* does NOT put the URL in data-settings (only {"video_type":"youtube"}) and
* builds the player via the JS API — so we look in several places.
*/
function videoUrlForWidget( widget ) {
const ds = widget.getAttribute( 'data-settings' ) || '';
// 1) Explicit URL keys in data-settings (older/other Elementor versions).
try {
const s = JSON.parse( ds || '{}' );
const u = s.youtube_url || s.vimeo_url || s.video_url || s.external_url
|| s.hosted_url || ( s.link && s.link.url ) || '';
if ( u ) return u;
} catch ( e ) { /* not JSON / no keys */ }
// 2) Any youtube/vimeo URL anywhere in the data-settings string (JSON values
// are quoted, so stop at a quote/space; tolerate escaped slashes \/).
const m = ds.match( /https?:[^"\s]*?(?:youtube\.com|youtu\.be|vimeo\.com)[^"\s]*/i );
if ( m ) return m[ 0 ].replace( /\\\//g, '/' ).replace( /\\/g, '' );
// 3) A server-rendered iframe already inside the widget.
const ifr = widget.querySelector( 'iframe[src]' );
if ( ifr ) return ifr.getAttribute( 'src' );
// 4) A YouTube thumbnail (lazyload / image-overlay mode) → rebuild the URL.
const thumb = widget.querySelector(
'img[src*="ytimg.com/vi/"], img[src*="img.youtube.com/vi/"], [style*="ytimg.com/vi/"]'
);
if ( thumb ) {
const id = ytIdFromThumb( thumb.getAttribute( 'src' ) || thumb.getAttribute( 'style' ) || '' );
if ( id ) return 'https://www.youtube.com/watch?v=' + id;
}
return '';
}
/** Elementor video widgets build the player via JS — replace them with our
* consent placeholder before that happens. */
function handleElementorVideos() {
const widgets = document.querySelectorAll(
'.elementor-widget-video, .elementor-widget[data-widget_type^="video"]'
);
widgets.forEach( function ( widget ) {
const container = widget.querySelector( '.elementor-widget-container' ) || widget;
// Already turned into a placeholder (e.g. by the server-side autodetect
// catching a rendered iframe) → leave it.
if ( container.querySelector( '.cb-blocker' ) ) return;
const raw = videoUrlForWidget( widget );
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' );
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();
}
} )();