Die Verwaltung kann umziehen: /admin ist frei waehlbar
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>
This commit is contained in:
1 parent
9f1ac39251
commit
aaea385422
26 files changed
+293
-45
No files matched your search
@@ -0,0 +1,126 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
final class Statistics
|
||||
{
|
||||
/** Everything that is not a page of this site, or not a visit at all. */
|
||||
private const IGNORED = '#^/(admin|media/|assets/|einwilligung$|feed\.xml$|sitemap\.xml$|robots\.txt$|llms\.txt$|favicon)#';
|
||||
private const IGNORED = '#^/(media/|assets/|einwilligung$|feed\.xml$|sitemap\.xml$|robots\.txt$|llms\.txt$|favicon)#';
|
||||
|
||||
/**
|
||||
* A heuristic, and nothing more: user agents lie. It keeps the obvious crawlers and monitors
|
||||
@@ -40,7 +40,7 @@ final class Statistics
|
||||
/** After two months the hours of a day are merged into one row. */
|
||||
public const COMPACT_AFTER_DAYS = 60;
|
||||
|
||||
public function __construct(private Connection $db)
|
||||
public function __construct(private Connection $db, private string $adminPath = 'admin')
|
||||
{
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ final class Statistics
|
||||
&& in_array($status, [200, 404], true)
|
||||
&& str_contains($contentType, 'text/html')
|
||||
&& 1 !== preg_match(self::IGNORED, $path)
|
||||
// Die Verwaltung ist kein Besuch - unabhaengig davon, wie sie heisst.
|
||||
&& !str_starts_with($path, '/'.$this->adminPath)
|
||||
&& 1 !== preg_match(self::MACHINES, $userAgent);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
#[Route('/admin')]
|
||||
#[Route('/%kant.admin_path%')]
|
||||
final class EditorController extends AbstractController
|
||||
{
|
||||
public function __construct(private PageRepository $pages, private DocumentValidator $validator, private MenuRepository $menus, private PostRepository $posts, private Theme $theme, private StyleCompiler $styles, private RedirectRepository $redirects, private PartialRepository $partials, private CustomCode $code, private CsrfTokenManagerInterface $csrf)
|
||||
|
||||
@@ -16,7 +16,7 @@ final class FormAdminController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/forms', name: 'admin_forms', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/forms', name: 'admin_forms', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
return $this->render('admin/forms.html.twig', [
|
||||
@@ -26,7 +26,7 @@ final class FormAdminController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/forms/{slug}', name: 'admin_form_save', requirements: ['slug' => '[a-z0-9-]{1,80}|neu'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/forms/{slug}', name: 'admin_form_save', requirements: ['slug' => '[a-z0-9-]{1,80}|neu'], methods: ['POST'])]
|
||||
public function save(Request $request, string $slug): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -18,7 +18,7 @@ final class MediaController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/media', name: 'admin_media', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/media', name: 'admin_media', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
// The screen renders the images itself, page by page. The dialog stays what it is: the way
|
||||
@@ -31,7 +31,7 @@ final class MediaController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/media/upload', name: 'admin_media_upload', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/media/upload', name: 'admin_media_upload', methods: ['POST'])]
|
||||
public function formUpload(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -57,7 +57,7 @@ final class MediaController extends AbstractController
|
||||
return $this->back($request, sprintf('%d Bild(er) hochgeladen.', $done), 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/media/{id}', name: 'admin_media_save', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/media/{id}', name: 'admin_media_save', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
public function formUpdate(Request $request, string $id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -95,7 +95,7 @@ final class MediaController extends AbstractController
|
||||
return $this->redirectToRoute('admin_media', $query);
|
||||
}
|
||||
|
||||
#[Route('/admin/api/media', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/api/media', methods: ['GET'])]
|
||||
public function list(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -103,8 +103,8 @@ final class MediaController extends AbstractController
|
||||
return $this->json(['items' => $this->media->list(), 'csrfToken' => $this->csrf->getToken('media')->getValue()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/api/media/upload', methods: ['POST'])]
|
||||
#[Route('/admin/api/media/{id}', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/api/media/upload', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/api/media/{id}', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
public function write(Request $request, ?string $id = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -17,7 +17,7 @@ final class MenuController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/menus', name: 'admin_menus', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/menus', name: 'admin_menus', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$menus = $this->menus->all();
|
||||
@@ -39,7 +39,7 @@ final class MenuController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/menus/{slot}', name: 'admin_menu_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/menus/{slot}', name: 'admin_menu_save', methods: ['POST'])]
|
||||
public function save(Request $request, string $slot): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -25,7 +25,7 @@ use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
* against areas without a second implementation. Areas have no draft and no publication: they are
|
||||
* shared, so a saved change is live. The editor hides the publish button for them.
|
||||
*/
|
||||
#[Route('/admin/partials')]
|
||||
#[Route('/%kant.admin_path%/partials')]
|
||||
final class PartialController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
@@ -9,7 +9,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
final class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/admin/login', name: 'admin_login', methods: ['GET', 'POST'])]
|
||||
#[Route('/%kant.admin_path%/login', name: 'admin_login', methods: ['GET', 'POST'])]
|
||||
public function login(AuthenticationUtils $auth): Response
|
||||
{
|
||||
if ($this->getUser()) {
|
||||
@@ -19,7 +19,7 @@ final class SecurityController extends AbstractController
|
||||
return $this->render('admin/login.html.twig', ['error' => $auth->getLastAuthenticationError(), 'username' => $auth->getLastUsername()]);
|
||||
}
|
||||
|
||||
#[Route('/admin/logout', name: 'admin_logout', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/logout', name: 'admin_logout', methods: ['POST'])]
|
||||
public function logout(): never
|
||||
{
|
||||
throw new \LogicException('Handled by Symfony Security.');
|
||||
|
||||
@@ -21,6 +21,7 @@ final class SeoController extends AbstractController
|
||||
private SiteSettings $settings,
|
||||
private PostRepository $posts,
|
||||
private string $siteOrigin,
|
||||
private string $adminPath,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -58,7 +59,15 @@ final class SeoController extends AbstractController
|
||||
public function robots(): Response
|
||||
{
|
||||
$settings = $this->settings->load();
|
||||
$lines = ['User-agent: *', 'Disallow: /admin', 'Disallow: /formular/'];
|
||||
// Die Verwaltung wird hier nur genannt, solange sie „admin" heisst - diese Adresse kennt
|
||||
// ohnehin jeder Scanner. Wer sie umbenannt hat, will sie nicht in einer oeffentlichen
|
||||
// Datei wiederfinden: robots.txt ist das erste, was ein Angreifer liest. Vom Indexieren
|
||||
// haelt die Verwaltung ihr eigener Kopf `X-Robots-Tag: noindex` ab.
|
||||
$lines = ['User-agent: *'];
|
||||
if ('admin' === $this->adminPath) {
|
||||
$lines[] = 'Disallow: /admin';
|
||||
}
|
||||
$lines[] = 'Disallow: /formular/';
|
||||
if ('' !== $settings['robotsExtra']) {
|
||||
$lines[] = $settings['robotsExtra'];
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ final class SettingsController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/settings', name: 'admin_settings', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/settings', name: 'admin_settings', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$choosable = [];
|
||||
@@ -44,7 +44,7 @@ final class SettingsController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/settings/company', name: 'admin_settings_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/settings/company', name: 'admin_settings_save', methods: ['POST'])]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -60,7 +60,7 @@ final class SettingsController extends AbstractController
|
||||
return $this->back('Die Unternehmensdaten wurden gespeichert.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/settings/redirects', name: 'admin_redirect_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/settings/redirects', name: 'admin_redirect_save', methods: ['POST'])]
|
||||
public function saveRedirect(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -92,7 +92,7 @@ final class SettingsController extends AbstractController
|
||||
* The pages are mandatory, the text is not something a CMS can write: what gets created is the
|
||||
* structure with every required item named and marked as still to be filled in.
|
||||
*/
|
||||
#[Route('/admin/settings/rechtliches', name: 'admin_legal_pages', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/settings/rechtliches', name: 'admin_legal_pages', methods: ['POST'])]
|
||||
public function legalPages(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -17,7 +17,7 @@ final class StatisticsController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/statistik', name: 'admin_statistics', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/statistik', name: 'admin_statistics', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -32,7 +32,7 @@ final class StatisticsController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/statistik.csv', name: 'admin_statistics_csv', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/statistik.csv', name: 'admin_statistics_csv', methods: ['GET'])]
|
||||
public function csv(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -19,11 +19,11 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
*/
|
||||
final class SystemController extends AbstractController
|
||||
{
|
||||
public function __construct(private Updater $updater, private Backup $backup)
|
||||
public function __construct(private Updater $updater, private Backup $backup, private string $adminPath)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/system', name: 'admin_system', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/system', name: 'admin_system', methods: ['GET'])]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -38,10 +38,11 @@ final class SystemController extends AbstractController
|
||||
'asked' => $request->query->getBoolean('pruefen'),
|
||||
'steps' => $request->getSession()->remove('update_steps') ?: [],
|
||||
'backups' => $this->backup->all(),
|
||||
'adminPath' => $this->adminPath,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/system/update', name: 'admin_system_update', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/system/update', name: 'admin_system_update', methods: ['POST'])]
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -69,7 +70,7 @@ final class SystemController extends AbstractController
|
||||
* `cms:restore` is the way, and the list says which backups exist and whether their checksum
|
||||
* still matches.
|
||||
*/
|
||||
#[Route('/admin/system/sicherung', name: 'admin_system_backup', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/system/sicherung', name: 'admin_system_backup', methods: ['POST'])]
|
||||
public function backup(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -94,7 +95,7 @@ final class SystemController extends AbstractController
|
||||
), 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/system/wartung', name: 'admin_system_maintenance', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/system/wartung', name: 'admin_system_maintenance', methods: ['POST'])]
|
||||
public function maintenance(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
@@ -16,7 +16,7 @@ final class ThemeController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/theme', name: 'admin_theme', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/theme', name: 'admin_theme', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$theme = $this->theme->load();
|
||||
@@ -36,7 +36,7 @@ final class ThemeController extends AbstractController
|
||||
* Auf Zuruf, nicht bei einem Seitenaufruf: das dauert Sekunden und braucht eine Verbindung
|
||||
* nach aussen, die ein Besucher nie abwarten soll.
|
||||
*/
|
||||
#[Route('/admin/theme/fonts', name: 'admin_theme_fonts', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/theme/fonts', name: 'admin_theme_fonts', methods: ['POST'])]
|
||||
public function fonts(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -65,7 +65,7 @@ final class ThemeController extends AbstractController
|
||||
return $bytes < 1024 * 1024 ? round($bytes / 1024).' KB' : round($bytes / 1024 / 1024, 1).' MB';
|
||||
}
|
||||
|
||||
#[Route('/admin/theme/save', name: 'admin_theme_save', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/theme/save', name: 'admin_theme_save', methods: ['POST'])]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -22,7 +22,7 @@ final class UserController extends AbstractController
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer', name: 'admin_users', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/benutzer', name: 'admin_users', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
@@ -37,7 +37,7 @@ final class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/neu', name: 'admin_user_create', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/benutzer/neu', name: 'admin_user_create', methods: ['POST'])]
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
if (null !== $refusal = $this->guard($request)) {
|
||||
@@ -56,7 +56,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Das Konto wurde angelegt.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/{id}/rolle', name: 'admin_user_role', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/benutzer/{id}/rolle', name: 'admin_user_role', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
public function role(Request $request, string $id): Response
|
||||
{
|
||||
if (null !== $refusal = $this->guard($request)) {
|
||||
@@ -71,7 +71,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Die Rolle wurde geändert. Offene Sitzungen dieses Kontos sind damit abgemeldet.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/{id}/passwort', name: 'admin_user_password', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/benutzer/{id}/passwort', name: 'admin_user_password', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
public function password(Request $request, string $id): Response
|
||||
{
|
||||
if (null !== $refusal = $this->guard($request)) {
|
||||
@@ -86,7 +86,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Das Passwort wurde gesetzt. Offene Sitzungen dieses Kontos sind damit abgemeldet.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/benutzer/{id}/loeschen', name: 'admin_user_delete', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/benutzer/{id}/loeschen', name: 'admin_user_delete', requirements: ['id' => '[a-zA-Z0-9_-]{1,64}'], methods: ['POST'])]
|
||||
public function delete(Request $request, string $id): Response
|
||||
{
|
||||
if (null !== $refusal = $this->guard($request)) {
|
||||
@@ -101,7 +101,7 @@ final class UserController extends AbstractController
|
||||
return $this->back('Das Konto wurde gelöscht.', 'success');
|
||||
}
|
||||
|
||||
#[Route('/admin/konto', name: 'admin_account', methods: ['GET'])]
|
||||
#[Route('/%kant.admin_path%/konto', name: 'admin_account', methods: ['GET'])]
|
||||
public function account(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
@@ -122,7 +122,7 @@ final class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/konto/passwort', name: 'admin_account_password', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/konto/passwort', name: 'admin_account_password', methods: ['POST'])]
|
||||
public function ownPassword(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
@@ -8,6 +8,10 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
final class AdminHeadersSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private string $adminPath)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [KernelEvents::RESPONSE => ['onResponse', -300]];
|
||||
@@ -15,7 +19,7 @@ final class AdminHeadersSubscriber implements EventSubscriberInterface
|
||||
|
||||
public function onResponse(ResponseEvent $event): void
|
||||
{
|
||||
if (!preg_match('#^/admin(?:/|$)#', $event->getRequest()->getPathInfo())) {
|
||||
if (!preg_match('#^/'.preg_quote($this->adminPath, '#').'(?:/|$)#', $event->getRequest()->getPathInfo())) {
|
||||
return;
|
||||
}
|
||||
$headers = $event->getResponse()->headers;
|
||||
|
||||
@@ -19,7 +19,7 @@ use Twig\Environment;
|
||||
*/
|
||||
final class MaintenanceSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private Environment $twig, private string $projectDir)
|
||||
public function __construct(private Environment $twig, private string $projectDir, private string $adminPath)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class MaintenanceSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
$path = $event->getRequest()->getPathInfo();
|
||||
if (preg_match('#^/(admin|assets/|media/|favicon)#', $path)) {
|
||||
if (preg_match('#^/('.preg_quote($this->adminPath, '#').'|assets/|media/|favicon)#', $path)) {
|
||||
return;
|
||||
}
|
||||
$since = @file_get_contents($this->projectDir.'/'.Updater::MAINTENANCE_FILE);
|
||||
|
||||
Reference in new issue
Block a user