Kant CMS: Grundgerüst, visueller Editor, Mediathek
Symfony 7.4 / Twig / Doctrine DBAL auf herkömmlichem PHP-Webhosting. Versionierte Blockdokumente (v1-v3) mit unveränderlichen Revisionen, getrennten Entwürfen/Veröffentlichungen und optimistischer Konflikterkennung. Visueller Editor mit Strukturbaum, Live-Vorschau, responsiven Gerätewerten, Abschnittsvorlagen, Bildmediathek, Galerien und Effekten. Öffentliche Ausgabe ohne Editor-JavaScript. Geprüft: 41 PHP-Tests / 155 Assertions, 10 JavaScript-Modelltests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
82b07820bc
87 files changed
+14463
No files matched your search
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Content\MediaLibrary;
|
||||
use App\Domain\StaleWrite;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
final class MediaController extends AbstractController
|
||||
{
|
||||
public function __construct(private MediaLibrary $media, private CsrfTokenManagerInterface $csrf)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/admin/media', name: 'admin_media', methods: ['GET'])]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('admin/media.html.twig');
|
||||
}
|
||||
|
||||
#[Route('/admin/api/media', methods: ['GET'])]
|
||||
public function list(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
|
||||
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'])]
|
||||
public function write(Request $request, ?string $id = null): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
if (!$this->isCsrfTokenValid('media', $request->headers->get('X-CSRF-Token'))) {
|
||||
return $this->json(['error' => 'Sitzung abgelaufen. Bitte erneut anmelden.'], 403);
|
||||
}
|
||||
try {
|
||||
if (null === $id) {
|
||||
$file = $request->files->get('file');
|
||||
if (!$file instanceof UploadedFile) {
|
||||
throw new \InvalidArgumentException('Keine Datei empfangen. Bitte das PHP-Uploadlimit und die Dateigröße prüfen.');
|
||||
}
|
||||
|
||||
return $this->json(['item' => $this->media->upload($file)], 201);
|
||||
}
|
||||
if (!$this->media->find($id)) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
if (strlen($request->getContent()) > 4096) {
|
||||
throw new \InvalidArgumentException('Medienangaben sind zu groß.');
|
||||
}
|
||||
$data = json_decode($request->getContent(), true, 16, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data)) {
|
||||
throw new \InvalidArgumentException('Ungültige Medienangaben.');
|
||||
}
|
||||
|
||||
return $this->json(['item' => $this->media->update($id, $data)]);
|
||||
} catch (StaleWrite) {
|
||||
return $this->json(['error' => 'Das Bild wurde inzwischen geändert. Angaben kopieren und die Mediathek neu öffnen.'], 409);
|
||||
} catch (\InvalidArgumentException|\JsonException $error) {
|
||||
return $this->json(['error' => $error->getMessage()], 422);
|
||||
} catch (\RuntimeException $error) {
|
||||
return $this->json(['error' => 'Das Bild konnte nicht gespeichert werden. GD/WebP und Schreibrechte des Medienordners prüfen.'], 503);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/media/{id}/{width}', requirements: ['id' => '[a-f0-9]{32}', 'width' => '\d{1,4}'], methods: ['GET'])]
|
||||
public function serve(string $id, int $width): Response
|
||||
{
|
||||
$path = $this->media->file($id, $width) ?? throw $this->createNotFoundException();
|
||||
|
||||
return new BinaryFileResponse($path, headers: ['Content-Type' => 'image/webp', 'X-Content-Type-Options' => 'nosniff', 'Cache-Control' => 'public, max-age=31536000, immutable', 'Content-Security-Policy' => "default-src 'none'"]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user