fix: tabs ohne seiten-rahmen, scan aller unterseiten, button-radius bei hover (v1.5.3)

- Admin-tabs: kein rahmen/box/outline in irgendeinem zustand mehr - nur die
  untere unterstreichung markiert den aktiven tab (WP-default-borders ueber-
  schrieben, auch :focus/:active).
- Scan deckt jetzt ALLE veroeffentlichten seiten/beitraege (alle public post
  types) ab statt nur 4. Backend: limit 10 -> 60 URLs und PARALLELE abfrage
  (concurrency 12, 8s/seite), plugin-AJAX-timeout 45 -> 90s.
- Platzhalter-button: border-radius in hover/focus/active festgenagelt (3px) ->
  theme kann die ecken beim hover nicht mehr veraendern.

+ version 1.5.3. (Backend-redeploy noetig fuer den scan.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-08 18:19:57 +02:00
parent ef84c43ded
commit 3228965c60
4 changed files with 59 additions and 40 deletions

View File

@@ -125,14 +125,17 @@
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
/* Hover: only the colour changes — no frame, no resize. */ /* Hover: only the colour changes — no frame, no resize, same corners. */
.cb-blocker .cb-blocker__button:hover { .cb-blocker .cb-blocker__button:hover,
.cb-blocker .cb-blocker__button:focus,
.cb-blocker .cb-blocker__button:active {
background-color: var(--cb-btn-hover-bg); background-color: var(--cb-btn-hover-bg);
color: var(--cb-btn-hover-text); color: var(--cb-btn-hover-text);
border: none; border: none;
outline: none; outline: none;
box-shadow: none; box-shadow: none;
transform: none; transform: none;
border-radius: 3px; /* lock the corners — theme hover must not round them */
} }
/* Keyboard focus keeps a subtle, accessible ring (not shown on mouse hover). */ /* Keyboard focus keeps a subtle, accessible ring (not shown on mouse hover). */

View File

@@ -3,7 +3,7 @@
* Plugin Name: GDPR Content Blocker * Plugin Name: GDPR Content Blocker
* Plugin URI: https://lucas-orth.de/wp-plugins/gdpr-content-blocker/ * Plugin URI: https://lucas-orth.de/wp-plugins/gdpr-content-blocker/
* 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.5.2 * Version: 1.5.3
* 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.5.2' ); define( 'CB_VERSION', '1.5.3' );
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

@@ -498,25 +498,31 @@ class CB_Settings {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
/* active tab indicator */ /* Tabs: no box/border/outline in any state — only a bottom underline
marks the active tab. Override WordPress' default nav-tab borders. */
.cb-admin-wrap .nav-tab-wrapper { .cb-admin-wrap .nav-tab-wrapper {
margin-bottom: 0; margin-bottom: 0;
} }
.cb-admin-wrap .nav-tab-active, .cb-admin-wrap .nav-tab,
.cb-admin-wrap .nav-tab-active:focus, .cb-admin-wrap .nav-tab:hover,
.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,
.cb-admin-wrap .nav-tab:focus-visible, .cb-admin-wrap .nav-tab:focus-visible,
.cb-admin-wrap .nav-tab:active { .cb-admin-wrap .nav-tab:active {
border: none;
border-bottom: 3px solid transparent;
background: transparent;
box-shadow: none; box-shadow: none;
outline: none; outline: none;
margin-bottom: -1px;
}
.cb-admin-wrap .nav-tab-active,
.cb-admin-wrap .nav-tab-active:focus,
.cb-admin-wrap .nav-tab-active:focus-visible,
.cb-admin-wrap .nav-tab-active:hover,
.cb-admin-wrap .nav-tab-active:active {
color: #2043B7;
border-bottom-color: #2043B7;
font-weight: 600;
} }
/* 20px breathing room between tabs and content */ /* 20px breathing room between tabs and content */
.cb-admin-wrap .cb-tab-content { .cb-admin-wrap .cb-tab-content {
@@ -807,16 +813,21 @@ class CB_Settings {
private static function scan_urls(): array { private static function scan_urls(): array {
$urls = [ home_url( '/' ) ]; $urls = [ home_url( '/' ) ];
$posts = get_posts( [ // All published content of every public post type (pages, posts, custom
'post_type' => [ 'page', 'post' ], // post types) — so the scan covers the whole site, not just a few pages.
$types = get_post_types( [ 'public' => true ], 'names' );
unset( $types['attachment'] );
$ids = get_posts( [
'post_type' => array_values( $types ),
'post_status' => 'publish', 'post_status' => 'publish',
'numberposts' => 4, 'numberposts' => 500, // safety cap for very large sites
'orderby' => 'comment_count', // roughly "most visited" 'orderby' => 'date',
'order' => 'DESC', 'order' => 'DESC',
'fields' => 'ids', 'fields' => 'ids',
'no_found_rows' => true, 'no_found_rows' => true,
] ); ] );
foreach ( $posts as $pid ) { foreach ( $ids as $pid ) {
$link = get_permalink( $pid ); $link = get_permalink( $pid );
if ( $link ) { if ( $link ) {
$urls[] = $link; $urls[] = $link;
@@ -838,7 +849,7 @@ class CB_Settings {
} }
$response = wp_remote_post( CB_License::api_url() . '/api/v1/scan', [ $response = wp_remote_post( CB_License::api_url() . '/api/v1/scan', [
'timeout' => 45, 'timeout' => 90,
'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ],
'body' => wp_json_encode( [ 'body' => wp_json_encode( [
'key' => $lic['key'], 'key' => $lic['key'],

View File

@@ -17,9 +17,10 @@ import {
} from './util.js'; } from './util.js';
import { extractResources, analyze, isPublicHost, isPrivateIp } from './scan.js'; import { extractResources, analyze, isPublicHost, isPrivateIp } from './scan.js';
const MAX_SCAN_URLS = 10; const MAX_SCAN_URLS = 60; // pages scanned per request (covers whole small/medium sites)
const MAX_SCAN_BYTES = 2_000_000; const MAX_SCAN_BYTES = 2_000_000;
const SCAN_TIMEOUT_MS = 10_000; const SCAN_TIMEOUT_MS = 8_000;
const SCAN_CONCURRENCY = 12; // fetch pages in parallel so "all pages" stays fast
const DATA_DIR = process.env.DATA_DIR || '/data'; const DATA_DIR = process.env.DATA_DIR || '/data';
const RELEASES_DIR = join(DATA_DIR, 'releases'); const RELEASES_DIR = join(DATA_DIR, 'releases');
@@ -366,43 +367,47 @@ app.post('/api/v1/scan', async (req, res) => {
} }
if (!targets.length) return fail(res, 400, 'no valid target URLs for this domain'); if (!targets.length) return fail(res, 400, 'no valid target URLs for this domain');
const pages = []; // Fetch one page (SSRF-guarded, IP-pinned, no redirects). Returns a page row.
for (const t of targets) { async function scanOne(t) {
try { try {
// SSRF hardening: resolve the host and refuse private/link-local IPs
// (e.g. a public hostname pointed at 169.254.169.254 cloud metadata).
const host = new URL(t).hostname; const host = new URL(t).hostname;
let address, family; let address, family;
try { try {
({ address, family } = await lookup(host)); ({ address, family } = await lookup(host));
} catch { } catch {
pages.push({ url: t, error: 'dns lookup failed', resources: [] }); return { url: t, error: 'dns lookup failed', resources: [] };
continue;
} }
if (isPrivateIp(address)) { if (isPrivateIp(address)) {
pages.push({ url: t, error: 'blocked: resolves to a private address', resources: [] }); return { url: t, error: 'blocked: resolves to a private address', resources: [] };
continue;
} }
// Connect to the validated IP (no re-resolution); do not follow redirects.
const r = await pinnedRequest(t, address, family, { const r = await pinnedRequest(t, address, family, {
maxBytes: MAX_SCAN_BYTES, maxBytes: MAX_SCAN_BYTES,
timeoutMs: SCAN_TIMEOUT_MS, timeoutMs: SCAN_TIMEOUT_MS,
headers: { 'User-Agent': 'ContentBlockerScanner/1.0', Accept: 'text/html' }, headers: { 'User-Agent': 'ContentBlockerScanner/1.0', Accept: 'text/html' },
}); });
if (r.status >= 300 && r.status < 400) { if (r.status >= 300 && r.status < 400) {
pages.push({ url: t, error: `redirect (${r.status}) not followed`, resources: [] }); return { url: t, error: `redirect (${r.status}) not followed`, resources: [] };
continue;
} }
pages.push({ return { url: t, resources: extractResources(r.buffer.toString('utf8').slice(0, MAX_SCAN_BYTES), t) };
url: t,
resources: extractResources(r.buffer.toString('utf8').slice(0, MAX_SCAN_BYTES), t),
});
} catch (e) { } catch (e) {
pages.push({ url: t, error: String(e?.message || e), resources: [] }); return { url: t, error: String(e?.message || e), resources: [] };
} }
} }
// Run with bounded concurrency so scanning all pages stays fast but doesn't
// hammer the target site.
const pages = new Array(targets.length);
let next = 0;
async function worker() {
while (next < targets.length) {
const idx = next++;
pages[idx] = await scanOne(targets[idx]);
}
}
await Promise.all(
Array.from({ length: Math.min(SCAN_CONCURRENCY, targets.length) }, worker)
);
const findings = analyze(pages, domain); const findings = analyze(pages, domain);
return res.json({ return res.json({
ok: true, ok: true,