feat: Werbe-Badge im content blocker (unten rechts) + abschaltbar nur mit unlimited (v1.5.0)

- Kleines Badge "GDPR Content Blocker" unten rechts im platzhalter, verlinkt
  auf die kaufseite (rel=nofollow sponsored). Server- und client-seitig.
- Wird IMMER angezeigt; abschaltbar via checkbox im Darstellung-tab
  ("Werbe-Badge ausblenden") - die checkbox ist aber nur mit aktiver
  Unlimited-lizenz nutzbar (sonst disabled) und greift am render-punkt nur,
  wenn is_unlimited() true ist.
- Dafuer speichert das plugin jetzt max_activations aus der backend-antwort
  (activate/validate/swap) und bietet is_unlimited().

+ i18n DE/EN (134 strings). version 1.5.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-08 14:08:08 +02:00
parent c796400492
commit 2ef1d0769b
10 changed files with 101 additions and 11 deletions

View File

@@ -20,6 +20,7 @@
/* ── Wrapper / placeholder ── */ /* ── Wrapper / placeholder ── */
.cb-blocker { .cb-blocker {
position: relative; /* anchor for the bottom-right badge */
display: flex; display: flex;
align-items: flex-start; /* pin content to the top */ align-items: flex-start; /* pin content to the top */
justify-content: center; justify-content: center;
@@ -27,12 +28,29 @@
background-color: var(--cb-bg); background-color: var(--cb-bg);
color: var(--cb-text); color: var(--cb-text);
box-sizing: border-box; box-sizing: border-box;
padding: 24px 8px 20px; /* min 20px top/bottom, 8px sides */ padding: 24px 8px 22px; /* min 20px top/bottom, 8px sides */
font-family: inherit; font-family: inherit;
border-radius: 4px; border-radius: 4px;
/* No reserved height: the placeholder is exactly as tall as its content. */ /* No reserved height: the placeholder is exactly as tall as its content. */
} }
/* ── Promo badge (bottom-right) ── */
.cb-blocker .cb-blocker__badge {
position: absolute;
right: 8px;
bottom: 5px;
font-size: 0.6875rem;
line-height: 1;
color: var(--cb-text);
opacity: 0.55;
text-decoration: none;
white-space: nowrap;
}
.cb-blocker .cb-blocker__badge:hover {
opacity: 1;
text-decoration: underline;
}
/* ── Inner content ── */ /* ── Inner content ── */
.cb-blocker .cb-blocker__inner { .cb-blocker .cb-blocker__inner {
width: 100%; width: 100%;

View File

@@ -319,6 +319,18 @@
inner.appendChild( label ); inner.appendChild( label );
wrap.appendChild( inner ); wrap.appendChild( inner );
// Promo badge (bottom-right), mirrors the server-rendered placeholder.
if ( CFG.showBadge ) {
const badge = document.createElement( 'a' );
badge.className = 'cb-blocker__badge';
badge.href = 'https://lucas-orth.de/wp-plugins/gdpr-content-blocker/';
badge.target = '_blank';
badge.rel = 'noopener noreferrer nofollow sponsored';
badge.textContent = 'GDPR Content Blocker';
wrap.appendChild( badge );
}
return wrap; return wrap;
} }

View File

@@ -3,7 +3,7 @@
* Plugin Name: GDPR Content Blocker * Plugin Name: GDPR Content Blocker
* Plugin URI: https://lucas-orth.de * Plugin URI: https://lucas-orth.de
* Description: DSGVO-konformer Consent-Blocker für externe iframes. Lädt Drittinhalte erst nach aktiver Einwilligung. * Description: DSGVO-konformer Consent-Blocker für externe iframes. Lädt Drittinhalte erst nach aktiver Einwilligung.
* Version: 1.4.9 * Version: 1.5.0
* Author: Lucas Orth * Author: Lucas Orth
* Author URI: https://lucas-orth.de * Author URI: https://lucas-orth.de
* Text Domain: gdpr-content-blocker * Text Domain: gdpr-content-blocker
@@ -15,7 +15,7 @@
defined( 'ABSPATH' ) || exit; defined( 'ABSPATH' ) || exit;
define( 'CB_VERSION', '1.4.9' ); define( 'CB_VERSION', '1.5.0' );
define( 'CB_FILE', __FILE__ ); define( 'CB_FILE', __FILE__ );
define( 'CB_DIR', plugin_dir_path( __FILE__ ) ); define( 'CB_DIR', plugin_dir_path( __FILE__ ) );
define( 'CB_URL', plugin_dir_url( __FILE__ ) ); define( 'CB_URL', plugin_dir_url( __FILE__ ) );

