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:
@@ -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). */
|
||||
|
||||
@@ -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__ ) );
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user