Files
GDPR-Content-Blocker/gdpr-content-blocker/includes/class-renderer.php
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

326 lines
12 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_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
}
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.
$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;}',
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'] )
);
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(),
'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_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 ) . '"' : '';
// Reserve the embed's height so the layout doesn't jump, and remember the
// original dimensions for the iframe that gets created on consent.
$width = isset( $dims['width'] ) ? (string) $dims['width'] : '';
$height = isset( $dims['height'] ) ? (string) $dims['height'] : '';
$style_at = '';
if ( $height !== '' && ctype_digit( $height ) ) {
$style_at = ' style="min-height:' . esc_attr( $height ) . 'px"';
}
$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 !== '' ? ' &mdash; ' . $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>'
. '</div>';
}
/** Pull the src attribute out of an iframe string. */
public static function extract_iframe_src( string $html ): string {
return self::extract_iframe_attrs( $html )['src'];
}
/**
* 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 ];
}
}