Files
2026-10-16 19:12:15 +00:00

178 lines
6.0 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* cron/anonymize_data.php
*
* Anonymisiert personenbezogene Daten aus geschlossenen/gelösten Tickets
* nach der in den Einstellungen konfigurierten Aufbewahrungsdauer.
*
* Crontab: 0 3 * * * php /pfad/zu/deskhub/cron/anonymize_data.php
*
* Was wird anonymisiert:
* - tickets.requester_name → "Anonymisiert"
* - tickets.requester_email → "anon_[hash]@deleted.local"
* - tickets.requester_ip → "0.0.0.0"
* - users.name + email → wenn der User keine anderen offenen Tickets hat
*
* Was bleibt erhalten:
* - Ticket-Nummer, Betreff, Status, Priorität, Abteilung
* - Alle Nachrichten (ticket_threads) inkl. Zeitstempel
* - Statistiken / Auswertungen
*/
// Direkter Web-Zugriff (Browser/URL) wird geblockt. Ausnahmen: echter CLI-Cronjob
// sowie Einbindung per require_once aus dem authentifizierten Backend (staff/settings.php),
// das vor dem require_once die Konstante DESKHUB_CRON_RUNNER definiert.
if (php_sapi_name() !== 'cli' && !defined('DESKHUB_CRON_RUNNER')) {
http_response_code(403);
exit('Kein Web-Zugriff.');
}
require_once __DIR__ . '/../includes/db.php';
if (php_sapi_name() === 'cli') {
$pdo = get_db();
$result = run_anonymization($pdo, true);
foreach ($result['log'] as $line) {
echo $line . PHP_EOL;
}
echo "Fertig: {$result['count']} Ticket(s) anonymisiert." . PHP_EOL;
}
// ─────────────────────────────────────────────────────────────────────────────
/**
* Führt die Anonymisierung aus.
* Kann von settings.php (manuell) und vom Cron aufgerufen werden.
*
* @return array{count: int, log: string[]}
*/
function run_anonymization(PDO $pdo, bool $verbose = false): array
{
$result = ['count' => 0, 'log' => []];
set_setting_local($pdo, 'cron_anonymize_data_last_run', date('Y-m-d H:i:s'));
// Einstellungen aus DB laden
$enabled = get_setting_local($pdo, 'retention_enabled', '0');
if ($enabled !== '1') {
$result['log'][] = '[skip] Automatische Anonymisierung ist deaktiviert.';
return $result;
}
$months = max(1, (int) get_setting_local($pdo, 'retention_months', '24'));
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
// Cutoff-Datum berechnen
$cutoff = date('Y-m-d H:i:s', strtotime("-{$months} months"));
if ($verbose) {
$result['log'][] = "[info] Anonymisiere Tickets geschlossen/gelöst vor: $cutoff";
}
// Betroffene Tickets finden (noch nicht anonymisiert = email enthält kein 'anon_')
$stmt = $pdo->prepare(
"SELECT id, requester_email, user_id
FROM tickets
WHERE status IN ('closed', 'resolved')
AND updated_at < ?
AND requester_email NOT LIKE 'anon\\_%' ESCAPE '\\\\'
LIMIT 500"
);
$stmt->execute([$cutoff]);
$tickets = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($tickets)) {
$result['log'][] = '[info] Keine Tickets zur Anonymisierung gefunden.';
return $result;
}
$updTicket = $pdo->prepare(
"UPDATE tickets
SET requester_name = 'Anonymisiert',
requester_email = ?,
requester_ip = '0.0.0.0'
WHERE id = ?"
);
// User anonymisieren wenn er keine anderen offenen Tickets mehr hat
$checkUser = $pdo->prepare(
"SELECT COUNT(*) FROM tickets
WHERE user_id = ?
AND status NOT IN ('closed', 'resolved')
AND id != ?"
);
$updUser = $pdo->prepare(
"UPDATE users
SET name = 'Anonymisiert',
email = ?,
phone = NULL
WHERE id = ? AND email NOT LIKE 'anon\\_%' ESCAPE '\\\\'"
);
foreach ($tickets as $ticket) {
$anonEmail = 'anon_' . substr(hash('sha256', $ticket['requester_email'] . 'deskhub-anon'), 0, 16) . '@deleted.local';
try {
$pdo->beginTransaction();
$updTicket->execute([$anonEmail, $ticket['id']]);
// User-Account anonymisieren wenn keine anderen offenen Tickets
if ($ticket['user_id']) {
$checkUser->execute([$ticket['user_id'], $ticket['id']]);
if ((int) $checkUser->fetchColumn() === 0) {
$updUser->execute([$anonEmail, $ticket['user_id']]);
}
}
$pdo->commit();
$result['count']++;
if ($verbose) {
$result['log'][] = "[ok] Ticket #{$ticket['id']} anonymisiert";
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
$result['log'][] = "[err] Ticket #{$ticket['id']}: " . $e->getMessage();
}
}
return $result;
}
/**
* Liest eine Einstellung direkt aus der DB (ohne Bootstrap-Abhängigkeit).
*/
function get_setting_local(PDO $pdo, string $key, string $default = ''): string
{
try {
$stmt = $pdo->prepare('SELECT setting_value FROM settings WHERE setting_key = ?');
$stmt->execute([$key]);
$val = $stmt->fetchColumn();
return $val !== false ? (string) $val : $default;
} catch (Throwable $e) {
return $default;
}
}
/**
* Speichert eine Einstellung direkt in der DB (ohne Bootstrap-Abhängigkeit, treiber-unabhängiges Upsert).
*/
function set_setting_local(PDO $pdo, string $key, string $value): void
{
try {
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
if ($driver === 'sqlite') {
$pdo->prepare('INSERT OR REPLACE INTO settings (setting_key, setting_value) VALUES (?,?)')
->execute([$key, $value]);
} else {
$pdo->prepare(
'INSERT INTO settings (setting_key, setting_value) VALUES (?,?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
)->execute([$key, $value]);
}
} catch (Throwable $e) {
// Best effort – Anonymisierung soll nicht daran scheitern.
}
}