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

+42 -1
View File
@@ -738,6 +738,47 @@ final class EditorSecurityTest extends WebTestCase
}
}
public function testPdfsGoThroughTheMediaUploadAndTheEditorPicker(): void
{
$client = $this->login('editor');
$crawler = $client->request('GET', '/admin/media');
$token = $crawler->filter('form[action="/admin/media/upload"] input[name="_csrf_token"]')->attr('value');
$path = tempnam(sys_get_temp_dir(), 'kant-form-pdf-');
try {
file_put_contents($path, "%PDF-1.7\n%%EOF\n");
// Dasselbe Feld wie für Bilder: der Inhalt entscheidet, wohin die Datei geht.
$client->request('POST', '/admin/media/upload', ['_csrf_token' => $token], ['files' => [new UploadedFile($path, 'Flyer.pdf', test: true)]]);
self::assertResponseRedirects('/admin/media');
self::assertSame(0, (int) $this->db->fetchOne('SELECT COUNT(*) FROM media'));
self::assertSame('Flyer', $this->db->fetchOne('SELECT title FROM document'));
$crawler = $client->followRedirect();
self::assertStringContainsString('1 Datei(en) hochgeladen', $crawler->filter('.notice.ok')->text());
self::assertCount(1, $crawler->filter('section[aria-labelledby="documents-heading"] .row'));
// Die Auswahl im Editor: Liste mit Token, Hochladen nur mit Token.
$client->request('GET', '/admin/api/dokumente');
self::assertResponseIsSuccessful();
$data = json_decode($client->getResponse()->getContent(), true);
self::assertSame('Flyer', $data['items'][0]['title']);
$client->request('POST', '/admin/api/dokumente/upload', files: ['file' => new UploadedFile($path, 'zweites.pdf', test: true)]);
self::assertResponseStatusCodeSame(403);
file_put_contents($path, '<?php echo 1;');
$client->request('POST', '/admin/api/dokumente/upload', files: ['file' => new UploadedFile($path, 'getarnt.pdf', test: true)], server: ['HTTP_X_CSRF_TOKEN' => $data['csrfToken']]);
self::assertResponseStatusCodeSame(422);
self::assertSame(1, (int) $this->db->fetchOne('SELECT COUNT(*) FROM document'));
// Archivieren nimmt es aus der Auswahl-Übersicht, die Datei bleibt erreichbar.
$id = $data['items'][0]['id'];
$client->request('POST', '/admin/api/dokumente/'.$id, server: ['CONTENT_TYPE' => 'application/json', 'HTTP_X_CSRF_TOKEN' => $data['csrfToken']], content: json_encode(['title' => 'Flyer 2026', 'archived' => true, 'version' => 1]));
self::assertResponseIsSuccessful();
self::assertCount(0, $client->request('GET', '/admin/media')->filter('section[aria-labelledby="documents-heading"] .row'));
$client->request('GET', '/media/'.$id.'.pdf');
self::assertResponseIsSuccessful();
} finally {
unlink($path);
}
}
public function testMediaPreviewReadsSmallAndArchivedImagesWithoutListingTheLibrary(): void
{
$id = str_repeat('a', 32);
@@ -785,7 +826,7 @@ final class EditorSecurityTest extends WebTestCase
$crawler = $client->followRedirect();
self::assertCount(2, $crawler->filter('.library-tile'));
self::assertCount(1, $crawler->filter('.notice.ok'));
self::assertStringContainsString('2 Bild(er) hochgeladen', $crawler->filter('.notice.ok')->text());
self::assertStringContainsString('2 Datei(en) hochgeladen', $crawler->filter('.notice.ok')->text());
$id = (string) $this->db->fetchOne('SELECT id FROM media LIMIT 1');
$client->request('POST', '/admin/media/'.$id, ['_csrf_token' => $token, 'title' => 'Bergblick', 'alt' => 'Grüne Berglandschaft', 'archived' => '1', 'version' => 1]);