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:
@@ -7,7 +7,65 @@ class CB_Renderer {
|
||||
add_shortcode( 'content_blocker', [ __CLASS__, 'shortcode' ] );
|
||||
add_shortcode( 'content_blocker_revoke', [ __CLASS__, 'revoke_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' ] );
|
||||
// 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 {
|
||||
@@ -147,6 +205,31 @@ class CB_Renderer {
|
||||
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]
|
||||
* Lists every configured third-party service with the Art. 13 details
|
||||
|
||||
Reference in New Issue
Block a user