View File

@@ -64,6 +64,7 @@ class CB_License {
'last_check' => 0, 'last_check' => 0,
'message' => '', 'message' => '',
'pending_domains' => [], 'pending_domains' => [],
'max_activations' => 0, // -1 = unlimited (from the backend)
] ); ] );
if ( ! is_array( $lic['pending_domains'] ) ) { if ( ! is_array( $lic['pending_domains'] ) ) {
$lic['pending_domains'] = []; $lic['pending_domains'] = [];
@@ -75,6 +76,12 @@ class CB_License {
return self::get_license()['status'] === 'active'; return self::get_license()['status'] === 'active';
} }
/** True only for an active, unlimited-seat (max_activations = -1) license. */
public static function is_unlimited(): bool {
$lic = self::get_license();
return $lic['status'] === 'active' && (int) ( $lic['max_activations'] ?? 0 ) === -1;
}
/** Mask a key for display: keep dashes + last 4 chars, hide the rest. */ /** Mask a key for display: keep dashes + last 4 chars, hide the rest. */
public static function mask_key( string $key ): string { public static function mask_key( string $key ): string {
$n = strlen( $key ); $n = strlen( $key );
@@ -148,7 +155,7 @@ class CB_License {
} }
if ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) { if ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) {
self::store( $key, 'active', __( 'Lizenz erfolgreich aktiviert.', 'gdpr-content-blocker' ) ); self::store( $key, 'active', __( 'Lizenz erfolgreich aktiviert.', 'gdpr-content-blocker' ), [], isset( $result['max_activations'] ) ? (int) $result['max_activations'] : null );
} elseif ( ( $result['code'] ?? '' ) === 'limit_reached' ) { } elseif ( ( $result['code'] ?? '' ) === 'limit_reached' ) {
// No free slots: keep the key and offer to release one of the bound domains. // No free slots: keep the key and offer to release one of the bound domains.
$domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : []; $domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : [];
@@ -216,7 +223,7 @@ class CB_License {
/* translators: %s: released domain */ /* translators: %s: released domain */
__( 'Domain %s freigegeben und diese Seite aktiviert.', 'gdpr-content-blocker' ), __( 'Domain %s freigegeben und diese Seite aktiviert.', 'gdpr-content-blocker' ),
$release $release
) ); ), [], isset( $result['max_activations'] ) ? (int) $result['max_activations'] : null );
} else { } else {
$domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : $lic['pending_domains']; $domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : $lic['pending_domains'];
$msg = $result['error'] ?? __( 'Aktivierung fehlgeschlagen.', 'gdpr-content-blocker' ); $msg = $result['error'] ?? __( 'Aktivierung fehlgeschlagen.', 'gdpr-content-blocker' );
@@ -269,14 +276,18 @@ class CB_License {
} }
if ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) { if ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) {
self::store( $lic['key'], 'active', '' ); self::store( $lic['key'], 'active', '', [], isset( $result['max_activations'] ) ? (int) $result['max_activations'] : null );
} else { } else {
$msg = $result['error'] ?? __( 'Lizenz nicht mehr gültig.', 'gdpr-content-blocker' ); $msg = $result['error'] ?? __( 'Lizenz nicht mehr gültig.', 'gdpr-content-blocker' );
self::store( $lic['key'], 'invalid', $msg ); self::store( $lic['key'], 'invalid', $msg );
} }
} }
private static function store( string $key, string $status, string $message, array $pending_domains = [] ): void { private static function store( string $key, string $status, string $message, array $pending_domains = [], ?int $max = null ): void {
// Preserve the previously stored seat count unless a fresh one is given.
$prev = get_option( CB_LICENSE_OPTION, [] );
$prev_max = is_array( $prev ) && isset( $prev['max_activations'] ) ? (int) $prev['max_activations'] : 0;
update_option( CB_LICENSE_OPTION, [ update_option( CB_LICENSE_OPTION, [
'key' => $key, 'key' => $key,
'status' => $status, 'status' => $status,
@@ -284,6 +295,7 @@ class CB_License {
'last_check' => time(), 'last_check' => time(),
'message' => $message, 'message' => $message,
'pending_domains' => array_values( array_filter( array_map( 'strval', $pending_domains ) ) ), 'pending_domains' => array_values( array_filter( array_map( 'strval', $pending_domains ) ) ),
'max_activations' => $max !== null ? $max : $prev_max,
] ); ] );
// License state changed → invalidate cached update info and force WP to // License state changed → invalidate cached update info and force WP to

View File

@@ -107,8 +107,9 @@ class CB_Renderer {
// Service data + i18n for client-side detection (JS-injected iframes such // Service data + i18n for client-side detection (JS-injected iframes such
// as Elementor video widgets, sliders, …). // as Elementor video widgets, sliders, …).
wp_localize_script( 'cb-frontend', 'cbConfig', [ wp_localize_script( 'cb-frontend', 'cbConfig', [
'services' => self::services_for_js(), 'services' => self::services_for_js(),
'i18n' => [ 'showBadge' => self::show_badge(),
'i18n' => [
'recipient' => __( 'Empfänger:', 'gdpr-content-blocker' ), 'recipient' => __( 'Empfänger:', 'gdpr-content-blocker' ),
'purpose' => __( 'Zweck:', 'gdpr-content-blocker' ), 'purpose' => __( 'Zweck:', 'gdpr-content-blocker' ),
'thirdCountry' => __( 'Datenübermittlung in ein Drittland außerhalb der EU/des EWR', 'gdpr-content-blocker' ), 'thirdCountry' => __( 'Datenübermittlung in ein Drittland außerhalb der EU/des EWR', 'gdpr-content-blocker' ),
@@ -366,9 +367,25 @@ class CB_Renderer {
. esc_html__( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' ) . esc_html__( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' )
. '</label>' . '</label>'
. '</div>' . '</div>'
. ( self::show_badge() ? self::badge_html() : '' )
. '</div>'; . '</div>';
} }
/**
* Whether to show the promo badge. Always shown, EXCEPT when the site owner
* has an active unlimited licence AND ticked "hide badge" in Appearance.
*/
public static function show_badge(): bool {
$style = CB_Settings::get_style();
return ! ( ! empty( $style['hide_badge'] ) && CB_License::is_unlimited() );
}
/** Small "GDPR Content Blocker" badge in the bottom-right of the placeholder. */
private static function badge_html(): string {
return '<a class="cb-blocker__badge" href="https://lucas-orth.de/wp-plugins/gdpr-content-blocker/"'
. ' target="_blank" rel="noopener noreferrer nofollow sponsored">GDPR Content Blocker</a>';
}
/** /**
* Pull src + width + height out of an iframe string. * Pull src + width + height out of an iframe string.
* Height is also read from an inline style="height:NNNpx" if no attribute. * Height is also read from an inline style="height:NNNpx" if no attribute.

View File

@@ -50,6 +50,9 @@ class CB_Settings {
$clean['custom_css'] = isset( $input['custom_css'] ) $clean['custom_css'] = isset( $input['custom_css'] )
? wp_strip_all_tags( $input['custom_css'] ) ? wp_strip_all_tags( $input['custom_css'] )
: ''; : '';
// Hiding the promo badge only ever takes effect with an active unlimited
// licence (enforced again at render time), but we store the choice as-is.
$clean['hide_badge'] = ! empty( $input['hide_badge'] );
return $clean; return $clean;
} }
@@ -69,6 +72,7 @@ class CB_Settings {
'button_hover_bg' => '#1a369a', 'button_hover_bg' => '#1a369a',
'button_hover_text' => '#ffffff', 'button_hover_text' => '#ffffff',
'custom_css' => '', 'custom_css' => '',
'hide_badge' => false,
]; ];
} }
@@ -666,6 +670,18 @@ class CB_Settings {
echo '<tr><th scope="row"><label>' . esc_html( $label ) . '</label></th>'; 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 '<td><input type="text" class="cb-color-picker" name="' . esc_attr( CB_STYLE_OPTION . '[' . $key . ']' ) . '" value="' . $val . '" data-default-color="' . $val . '"></td></tr>';
} }
// Promo-badge toggle — only effective with an active unlimited licence.
$unlimited = CB_License::is_unlimited();
echo '<tr><th scope="row">' . esc_html__( 'Werbe-Badge', 'gdpr-content-blocker' ) . '</th><td>';
echo '<label><input type="checkbox" name="' . esc_attr( CB_STYLE_OPTION . '[hide_badge]' ) . '" value="1" '
. checked( ! empty( $style['hide_badge'] ), true, false )
. ( $unlimited ? '' : ' disabled' ) . '> '
. esc_html__( 'Badge „GDPR Content Blocker" im Platzhalter ausblenden', 'gdpr-content-blocker' ) . '</label>';
if ( ! $unlimited ) {
echo '<p class="description">' . esc_html__( 'Das Ausblenden ist nur mit einer aktiven Unlimited-Lizenz möglich.', 'gdpr-content-blocker' ) . '</p>';
}
echo '</td></tr>';
echo '<tr><th scope="row"><label>' . esc_html__( 'Custom CSS', 'gdpr-content-blocker' ) . '</label></th>'; 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 '<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">' . 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>';

View File

@@ -312,6 +312,15 @@ msgstr "Button hover: background color"
msgid "Button Hover: Textfarbe" msgid "Button Hover: Textfarbe"
msgstr "Button hover: text color" msgstr "Button hover: text color"
msgid "Werbe-Badge"
msgstr "Promo badge"
msgid "Badge „GDPR Content Blocker\" im Platzhalter ausblenden"
msgstr "Hide the \"GDPR Content Blocker\" badge in the placeholder"
msgid "Das Ausblenden ist nur mit einer aktiven Unlimited-Lizenz möglich."
msgstr "Hiding it is only possible with an active unlimited licence."
msgid "Custom CSS" msgid "Custom CSS"
msgstr "Custom CSS" msgstr "Custom CSS"

View File

@@ -1,4 +1,4 @@
# 131 unique strings # 134 unique strings
MSGID Ungültige Antwort vom Lizenzserver. MSGID Ungültige Antwort vom Lizenzserver.
MSGID Keine Berechtigung. MSGID Keine Berechtigung.
MSGID Bitte einen Lizenzschlüssel eingeben. MSGID Bitte einen Lizenzschlüssel eingeben.
@@ -101,6 +101,9 @@ MSGID Button: Hintergrundfarbe
MSGID Button: Textfarbe MSGID Button: Textfarbe
MSGID Button Hover: Hintergrundfarbe MSGID Button Hover: Hintergrundfarbe
MSGID Button Hover: Textfarbe MSGID Button Hover: Textfarbe
MSGID Werbe-Badge
MSGID Badge „GDPR Content Blocker" im Platzhalter ausblenden
MSGID Das Ausblenden ist nur mit einer aktiven Unlimited-Lizenz möglich.
MSGID Custom CSS MSGID Custom CSS
MSGID Wird nach den CSS-Variablen eingebunden und kann diese überschreiben. Tipp: denselben Präfix verwenden, z. B. .cb-blocker .cb-blocker__button { … } MSGID Wird nach den CSS-Variablen eingebunden und kann diese überschreiben. Tipp: denselben Präfix verwenden, z. B. .cb-blocker .cb-blocker__button { … }
MSGID CSS-Klassen: MSGID CSS-Klassen:

View File

@@ -130,5 +130,8 @@
"Erteilt die Einwilligung für einen Dienst (z. B. einen reinen Skript-Blocker ohne sichtbaren Platzhalter) und lädt die Seite neu, sodass das Skript freigegeben wird.": "Grants consent for a service (e.g. a script-only blocker with no visible placeholder) and reloads the page so the script is released.", "Erteilt die Einwilligung für einen Dienst (z. B. einen reinen Skript-Blocker ohne sichtbaren Platzhalter) und lädt die Seite neu, sodass das Skript freigegeben wird.": "Grants consent for a service (e.g. a script-only blocker with no visible placeholder) and reloads the page so the script is released.",
"Pflicht. Die ID des Dienstes (siehe Tab „Dienste\", klein unter dem Namen), z. B. eigenes-matomo.": "Required. The service ID (see the \"Services\" tab, shown small under the name), e.g. eigenes-matomo.", "Pflicht. Die ID des Dienstes (siehe Tab „Dienste\", klein unter dem Namen), z. B. eigenes-matomo.": "Required. The service ID (see the \"Services\" tab, shown small under the name), e.g. eigenes-matomo.",
"Die Beschriftung der Schaltfläche bzw. des Links, z. B. text=\"Statistik aktivieren\".": "The label of the button or link, e.g. text=\"Enable statistics\".", "Die Beschriftung der Schaltfläche bzw. des Links, z. B. text=\"Statistik aktivieren\".": "The label of the button or link, e.g. text=\"Enable statistics\".",
"style=\"button\" zeigt eine Schaltfläche (Standard), style=\"link\" einen schlichten Textlink.": "style=\"button\" shows a button (default), style=\"link\" a plain text link." "style=\"button\" zeigt eine Schaltfläche (Standard), style=\"link\" einen schlichten Textlink.": "style=\"button\" shows a button (default), style=\"link\" a plain text link.",
"Werbe-Badge": "Promo badge",
"Badge „GDPR Content Blocker\" im Platzhalter ausblenden": "Hide the \"GDPR Content Blocker\" badge in the placeholder",
"Das Ausblenden ist nur mit einer aktiven Unlimited-Lizenz möglich.": "Hiding it is only possible with an active unlimited licence."
} }