Cards, quotes and call-to-action bands side by side in a row are equal in height again: the stacking margin meant for a column no longer applies inside a row. The header can carry a second set of colours for the scrolled page, the Flatsome transparent header: a "scrolled" state next to "hover", limited to colours, shadow and opacity, marked by a small script only where it is used, plus a setting that lets the header float over the first section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
483 lines
23 KiB
PHP
483 lines
23 KiB
PHP
<?php
|
||
|
||
namespace App\Controller;
|
||
|
||
use App\Content\CustomCode;
|
||
use App\Content\DemoContent;
|
||
use App\Content\DocumentValidator;
|
||
use App\Content\MenuRepository;
|
||
use App\Content\PageRepository;
|
||
use App\Content\PartialRepository;
|
||
use App\Content\PostLayouts;
|
||
use App\Content\PostRepository;
|
||
use App\Content\RedirectRepository;
|
||
use App\Content\StyleCompiler;
|
||
use App\Content\StyleSchema;
|
||
use App\Content\Theme;
|
||
use App\Domain\InvalidDocument;
|
||
use App\Domain\InvalidPage;
|
||
use App\Domain\StaleWrite;
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||
use Symfony\Component\HttpFoundation\Request;
|
||
use Symfony\Component\HttpFoundation\RequestStack;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
use Symfony\Component\Routing\Attribute\Route;
|
||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||
|
||
#[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, private RequestStack $requests)
|
||
{
|
||
}
|
||
|
||
#[Route('', name: 'admin_pages', methods: ['GET'])]
|
||
#[Route('/posts', name: 'admin_posts', methods: ['GET'])]
|
||
public function index(Request $request): Response
|
||
{
|
||
$type = 'admin_posts' === $request->attributes->get('_route') ? 'post' : 'page';
|
||
$query = mb_substr(trim($request->query->getString('q')), 0, 160);
|
||
$status = $request->query->getString('status', 'all');
|
||
$status = in_array($status, ['all', 'draft', 'published', 'changed', 'trash'], true) ? $status : 'all';
|
||
$result = $this->pages->editorPage($type, $request->query->getInt('seite', 1), $request->query->getInt('limit', 20), $query, $status);
|
||
$pages = $result['items'];
|
||
$parentChoices = [];
|
||
foreach ($pages as &$page) {
|
||
$page['document'] = json_decode($page['document'] ?? '{}', true, 512, JSON_THROW_ON_ERROR);
|
||
$page['title'] = $page['document']['title'] ?? $page['slug'];
|
||
$page['depth'] = substr_count((string) $page['path'], '/');
|
||
$page['terms'] = 'post' === $page['type'] ? $this->posts->namesFor($page['id']) : ['category' => '', 'tag' => ''];
|
||
if ($page['parent_id'] && !isset($parentChoices[$page['parent_id']])) {
|
||
$parentChoices[$page['parent_id']] = $this->pages->choices(id: $page['parent_id'])[0] ?? null;
|
||
}
|
||
}
|
||
unset($page);
|
||
|
||
$view = [
|
||
'pages' => 'trash' === $status ? [] : $pages,
|
||
'trash' => 'trash' === $status ? array_map(static fn (array $p): array => [...$p, 'deletedAt' => $p['deleted_at']], $pages) : [],
|
||
'parentChoices' => $parentChoices, 'postLayouts' => PostLayouts::all(),
|
||
'contentType' => $type, 'result' => $result, 'query' => $query, 'status' => $status,
|
||
'listRoute' => 'post' === $type ? 'admin_posts' : 'admin_pages',
|
||
'areaChoices' => ['header' => $this->partials->choices('header'), 'footer' => $this->partials->choices('footer')],
|
||
];
|
||
if ($request->isXmlHttpRequest()) {
|
||
return $this->json([
|
||
'html' => $this->renderView('admin/content-results.html.twig', $view),
|
||
'total' => $result['total'],
|
||
'parameters' => ['q' => $query, 'status' => $status, 'limit' => $result['limit'], 'seite' => $result['page']],
|
||
]);
|
||
}
|
||
|
||
return $this->render('admin/pages.html.twig', $view);
|
||
}
|
||
|
||
#[Route('/api/content-choices', name: 'admin_content_choices', methods: ['GET'])]
|
||
public function contentChoices(Request $request): JsonResponse
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
|
||
return $this->json(['items' => $this->pages->choices(
|
||
mb_substr($request->query->getString('q'), 0, 160),
|
||
$request->query->getString('type'), $request->query->getBoolean('parents'),
|
||
)]);
|
||
}
|
||
|
||
#[Route('/pages', name: 'admin_create', methods: ['POST'])]
|
||
public function create(Request $request): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
$title = trim($request->request->getString('title'));
|
||
if ('' === $title || mb_strlen($title) > 160) {
|
||
return $this->back('Bitte gib einen Titel mit höchstens 160 Zeichen an.');
|
||
}
|
||
$type = $request->request->getString('type', 'page');
|
||
$parentId = $request->request->getString('parent_id') ?: null;
|
||
$slug = trim($request->request->getString('slug')) ?: $this->slugify($title);
|
||
try {
|
||
// Ein Beitrag beginnt mit einem Aufbau, eine Seite leer: bei einer Seite weiß
|
||
// niemand vorher, was daraus wird, bei einem Beitrag fast immer.
|
||
$layout = $request->request->getString('layout', PostLayouts::DEFAULT);
|
||
$document = 'post' === $type && PostLayouts::exists($layout)
|
||
? PostLayouts::document($layout, $title)
|
||
: DemoContent::starter($title, $type);
|
||
$id = $this->pages->createPage($slug, $parentId, $type, $document);
|
||
} catch (InvalidPage|InvalidDocument $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
$this->addFlash('success', sprintf('„%s“ wurde als Entwurf angelegt. Die Seite ist erst nach dem Veröffentlichen öffentlich.', $title));
|
||
|
||
return $this->redirectToRoute('admin_editor', ['id' => $id]);
|
||
}
|
||
|
||
/** The editor reads the style vocabulary and the palette from here, so both stay in one place. */
|
||
#[Route('/api/design', name: 'admin_design', methods: ['GET'])]
|
||
public function design(): JsonResponse
|
||
{
|
||
$theme = $this->theme->load();
|
||
|
||
return $this->json([
|
||
'properties' => StyleSchema::properties(),
|
||
'devices' => StyleSchema::DEVICES,
|
||
'sides' => StyleSchema::sides(),
|
||
'corners' => StyleSchema::corners(),
|
||
'hoverable' => StyleSchema::hoverable(),
|
||
'scrollable' => StyleSchema::scrollable(),
|
||
'colors' => $theme['colors'],
|
||
'fonts' => $theme['fonts'],
|
||
'presets' => $theme['presets'],
|
||
'base' => $theme['base'],
|
||
'buttons' => $theme['buttons'],
|
||
'buttonOptions' => \App\Content\ButtonDesign::OPTIONS,
|
||
'buttonPresets' => \App\Content\ButtonDesign::PRESETS,
|
||
'csrfToken' => $this->csrf->getToken('editor')->getValue(),
|
||
]);
|
||
}
|
||
|
||
/** Saves or removes a reusable style, so a look can be applied to other blocks and pages. */
|
||
#[Route('/api/style-presets', name: 'admin_style_preset', methods: ['POST'])]
|
||
public function stylePreset(Request $request): JsonResponse
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('editor', $request->headers->get('X-CSRF-Token'))) {
|
||
return $this->json(['error' => 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'], 403);
|
||
}
|
||
try {
|
||
$body = json_decode($request->getContent(), true, 16, JSON_THROW_ON_ERROR);
|
||
$style = $body['style'] ?? null;
|
||
$this->theme->savePreset((string) ($body['name'] ?? ''), is_array($style) ? $style : null, $this->styles);
|
||
} catch (InvalidPage|InvalidDocument $e) {
|
||
return $this->json(['error' => $e->getMessage()], 422);
|
||
} catch (\JsonException) {
|
||
return $this->json(['error' => 'Die Anfrage ist unvollständig.'], 422);
|
||
}
|
||
|
||
return $this->json(['presets' => $this->theme->load()['presets']]);
|
||
}
|
||
|
||
#[Route('/pages/{id}/meta', name: 'admin_post_meta', methods: ['POST'])]
|
||
public function metadata(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
$terms = static function (string $raw): array {
|
||
$names = array_filter(array_map(trim(...), explode(',', $raw)), static fn (string $n): bool => '' !== $n);
|
||
|
||
return array_values(array_unique($names));
|
||
};
|
||
try {
|
||
$this->posts->saveMetadata(
|
||
$id,
|
||
[
|
||
'excerpt' => $request->request->getString('excerpt'),
|
||
'author' => $request->request->getString('author'),
|
||
'publishedAt' => $request->request->getString('publishedAt'),
|
||
'featuredMediaId' => $request->request->getString('featuredMediaId'),
|
||
'ogMediaId' => $request->request->getString('ogMediaId'),
|
||
'seoTitle' => $request->request->getString('seoTitle'),
|
||
'robots' => $request->request->getString('robots', 'index'),
|
||
],
|
||
$terms($request->request->getString('categories')),
|
||
$terms($request->request->getString('tags')),
|
||
$request->request->getInt('expectedVersion', -1),
|
||
);
|
||
} catch (InvalidPage|StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back('Die Beitragsdaten wurden gespeichert.', 'success');
|
||
}
|
||
|
||
/** Which header and footer this page uses, including none at all. */
|
||
#[Route('/pages/{id}/bereiche', name: 'admin_page_areas', methods: ['POST'])]
|
||
public function areas(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
// An empty field means "the standard area"; that is stored as null, not as an address.
|
||
$choice = static fn (string $value): ?string => '' === $value ? null : $value;
|
||
try {
|
||
$this->pages->saveAreas(
|
||
$id,
|
||
$choice($request->request->getString('headerArea')),
|
||
$choice($request->request->getString('footerArea')),
|
||
$request->request->getInt('expectedVersion', -1),
|
||
);
|
||
} catch (InvalidPage|StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back('Kopf- und Fußbereich dieser Seite wurden gespeichert.', 'success');
|
||
}
|
||
|
||
#[Route('/pages/{id}/move', name: 'admin_move', methods: ['POST'])]
|
||
public function move(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
try {
|
||
$moved = $this->pages->movePage(
|
||
$id,
|
||
$request->request->getString('slug'),
|
||
$request->request->getString('parent_id') ?: null,
|
||
$request->request->getInt('expectedVersion', -1),
|
||
$this->redirects,
|
||
);
|
||
} catch (InvalidPage|StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back(0 === $moved
|
||
? 'Die Adresse ist unverändert.'
|
||
: sprintf('%d Adresse(n) geändert. Von den alten Adressen wird dauerhaft weitergeleitet.', $moved), 'success');
|
||
}
|
||
|
||
#[Route('/pages/{id}/schedule', name: 'admin_schedule', methods: ['POST'])]
|
||
public function schedule(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
try {
|
||
$this->pages->schedule($id, $request->request->getString('publishAt'), $request->request->getInt('expectedVersion', -1));
|
||
} catch (InvalidPage|StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back('' === trim($request->request->getString('publishAt'))
|
||
? 'Die geplante Veröffentlichung wurde entfernt.'
|
||
: 'Die Veröffentlichung ist geplant. Sie erfolgt beim ersten Aufruf der Website nach diesem Zeitpunkt – siehe „Aufgaben“.', 'success');
|
||
}
|
||
|
||
#[Route('/pages/{id}/restore', name: 'admin_restore_page', methods: ['POST'])]
|
||
public function restorePage(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
try {
|
||
$this->pages->restorePage($id);
|
||
} catch (InvalidPage $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back('Die Seite ist zurück. Sie ist erst nach dem Veröffentlichen wieder öffentlich.', 'success');
|
||
}
|
||
|
||
#[Route('/pages/{id}/revisions/{revision}', name: 'admin_restore_revision', requirements: ['revision' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||
public function restoreRevision(Request $request, string $id, string $revision): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
try {
|
||
$this->pages->restoreRevision($id, $revision, $request->request->getInt('expectedVersion', -1));
|
||
} catch (InvalidPage|StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back('Die Fassung ist als Entwurf zurück. Der bisherige Stand bleibt als eigene Fassung erhalten.', 'success');
|
||
}
|
||
|
||
#[Route('/pages/{id}/revisions', name: 'admin_revisions', methods: ['GET'])]
|
||
public function revisions(Request $request, string $id): Response
|
||
{
|
||
$page = $this->page($id);
|
||
|
||
return $this->render('admin/revisions.html.twig', [
|
||
'page' => $page,
|
||
'title' => $page['document']['title'] ?? $page['path'],
|
||
'revisions' => $this->pages->revisions($id, $request->query->getInt('seite', 1)),
|
||
]);
|
||
}
|
||
|
||
#[Route('/pages/{id}/trash', name: 'admin_trash', methods: ['POST'])]
|
||
public function trash(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
try {
|
||
$count = $this->pages->trashPage($id, $request->request->getInt('expectedVersion', -1));
|
||
} catch (InvalidPage|StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back(sprintf('%d Seite(n) in den Papierkorb gelegt. Öffentlich nicht mehr erreichbar, aber wiederherstellbar.', $count), 'success');
|
||
}
|
||
|
||
#[Route('/pages/{id}/delete', name: 'admin_delete', methods: ['POST'])]
|
||
public function delete(Request $request, string $id): Response
|
||
{
|
||
// The final removal deletes every revision, so it stays with the administrator role.
|
||
// Everyone with the editor role uses the bin instead.
|
||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||
if (!$this->isCsrfTokenValid('pages', $request->request->getString('_csrf_token'))) {
|
||
return $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||
}
|
||
try {
|
||
$this->pages->deletePage($id, $request->request->getInt('expectedVersion', -1));
|
||
} catch (InvalidPage $e) {
|
||
return $this->back($e->getMessage());
|
||
} catch (StaleWrite $e) {
|
||
return $this->back($e->getMessage());
|
||
}
|
||
|
||
return $this->back('Die Seite wurde mit allen Ständen gelöscht.', 'success');
|
||
}
|
||
|
||
/** Derives an address from the title so the editor does not have to invent one. */
|
||
private function slugify(string $title): string
|
||
{
|
||
$slug = strtr(mb_strtolower($title, 'UTF-8'), ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss']);
|
||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';
|
||
|
||
return substr(trim($slug, '-'), 0, 80);
|
||
}
|
||
|
||
private function back(string $message, string $type = 'error'): Response
|
||
{
|
||
$this->addFlash($type, $message);
|
||
$request = $this->requests->getCurrentRequest();
|
||
$post = 'post' === $request?->request->getString('_list_type', $request?->request->getString('type', 'page'));
|
||
|
||
if (!$request?->request->has('_list_type')) {
|
||
return $this->redirectToRoute($post ? 'admin_posts' : 'admin_pages');
|
||
}
|
||
|
||
$params = [
|
||
'q' => $request?->request->getString('_list_query', ''),
|
||
'status' => $request?->request->getString('_list_status', 'all'),
|
||
'seite' => $request?->request->getInt('_list_page', 1),
|
||
'limit' => $request?->request->getInt('_list_limit', 20),
|
||
];
|
||
foreach (['q' => '', 'status' => 'all', 'seite' => 1, 'limit' => 20] as $key => $default) {
|
||
if ($params[$key] === $default) {
|
||
unset($params[$key]);
|
||
}
|
||
}
|
||
|
||
return $this->redirectToRoute($post ? 'admin_posts' : 'admin_pages', $params);
|
||
}
|
||
|
||
#[Route('/pages/{id}', name: 'admin_editor', methods: ['GET'])]
|
||
public function editor(string $id): Response
|
||
{
|
||
$page = $this->page($id);
|
||
|
||
return $this->render('admin/editor.html.twig', ['page' => $page]);
|
||
}
|
||
|
||
#[Route('/api/pages/{id}', name: 'admin_document', methods: ['GET'])]
|
||
public function document(string $id): JsonResponse
|
||
{
|
||
return $this->json($this->payload($this->page($id)));
|
||
}
|
||
|
||
#[Route('/api/pages/{id}/draft', name: 'admin_save', methods: ['POST'])]
|
||
#[Route('/api/pages/{id}/publish', name: 'admin_publish', methods: ['POST'])]
|
||
#[Route('/api/pages/{id}/preview', name: 'admin_preview', methods: ['POST'])]
|
||
public function write(Request $request, string $id): Response
|
||
{
|
||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||
if (!$this->isCsrfTokenValid('editor', $request->headers->get('X-CSRF-Token'))) {
|
||
return $this->json(['error' => 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'], 403);
|
||
}
|
||
if (strlen($request->getContent()) > 270000) {
|
||
return $this->json(['error' => 'Die Seite ist zu groß (maximal 256 KiB Inhalt).'], 413);
|
||
}
|
||
$page = $this->page($id);
|
||
try {
|
||
$body = json_decode($request->getContent(), true, 64, JSON_THROW_ON_ERROR);
|
||
if (!is_array($body)) {
|
||
throw new \InvalidArgumentException();
|
||
}
|
||
$route = $request->attributes->get('_route');
|
||
if ('admin_preview' === $route) {
|
||
$document = $body['document'] ?? null;
|
||
if (!is_array($document)) {
|
||
throw new \InvalidArgumentException();
|
||
}
|
||
$this->validator->validate($document);
|
||
|
||
$theme = $this->theme->load();
|
||
$areas = [];
|
||
foreach (['header', 'footer'] as $kind) {
|
||
$area = $this->partials->findByKind($kind);
|
||
if (null !== $area) {
|
||
$areas[$kind] = $area['document'];
|
||
}
|
||
}
|
||
$sections = [];
|
||
foreach ($this->partials->referencedSlugs($document) as $slug) {
|
||
$section = $this->partials->resolveSection($slug);
|
||
if (null !== $section) {
|
||
$sections[$slug] = $section['document'];
|
||
}
|
||
}
|
||
$navigation = [];
|
||
foreach ($this->menus->all() as $slot => $menu) {
|
||
$navigation[$slot] = $this->menus->resolve($menu['items'], (string) $page['path']);
|
||
}
|
||
|
||
return $this->render('page.html.twig', ['page' => [...$page, 'document' => $document], 'canonical' => '', 'editing' => true, 'navigation' => $navigation, 'areas' => $areas, 'sections' => $sections, 'needsTabs' => false, 'pageStyles' => $this->theme->css($theme).$this->styles->compileDocument($document), 'googleFonts' => $this->theme->googleFontsUrl($theme)]);
|
||
}
|
||
if (!isset($body['expectedVersion']) || !is_int($body['expectedVersion']) || $body['expectedVersion'] < 0) {
|
||
throw new \InvalidArgumentException();
|
||
}
|
||
if ('admin_publish' === $route) {
|
||
$this->denyAccessUnlessGranted('ROLE_PUBLISHER');
|
||
// The user must publish the exact draft they reviewed, never whichever draft happens to be latest.
|
||
if (($body['revisionId'] ?? null) !== $page['draft_revision_id']) {
|
||
throw new StaleWrite();
|
||
}
|
||
$this->pages->publish($id, $body['revisionId'], $body['expectedVersion']);
|
||
$page['published_revision_id'] = $body['revisionId'];
|
||
} else {
|
||
if (!is_array($body['document'] ?? null)) {
|
||
throw new \InvalidArgumentException();
|
||
}
|
||
if (!$this->isGranted('ROLE_ADMIN')) {
|
||
$this->code->assertScriptsUnchanged($body['document'], $page['document']);
|
||
}
|
||
$page['draft_revision_id'] = $this->pages->saveDraft($id, $body['document'], $body['expectedVersion']);
|
||
$page['document'] = $body['document'];
|
||
}
|
||
$page['version'] = $body['expectedVersion'] + 1;
|
||
|
||
return $this->json($this->payload($page));
|
||
} catch (StaleWrite) {
|
||
return $this->json(['error' => 'Inzwischen wurde ein neuerer Stand gespeichert. Deine Änderungen bleiben im Editor. Lade sie als JSON herunter, bevor du den aktuellen Serverstand lädst.', 'conflict' => true], 409);
|
||
} catch (InvalidDocument $e) {
|
||
return $this->json(['error' => 'Bitte prüfe die Inhalte: '.$e->getMessage()], 422);
|
||
} catch (\JsonException|\InvalidArgumentException) {
|
||
return $this->json(['error' => 'Die Anfrage enthält kein gültiges Seitendokument oder keine gültige Version.'], 422);
|
||
}
|
||
}
|
||
|
||
private function page(string $id): array
|
||
{
|
||
return $this->pages->findForEditor($id) ?? throw $this->createNotFoundException('Seite nicht gefunden.');
|
||
}
|
||
|
||
private function payload(array $page): array
|
||
{
|
||
return ['document' => $page['document'], 'version' => $page['version'], 'revisionId' => $page['draft_revision_id'], 'publishedRevisionId' => $page['published_revision_id'], 'csrfToken' => $this->csrf->getToken('editor')->getValue()];
|
||
}
|
||
}
|