Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Controllers\Admin\StatusAdminController;
use OpenNIT\Controllers\StatusController;
/**
* Deckt die optionale Absicherung der öffentlichen Statusseite ab.
*
* Ohne Einstellung bleibt `/status` offen wie bisher. Eingeschaltet entscheidet
* ein Token in der Adresse – für die Seite selbst wie für die iframe-Variante,
* und zwar bevor irgendetwas ausgegeben oder aus dem Cache geholt wird.
*/
return static function (): void {
/** Der Quelltext des öffentlichen Controllers. */
$controller = static fn (): string => (string)file_get_contents(
dirname(__DIR__) . '/src/Controllers/StatusController.php'
);
/**
* Ein benannter Methodenrumpf – bis zur schließenden Klammer der Methode.
* Ohne diese Grenze liefe die Prüfung in die nächste Methode hinein.
*/
$rumpf = static function (string $name) use ($controller): string {
$src = $controller();
$von = strpos($src, 'private function ' . $name);
if ($von === false) {
$von = strpos($src, 'public function ' . $name);
}
if ($von === false) {
return '';
}
$ende = strpos($src, "\n }\n", $von);
return $ende !== false ? substr($src, $von, $ende - $von) : substr($src, $von);
};
// ----------------------------------------------------------------
// Standard: offen
// ----------------------------------------------------------------
test('Ohne Einstellung bleibt die Seite offen', function () use ($rumpf) {
// Bestandsinstanzen haben den Schlüssel nicht – dort darf sich nichts ändern.
$body = $rumpf('publicAccessAllowed');
assertTrue(str_contains($body, "get('statuspage_token_enabled') !== '1'"), 'Kein Schalter');
assertTrue(
strpos($body, 'return true;') < strpos($body, 'hash_equals'),
'Der offene Fall wird nicht zuerst entschieden'
);
});
// ----------------------------------------------------------------
// Eingeschaltet: beide Einstiege geschützt
// ----------------------------------------------------------------
test('Seite und iframe-Variante prüfen beide', function () use ($rumpf) {
foreach (['index', 'embed'] as $einstieg) {
$body = $rumpf($einstieg);
assertTrue(str_contains($body, 'publicAccessAllowed($request)'), 'Ungeprüft: ' . $einstieg);
assertTrue(str_contains($body, 'denyPublic()'), 'Ohne Abweisung: ' . $einstieg);
}
});
test('Die Prüfung läuft vor Ausgabe und Cache', function () use ($rumpf) {
// Ein Treffer im Kurzzeit-Cache darf nicht an der Prüfung vorbeikommen.
foreach (['index', 'embed'] as $einstieg) {
$body = $rumpf($einstieg);
assertTrue(
strpos($body, 'publicAccessAllowed') < strpos($body, 'renderPublic'),
'Cache vor Prüfung in ' . $einstieg
);
}
});
test('Der Vergleich ist zeitkonstant und weist Leeres ab', function () use ($rumpf) {
$body = $rumpf('publicAccessAllowed');
assertTrue(str_contains($body, 'hash_equals('), 'Kein zeitkonstanter Vergleich');
assertFalse((bool)preg_match('/\$hinterlegt\s*===\s*\$gesendet/', $body), 'Direkter Stringvergleich');
assertTrue(str_contains($body, "\$hinterlegt !== '' && \$gesendet !== ''"), 'Leere Werte gelten als Treffer');
});
test('Fehlversuche sind gedrosselt, gültige Aufrufe nicht', function () use ($rumpf) {
// Der Endpunkt ist ohne Anmeldung erreichbar; Durchprobieren soll teuer
// sein. Eine Drosselung über alle Aufrufe träfe dagegen auch mehrere
// Besucher hinter derselben Adresse.
$body = $rumpf('publicAccessAllowed');
assertTrue(str_contains($body, 'RateLimiter'), 'Keine Drosselung');
assertTrue(
strpos($body, 'hash_equals') < strpos($body, 'RateLimiter'),
'Die Drosselung greift vor der Prüfung und damit auch bei gültigem Token'
);
});
test('Die Abweisung verrät nichts über die Seite', function () use ($rumpf) {
$body = $rumpf('denyPublic');
assertTrue(str_contains($body, 'http_response_code(404)'), 'Kein neutraler Status');
assertTrue(str_contains($body, 'noindex'), 'Ohne Hinweis an Suchmaschinen');
assertTrue(str_contains($body, 'no-store'), 'Die Abweisung wäre zwischenspeicherbar');
assertFalse(str_contains($body, 'Token'), 'Die Meldung nennt den Grund');
});
test('Mit Absicherung gehört die Antwort nicht in einen geteilten Cache', function () use ($controller) {
assertTrue(
str_contains($controller(), "'Cache-Control: ' . (\$geschuetzt ? 'private' : 'public')"),
'Die Antwort bliebe öffentlich zwischenspeicherbar'
);
});
// ----------------------------------------------------------------
// Ablage des Tokens
// ----------------------------------------------------------------
test('Der Token liegt verschlüsselt und nie im Protokoll', function () {
$admin = (string)file_get_contents(
dirname(__DIR__) . '/src/Controllers/Admin/StatusAdminController.php'
);
assertTrue(method_exists(StatusAdminController::class, 'regenerateToken'), 'Kein Neu-Erzeugen');
assertTrue(str_contains($admin, 'CredentialCrypto::encrypt'), 'Der Token läge im Klartext');
assertTrue(str_contains($admin, 'random_bytes(24)'), 'Kein zufälliger Token');
// Im Audit-Eintrag steht der Vorgang, nicht der Wert.
$von = strpos($admin, 'public function regenerateToken');
$block = substr($admin, $von, 1200);
assertTrue(str_contains($block, "'statuspage_token_regenerated'"), 'Kein Audit-Eintrag');
assertFalse((bool)preg_match('/\$token/', $block), 'Der Token taucht im Protokoll-Block auf');
});
test('Beim Einschalten entsteht ein Token', function () {
$admin = (string)file_get_contents(
dirname(__DIR__) . '/src/Controllers/Admin/StatusAdminController.php'
);
assertTrue(
str_contains($admin, "if (\$tokenOn === '1' && \$this->statusToken() === '') {"),
'Ohne Token ließe sich die Seite sperren, ohne sie noch öffnen zu können'
);
});
test('Beide Seiten lesen denselben Schlüssel', function () use ($controller) {
$admin = (string)file_get_contents(
dirname(__DIR__) . '/src/Controllers/Admin/StatusAdminController.php'
);
foreach (['statuspage_token_enabled', 'statuspage_token'] as $schluessel) {
assertTrue(str_contains($controller(), $schluessel), 'Öffentlich fehlt: ' . $schluessel);
assertTrue(str_contains($admin, $schluessel), 'Backend fehlt: ' . $schluessel);
}
});
// ----------------------------------------------------------------
// Adressen und Einbettung
// ----------------------------------------------------------------
test('Die Verwaltung kennt den Token nur bei aktiver Absicherung', function () {
$src = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/StatusController.php');
assertTrue(
str_contains($src, "get('statuspage_token_enabled') === '1'\n ? \$this->statusToken() : ''"),
'Der Token ginge auch ohne Absicherung an die Ansicht'
);
assertTrue(method_exists(StatusController::class, 'manage'));
});
test('Der Einbettungscode trägt den Token', function () {
// Ohne ihn bliebe der eingebettete Rahmen leer.
$tpl = (string)file_get_contents(dirname(__DIR__) . '/templates/status/manage.php');
assertTrue(str_contains($tpl, "'/status/embed' + query"), 'iframe ohne Token');
assertTrue(str_contains($tpl, "origin + '/status' + query"), 'Die genannte Adresse ohne Token');
assertTrue(str_contains($tpl, "'?token=' . \$status_token"), 'Der Token wird nicht eingesetzt');
});
test('Auch die Verweise im Backend führen zur erreichbaren Seite', function () {
$tpl = (string)file_get_contents(dirname(__DIR__) . '/templates/status/manage.php');
$admin = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/statuspage/settings.php');
// Kopfzeile der Verwaltung
assertTrue(str_contains($tpl, 'href="/status<?= ($status_token ?? \'\') !== \'\''), 'Kopf-Verweis ohne Token');
// Fertige Links im Backend
assertTrue(str_contains($admin, '/status?token='), 'Kein fertiger Link zur Seite');
assertTrue(str_contains($admin, '/status/embed?token='), 'Kein fertiger Link zur iframe-Variante');
});
test('Die Links erscheinen erst, wenn die Absicherung aktiv ist', function () {
$admin = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/statuspage/settings.php');
assertTrue(
str_contains($admin, "(\$v['statuspage_token_enabled'] ?? '0') === '1' && (\$status_token ?? '') !== ''"),
'Die Karte hängt an keiner Bedingung'
);
});
test('Die Oberfläche benennt die Grenze des Verfahrens', function () {
// Ein Token in der Adresse landet in Verlauf, Lesezeichen und Protokollen –
// das soll in der Beschreibung stehen, nicht nur im Quelltext.
$admin = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/statuspage/settings.php');
assertTrue(str_contains($admin, 'ersetzt keine Anmeldung'), 'Die Einschränkung fehlt');
assertTrue(str_contains($admin, 'Server-Protokollen'), 'Der Hinweis auf Protokolle fehlt');
});
test('Die Symbole sind der Emoji-Umschaltung bekannt', function () {
$map = (string)file_get_contents(dirname(__DIR__) . '/src/Core/IconMap.php');
$admin = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/statuspage/settings.php');
assertTrue(str_contains($admin, '<i class="fas fa-sync-alt"></i>'), 'Icon fehlt oder ohne Stil-Präfix');
assertTrue(str_contains($map, "'fa-sync-alt'"), 'Nicht in der IconMap');
});
};