Files
s4luorth ef84c43ded fix: textausrichtung-option + hover-fixes (link/badge) + remember-farbe (v1.5.2)
- Darstellung: neue option "Textausrichtung" (mittig/links/rechts). Setzt
  --cb-align (text-align) und --cb-justify (remember-zeile) inline.
- "Diesen Dienst kuenftig immer laden" nutzt jetzt die textfarbe (var(--cb-text),
  ohne dimming).
- Datenschutz-link: schriftgroesse/-gewicht/laufweite/transform/decoration in
  allen zustaenden festgenagelt -> keine theme-a:hover-aenderung mehr.
- Werbe-badge: dito -> wird beim hover nicht mehr riesig (theme-scaling/
  font-size gesperrt).

+ i18n DE/EN (138 strings). version 1.5.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:41:26 +02:00

424 lines
16 KiB
PHP

<?php
defined( 'ABSPATH' ) || exit;
class CB_Renderer {
public static function init(): void {
add_shortcode( 'content_blocker', [ __CLASS__, 'shortcode' ] );
add_shortcode( 'content_blocker_revoke', [ __CLASS__, 'revoke_shortcode' ] );
add_shortcode( 'content_blocker_services', [ __CLASS__, 'services_shortcode' ] );
add_shortcode( 'content_blocker_consent', [ __CLASS__, 'consent_shortcode' ] );
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
// Earliest possible: install the dynamic-script guard before theme/builder
// scripts run, so JS-injected third-party scripts (e.g. YouTube iframe_api)
// are caught before they fetch.
add_action( 'wp_head', [ __CLASS__, 'print_script_guard' ], 0 );
}
/** Enabled services that should also block matching <script> tags. */
public static function script_services(): array {
$out = [];
foreach ( CB_Settings::get_services() as $svc ) {
if ( empty( $svc['match_pattern'] ) || empty( $svc['loads_script'] ) || ! ( $svc['enabled'] ?? true ) ) {
continue;
}
$out[] = [ 'id' => $svc['id'], 'match' => $svc['match_pattern'] ];
}
return $out;
}
/**
* Inline guard printed very early in <head>. It blocks third-party scripts
* (both already in the DOM and dynamically injected) until the matching
* service has consent, by neutralising the <script> node right before it is
* inserted (no network request is made). Re-injection happens on consent.
*/
public static function print_script_guard(): void {
if ( is_admin() ) {
return;
}
$services = self::script_services();
if ( empty( $services ) ) {
return;
}
$json = wp_json_encode( $services );
echo "<script>\n/* gdpr-content-blocker: script guard */\n";
echo '(function(){"use strict";var S=' . $json . ';if(!S.length)return;'
. 'var P="cb_consent_";'
. 'function ok(id){try{return localStorage.getItem(P+id)==="1";}catch(e){return false;}}'
. 'function find(u){u=String(u||"");for(var i=0;i<S.length;i++){if(u.indexOf(S[i].match)!==-1)return S[i];}return null;}'
. 'var blocked=[];'
. 'function neutralize(node){try{if(!node||node.tagName!=="SCRIPT")return;'
. 'var src=node.src||(node.getAttribute?node.getAttribute("src"):"")||"";if(!src)return;'
. 'var s=find(src);if(!s||ok(s.id))return;'
. 'node.type="text/plain";node.setAttribute("data-cb-src",src);node.setAttribute("data-cb-id",s.id);'
. 'if(node.removeAttribute)node.removeAttribute("src");blocked.push(node);}catch(e){}}'
. 'function patch(proto,name){var o=proto[name];if(!o)return;proto[name]=function(n){try{neutralize(n);}catch(e){}return o.apply(this,arguments);};}'
. 'patch(Node.prototype,"appendChild");patch(Node.prototype,"insertBefore");patch(Node.prototype,"replaceChild");'
. 'function rein(el){try{var src=el.getAttribute&&el.getAttribute("data-cb-src");if(!src)return;'
. 'var s=document.createElement("script");for(var i=0;i<el.attributes.length;i++){var a=el.attributes[i];'
. 'if(/^(type|src|data-cb-src|data-cb-id)$/i.test(a.name))continue;try{s.setAttribute(a.name,a.value);}catch(e){}}'
. 's.src=src;var p=el.parentNode;if(p){p.insertBefore(s,el.nextSibling);p.removeChild(el);}}catch(e){}}'
. 'window.cbActivateScripts=function(id){blocked.slice().forEach(function(el){if(el.getAttribute("data-cb-id")===id)rein(el);});'
. 'var n=document.querySelectorAll(\'script[type="text/plain"][data-cb-src][data-cb-id]\');'
. 'Array.prototype.forEach.call(n,function(el){if(el.getAttribute("data-cb-id")===id)rein(el);});};'
. 'function pre(){for(var i=0;i<S.length;i++){if(ok(S[i].id))window.cbActivateScripts(S[i].id);}}'
. 'if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",pre);}else{pre();}'
. '})();';
echo "\n</script>\n";
}
public static function enqueue_assets(): void {
wp_enqueue_style(
'cb-frontend',
CB_URL . 'assets/frontend.css',
[],
CB_VERSION
);
$style = CB_Settings::get_style();
// Build one inline block: CSS variables first, the user's Custom-CSS LAST
// so it always overrides. Emitted as a single wp_add_inline_style call.
$align = in_array( $style['text_align'] ?? 'center', [ 'left', 'center', 'right' ], true ) ? $style['text_align'] : 'center';
$justify = [ 'left' => 'flex-start', 'center' => 'center', 'right' => 'flex-end' ][ $align ];
$inline = sprintf(
':root{--cb-text:%s;--cb-bg:%s;--cb-btn-bg:%s;--cb-btn-text:%s;--cb-btn-hover-bg:%s;--cb-btn-hover-text:%s;--cb-align:%s;--cb-justify:%s;}',
esc_attr( $style['text_color'] ),
esc_attr( $style['bg_color'] ),
esc_attr( $style['button_bg'] ),
esc_attr( $style['button_text'] ),
esc_attr( $style['button_hover_bg'] ),
esc_attr( $style['button_hover_text'] ),
esc_attr( $align ),
esc_attr( $justify )
);
if ( trim( (string) $style['custom_css'] ) !== '' ) {
$inline .= "\n/* gdpr-content-blocker custom css */\n" . $style['custom_css'];
}
wp_add_inline_style( 'cb-frontend', $inline );
wp_enqueue_script(
'cb-frontend',
CB_URL . 'assets/frontend.js',
[],
CB_VERSION,
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(),
'showBadge' => self::show_badge(),
'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;
}
/**
* Shortcode: [content_blocker id="google-maps"]<iframe ...></iframe>[/content_blocker]
*/
public static function shortcode( array $atts, ?string $content = null ): string {
$atts = shortcode_atts( [ 'id' => '' ], $atts, 'content_blocker' );
$id = sanitize_key( $atts['id'] );
if ( $id === '' ) {
return '';
}
$svc = CB_Settings::get_service( $id );
if ( $svc === null ) {
return '<!-- gdpr-content-blocker: unknown service id "' . esc_html( $id ) . '" -->';
}
// Disabled blocker → let the embedded content load normally (no blocking).
if ( ! ( $svc['enabled'] ?? true ) ) {
return $content ?? '';
}
// Extract src + dimensions from the inner iframe
$src = '';
$dims = [];
if ( $content !== null && $content !== '' ) {
$attrs = self::extract_iframe_attrs( $content );
$src = $attrs['src'];
$dims = [ 'width' => $attrs['width'], 'height' => $attrs['height'] ];
}
return self::render_placeholder( $svc, $src, $dims );
}
/**
* Shortcode: [content_blocker_revoke]
* Renders, by default, a visible text link (not a button) so it doesn't get
* confused with a cookie banner's revoke control. Attributes:
* text="…" custom link label
* style="link|button" default "link"
* note="yes|no" show the clarifying hint (default "yes")
*/
public static function revoke_shortcode( array|string $atts = [] ): string {
$atts = shortcode_atts( [
'text' => __( 'Einwilligung für externe Inhalte widerrufen', 'gdpr-content-blocker' ),
'style' => 'link',
'note' => 'yes',
], $atts, 'content_blocker_revoke' );
$class = $atts['style'] === 'button' ? 'cb-revoke-btn' : 'cb-revoke-link';
$out = '<a href="#" class="' . esc_attr( $class ) . '" role="button" '
. 'onclick="cbRevokeAll();return false;">'
. esc_html( $atts['text'] )
. '</a>';
if ( $atts['note'] === 'yes' ) {
$out .= '<span class="cb-revoke-note">'
. esc_html__( 'Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet.', 'gdpr-content-blocker' )
. '</span>';
}
return $out;
}
/**
* Shortcode: [content_blocker_consent id="youtube" text="…" style="button|link"]
* A consent control for services that only block scripts (no visible embed
* to click). Grants consent for the given service id and reloads so the
* blocked scripts/embeds load.
*/
public static function consent_shortcode( array|string $atts = [] ): string {
$atts = shortcode_atts( [
'id' => '',
'text' => __( 'Externe Inhalte aktivieren', 'gdpr-content-blocker' ),
'style' => 'button',
], $atts, 'content_blocker_consent' );
$id = sanitize_key( $atts['id'] );
if ( $id === '' ) {
return '';
}
$class = $atts['style'] === 'link' ? 'cb-revoke-link' : 'cb-revoke-btn';
return '<a href="#" class="' . esc_attr( $class ) . '" role="button" '
. 'onclick="cbConsent(\'' . esc_js( $id ) . '\');return false;">'
. esc_html( $atts['text'] )
. '</a>';
}
/**
* Shortcode: [content_blocker_services]
* Lists every configured third-party service with the Art. 13 details
* (provider, recipient, third-country note, purpose, privacy link).
* Meant for embedding in the privacy policy.
*/
public static function services_shortcode(): string {
$services = CB_Settings::get_services();
if ( empty( $services ) ) {
return '';
}
$out = '<ul class="cb-services-list">';
foreach ( $services as $svc ) {
// Only list active blockers — disabled ones aren't being managed.
if ( ! ( $svc['enabled'] ?? true ) ) {
continue;
}
$name = esc_html( $svc['name'] ?? '' );
if ( $name === '' ) {
continue;
}
$out .= '<li class="cb-service-entry">';
$out .= '<span class="cb-service-entry__name">' . $name . '</span>';
$out .= '<dl>';
if ( ! empty( $svc['recipient'] ) ) {
$recipient = esc_html( $svc['recipient'] );
if ( ! empty( $svc['third_country'] ) ) {
$recipient .= ' (' . esc_html__( 'Übermittlung in ein Drittland außerhalb der EU/des EWR', 'gdpr-content-blocker' ) . ')';
}
$out .= '<dt>' . esc_html__( 'Empfänger', 'gdpr-content-blocker' ) . '</dt><dd>' . $recipient . '</dd>';
}
if ( ! empty( $svc['purpose'] ) ) {
$out .= '<dt>' . esc_html__( 'Zweck', 'gdpr-content-blocker' ) . '</dt><dd>' . esc_html( $svc['purpose'] ) . '</dd>';
}
$out .= '<dt>' . esc_html__( 'Setzt Cookies', 'gdpr-content-blocker' ) . '</dt><dd>'
. ( ! empty( $svc['sets_cookie'] ) ? esc_html__( 'Ja', 'gdpr-content-blocker' ) : esc_html__( 'Nein', 'gdpr-content-blocker' ) )
. '</dd>';
if ( ! empty( $svc['privacy_url'] ) ) {
$url = esc_url( $svc['privacy_url'] );
$out .= '<dt>' . esc_html__( 'Datenschutz', 'gdpr-content-blocker' ) . '</dt>'
. '<dd><a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $url . '</a></dd>';
}
$out .= '</dl></li>';
}
$out .= '</ul>';
return $out;
}
/**
* Build the placeholder HTML for a given service.
* $src is the real iframe URL (empty string if unknown).
* $dims may contain 'width' and 'height' (numbers or e.g. "100%") taken
* from the original iframe so the placeholder reserves the same height.
*/
public static function render_placeholder( array $svc, string $src = '', array $dims = [] ): string {
$id = esc_attr( $svc['id'] );
$name = esc_html( $svc['name'] );
$recipient = esc_html( $svc['recipient'] );
$purpose = esc_html( $svc['purpose'] );
$privacy_url = esc_url( $svc['privacy_url'] ?? '' );
$third = ! empty( $svc['third_country'] );
$custom_text = $svc['placeholder_text'] ?? '';
if ( $custom_text !== '' ) {
$info_text = esc_html( $custom_text );
} else {
$info_text = sprintf(
/* translators: %s: provider name */
esc_html__( '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' ),
'<strong>' . $name . '</strong>'
);
}
$third_note = '';
if ( $third ) {
$third_note = '<span class="cb-blocker__third-country">'
. esc_html__( 'Datenübermittlung in ein Drittland außerhalb der EU/des EWR', 'gdpr-content-blocker' )
. '</span>';
}
$privacy_link = '';
if ( $privacy_url !== '' ) {
$privacy_link = '<a href="' . $privacy_url . '" target="_blank" rel="noopener noreferrer" class="cb-blocker__privacy-link">'
. esc_html__( 'Datenschutzerklärung des Anbieters', 'gdpr-content-blocker' )
. '</a>';
}
$data_src = $src !== '' ? ' data-src="' . esc_attr( $src ) . '"' : '';
// Remember the original dimensions for the iframe that gets created on
// consent. The placeholder itself is sized to its own content (no reserved
// height), so there is no empty space below it.
$width = isset( $dims['width'] ) ? (string) $dims['width'] : '';
$height = isset( $dims['height'] ) ? (string) $dims['height'] : '';
$style_at = '';
$data_dims = '';
if ( $width !== '' ) {
$data_dims .= ' data-width="' . esc_attr( $width ) . '"';
}
if ( $height !== '' ) {
$data_dims .= ' data-height="' . esc_attr( $height ) . '"';
}
return '<div class="cb-blocker" data-cb-id="' . $id . '"' . $data_src . $data_dims . $style_at . '>'
. '<div class="cb-blocker__inner">'
. '<p class="cb-blocker__text">' . $info_text . '</p>'
. '<p class="cb-blocker__recipient">'
. '<strong>' . esc_html__( 'Empfänger:', 'gdpr-content-blocker' ) . '</strong> '
. $recipient
. $third_note
. '</p>'
. '<p class="cb-blocker__purpose">'
. '<strong>' . esc_html__( 'Zweck:', 'gdpr-content-blocker' ) . '</strong> '
. $purpose
. '</p>'
. ( $privacy_link !== '' ? '<p>' . $privacy_link . '</p>' : '' )
. '<button type="button" class="cb-blocker__button" data-cb-id="' . $id . '">'
. sprintf(
/* translators: %s: provider name */
esc_html__( '%s jetzt laden', 'gdpr-content-blocker' ),
$name
)
. '</button>'
. '<label class="cb-blocker__remember">'
. '<input type="checkbox" class="cb-blocker__remember-cb" checked> '
. esc_html__( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' )
. '</label>'
. '</div>'
. ( self::show_badge() ? self::badge_html() : '' )
. '</div>';
}
/**
* Whether to show the promo badge. Always shown, EXCEPT when the site owner
* has an active unlimited licence AND ticked "hide badge" in Appearance.
*/
public static function show_badge(): bool {
$style = CB_Settings::get_style();
return ! ( ! empty( $style['hide_badge'] ) && CB_License::is_unlimited() );
}
/** Small "GDPR Content Blocker" badge in the bottom-right of the placeholder. */
private static function badge_html(): string {
return '<a class="cb-blocker__badge" href="https://lucas-orth.de/wp-plugins/gdpr-content-blocker/"'
. ' target="_blank" rel="noopener noreferrer nofollow sponsored">GDPR Content Blocker</a>';
}
/**
* Pull src + width + height out of an iframe string.
* Height is also read from an inline style="height:NNNpx" if no attribute.
* Returns [ 'src' => string, 'width' => string, 'height' => string ].
*/
public static function extract_iframe_attrs( string $html ): array {
$src = '';
if ( preg_match( '/\bsrc=["\']([^"\']+)["\']/i', $html, $m ) ) {
$src = esc_url_raw( $m[1] );
}
$width = '';
if ( preg_match( '/\bwidth=["\']?(\d+(?:%|px)?)["\']?/i', $html, $m ) ) {
$width = $m[1];
}
$height = '';
if ( preg_match( '/\bheight=["\']?(\d+(?:%|px)?)["\']?/i', $html, $m ) ) {
$height = $m[1];
} elseif ( preg_match( '/height\s*:\s*(\d+)px/i', $html, $m ) ) {
$height = $m[1];
}
// Normalise "450px" → "450" so it can be used as a numeric attribute.
$width = preg_replace( '/px$/', '', $width );
$height = preg_replace( '/px$/', '', $height );
return [ 'src' => $src, 'width' => $width, 'height' => $height ];
}
}