Files
deskhub/DeskHub-Webseite/tests/error_alert.php
T
2026-10-24 20:14:41 +00:00

87 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft die Fehler-Mail bei schweren PHP-Fehlern: Empfänger, Inhalt (ohne Parameter und Serverpfade), Drosselung, Ausschalter
* und einmal echt: Eine Seite mit einem schweren Fehler löst in einem eigenen Prozess die Meldung aus.
*/
$db = sys_get_temp_dir() . '/deskhub-ea-' . bin2hex(random_bytes(4)) . '.sqlite';
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_SQLITE_PATH=' . $db);
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void {
if (!$ok) $failures[] = $what;
};
$pdo = get_db();
$mails = [];
$send = function (string $to, string $subject, string $html) use (&$mails): bool {
$mails[] = compact('to', 'subject', 'html');
return true;
};
$root = str_replace('\\', '/', dirname(__DIR__));
$err = ['type' => E_ERROR, 'message' => "Uncaught Error: Call to undefined function foo() in {$root}/staff/x.php:12", 'file' => $root . '/staff/x.php', 'line' => 12];
// Empfänger
$pdo->prepare("INSERT INTO staff (name, email, password_hash, role, is_active, created_at) VALUES ('Chef','chef@firma.example','x','admin',1,?)")->execute([db_now()]);
set_setting('admin_email', '');
$check(in_array('chef@firma.example', error_alert_recipients($pdo), true), 'ohne eigene Adresse gehen die Mails an die Administratoren');
set_setting('admin_email', 'it@firma.example');
$check(error_alert_recipients($pdo) === ['it@firma.example'], 'die Admin-Adresse der Seite hat Vorrang');
set_setting('error_alert_email', 'a@firma.example, b@firma.example; kaputt');
$check(error_alert_recipients($pdo) === ['a@firma.example', 'b@firma.example'], 'eigene Empfänger (mehrere, ungültige werden übersprungen)');
// Inhalt
$_SERVER['REQUEST_URI'] = '/staff/tickets.php?token=GEHEIM&x=1';
[$subject, $html] = error_alert_compose($err);
$check(str_contains($html, 'staff/x.php') && str_contains($html, 'Zeile 12') && str_contains($html, '/staff/tickets.php'), 'Mail nennt Datei, Zeile und Seite');
$check(!str_contains($html, 'GEHEIM') && !str_contains($html, $root), 'Mail enthält weder Adress-Parameter noch den Serverpfad');
[, $evil] = error_alert_compose(['message' => '<script>alert(1)</script>', 'file' => 'x', 'line' => 1]);
$check(!str_contains($evil, '<script>'), 'Fehlertext wird maskiert');
// Senden und Drosselung
$n = error_alert_dispatch($pdo, $err, $send);
$check($n === 2 && count($mails) === 2, 'erste Meldung geht an beide Empfänger');
$check(error_alert_dispatch($pdo, $err, $send) === 0 && count($mails) === 2, 'dieselbe Meldung wird innerhalb einer Stunde nicht noch einmal gesendet');
$check(error_alert_dispatch($pdo, ['line' => 99, 'message' => 'Anderer Fehler'] + $err, $send) === 2, 'eine andere Meldung geht durch');
$same = error_alert_dispatch($pdo, ['message' => "Uncaught Error: Call to undefined function foo() in {$root}/staff/x.php:13", 'line' => 13] + $err, $send);
$check($same === 0, 'dieselbe Meldung mit anderer Zeilennummer im Text zählt als gleich');
for ($i = 0; $i < 20; $i++) error_alert_dispatch($pdo, ['message' => "Fehler Nummer-{$i}x", 'file' => "/f{$i}x.php"] + $err, $send);
$check(count($mails) <= 2 * 10 + 0 && count(array_unique(array_map(fn($m) => $m['subject'] . $m['html'], $mails))) <= 10, 'höchstens 10 verschiedene Fehler-Mails pro Stunde');
// Ausschalter
set_setting('error_alert_log', '[]');
set_setting('error_alert', '0');
$before = count($mails);
$check(error_alert_dispatch($pdo, $err, $send) === 0 && count($mails) === $before, 'ausgeschaltet wird nichts gesendet');
set_setting('error_alert', '1');
// Nicht schwere Fehler und Hinweise lösen nichts aus
$check(!in_array(E_WARNING, ERROR_ALERT_FATAL, true) && !in_array(E_NOTICE, ERROR_ALERT_FATAL, true) && !in_array(E_DEPRECATED, ERROR_ALERT_FATAL, true), 'Warnungen und Hinweise zählen nicht');
// Echt: eigener Prozess mit schwerem Fehler
set_setting('error_alert_log', '[]');
$script = sys_get_temp_dir() . '/ea-page-' . bin2hex(random_bytes(3)) . '.php';
file_put_contents($script, "<?php\nputenv('DESKHUB_TESTING=1');\nputenv('DESKHUB_SQLITE_PATH=' . " . var_export($db, true) . ");\nrequire " . var_export(dirname(__DIR__) . '/includes/bootstrap.php', true) . ";\nnoch_nie_definiert_im_system();\n");
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($script) . ' 2>&1', $out, $code);
@unlink($script);
$log = json_decode((string) $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'error_alert_log'")->fetchColumn(), true);
$check(is_array($log) && count($log) === 1, 'echter schwerer Fehler in einer Seite löst die Meldung aus (Prozess-Ende ' . $code . ')');
// Seite ohne Fehler: kein Eintrag
set_setting('error_alert_log', '[]');
$script = sys_get_temp_dir() . '/ea-ok-' . bin2hex(random_bytes(3)) . '.php';
file_put_contents($script, "<?php\nputenv('DESKHUB_TESTING=1');\nputenv('DESKHUB_SQLITE_PATH=' . " . var_export($db, true) . ");\nrequire " . var_export(dirname(__DIR__) . '/includes/bootstrap.php', true) . ";\ntrigger_error('nur eine Warnung', E_USER_WARNING);\n");
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($script) . ' 2>&1', $out2, $code2);
@unlink($script);
$log = json_decode((string) $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'error_alert_log'")->fetchColumn(), true);
$check(($log === [] || $log === null), 'eine Warnung löst keine Meldung aus');
@unlink($db);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo 'Error alert checks passed.' . "\n";