chore: monorepo - plugin, backend und hilfsdaten in einem repo
- Eltern-Ordner ist jetzt EIN Git-Repo (statt getrennter Repos). - root .gitignore haelt Secrets (.env), node_modules, DB und Build-Artefakte raus. - release.ps1: manueller Release (ZIP bauen + ans Backend laden). - root README mit Struktur und Release-Ablauf. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
285
gdpr-content-blocker/includes/class-renderer.php
Normal file
285
gdpr-content-blocker/includes/class-renderer.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>'
|
||||
. '</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 ];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user