Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Haelt fest, welche Schreibwege protokolliert werden – und welche bewusst nicht.
*
* Ein Protokoll nuetzt nur, solange es vollstaendig ist: Faellt ein Eintrag
* weg, merkt das niemand, denn es fehlt ja gerade die Spur. Umgekehrt ist ein
* Protokoll wertlos, wenn jeder Tastendruck darin steht – dann findet niemand
* mehr die eine Aenderung, auf die es ankommt.
*
* Deshalb halten diese Faelle beide Richtungen fest: geschlossene Luecken
* bleiben geschlossen, und die bewussten Ausnahmen bleiben Ausnahmen.
*/
return static function (): void {
$wurzel = dirname(__DIR__);
$lies = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/src/Controllers/' . $pfad);
/**
* Sucht den Rumpf einer Methode.
*
* @return string Rumpf bis zur naechsten Methode gleicher Einrueckung.
*/
$rumpf = static function (string $quelle, string $methode): string {
$start = strpos($quelle, 'function ' . $methode . '(');
if ($start === false) {
return '';
}
$naechste = preg_match('/\n (?:public|private|protected)\s+function\s/', substr($quelle, $start), $m, PREG_OFFSET_CAPTURE)
? $m[0][1] : strlen($quelle) - $start;
return substr($quelle, $start, $naechste);
};
/** @return bool Protokolliert der Rumpf? */
$loggt = static fn (string $body): bool => (bool)preg_match('/audit\(\)->log|auditLogger|getAuditLogger\(\)/', $body);
// ----------------------------------------------------------------
// Geschlossene Luecken
// ----------------------------------------------------------------
test('Loeschungen in der Verwaltung hinterlassen eine Spur', function () use ($lies, $rumpf, $loggt) {
// Was geloescht wurde, laesst sich nachher nirgends mehr ablesen –
// ausser im Protokoll.
// Schlagworte und Seitenvorlagen protokolliert ihre Fachklasse – siehe
// den Fall weiter unten; hier stehen die Wege ohne eigene Fachklasse.
$faelle = [
'Admin/AssetAdminController.php' => ['typeImagesDestroy', 'categoryFieldsDestroy', 'typeFieldsDestroy'],
'Admin/WorkflowAdminController.php' => ['deleteField', 'deleteStep'],
'WorkflowController.php' => ['deleteWebhook'],
];
foreach ($faelle as $datei => $methoden) {
$quelle = $lies($datei);
foreach ($methoden as $methode) {
$body = $rumpf($quelle, $methode);
assertTrue($body !== '', "Methode {$methode} in {$datei} nicht gefunden");
assertTrue($loggt($body), "Loeschung ohne Protokolleintrag: {$datei}::{$methode}");
}
}
});
test('Eine Loeschung liest den Datensatz, bevor sie ihn entfernt', function () use ($lies, $rumpf) {
// Sonst steht im Protokoll nur eine Nummer und niemand weiss, was fehlt.
$faelle = [
'Admin/WorkflowAdminController.php' => 'deleteField',
'WorkflowController.php' => 'deleteWebhook',
];
foreach ($faelle as $datei => $methode) {
$body = $rumpf($lies($datei), $methode);
$lesen = (int)preg_match('/fetchOne|findById|ownWebhook/', $body, $m, PREG_OFFSET_CAPTURE) ? $m[0][1] : PHP_INT_MAX;
$loesch = preg_match('/->delete\(|DELETE FROM/', $body, $m2, PREG_OFFSET_CAPTURE) ? $m2[0][1] : PHP_INT_MAX;
assertTrue($lesen < $loesch, "Wird erst geloescht, dann gelesen: {$datei}::{$methode}");
}
});
test('Aenderungen an der Einrichtung stehen im Protokoll', function () use ($lies, $rumpf, $loggt) {
$faelle = [
'Admin/AssetPdfTemplateController.php' => ['update'],
'Admin/MonitoringAdminController.php' => ['saveSettings', 'regenerateToken'],
'Admin/WordPressAdminController.php' => ['saveTexts'],
'Admin/DirectoryAdminController.php' => ['uploadImage'],
'Admin/AssetAdminController.php' => ['typeImagesUpload'],
];
foreach ($faelle as $datei => $methoden) {
$quelle = $lies($datei);
foreach ($methoden as $methode) {
$body = $rumpf($quelle, $methode);
assertTrue($body !== '', "Methode {$methode} in {$datei} nicht gefunden");
assertTrue($loggt($body), "Einrichtung geaendert ohne Protokolleintrag: {$datei}::{$methode}");
}
}
});
test('Das Portal protokolliert wie der Weg im Haus', function () use ($lies, $rumpf) {
// Dieselbe Aenderung darf nicht davon abhaengen, ueber welchen Zugang
// sie hereinkam – sonst entgeht dem Protokoll alles aus dem Portal.
$quelle = $lies('WordPressApiController.php');
foreach (['createOrder', 'ticketReply', 'cancelLoanRequest'] as $methode) {
$body = $rumpf($quelle, $methode);
assertTrue($body !== '', "Methode {$methode} nicht gefunden");
assertTrue(str_contains($body, '$this->protokolliere('), "Portal-Aktion ohne Protokolleintrag: {$methode}");
}
});
test('Ein Protokollfehler laesst die Portal-Antwort unberuehrt', function () use ($wurzel, $rumpf) {
// Das Portal wartet auf eine Antwort; ein Protokollfehler darf den
// Vorgang nicht scheitern lassen, nachdem er bereits geschrieben wurde.
$body = $rumpf((string)file_get_contents($wurzel . '/src/Api/TokenAuth.php'), 'protokolliere');
assertTrue(str_contains($body, 'try {'), 'Der Protokolleintrag ist nicht abgesichert');
assertTrue(str_contains($body, 'catch (\Throwable)'), 'Ein Protokollfehler schlaegt durch');
});
test('Wo die Fachklasse protokolliert, tut es der Controller nicht noch einmal', function () use ($wurzel, $lies, $rumpf, $loggt) {
// Ein zweiter Eintrag fuer denselben Vorgang macht das Protokoll nicht
// vollstaendiger, nur laenger – und laesst eine Aenderung doppelt
// erscheinen, als waere sie zweimal geschehen.
$paare = [
'Admin/TagController.php' => ['Content/TagManager.php', ['create_tag', 'update_tag', 'delete_tag']],
'Admin/PageTemplateController.php' => ['Content/PageTemplateManager.php', ['create_template', 'update_template', 'delete_template']],
];
foreach ($paare as $controller => [$manager, $vorgaenge]) {
$quelleM = (string)file_get_contents($wurzel . '/src/' . $manager);
foreach ($vorgaenge as $vorgang) {
assertTrue(str_contains($quelleM, "'{$vorgang}'"), "{$manager} protokolliert {$vorgang} nicht mehr");
}
$quelleC = $lies($controller);
foreach (['store', 'update', 'destroy'] as $methode) {
$body = $rumpf($quelleC, $methode);
if ($body === '') { continue; }
assertFalse($loggt($body), "Doppelter Protokolleintrag: {$controller}::{$methode} neben {$manager}");
}
}
});
// ----------------------------------------------------------------
// Bewusste Ausnahmen
// ----------------------------------------------------------------
test('Was staendig passiert, bleibt aus dem Protokoll', function () use ($lies, $rumpf, $loggt) {
// Diese Wege schreiben im Sekundentakt oder ohne Zutun eines Menschen.
// Ein Eintrag je Vorgang machte das Protokoll unlesbar – die eigentliche
// Aenderung steht jeweils an anderer Stelle darin.
$ausnahmen = [
'TicketController.php' => ['viewersPing'], // wer gerade zusieht
'PageController.php' => ['autosave'], // Entwurf im Sekundentakt
'MonitoringController.php' => ['webhook'], // Meldungen der Überwachung
'WordPressApiController.php' => ['preflight'], // technischer Handschlag
];
foreach ($ausnahmen as $datei => $methoden) {
$quelle = $lies($datei);
foreach ($methoden as $methode) {
$body = $rumpf($quelle, $methode);
assertTrue($body !== '', "Methode {$methode} in {$datei} nicht gefunden");
assertFalse($loggt($body), "Protokollflut droht: {$datei}::{$methode} schreibt jetzt Eintraege");
}
}
});
test('Sperren und Anwesenheit bleiben aussen vor', function () use ($lies, $rumpf, $loggt) {
// Eine Schreibsperre sagt nichts darueber, ob sich etwas geaendert hat;
// das Speichern selbst steht im Protokoll.
$quelle = $lies('PageController.php');
foreach (['writeLock', 'deleteLock'] as $methode) {
assertFalse($loggt($rumpf($quelle, $methode)), "Sperre erzeugt jetzt Protokolleintraege: {$methode}");
}
});
};