Files
s4luorth 576ad1f74a fix: pfeil groesser, tab ohne fokus-kasten, version 1.1.0 (cache-bust)
- Aufklapp-Pfeil deutlich groesser (26px).
- Tabs: kein Fokus-Kasten mehr, nur untere Linie markiert den aktiven Tab.
- CB_VERSION + Header auf 1.1.0 -> bricht gecachte alte admin.js/frontend.js
  (Ursache, dass ein Dienst auf einer Seite nicht aufklappbar war).
- Aufklappen via Event-Delegation (robust gegen Load-Order/dynamische Zeilen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:31:28 +02:00

837 lines
31 KiB
PHP
Raw Permalink 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.
<?php
defined( 'ABSPATH' ) || exit;
class CB_Settings {
public static function init(): void {
add_action( 'admin_menu', [ __CLASS__, 'add_menu' ] );
add_action( 'admin_init', [ __CLASS__, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_admin_assets' ] );
add_action( 'admin_post_cb_save_services', [ __CLASS__, 'save_services' ] );
add_action( 'wp_ajax_cb_scan', [ __CLASS__, 'ajax_scan' ] );
}
public static function add_menu(): void {
add_options_page(
__( 'GDPR Content Blocker', 'gdpr-content-blocker' ),
__( 'GDPR Content Blocker', 'gdpr-content-blocker' ),
'manage_options',
'gdpr-content-blocker',
[ __CLASS__, 'render_page' ]
);
}
public static function register_settings(): void {
register_setting(
'cb_style_group',
CB_STYLE_OPTION,
[ 'sanitize_callback' => [ __CLASS__, 'sanitize_style' ] ]
);
}
public static function sanitize_style( mixed $input ): array {
$defaults = self::get_style_defaults();
if ( ! is_array( $input ) ) {
return $defaults;
}
$clean = [];
$color_keys = [
'text_color',
'bg_color',
'button_bg',
'button_text',
'button_hover_bg',
'button_hover_text',
];
foreach ( $color_keys as $key ) {
$val = isset( $input[ $key ] ) ? sanitize_text_field( $input[ $key ] ) : '';
$clean[ $key ] = self::sanitize_hex_color( $val ) ?? $defaults[ $key ];
}
$clean['custom_css'] = isset( $input['custom_css'] )
? wp_strip_all_tags( $input['custom_css'] )
: '';
return $clean;
}
private static function sanitize_hex_color( string $color ): ?string {
if ( preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color ) ) {
return $color;
}
return null;
}
public static function get_style_defaults(): array {
return [
'text_color' => '#ffffff',
'bg_color' => '#111111',
'button_bg' => '#2043B7',
'button_text' => '#ffffff',
'button_hover_bg' => '#1a369a',
'button_hover_text' => '#ffffff',
'custom_css' => '',
];
}
public static function get_style(): array {
$saved = get_option( CB_STYLE_OPTION, [] );
return wp_parse_args( is_array( $saved ) ? $saved : [], self::get_style_defaults() );
}
/** Returns all services from the option, always as a list. */
public static function get_services(): array {
$raw = get_option( CB_OPTION, [] );
return is_array( $raw ) ? array_values( $raw ) : [];
}
/** Returns a single service by id, or null. */
public static function get_service( string $id ): ?array {
foreach ( self::get_services() as $svc ) {
if ( isset( $svc['id'] ) && $svc['id'] === $id ) {
return $svc;
}
}
return null;
}
public static function save_services(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Keine Berechtigung.', 'gdpr-content-blocker' ) );
}
check_admin_referer( 'cb_save_services', 'cb_nonce' );
$raw_services = isset( $_POST['cb_services'] ) && is_array( $_POST['cb_services'] )
? $_POST['cb_services']
: [];
$services = [];
foreach ( $raw_services as $item ) {
if ( ! is_array( $item ) ) {
continue;
}
$id = sanitize_key( $item['id'] ?? '' );
if ( $id === '' ) {
continue;
}
$services[] = [
'id' => $id,
'name' => sanitize_text_field( $item['name'] ?? '' ),
'enabled' => ! empty( $item['enabled'] ),
'match_pattern' => sanitize_text_field( $item['match_pattern'] ?? '' ),
'recipient' => sanitize_text_field( $item['recipient'] ?? '' ),
'third_country' => ! empty( $item['third_country'] ),
'sets_cookie' => ! empty( $item['sets_cookie'] ),
'loads_script' => ! empty( $item['loads_script'] ),
'purpose' => sanitize_textarea_field( $item['purpose'] ?? '' ),
'privacy_url' => esc_url_raw( $item['privacy_url'] ?? '' ),
'placeholder_text' => sanitize_textarea_field( $item['placeholder_text'] ?? '' ),
];
}
update_option( CB_OPTION, $services );
wp_safe_redirect( admin_url( 'options-general.php?page=gdpr-content-blocker&cb_saved=1' ) );
exit;
}
public static function enqueue_admin_assets( string $hook ): void {
if ( $hook !== 'settings_page_gdpr-content-blocker' ) {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_style( 'dashicons' );
wp_enqueue_script(
'cb-admin',
CB_URL . 'assets/admin.js',
[ 'wp-color-picker', 'jquery' ],
CB_VERSION,
true
);
wp_localize_script( 'cb-admin', 'cbAdmin', [
'confirmRemove' => __( 'Dienst wirklich entfernen?', 'gdpr-content-blocker' ),
'presets' => self::get_presets(),
'newServiceLbl' => __( 'Neuer Dienst', 'gdpr-content-blocker' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'scanNonce' => wp_create_nonce( 'cb_scan' ),
'i18n' => [
'scanning' => __( 'Scanne Webseite …', 'gdpr-content-blocker' ),
'scanError' => __( 'Scan fehlgeschlagen:', 'gdpr-content-blocker' ),
'noFindings' => __( 'Keine externen Einbindungen gefunden.', 'gdpr-content-blocker' ),
'host' => __( 'Anbieter / Host', 'gdpr-content-blocker' ),
'type' => __( 'Typ', 'gdpr-content-blocker' ),
'count' => __( 'Anzahl', 'gdpr-content-blocker' ),
'example' => __( 'Beispiel-URL', 'gdpr-content-blocker' ),
'status' => __( 'Status', 'gdpr-content-blocker' ),
'action' => __( 'Aktion', 'gdpr-content-blocker' ),
'covered' => __( 'abgedeckt', 'gdpr-content-blocker' ),
'thirdParty' => __( 'Drittanbieter', 'gdpr-content-blocker' ),
'firstParty' => __( 'eigene Domain', 'gdpr-content-blocker' ),
'addService' => __( 'Als Dienst übernehmen', 'gdpr-content-blocker' ),
'useTemplate' => __( 'Vorlage übernehmen', 'gdpr-content-blocker' ),
'templateAvail' => __( 'Vorlage verfügbar', 'gdpr-content-blocker' ),
'scannedPages'=> __( 'Gescannte Seiten:', 'gdpr-content-blocker' ),
'foundOn' => __( 'Gefunden auf', 'gdpr-content-blocker' ),
],
] );
}
/**
* Ready-made templates for the most common third-party iframes.
* Everything stays editable after insertion.
*/
public static function get_presets(): array {
return [
'google-maps' => [
'id' => 'google-maps',
'name' => 'Google Maps',
'match_pattern' => 'google.com/maps',
'recipient' => __( 'Google Ireland Ltd., Irland / Google LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => true,
'loads_script' => true,
'purpose' => __( 'Darstellung interaktiver Karten und Standortinformationen.', 'gdpr-content-blocker' ),
'privacy_url' => 'https://policies.google.com/privacy',
],
'youtube' => [
'id' => 'youtube',
'name' => 'YouTube',
'match_pattern' => 'youtube',
'recipient' => __( 'Google Ireland Ltd., Irland / Google LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => true,
'loads_script' => true,
'purpose' => __( 'Einbettung und Wiedergabe von Videos.', 'gdpr-content-blocker' ),
'privacy_url' => 'https://policies.google.com/privacy',
],
'openstreetmap' => [
'id' => 'openstreetmap',
'name' => 'OpenStreetMap',
'match_pattern' => 'openstreetmap.org',
'recipient' => __( 'OpenStreetMap Foundation, Großbritannien', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => false,
'loads_script' => true,
'purpose' => __( 'Darstellung interaktiver Karten.', 'gdpr-content-blocker' ),
'privacy_url' => 'https://wiki.osmfoundation.org/wiki/Privacy_Policy',
],
'vimeo' => [
'id' => 'vimeo',
'name' => 'Vimeo',
'match_pattern' => 'player.vimeo.com',
'recipient' => __( 'Vimeo LLC, USA', 'gdpr-content-blocker' ),
'third_country' => true,
'sets_cookie' => true,
'loads_script' => true,
'purpose' => __( 'Einbettung und Wiedergabe von Videos.', 'gdpr-content-blocker' ),
'privacy_url' => 'https://vimeo.com/privacy',
],
];
}
public static function render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$services = self::get_services();
$style = self::get_style();
$saved = isset( $_GET['cb_saved'] ) && $_GET['cb_saved'] === '1';
$settings_saved = isset( $_GET['settings-updated'] ) && $_GET['settings-updated'] === 'true';
?>
<div class="wrap cb-admin-wrap">
<h1><?php esc_html_e( 'GDPR Content Blocker', 'gdpr-content-blocker' ); ?></h1>
<?php if ( $saved || $settings_saved ) : ?>
<div class="notice notice-success is-dismissible">
<p><?php esc_html_e( 'Einstellungen gespeichert.', 'gdpr-content-blocker' ); ?></p>
</div>
<?php endif; ?>
<nav class="nav-tab-wrapper">
<a href="#cb-tab-services" class="nav-tab nav-tab-active" data-cb-tab="services">
<?php esc_html_e( 'Dienste', 'gdpr-content-blocker' ); ?>
</a>
<a href="#cb-tab-scan" class="nav-tab" data-cb-tab="scan">
<?php esc_html_e( 'Scan', 'gdpr-content-blocker' ); ?>
</a>
<a href="#cb-tab-style" class="nav-tab" data-cb-tab="style">
<?php esc_html_e( 'Darstellung', 'gdpr-content-blocker' ); ?>
</a>
<a href="#cb-tab-shortcodes" class="nav-tab" data-cb-tab="shortcodes">
<?php esc_html_e( 'Shortcodes', 'gdpr-content-blocker' ); ?>
</a>
<a href="#cb-tab-license" class="nav-tab" data-cb-tab="license">
<?php esc_html_e( 'Lizenz', 'gdpr-content-blocker' ); ?>
</a>
<a href="#cb-tab-about" class="nav-tab" data-cb-tab="about">
<?php esc_html_e( 'Über das Plugin', 'gdpr-content-blocker' ); ?>
</a>
</nav>
<!-- Services Tab -->
<div id="cb-tab-services" class="cb-tab-content">
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="cb_save_services">
<?php wp_nonce_field( 'cb_save_services', 'cb_nonce' ); ?>
<div id="cb-services-list">
<?php foreach ( $services as $i => $svc ) : ?>
<?php self::render_service_row( $i, $svc ); ?>
<?php endforeach; ?>
</div>
<div class="cb-add-row">
<button type="button" id="cb-add-service" class="button">
<?php esc_html_e( '+ Leeren Dienst hinzufügen', 'gdpr-content-blocker' ); ?>
</button>
<select id="cb-preset-select">
<option value=""><?php esc_html_e( '— Vorlage einfügen —', 'gdpr-content-blocker' ); ?></option>
<?php foreach ( self::get_presets() as $key => $preset ) : ?>
<option value="<?php echo esc_attr( $key ); ?>">
<?php echo esc_html( $preset['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php submit_button( __( 'Dienste speichern', 'gdpr-content-blocker' ) ); ?>
</form>
<!-- Hidden template for JS -->
<template id="cb-service-template">
<?php self::render_service_row( '__INDEX__', [] ); ?>
</template>
</div>
<!-- Scan Tab -->
<div id="cb-tab-scan" class="cb-tab-content" style="display:none;">
<p class="description" style="max-width:760px;">
<?php esc_html_e( 'Der Lizenzserver besucht Ihre Webseite und listet alle eingebundenen Drittanbieter-Ressourcen (iframes, Skripte, Schriften, Bilder, …) auf. So sehen Sie auf einen Blick, welche externen Dienste Sie blockieren sollten. Erfordert eine aktive Lizenz.', 'gdpr-content-blocker' ); ?>
</p>
<p>
<button type="button" id="cb-scan-btn" class="button button-primary">
<?php esc_html_e( 'Webseite scannen', 'gdpr-content-blocker' ); ?>
</button>
<span id="cb-scan-status" style="margin-left:10px;"></span>
</p>
<div id="cb-scan-results"></div>
</div>
<!-- Style Tab -->
<div id="cb-tab-style" class="cb-tab-content" style="display:none;">
<form method="post" action="<?php echo esc_url( admin_url( 'options.php' ) ); ?>">
<?php
settings_fields( 'cb_style_group' );
self::render_style_fields( $style );
submit_button( __( 'Darstellung speichern', 'gdpr-content-blocker' ) );
?>
</form>
</div>
<!-- Shortcodes Tab -->
<div id="cb-tab-shortcodes" class="cb-tab-content" style="display:none;">
<?php self::render_shortcodes_help(); ?>
</div>
<!-- License Tab -->
<div id="cb-tab-license" class="cb-tab-content" style="display:none;">
<?php CB_License::render_tab(); ?>
</div>
<!-- About Tab -->
<div id="cb-tab-about" class="cb-tab-content" style="display:none;">
<?php self::render_about(); ?>
</div>
</div>
<style>
.cb-admin-wrap .cb-service-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px 16px;
margin: 8px 0;
}
.cb-admin-wrap .cb-service-head {
display: flex;
align-items: center;
gap: 10px;
}
.cb-admin-wrap .cb-service-toggle {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 4px 6px;
color: #50575e;
}
.cb-admin-wrap .cb-service-title {
flex: 1;
font-weight: 600;
cursor: pointer;
user-select: none;
}
.cb-admin-wrap .cb-service-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 24px;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid #eee;
}
/* toggle switch */
.cb-admin-wrap .cb-switch {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
flex: 0 0 auto;
}
.cb-admin-wrap .cb-switch input {
opacity: 0;
width: 0;
height: 0;
}
.cb-admin-wrap .cb-switch__slider {
position: absolute;
inset: 0;
background: #c3c4c7;
border-radius: 22px;
transition: background 0.2s;
}
.cb-admin-wrap .cb-switch__slider::before {
content: "";
position: absolute;
height: 16px;
width: 16px;
left: 3px;
top: 3px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.cb-admin-wrap .cb-switch input:checked + .cb-switch__slider {
background: #2043B7;
}
.cb-admin-wrap .cb-switch input:checked + .cb-switch__slider::before {
transform: translateX(18px);
}
/* expand/collapse arrow (black, no box) */
.cb-admin-wrap .cb-service-toggle {
width: 32px;
height: 32px;
font-size: 26px;
line-height: 1;
font-weight: 400;
color: #1d2327;
background: none;
border: none;
padding: 0;
cursor: pointer;
}
.cb-admin-wrap .cb-service-toggle:hover {
color: #000;
}
/* trash remove button */
.cb-admin-wrap .cb-remove-service {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: #888;
display: inline-flex;
align-items: center;
}
.cb-admin-wrap .cb-remove-service:hover {
color: #b32d2e;
}
.cb-admin-wrap .cb-remove-service .dashicons {
font-size: 20px;
width: 20px;
height: 20px;
}
/* active tab indicator */
.cb-admin-wrap .nav-tab-wrapper {
margin-bottom: 0;
}
.cb-admin-wrap .nav-tab-active,
.cb-admin-wrap .nav-tab-active:focus,
.cb-admin-wrap .nav-tab-active:hover {
background: #fff;
color: #2043B7;
border-bottom: 3px solid #2043B7;
font-weight: 600;
margin-bottom: -1px;
}
/* No focus box on tabs — only the bottom underline marks the active tab. */
.cb-admin-wrap .nav-tab:focus,
.cb-admin-wrap .nav-tab:focus-visible,
.cb-admin-wrap .nav-tab:active {
box-shadow: none;
outline: none;
}
/* 20px breathing room between tabs and content */
.cb-admin-wrap .cb-tab-content {
padding-top: 20px;
}
/* bound-domains list (license full) */
.cb-admin-wrap .cb-domain-list {
margin: 10px 0 0;
max-width: 480px;
}
.cb-admin-wrap .cb-domain-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 12px;
margin: 0 0 6px;
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
}
.cb-admin-wrap .cb-domain-list form {
margin: 0;
}
.cb-admin-wrap .cb-field label {
display: block;
font-weight: 600;
margin-bottom: 4px;
}
.cb-admin-wrap .cb-field input[type="text"],
.cb-admin-wrap .cb-field input[type="url"],
.cb-admin-wrap .cb-field textarea {
width: 100%;
}
.cb-admin-wrap .cb-field-full {
grid-column: 1 / -1;
}
.cb-admin-wrap .cb-checks {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.cb-admin-wrap .cb-add-row {
display: flex;
gap: 10px;
align-items: center;
margin: 12px 0;
}
.cb-admin-wrap .cb-add-row select {
max-width: 240px;
}
</style>
<?php
}
public static function render_service_row( int|string $index, array $svc ): void {
$f = fn( string $key, string $default = '' ) => esc_attr( $svc[ $key ] ?? $default );
$b = fn( string $key ) => ! empty( $svc[ $key ] );
$idx = esc_attr( (string) $index );
$name = $svc['name'] ?? '';
$id = $svc['id'] ?? '';
$enabled = $svc['enabled'] ?? true; // default ON (new + legacy services)
// Header label: "interner-name — Anbietername"
$headname = trim( ( $id !== '' ? $id : __( 'neu', 'gdpr-content-blocker' ) ) . ( $name !== '' ? ' — ' . $name : '' ) );
?>
<div class="cb-service-box" data-cb-index="<?php echo $idx; ?>">
<div class="cb-service-head">
<button type="button" class="cb-service-toggle" aria-expanded="false" aria-label="<?php esc_attr_e( 'Details anzeigen/ausblenden', 'gdpr-content-blocker' ); ?>">▸</button>
<span class="cb-service-title"><?php echo esc_html( $headname ); ?></span>
<label class="cb-switch" title="<?php esc_attr_e( 'Blocker aktiv/inaktiv', 'gdpr-content-blocker' ); ?>">
<input type="checkbox" class="cb-input-enabled" name="cb_services[<?php echo $idx; ?>][enabled]"
value="1" <?php checked( $enabled ); ?>>
<span class="cb-switch__slider"></span>
</label>
<button type="button" class="cb-remove-service" aria-label="<?php esc_attr_e( 'Dienst entfernen', 'gdpr-content-blocker' ); ?>" title="<?php esc_attr_e( 'Dienst entfernen', 'gdpr-content-blocker' ); ?>">
<span class="dashicons dashicons-trash"></span>
</button>
</div>
<div class="cb-service-grid" style="display:none;">
<div class="cb-field">
<label><?php esc_html_e( 'Interner Name (Slug)', 'gdpr-content-blocker' ); ?> *</label>
<input type="text" name="cb_services[<?php echo $idx; ?>][id]"
value="<?php echo $f( 'id' ); ?>" required
placeholder="google-maps" pattern="[a-z0-9\-]+" class="cb-input-id">
</div>
<div class="cb-field">
<label><?php esc_html_e( 'Anbietername', 'gdpr-content-blocker' ); ?> *</label>
<input type="text" name="cb_services[<?php echo $idx; ?>][name]"
value="<?php echo $f( 'name' ); ?>" required class="cb-input-name">
</div>
<div class="cb-field">
<label><?php esc_html_e( 'Erkennungsmuster (Domain/Pfad)', 'gdpr-content-blocker' ); ?></label>
<input type="text" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][match_pattern]"
value="<?php echo $f( 'match_pattern' ); ?>"
placeholder="google.com/maps">
</div>
<div class="cb-field">
<label><?php esc_html_e( 'Empfänger (inkl. Land)', 'gdpr-content-blocker' ); ?> *</label>
<input type="text" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][recipient]"
value="<?php echo $f( 'recipient' ); ?>"
placeholder="Google Ireland Ltd. / USA" required>
</div>
<div class="cb-field">
<label><?php esc_html_e( 'Datenschutz-URL des Anbieters', 'gdpr-content-blocker' ); ?></label>
<input type="url" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][privacy_url]"
value="<?php echo esc_attr( esc_url( $svc['privacy_url'] ?? '' ) ); ?>"
placeholder="https://policies.google.com/privacy">
</div>
<div class="cb-field cb-field-full">
<label><?php esc_html_e( 'Verarbeitungszweck', 'gdpr-content-blocker' ); ?> *</label>
<textarea name="cb_services[<?php echo esc_attr( (string) $index ); ?>][purpose]"
rows="2" required><?php echo esc_textarea( $svc['purpose'] ?? '' ); ?></textarea>
</div>
<div class="cb-field cb-field-full">
<label><?php esc_html_e( 'Individueller Platzhaltertext (leer = Standard)', 'gdpr-content-blocker' ); ?></label>
<textarea name="cb_services[<?php echo esc_attr( (string) $index ); ?>][placeholder_text]"
rows="2"><?php echo esc_textarea( $svc['placeholder_text'] ?? '' ); ?></textarea>
</div>
<div class="cb-field cb-field-full cb-checks">
<label>
<input type="checkbox" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][third_country]"
value="1" <?php checked( $b( 'third_country' ) ); ?>>
<?php esc_html_e( 'Datenübermittlung in Drittland (außerhalb EU/EWR)', 'gdpr-content-blocker' ); ?>
</label>
<label>
<input type="checkbox" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][sets_cookie]"
value="1" <?php checked( $b( 'sets_cookie' ) ); ?>>
<?php esc_html_e( 'Setzt Cookies', 'gdpr-content-blocker' ); ?>
</label>
<label>
<input type="checkbox" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][loads_script]"
value="1" <?php checked( $b( 'loads_script' ) ); ?>>
<?php esc_html_e( 'Lädt externe Skripte', 'gdpr-content-blocker' ); ?>
</label>
</div>
</div>
</div>
<?php
}
private static function render_style_fields( array $style ): void {
$fields = [
'text_color' => __( 'Textfarbe Platzhalter', 'gdpr-content-blocker' ),
'bg_color' => __( 'Hintergrundfarbe Platzhalter', 'gdpr-content-blocker' ),
'button_bg' => __( 'Button: Hintergrundfarbe', 'gdpr-content-blocker' ),
'button_text' => __( 'Button: Textfarbe', 'gdpr-content-blocker' ),
'button_hover_bg' => __( 'Button Hover: Hintergrundfarbe', 'gdpr-content-blocker' ),
'button_hover_text' => __( 'Button Hover: Textfarbe', 'gdpr-content-blocker' ),
];
echo '<table class="form-table" role="presentation"><tbody>';
foreach ( $fields as $key => $label ) {
$val = esc_attr( $style[ $key ] );
echo '<tr><th scope="row"><label>' . esc_html( $label ) . '</label></th>';
echo '<td><input type="text" class="cb-color-picker" name="' . esc_attr( CB_STYLE_OPTION . '[' . $key . ']' ) . '" value="' . $val . '" data-default-color="' . $val . '"></td></tr>';
}
echo '<tr><th scope="row"><label>' . esc_html__( 'Custom CSS', 'gdpr-content-blocker' ) . '</label></th>';
echo '<td><textarea name="' . esc_attr( CB_STYLE_OPTION . '[custom_css]' ) . '" rows="8" class="large-text code">' . esc_textarea( $style['custom_css'] ) . '</textarea>';
echo '<p class="description">' . esc_html__( 'Wird nach den CSS-Variablen eingebunden und kann diese überschreiben. Tipp: denselben Präfix verwenden, z. B. .cb-blocker .cb-blocker__button { … }', 'gdpr-content-blocker' ) . '</p>';
echo '<p class="description"><strong>' . esc_html__( 'CSS-Klassen:', 'gdpr-content-blocker' ) . '</strong> '
. '<code>.cb-blocker</code>, <code>.cb-blocker__text</code>, <code>.cb-blocker__recipient</code>, '
. '<code>.cb-blocker__purpose</code>, <code>.cb-blocker__privacy-link</code>, '
. '<code>.cb-blocker__button</code>, <code>.cb-revoke-link</code></p></td></tr>';
echo '</tbody></table>';
}
/** "Shortcodes" tab: only the shortcodes (no feature listing). */
private static function render_shortcodes_help(): void {
$code = static fn( string $s ): string => '<code>' . esc_html( $s ) . '</code>';
?>
<h2><?php esc_html_e( 'Shortcodes', 'gdpr-content-blocker' ); ?></h2>
<table class="widefat striped" style="max-width:900px;">
<tbody>
<tr>
<td style="width:300px;vertical-align:top;"><strong><?php esc_html_e( 'Widerruf (Datenschutzerklärung)', 'gdpr-content-blocker' ); ?></strong><br>
<?php echo $code( '[content_blocker_revoke]' ); ?></td>
<td><?php esc_html_e( 'Rendert einen gut sichtbaren Link, der die Einwilligung für externe Einbettungen widerruft und die Seite neu lädt (Art. 7 Abs. 3 DSGVO). Betrifft NICHT die Cookie-Einwilligung eines separaten Cookie-Plugins. Optionen: text="…", style="link|button", note="yes|no".', 'gdpr-content-blocker' ); ?></td>
</tr>
<tr>
<td style="vertical-align:top;"><strong><?php esc_html_e( 'Inhalt manuell blockieren', 'gdpr-content-blocker' ); ?></strong><br>
<?php echo $code( '[content_blocker id="google-maps"]…iframe…[/content_blocker]' ); ?></td>
<td><?php esc_html_e( 'Umschließt ein iframe und ersetzt es durch den Platzhalter des angegebenen Dienstes. Verwenden Sie den internen Namen aus dem Tab „Dienste".', 'gdpr-content-blocker' ); ?></td>
</tr>
<tr>
<td style="vertical-align:top;"><strong><?php esc_html_e( 'Dienste-Übersicht (Datenschutzerklärung)', 'gdpr-content-blocker' ); ?></strong><br>
<?php echo $code( '[content_blocker_services]' ); ?></td>
<td><?php esc_html_e( 'Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO).', 'gdpr-content-blocker' ); ?></td>
</tr>
</tbody>
</table>
<?php
}
/** "Über das Plugin" tab: description + legal links. */
private static function render_about(): void {
?>
<div style="max-width:760px;">
<h2>GDPR Content Blocker</h2>
<p class="description" style="font-size:13px;">
<?php
printf(
/* translators: %s: version */
esc_html__( 'Version %s', 'gdpr-content-blocker' ),
esc_html( CB_VERSION )
);
?>
</p>
<p>
<?php esc_html_e( 'GDPR Content Blocker lädt externe Einbindungen (z. B. Google Maps, YouTube, OpenStreetMap, Vimeo) erst nach aktiver Einwilligung der Besucher. Vor dem Klick wird keine Verbindung zum Drittanbieter aufgebaut es werden also keine personenbezogenen Daten (z. B. die IP-Adresse) übertragen. So lassen sich externe Inhalte DSGVO-konform einbinden, ohne ein schweres Consent-Tool.', 'gdpr-content-blocker' ); ?>
</p>
<h3><?php esc_html_e( 'Funktionen', 'gdpr-content-blocker' ); ?></h3>
<ul style="list-style:disc;padding-left:20px;">
<li><?php esc_html_e( 'Echtes Click-to-Load (kein vorab geladenes iframe)', 'gdpr-content-blocker' ); ?></li>
<li><?php esc_html_e( 'Granulare Einwilligung pro Dienst', 'gdpr-content-blocker' ); ?></li>
<li><?php esc_html_e( 'Art.-13-konformer Platzhalter mit Empfänger, Zweck, Drittland-Hinweis und Datenschutz-Link', 'gdpr-content-blocker' ); ?></li>
<li><?php esc_html_e( 'Einfacher Widerruf per Shortcode (Art. 7 Abs. 3 DSGVO)', 'gdpr-content-blocker' ); ?></li>
<li><?php esc_html_e( 'Automatische Erkennung gängiger Anbieter + manueller Shortcode', 'gdpr-content-blocker' ); ?></li>
</ul>
<h3><?php esc_html_e( 'Rechtliches', 'gdpr-content-blocker' ); ?></h3>
<p>
<a href="https://lucas-orth.de/impressum" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Impressum', 'gdpr-content-blocker' ); ?></a><br>
<a href="https://lucas-orth.de/datenschutz" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Datenschutzerklärung', 'gdpr-content-blocker' ); ?></a>
</p>
<p class="description">
<?php esc_html_e( 'Hinweis: Dieses Plugin ist ein technisches Hilfsmittel und ersetzt keine Rechtsberatung. Für die rechtskonforme Konfiguration (Empfänger, Zwecke, Datenschutzerklärung) ist der Seitenbetreiber verantwortlich.', 'gdpr-content-blocker' ); ?>
</p>
<p style="margin-top:20px;">
<?php
printf(
/* translators: %s: author link */
esc_html__( 'Entwickelt von %s', 'gdpr-content-blocker' ),
'<a href="https://lucas-orth.de" target="_blank" rel="noopener noreferrer">Lucas Orth</a>'
);
?>
</p>
</div>
<?php
}
/* ───────────────────────── Scan (AJAX) ───────────────────────── */
/**
* Build the list of URLs to scan: the home page plus a few published
* pages/posts, so the scan covers more than just the front page.
*/
private static function scan_urls(): array {
$urls = [ home_url( '/' ) ];
$posts = get_posts( [
'post_type' => [ 'page', 'post' ],
'post_status' => 'publish',
'numberposts' => 4,
'orderby' => 'comment_count', // roughly "most visited"
'order' => 'DESC',
'fields' => 'ids',
'no_found_rows' => true,
] );
foreach ( $posts as $pid ) {
$link = get_permalink( $pid );
if ( $link ) {
$urls[] = $link;
}
}
return array_values( array_unique( $urls ) );
}
public static function ajax_scan(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Keine Berechtigung.', 'gdpr-content-blocker' ) ], 403 );
}
check_ajax_referer( 'cb_scan', 'nonce' );
$lic = CB_License::get_license();
if ( ( $lic['status'] ?? '' ) !== 'active' ) {
wp_send_json_error( [ 'message' => __( 'Bitte zuerst eine Lizenz aktivieren (Tab „Lizenz").', 'gdpr-content-blocker' ) ] );
}
$response = wp_remote_post( CB_License::api_url() . '/api/v1/scan', [
'timeout' => 45,
'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ],
'body' => wp_json_encode( [
'key' => $lic['key'],
'product' => CB_PRODUCT_SLUG,
'domain' => CB_License::domain(),
'urls' => self::scan_urls(),
] ),
] );
if ( is_wp_error( $response ) ) {
wp_send_json_error( [ 'message' => $response->get_error_message() ] );
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $data ) || empty( $data['ok'] ) ) {
$msg = is_array( $data ) && ! empty( $data['error'] )
? $data['error']
: __( 'Unerwartete Antwort vom Lizenzserver.', 'gdpr-content-blocker' );
wp_send_json_error( [ 'message' => $msg ] );
}
// Annotate each finding: already covered by a configured service? And is
// there a ready-made template (preset) for it?
$services = self::get_services();
$presets = self::get_presets();
$findings = is_array( $data['findings'] ?? null ) ? $data['findings'] : [];
foreach ( $findings as &$f ) {
$f['covered'] = self::is_covered( $f, $services );
$f['preset'] = self::match_preset( $f, $presets ); // preset key or ''
}
unset( $f );
wp_send_json_success( [
'findings' => $findings,
'scanned' => $data['scanned'] ?? [],
] );
}
/** Collect host + sample URLs of a finding for substring matching. */
private static function finding_haystacks( array $finding ): array {
return array_merge(
[ (string) ( $finding['host'] ?? '' ) ],
array_map( 'strval', (array) ( $finding['sample_urls'] ?? [] ) )
);
}
/** True if any configured service's match_pattern matches this finding. */
private static function is_covered( array $finding, array $services ): bool {
$haystacks = self::finding_haystacks( $finding );
foreach ( $services as $svc ) {
$pattern = $svc['match_pattern'] ?? '';
if ( $pattern === '' ) {
continue;
}
foreach ( $haystacks as $h ) {
if ( $h !== '' && str_contains( $h, $pattern ) ) {
return true;
}
}
}
return false;
}
/** Return the key of a preset whose match_pattern fits this finding, or ''. */
private static function match_preset( array $finding, array $presets ): string {
$haystacks = self::finding_haystacks( $finding );
foreach ( $presets as $key => $preset ) {
$pattern = $preset['match_pattern'] ?? '';
if ( $pattern === '' ) {
continue;
}
foreach ( $haystacks as $h ) {
if ( $h !== '' && str_contains( $h, $pattern ) ) {
return (string) $key;
}
}
}
return '';
}
}