php bin/console cms:admin:path <neu> legt die Verwaltung auf eine andere Adresse. Geprueft wird, dass der Name frei ist und keine Seite verdeckt; der Wert steht in .env.local und fuellt einen Parameter, der als Praefix in jeder Verwaltungsroute steht, in der Firewall, in den Schutzkopfzeilen und in der Statistik. Die robots.txt nennt einen geaenderten Namen nicht - sie ist oeffentlich und waere sonst ein Wegweiser. Beim Standardnamen bleibt die Zeile, weil diese Adresse ohnehin jeder kennt. Die Auffangroute fuer Seiten steht jetzt zuletzt: sie kann einen frei gewaehlten Namen nicht per Regex ausschliessen, und die Reihenfolge ist ohnehin die richtige. Der Befehl raeumt den Zwischenspeicher auf, aber nicht den, in dem er selbst laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
5.4 KiB
PHP
127 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Command;
|
|
|
|
use Doctrine\DBAL\Connection;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
|
|
/**
|
|
* Ändert die Adresse der Verwaltung.
|
|
*
|
|
* `/admin` ist die erste Adresse, die ein Scanner probiert, und jeder Anmeldeversuch, der dort
|
|
* ankommt, kostet Rechenzeit und Logzeilen. Heißt die Verwaltung anders, findet sie niemand, der
|
|
* sie nicht kennt. Das ersetzt kein gutes Passwort - es nimmt der Maschinerie nur das Ziel.
|
|
*
|
|
* Der Wert steht in `.env.local` und nicht in der Datenbank: die Routen stehen fest, bevor die
|
|
* erste Datenbankabfrage läuft. Deshalb dieser Befehl - er schreibt die Datei, prüft vorher, dass
|
|
* die Adresse benutzbar ist, und räumt den Zwischenspeicher auf, damit die neue Adresse sofort
|
|
* gilt.
|
|
*/
|
|
#[AsCommand(name: 'cms:admin:path', description: 'Zeigt die Adresse der Verwaltung oder ändert sie.')]
|
|
final class AdminPathCommand extends Command
|
|
{
|
|
/** Adressen, die schon vergeben sind oder es werden könnten. */
|
|
private const TABU = ['assets', 'media', 'install', 'einwilligung', 'formular', 'kategorie', 'schlagwort', 'beitraege', 'feed', 'sitemap', 'robots', 'llms', 'favicon'];
|
|
|
|
public function __construct(private Connection $db, private string $projectDir, private string $adminPath, private string $environment)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->addArgument('pfad', InputArgument::OPTIONAL, 'Die neue Adresse, ohne Schrägstrich. Ohne Angabe wird die aktuelle gezeigt.');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
$neu = trim((string) $input->getArgument('pfad'), " \t/");
|
|
if ('' === $neu) {
|
|
$io->writeln(sprintf('Die Verwaltung liegt unter <info>/%s</info>.', $this->adminPath));
|
|
$io->writeln('Ändern: <comment>php bin/console cms:admin:path meine-verwaltung</comment>');
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
if (1 !== preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/D', $neu) || strlen($neu) > 40) {
|
|
$io->error('Erlaubt sind Kleinbuchstaben, Ziffern und Bindestriche, höchstens 40 Zeichen.');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
if (in_array($neu, self::TABU, true)) {
|
|
$io->error(sprintf('„%s" ist für die Website selbst reserviert.', $neu));
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
// Eine Seite mit dieser Adresse wäre von der Verwaltung verdeckt - unauffindbar, ohne dass
|
|
// jemand merkt, warum.
|
|
$seite = $this->db->fetchOne('SELECT path FROM page WHERE website_id = ? AND path = ? AND deleted_at IS NULL', ['default', $neu]);
|
|
if (is_string($seite)) {
|
|
$io->error(sprintf('Es gibt bereits eine Seite unter /%s. Die Verwaltung würde sie verdecken.', $neu));
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$datei = $this->projectDir.'/.env.local';
|
|
$inhalt = is_file($datei) ? (string) file_get_contents($datei) : '';
|
|
$zeile = sprintf('ADMIN_PATH=%s', $neu);
|
|
$inhalt = 1 === preg_match('/^ADMIN_PATH=.*$/m', $inhalt)
|
|
? (string) preg_replace('/^ADMIN_PATH=.*$/m', $zeile, $inhalt)
|
|
: rtrim($inhalt)."\n".$zeile."\n";
|
|
if (false === @file_put_contents($datei, ltrim($inhalt, "\n"))) {
|
|
$io->error('.env.local ist nicht beschreibbar. Trage dort von Hand ein: '.$zeile);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
// Ohne das Aufräumen gälte die alte Adresse weiter - und die neue wäre nicht erreichbar.
|
|
// Die Umgebung, in der dieser Befehl gerade läuft, bleibt dabei unangetastet: Symfony lädt
|
|
// Dienste nach, und wer sich den Boden unter den Füßen wegräumt, endet mit einem Fehler,
|
|
// obwohl alles geklappt hat. Der nächste Aufruf baut sie ohnehin neu.
|
|
$offen = [];
|
|
foreach (glob($this->projectDir.'/var/cache/*') ?: [] as $pfad) {
|
|
if (!is_dir($pfad)) {
|
|
continue;
|
|
}
|
|
if (basename($pfad) === $this->environment) {
|
|
$offen[] = basename($pfad);
|
|
|
|
continue;
|
|
}
|
|
$this->removeDirectory($pfad);
|
|
}
|
|
|
|
$io->success(sprintf('Die Verwaltung liegt jetzt unter /%s.', $neu));
|
|
if ([] !== $offen) {
|
|
$io->writeln(sprintf('Falls die Website noch die alte Adresse kennt: <comment>php bin/console cache:clear --env=%s</comment>', implode(' --env=', $offen)));
|
|
}
|
|
if ($this->adminPath !== $neu) {
|
|
$io->writeln('Merk dir die Adresse: <comment>/'.$this->adminPath.'</comment> antwortet ab sofort mit 404, wie jede andere unbekannte Adresse auch.');
|
|
}
|
|
$io->writeln('Zurück geht es mit: <comment>php bin/console cms:admin:path admin</comment>');
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
private function removeDirectory(string $pfad): void
|
|
{
|
|
if (!is_dir($pfad)) {
|
|
return;
|
|
}
|
|
foreach (scandir($pfad) ?: [] as $eintrag) {
|
|
if ('.' === $eintrag || '..' === $eintrag) {
|
|
continue;
|
|
}
|
|
$voll = $pfad.'/'.$eintrag;
|
|
is_dir($voll) ? $this->removeDirectory($voll) : @unlink($voll);
|
|
}
|
|
@rmdir($pfad);
|
|
}
|
|
}
|