diff --git a/gdpr-content-blocker/assets/frontend.css b/gdpr-content-blocker/assets/frontend.css index 556cb2f..e12a10e 100644 --- a/gdpr-content-blocker/assets/frontend.css +++ b/gdpr-content-blocker/assets/frontend.css @@ -125,14 +125,17 @@ letter-spacing: 0.01em; } -/* Hover: only the colour changes — no frame, no resize. */ -.cb-blocker .cb-blocker__button:hover { +/* Hover: only the colour changes — no frame, no resize, same corners. */ +.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); color: var(--cb-btn-hover-text); border: none; outline: none; box-shadow: 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). */ diff --git a/gdpr-content-blocker/gdpr-content-blocker.php b/gdpr-content-blocker/gdpr-content-blocker.php index b509d5e..8d7dcd6 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/wp-plugins/gdpr-content-blocker/ * 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 URI: https://lucas-orth.de * Text Domain: gdpr-content-blocker @@ -15,7 +15,7 @@ defined( 'ABSPATH' ) || exit; -define( 'CB_VERSION', '1.5.2' ); +define( 'CB_VERSION', '1.5.3' ); 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-settings.php b/gdpr-content-blocker/includes/class-settings.php index ba31145..9597688 100644 --- a/gdpr-content-blocker/includes/class-settings.php +++ b/gdpr-content-blocker/includes/class-settings.php @@ -498,25 +498,31 @@ class CB_Settings { width: 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 { margin-bottom: 0; } - .cb-admin-wrap .nav-tab-active, - .cb-admin-wrap .nav-tab-active:focus, - .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, + .cb-admin-wrap .nav-tab:hover, .cb-admin-wrap .nav-tab:focus, .cb-admin-wrap .nav-tab:focus-visible, .cb-admin-wrap .nav-tab:active { + border: none; + border-bottom: 3px solid transparent; + background: transparent; box-shadow: 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 */ .cb-admin-wrap .cb-tab-content { @@ -807,16 +813,21 @@ class CB_Settings { private static function scan_urls(): array { $urls = [ home_url( '/' ) ]; - $posts = get_posts( [ - 'post_type' => [ 'page', 'post' ], + // All published content of every public post type (pages, posts, custom + // 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', - 'numberposts' => 4, - 'orderby' => 'comment_count', // roughly "most visited" + 'numberposts' => 500, // safety cap for very large sites + 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', 'no_found_rows' => true, ] ); - foreach ( $posts as $pid ) { + foreach ( $ids as $pid ) { $link = get_permalink( $pid ); if ( $link ) { $urls[] = $link; @@ -838,7 +849,7 @@ class CB_Settings { } $response = wp_remote_post( CB_License::api_url() . '/api/v1/scan', [ - 'timeout' => 45, + 'timeout' => 90, 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'body' => wp_json_encode( [ 'key' => $lic['key'], diff --git a/license-backend/src/server.js b/license-backend/src/server.js index 07d6869..c7c3557 100644 --- a/license-backend/src/server.js +++ b/license-backend/src/server.js @@ -17,9 +17,10 @@ import { } from './util.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 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 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'); - const pages = []; - for (const t of targets) { + // Fetch one page (SSRF-guarded, IP-pinned, no redirects). Returns a page row. + async function scanOne(t) { 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; let address, family; try { ({ address, family } = await lookup(host)); } catch { - pages.push({ url: t, error: 'dns lookup failed', resources: [] }); - continue; + return { url: t, error: 'dns lookup failed', resources: [] }; } if (isPrivateIp(address)) { - pages.push({ url: t, error: 'blocked: resolves to a private address', resources: [] }); - continue; + return { url: t, error: 'blocked: resolves to a private address', resources: [] }; } - - // Connect to the validated IP (no re-resolution); do not follow redirects. const r = await pinnedRequest(t, address, family, { maxBytes: MAX_SCAN_BYTES, timeoutMs: SCAN_TIMEOUT_MS, headers: { 'User-Agent': 'ContentBlockerScanner/1.0', Accept: 'text/html' }, }); if (r.status >= 300 && r.status < 400) { - pages.push({ url: t, error: `redirect (${r.status}) not followed`, resources: [] }); - continue; + return { url: t, error: `redirect (${r.status}) not followed`, resources: [] }; } - pages.push({ - url: t, - resources: extractResources(r.buffer.toString('utf8').slice(0, MAX_SCAN_BYTES), t), - }); + return { url: t, resources: extractResources(r.buffer.toString('utf8').slice(0, MAX_SCAN_BYTES), t) }; } 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); return res.json({ ok: true,