178 lines
6.0 KiB
PHP
178 lines
6.0 KiB
PHP
<?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.
|
||
}
|
||
}
|