From 2ef1d0769b1495f6c7fa74b369c1a825cec9a867 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Mon, 8 Jun 2026 14:08:08 +0200 Subject: [PATCH] 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 --- gdpr-content-blocker/assets/frontend.css | 20 ++++++++++++++++- gdpr-content-blocker/assets/frontend.js | 12 ++++++++++ gdpr-content-blocker/gdpr-content-blocker.php | 4 ++-- .../includes/class-license.php | 20 +++++++++++++---- .../includes/class-renderer.php | 21 ++++++++++++++++-- .../includes/class-settings.php | 16 +++++++++++++ .../languages/gdpr-content-blocker-en_US.mo | Bin 13319 -> 13634 bytes .../languages/gdpr-content-blocker-en_US.po | 9 ++++++++ hilfsdaten/strings.txt | 5 ++++- hilfsdaten/translations-en.json | 5 ++++- 10 files changed, 101 insertions(+), 11 deletions(-) diff --git a/gdpr-content-blocker/assets/frontend.css b/gdpr-content-blocker/assets/frontend.css index f3e444e..af867d8 100644 --- a/gdpr-content-blocker/assets/frontend.css +++ b/gdpr-content-blocker/assets/frontend.css @@ -20,6 +20,7 @@ /* ── Wrapper / placeholder ── */ .cb-blocker { + position: relative; /* anchor for the bottom-right badge */ display: flex; align-items: flex-start; /* pin content to the top */ justify-content: center; @@ -27,12 +28,29 @@ background-color: var(--cb-bg); color: var(--cb-text); 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; border-radius: 4px; /* 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 ── */ .cb-blocker .cb-blocker__inner { width: 100%; diff --git a/gdpr-content-blocker/assets/frontend.js b/gdpr-content-blocker/assets/frontend.js index efd51fe..8b0343b 100644 --- a/gdpr-content-blocker/assets/frontend.js +++ b/gdpr-content-blocker/assets/frontend.js @@ -319,6 +319,18 @@ inner.appendChild( label ); 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; } diff --git a/gdpr-content-blocker/gdpr-content-blocker.php b/gdpr-content-blocker/gdpr-content-blocker.php index b067506..77543df 100644 --- a/gdpr-content-blocker/gdpr-content-blocker.php +++ b/gdpr-content-blocker/gdpr-content-blocker.php @@ -3,7 +3,7 @@ * Plugin Name: GDPR Content Blocker * Plugin URI: https://lucas-orth.de * 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 URI: https://lucas-orth.de * Text Domain: gdpr-content-blocker @@ -15,7 +15,7 @@ defined( 'ABSPATH' ) || exit; -define( 'CB_VERSION', '1.4.9' ); +define( 'CB_VERSION', '1.5.0' ); define( 'CB_FILE', __FILE__ ); define( 'CB_DIR', plugin_dir_path( __FILE__ ) ); define( 'CB_URL', plugin_dir_url( __FILE__ ) ); diff --git a/gdpr-content-blocker/includes/class-license.php b/gdpr-content-blocker/includes/class-license.php index 10a30de..8e40da8 100644 --- a/gdpr-content-blocker/includes/class-license.php +++ b/gdpr-content-blocker/includes/class-license.php @@ -64,6 +64,7 @@ class CB_License { 'last_check' => 0, 'message' => '', 'pending_domains' => [], + 'max_activations' => 0, // -1 = unlimited (from the backend) ] ); if ( ! is_array( $lic['pending_domains'] ) ) { $lic['pending_domains'] = []; @@ -75,6 +76,12 @@ class CB_License { 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. */ public static function mask_key( string $key ): string { $n = strlen( $key ); @@ -148,7 +155,7 @@ class CB_License { } 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' ) { // No free slots: keep the key and offer to release one of the bound domains. $domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : []; @@ -216,7 +223,7 @@ class CB_License { /* translators: %s: released domain */ __( 'Domain %s freigegeben und diese Seite aktiviert.', 'gdpr-content-blocker' ), $release - ) ); + ), [], isset( $result['max_activations'] ) ? (int) $result['max_activations'] : null ); } else { $domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : $lic['pending_domains']; $msg = $result['error'] ?? __( 'Aktivierung fehlgeschlagen.', 'gdpr-content-blocker' ); @@ -269,14 +276,18 @@ class CB_License { } 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 { $msg = $result['error'] ?? __( 'Lizenz nicht mehr gültig.', 'gdpr-content-blocker' ); 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, [ 'key' => $key, 'status' => $status, @@ -284,6 +295,7 @@ class CB_License { 'last_check' => time(), 'message' => $message, '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 diff --git a/gdpr-content-blocker/includes/class-renderer.php b/gdpr-content-blocker/includes/class-renderer.php index 75bd872..d5c6027 100644 --- a/gdpr-content-blocker/includes/class-renderer.php +++ b/gdpr-content-blocker/includes/class-renderer.php @@ -107,8 +107,9 @@ class CB_Renderer { // Service data + i18n for client-side detection (JS-injected iframes such // as Elementor video widgets, sliders, …). wp_localize_script( 'cb-frontend', 'cbConfig', [ - 'services' => self::services_for_js(), - 'i18n' => [ + 'services' => self::services_for_js(), + 'showBadge' => self::show_badge(), + 'i18n' => [ 'recipient' => __( 'Empfänger:', 'gdpr-content-blocker' ), 'purpose' => __( 'Zweck:', 'gdpr-content-blocker' ), 'thirdCountry' => __( 'Datenübermittlung in ein Drittland außerhalb der EU/des EWR', 'gdpr-content-blocker' ), @@ -366,9 +367,25 @@ class CB_Renderer { . esc_html__( 'Diesen Dienst künftig immer laden', 'gdpr-content-blocker' ) . '' . '' + . ( self::show_badge() ? self::badge_html() : '' ) . ''; } + /** + * 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 'GDPR Content Blocker'; + } + /** * Pull src + width + height out of an iframe string. * Height is also read from an inline style="height:NNNpx" if no attribute. diff --git a/gdpr-content-blocker/includes/class-settings.php b/gdpr-content-blocker/includes/class-settings.php index 03e6e60..244e0a8 100644 --- a/gdpr-content-blocker/includes/class-settings.php +++ b/gdpr-content-blocker/includes/class-settings.php @@ -50,6 +50,9 @@ class CB_Settings { $clean['custom_css'] = isset( $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; } @@ -69,6 +72,7 @@ class CB_Settings { 'button_hover_bg' => '#1a369a', 'button_hover_text' => '#ffffff', 'custom_css' => '', + 'hide_badge' => false, ]; } @@ -666,6 +670,18 @@ class CB_Settings { echo ''; echo ''; } + // Promo-badge toggle — only effective with an active unlimited licence. + $unlimited = CB_License::is_unlimited(); + echo '' . esc_html__( 'Werbe-Badge', 'gdpr-content-blocker' ) . ''; + echo ''; + if ( ! $unlimited ) { + echo '

' . esc_html__( 'Das Ausblenden ist nur mit einer aktiven Unlimited-Lizenz möglich.', 'gdpr-content-blocker' ) . '

'; + } + echo ''; + echo ''; echo ''; echo '

' . 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' ) . '

