Social profiles are maintained once under settings (one HTTPS address per network, SocialProfiles::NETWORKS) and shown by the new `social` block (document schema v23) wherever it stands: size, shape, layout, labels, new tab and an optional subset of networks on the block; colour, hover, spacing and alignment through the free block styles. The addresses also feed sameAs in the structured data. Icons are own outline drawings in the style of the existing icon set. ADR 093. Elektro Bernhard is rebuilt with a dark hero over the team photo, a transparent header that turns white on scroll, Lato, cards with shadows and hover lift, a review slider with six real Google reviews, a dark solar band, page headers on every subpage and staggered entrance effects. Social profiles sit in the top bar and the footer. The site is a second local reference installation next to the Kant-IT pilot (var/elektro-bernhard.db, launch config on port 8088); the seed script rebuilds it from scratch. Checked: PHP suite 492 tests (only the documented old failure remains), 33 JS tests, Twig lint, PHP style, full editor browser check with axe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
187 lines
8.3 KiB
PHP
187 lines
8.3 KiB
PHP
<?php
|
||
|
||
namespace App\Controller;
|
||
|
||
use App\Content\Consent;
|
||
use App\Content\DemoContent;
|
||
use App\Content\MediaLibrary;
|
||
use App\Content\NotFoundPage;
|
||
use App\Content\PageRepository;
|
||
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 MediaLibrary $media, private Theme $theme, private NotFoundPage $notFoundPage)
|
||
{
|
||
}
|
||
|
||
#[Route('/%kant.admin_path%/settings', name: 'admin_settings', methods: ['GET'])]
|
||
public function index(Request $request): Response
|
||
{
|
||
$choosable = [];
|
||
foreach ($this->pages->listForEditor() as $page) {
|
||
// 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' => $this->settings->load(),
|
||
'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,
|
||
// Für das Symbol der Website: die Mediathek zur Auswahl, die neuesten zuerst.
|
||
'images' => $this->media->page(1, false, 96)['items'],
|
||
// Fuer die Farbfelder: dieselbe Palette, die auch der Editor anbietet.
|
||
'palette' => $this->theme->load()['colors'],
|
||
'socialNetworks' => SocialProfiles::NETWORKS,
|
||
'socialPlaceholders' => SocialProfiles::PLACEHOLDERS,
|
||
]);
|
||
}
|
||
|
||
#[Route('/%kant.admin_path%/settings/company', name: 'admin_settings_save', methods: ['POST'])]
|
||
public function save(Request $request): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
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_EDITOR');
|
||
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_EDITOR');
|
||
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');
|
||
}
|
||
}
|