feat: third-party-scripts blockieren (server + client-guard)

Loest die YouTube/Vimeo-API-Luecke: viele dienste laden ueber <script>
(z. B. youtube.com/iframe_api, www-widgetapi.js, analytics) statt iframes -
oft per JavaScript nachgeladen, daher fuer den scanner unsichtbar.

- Pro dienst aktivierbar ueber das (umbenannte) feld "Zugehoerige Skripte
  blockieren (z. B. YouTube-/Vimeo-API)" = das vorhandene loads_script-flag.
  Presets (YouTube, Vimeo, Maps) haben es bereits an.
- Server-seitig: passende <script src> werden zu type="text/plain"
  (src -> data-cb-src) neutralisiert, laden also nicht.
- Client-seitig: winziger guard ganz frueh im <head> patcht
  appendChild/insertBefore/replaceChild und neutralisiert dynamisch
  injizierte scripts VOR dem einfuegen -> kein request. Faengt damit auch
  die per JS nachgeladene iframe_api ab.
- Einwilligung (per-dienst-consent, z. B. ueber den video-platzhalter)
  schaltet die scripts via cbActivateScripts frei und laedt sie nach.
- Neuer shortcode [content_blocker_consent id="…"] als einwilligungs-button
  fuer reine skript-dienste ohne sichtbaren platzhalter.
- guard-logik mit DOM-mock getestet (block + reinject), server-regex
  isoliert geprueft. i18n DE/EN ergaenzt (127 strings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-08 08:24:33 +02:00
parent b6df4c64af
commit ca945d2d80
9 changed files with 203 additions and 21 deletions

View File

@@ -26,8 +26,26 @@
} catch ( e ) { } catch ( e ) {
// localStorage unavailable; allow the load for this session only. // localStorage unavailable; allow the load for this session only.
} }
// Release any third-party <script> blocked for this service (handled by
// the early head guard, which exposes cbActivateScripts).
if ( typeof window.cbActivateScripts === 'function' ) {
window.cbActivateScripts( serviceId );
}
} }
/**
* Public: grant consent for a service from a custom control
* ([content_blocker_consent]) — used for script-only services that have no
* visible embed. Reloads so blocked scripts/embeds load cleanly.
*/
window.cbConsent = function ( serviceId ) {
if ( ! serviceId ) {
return;
}
grantConsent( serviceId );
window.location.reload();
};
/* ───────────────────────── iframe loading ────────────────────────── */ /* ───────────────────────── iframe loading ────────────────────────── */
/** Replace a .cb-blocker element with the real iframe. src comes from data-src. */ /** Replace a .cb-blocker element with the real iframe. src comes from data-src. */

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.3.0 * Version: 1.4.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

View File

