Upload via GUI (258 Dateien)

This commit is contained in:
2026-10-16 19:12:15 +00:00
parent 491b87f468
commit 56deade877
50 changed files with 23552 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
<?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.
}
}