170 lines
9.2 KiB
PHP
170 lines
9.2 KiB
PHP
<?php
|
|
/**
|
|
* Datenschutz-Werkzeuge für Auskunft und Löschung auf Anfrage eines Kunden (DSGVO Art. 15 und 17).
|
|
* Nur für Administratoren und nur, wenn unter Einstellungen → Datenschutz eingeschaltet (gdpr_tools_enabled).
|
|
*
|
|
* - gdpr_customer_export(): alle zum Kunden gespeicherten Daten als Struktur (wird als JSON-Datei geliefert)
|
|
* - gdpr_customer_erase(): anonymisiert Tickets, entfernt Nachrichten und Anhänge des Kunden, Notizen und das Kundenkonto
|
|
*
|
|
* Ein Kunde ist entweder ein Konto (user_id) oder ein Gast, der nur über seine E-Mail-Adresse in Tickets vorkommt.
|
|
*/
|
|
|
|
function gdpr_tools_enabled(): bool
|
|
{
|
|
return get_setting('gdpr_tools_enabled', '0') === '1';
|
|
}
|
|
|
|
/**
|
|
* @param array{user_id?:int,email?:string} $ref
|
|
* @return array{email:string,user:?array,tickets:list<array>}|null
|
|
*/
|
|
function gdpr_resolve_customer(PDO $pdo, array $ref): ?array
|
|
{
|
|
$user = null;
|
|
$email = trim((string) ($ref['email'] ?? ''));
|
|
if (!empty($ref['user_id'])) {
|
|
$stmt = $pdo->prepare('SELECT u.*, o.name AS org_name FROM users u LEFT JOIN organizations o ON o.id = u.organization_id WHERE u.id = ?');
|
|
$stmt->execute([(int) $ref['user_id']]);
|
|
$user = $stmt->fetch() ?: null;
|
|
if (!$user) return null;
|
|
$email = (string) $user['email'];
|
|
}
|
|
if ($email === '') return null;
|
|
$stmt = $pdo->prepare('SELECT * FROM tickets WHERE requester_email = ? OR (user_id IS NOT NULL AND user_id = ?) ORDER BY created_at');
|
|
$stmt->execute([$email, $user ? (int) $user['id'] : 0]);
|
|
$tickets = $stmt->fetchAll();
|
|
if (!$user && !$tickets) return null;
|
|
return ['email' => $email, 'user' => $user, 'tickets' => $tickets];
|
|
}
|
|
|
|
function gdpr_plain(string $html): string
|
|
{
|
|
$t = preg_replace('~<(br|/p|/div|/li|/tr)\b[^>]*>~i', "\n", $html) ?? $html;
|
|
return trim(html_entity_decode(strip_tags($t), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
|
}
|
|
|
|
/** Alle gespeicherten Daten zu einem Kunden. @return array<string,mixed>|null */
|
|
function gdpr_customer_export(PDO $pdo, array $ref): ?array
|
|
{
|
|
$c = gdpr_resolve_customer($pdo, $ref);
|
|
if ($c === null) return null;
|
|
$user = $c['user'];
|
|
$out = [
|
|
'erstellt_am' => date('c'),
|
|
'hinweis' => 'Auskunft nach Art. 15 DSGVO. Interne Notizen der Mitarbeiter sind als intern gekennzeichnet und vor der Weitergabe zu prüfen. Dateiinhalte der Anhänge sind nicht enthalten, nur deren Namen.',
|
|
'kunde' => [
|
|
'name' => $user['name'] ?? ($c['tickets'][0]['requester_name'] ?? ''),
|
|
'email' => $c['email'],
|
|
'telefon' => $user['phone'] ?? null,
|
|
'organisation' => $user['org_name'] ?? null,
|
|
'konto_angelegt_am' => $user['created_at'] ?? null,
|
|
'hat_konto' => $user !== null,
|
|
],
|
|
'tickets' => [],
|
|
'notizen_zum_kunden' => [],
|
|
];
|
|
$dept = $pdo->prepare('SELECT name FROM departments WHERE id = ?');
|
|
$threads = $pdo->prepare('SELECT t.*, s.name AS staff_name FROM ticket_threads t LEFT JOIN staff s ON s.id = t.staff_id WHERE t.ticket_id = ? ORDER BY t.created_at, t.id');
|
|
$atts = $pdo->prepare('SELECT a.original_name, a.filesize, a.created_at FROM ticket_attachments a JOIN ticket_threads t ON t.id = a.thread_id WHERE t.ticket_id = ? ORDER BY a.id');
|
|
$collab = $pdo->prepare('SELECT email, name FROM ticket_collaborators WHERE ticket_id = ?');
|
|
$rating = $pdo->prepare('SELECT rating, comment, created_at FROM ticket_ratings WHERE ticket_id = ?');
|
|
$csat = $pdo->prepare('SELECT rating, comment, answered_at FROM ticket_csat WHERE ticket_id = ? AND answered_at IS NOT NULL');
|
|
foreach ($c['tickets'] as $t) {
|
|
$dept->execute([(int) $t['department_id']]);
|
|
$threads->execute([(int) $t['id']]);
|
|
$atts->execute([(int) $t['id']]);
|
|
$collab->execute([(int) $t['id']]);
|
|
$rating->execute([(int) $t['id']]);
|
|
$csat->execute([(int) $t['id']]);
|
|
$messages = [];
|
|
foreach ($threads->fetchAll() as $m) {
|
|
$messages[] = [
|
|
'zeitpunkt' => $m['created_at'],
|
|
'von' => $m['author_type'] === 'customer' ? 'Kunde' : ($m['author_type'] === 'staff' ? 'Mitarbeiter' : 'System'),
|
|
'mitarbeiter' => $m['author_type'] === 'staff' ? ($m['staff_name'] ?? null) : null,
|
|
'intern' => (bool) $m['is_internal_note'],
|
|
'text' => gdpr_plain((string) $m['message']),
|
|
];
|
|
}
|
|
$out['tickets'][] = [
|
|
'nummer' => $t['ticket_number'], 'betreff' => $t['subject'], 'status' => $t['status'], 'quelle' => $t['source'],
|
|
'abteilung' => (string) ($dept->fetchColumn() ?: ''), 'angelegt_am' => $t['created_at'], 'geschlossen_am' => $t['closed_at'] ?: null,
|
|
'name_im_ticket' => $t['requester_name'], 'email_im_ticket' => $t['requester_email'], 'ip_adresse' => $t['requester_ip'] ?: null,
|
|
'nachrichten' => $messages,
|
|
'anhaenge' => array_map(fn($a) => ['name' => $a['original_name'], 'groesse_bytes' => (int) $a['filesize'], 'hinzugefuegt_am' => $a['created_at']], $atts->fetchAll()),
|
|
'mitleser' => $collab->fetchAll(PDO::FETCH_ASSOC),
|
|
'bewertung' => $rating->fetch(PDO::FETCH_ASSOC) ?: ($csat->fetch(PDO::FETCH_ASSOC) ?: null),
|
|
];
|
|
}
|
|
if ($user) {
|
|
$n = $pdo->prepare('SELECT note, created_at FROM customer_notes WHERE user_id = ? ORDER BY created_at');
|
|
$n->execute([(int) $user['id']]);
|
|
$out['notizen_zum_kunden'] = array_map(fn($r) => ['zeitpunkt' => $r['created_at'], 'text' => $r['note']], $n->fetchAll());
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Löscht die personenbezogenen Daten eines Kunden. Ticketnummer, Betreff, Status und Zeiten bleiben für die Statistik erhalten.
|
|
* Offene Tickets verhindern die Löschung, solange $force nicht gesetzt ist.
|
|
* @return array{error:?string,tickets:int,messages:int,files:int}
|
|
*/
|
|
function gdpr_customer_erase(PDO $pdo, array $ref, bool $force = false): array
|
|
{
|
|
$res = ['error' => null, 'tickets' => 0, 'messages' => 0, 'files' => 0];
|
|
$c = gdpr_resolve_customer($pdo, $ref);
|
|
if ($c === null) { $res['error'] = 'Kunde nicht gefunden.'; return $res; }
|
|
$open = array_filter($c['tickets'], fn($t) => !in_array($t['status'], ['resolved', 'closed'], true));
|
|
if ($open && !$force) {
|
|
$res['error'] = count($open) . ' Ticket(s) sind noch offen. Bitte zuerst schließen oder die Löschung ausdrücklich erzwingen.';
|
|
return $res;
|
|
}
|
|
$user = $c['user'];
|
|
$email = $c['email'];
|
|
$anon = 'anon_' . substr(hash('sha256', $email . 'deskhub-anon'), 0, 16) . '@deleted.local';
|
|
$uploads = realpath((string) (config()['uploads']['path'] ?? '')) ?: '';
|
|
$files = [];
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
foreach ($c['tickets'] as $t) {
|
|
$tid = (int) $t['id'];
|
|
// Dateien der Kunden-Nachrichten merken und Zeilen entfernen
|
|
$stmt = $pdo->prepare("SELECT a.id, a.stored_name FROM ticket_attachments a JOIN ticket_threads th ON th.id = a.thread_id WHERE th.ticket_id = ? AND th.author_type = 'customer'");
|
|
$stmt->execute([$tid]);
|
|
foreach ($stmt->fetchAll() as $a) {
|
|
$files[] = (string) $a['stored_name'];
|
|
$pdo->prepare('DELETE FROM ticket_attachments WHERE id = ?')->execute([(int) $a['id']]);
|
|
}
|
|
$hasHtml = function_exists('ticket_mail_html_ready') && ticket_mail_html_ready($pdo);
|
|
$upd = $pdo->prepare("UPDATE ticket_threads SET message = ?, " . ($hasHtml ? 'message_html = NULL, ' : '') . "user_id = NULL WHERE ticket_id = ? AND author_type = 'customer'");
|
|
$upd->execute(['[Auf Anfrage des Betroffenen gelöscht]', $tid]);
|
|
$res['messages'] += $upd->rowCount();
|
|
$pdo->prepare('DELETE FROM ticket_collaborators WHERE ticket_id = ? AND LOWER(email) = LOWER(?)')->execute([$tid, $email]);
|
|
$pdo->prepare('UPDATE ticket_ratings SET comment = NULL, user_id = NULL WHERE ticket_id = ?')->execute([$tid]);
|
|
$pdo->prepare('UPDATE ticket_csat SET comment = NULL WHERE ticket_id = ?')->execute([$tid]);
|
|
$pdo->prepare("UPDATE tickets SET requester_name = 'Gelöscht', requester_email = ?, requester_ip = '0.0.0.0', user_id = NULL WHERE id = ?")->execute([$anon, $tid]);
|
|
$res['tickets']++;
|
|
}
|
|
if ($user) {
|
|
$uid = (int) $user['id'];
|
|
$pdo->prepare('DELETE FROM customer_notes WHERE user_id = ?')->execute([$uid]);
|
|
$pdo->prepare('UPDATE tickets SET user_id = NULL WHERE user_id = ?')->execute([$uid]);
|
|
$pdo->prepare('UPDATE ticket_threads SET user_id = NULL WHERE user_id = ?')->execute([$uid]);
|
|
$pdo->prepare('DELETE FROM users WHERE id = ?')->execute([$uid]);
|
|
}
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
$res['error'] = 'Löschen fehlgeschlagen: ' . $e->getMessage();
|
|
return $res;
|
|
}
|
|
// Dateien erst nach erfolgreichem Abschluss löschen (nur innerhalb des Upload-Ordners)
|
|
foreach (array_unique($files) as $name) {
|
|
if ($name === '' || $uploads === '' || $name !== basename($name)) continue;
|
|
$path = $uploads . DIRECTORY_SEPARATOR . $name;
|
|
if (is_file($path) && @unlink($path)) $res['files']++;
|
|
}
|
|
return $res;
|
|
}
|