Redirect the other www spelling to SITE_ORIGIN, hold roles to the role help and fix the audit findings

SITE_ORIGIN alone decides whether the site is www or not: CanonicalHostSubscriber
sends the other spelling there with 301, cms:site:origin writes the setting, the
system screen shows it. The roles now match the role help (editor: drafts,
publisher: live content, admin: design, settings, users, system). The installer no
longer reopens on an unreachable database, the session cookie follows ADMIN_PATH,
percent-encoded credentials no longer break the container, consent only accepts
same-site decisions, JSON-LD is HTML-safe, own CSS stays in its block, plus the
editor, updater, download and public-output findings from the full review
(ADR 099, ADR 100).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.1 committed 2026-09-15 20:15:20 +02:00
1 parent 45040b2ca6
commit 7eb462ac7b
53 files changed
+1023 -122

No files matched your search

+27 -2
View File
@@ -413,6 +413,17 @@ final class CmsTest extends TestCase
self::assertSame('/', $response->headers->get('Location'));
}
public function testTheOtherSpellingOfTheHostIsSentToSiteOrigin(): void
{
// SITE_ORIGIN der Tests ist https://cms.test - die www-Fassung fuehrt dorthin, mit Pfad
// und Abfrage, bevor irgendjemand die Datenbank fragt.
$response = $this->request('http://www.cms.test/home?a=1');
self::assertSame(301, $response->getStatusCode());
self::assertSame('https://cms.test/home?a=1', $response->headers->get('Location'));
// Ein fremder Host bleibt erreichbar: eine falsch eingetragene Adresse sperrt niemanden aus.
self::assertSame(200, $this->request('http://vorschau.hoster.test/robots.txt')->getStatusCode());
}
public function testCreatedPageStaysPrivateUntilPublishedAndKeepsItsAddress(): void
{
$id = $this->pages->createPage('ueber-uns', null, 'page', DemoContent::starter('Über uns'));
@@ -1431,7 +1442,7 @@ final class CmsTest extends TestCase
$ohne = $this->request('/');
self::assertStringNotContainsString('fonts.googleapis.com', $ohne->getContent());
self::assertStringNotContainsString('fonts.gstatic.com', $ohne->getContent());
self::assertStringNotContainsString('rel="preload"'." href=\"/assets/fonts", $ohne->getContent());
self::assertStringNotContainsString('rel="preload" href="/assets/fonts', $ohne->getContent());
// Mit Dateien: die eigenen Dateien, und zwar **jeder** Schnitt vorab. Fehlte einer, läge
// er erst nach dem Stylesheet vor, und die Überschrift spränge sichtbar von der
@@ -2036,6 +2047,15 @@ final class CmsTest extends TestCase
self::assertStringContainsString("script-src 'none'", (string) $other->headers->get('Content-Security-Policy'));
self::assertSame(404, $this->request('/gibt-es-nicht')->getStatusCode());
self::assertStringContainsString("script-src 'none'", (string) $this->request('/gibt-es-nicht')->headers->get('Content-Security-Policy'));
// Tabs im Fussbereich: die Seite selbst hat keine, braucht das Skript aber trotzdem - und
// eine Richtlinie, die es zulaesst. Vorher blieb der Fuss eine Ankerliste mit blockiertem
// Skript.
$footer = $this->partials->findByKind('footer');
$this->partials->save($footer['id'], [...$footer['document'], 'schemaVersion' => max((int) $footer['document']['schemaVersion'], 7), 'blocks' => [...$footer['document']['blocks'], ...array_slice($document['blocks'], -1)]], (int) $footer['version']);
$withFooterTabs = $this->request('/ohne-tabs');
self::assertStringContainsString('<script src="/assets/tabs.js?v=', $withFooterTabs->getContent());
self::assertStringContainsString("script-src 'self'", (string) $withFooterTabs->headers->get('Content-Security-Policy'));
}
public function testTabsSurviveSavingAndRejectMisplacedEntries(): void
@@ -2075,7 +2095,11 @@ final class CmsTest extends TestCase
$this->posts->saveMetadata($hidden, ['robots' => 'noindex'], [], [], $this->pages->findForEditor($hidden)['version']);
$this->publishedPage('offen', 'Offen');
$this->pages->createPage('entwurf', null, 'page', DemoContent::starter('Entwurf'));
$this->publishedPost('ein-beitrag', 'Ein Beitrag', '2026-02-01', ['Projekte'], []);
$post = $this->publishedPost('ein-beitrag', 'Ein Beitrag', '2026-02-01', ['Projekte'], []);
// Neu veroeffentlicht nach einer Aenderung: lastmod ist das Datum der Fassung, nicht das
// der ersten Veroeffentlichung.
$edited = $this->pages->findForEditor($post);
$this->pages->publish($post, $this->pages->saveDraft($post, $edited['document'], $edited['version']), $edited['version'] + 1);
$sitemap = $this->request('/sitemap.xml');
self::assertSame(200, $sitemap->getStatusCode());
@@ -2086,6 +2110,7 @@ final class CmsTest extends TestCase
self::assertStringContainsString('<loc>https://cms.test/offen</loc>', $xml);
self::assertStringContainsString('<loc>https://cms.test/beitraege</loc>', $xml);
self::assertStringContainsString('<loc>https://cms.test/kategorie/projekte</loc>', $xml);
self::assertStringContainsString("<loc>https://cms.test/ein-beitrag</loc>\n <lastmod>".gmdate('Y-m-d').'</lastmod>', $xml);
// A noindex page and an unpublished page have no business in a sitemap.
self::assertStringNotContainsString('/intern', $xml);
self::assertStringNotContainsString('/entwurf', $xml);