chore: monorepo - plugin, backend und hilfsdaten in einem repo

- Eltern-Ordner ist jetzt EIN Git-Repo (statt getrennter Repos).
- root .gitignore haelt Secrets (.env), node_modules, DB und Build-Artefakte raus.
- release.ps1: manueller Release (ZIP bauen + ans Backend laden).
- root README mit Struktur und Release-Ablauf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-06-07 14:41:38 +02:00
commit ecb5e1bd22
37 changed files with 4390 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
/**
* Content Blocker admin.js
* Color-picker init + service repeater (add / remove rows).
*/
( function ( $ ) {
'use strict';
/* ── Color pickers ── */
function initColorPickers( ctx ) {
$( '.cb-color-picker', ctx ).wpColorPicker();
}
/* ── Live header update: "interner-name — Anbietername" ── */
function syncTitle( box ) {
const id = ( $( '.cb-input-id', box ).val() || '' ).trim();
const name = ( $( '.cb-input-name', box ).val() || '' ).trim();
const head = ( id || ( cbAdmin.newServiceLbl || 'neu' ) ) + ( name ? ' — ' + name : '' );
$( '.cb-service-title', box ).first().text( head );
}
function bindNameSync( box ) {
$( '.cb-input-name, .cb-input-id', box ).on( 'input', function () {
syncTitle( box );
} );
}
/* ── Expand / collapse a service box ── */
function setExpanded( box, open ) {
$( '.cb-service-grid', box ).toggle( open );
const $btn = $( '.cb-service-toggle', box );
$btn.attr( 'aria-expanded', open ? 'true' : 'false' ).text( open ? '▾' : '▸' );
}
function bindToggle( box ) {
$( '.cb-service-toggle', box ).on( 'click', function () {
const open = $( this ).attr( 'aria-expanded' ) !== 'true';
setExpanded( box, open );
} );
// Clicking the title also expands/collapses.
$( '.cb-service-title', box ).on( 'click', function () {
const open = $( '.cb-service-toggle', box ).attr( 'aria-expanded' ) !== 'true';
setExpanded( box, open );
} );
}
/* ── Re-index a row's field names after a remove ── */
function reindexRows() {
$( '#cb-services-list .cb-service-box' ).each( function ( i ) {
$( this ).attr( 'data-cb-index', i );
$( this ).find( '[name]' ).each( function () {
const n = $( this ).attr( 'name' );
$( this ).attr( 'name', n.replace( /cb_services\[\d+\]/, 'cb_services[' + i + ']' ) );
} );
} );
}
/* ── Add a new (optionally prefilled) service row ── */
function addServiceRow( preset ) {
const index = $( '#cb-services-list .cb-service-box' ).length;
const template = $( '#cb-service-template' ).html();
const newHtml = template.replace( /__INDEX__/g, index );
const $box = $( newHtml );
$( '#cb-services-list' ).append( $box );
bindNameSync( $box );
bindToggle( $box );
$box.find( '.cb-remove-service' ).on( 'click', handleRemove );
if ( preset ) {
fillPreset( $box, preset );
}
setExpanded( $box, true ); // new rows start expanded for editing
syncTitle( $box );
return $box;
}
/* ── Fill a row from a preset object ── */
function fillPreset( $box, p ) {
const setText = function ( field, val ) {
$box.find( '[name$="[' + field + ']"]' ).val( val != null ? val : '' );
};
const setBool = function ( field, val ) {
$box.find( '[name$="[' + field + ']"]' ).prop( 'checked', !! val );
};
setText( 'id', p.id );
setText( 'name', p.name );
setText( 'match_pattern', p.match_pattern );
setText( 'recipient', p.recipient );
setText( 'privacy_url', p.privacy_url );
setText( 'purpose', p.purpose );
setBool( 'third_country', p.third_country );
setBool( 'sets_cookie', p.sets_cookie );
setBool( 'loads_script', p.loads_script );
$box.find( '.cb-service-title' ).text( p.name || cbAdmin.newServiceLbl );
}
/* ── Add empty service ── */
$( '#cb-add-service' ).on( 'click', function () {
addServiceRow( null );
} );
/* ── Insert preset ── */
$( '#cb-preset-select' ).on( 'change', function () {
const key = $( this ).val();
if ( ! key || ! cbAdmin.presets || ! cbAdmin.presets[ key ] ) {
return;
}
addServiceRow( cbAdmin.presets[ key ] );
$( this ).val( '' ); // reset so the same preset can be added again
} );
/* ── Remove service ── */
function handleRemove() {
if ( ! window.confirm( cbAdmin.confirmRemove ) ) {
return;
}
$( this ).closest( '.cb-service-box' ).remove();
reindexRows();
}
/* ── Tab switching ── */
$( '[data-cb-tab]' ).on( 'click', function ( e ) {
e.preventDefault();
activateTab( $( this ).data( 'cb-tab' ) );
} );
/* ── Website scan ── */
$( '#cb-scan-btn' ).on( 'click', function () {
const $btn = $( this ).prop( 'disabled', true );
const i18n = cbAdmin.i18n || {};
$( '#cb-scan-status' ).text( i18n.scanning || 'Scanning…' );
$( '#cb-scan-results' ).empty();
$.post( cbAdmin.ajaxUrl, { action: 'cb_scan', nonce: cbAdmin.scanNonce } )
.done( function ( resp ) {
if ( ! resp || ! resp.success ) {
const msg = resp && resp.data && resp.data.message ? resp.data.message : 'Error';
$( '#cb-scan-status' ).text( ( i18n.scanError || 'Scan failed:' ) + ' ' + msg );
return;
}
$( '#cb-scan-status' ).text( '' );
renderScan( resp.data );
} )
.fail( function ( xhr ) {
$( '#cb-scan-status' ).text( ( i18n.scanError || 'Scan failed:' ) + ' ' + xhr.status );
} )
.always( function () {
$btn.prop( 'disabled', false );
} );
} );
function renderScan( data ) {
const i18n = cbAdmin.i18n || {};
const findings = ( data && data.findings ) || [];
const $out = $( '#cb-scan-results' ).empty();
// Scanned pages summary
if ( data && data.scanned && data.scanned.length ) {
const $p = $( '<p class="description"></p>' ).text( ( i18n.scannedPages || 'Scanned:' ) + ' ' );
data.scanned.forEach( function ( s, i ) {
if ( i > 0 ) $p.append( ', ' );
$p.append( $( '<code></code>' ).text( s.url + ( s.error ? ' (!)' : '' ) ) );
} );
$out.append( $p );
}
if ( ! findings.length ) {
$out.append( $( '<p></p>' ).text( i18n.noFindings || 'No external resources found.' ) );
return;
}
const $table = $( '<table class="widefat striped"></table>' );
const $head = $( '<tr></tr>' );
[ i18n.host || 'Host', i18n.type || 'Type', i18n.count || 'Count', i18n.foundOn || 'Found on', i18n.example || 'Example', i18n.status || 'Status', i18n.action || 'Action' ]
.forEach( function ( h ) { $head.append( $( '<th></th>' ).text( h ) ); } );
$table.append( $( '<thead></thead>' ).append( $head ) );
const $body = $( '<tbody></tbody>' );
findings.forEach( function ( f ) {
const $tr = $( '<tr></tr>' );
// Host (+ party badge)
const $host = $( '<td></td>' );
$host.append( $( '<strong></strong>' ).text( f.host ) );
$host.append( document.createElement( 'br' ) );
$host.append( $( '<span></span>' )
.css( { fontSize: '11px', color: f.third_party ? '#b32d2e' : '#1a7f37' } )
.text( f.third_party ? ( i18n.thirdParty || 'third-party' ) : ( i18n.firstParty || 'first-party' ) ) );
$tr.append( $host );
// Types
$tr.append( $( '<td></td>' ).text( ( f.types || [] ).join( ', ' ) ) );
// Count
$tr.append( $( '<td></td>' ).text( f.count ) );
// Found on which pages
const $pages = $( '<td></td>' ).css( { fontSize: '11px' } );
( f.pages || [] ).forEach( function ( pageUrl, i ) {
if ( i > 0 ) $pages.append( document.createElement( 'br' ) );
let label = pageUrl;
try { label = new URL( pageUrl ).pathname || pageUrl; } catch ( e ) {}
$pages.append( $( '<a></a>' )
.attr( { href: pageUrl, target: '_blank', rel: 'noopener noreferrer', title: pageUrl } )
.text( label ) );
} );
$tr.append( $pages );
// Example URL
const $ex = $( '<td></td>' );
const sample = ( f.sample_urls || [] )[ 0 ] || '';
$ex.append( $( '<code></code>' ).css( { fontSize: '11px', wordBreak: 'break-all' } ).text( sample ) );
$tr.append( $ex );
// Status
const $st = $( '<td></td>' );
if ( f.covered ) {
$st.append( $( '<span></span>' ).css( { color: '#1a7f37', fontWeight: '600' } ).text( '✓ ' + ( i18n.covered || 'covered' ) ) );
}
$tr.append( $st );
// Action: take over as a new service (only useful for uncovered third parties)
const $act = $( '<td></td>' );
if ( f.third_party && ! f.covered ) {
$( '<button type="button" class="button button-small"></button>' )
.text( i18n.addService || 'Add as service' )
.on( 'click', function () {
activateTab( 'services' );
const $box = addServiceRow( { name: f.host, match_pattern: f.suggested_pattern || f.host, third_country: true } );
if ( $box && $box.length ) {
$box[ 0 ].scrollIntoView( { behavior: 'smooth', block: 'center' } );
$box.find( '.cb-input-id' ).trigger( 'focus' );
}
} )
.appendTo( $act );
}
$tr.append( $act );
$body.append( $tr );
} );
$table.append( $body );
$out.append( $table );
}
/* ── Activate a tab by key ── */
function activateTab( tab ) {
$( '.cb-tab-content' ).hide();
$( '#cb-tab-' + tab ).show();
$( '[data-cb-tab]' ).removeClass( 'nav-tab-active' );
$( '[data-cb-tab="' + tab + '"]' ).addClass( 'nav-tab-active' );
}
/* ── Bootstrap ── */
$( document ).ready( function () {
initColorPickers( document );
$( '#cb-services-list .cb-service-box' ).each( function () {
bindNameSync( this );
bindToggle( this );
$( '.cb-remove-service', this ).on( 'click', handleRemove );
} );
// Honor ?cb_tab=… so license redirects land on the right tab.
const params = new URLSearchParams( window.location.search );
const tab = params.get( 'cb_tab' );
if ( tab && $( '#cb-tab-' + tab ).length ) {
activateTab( tab );
}
} );
// Expose strings from wp_localize_script if needed.
window.cbAdmin = window.cbAdmin || { confirmRemove: 'Dienst wirklich entfernen?' };
} )( jQuery );

