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>
84 lines
3.6 KiB
PHP
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;
|
|
}
|
|
}
|