Tabs als fortschreitende Verbesserung (Dokumentschema v7)
Auf Entscheidung von Umut werden Tabs jetzt mit JavaScript umgesetzt. Die Regel "der Seiteninhalt braucht kein Browser-JavaScript" bleibt dabei eingehalten: der Server liefert eine Ankerliste und alle Panels mit eigenen Überschriften, versteckt nichts, und ohne Skript ist jeder Inhalt lesbar und über die Tastatur erreichbar. public/assets/tabs.js ergänzt tablist/tab/tabpanel, Auswahl per Pfeiltasten, Home und Ende sowie aria-selected und wählt bei einem passenden Sprungziel das zugehörige Panel. Die strenge Richtlinie bleibt Voreinstellung: öffentliche Seiten senden weiterhin script-src 'none'. Nur eine Seite mit Tab-Block sendet script-src 'self' und bindet die eine externe Datei ein; Inline-Skript ist nie erlaubt. Geprüft: 109 PHP-Tests / 395 Assertions unter PHP 8.2.12 und 8.4.22, 14 JavaScript-Modelltests, Lint/Format/Container. Das Skript selbst wurde nicht ausgeführt: Tastaturbedienung und ARIA-Verhalten sind ohne Browserlauf ungeprüft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8600384f1a
commit
c6ec00a906
17 files changed
+279
-26
No files matched your search
@@ -38,23 +38,46 @@ final class PageController extends AbstractController
|
||||
'navigation' => $this->navigation($path),
|
||||
'postLists' => $this->postLists($page['document']),
|
||||
'postMeta' => 'post' === $page['type'] ? $this->posts->termsFor($page['id']) : null,
|
||||
'needsTabs' => $this->hasType($page['document'], 'tabs'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->shell($response);
|
||||
return $this->shell($response, null !== $page && $this->hasType($page['document'], 'tabs'));
|
||||
}
|
||||
|
||||
/** Every public response carries the same headers, whether page, archive or feed. */
|
||||
private function shell(Response $response): Response
|
||||
/**
|
||||
* Every public response carries the same headers. Scripting stays forbidden unless the page
|
||||
* actually contains a tab group: only then may the browser load the one enhancement script,
|
||||
* and only from this origin. Inline script is never allowed.
|
||||
*/
|
||||
private function shell(Response $response, bool $allowScript = false): Response
|
||||
{
|
||||
$response->headers->set('Cache-Control', 'no-store');
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'none'; style-src 'self'; img-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'");
|
||||
$response->headers->set('Content-Security-Policy', sprintf(
|
||||
"default-src 'self'; script-src %s; style-src 'self'; img-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
|
||||
$allowScript ? "'self'" : "'none'",
|
||||
));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/** True when the stored document contains at least one block of that type. */
|
||||
private function hasType(array $document, string $type): bool
|
||||
{
|
||||
$found = false;
|
||||
$walk = function (array $blocks) use (&$walk, &$found, $type): void {
|
||||
foreach ($blocks as $block) {
|
||||
$found = $found || $type === ($block['type'] ?? null);
|
||||
$walk($block['children'] ?? []);
|
||||
}
|
||||
};
|
||||
$walk($document['blocks'] ?? []);
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
#[Route('/beitraege', name: 'posts', methods: ['GET'])]
|
||||
#[Route('/kategorie/{slug}', name: 'category', requirements: ['slug' => '[a-z0-9-]{1,80}'], methods: ['GET'])]
|
||||
#[Route('/schlagwort/{slug}', name: 'tag', requirements: ['slug' => '[a-z0-9-]{1,80}'], methods: ['GET'])]
|
||||
|
||||
Reference in new issue
Block a user