View File

@@ -0,0 +1,198 @@
/**
* Content Blocker frontend.css
* Design: minimalist black/white with accent #2043B7
* Colors come from CSS custom properties set inline by PHP.
*
* Note on specificity: every visible rule is scoped under `.cb-blocker`
* (specificity 0,2,0) so theme button/link styles (typically 0,1,1) cannot
* override the plugin. Your Custom-CSS is injected AFTER this file, so it can
* still override everything — use the same `.cb-blocker …` prefix to be safe.
*/
:root {
--cb-text: #ffffff;
--cb-bg: #111111;
--cb-btn-bg: #2043B7;
--cb-btn-text: #ffffff;
--cb-btn-hover-bg: #1a369a;
--cb-btn-hover-text: #ffffff;
}
/* ── Wrapper / placeholder ── */
.cb-blocker {
display: flex;
align-items: flex-start; /* pin content to the top */
justify-content: center;
min-height: 180px; /* fallback only; real height is set inline */
width: 100%;
background-color: var(--cb-bg);
color: var(--cb-text);
box-sizing: border-box;
padding: 24px 8px 20px; /* min 20px top/bottom, 8px sides */
font-family: inherit;
border-radius: 4px;
overflow: auto; /* if text is taller than the embed, scroll, don't clip */
}
/* ── Inner content ── */
.cb-blocker .cb-blocker__inner {
width: 100%;
max-width: 520px;
text-align: center;
}
/* ── Text elements ── */
.cb-blocker .cb-blocker__text,
.cb-blocker .cb-blocker__recipient,
.cb-blocker .cb-blocker__purpose {
margin: 0 0 0.65em;
font-size: 0.875rem;
line-height: 1.6;
color: var(--cb-text);
}
.cb-blocker .cb-blocker__text {
font-size: 0.9375rem;
}
.cb-blocker .cb-blocker__third-country {
display: inline-block;
margin-top: 0.25em;
font-size: 0.8rem;
color: #f0b429; /* amber — universal caution, brand-neutral */
font-weight: 600;
}
/* ── Privacy link ── */
.cb-blocker .cb-blocker__privacy-link {
display: inline-block;
margin-bottom: 0.75em;
font-size: 0.8125rem;
line-height: 1.5;
color: var(--cb-btn-bg);
text-decoration: underline;
text-underline-offset: 3px;
transform: none; /* neutralise theme hover scaling */
}
.cb-blocker .cb-blocker__privacy-link:hover,
.cb-blocker .cb-blocker__privacy-link:focus {
color: var(--cb-btn-hover-bg);
transform: none; /* link must not grow on hover */
font-size: 0.8125rem;
}
/* ── Button ── */
.cb-blocker .cb-blocker__button {
display: inline-block;
margin-top: 1em;
padding: 0.6em 1.4em;
background-color: var(--cb-btn-bg);
color: var(--cb-btn-text);
border: none; /* never a border/frame */
outline: none;
box-shadow: none;
transform: none; /* no size change, even with theme hover styles */
border-radius: 3px;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.18s ease, color 0.18s ease;
letter-spacing: 0.01em;
}
/* Hover: only the colour changes — no frame, no resize. */
.cb-blocker .cb-blocker__button:hover {
background-color: var(--cb-btn-hover-bg);
color: var(--cb-btn-hover-text);
border: none;
outline: none;
box-shadow: none;
transform: none;
}
/* Keyboard focus keeps a subtle, accessible ring (not shown on mouse hover). */
.cb-blocker .cb-blocker__button:focus-visible {
outline: 2px solid var(--cb-btn-hover-bg);
outline-offset: 2px;
}
/* ── Revoke button ── */
.cb-revoke-btn {
display: inline-block;
padding: 0.5em 1.2em;
background-color: #111111;
color: #ffffff;
border: 2px solid #111111;
border-radius: 3px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.18s ease, color 0.18s ease;
}
.cb-revoke-btn:hover,
.cb-revoke-btn:focus-visible {
background-color: #ffffff;
color: #111111;
}
/* ── Revoke as a visible text link (default) ── */
.cb-revoke-link {
display: inline-block;
color: var(--cb-btn-bg);
font-weight: 600;
text-decoration: underline;
text-underline-offset: 3px;
cursor: pointer;
}
.cb-revoke-link:hover,
.cb-revoke-link:focus-visible {
color: var(--cb-btn-hover-bg);
text-decoration: underline;
}
.cb-revoke-note {
display: block;
margin-top: 6px;
font-size: 0.8125rem;
opacity: 0.8;
}
/* ── Services overview (for the privacy policy) ── */
.cb-services-list {
list-style: none;
margin: 1em 0;
padding: 0;
}
.cb-services-list .cb-service-entry {
margin: 0 0 1.1em;
padding: 0 0 1.1em;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.cb-services-list .cb-service-entry:last-child {
border-bottom: none;
}
.cb-services-list .cb-service-entry__name {
font-weight: 700;
font-size: 1.02em;
}
.cb-services-list .cb-service-entry dl {
margin: 0.4em 0 0;
display: grid;
grid-template-columns: max-content 1fr;
gap: 2px 14px;
}
.cb-services-list .cb-service-entry dt {
font-weight: 600;
}
.cb-services-list .cb-service-entry dd {
margin: 0;
}

View File

@@ -0,0 +1,115 @@
/**
* Content Blocker frontend.js
* Click-to-load: the real iframe is CREATED only after the user actively
* consents per service. Consent is stored in localStorage per service id.
*/
( function () {
'use strict';
const STORAGE_PREFIX = 'cb_consent_';
/* ───────────────────────── consent storage ───────────────────────── */
function hasConsent( serviceId ) {
try {
return localStorage.getItem( STORAGE_PREFIX + serviceId ) === '1';
} catch ( e ) {
return false;
}
}
function grantConsent( serviceId ) {
try {
localStorage.setItem( STORAGE_PREFIX + serviceId, '1' );
} catch ( e ) {
// localStorage unavailable; allow the load for this session only.
}
}
/* ───────────────────────── iframe loading ────────────────────────── */
/** Replace a .cb-blocker element with the real iframe. src comes from data-src. */
function loadContent( blockerEl ) {
const src = blockerEl.dataset.src;
if ( ! src ) {
blockerEl.remove();
return;
}
const iframe = document.createElement( 'iframe' );
if ( blockerEl.dataset.width ) iframe.width = blockerEl.dataset.width;
if ( blockerEl.dataset.height ) iframe.height = blockerEl.dataset.height;
iframe.setAttribute( 'loading', 'lazy' );
iframe.setAttribute( 'allowfullscreen', '' );
iframe.setAttribute( 'referrerpolicy', 'no-referrer-when-downgrade' );
// Set src last — this is the moment the network request is made.
iframe.src = src;
blockerEl.parentNode.replaceChild( iframe, blockerEl );
}
function loadPreConsented() {
document.querySelectorAll( '.cb-blocker[data-cb-id]' ).forEach( function ( el ) {
const id = el.dataset.cbId;
if ( id && hasConsent( id ) ) {
loadContent( el );
}
} );
}
/* ───────────────────────── consent buttons ───────────────────────── */
function attachButtons() {
document.addEventListener( 'click', function ( e ) {
const btn = e.target.closest( '.cb-blocker__button' );
if ( ! btn ) return;
const serviceId = btn.dataset.cbId;
if ( ! serviceId ) return;
grantConsent( serviceId );
const wrapper = btn.closest( '.cb-blocker' );
if ( wrapper ) {
loadContent( wrapper );
}
} );
}
/* ───────────────────────── revoke (Art. 7 (3)) ───────────────────── */
window.cbRevokeAll = function () {
try {
const keysToRemove = [];
for ( let i = 0; i < localStorage.length; i++ ) {
const key = localStorage.key( i );
if ( key && key.indexOf( STORAGE_PREFIX ) === 0 ) {
keysToRemove.push( key );
}
}
keysToRemove.forEach( function ( k ) {
localStorage.removeItem( k );
} );
} catch ( e ) {
// Silently ignore if localStorage is unavailable.
}
window.location.reload();
};
/* ───────────────────────── bootstrap ─────────────────────────────── */
function init() {
loadPreConsented();
attachButtons();
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
} )();