Files
deskhub/Plugins/plugins/datenschutz/pages/datenschutz.php
T
2026-10-24 20:15:15 +00:00

180 lines
14 KiB
PHP

<?php
// Seite des Datenschutz-Plugins. Verfügbar: $staff, $pdo. Nur für Administratoren.
if (($staff['role'] ?? '') !== 'admin') { echo '<h1>Datenschutz</h1><div class="flash error">Diese Seite ist nur für Administratoren zugänglich.</div>'; return; }
$message = null;
$error = null;
$found = null;
$searched = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'settings') {
$unit = isset(datenschutz_units()[$_POST['unit'] ?? '']) ? $_POST['unit'] : 'days';
$picked = array_values(array_intersect(array_keys(datenschutz_field_labels()), (array) ($_POST['fields'] ?? [])));
if (!$picked) {
$error = 'Bitte mindestens eine Datenart zum Anonymisieren wählen.';
} else {
plugin_set_setting(DATENSCHUTZ, 'amount', (string) max(1, (int) ($_POST['amount'] ?? 30)));
plugin_set_setting(DATENSCHUTZ, 'unit', $unit);
plugin_set_setting(DATENSCHUTZ, 'basis', ($_POST['basis'] ?? '') === 'created' ? 'created' : 'closed');
plugin_set_setting(DATENSCHUTZ, 'include_open', !empty($_POST['include_open']) ? '1' : '0');
plugin_set_setting(DATENSCHUTZ, 'fields', implode(',', $picked));
$message = 'Einstellungen gespeichert.';
}
} elseif ($action === 'enable') {
if (empty($_POST['confirm'])) $error = 'Bitte bestätigen, dass Sie die unwiderrufliche Anonymisierung einschalten wollen.';
else { plugin_set_setting(DATENSCHUTZ, 'enabled', '1'); datenschutz_log($pdo, 'manual', 0, 0, 'Automatische Anonymisierung eingeschaltet', (int) $staff['id']); $message = 'Die automatische Anonymisierung ist eingeschaltet. Sie läuft ab jetzt im Hintergrund.'; }
} elseif ($action === 'disable') {
plugin_set_setting(DATENSCHUTZ, 'enabled', '0');
datenschutz_log($pdo, 'manual', 0, 0, 'Automatische Anonymisierung ausgeschaltet', (int) $staff['id']);
$message = 'Die automatische Anonymisierung ist ausgeschaltet.';
} elseif ($action === 'run') {
$n = datenschutz_run($pdo, 500, 'manual', (int) $staff['id']);
$message = $n . ' Ticket(s) anonymisiert.';
} elseif ($action === 'core_off') {
set_setting('retention_enabled', '0');
audit_log($staff['id'], 'Automatische Datenlöschung des Kerns ausgeschaltet (Plugin Datenschutz übernimmt)');
$message = 'Die automatische Datenlöschung des Kerns ist ausgeschaltet.';
} elseif ($action === 'find') {
$searched = trim((string) ($_POST['email'] ?? ''));
$found = datenschutz_find_customer($pdo, $searched);
if (!$found) $error = 'Unter dieser Adresse gibt es keinen registrierten Kunden.';
} elseif ($action === 'delete_customer') {
if (empty($_POST['confirm'])) $error = 'Bitte die Löschung bestätigen.';
else {
$r = datenschutz_delete_customer($pdo, $staff, (int) ($_POST['user_id'] ?? 0), !empty($_POST['close_open']));
if ($r['ok']) $message = 'Die Daten des Kunden wurden unwiderruflich gelöscht (' . $r['tickets'] . ' Ticket(s) anonymisiert, Konto anonymisiert, Nachrichten und Anhänge entfernt).';
else { $error = $r['error']; $searched = trim((string) ($_POST['email'] ?? '')); $found = datenschutz_find_customer($pdo, $searched); }
}
}
}
$cfg = datenschutz_config();
$count = datenschutz_candidate_count($pdo);
$tabs = ['uebersicht' => 'Übersicht', 'einstellungen' => 'Einstellungen', 'antrag' => 'Löschung auf Antrag', 'protokoll' => 'Protokoll'];
$tab = (string) ($_GET['tab'] ?? 'uebersicht');
if (!isset($tabs[$tab])) $tab = 'uebersicht';
// Nach einer Suche bzw. Löschung bleibt man im Reiter „Löschung auf Antrag“
if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array($_POST['action'] ?? '', ['find', 'delete_customer'], true)) $tab = 'antrag';
$log = [];
$logTotal = 0;
$perPage = 50;
$seite = 1;
if ($tab === 'protokoll') {
$logTotal = (int) $pdo->query('SELECT COUNT(*) FROM plugin_datenschutz_log')->fetchColumn();
$pages = max(1, (int) ceil($logTotal / $perPage));
$seite = min($pages, max(1, (int) ($_GET['seite'] ?? 1)));
$log = $pdo->query('SELECT l.*, s.name AS staff_name FROM plugin_datenschutz_log l LEFT JOIN staff s ON s.id = l.staff_id ORDER BY l.id DESC LIMIT ' . $perPage . ' OFFSET ' . (($seite - 1) * $perPage))->fetchAll();
}
$coreOn = get_setting('retention_enabled', '0') === '1';
$kinds = ['auto' => 'Automatisch', 'manual' => 'Von Hand', 'request' => 'Löschung auf Antrag'];
?>
<h1>Datenschutz</h1>
<?php if ($message): ?><div class="flash success"><?= e($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="flash error"><?= e($error) ?></div><?php endif; ?>
<?= datenschutz_tabs($tab, $tabs) ?>
<?php if ($coreOn): ?>
<div class="flash error" style="max-width:900px">
Die automatische Datenlöschung von DeskHub (Einstellungen → Datenschutz) ist eingeschaltet. Sie anonymisiert nach ihrer Frist <strong>auch registrierte Kunden</strong>. Das widerspricht der Regel „Daten registrierter Kunden bleiben bis zur Löschung auf Antrag“. Schalten Sie sie aus, dieses Plugin übernimmt die Aufgabe.
<form method="post" style="margin-top:8px"><?= csrf_field() ?><input type="hidden" name="action" value="core_off"><button class="btn secondary" type="submit">Automatische Löschung von DeskHub ausschalten</button></form>
</div>
<?php endif; ?>
<?php if ($tab === 'uebersicht'): ?>
<div class="card" style="max-width:900px;margin-bottom:16px">
<h3>Regeln</h3>
<ul style="font-size:.92rem;line-height:1.6;padding-left:18px">
<li><strong>Tickets ohne Registrierung:</strong> Nach <?= e(datenschutz_frist_label($cfg)) ?> <?= $cfg['basis'] === 'closed' ? 'nach dem Schließen oder Lösen' : 'nach der Erstellung' ?> werden unwiderruflich entfernt: <strong><?= e(implode(', ', array_map(fn($k) => datenschutz_field_labels()[$k], $cfg['fields']))) ?></strong>. Eine Wiederherstellung ist nicht möglich<?= in_array('email', $cfg['fields'], true) ? ' (die Ersatzadresse ist ein Zufallswert)' : '' ?>.</li>
<li><strong>Registrierte Kunden:</strong> Die Daten bleiben gespeichert, bis der Kunde die Löschung beantragt. Sie führen die Löschung unten aus.</li>
</ul>
<p style="margin-top:8px">
<?php if ($cfg['enabled']): ?><strong style="color:#157a45">✓ Automatische Anonymisierung ist eingeschaltet.</strong> Zurzeit fällig: <strong><?= $count ?></strong> Ticket(s). Letzter Lauf: <?= e(plugin_setting(DATENSCHUTZ, 'last_run', '') ? format_dt((string) plugin_setting(DATENSCHUTZ, 'last_run', '')) : 'noch nicht') ?>.
<?php else: ?><strong style="color:#b45309">Die automatische Anonymisierung ist noch ausgeschaltet.</strong> Beim Einschalten würden sofort <strong><?= $count ?></strong> vorhandene Ticket(s) anonymisiert.<?php endif; ?>
</p>
<?php if ($cfg['enabled']): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Jetzt <?= (int) $count ?> Ticket(s) unwiderruflich anonymisieren?')"><?= csrf_field() ?><input type="hidden" name="action" value="run"><button class="btn secondary" type="submit">Jetzt ausführen</button></form>
<form method="post" style="display:inline"><?= csrf_field() ?><input type="hidden" name="action" value="disable"><button class="btn secondary" type="submit">Ausschalten</button></form>
<?php else: ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="enable">
<label style="font-weight:400"><input type="checkbox" name="confirm" value="1" style="width:auto"> Ich weiß, dass die Anonymisierung unwiderruflich ist und sofort <?= $count ?> vorhandene Ticket(s) betrifft.</label>
<div class="btn-row"><button class="btn" type="submit">Automatische Anonymisierung einschalten</button></div></form>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($tab === 'einstellungen'): ?>
<form method="post" class="card" style="max-width:900px;margin-bottom:16px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="settings">
<h3>Einstellungen (nur Administratoren)</h3>
<label>Frist, nach der anonymisiert wird</label>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<input type="number" name="amount" min="1" max="3650" value="<?= (int) $cfg['amount'] ?>" style="max-width:110px">
<select name="unit" style="max-width:140px"><?php foreach (datenschutz_units() as $k => $l): ?><option value="<?= e($k) ?>" <?= $cfg['unit'] === $k ? 'selected' : '' ?>><?= e($l) ?></option><?php endforeach; ?></select>
<select name="basis"><option value="closed" <?= $cfg['basis'] === 'closed' ? 'selected' : '' ?>>nach dem Schließen bzw. Lösen des Tickets</option><option value="created" <?= $cfg['basis'] === 'created' ? 'selected' : '' ?>>nach der Erstellung des Tickets</option></select>
</div>
<label style="display:block;margin-top:8px;font-weight:400"><input type="checkbox" name="include_open" value="1" style="width:auto" <?= $cfg['include_open'] ? 'checked' : '' ?>> Auch offene Tickets anonymisieren, wenn sie seit Fristablauf nicht bearbeitet wurden (sonst nur geschlossene und gelöste)</label>
<label style="margin-top:14px">Was soll anonymisiert werden?</label>
<div id="ds-presets" style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px">
<?php foreach (datenschutz_presets() as $key => [$plabel, $pfields]): ?><button type="button" class="btn secondary" style="padding:3px 11px;font-size:.8rem" data-fields="<?= e(implode(',', $pfields)) ?>"><?= e($plabel) ?></button><?php endforeach; ?>
</div>
<?php foreach (datenschutz_field_labels() as $key => $flabel): ?>
<label style="display:block;font-weight:400"><input type="checkbox" name="fields[]" value="<?= e($key) ?>" style="width:auto" <?= in_array($key, $cfg['fields'], true) ? 'checked' : '' ?>> <?= e($flabel) ?></label>
<?php endforeach; ?>
<p class="muted" style="font-size:.8rem;margin-top:6px">Nachrichten können Namen, Adressen oder Telefonnummern im Text enthalten; Antworten der Mitarbeiter bleiben immer erhalten. Die Auswahl gilt für Tickets, die ab jetzt anonymisiert werden. Bereits bearbeitete Tickets werden nicht noch einmal angefasst.</p>
<script>
(function () {
var box = document.getElementById('ds-presets');
box.addEventListener('click', function (e) {
var b = e.target.closest('button[data-fields]'); if (!b) return;
var want = b.getAttribute('data-fields').split(',');
b.form.querySelectorAll('input[name="fields[]"]').forEach(function (c) { c.checked = want.indexOf(c.value) !== -1; });
});
})();
</script>
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
</form>
<?php endif; ?>
<?php if ($tab === 'antrag'): ?>
<div class="card" style="max-width:900px;margin-bottom:16px">
<h3>Löschung auf Antrag eines registrierten Kunden</h3>
<p class="muted" style="font-size:.85rem">Der Kunde hat die Löschung seiner Daten verlangt. Das Konto wird anonymisiert und kann sich nicht mehr anmelden. Seine Tickets verlieren Name, Adresse, Nachrichten und Anhänge. Das ist nicht rückgängig zu machen.</p>
<form method="post" style="display:flex;gap:8px;flex-wrap:wrap"><?= csrf_field() ?><input type="hidden" name="action" value="find"><input type="text" name="email" value="<?= e($searched) ?>" placeholder="E-Mail-Adresse des Kunden" style="flex:1;min-width:220px" required><button class="btn secondary" type="submit">Suchen</button></form>
<?php if ($found && !$found['already']): ?>
<form method="post" style="margin-top:12px;border-top:1px solid var(--border,#e3e9ee);padding-top:12px" onsubmit="return confirm('Die Daten dieses Kunden jetzt unwiderruflich löschen?')">
<?= csrf_field() ?><input type="hidden" name="action" value="delete_customer"><input type="hidden" name="user_id" value="<?= (int) $found['user']['id'] ?>"><input type="hidden" name="email" value="<?= e($searched) ?>">
<p><strong><?= e((string) $found['user']['name']) ?></strong> (<?= e((string) $found['user']['email']) ?>)<br><?= (int) $found['tickets'] ?> Ticket(s), davon <?= (int) $found['open'] ?> offen.</p>
<?php if ($found['open']): ?><label style="font-weight:400"><input type="checkbox" name="close_open" value="1" style="width:auto"> Offene Tickets schließen und anonymisieren</label><?php endif; ?>
<label style="display:block;font-weight:400"><input type="checkbox" name="confirm" value="1" style="width:auto"> Der Kunde hat die Löschung beantragt. Ich weiß, dass sie unwiderruflich ist.</label>
<div class="btn-row"><button class="btn danger" type="submit">Daten unwiderruflich löschen</button></div>
</form>
<?php elseif ($found): ?><p class="muted" style="margin-top:10px">Dieser Kunde ist bereits anonymisiert.</p><?php endif; ?>
</div>
<?php endif; ?>
<?php if ($tab === 'protokoll'): ?>
<div class="card" style="max-width:900px">
<h3>Protokoll</h3>
<?php if (!$log): ?><p class="muted">Noch keine Einträge. Das Protokoll enthält keine Kundendaten, bei Löschanträgen nur eine verkürzte Adresse.</p><?php else: ?>
<table style="width:100%"><thead><tr><th>Wann</th><th>Art</th><th>Tickets</th><th>Hinweis</th><th>Von</th></tr></thead><tbody>
<?php foreach ($log as $l): ?>
<tr><td class="muted" style="white-space:nowrap"><?= e(format_dt((string) $l['created_at'])) ?></td><td><?= e($kinds[$l['kind']] ?? $l['kind']) ?></td><td><?= (int) $l['tickets'] ?></td><td><?= e((string) $l['note']) ?></td><td><?= e((string) ($l['staff_name'] ?? '')) ?></td></tr>
<?php endforeach; ?>
</tbody></table>
<?php if ($pages > 1): ?>
<p class="muted" style="margin-top:10px;font-size:.85rem">
<?php if ($seite > 1): ?><a href="<?= e(datenschutz_tab_url('protokoll', ['seite' => $seite - 1])) ?>">← Neuere</a> · <?php endif; ?>
Seite <?= $seite ?> von <?= $pages ?>
<?php if ($seite < $pages): ?> · <a href="<?= e(datenschutz_tab_url('protokoll', ['seite' => $seite + 1])) ?>">Ältere →</a><?php endif; ?>
</p>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>