diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index ed3942172..6e5dd6179 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -38,7 +38,8 @@ "save": "Save", "saveToBrowser": "Save to Browser", "close": "Close", - "filesSelected": "files selected", + "fileSelected": "Selected: {{filename}}", + "filesSelected": "{{count}} files selected", "noFavourites": "No favourites added", "downloadComplete": "Download Complete", "bored": "Bored Waiting?", @@ -391,6 +392,10 @@ "title": "Compress", "desc": "Compress PDFs to reduce their file size." }, + "sanitize": { + "title": "Sanitise", + "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." @@ -504,7 +509,7 @@ "desc": "Auto Split Scanned PDF with physical scanned page splitter QR Code" }, "sanitizePdf": { - "title": "Sanitize", + "title": "Sanitise", "desc": "Remove scripts and other elements from PDF files" }, "URLToPDF": { @@ -1425,8 +1430,8 @@ "placeholder": "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" }, "sanitizePDF": { - "title": "Sanitize PDF", - "header": "Sanitize a PDF file", + "title": "Sanitise PDF", + "header": "Sanitise a PDF file", "selectText": { "1": "Remove JavaScript actions", "2": "Remove embedded files", @@ -1761,5 +1766,37 @@ "fileTooLarge": "File too large. Maximum size per file is", "storageQuotaExceeded": "Storage quota exceeded. Please remove some files before uploading more.", "approximateSize": "Approximate size" + }, + "sanitize": { + "submit": "Sanitise PDF", + "completed": "Sanitisation completed successfully", + "error.generic": "Sanitisation failed", + "error.failed": "An error occurred while sanitising the PDF.", + "filenamePrefix": "sanitised", + "sanitizationResults": "Sanitisation Results", + "steps": { + "files": "Files", + "settings": "Settings", + "results": "Results" + }, + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "options": { + "title": "Sanitisation 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/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 56279f8b4..6ca67480b 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -38,7 +38,8 @@ "save": "Save", "saveToBrowser": "Save to Browser", "close": "Close", - "filesSelected": "files selected", + "fileSelected": "Selected: {{filename}}", + "filesSelected": "{{count}} files selected", "noFavourites": "No favorites added", "downloadComplete": "Download Complete", "bored": "Bored Waiting?", @@ -387,6 +388,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 +1617,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", + "completed": "Sanitization completed successfully", + "error.generic": "Sanitization failed", + "error.failed": "An error occurred while sanitizing the PDF.", + "filenamePrefix": "sanitized", + "sanitizationResults": "Sanitization Results", + "steps": { + "files": "Files", + "settings": "Settings", + "results": "Results" + }, + "files": { + "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.test.tsx b/frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx new file mode 100644 index 000000000..055de2466 --- /dev/null +++ b/frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx @@ -0,0 +1,194 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import SanitizeSettings from './SanitizeSettings'; +import { SanitizeParameters } from '../../../hooks/tools/sanitize/useSanitizeParameters'; + +// Mock useTranslation with predictable return values +const mockT = vi.fn((key: string) => `mock-${key}`); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('SanitizeSettings', () => { + const defaultParameters: SanitizeParameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false, + }; + + const mockOnParameterChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render all sanitization option checkboxes', () => { + render( + + + + ); + + // Should render one checkbox for each parameter + const expectedCheckboxCount = Object.keys(defaultParameters).length; + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes).toHaveLength(expectedCheckboxCount); + }); + + test('should show correct initial checkbox states based on parameters', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + const parameterValues = Object.values(defaultParameters); + + parameterValues.forEach((value, index) => { + if (value) { + expect(checkboxes[index]).toBeChecked(); + } else { + expect(checkboxes[index]).not.toBeChecked(); + } + }); + }); + + test('should call onParameterChange with correct parameters when checkboxes are clicked', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + + // Click the first checkbox (removeJavaScript - should toggle from true to false) + fireEvent.click(checkboxes[0]); + expect(mockOnParameterChange).toHaveBeenCalledWith('removeJavaScript', false); + + // Click the third checkbox (removeXMPMetadata - should toggle from false to true) + fireEvent.click(checkboxes[2]); + expect(mockOnParameterChange).toHaveBeenCalledWith('removeXMPMetadata', true); + }); + + test('should disable all checkboxes when disabled prop is true', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + checkboxes.forEach(checkbox => { + expect(checkbox).toBeDisabled(); + }); + }); + + test('should enable all checkboxes when disabled prop is false or undefined', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + checkboxes.forEach(checkbox => { + expect(checkbox).not.toBeDisabled(); + }); + }); + + test('should handle different parameter combinations', () => { + const allEnabledParameters: SanitizeParameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: true, + removeMetadata: true, + removeLinks: true, + removeFonts: true, + }; + + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + checkboxes.forEach(checkbox => { + expect(checkbox).toBeChecked(); + }); + }); + + test('should call translation function with correct keys', () => { + render( + + + + ); + + // Verify that translation keys are being called (just check that it was called, not specific order) + expect(mockT).toHaveBeenCalledWith('sanitize.options.title', expect.any(String)); + expect(mockT).toHaveBeenCalledWith('sanitize.options.removeJavaScript', expect.any(String)); + expect(mockT).toHaveBeenCalledWith('sanitize.options.removeEmbeddedFiles', expect.any(String)); + expect(mockT).toHaveBeenCalledWith('sanitize.options.note', expect.any(String)); + }); + + test('should not call onParameterChange when disabled', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + + // Verify checkboxes are disabled + checkboxes.forEach(checkbox => { + expect(checkbox).toBeDisabled(); + }); + + // Try to click a disabled checkbox - this might still fire the event in tests + // but we can verify the checkbox state doesn't actually change + const firstCheckbox = checkboxes[0] as HTMLInputElement; + const initialChecked = firstCheckbox.checked; + fireEvent.click(firstCheckbox); + expect(firstCheckbox.checked).toBe(initialChecked); + }); +}); diff --git a/frontend/src/components/tools/sanitize/SanitizeSettings.tsx b/frontend/src/components/tools/sanitize/SanitizeSettings.tsx new file mode 100644 index 000000000..fb5304431 --- /dev/null +++ b/frontend/src/components/tools/sanitize/SanitizeSettings.tsx @@ -0,0 +1,51 @@ +import { Stack, Text, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { SanitizeParameters, defaultParameters } 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 = (Object.keys(defaultParameters) as Array).map((key) => ({ + key: key, + label: t(`sanitize.options.${key}`, key), + description: t(`sanitize.options.${key}.desc`, `${key} from the PDF`), + default: defaultParameters[key], + })); + + 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/compress/useCompressOperation.ts b/frontend/src/hooks/tools/compress/useCompressOperation.ts index e254cbe1c..ebcac8b66 100644 --- a/frontend/src/hooks/tools/compress/useCompressOperation.ts +++ b/frontend/src/hooks/tools/compress/useCompressOperation.ts @@ -38,12 +38,6 @@ export const useCompressOperation = () => { buildFormData, filePrefix: 'compressed_', multiFileEndpoint: false, // Individual API calls per file - validateParams: (params) => { - if (params.compressionMethod === 'filesize' && !params.fileSizeValue) { - return { valid: false, errors: [t('compress.validation.fileSizeRequired', 'File size value is required when using filesize method')] }; - } - return { valid: true }; - }, getErrorMessage: createStandardErrorHandler(t('compress.error.failed', 'An error occurred while compressing the PDF.')) }); }; diff --git a/frontend/src/hooks/tools/convert/useConvertOperation.ts b/frontend/src/hooks/tools/convert/useConvertOperation.ts index 49b55d0d1..f14302aaa 100644 --- a/frontend/src/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/hooks/tools/convert/useConvertOperation.ts @@ -134,9 +134,6 @@ export const useConvertOperation = () => { buildFormData, // Not used with customProcessor but required filePrefix: 'converted_', customProcessor: customConvertProcessor, // Convert handles its own routing - validateParams: (params) => { - return { valid: true }; - }, getErrorMessage: (error) => { if (error.response?.data && typeof error.response.data === 'string') { return error.response.data; diff --git a/frontend/src/hooks/tools/ocr/useOCROperation.ts b/frontend/src/hooks/tools/ocr/useOCROperation.ts index b684657ad..eeebb8747 100644 --- a/frontend/src/hooks/tools/ocr/useOCROperation.ts +++ b/frontend/src/hooks/tools/ocr/useOCROperation.ts @@ -103,10 +103,6 @@ export const useOCROperation = () => { filePrefix: 'ocr_', multiFileEndpoint: false, // Process files individually responseHandler, // use shared flow - validateParams: (params) => - params.languages.length === 0 - ? { valid: false, errors: [t('ocr.validation.languageRequired', 'Please select at least one language for OCR processing.')] } - : { valid: true }, getErrorMessage: (error) => error.message?.includes('OCR tools') && error.message?.includes('not installed') ? 'OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.' diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts new file mode 100644 index 000000000..c83086121 --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { SanitizeParameters } from './useSanitizeParameters'; + +const buildFormData = (parameters: SanitizeParameters, file: File): FormData => { + const formData = new FormData(); + formData.append('fileInput', file); + + // 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()); + + return formData; +}; + +export const useSanitizeOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + operationType: 'sanitize', + endpoint: '/api/v1/security/sanitize-pdf', + buildFormData, + filePrefix: t('sanitize.filenamePrefix', 'sanitized') + '_', + multiFileEndpoint: false, // Individual API calls per file + getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.')) + }); +}; diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts new file mode 100644 index 000000000..f86a6c81a --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, test } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { defaultParameters, useSanitizeParameters } from './useSanitizeParameters'; + +describe('useSanitizeParameters', () => { + test('should initialize with default parameters', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + expect(result.current.parameters).toStrictEqual(defaultParameters); + }); + + test('should update individual parameters', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + act(() => { + result.current.updateParameter('removeXMPMetadata', true); + }); + + expect(result.current.parameters).toStrictEqual({ + ...defaultParameters, // Other params unchanged + removeXMPMetadata: true, + }); + }); + + test('should reset parameters to defaults', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + // First, change some parameters + act(() => { + result.current.updateParameter('removeXMPMetadata', true); + result.current.updateParameter('removeJavaScript', false); + }); + + expect(result.current.parameters.removeXMPMetadata).toBe(true); + expect(result.current.parameters.removeJavaScript).toBe(false); + + // Then reset + act(() => { + result.current.resetParameters(); + }); + + expect(result.current.parameters).toStrictEqual(defaultParameters); + }); + + test('should return correct endpoint name', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + expect(result.current.getEndpointName()).toBe('sanitize-pdf'); + }); + + test('should validate parameters correctly', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + // Default state should be valid (has removeJavaScript and removeEmbeddedFiles enabled) + expect(result.current.validateParameters()).toBe(true); + + // Turn off all parameters - should be invalid + act(() => { + result.current.updateParameter('removeJavaScript', false); + result.current.updateParameter('removeEmbeddedFiles', false); + }); + + expect(result.current.validateParameters()).toBe(false); + + // Turn on one parameter - should be valid again + act(() => { + result.current.updateParameter('removeLinks', true); + }); + + expect(result.current.validateParameters()).toBe(true); + }); + + test('should handle all parameter types correctly', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + const allParameters = Object.keys(defaultParameters) as (keyof typeof defaultParameters)[]; + + allParameters.forEach(param => { + act(() => { + result.current.updateParameter(param, true); + }); + expect(result.current.parameters[param]).toBe(true); + + act(() => { + result.current.updateParameter(param, false); + }); + expect(result.current.parameters[param]).toBe(false); + }); + }); +}); diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts new file mode 100644 index 000000000..ea3068c2d --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.ts @@ -0,0 +1,53 @@ +import { useState, useCallback } from 'react'; + +export interface SanitizeParameters { + removeJavaScript: boolean; + removeEmbeddedFiles: boolean; + removeXMPMetadata: boolean; + removeMetadata: boolean; + removeLinks: boolean; + removeFonts: boolean; +} + +export 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 validateParameters = useCallback(() => { + return Object.values(parameters).some(value => value === true); + }, [parameters]); + + const getEndpointName = () => { + return 'sanitize-pdf' + }; + + return { + parameters, + updateParameter, + resetParameters, + validateParameters, + getEndpointName, + }; +}; diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index cebdc5a23..9fa883490 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -9,11 +9,6 @@ import { extractErrorMessage } from '../../../utils/toolErrorHandler'; import { createOperation } from '../../../utils/toolOperationTracker'; import { ResponseHandler } from '../../../utils/toolResponseProcessor'; -export interface ValidationResult { - valid: boolean; - errors?: string[]; -} - // Re-export for backwards compatibility export type { ProcessingProgress, ResponseHandler }; @@ -64,9 +59,6 @@ export interface ToolOperationConfig { */ customProcessor?: (params: TParams, files: File[]) => Promise; - /** Validate parameters before execution. Return validation errors if invalid. */ - validateParams?: (params: TParams) => ValidationResult; - /** Extract user-friendly error messages from API errors */ getErrorMessage?: (error: any) => string; } @@ -129,14 +121,6 @@ export const useToolOperation = ( return; } - if (config.validateParams) { - const validation = config.validateParams(params); - if (!validation.valid) { - actions.setError(validation.errors?.join(', ') || 'Invalid parameters'); - return; - } - } - const validFiles = selectedFiles.filter(file => file.size > 0); if (validFiles.length === 0) { actions.setError(t('noValidFiles', 'No valid files to process')); @@ -186,7 +170,7 @@ export const useToolOperation = ( // Individual file processing - separate API call per file const apiCallsConfig: ApiCallsConfig = { endpoint: config.endpoint, - buildFormData: (file: File, params: TParams) => (config.buildFormData as any /* FIX ME */)(file, params), + buildFormData: (file: File, params: TParams) => (config.buildFormData as (params: TParams, file: File) => FormData /* FIX ME */)(params, file), filePrefix: config.filePrefix, responseHandler: config.responseHandler }; diff --git a/frontend/src/hooks/tools/split/useSplitOperation.ts b/frontend/src/hooks/tools/split/useSplitOperation.ts index 4979d0ea0..184289e18 100644 --- a/frontend/src/hooks/tools/split/useSplitOperation.ts +++ b/frontend/src/hooks/tools/split/useSplitOperation.ts @@ -1,7 +1,5 @@ -import { useCallback } from 'react'; -import axios from 'axios'; import { useTranslation } from 'react-i18next'; -import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; +import { useToolOperation } from '../shared/useToolOperation'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { SplitParameters } from '../../../components/tools/split/SplitSettings'; import { SPLIT_MODES } from '../../../constants/splitConstants'; @@ -66,17 +64,6 @@ export const useSplitOperation = () => { buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData filePrefix: 'split_', multiFileEndpoint: true, // Single API call with all files - validateParams: (params) => { - if (!params.mode) { - return { valid: false, errors: [t('split.validation.modeRequired', 'Split mode is required')] }; - } - - if (params.mode === SPLIT_MODES.BY_PAGES && !params.pages) { - return { valid: false, errors: [t('split.validation.pagesRequired', 'Page numbers are required for split by pages')] }; - } - - return { valid: true }; - }, getErrorMessage: createStandardErrorHandler(t('split.error.failed', 'An error occurred while splitting the PDF.')) }); }; diff --git a/frontend/src/hooks/tools/split/useSplitParameters.ts b/frontend/src/hooks/tools/split/useSplitParameters.ts index 72d479311..54003e79c 100644 --- a/frontend/src/hooks/tools/split/useSplitParameters.ts +++ b/frontend/src/hooks/tools/split/useSplitParameters.ts @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, type SplitType } from '../../../constants/splitConstants'; +import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode } from '../../../constants/splitConstants'; import { SplitParameters } from '../../../components/tools/split/SplitSettings'; export interface SplitParametersHook { @@ -63,4 +63,4 @@ export const useSplitParameters = (): SplitParametersHook => { validateParameters, getEndpointName, }; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index debd3f5b1..1de26814d 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/Convert.tsx b/frontend/src/tools/Convert.tsx index 3512ca8eb..88e50df23 100644 --- a/frontend/src/tools/Convert.tsx +++ b/frontend/src/tools/Convert.tsx @@ -122,7 +122,11 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { isVisible={true} isCollapsed={filesCollapsed} isCompleted={filesCollapsed} - completedMessage={hasFiles ? `${selectedFiles.length} ${t("filesSelected", "files selected")}` : undefined} + completedMessage={hasFiles ? + selectedFiles.length === 1 + ? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name }) + : t('filesSelected', '{{count}} files selected', { count: selectedFiles.length }) + : undefined} > { + const { t } = useTranslation(); + + const { selectedFiles } = useToolFileSelection(); + const { setCurrentMode } = useFileContext(); + + 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.files && onComplete) { + onComplete(sanitizeOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed')); + } + } + }; + + const handleSettingsReset = () => { + sanitizeOperation.resetResults(); + onPreviewFile?.(null); + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem('previousMode', 'sanitize'); + setCurrentMode('viewer'); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = sanitizeOperation.files.length > 0; + const filesCollapsed = hasFiles; + const settingsCollapsed = hasResults; + + return ( + + + {/* Files Step */} + + + + + {/* Settings Step */} + + + + + + + + + {/* Results Step */} + + + {sanitizeOperation.status && ( + {sanitizeOperation.status} + )} + + + + {sanitizeOperation.downloadUrl && ( + + )} + + ({ + file, + thumbnail: sanitizeOperation.thumbnails[index] + }))} + onFileClick={handleThumbnailClick} + isGeneratingThumbnails={sanitizeOperation.isGeneratingThumbnails} + title={t("sanitize.sanitizationResults", "Sanitization Results")} + /> + + + + + ); +} + +export default Sanitize; diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 6ced5a35c..555abdc4c 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -11,7 +11,7 @@ export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; export type ToolType = 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize'; -export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr'; +export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr' | 'sanitize'; export interface FileOperation { id: string;