Fix: gate preview "open in new tab" on inline-safe MIME types
The header open-in-new-tab button opened the blob: URL as a top-level navigation for any preview that produced an objectUrl, including HTML and SVG attachments. Blob URLs inherit our origin, so a script-bearing attachment (text/html, image/svg+xml, ...) would execute in-origin when opened that way - the exact case isMimeTypeSafeForInlinePreview() already guards. Gate the button on that helper so it only appears for inert types (images except SVG, audio, video, PDF, text/plain).
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { X, Download, Loader2, ExternalLink } from "lucide-react";
|
import { X, Download, Loader2, ExternalLink } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getFilePreviewKind } from "@/lib/file-preview";
|
import { getFilePreviewKind, isMimeTypeSafeForInlinePreview } from "@/lib/file-preview";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// pdf.js-based inline viewer for mobile (no native inline PDF viewer). Loaded
|
// pdf.js-based inline viewer for mobile (no native inline PDF viewer). Loaded
|
||||||
@@ -149,6 +149,10 @@ export function FilePreviewModal({ name, onClose, onDownload, getFileContent }:
|
|||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [resolvedFileType, setResolvedFileType] = useState(() => getFilePreviewKind(name));
|
const [resolvedFileType, setResolvedFileType] = useState(() => getFilePreviewKind(name));
|
||||||
const [pdfInlineSupported, setPdfInlineSupported] = useState(true);
|
const [pdfInlineSupported, setPdfInlineSupported] = useState(true);
|
||||||
|
// Whether the resolved blob MIME is inert enough to open as a top-level
|
||||||
|
// navigation. Blob URLs inherit our origin, so opening a script-bearing type
|
||||||
|
// (text/html, image/svg+xml, ...) in a new tab would execute it in-origin.
|
||||||
|
const [canOpenInNewTab, setCanOpenInNewTab] = useState(false);
|
||||||
|
|
||||||
// Decide whether to render the PDF in a plain <iframe> (desktop) or with the
|
// Decide whether to render the PDF in a plain <iframe> (desktop) or with the
|
||||||
// pdf.js canvas viewer (mobile). navigator.pdfViewerEnabled is the standard
|
// pdf.js canvas viewer (mobile). navigator.pdfViewerEnabled is the standard
|
||||||
@@ -200,6 +204,7 @@ export function FilePreviewModal({ name, onClose, onDownload, getFileContent }:
|
|||||||
|
|
||||||
setContent(null);
|
setContent(null);
|
||||||
setObjectUrl(null);
|
setObjectUrl(null);
|
||||||
|
setCanOpenInNewTab(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
setResolvedFileType(getFilePreviewKind(name));
|
setResolvedFileType(getFilePreviewKind(name));
|
||||||
@@ -236,7 +241,10 @@ export function FilePreviewModal({ name, onClose, onDownload, getFileContent }:
|
|||||||
? new Blob([blob], { type: effectiveType })
|
? new Blob([blob], { type: effectiveType })
|
||||||
: blob;
|
: blob;
|
||||||
revokeUrl = URL.createObjectURL(typedBlob);
|
revokeUrl = URL.createObjectURL(typedBlob);
|
||||||
if (!cancelled) setObjectUrl(revokeUrl);
|
if (!cancelled) {
|
||||||
|
setObjectUrl(revokeUrl);
|
||||||
|
setCanOpenInNewTab(isMimeTypeSafeForInlinePreview(effectiveType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setError(true);
|
if (!cancelled) setError(true);
|
||||||
@@ -271,7 +279,7 @@ export function FilePreviewModal({ name, onClose, onDownload, getFileContent }:
|
|||||||
<div className="flex items-center justify-between px-4 py-3 bg-background/90 backdrop-blur border-b border-border" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center justify-between px-4 py-3 bg-background/90 backdrop-blur border-b border-border" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3 className="text-sm font-medium truncate">{name}</h3>
|
<h3 className="text-sm font-medium truncate">{name}</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{objectUrl && (
|
{objectUrl && canOpenInNewTab && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
Reference in New Issue
Block a user