- 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>
837 lines
31 KiB
PHP
837 lines
31 KiB
PHP
<?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 '';
|
||
}
|
||
}
|