Files
GDPR-Content-Blocker/gdpr-content-blocker/assets/frontend.js
s4luorth 494518e98e fix: kein s.w.org-emoji-request + kein weissraum unter dem blocker (v1.4.3)
1) Das Warn-Emoji "⚠" im platzhalter-text loeste WordPress' wp-emoji aus, das
   die grafik von s.w.org nachlud (drittanbieter-request). Emoji aus den texten
   entfernt; das warn-dreieck wird jetzt rein per CSS gezeichnet (kein netzwerk).
   Hinweis: s.w.org ist der WP-emoji-CDN, NICHT Google.
2) Der platzhalter reservierte die hoehe des eingebetteten elements -> bei
   kuerzerem inhalt entstand weissraum darunter. Hoehen-reservierung entfernt
   (PHP min-height-style, JS minHeight, CSS min-height:180px) - der content
   blocker ist jetzt exakt so hoch wie sein eigener inhalt. Die original-masse
   bleiben als data-attribute fuer das iframe nach einwilligung erhalten.
3) Redundante, partielle de_DE-.po/.mo (mit altem emoji) entfernt - deutsche
   locales nutzen direkt die deutschen quell-strings.

+ version 1.4.3 (assets cache-busting).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 13:23:34 +02:00

399 lines
14 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' );
// Placeholder without its own embed URL (e.g. an Elementor widget we
// couldn't read the URL from): persist consent and reload so the
// original embed renders normally.
if ( wrapper && wrapper.getAttribute( 'data-cb-reload' ) === '1' ) {
grantConsent( serviceId );
window.location.reload();
return;
}
// "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;
// Keep the dims for the iframe we create on consent, but do NOT reserve
// height — the placeholder is sized to its own content (no empty space).
if ( dims.width ) wrap.dataset.width = dims.width;
if ( dims.height ) wrap.dataset.height = dims.height;
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.source || s.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"], '
+ '.e-youtube-base, .e-vimeo-base, [data-e-type^="e-youtube"], [data-e-type^="e-vimeo"]'
);
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 );
const embed = raw ? toEmbedUrl( raw ) : '';
let svc = raw ? ( findService( raw ) || findService( embed ) ) : null;
// Fallback: no URL in the DOM (Elementor only kept {"video_type":"…"}).
// Identify the service by the video type so we can still show consent.
if ( ! svc ) {
const vt = ( widget.getAttribute( 'data-settings' ) || '' )
.match( /"video_type"\s*:\s*"(youtube|vimeo|dailymotion)"/i );
if ( vt ) {
svc = findService( vt[ 1 ] + '.com' );
}
}
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 placeholder = buildPlaceholder( svc, embed, {} );
// No embed URL → we can't build the iframe ourselves. Make the button
// grant consent and reload; Elementor then renders the video normally
// (scripts are released after consent). Drop the "load once" option.
if ( ! embed ) {
placeholder.setAttribute( 'data-cb-reload', '1' );
const remember = placeholder.querySelector( '.cb-blocker__remember' );
if ( remember ) remember.remove();
}
container.innerHTML = '';
container.appendChild( placeholder );
} );
}
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();
}
} )();