@@ -47,23 +47,78 @@ class CB_Autodetect {
} }
public static function process( string $html, array $services ): string { public static function process( string $html, array $services ): string {
if ( $html === '' || stripos( $html, '<iframe' ) === false ) { if ( $html === '' ) {
return $html; return $html;
} }
// Locate iframe blocks only. Attribute parsing happens via DOMDocument below. // Pass 1 — iframes: replace matched embeds with a consent placeholder.
$out = preg_replace_callback( if ( stripos( $html, '<iframe' ) !== false ) {
'#<iframe\b[^>]*>.*?</iframe>#is', $out = preg_replace_callback(
function ( array $m ) use ( $services ): string { '#<iframe\b[^>]*>.*?</iframe>#is',
return self::maybe_replace_iframe( $m[0], $services ); function ( array $m ) use ( $services ): string {
}, return self::maybe_replace_iframe( $m[0], $services );
$html },
); $html
);
// On a PCRE error (e.g. backtrack/recursion limit on a huge page),
// preg_replace_callback returns null. Never blank the page.
if ( $out !== null ) {
$html = $out;
}
}
// On a PCRE error (e.g. backtrack/recursion limit on a huge page), // Pass 2 — scripts: neutralise <script src> of services that block scripts
// preg_replace_callback returns null. Never blank the page — fall back to // so they don't execute/fetch until consent (the JS guard re-injects them).
// the original, unmodified HTML. $script_services = array_values( array_filter(
return $out === null ? $html : $out; $services,
static fn( $s ) => ! empty( $s['loads_script'] )
) );
if ( $script_services && stripos( $html, '<script' ) !== false ) {
$out = preg_replace_callback(
'#<script\b[^>]*\bsrc\s*=\s*["\'][^"\']+["\'][^>]*>#i',
function ( array $m ) use ( $script_services ): string {
return self::maybe_block_script( $m[0], $script_services );
},
$html
);
if ( $out !== null ) {
$html = $out;
}
}
return $html;
}
/**
* Neutralise a <script src="…"> opening tag if its src matches a script-
* blocking service: rename src→data-cb-src, force type="text/plain", and tag
* it with the service id. The early head guard re-injects it on consent.
*/
private static function maybe_block_script( string $tag, array $services ): string {
if ( ! preg_match( '/\bsrc\s*=\s*["\']([^"\']+)["\']/i', $tag, $m ) ) {
return $tag;
}
$src = str_replace( '&amp;', '&', $m[1] );
foreach ( $services as $svc ) {
$pattern = $svc['match_pattern'] ?? '';
if ( $pattern === '' || ! str_contains( $src, $pattern ) ) {
continue;
}
$id = (string) ( $svc['id'] ?? '' );
// src → data-cb-src (first occurrence), drop any existing type, then
// force type="text/plain" + the service id on the opening tag.
$t = preg_replace( '#\bsrc(\s*=\s*)#i', 'data-cb-src$1', $tag, 1 );
$t = preg_replace( '#\btype\s*=\s*("[^"]*"|\'[^\']*\'|\S+)#i', '', $t );
$t = preg_replace(
'#^<script\b#i',
'<script type="text/plain" data-cb-id="' . esc_attr( $id ) . '"',
$t,
1
);
return $t ?? $tag;
}
return $tag;
} }
/** /**

View File

@@ -7,7 +7,65 @@ class CB_Renderer {
add_shortcode( 'content_blocker', [ __CLASS__, 'shortcode' ] ); add_shortcode( 'content_blocker', [ __CLASS__, 'shortcode' ] );
add_shortcode( 'content_blocker_revoke', [ __CLASS__, 'revoke_shortcode' ] ); add_shortcode( 'content_blocker_revoke', [ __CLASS__, 'revoke_shortcode' ] );
add_shortcode( 'content_blocker_services', [ __CLASS__, 'services_shortcode' ] ); add_shortcode( 'content_blocker_services', [ __CLASS__, 'services_shortcode' ] );
add_shortcode( 'content_blocker_consent', [ __CLASS__, 'consent_shortcode' ] );
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] ); add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
// Earliest possible: install the dynamic-script guard before theme/builder
// scripts run, so JS-injected third-party scripts (e.g. YouTube iframe_api)
// are caught before they fetch.
add_action( 'wp_head', [ __CLASS__, 'print_script_guard' ], 0 );
}
/** Enabled services that should also block matching <script> tags. */
public static function script_services(): array {
$out = [];
foreach ( CB_Settings::get_services() as $svc ) {
if ( empty( $svc['match_pattern'] ) || empty( $svc['loads_script'] ) || ! ( $svc['enabled'] ?? true ) ) {
continue;
}
$out[] = [ 'id' => $svc['id'], 'match' => $svc['match_pattern'] ];
}
return $out;
}
/**
* Inline guard printed very early in <head>. It blocks third-party scripts
* (both already in the DOM and dynamically injected) until the matching
* service has consent, by neutralising the <script> node right before it is
* inserted (no network request is made). Re-injection happens on consent.
*/
public static function print_script_guard(): void {
if ( is_admin() ) {
return;
}
$services = self::script_services();
if ( empty( $services ) ) {
return;
}
$json = wp_json_encode( $services );
echo "<script>\n/* gdpr-content-blocker: script guard */\n";
echo '(function(){"use strict";var S=' . $json . ';if(!S.length)return;'
. 'var P="cb_consent_";'
. 'function ok(id){try{return localStorage.getItem(P+id)==="1";}catch(e){return false;}}'
. 'function find(u){u=String(u||"");for(var i=0;i<S.length;i++){if(u.indexOf(S[i].match)!==-1)return S[i];}return null;}'
. 'var blocked=[];'
. 'function neutralize(node){try{if(!node||node.tagName!=="SCRIPT")return;'
. 'var src=node.src||(node.getAttribute?node.getAttribute("src"):"")||"";if(!src)return;'
. 'var s=find(src);if(!s||ok(s.id))return;'
. 'node.type="text/plain";node.setAttribute("data-cb-src",src);node.setAttribute("data-cb-id",s.id);'
. 'if(node.removeAttribute)node.removeAttribute("src");blocked.push(node);}catch(e){}}'
. 'function patch(proto,name){var o=proto[name];if(!o)return;proto[name]=function(n){try{neutralize(n);}catch(e){}return o.apply(this,arguments);};}'
. 'patch(Node.prototype,"appendChild");patch(Node.prototype,"insertBefore");patch(Node.prototype,"replaceChild");'
. 'function rein(el){try{var src=el.getAttribute&&el.getAttribute("data-cb-src");if(!src)return;'
. 'var s=document.createElement("script");for(var i=0;i<el.attributes.length;i++){var a=el.attributes[i];'
. 'if(/^(type|src|data-cb-src|data-cb-id)$/i.test(a.name))continue;try{s.setAttribute(a.name,a.value);}catch(e){}}'
. 's.src=src;var p=el.parentNode;if(p){p.insertBefore(s,el.nextSibling);p.removeChild(el);}}catch(e){}}'
. 'window.cbActivateScripts=function(id){blocked.slice().forEach(function(el){if(el.getAttribute("data-cb-id")===id)rein(el);});'
. 'var n=document.querySelectorAll(\'script[type="text/plain"][data-cb-src][data-cb-id]\');'
. 'Array.prototype.forEach.call(n,function(el){if(el.getAttribute("data-cb-id")===id)rein(el);});};'
. 'function pre(){for(var i=0;i<S.length;i++){if(ok(S[i].id))window.cbActivateScripts(S[i].id);}}'
. 'if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",pre);}else{pre();}'
. '})();';
echo "\n</script>\n";
} }
public static function enqueue_assets(): void { public static function enqueue_assets(): void {
@@ -147,6 +205,31 @@ class CB_Renderer {
return $out; return $out;
} }
/**
* Shortcode: [content_blocker_consent id="youtube" text="…" style="button|link"]
* A consent control for services that only block scripts (no visible embed
* to click). Grants consent for the given service id and reloads so the
* blocked scripts/embeds load.
*/
public static function consent_shortcode( array|string $atts = [] ): string {
$atts = shortcode_atts( [
'id' => '',
'text' => __( 'Externe Inhalte aktivieren', 'gdpr-content-blocker' ),
'style' => 'button',
], $atts, 'content_blocker_consent' );
$id = sanitize_key( $atts['id'] );
if ( $id === '' ) {
return '';
}
$class = $atts['style'] === 'link' ? 'cb-revoke-link' : 'cb-revoke-btn';
return '<a href="#" class="' . esc_attr( $class ) . '" role="button" '
. 'onclick="cbConsent(\'' . esc_js( $id ) . '\');return false;">'
. esc_html( $atts['text'] )
. '</a>';
}
/** /**
* Shortcode: [content_blocker_services] * Shortcode: [content_blocker_services]
* Lists every configured third-party service with the Art. 13 details * Lists every configured third-party service with the Art. 13 details

View File

@@ -616,10 +616,10 @@ class CB_Settings {
value="1" <?php checked( $b( 'sets_cookie' ) ); ?>> value="1" <?php checked( $b( 'sets_cookie' ) ); ?>>
<?php esc_html_e( 'Setzt Cookies', 'gdpr-content-blocker' ); ?> <?php esc_html_e( 'Setzt Cookies', 'gdpr-content-blocker' ); ?>
</label> </label>
<label> <label title="<?php esc_attr_e( 'Blockiert zusätzlich passende <script>-Einbindungen (auch per JavaScript nachgeladene, z. B. die YouTube-/Vimeo-API) bis zur Einwilligung.', 'gdpr-content-blocker' ); ?>">
<input type="checkbox" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][loads_script]" <input type="checkbox" name="cb_services[<?php echo esc_attr( (string) $index ); ?>][loads_script]"
value="1" <?php checked( $b( 'loads_script' ) ); ?>> value="1" <?php checked( $b( 'loads_script' ) ); ?>>
<?php esc_html_e( 'Lädt externe Skripte', 'gdpr-content-blocker' ); ?> <?php esc_html_e( 'Zugehörige Skripte blockieren (z. B. YouTube-/Vimeo-API)', 'gdpr-content-blocker' ); ?>
</label> </label>
</div> </div>
</div> </div>
@@ -674,6 +674,11 @@ class CB_Settings {
<?php echo $code( '[content_blocker_services]' ); ?></td> <?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> <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> </tr>
<tr>
<td style="vertical-align:top;"><strong><?php esc_html_e( 'Einwilligungs-Button (für reine Skript-Dienste)', 'gdpr-content-blocker' ); ?></strong><br>
<?php echo $code( '[content_blocker_consent id="youtube" text="…"]' ); ?></td>
<td><?php esc_html_e( 'Erteilt die Einwilligung für einen Dienst und lädt die Seite neu. Gedacht für Dienste, die nur Skripte blockieren und keinen sichtbaren Platzhalter haben. Optionen: id="…" (Dienst-ID, Pflicht), text="…", style="button|link".', 'gdpr-content-blocker' ); ?></td>
</tr>
</tbody> </tbody>
</table> </table>
<?php <?php

View File

@@ -114,6 +114,9 @@ msgstr "Withdraw consent for external content"
msgid "Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet." msgid "Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet."
msgstr "Affects only the release of external embeds (e.g. maps, videos). Cookie settings are managed separately." msgstr "Affects only the release of external embeds (e.g. maps, videos). Cookie settings are managed separately."
msgid "Externe Inhalte aktivieren"
msgstr "Enable external content"
msgid "Übermittlung in ein Drittland außerhalb der EU/des EWR" msgid "Übermittlung in ein Drittland außerhalb der EU/des EWR"
msgstr "Transfer to a third country outside the EU/EEA" msgstr "Transfer to a third country outside the EU/EEA"
@@ -282,8 +285,11 @@ msgstr "Custom placeholder text (empty = default)"
msgid "Datenübermittlung in Drittland (außerhalb EU/EWR)" msgid "Datenübermittlung in Drittland (außerhalb EU/EWR)"
msgstr "Data transfer to a third country (outside EU/EEA)" msgstr "Data transfer to a third country (outside EU/EEA)"
msgid "Lädt externe Skripte" msgid "Blockiert zusätzlich passende <script>-Einbindungen (auch per JavaScript nachgeladene, z. B. die YouTube-/Vimeo-API) bis zur Einwilligung."
msgstr "Loads external scripts" msgstr "Additionally blocks matching <script> includes (including ones injected via JavaScript, e.g. the YouTube/Vimeo API) until consent."
msgid "Zugehörige Skripte blockieren (z. B. YouTube-/Vimeo-API)"
msgstr "Also block related scripts (e.g. YouTube/Vimeo API)"
msgid "Textfarbe Platzhalter" msgid "Textfarbe Platzhalter"
msgstr "Placeholder text color" msgstr "Placeholder text color"
@@ -330,6 +336,12 @@ msgstr "Services overview (privacy policy)"
msgid "Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO)." msgid "Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO)."
msgstr "Lists all configured services with recipient, third-country note, purpose and privacy link. Ideal for inclusion in your privacy policy (Art. 13 GDPR)." msgstr "Lists all configured services with recipient, third-country note, purpose and privacy link. Ideal for inclusion in your privacy policy (Art. 13 GDPR)."
msgid "Einwilligungs-Button (für reine Skript-Dienste)"
msgstr "Consent button (for script-only services)"
msgid "Erteilt die Einwilligung für einen Dienst und lädt die Seite neu. Gedacht für Dienste, die nur Skripte blockieren und keinen sichtbaren Platzhalter haben. Optionen: id=\"…\" (Dienst-ID, Pflicht), text=\"…\", style=\"button|link\"."
msgstr "Grants consent for a service and reloads the page. Intended for services that only block scripts and have no visible placeholder. Options: id=\"…\" (service ID, required), text=\"…\", style=\"button|link\"."
msgid "Version %s" msgid "Version %s"
msgstr "Version %s" msgstr "Version %s"

View File

@@ -1,4 +1,4 @@
# 123 unique strings # 127 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.
@@ -35,6 +35,7 @@ MSGID Diesen Dienst künftig immer laden
MSGID Um diesen Inhalt von %s zu laden, ist Ihre Einwilligung erforderlich. Dabei werden personenbezogene Daten (z. B. Ihre IP-Adresse) an den Anbieter übertragen. MSGID Um diesen Inhalt von %s zu laden, ist Ihre Einwilligung erforderlich. Dabei werden personenbezogene Daten (z. B. Ihre IP-Adresse) an den Anbieter übertragen.
MSGID Einwilligung für externe Inhalte widerrufen MSGID Einwilligung für externe Inhalte widerrufen
MSGID Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet. MSGID Betrifft nur die Freigabe externer Einbettungen (z. B. Karten, Videos). Cookie-Einstellungen werden separat verwaltet.
MSGID Externe Inhalte aktivieren
MSGID Übermittlung in ein Drittland außerhalb der EU/des EWR MSGID Übermittlung in ein Drittland außerhalb der EU/des EWR
MSGID Empfänger MSGID Empfänger
MSGID Zweck MSGID Zweck
@@ -91,7 +92,8 @@ MSGID Datenschutz-URL des Anbieters
MSGID Verarbeitungszweck MSGID Verarbeitungszweck
MSGID Individueller Platzhaltertext (leer = Standard) MSGID Individueller Platzhaltertext (leer = Standard)
MSGID Datenübermittlung in Drittland (außerhalb EU/EWR) MSGID Datenübermittlung in Drittland (außerhalb EU/EWR)
MSGID Lädt externe Skripte MSGID Blockiert zusätzlich passende <script>-Einbindungen (auch per JavaScript nachgeladene, z. B. die YouTube-/Vimeo-API) bis zur Einwilligung.
MSGID Zugehörige Skripte blockieren (z. B. YouTube-/Vimeo-API)
MSGID Textfarbe Platzhalter MSGID Textfarbe Platzhalter
MSGID Hintergrundfarbe Platzhalter MSGID Hintergrundfarbe Platzhalter
MSGID Button: Hintergrundfarbe MSGID Button: Hintergrundfarbe
@@ -107,6 +109,8 @@ MSGID Inhalt manuell blockieren
MSGID Umschließt ein iframe und ersetzt es durch den Platzhalter des angegebenen Dienstes. Die ID ist der Anbietername in Kleinbuchstaben mit Bindestrichen (z. B. „Google Maps" → google-maps). MSGID Umschließt ein iframe und ersetzt es durch den Platzhalter des angegebenen Dienstes. Die ID ist der Anbietername in Kleinbuchstaben mit Bindestrichen (z. B. „Google Maps" → google-maps).
MSGID Dienste-Übersicht (Datenschutzerklärung) MSGID Dienste-Übersicht (Datenschutzerklärung)
MSGID Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO). MSGID Listet alle konfigurierten Dienste mit Empfänger, Drittland-Hinweis, Zweck und Datenschutz-Link auf. Ideal zum Einbinden in die Datenschutzerklärung (Art. 13 DSGVO).
MSGID Einwilligungs-Button (für reine Skript-Dienste)
MSGID Erteilt die Einwilligung für einen Dienst und lädt die Seite neu. Gedacht für Dienste, die nur Skripte blockieren und keinen sichtbaren Platzhalter haben. Optionen: id="…" (Dienst-ID, Pflicht), text="…", style="button|link".
MSGID Version %s MSGID Version %s
MSGID 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. MSGID 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.
MSGID Funktionen MSGID Funktionen

View File

@@ -121,5 +121,10 @@
"Unerwartete Antwort vom Lizenzserver.": "Unexpected response from the license server.", "Unerwartete Antwort vom Lizenzserver.": "Unexpected response from the license server.",
"Lizenz upgraden": "Upgrade license", "Lizenz upgraden": "Upgrade license",
"Schalten Sie weitere Domains frei, um diese Seite zusätzlich zu aktivieren – oder geben Sie unten eine bestehende Domain frei.": "Unlock more domains to additionally activate this site — or release an existing domain below.", "Schalten Sie weitere Domains frei, um diese Seite zusätzlich zu aktivieren – oder geben Sie unten eine bestehende Domain frei.": "Unlock more domains to additionally activate this site — or release an existing domain below.",
"Schriftart für DSGVO-konformen Betrieb lokal hosten.": "Host this font locally for GDPR-compliant operation." "Schriftart für DSGVO-konformen Betrieb lokal hosten.": "Host this font locally for GDPR-compliant operation.",
"Zugehörige Skripte blockieren (z. B. YouTube-/Vimeo-API)": "Also block related scripts (e.g. YouTube/Vimeo API)",
"Blockiert zusätzlich passende <script>-Einbindungen (auch per JavaScript nachgeladene, z. B. die YouTube-/Vimeo-API) bis zur Einwilligung.": "Additionally blocks matching <script> includes (including ones injected via JavaScript, e.g. the YouTube/Vimeo API) until consent.",
"Externe Inhalte aktivieren": "Enable external content",
"Einwilligungs-Button (für reine Skript-Dienste)": "Consent button (for script-only services)",
"Erteilt die Einwilligung für einen Dienst und lädt die Seite neu. Gedacht für Dienste, die nur Skripte blockieren und keinen sichtbaren Platzhalter haben. Optionen: id=\"…\" (Dienst-ID, Pflicht), text=\"…\", style=\"button|link\".": "Grants consent for a service and reloads the page. Intended for services that only block scripts and have no visible placeholder. Options: id=\"…\" (service ID, required), text=\"…\", style=\"button|link\"."
} }