Files
KantSites/src/EventSubscriber/CanonicalHostSubscriber.php
T
kantarciandClaude Fable 5.1 7eb462ac7b 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>
2026-09-15 20:15:20 +02:00

84 lines
3.6 KiB
PHP

<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Eine Website, eine Adresse: mit oder ohne www entscheidet SITE_ORIGIN, nicht der Webserver.
*
* Bisher stand die Umleitung auskommentiert in public/.htaccess und musste von Hand zu SITE_ORIGIN
* passen. Zwei Stellen fuer eine Entscheidung gehen auseinander, und dann zeigt jede Seite in
* Canonical, Sitemap und Feed auf eine Adresse, die weiterleitet. Jetzt liest die Anwendung die
* konfigurierte Adresse und schickt die andere Schreibweise dorthin - dauerhaft (301), mit Pfad
* und Abfrage.
*
* Was sie bewusst nicht tut: einen fremden Host umleiten. Eine Vorschauadresse des Hosters, eine
* IP-Adresse oder localhost bleiben erreichbar, auch wenn SITE_ORIGIN etwas anderes nennt - sonst
* sperrt eine falsch eingetragene Adresse jeden aus, samt der Verwaltung. Umgeleitet wird nur das
* Paar aus `example.de` und `www.example.de`. Und sie erzwingt kein HTTPS: hinter einem Proxy
* sieht PHP oft nur http, und eine Schleife waere schlimmer als fehlendes HTTPS. Die drei Zeilen
* dafuer stehen weiter in .htaccess, wo der Webserver die Verbindung wirklich kennt.
*/
final class CanonicalHostSubscriber implements EventSubscriberInterface
{
public function __construct(private string $siteOrigin)
{
}
public static function getSubscribedEvents(): array
{
// Vor Wartung (40) und Installer (36): auf dem falschen Host gibt es genau eine Antwort,
// und fuer die braucht niemand die Datenbank.
return [KernelEvents::REQUEST => ['onRequest', 48]];
}
public function onRequest(RequestEvent $event): void
{
$request = $event->getRequest();
// Nur lesende Anfragen: ein 301 macht aus einem POST ein GET und verwirft die Eingabe.
// Ein Formular auf der anderen Schreibweise darf noch ankommen; die Seite dahinter
// fuehrt dann ohnehin auf die richtige Adresse.
if (!$event->isMainRequest() || !\in_array($request->getMethod(), ['GET', 'HEAD'], true)) {
return;
}
$target = self::target($this->siteOrigin, $request);
if (null !== $target) {
$event->setResponse(new RedirectResponse($target, 301, ['Cache-Control' => 'public, max-age=3600']));
}
}
/** Die Adresse, auf die diese Anfrage gehoert - oder null, wenn sie schon dort ist. */
public static function target(string $origin, Request $request): ?string
{
$parts = parse_url(trim($origin));
$wanted = strtolower((string) ($parts['host'] ?? ''));
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
if ('' === $wanted || !\in_array($scheme, ['http', 'https'], true)) {
return null;
}
try {
$host = strtolower($request->getHost());
} catch (\UnexpectedValueException) {
// Ein Host, den Symfony nicht annimmt, bekommt seine 400 vom Kernel - nicht von hier.
return null;
}
if ($host === $wanted || !self::wwwTwins($host, $wanted)) {
return null;
}
$port = isset($parts['port']) ? ':'.$parts['port'] : '';
return $scheme.'://'.$wanted.$port.$request->getRequestUri();
}
/** `example.de` und `www.example.de` - und sonst nichts. */
private static function wwwTwins(string $a, string $b): bool
{
return 'www.'.$a === $b || $a === 'www.'.$b;
}
}