From 13ee46745b1e53c6b6c3e776f7fd46db7bfc0c2a Mon Sep 17 00:00:00 2001 From: James Date: Tue, 5 Aug 2025 15:22:56 +0100 Subject: [PATCH] Initial commit of Sanitize UI --- .../public/locales/en-US/translation.json | 38 ++++- .../tools/sanitize/SanitizeSettings.tsx | 83 ++++++++++ .../tools/sanitize/useSanitizeOperation.ts | 82 ++++++++++ .../tools/sanitize/useSanitizeParameters.ts | 52 ++++++ frontend/src/hooks/useToolManagement.tsx | 10 ++ frontend/src/tools/Sanitize.tsx | 154 ++++++++++++++++++ 6 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/tools/sanitize/SanitizeSettings.tsx create mode 100644 frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts create mode 100644 frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts create mode 100644 frontend/src/tools/Sanitize.tsx diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 56279f8b4..776213dbe 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -387,6 +387,10 @@ "title": "Compress", "desc": "Compress PDFs to reduce their file size." }, + "sanitize": { + "title": "Sanitize", + "desc": "Remove potentially harmful elements from PDF files." + }, "unlockPDFForms": { "title": "Unlock PDF Forms", "desc": "Remove read-only property of form fields in a PDF document." @@ -1612,6 +1616,38 @@ "pdfaOptions": "PDF/A Options", "outputFormat": "Output Format", "pdfaNote": "PDF/A-1b is more compatible, PDF/A-2b supports more features.", - "pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step." + "pdfaDigitalSignatureWarning": "The PDF contains a digital signature. This will be removed in the next step.", + "sanitize": { + "submit": "Sanitize PDF", + "processing": "Sanitizing PDF...", + "completed": "Sanitization completed successfully", + "error": "Sanitization failed: {{error}}", + "error.generic": "Sanitization failed", + "steps": { + "files": "Files", + "settings": "Settings", + "results": "Results" + }, + "files": { + "selected": "Selected: {{filename}}", + "placeholder": "Select a PDF file in the main view to get started" + }, + "options": { + "title": "Sanitization Options", + "note": "Select the elements you want to remove from the PDF. At least one option must be selected.", + "removeJavaScript": "Remove JavaScript", + "removeJavaScript.desc": "Remove JavaScript actions and scripts from the PDF", + "removeEmbeddedFiles": "Remove Embedded Files", + "removeEmbeddedFiles.desc": "Remove any files embedded within the PDF", + "removeXMPMetadata": "Remove XMP Metadata", + "removeXMPMetadata.desc": "Remove XMP metadata from the PDF", + "removeMetadata": "Remove Document Metadata", + "removeMetadata.desc": "Remove document information metadata (title, author, etc.)", + "removeLinks": "Remove Links", + "removeLinks.desc": "Remove external links and launch actions from the PDF", + "removeFonts": "Remove Fonts", + "removeFonts.desc": "Remove embedded fonts from the PDF" + } + } } } \ No newline at end of file diff --git a/frontend/src/components/tools/sanitize/SanitizeSettings.tsx b/frontend/src/components/tools/sanitize/SanitizeSettings.tsx new file mode 100644 index 000000000..f849d14f0 --- /dev/null +++ b/frontend/src/components/tools/sanitize/SanitizeSettings.tsx @@ -0,0 +1,83 @@ +import { Stack, Text, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { SanitizeParameters } from "../../../hooks/tools/sanitize/useSanitizeParameters"; + +interface SanitizeSettingsProps { + parameters: SanitizeParameters; + onParameterChange: (key: keyof SanitizeParameters, value: boolean) => void; + disabled?: boolean; +} + +const SanitizeSettings = ({ parameters, onParameterChange, disabled = false }: SanitizeSettingsProps) => { + const { t } = useTranslation(); + + const options = [ + { + key: 'removeJavaScript' as const, + label: t('sanitize.options.removeJavaScript', 'Remove JavaScript'), + description: t('sanitize.options.removeJavaScript.desc', 'Remove JavaScript actions and scripts from the PDF'), + default: true, + }, + { + key: 'removeEmbeddedFiles' as const, + label: t('sanitize.options.removeEmbeddedFiles', 'Remove Embedded Files'), + description: t('sanitize.options.removeEmbeddedFiles.desc', 'Remove any files embedded within the PDF'), + default: true, + }, + { + key: 'removeXMPMetadata' as const, + label: t('sanitize.options.removeXMPMetadata', 'Remove XMP Metadata'), + description: t('sanitize.options.removeXMPMetadata.desc', 'Remove XMP metadata from the PDF'), + default: false, + }, + { + key: 'removeMetadata' as const, + label: t('sanitize.options.removeMetadata', 'Remove Document Metadata'), + description: t('sanitize.options.removeMetadata.desc', 'Remove document information metadata (title, author, etc.)'), + default: false, + }, + { + key: 'removeLinks' as const, + label: t('sanitize.options.removeLinks', 'Remove Links'), + description: t('sanitize.options.removeLinks.desc', 'Remove external links and launch actions from the PDF'), + default: false, + }, + { + key: 'removeFonts' as const, + label: t('sanitize.options.removeFonts', 'Remove Fonts'), + description: t('sanitize.options.removeFonts.desc', 'Remove embedded fonts from the PDF'), + default: false, + }, + ]; + + return ( + + + {t('sanitize.options.title', 'Sanitization Options')} + + + + {options.map((option) => ( + onParameterChange(option.key, event.currentTarget.checked)} + disabled={disabled} + label={ +
+ {option.label} + {option.description} +
+ } + /> + ))} +
+ + + {t('sanitize.options.note', 'Select the elements you want to remove from the PDF. At least one option must be selected.')} + +
+ ); +}; + +export default SanitizeSettings; diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts new file mode 100644 index 000000000..0d92f4c07 --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts @@ -0,0 +1,82 @@ +import { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SanitizeParameters } from './useSanitizeParameters'; + +export const useSanitizeOperation = () => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [downloadUrl, setDownloadUrl] = useState(null); + const [status, setStatus] = useState(null); + + const executeOperation = useCallback(async ( + parameters: SanitizeParameters, + selectedFiles: File[] + ) => { + if (selectedFiles.length === 0) { + throw new Error(t('error.noFilesSelected', 'No files selected')); + } + + setIsLoading(true); + setErrorMessage(null); + setStatus(t('sanitize.processing', 'Sanitizing PDF...')); + + try { + const formData = new FormData(); + formData.append('fileInput', selectedFiles[0]); + + // Add parameters + formData.append('removeJavaScript', parameters.removeJavaScript.toString()); + formData.append('removeEmbeddedFiles', parameters.removeEmbeddedFiles.toString()); + formData.append('removeXMPMetadata', parameters.removeXMPMetadata.toString()); + formData.append('removeMetadata', parameters.removeMetadata.toString()); + formData.append('removeLinks', parameters.removeLinks.toString()); + formData.append('removeFonts', parameters.removeFonts.toString()); + + const response = await fetch('/api/v1/security/sanitize-pdf', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(t('sanitize.error', 'Sanitization failed: {{error}}', { error: errorText })); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + setDownloadUrl(url); + setStatus(t('sanitize.completed', 'Sanitization completed successfully')); + } catch (error) { + const message = error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed'); + setErrorMessage(message); + setStatus(null); + throw error; + } finally { + setIsLoading(false); + } + }, [t]); + + const resetResults = useCallback(() => { + if (downloadUrl) { + URL.revokeObjectURL(downloadUrl); + } + setDownloadUrl(null); + setErrorMessage(null); + setStatus(null); + }, [downloadUrl]); + + const clearError = useCallback(() => { + setErrorMessage(null); + }, []); + + return { + isLoading, + errorMessage, + downloadUrl, + status, + executeOperation, + resetResults, + clearError, + }; +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts new file mode 100644 index 000000000..0e7defb7d --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts @@ -0,0 +1,52 @@ +import { useState, useCallback } from 'react'; + +export interface SanitizeParameters { + removeJavaScript: boolean; + removeEmbeddedFiles: boolean; + removeXMPMetadata: boolean; + removeMetadata: boolean; + removeLinks: boolean; + removeFonts: boolean; +} + +const defaultParameters: SanitizeParameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false, +}; + +export const useSanitizeParameters = () => { + const [parameters, setParameters] = useState(defaultParameters); + + const updateParameter = useCallback(( + key: K, + value: SanitizeParameters[K] + ) => { + setParameters(prev => ({ + ...prev, + [key]: value + })); + }, []); + + const resetParameters = useCallback(() => { + setParameters(defaultParameters); + }, []); + + const getEndpointName = useCallback(() => 'sanitize-pdf', []); + + const validateParameters = useCallback(() => { + // At least one sanitization option must be selected + return Object.values(parameters).some(value => value === true); + }, [parameters]); + + return { + parameters, + updateParameter, + resetParameters, + getEndpointName, + validateParameters, + }; +}; \ No newline at end of file diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index debd3f5b1..f4b68fe88 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -4,6 +4,7 @@ import ContentCutIcon from "@mui/icons-material/ContentCut"; import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; import SwapHorizIcon from "@mui/icons-material/SwapHoriz"; import ApiIcon from "@mui/icons-material/Api"; +import CleaningServicesIcon from "@mui/icons-material/CleaningServices"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool"; @@ -75,6 +76,15 @@ const toolDefinitions: Record = { description: "Extract text from images using OCR", endpoints: ["ocr-pdf"] }, + sanitize: { + id: "sanitize", + icon: , + component: React.lazy(() => import("../tools/Sanitize")), + maxFiles: 1, + category: "security", + description: "Remove potentially harmful elements from PDF files", + endpoints: ["sanitize-pdf"] + }, }; diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx new file mode 100644 index 000000000..233a417db --- /dev/null +++ b/frontend/src/tools/Sanitize.tsx @@ -0,0 +1,154 @@ +import { useEffect } from "react"; +import { Button, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import DownloadIcon from "@mui/icons-material/Download"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useFileContext } from "../contexts/FileContext"; +import { useToolFileSelection } from "../contexts/FileSelectionContext"; + +import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep"; +import OperationButton from "../components/tools/shared/OperationButton"; +import ErrorNotification from "../components/tools/shared/ErrorNotification"; +import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"; +import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings"; + +import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters"; +import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation"; +import { BaseToolProps } from "../types/tool"; + +const generateSanitizedFileName = (originalFileName?: string): string => { + const baseName = originalFileName?.replace(/\.[^/.]+$/, '') || 'document'; + return `${baseName}_sanitized.pdf`; +}; + +const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const sanitizeParams = useSanitizeParameters(); + const sanitizeOperation = useSanitizeOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled( + sanitizeParams.getEndpointName() + ); + + useEffect(() => { + sanitizeOperation.resetResults(); + onPreviewFile?.(null); + }, [sanitizeParams.parameters, selectedFiles]); + + const handleSanitize = async () => { + try { + await sanitizeOperation.executeOperation( + sanitizeParams.parameters, + selectedFiles + ); + if (sanitizeOperation.downloadUrl && onComplete) { + // Create a File object from the download URL for completion callback + const response = await fetch(sanitizeOperation.downloadUrl); + const blob = await response.blob(); + const sanitizedFileName = generateSanitizedFileName(selectedFiles[0]?.name); + const file = new File([blob], sanitizedFileName, { + type: 'application/pdf' + }); + onComplete([file]); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed')); + } + } + }; + + const handleSettingsReset = () => { + sanitizeOperation.resetResults(); + onPreviewFile?.(null); + // JB: Does this need setCurrentMode()? + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = sanitizeOperation.downloadUrl !== null; + const filesCollapsed = hasFiles; + const settingsCollapsed = hasResults; + + return ( + + + {/* Files Step */} + + + + + {/* Settings Step */} + + + + + + + + + {/* Results Step */} + + + {sanitizeOperation.status && ( + {sanitizeOperation.status} + )} + + + + {sanitizeOperation.downloadUrl && ( + + )} + + + + + ); +} + +export default Sanitize;