diff --git a/features/PROJ-81-anhang-online-vorschau.md b/features/PROJ-81-anhang-online-vorschau.md index bf40c09..2de1a11 100644 --- a/features/PROJ-81-anhang-online-vorschau.md +++ b/features/PROJ-81-anhang-online-vorschau.md @@ -77,7 +77,7 @@ Mail-Ansicht (/mail/[id]) ### D) Abhängigkeiten (Pakete) -- PDF-Viewer-Bibliothek im Frontend (Anzeige im Dialog, ohne Download). Bilder benötigen keine zusätzliche Bibliothek (natives ``). +- PDF-Viewer-Bibliothek im Frontend (Anzeige im Dialog, ohne Download): **`pdfjs-dist@6.2.108`**, ergänzt im dritten Fix-Durchgang (BUG-81-3). Der zunächst genutzte browsereigene Viewer im iframe war nicht browserübergreifend nutzbar. Bilder benötigen keine zusätzliche Bibliothek (natives ``). - Keine neuen Server-/Systemabhängigkeiten. ## Implementation Notes (Frontend, 2026-08-06) @@ -252,20 +252,49 @@ Kein Code-Fix. AC1 oben umformuliert: statt "Klick auf Anhang öffnet Vorschau" **Das Security-Modell bleibt unverändert:** der Server-Wert entscheidet ausschließlich, *welcher* Eintrag aus der festen Whitelist gewählt wird. Gerendert wird nach wie vor nur mit dem clientseitig erzwungenen MIME-Typ aus dieser Liste. Ein Wert außerhalb der Whitelist — insbesondere `image/svg+xml` (PROJ-61-Vektor) oder `text/html` — führt zu "keine Vorschau", nicht zum Rendern. SVG ist in `ALLOWED_MIME_TYPES` bewusst nicht enthalten. Ein Angreifer gewinnt dadurch nichts: er könnte über den Content-Type höchstens erreichen, dass seine endungslose Datei als PDF oder Bild *interpretiert* wird — genau die beiden Pfade, die ohnehin als sicher ausgelegt sind (erzwungener MIME-Typ, PDF im Sandbox-iframe ohne `allow-same-origin`). +### BUG-81-3 (Medium) — PDF-Vorschau blank in Chromium/Brave — **fixed** + +**Befund aus dem Live-Test auf 132:** Firefox rendert die PDF-Vorschau einwandfrei, Chromium/Brave zeigt eine leere weiße Fläche — kein Fehler, kein Download-Prompt. Ursache bestätigt: Chromiums PDFium läuft als MimeHandlerView-Extension und benötigt dafür Same-Origin-Zugriff; in einem iframe mit `sandbox="allow-scripts"` ohne `allow-same-origin` verweigert er sich still. Firefox' pdf.js-basierter Viewer kommt damit klar. Kein Spec-Verstoß, sondern Chromium-spezifisches Verhalten. + +#### Bewertung Option 1 (`allow-same-origin` ergänzen) — **verworfen** +Die entscheidende Frage war, ob eine Blob-URL einen eigenen opaken Origin hat. **Hat sie nicht.** `URL.createObjectURL()` erzeugt eine URL der Form `blob:https://host/uuid`; der Origin des daraus geladenen Dokuments ist per Spezifikation der Origin des erzeugenden Kontexts, also unser App-Origin. Opak wird ein Frame nur durch die Sandbox selbst (`allow-same-origin` weggelassen) — nicht durch das Blob. + +Daraus folgt: mit `sandbox="allow-scripts allow-same-origin"` auf same-origin-Inhalt wäre der Frame vollwertig App-Origin. Diese Kombination hebt die Sandbox praktisch auf, weil das eingebettete Dokument über `parent.document` an das Sandbox-Attribut selbst herankommt und sich per Re-Navigation entsandboxen kann; die HTML-Spezifikation warnt genau vor dieser Kombination. Der Frame hätte damit Zugriff auf DOM, nicht-httpOnly-Cookies, `localStorage` und credential-behaftete API-Aufrufe. + +Man könnte einwenden, dass ein getarntes HTML-Dokument wegen des erzwungenen Blob-MIME-Typs (`application/pdf`) ohnehin nie als HTML geparst wird — das stimmt. Aber damit hinge die gesamte Absicherung an genau einer Kontrolle. Für ein Archiv mit fremdem, unkontrolliertem Mail-Inhalt und der PROJ-61-Vorgeschichte ist das die falsche Richtung. + +#### Umgesetzt: Option 2 (pdf.js gebündelt) +Neue Komponente `src/components/mail/PdfCanvasPreview.tsx`, neue Dependency `pdfjs-dist@6.2.108`. +- PDF wird von pdf.js **selbst nach `` gerendert** — kein iframe, kein eingebettetes Dokument, damit entfällt die Sandbox-Frage vollständig statt sie aufzuweichen. +- Verhalten ist in Chromium und Firefox identisch, weil nicht mehr vom Viewer des Browsers abhängig. +- Parsing läuft im Worker; das Worker-Bundle wird vom Build als statisches Asset emittiert (verifiziert: `.next/static/media/pdf.worker.min.*.mjs`). +- PDF-eigenes JavaScript wird nicht ausgeführt: das Scripting-Sandbox-Bundle (`pdf.sandbox`) wird nicht geladen. `isEvalSupported` gibt es in pdfjs-dist v6 nicht mehr, weil `eval()` dort bereits vollständig entfernt wurde. +- Es wird ausschließlich in Canvas gezeichnet, kein Text-/Annotationslayer — es gelangt also kein HTML aus dem PDF ins DOM. +- Seitenlimit `MAX_RENDERED_PAGES = 30` mit Hinweis, damit ein PDF mit sehr vielen Seiten den Tab nicht blockiert. +- Eigener Fehlerzweig für passwortgeschützte/beschädigte PDFs (Edge Case aus der Spec) mit Download-Hinweis; `doc.destroy()` beim Unmount. + +Im Dialog hält jetzt `pdfBlob` das PDF, Bilder laufen unverändert über die Blob-URL (Bildpfad nicht angefasst, er funktioniert live). Der Guard gegen die BUG-81-1-Schleife nutzt jetzt `loadedRef` statt `urlRef`, damit er beide Pfade abdeckt. + +**Restrisiko / ehrlich:** die Verlagerung des PDF-Parsings in unseren Origin heißt, dass eine Schwachstelle in pdf.js im App-Origin landet statt im Viewer-Prozess des Browsers. pdf.js ist genau für feindliche PDFs ausgelegt, parst im Worker und ohne `eval`, und die Alternative (Option 1) hätte den App-Origin ohnehin direkt preisgegeben. Die Dependency muss aber mitgepflegt werden — pdf.js ist ein regelmäßiges CVE-Ziel. + ### Offen, wie abgestimmt (kein Code-Fix in diesem Durchgang) -- **BUG-81-3** — PDF-Sandbox in Firefox/Safari unbelegt: nach Deploy im Browser verifizieren. Fällt das Rendering aus, braucht es einen sichtbaren Hinweis statt eines leeren weißen Rahmens. - **Audit-Logging für Anhang-Abrufe** (Bestandsfund aus dem Security-Audit): bewusst **nicht** hier mitgefixt, bekommt ein eigenes Ticket. - Die drei Backend-Fragen sind vom QA-Lauf beantwortet (nosniff nur über nginx, `Content-Disposition: attachment` gesetzt, **keine** serverseitige Content-Type-Whitelist). Für die Vorschau unkritisch, weil der Dialog den Server-Content-Type ignoriert; die empfohlene Handler-seitige Härtung ist Backend-Scope. Nach den Fixes: `npx tsc --noEmit` und `npm run build` fehlerfrei. -### Stand nach zweitem Fix-Durchgang -Alle im QA-Bericht gemeldeten Bugs sind erledigt: BUG-81-1 (High/Blocker), BUG-81-2 (Medium), BUG-81-4 (Low), BUG-81-5 (Low, Spec-Text), BUG-81-6 (Low). Offen bleibt allein **BUG-81-3** — die PDF-Darstellung im Sandbox-iframe ist für Firefox und Safari unbelegt und lässt sich nur im echten Browser gegen eine laufende Instanz prüfen. +### Stand nach drittem Fix-Durchgang +Alle im QA-Bericht gemeldeten Bugs sind erledigt: BUG-81-1 (High/Blocker), BUG-81-2 (Medium), BUG-81-3 (Medium), BUG-81-4, BUG-81-5, BUG-81-6 (Low). -**Deployment-Empfehlung: deploybar auf 132.** Kein offener Blocker im Code. Der Status bleibt bewusst auf **In Review** und geht erst auf Deployed, wenn nach dem Deploy verifiziert ist: -- BUG-81-3: PDF-Vorschau in Chrome, Firefox, Safari, Edge. Rendert ein Browser nichts, braucht es einen sichtbaren Hinweis statt eines leeren weißen Rahmens. +**Deployment-Empfehlung: erneut auf 132 deployen und nachtesten.** Der Status bleibt auf **In Review**, weil der PDF-Pfad komplett ausgetauscht wurde und der Fix bisher nur durch Build und statische Analyse belegt ist, nicht im Browser. Nach dem Deploy zu verifizieren: +- **PDF-Vorschau in Chromium/Brave** — das war das ursprüngliche Symptom, hier muss jetzt gerendert werden. +- **PDF-Vorschau in Firefox** — Regressionsprobe, dort funktionierte es vorher schon. +- Mehrseitiges PDF: Seiten scrollbar, ab 30 Seiten erscheint der Kürzungshinweis. +- Passwortgeschütztes/beschädigtes PDF: sichtbarer Fehlertext statt leerer Fläche. +- Bild-Vorschau (JPG) unverändert funktionsfähig — der Pfad wurde bewusst nicht angefasst. - Gegenprobe zu BUG-81-1: fehlgeschlagene Vorschau (z.B. 404) erzeugt genau **einen** Request, keine Schleife — im Netzwerk-Tab prüfen. - Gegenprobe zu BUG-81-6: endungsloser PDF-Anhang bekommt einen Vorschau-Button und rendert. +- Ladezeit gegen das Technical Requirement (< 2 s bei normaler Dateigröße) gegenprüfen — pdf.js rendert langsamer als der native Viewer. ## Deployment _To be added by /deploy_ diff --git a/package-lock.json b/package-lock.json index 19aa7b6..ed3d10d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "lucide-react": "^1.28.0", "next": "^16.1.1", "next-themes": "^0.4.6", + "pdfjs-dist": "^6.2.108", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.71.1", @@ -1198,6 +1199,256 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/canvas": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.3.tgz", + "integrity": "sha512-OlI657a5XXvKGFX7kNeIzJ8rO7IXt87Mqu2H8rXE46viAuOfum/JA7ysX7+eBhxNKznT+RCZh418mndlcFX3+w==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "1.0.3", + "@napi-rs/canvas-darwin-arm64": "1.0.3", + "@napi-rs/canvas-darwin-x64": "1.0.3", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.3", + "@napi-rs/canvas-linux-arm64-gnu": "1.0.3", + "@napi-rs/canvas-linux-arm64-musl": "1.0.3", + "@napi-rs/canvas-linux-riscv64-gnu": "1.0.3", + "@napi-rs/canvas-linux-x64-gnu": "1.0.3", + "@napi-rs/canvas-linux-x64-musl": "1.0.3", + "@napi-rs/canvas-win32-arm64-msvc": "1.0.3", + "@napi-rs/canvas-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.3.tgz", + "integrity": "sha512-7kSCdUhoXiO+AaIMXdBGdtp6EctZNkmF62Rea/BmVQlwKaM3bBhOzyGUzxyxz9dv5vdBfpyAaxhSRSJF4kqK4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-ds14V1BPagLszQyaDTeggny5fNeTCqsUQ5QhFj9VDxSEfzrVxXtdbR0LoFyKa0Siaaw8KvqSk4t7k/WoZJwvbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.3.tgz", + "integrity": "sha512-qof3LRAAycmkV2I1izZo9RoSHF8kCQr5O05sFwv0jK8rSdYV6KHVwimo6Qb7RxZj40WHKbLHm5JDaUF0o5XUAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-FU2kKZLmolHA9+KcUA+l1+xH3WTLUUTQDU/kLv9SEUr2TrRPu94aytOeizFJDHPs/QBcw4QL1mCQhetQXYBbag==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-GVSjntxKeA+/y/ZKf1F+cmUw1WeIkE5aMRPqnZUlBTBvBcrvgWccJAWuYCKPX4QJQwZILIIwhgdAbl51yj6fpA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-J51oK/axyZ13kxycumSMfLiDZMdWdOVvqDFI28BpuViZHE3A0bQfr8B5vg8YnPEnqLD3BSn1hkdlh2buspEcNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.3.tgz", + "integrity": "sha512-CtQgQjoVTX67jS9XuCTtJ40Sl7wRLMguoFnnGnfDmCWf7kzKFZVwj5ynqUOIGKFMSB61ZCuQlwPvVNxYTTseaw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-jtfzAHFp+FRaR7zGT4jyCe6wUgAG/dVb5A4Apd8FY9jKarntDfUAlJXscugiH7ZF5kKnu7/lHFk9LaDPcrGEVQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-xTzaUCKUHTY4bCGadeeRZggbRVbGUT1petg7Z8r9AJR2+D9Bqu6nQAgqBGC6D47tA70LjaaaLTrJ7wNY1T74dg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-ktVLuBkI6QVOm5BwO/WbdGwxgeetAMJa7TTmR8qBarXF0OU2NKjvjUtPJAl2y8t+zBRczJl/1VOl9gua6WcK2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-SGhlQ8bDjL1Cz2KnsKMasr/5sTcwG/SZkB6WCJxLsmSm/3aS2C+3p39bA7iZ2/94+NkVDySZfbiGoaSZSFHYxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", @@ -7869,6 +8120,18 @@ "dev": true, "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "6.2.108", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz", + "integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^1.0.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index ed20189..ff928cc 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "lucide-react": "^1.28.0", "next": "^16.1.1", "next-themes": "^0.4.6", + "pdfjs-dist": "^6.2.108", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.71.1", diff --git a/src/components/mail/AttachmentPreviewDialog.tsx b/src/components/mail/AttachmentPreviewDialog.tsx index ec6a6a9..b3902c8 100644 --- a/src/components/mail/AttachmentPreviewDialog.tsx +++ b/src/components/mail/AttachmentPreviewDialog.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Download, Loader2 } from "lucide-react"; import { downloadMailAttachment, type MailAttachment } from "@/lib/api"; +import { PdfCanvasPreview } from "@/components/mail/PdfCanvasPreview"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { @@ -27,10 +28,10 @@ import { * - SVG ist absichtlich NICHT in der Whitelist (SVG kann Skripte enthalten). * - Der Server-Content-Type wird nur bei Anhaengen OHNE Dateiendung als Hinweis * zur Auswahl aus derselben Whitelist genutzt (BUG-81-6), nie zum Rendern. - * - PDFs laufen in einem sandboxed iframe MIT allow-scripts, aber ohne - * allow-same-origin: der Frame hat dadurch einen eigenen, undurchsichtigen - * Origin und keinen Zugriff auf Cookies/DOM der App. allow-scripts ist noetig, - * weil die browsereigenen PDF-Viewer sonst nicht rendern. + * - PDFs werden gar nicht mehr in einem iframe angezeigt, sondern von pdf.js + * nach Canvas gerendert (siehe PdfCanvasPreview, BUG-81-3). Damit entfaellt + * die Frage nach dem Sandbox-Attribut komplett: es gibt kein eingebettetes + * Dokument, das Rechte erben koennte. * - Es entsteht kein oeffentlicher Link: die Blob-URL lebt nur im Tab und wird * beim Schliessen des Dialogs wieder freigegeben. */ @@ -146,18 +147,23 @@ export function AttachmentPreviewDialog({ const isLarge = attachment.size > PREVIEW_SIZE_WARN_BYTES; const [objectUrl, setObjectUrl] = useState(null); + // Nur fuer den PDF-Pfad: pdf.js rendert aus dem Blob, nicht aus einer URL. + const [pdfBlob, setPdfBlob] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [confirmedLarge, setConfirmedLarge] = useState(false); // Blob-URL zuverlaessig freigeben, auch bei Unmount waehrend des Ladens. const urlRef = useRef(null); + const loadedRef = useRef(false); const revoke = useCallback(() => { if (urlRef.current) { URL.revokeObjectURL(urlRef.current); urlRef.current = null; } + loadedRef.current = false; setObjectUrl(null); + setPdfBlob(null); }, []); const load = useCallback(async () => { @@ -167,9 +173,15 @@ export function AttachmentPreviewDialog({ const { blob } = await downloadMailAttachment(mailId, attachment.index); // Erzwungener MIME-Typ statt Server-Angabe (siehe Security-Hinweis oben). const safeBlob = new Blob([blob], { type: forcedMimeType(attachment) }); - const url = URL.createObjectURL(safeBlob); - urlRef.current = url; - setObjectUrl(url); + loadedRef.current = true; + if (safeBlob.type === "application/pdf") { + // PDF: kein iframe, keine Blob-URL — pdf.js rendert direkt nach Canvas. + setPdfBlob(safeBlob); + } else { + const url = URL.createObjectURL(safeBlob); + urlRef.current = url; + setObjectUrl(url); + } } catch (e) { setError( e instanceof Error @@ -194,7 +206,8 @@ export function AttachmentPreviewDialog({ if (kind === "unsupported") return; if (isLarge && !confirmedLarge) return; if (error) return; - if (urlRef.current || loading) return; + // loadedRef deckt beide Pfade ab (Blob-URL fuer Bilder, Blob fuer PDF). + if (loadedRef.current || loading) return; void load(); }, [open, kind, isLarge, confirmedLarge, error, loading, load]); @@ -283,17 +296,16 @@ export function AttachmentPreviewDialog({ )} {/* - allow-scripts ohne allow-same-origin: der Frame laeuft in einem - eigenen, undurchsichtigen Origin (kein Zugriff auf Cookies/DOM der - App). Skripte sind noetig, weil die eingebauten PDF-Viewer der - Browser sonst nicht rendern. + BUG-81-3: kein iframe mehr fuer PDF. Chromiums PDFium-Viewer + rendert in einem sandboxed iframe ohne allow-same-origin still + gar nichts (leere weisse Flaeche), Firefox schon. pdf.js rendert + stattdessen selbst nach Canvas — browserunabhaengig und ohne die + Sandbox aufweichen zu muessen. Siehe PdfCanvasPreview. */} - {!loading && !error && objectUrl && kind === "pdf" && ( -