Upload PDFs to the library, link them from any link target, show them in a viewer

PDFs go through the same upload field as images, are checked by their content and kept
unchanged in their own table. Every link field can pick one, and a page that links a PDF
opens it in a dialog with the browser's own viewer; touch devices and browsers without a
viewer get a new tab instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-22 07:56:29 +02:00
1 parent 04927b9563
commit e8a9dcdf5c
20 files changed
+667 -30

No files matched your search

+14 -4
View File
@@ -130,6 +130,10 @@ final class PageController extends AbstractController
// Stylesheet und Skript eines Plugin-Bausteins kommen nur dorthin, wo er vorkommt.
$pluginAssets = $this->plugins->assetsFor($documents);
$needsZoom = (bool) array_filter($documents, fn (array $doc): bool => $this->hasZoom($doc));
// Ein Link auf ein PDF der Mediathek - in einem Button, einem Bild oder im Fliesstext.
// Gesucht wird im gespeicherten Dokument als Text: so findet sich auch ein Link, den
// jemand in einen formatierten Absatz geschrieben hat.
$needsDocument = (bool) array_filter($documents, static fn (array $doc): bool => 1 === preg_match('#/media/[a-f0-9]{32}\.pdf#', json_encode($doc, JSON_UNESCAPED_SLASHES) ?: ''));
// Only the header changes with the scroll position; a scrolled value anywhere in it asks
// for the small script that marks the page as scrolled.
// Den Nach-oben-Knopf blendet dasselbe Skript ein; ohne es bleibt er dauerhaft sichtbar.
@@ -198,6 +202,9 @@ final class PageController extends AbstractController
// Ein Bild, das sich vergroessern laesst, ist ohne Skript ein Verweis auf die
// grosse Fassung. Das Skript kommt nur dazu, wo es etwas zu tun hat.
'needsZoom' => $needsZoom,
// Der PDF-Betrachter, nach demselben Muster und im selben Skript: ohne es ein
// gewoehnlicher Link.
'needsDocument' => $needsDocument,
// Own scripts arrive as one file per page, never inline, so the policy can stay
// hash based and no page without a script loses its strict setting.
'pageScript' => [] === $this->code->scriptsOf($page['document']) ? null : '/assets/seite/'.$page['id'].'.js',
@@ -228,7 +235,7 @@ final class PageController extends AbstractController
// Die Pruefung braucht ein Skript von Cloudflare und einen Rahmen dafuer. Beides wird nur
// dort erlaubt, wo tatsaechlich ein Formular steht - nicht auf jeder Seite der Website.
$mitTurnstile = $this->turnstile->isActive() && (bool) array_filter($documents, fn (array $doc): bool => $this->hasType($doc, 'form'));
$wantsScript = ($mitTurnstile || $needsTabs || $needsFormScript || [] !== $pluginAssets['scripts'] || [] !== $areaScripts || $needsSlider || $needsMotion || $needsScrolled || $needsZoom || [] !== $this->code->scriptsOf($page['document']));
$wantsScript = ($mitTurnstile || $needsTabs || $needsFormScript || [] !== $pluginAssets['scripts'] || [] !== $areaScripts || $needsSlider || $needsMotion || $needsScrolled || $needsZoom || $needsDocument || [] !== $this->code->scriptsOf($page['document']));
$frames = $this->frameHosts($request, $page['document'], ...array_values($areas), ...array_values($sections));
if ($mitTurnstile) {
$frames[] = 'challenges.cloudflare.com';
@@ -238,7 +245,7 @@ final class PageController extends AbstractController
$response->headers->set('X-Robots-Tag', 'noindex, follow');
}
return $this->shell($response, $wantsScript, $styles, $frames);
return $this->shell($response, $wantsScript, $styles, $frames, $needsDocument);
}
/**
@@ -252,8 +259,9 @@ final class PageController extends AbstractController
* @param list<string> $frameHosts Hosts of embeds that are actually rendered on this response.
* Nothing is added to the policy for an embed still waiting
* for consent.
* @param bool $frameSelf whether the page may frame its own PDFs (the viewer)
*/
private function shell(Response $response, bool $allowScript = false, string $styles = '', array $frameHosts = []): Response
private function shell(Response $response, bool $allowScript = false, string $styles = '', array $frameHosts = [], bool $frameSelf = false): Response
{
$styleSources = ["'self'"];
if ('' !== $styles) {
@@ -280,7 +288,9 @@ final class PageController extends AbstractController
implode(' ', $scriptSources),
implode(' ', $styleSources),
implode(' ', $fontSources),
[] === $frameHosts ? "'none'" : implode(' ', array_map(static fn (string $host): string => 'https://'.$host, $frameHosts)),
// Der PDF-Betrachter zeigt eine Datei dieser Website in einem Rahmen - nur dort, wo
// eine Seite auf ein PDF verlinkt, und nur von hier.
[] === $frameHosts && !$frameSelf ? "'none'" : implode(' ', [...($frameSelf ? ["'self'"] : []), ...array_map(static fn (string $host): string => 'https://'.$host, $frameHosts)]),
));
return $response;