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 );