Set the site address in the administration, checked against the installation itself

- System > This installation > Address has a field and a link to the other
  spelling (with or without www), like the WordPress address. It writes
  SITE_ORIGIN to .env through Environment::write and takes effect on the
  next request. With SITE_ORIGIN from the server environment there is no
  field, and a POST is refused.
- Before saving, OriginCheck requests /.well-known/kantsites-origin under the
  new address; it has to return an HMAC of APP_SECRET that only this
  installation knows. Switching to www without DNS or certificate would
  otherwise send every page, the administration included, nowhere. "Save
  anyway" appears only after a refusal. The probe also answers during
  maintenance.
- The message says when the administration changes host and needs a new
  sign-in, since the session cookie belongs to one spelling.

Tests: 584 PHPUnit on PHP 8.2 and 8.4, 35 node, test:editor and
test:browser green (test:browser reports only the known editor colour
contrast on /grundlagen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-21 08:36:38 +02:00
1 parent e21e800f2c
commit 3b1dfc0948
10 files changed
+238 -7

No files matched your search

+61 -1
View File
@@ -2,10 +2,13 @@
namespace App\Controller;
use App\Command\SiteOriginCommand;
use App\Domain\InvalidPage;
use App\Ops\ArchiveUpdate;
use App\Ops\Backup;
use App\Ops\Environment;
use App\Ops\Installation;
use App\Ops\OriginCheck;
use App\Ops\Updater;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -21,7 +24,7 @@ use Symfony\Component\Routing\Attribute\Route;
*/
final class SystemController extends AbstractController
{
public function __construct(private Updater $updater, private Backup $backup, private ArchiveUpdate $archive, private Installation $installation, private string $adminPath, private string $databaseUrl, private string $mediaDirectory, private string $projectDir, private string $siteOrigin)
public function __construct(private Updater $updater, private Backup $backup, private ArchiveUpdate $archive, private Installation $installation, private string $adminPath, private string $databaseUrl, private string $mediaDirectory, private string $projectDir, private string $siteOrigin, private OriginCheck $originCheck)
{
}
@@ -55,6 +58,7 @@ final class SystemController extends AbstractController
// ein ganz anderer Host (Vorschauadresse des Hosters, IP) bleibt erreichbar und
// bekommt hier den Hinweis, dass die Website woanders eingetragen ist.
'origin' => $this->originFacts($request),
'originRejected' => $request->getSession()->remove('origin_rejected'),
]);
}
@@ -258,6 +262,62 @@ final class SystemController extends AbstractController
: 'Der Wartungszustand ist aufgehoben.', 'success');
}
/**
* Die Adresse der Website, wie „WordPress-Adresse“ in WordPress: sie landet in .env als
* SITE_ORIGIN und gilt ab der nächsten Anfrage.
*
* Vorher fragt die Installation sich selbst unter der neuen Adresse an. Eine Umstellung auf
* www ohne DNS-Eintrag leitete sonst jede Seite ins Leere, die Verwaltung eingeschlossen.
* Wer weiß, was er tut (Server erreicht sich selbst nicht, http im lokalen Betrieb), kann die
* Prüfung ausdrücklich übergehen.
*/
#[Route('/%kant.admin_path%/system/adresse', name: 'admin_system_origin', methods: ['POST'])]
public function origin(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
if (!$this->isCsrfTokenValid('system', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
if ('.env' !== $this->environmentSource('SITE_ORIGIN')) {
return $this->back('Die Adresse ist in der Server-Umgebung gesetzt und hat dort Vorrang. Sie lässt sich nur beim Hoster ändern.');
}
try {
$neu = SiteOriginCommand::normalize($request->request->getString('origin'));
} catch (\InvalidArgumentException $error) {
return $this->back($error->getMessage());
}
if ($neu === rtrim(trim($this->siteOrigin), '/')) {
return $this->back('Die Adresse ist unverändert.', 'success');
}
if (!$request->request->getBoolean('force') && null !== $problem = $this->originCheck->problem($neu)) {
$request->getSession()->set('origin_rejected', $neu);
return $this->back($problem.' Nichts geändert.');
}
try {
Environment::write($this->projectDir, ['SITE_ORIGIN' => $neu]);
} catch (\RuntimeException $error) {
return $this->back($error->getMessage());
}
// Die Anmeldung gilt nur für die Schreibweise, unter der sie entstand. Wechselt die
// Verwaltung dadurch den Host, ist dort eine neue Anmeldung fällig - das sagt die Meldung,
// solange sie noch auf dem alten Host steht.
return $this->back(sprintf(
'Die Website ist jetzt eingetragen als %s. Canonical, Sitemap und Feed nennen diese Adresse, die Schreibweise %s www leitet dorthin um.%s',
$neu,
str_starts_with((string) parse_url($neu, PHP_URL_HOST), 'www.') ? 'ohne' : 'mit',
strtolower($request->getHost()) !== strtolower((string) parse_url($neu, PHP_URL_HOST)) ? ' Unter der neuen Adresse ist eine neue Anmeldung nötig.' : '',
), 'success');
}
/** Die Gegenstelle zur Prüfung oben: öffentlich, und sie verrät nichts als diese Kennung. */
#[Route(OriginCheck::PATH, name: 'site_origin_probe', methods: ['GET'], priority: 10)]
public function originProbe(): Response
{
return new Response($this->originCheck->token(), 200, ['Content-Type' => 'text/plain; charset=UTF-8', 'Cache-Control' => 'no-store', 'X-Robots-Tag' => 'noindex']);
}
private function back(string $message, string $type = 'error'): Response
{
$this->addFlash($type, $message);