Files
KantSites/src/Controller/SettingsController.php
T
kantarciandClaude Opus 5.5 f2c04b311f Add business type, location and logo to the structured data
Three optional settings extend LocalBusiness: a subtype from a fixed list, coordinates as a pair, and a logo from the media library. Without them the output stays as it was, and stored settings need no migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:02:38 +01:00

250 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controller;
use App\Content\Consent;
use App\Content\DemoContent;
use App\Content\Languages;
use App\Content\NotFoundPage;
use App\Content\PageRepository;
use App\Content\PostRepository;
use App\Content\RedirectRepository;
use App\Content\SiteSettings;
use App\Content\SocialProfiles;
use App\Content\Theme;
use App\Domain\InvalidPage;
use App\Domain\StaleWrite;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class SettingsController extends AbstractController
{
public function __construct(private SiteSettings $settings, private RedirectRepository $redirects, private PageRepository $pages, private Theme $theme, private NotFoundPage $notFoundPage, private Languages $languages, private PostRepository $posts)
{
}
#[Route('/%kant.admin_path%/settings', name: 'admin_settings', methods: ['GET'])]
public function index(Request $request): Response
{
// Alles hier wirkt sofort auf die Website: 404-Seite und Weiterleitungen sind Sache des
// Veroeffentlichers, die Unternehmensdaten samt Einwilligung und Spamschutz die des
// Administrators - der Bildschirm blendet den Teil aus, den die Rolle nicht aendern darf.
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
$settings = $this->settings->load();
$choosable = [];
// Datenschutz und Impressum werden per Suche gewählt; das Feld zeigt Titel und Adresse der
// gespeicherten Seite. Eine Adresse ohne Seite bleibt sichtbar, statt still zu verschwinden.
$legal = ['privacyPath' => null, 'imprintPath' => null];
foreach (array_keys($legal) as $key) {
if ('' !== $settings[$key]) {
$legal[$key] = ['path' => $settings[$key], 'title' => $settings[$key]];
}
}
foreach ($this->pages->listForEditor() as $page) {
foreach ($legal as $key => $selection) {
if (null !== $selection && $selection['path'] === $page['path']) {
$legal[$key]['title'] = json_decode($page['document'] ?? '{}', true, 512, JSON_THROW_ON_ERROR)['title'] ?? $page['path'];
}
}
// Only a published page can serve as the root; a draft would answer every visit with 404.
if ('page' === $page['type'] && null !== $page['published_revision_id']) {
$document = json_decode($page['document'] ?? '{}', true, 512, JSON_THROW_ON_ERROR);
$choosable[] = ['path' => $page['path'], 'title' => $document['title'] ?? $page['path']];
}
}
return $this->render('admin/settings.html.twig', [
'settings' => $settings,
'businessTypes' => SiteSettings::BUSINESS_TYPES,
'legalSelection' => $legal,
'notFoundSelection' => $this->notFoundPage->selected(),
'redirects' => $this->redirects->all($request->query->getInt('seite', 1)),
'choosableHome' => $choosable,
'consentGroups' => Consent::GROUPS,
'missingLegal' => $this->settings->missingLegalPages(),
'consentGroupHelp' => Consent::GROUP_HELP,
// Fuer die Farbfelder: dieselbe Palette, die auch der Editor anbietet.
'palette' => $this->theme->load()['colors'],
'socialNetworks' => SocialProfiles::NETWORKS,
'weekdays' => SiteSettings::DAYS,
'socialPlaceholders' => SocialProfiles::PLACEHOLDERS,
'mainLanguage' => $this->languages->describe($this->languages->main()),
'extraLanguages' => $this->languages->extra(),
'terms' => [] === $this->languages->extra() ? [] : $this->posts->allTerms(),
'languageCatalog' => array_map(fn (string $code): array => $this->languages->describe($code), array_values(array_diff(array_keys(Languages::CATALOG), [$this->languages->main()]))),
]);
}
/**
* Die weiteren Sprachen (ADR 111). Eine Sprache dazunehmen ändert an der Website nichts,
* solange keine Seite übersetzt und veröffentlicht ist; eine Sprache wegnehmen nimmt ihre
* Seiten vom Netz, lässt die Übersetzungen aber in den Seiten liegen.
*/
/** Kategorien, Schlagwörter und Einwilligungshinweis in den weiteren Sprachen (ADR 111). */
#[Route('/%kant.admin_path%/settings/sprachen/texte', name: 'admin_language_texts_save', methods: ['POST'])]
public function saveLanguageTexts(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
if (!$this->isCsrfTokenValid('settings', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
$all = $request->request->all();
$this->settings->saveLanguageTexts(is_array($all['termNames'] ?? null) ? $all['termNames'] : [], is_array($all['consentTexts'] ?? null) ? $all['consentTexts'] : []);
$this->addFlash('success', 'Die Übersetzungen wurden gespeichert.');
return $this->redirectToRoute('admin_settings', ['_fragment' => 'sprachen']);
}
#[Route('/%kant.admin_path%/settings/sprachen', name: 'admin_languages_save', methods: ['POST'])]
public function saveLanguages(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
if (!$this->isCsrfTokenValid('settings', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
try {
$this->languages->save(array_values(array_map('strval', (array) ($request->request->all()['languages'] ?? []))));
} catch (InvalidPage $e) {
$this->addFlash('error', $e->getMessage());
return $this->redirectToRoute('admin_settings', ['_fragment' => 'sprachen']);
}
$this->addFlash('success', [] === $this->languages->extra() ? 'Die Website ist einsprachig.' : 'Die Sprachen wurden gespeichert. Übersetzt wird im Editor jeder Seite über den Sprachumschalter oben.');
return $this->redirectToRoute('admin_settings', ['_fragment' => 'sprachen']);
}
#[Route('/%kant.admin_path%/settings/company', name: 'admin_settings_save', methods: ['POST'])]
public function save(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
if (!$this->isCsrfTokenValid('settings', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
try {
$this->settings->save($request->request->all());
} catch (InvalidPage $e) {
return $this->back($e->getMessage());
}
return $this->back('Die Unternehmensdaten wurden gespeichert.', 'success');
}
#[Route('/%kant.admin_path%/settings/not-found', name: 'admin_not_found_save', methods: ['POST'])]
public function saveNotFound(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
if (!$this->isCsrfTokenValid('settings', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
try {
$expectedId = $request->request->getString('expectedId');
if ('create' === $request->request->getString('action')) {
$id = $this->notFoundPage->create($expectedId);
$this->addFlash('success', 'Die 404-Vorlage wurde als Entwurf angelegt und zugewiesen. Nach dem Veröffentlichen ersetzt sie die Standard-404.');
return $this->redirectToRoute('admin_editor', ['id' => $id]);
}
$id = $request->request->getString('notFoundPageId');
$this->settings->assignNotFoundPage($id, $expectedId);
$selection = $this->notFoundPage->selected();
$message = '' === $id ? 'Die Standard-404 im Website-Design ist wieder aktiv.'
: ($selection['published'] ? 'Die 404-Seite wurde zugewiesen.' : 'Die Seite wurde zugewiesen. Bis zu ihrer Veröffentlichung erscheint die Standard-404.');
$this->addFlash('success', $message);
} catch (InvalidPage|StaleWrite $error) {
$this->addFlash('error', $error->getMessage());
}
return $this->redirectToRoute('admin_settings', ['_fragment' => 'not-found']);
}
#[Route('/%kant.admin_path%/settings/redirects', name: 'admin_redirect_save', methods: ['POST'])]
public function saveRedirect(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
if (!$this->isCsrfTokenValid('settings', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
$remove = $request->request->getString('remove');
if ('' !== $remove) {
$this->redirects->delete($remove);
return $this->back('Die Weiterleitung wurde entfernt.', 'success');
}
try {
$this->redirects->save(
$request->request->getString('from'),
$request->request->getString('to'),
$request->request->getInt('status', 301),
);
} catch (InvalidPage $e) {
return $this->back($e->getMessage());
}
return $this->back('Die Weiterleitung wurde gespeichert.', 'success');
}
/**
* Creates the privacy statement and the imprint, with a skeleton whose gaps are marked.
*
* 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('/%kant.admin_path%/settings/rechtliches', name: 'admin_legal_pages', methods: ['POST'])]
public function legalPages(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
if (!$this->isCsrfTokenValid('settings', $request->request->getString('_csrf_token'))) {
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
}
$settings = $this->settings->load();
$created = [];
try {
foreach (DemoContent::LEGAL as $slug => $label) {
$key = 'datenschutz' === $slug ? 'privacyPath' : 'imprintPath';
// An existing page is never overwritten - it may already carry the real text.
$existing = null !== $this->pages->findPublished($slug) || $this->pageExists($slug);
if (!$existing) {
$id = $this->pages->createPage($slug, null, 'page', DemoContent::legalPage($slug));
$page = $this->pages->findForEditor($id);
$this->pages->publish($id, $page['draft_revision_id'], $page['version']);
$created[] = $label;
}
$settings[$key] = $slug;
}
$this->settings->save([...$settings, 'consentGroups' => $settings['consentGroups'], 'sameAs' => implode('
', $settings['sameAs'])]);
} catch (InvalidPage $e) {
return $this->back($e->getMessage());
}
return $this->back(
[] === $created
? 'Die Seiten waren schon vorhanden und sind jetzt verknüpft. Der Inhalt bleibt unverändert.'
: sprintf('%s angelegt und verknüpft. Beide Seiten sind veröffentlicht und müssen noch ausgefüllt werden – die Pflichtangaben sind darin markiert.', implode(' und ', $created)),
'success',
);
}
/** Also finds a page that exists but is not published yet. */
private function pageExists(string $path): bool
{
foreach ($this->pages->listForEditor() as $page) {
if ($path === $page['path']) {
return true;
}
}
return false;
}
private function back(string $message, string $type = 'error'): Response
{
$this->addFlash($type, $message);
return $this->redirectToRoute('admin_settings');
}
}