- 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>
326 lines
12 KiB
PHP
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 !== '' ? ' — ' . $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 ];
|
|
}
|
|
}
|