query->getBoolean('archiv'); return $this->render('admin/media.html.twig', [ 'library' => $this->media->page($request->query->getInt('seite', 1), $archived), 'archived' => $archived, ]); } #[Route('/%kant.admin_path%/media/upload', name: 'admin_media_upload', methods: ['POST'])] public function formUpload(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_EDITOR'); if (!$this->isCsrfTokenValid('media-form', $request->request->getString('_csrf_token'))) { return $this->back($request, 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'); } $files = array_filter((array) $request->files->get('files'), static fn ($file): bool => $file instanceof UploadedFile); if ([] === $files) { return $this->back($request, 'Keine Datei empfangen. Bitte die Dateigröße und das PHP-Uploadlimit prüfen.'); } $done = 0; foreach ($files as $file) { try { $this->media->upload($file); ++$done; } catch (\InvalidArgumentException $error) { return $this->back($request, sprintf('%s (%s)%s', $error->getMessage(), $file->getClientOriginalName(), $done > 0 ? sprintf(' %d Bild(er) davor wurden hochgeladen.', $done) : '')); } catch (\RuntimeException) { return $this->back($request, 'Das Bild konnte nicht verarbeitet werden. GD/WebP und die Schreibrechte des Medienordners prüfen.'); } } return $this->back($request, sprintf('%d Bild(er) hochgeladen.', $done), 'success'); } #[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'); if (!$this->isCsrfTokenValid('media-form', $request->request->getString('_csrf_token'))) { return $this->back($request, 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.'); } if (!$this->media->find($id)) { throw $this->createNotFoundException(); } try { $this->media->update($id, [ 'title' => $request->request->getString('title'), 'alt' => $request->request->getString('alt'), 'archived' => $request->request->getBoolean('archived'), 'version' => $request->request->getInt('version'), ]); } catch (StaleWrite) { return $this->back($request, 'Das Bild wurde inzwischen anderswo geändert. Lade die Seite neu und trage die Angaben erneut ein.'); } catch (\InvalidArgumentException $error) { return $this->back($request, $error->getMessage()); } return $this->back($request, 'Die Bildangaben wurden gespeichert.', 'success'); } /** Back to the page the form was on, so a change does not jump to the first page. */ private function back(Request $request, string $message, string $type = 'error'): Response { $this->addFlash($type, $message); $query = array_filter([ 'seite' => $request->request->getInt('seite', 1) > 1 ? $request->request->getInt('seite') : null, 'archiv' => $request->request->getBoolean('archiv') ? '1' : null, ]); return $this->redirectToRoute('admin_media', $query); } #[Route('/%kant.admin_path%/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('/%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'); 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); } } /** * Das Symbol der Website, in den beiden Größen, die Browser abfragen. * * Ohne eingestelltes Bild gibt es die Dateien nicht - dann bleibt es beim mitgelieferten * favicon.svg, auf das die Seiten weiterhin verweisen. */ #[Route('/favicon.png', name: 'favicon_png', methods: ['GET'])] #[Route('/apple-touch-icon.png', name: 'favicon_touch', methods: ['GET'])] public function favicon(Request $request): Response { $size = 'favicon_touch' === $request->attributes->get('_route') ? 180 : 32; $path = $this->media->faviconFile($size) ?? throw $this->createNotFoundException(); return new BinaryFileResponse($path, headers: ['Content-Type' => 'image/png', 'X-Content-Type-Options' => 'nosniff', 'Cache-Control' => 'public, max-age=604800', 'Content-Security-Policy' => "default-src 'none'"]); } #[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'"]); } }