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