'; diff --git a/gdpr-content-blocker/languages/gdpr-content-blocker-en_US.mo b/gdpr-content-blocker/languages/gdpr-content-blocker-en_US.mo index 66746346a60f29a4f9357871e378246d90770176..f60b23b83538a8f14cb4e2b1d551b70b26defd18 100644 GIT binary patch delta 2518 zcmZwHdu&ui6vy#tDfB@L6%<;qGPHoDQi{+*!72h(KoJ`PA`m3o_S(I&y<2v-R0_B# zuY_j;N>LtxU_?z+Oi>V26rurwibM?|SW=Dh5;O)H9|8UTx~u+Uv)j+i+Zvb#*|_M>VZ9|2|mF2_!$;sd1qq=-~uec9e5jl zgRAi;yaS(RlAhRvS@;&-f=4l(_01U?%xTUe&zN6v6y8LOBQs(X&%|-`>rnUY#T@)7 ze!T_r=wHKOm_^y{#Yy-(u0mCC8QDz39hk}brj14~OwQzsxu^&WumH=j2y1XFHeoIn zbCXIr9f#l($R)ELiNWl^q4*~D$5tGU$=zb>-G|XZ49uXR309ygu@6CNpAkUf~ zQ6>EiwNOtsd^hH!5;%!^{xaT*ZP!T)~Z)Y-0;; zK?SfImFU5kQFEAvB0q+j=qyggE2s?0I2bDZ!>EO4p#pPpCI+z>kK-u(3l+#HUJ+#& zMm-lrKd!}mY{TAq|NHRL4q@OP)Pezg4i_RtFgKCoVRERm9F7Y3QPkO}M3p=kw-L3+ z%Tb5-1+Ib6DRL)6l>aROx(pH!j3s_y(TFR@5mzFd+6zc>?uZ`kk>K zmfn~`e*`My`;jEgWV{RK;R@V_#9;bShN$*pA`J~xpdPG8m2N5WG3)t9Rn2}JjAu|8 zrSd8elgUOtW;Fj&(M2T^Mpb4l>i*5hu`sVs3@GC7 zQ4`(3P1uXslksiT;d~FZ_s3D0e2cuP<_0Rm?4nrZ24W)pl6b!qGw7G2Dmn$LaAA~2 zM;hnwBL09$_ys329lt_Vq7~<08|t(_#sSgw)tH8xQCqnSmD%g4c|OL)_$j`Ee0jwbJ1*!&94}(W27*;{ zolvfIYi&iq7iskS0ud)7cWVM}m49$^r;K@--JMXSQ#e+UnCWh{6AINoxp8OL z?B=GNOC3rlFk&Nq$L9Wjh?VUbxL*4`bpc#I_fw*H?WEs;51l{2{cdft*QX%(ShZP03!=J3$x6aEIWGFc4( delta 2219 zcmYM!eN2^Q7{~GJ91n0fym-(~nHnBA9+BwqZUqxu&jWo{wVGEM~hI)N|ouT!$$>=f*nJgPXA*_hLEzfUD4-X0`?&zyLmp$MGm; z<3e`33kz`x)?ya6BER-DA3pZCLkxJvhOrXIFo<8F7QUJIJ|)BK9_EWt*Gn*jby$X7 zScPZsPrQn%*te8#3*JIMHjvd)Y{nG!w>}1nY!L6o6Ig^F;Tt%Qxp<7HRl0Yu7$=cm zo8yDM>=u@yi^alNj`w3TYQvLwAHIjhcm-or(|n7ZQUy?zh#=ot1*(*fpf>8ni#UK< zFvNlD`D(la8}M1&fd^e?-yp@aO_WRP?ZgN^gE@FMK>gKpvt0NZf5HVgMd#2CHjS#p zCA=GdM>qO7Sk|@-RHiwo!0J#Hx`Ep8HtIev4WOf2jtaaIDXMK*Lj9Fl8yD30{iuis z5;qKCKJ(XcH_oCe6egN9GAuzoSH-HTKn-fct#}x_a1H*7I-(E_p^BBD0$m$pK+^Uw zD)Vjl7PjCg_zx<>8Oo%J%;8GBj+}yJk(L%*iF!?oP}i$bfjo-;C5S=h{Wy-}Sb?!3 z4p{GT2P&d{sEztj5kHSnd=<6OJU)njqmHC}h1n=Jp(-HRNfpvaG+&SV6&@!P1tCa@W&QSW~iol}Xms0|OJ?tcZ_@ieYRPpVX~-=-L-TaR+ns~gIAH`x9jLd?LFpxHQk1 z=b$Q7i>laWRE2h8jB431gF<{4mC5(Wo;Hu_em{4sCn`~ytVdO32kQO~`4?fQP~T6W0{#%S&SiWJe?dKWBtrewjW0)>Ge3>WFQYP?$29y4-Iz>t znx|q029RVegnO_NlW+pB;WT>iY?0Gb=TViIiZN(q@Hwix^Jvv{+>R=72kI<)QJD>( z78=1p9LEb7ptDHIW>A@3LsjG^s+pISI_ni;Ci4o^^;kUvZ5U11j>^0b58_Eww`TI8 zg=