| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/clients/client1/seaside.pacim.de/web/ |
Upload File : |
<?php
/**
* dsgvo-delete.php – DSGVO-Löschung: löscht einen Kundendatensatz UNWIDERRUFLICH und
* vollständig inkl. aller verknüpften Daten (Buchungen, Rechnungen, Positionen, Zahlungen,
* Nachrichten/History, API-Log, Anfragen, Importe, Views, Newsletter, Dokumente).
*
* Ausgangspunkt ist immer GENAU EIN Kundendatensatz. Existieren weitere Kundendatensätze
* mit derselben E-Mail-Adresse, werden diese im info-Schritt angezeigt und können optional
* mitgelöscht (oder einzeln abgewählt) werden.
*
* Aktionen:
* info GET/POST invoice_id | customer_id -> { primary:{…}, duplicates:[…], email }
* delete POST customer_ids(csv), primary_id, confirm="LÖSCHEN" -> { ok, deleted:{…} }
*
* Zugriff: nur Super-Admins (is_super()). Hard delete, KEIN Backup.
*/
require_once( __DIR__ . '/includes/config.inc.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
ob_start();
function gd_json( $a ) { while ( ob_get_level() > 0 ) ob_end_clean(); header( 'Content-Type: application/json; charset=utf-8' ); echo json_encode( $a ); exit(); }
function gd_fail( $m, $code = 400 ) { http_response_code( $code ); gd_json( array( 'ok' => false, 'error' => $m ) ); }
if ( ! ( function_exists( 'is_admin' ) && is_admin() ) ) gd_fail( 'forbidden', 403 );
if ( ! ( function_exists( 'is_super' ) && is_super() ) ) gd_fail( 'Keine Berechtigung (Superrecht erforderlich).', 403 );
$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
$action = $_GET['action'] ?? ( $_POST['action'] ?? 'info' );
/* ---------------------------------------------------------------- Helfer */
function gd_intlist( $arr ) { return array_values( array_unique( array_filter( array_map( 'intval', (array) $arr ), function ( $v ) { return $v > 0; } ) ) ); }
function gd_count( $db, $sql, $params = array() ) { $r = $db->rawQueryOne( $sql, $params ); return $r ? (int) array_values( $r )[0] : 0; }
/** Echte E-Mail? Platzhalter wie "0", "-", leere Werte gelten NICHT als Identitätsschlüssel. */
function gd_valid_email( $e ) {
$e = trim( (string) $e );
return ( $e !== '' && filter_var( $e, FILTER_VALIDATE_EMAIL ) !== false );
}
function gd_display_name( $c ) {
if ( (int) ( $c['customer_gender'] ?? 0 ) === 3 && trim( (string) ( $c['customer_name'] ?? '' ) ) !== '' ) return trim( $c['customer_name'] );
$n = trim( trim( (string) ( $c['customer_firstname'] ?? '' ) ) . ' ' . trim( (string) ( $c['customer_lastname'] ?? '' ) ) );
return $n !== '' ? $n : ( trim( (string) ( $c['customer_name'] ?? '' ) ) ?: '—' );
}
/** Buchungen + Rechnungen eines Kunden ermitteln. */
function gd_scope( $db, $cid ) {
$bookings = $db->rawQuery( 'SELECT booking_id, booking_no, booking_invoice FROM pacim_booking WHERE booking_customer = ?', array( $cid ) );
$invoices = $db->rawQuery( 'SELECT invoice_id, invoice_no FROM pacim_invoice WHERE invoice_customer = ?', array( $cid ) );
$bks = gd_intlist( array_map( function ( $b ) { return $b['booking_id']; }, $bookings ) );
$invs = gd_intlist( array_merge(
array_map( function ( $i ) { return $i['invoice_id']; }, $invoices ),
array_map( function ( $b ) { return $b['booking_invoice']; }, $bookings )
) );
return array( 'bookings' => $bookings, 'invoices' => $invoices, 'bks' => $bks, 'invs' => $invs );
}
/** Vorhandene Dokument-Pfade (Buchungsbestätigungen + Rechnungs-PDFs) eines Kunden. */
function gd_docpaths( $scope ) {
$base = rtrim( PATH_DATAS, '/' );
$paths = array();
foreach ( $scope['bookings'] as $b ) {
$no = trim( (string) $b['booking_no'] );
if ( $no !== '' && preg_match( '#^[A-Za-z0-9._\- ]+$#', $no ) ) $paths[] = $base . '/booking/' . basename( $no ) . '.pdf';
}
foreach ( $scope['invoices'] as $i ) {
$no = trim( (string) $i['invoice_no'] );
if ( $no !== '' && preg_match( '#^[A-Za-z0-9._\- ]+$#', $no ) ) $paths[] = $base . '/invoice/' . basename( $no ) . '.pdf';
}
return array_values( array_unique( $paths ) );
}
/** Vollständige „Fußabdruck"-Zusammenfassung eines Kundendatensatzes (für die Anzeige). */
function gd_footprint( $db, $cid ) {
$cust = $db->rawQueryOne( 'SELECT customer_id, customer_firstname, customer_lastname, customer_name, customer_email, customer_gender FROM pacim_customer WHERE customer_id = ?', array( $cid ) );
if ( ! $cust ) return null;
$sc = gd_scope( $db, $cid );
$bksIn = $sc['bks'] ? implode( ',', $sc['bks'] ) : '0';
$invsIn = $sc['invs'] ? implode( ',', $sc['invs'] ) : '0';
$docs = 0; foreach ( gd_docpaths( $sc ) as $p ) { if ( is_file( $p ) ) $docs++; }
return array(
'customer_id' => (int) $cid,
'name' => gd_display_name( $cust ),
'email' => (string) $cust['customer_email'],
'bookings' => count( $sc['bookings'] ),
'booking_nos' => array_values( array_filter( array_map( function ( $b ) { return trim( (string) $b['booking_no'] ); }, $sc['bookings'] ) ) ),
'invoices' => count( $sc['invoices'] ),
'positions' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_invoice_pos WHERE invoice_id IN ($invsIn) OR booking_id IN ($bksIn)" ),
'payments' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_payments WHERE payment_invoice IN ($invsIn)" ),
'messages' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_messages WHERE message_customer = $cid OR message_invoice IN ($invsIn) OR message_booking IN ($bksIn)" ),
'history' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_api_log WHERE log_invoice IN ($invsIn) OR log_booking IN ($bksIn)" ),
'requests' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_booking_requests WHERE request_invoice IN ($invsIn) OR request_booking IN ($bksIn)" ),
'views' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_view WHERE view_booking IN ($bksIn)" ),
'newsletter' => gd_count( $db, "SELECT COUNT(*) c FROM pacim_newsletter_recipient WHERE recipient_customer = $cid" ) > 0,
'documents' => $docs,
);
}
/* ---------------------------------------------------------------- info */
if ( $action === 'info' ) {
$cid = (int) ( $_POST['customer_id'] ?? ( $_GET['customer_id'] ?? 0 ) );
$invoiceId = (int) ( $_POST['invoice_id'] ?? ( $_GET['invoice_id'] ?? 0 ) );
if ( $cid <= 0 && $invoiceId > 0 ) {
$r = $db->rawQueryOne( 'SELECT invoice_customer FROM pacim_invoice WHERE invoice_id = ?', array( $invoiceId ) );
$cid = $r ? (int) $r['invoice_customer'] : 0;
if ( $cid <= 0 ) { $b = $db->rawQueryOne( 'SELECT booking_customer FROM pacim_booking WHERE booking_invoice = ? ORDER BY booking_id ASC LIMIT 1', array( $invoiceId ) ); $cid = $b ? (int) $b['booking_customer'] : 0; }
}
if ( $cid <= 0 ) gd_fail( 'Kundendatensatz nicht gefunden.' );
$primary = gd_footprint( $db, $cid );
if ( ! $primary ) gd_fail( 'Kundendatensatz nicht gefunden.' );
$rawEmail = trim( (string) $primary['email'] );
// Nur ECHTE E-Mail-Adressen gelten als Identität. Ohne (gültige) E-Mail wird NICHT nach
// weiteren Datensätzen gesucht (Platzhalter "0" o. Ä. würden sonst tausende Fremd-Kunden ziehen).
$email = gd_valid_email( $rawEmail ) ? $rawEmail : '';
$primary['email'] = $email;
$dups = array(); $dupCapped = false;
if ( $email !== '' ) {
$primCust = $db->rawQueryOne( 'SELECT customer_lastname FROM pacim_customer WHERE customer_id = ?', array( $cid ) );
$lastname = trim( (string) ( $primCust['customer_lastname'] ?? '' ) );
if ( $lastname !== '' ) {
// Plausible Duplikate DESSELBEN Kunden: gleiche echte E-Mail UND gleicher Nachname
// (verhindert das Mitziehen fremder Personen bei geteilten Agentur-/Sammel-Adressen).
$cap = 200;
$rows = $db->rawQuery(
'SELECT customer_id, customer_firstname, customer_lastname, customer_name, customer_gender
FROM pacim_customer
WHERE customer_email = ? AND customer_id <> ? AND TRIM(customer_lastname) = ?
ORDER BY customer_id DESC LIMIT ' . ( $cap + 1 ),
array( $email, $cid, $lastname ) );
if ( count( $rows ) > $cap ) { $dupCapped = true; $rows = array_slice( $rows, 0, $cap ); }
$dupIds = array_map( function ( $r ) { return (int) $r['customer_id']; }, $rows );
$bk = array();
if ( $dupIds ) {
$in = implode( ',', $dupIds );
foreach ( (array) $db->rawQuery( "SELECT booking_customer, COUNT(*) c FROM pacim_booking WHERE booking_customer IN ($in) GROUP BY booking_customer" ) as $b )
$bk[ (int) $b['booking_customer'] ] = (int) $b['c'];
}
foreach ( $rows as $rr ) {
$dups[] = array(
'customer_id' => (int) $rr['customer_id'],
'name' => gd_display_name( $rr ),
'email' => $email,
'bookings' => isset( $bk[ (int) $rr['customer_id'] ] ) ? $bk[ (int) $rr['customer_id'] ] : 0,
);
}
}
}
gd_json( array( 'ok' => true, 'primary' => $primary, 'duplicates' => $dups, 'email' => $email, 'capped' => $dupCapped ) );
}
/* ---------------------------------------------------------------- delete */
if ( $action === 'delete' ) {
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) gd_fail( 'method' );
if ( trim( (string) ( $_POST['confirm'] ?? '' ) ) !== 'LÖSCHEN' ) gd_fail( 'Bitte zur Bestätigung exakt LÖSCHEN eingeben.' );
$primaryId = (int) ( $_POST['primary_id'] ?? 0 );
$ids = gd_intlist( explode( ',', (string) ( $_POST['customer_ids'] ?? '' ) ) );
if ( $primaryId <= 0 || ! in_array( $primaryId, $ids, true ) ) gd_fail( 'Ungültige Auswahl.' );
if ( count( $ids ) > 300 ) gd_fail( 'Zu viele Datensätze auf einmal (max. 300). Bitte in kleineren Schritten löschen.' );
// Nur tatsächlich existierende Kunden
$valid = array();
foreach ( $ids as $cid ) { if ( $db->rawQueryOne( 'SELECT customer_id FROM pacim_customer WHERE customer_id = ?', array( $cid ) ) ) $valid[] = $cid; }
if ( ! $valid ) gd_fail( 'Keine gültigen Kundendatensätze.' );
// Audit-Infos VOR dem Löschen sammeln + AIDA-invoice_no für die Sperrliste merken
// (sonst legt der AIDA-Import die gelöschte Buchung beim nächsten Lauf wieder an).
$auditParts = array(); $allDocs = array(); $aidaBlock = array();
foreach ( $valid as $cid ) {
$c = $db->rawQueryOne( 'SELECT customer_id, customer_firstname, customer_lastname, customer_name, customer_email, customer_gender FROM pacim_customer WHERE customer_id = ?', array( $cid ) );
$auditParts[] = '#' . $cid . ' ' . gd_display_name( $c ) . ' <' . (string) $c['customer_email'] . '>';
$sc0 = gd_scope( $db, $cid );
$allDocs = array_merge( $allDocs, gd_docpaths( $sc0 ) );
$invs0 = $sc0['invs'] ? implode( ',', $sc0['invs'] ) : '0';
$nos = $db->rawQuery( "SELECT invoice_no FROM pacim_invoice WHERE invoice_id IN ($invs0) AND invoice_no LIKE 'AIDA-%'" );
foreach ( (array) $nos as $n ) { $no = trim( (string) $n['invoice_no'] ); if ( $no !== '' ) $aidaBlock[ $no ] = (string) $c['customer_email']; }
}
// Sperrlisten-Tabelle VOR der Transaktion sicherstellen (CREATE TABLE würde sonst implizit committen).
if ( function_exists( 'pacim_dsgvo_block_ensure' ) ) pacim_dsgvo_block_ensure( $db );
$db->startTransaction();
try {
foreach ( $valid as $cid ) {
$sc = gd_scope( $db, $cid );
// WICHTIG: leere ID-Listen als NULL (nicht '0')! "x IN (0)" würde sonst alle Zeilen mit
// Wert 0 treffen (z. B. Wunschzeitraum-Anfragen mit request_invoice=0) und fremde Daten löschen.
// "x IN (NULL)" trifft dagegen nie etwas.
$bksIn = $sc['bks'] ? implode( ',', $sc['bks'] ) : 'NULL';
$invsIn = $sc['invs'] ? implode( ',', $sc['invs'] ) : 'NULL';
$db->rawQuery( "DELETE FROM pacim_invoice_pos WHERE invoice_id IN ($invsIn) OR booking_id IN ($bksIn)" );
$db->rawQuery( "DELETE FROM pacim_payments WHERE payment_invoice IN ($invsIn)" );
$db->rawQuery( "DELETE FROM pacim_messages WHERE message_customer = $cid OR message_invoice IN ($invsIn) OR message_booking IN ($bksIn)" );
$db->rawQuery( "DELETE FROM pacim_api_log WHERE log_invoice IN ($invsIn) OR log_booking IN ($bksIn)" );
$db->rawQuery( "DELETE FROM pacim_booking_requests WHERE request_invoice IN ($invsIn) OR request_booking IN ($bksIn)" );
$db->rawQuery( "DELETE FROM pacim_import WHERE import_customer = $cid OR import_invoice IN ($invsIn) OR import_booking IN ($bksIn)" );
$db->rawQuery( "DELETE FROM pacim_view WHERE view_booking IN ($bksIn)" );
$db->rawQuery( "DELETE FROM pacim_mail_suppression WHERE customer_id = $cid" );
$db->rawQuery( "DELETE FROM pacim_newsletter_recipient WHERE recipient_customer = $cid" );
$db->rawQuery( "DELETE FROM pacim_booking WHERE booking_customer = $cid" );
$db->rawQuery( "DELETE FROM pacim_invoice WHERE invoice_customer = $cid OR invoice_id IN ($invsIn)" );
$db->rawQuery( "DELETE FROM pacim_customer WHERE customer_id = $cid" );
}
$db->commit();
} catch ( Exception $e ) {
$db->rollback();
gd_fail( 'Datenbankfehler – nichts wurde gelöscht.', 500 );
}
// Gelöschte AIDA-Buchungen sperren, damit der Import sie nicht erneut anlegt.
if ( $aidaBlock && function_exists( 'pacim_dsgvo_block_add' ) ) {
$byName = trim( (string) ( $_SESSION['user_name'] ?? 'Administrator' ) );
foreach ( $aidaBlock as $no => $em ) pacim_dsgvo_block_add( $db, $no, $em, $byName );
}
// Dokumente endgültig entfernen (erst nach erfolgreichem Commit; Pfad-Whitelist gegen Traversal)
$baseReal = realpath( rtrim( PATH_DATAS, '/' ) );
$delDocs = 0;
foreach ( array_unique( $allDocs ) as $p ) {
if ( ! is_file( $p ) ) continue;
$rp = realpath( $p );
if ( $rp && $baseReal && strpos( $rp, $baseReal ) === 0 && @unlink( $rp ) ) $delDocs++;
}
// Audit (darf das Ergebnis nie kippen)
try {
if ( class_exists( 'apilog' ) ) {
apilog::record( array(
'key_name' => trim( (string) ( $_SESSION['user_name'] ?? 'Administrator' ) ),
'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'method' => 'POST',
'resource' => 'customer', 'action' => 'dsgvo-delete', 'kind' => 'write',
'booking' => 0, 'invoice' => 0,
'target' => 'DSGVO-Löschung (' . count( $valid ) . ' Datensatz/Datensätze): ' . implode( '; ', $auditParts ),
'changes' => null,
) );
}
} catch ( Exception $e ) {}
gd_json( array( 'ok' => true, 'deleted' => array( 'customers' => count( $valid ), 'documents' => $delDocs ) ) );
}
gd_fail( 'Unbekannte Aktion.' );