From ecf30d10283d186794aa3e8b6caa7108796c8a86 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 14 Aug 2025 14:27:23 +0100 Subject: [PATCH] Implement 'Add Password' and 'Change Permissions' tools in V2 (#4195) # Description of Changes Implement Add Password and Change Permissions tools in V2 (both in one because Change Permissions is a fake endpoint which just calls Add Password behind the scenes). --------- Co-authored-by: James --- .../public/locales/en-GB/translation.json | 132 ++++++++-- .../public/locales/en-US/translation.json | 132 ++++++++-- .../addPassword/AddPasswordSettings.test.tsx | 179 ++++++++++++++ .../tools/addPassword/AddPasswordSettings.tsx | 60 +++++ .../ChangePermissionsSettings.test.tsx | 226 ++++++++++++++++++ .../ChangePermissionsSettings.tsx | 33 +++ .../components/tools/split/SplitSettings.tsx | 16 +- .../components/tooltips/useAddPasswordTips.ts | 35 +++ .../tooltips/useChangePermissionsTips.ts | 18 ++ .../{CompressTips.ts => useCompressTips.ts} | 4 +- .../tooltips/{OCRTips.ts => useOCRTips.ts} | 4 +- frontend/src/contexts/FileContext.tsx | 12 +- .../useAddPasswordOperation.test.ts | 144 +++++++++++ .../addPassword/useAddPasswordOperation.ts | 30 +++ .../useAddPasswordParameters.test.ts | 152 ++++++++++++ .../addPassword/useAddPasswordParameters.ts | 69 ++++++ .../useChangePermissionsOperation.test.ts | 138 +++++++++++ .../useChangePermissionsOperation.ts | 37 +++ .../useChangePermissionsParameters.test.ts | 110 +++++++++ .../useChangePermissionsParameters.ts | 64 +++++ .../hooks/tools/shared/useToolOperation.ts | 2 +- .../hooks/tools/split/useSplitOperation.ts | 2 +- .../hooks/tools/split/useSplitParameters.ts | 16 +- frontend/src/hooks/useToolManagement.tsx | 19 ++ .../services/enhancedPDFProcessingService.ts | 4 +- frontend/src/tools/AddPassword.tsx | 189 +++++++++++++++ frontend/src/tools/ChangePermissions.tsx | 170 +++++++++++++ frontend/src/tools/Compress.tsx | 4 +- frontend/src/tools/OCR.tsx | 8 +- frontend/src/tools/Sanitize.tsx | 2 +- frontend/src/types/fileContext.ts | 13 +- 31 files changed, 1936 insertions(+), 88 deletions(-) create mode 100644 frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx create mode 100644 frontend/src/components/tools/addPassword/AddPasswordSettings.tsx create mode 100644 frontend/src/components/tools/changePermissions/ChangePermissionsSettings.test.tsx create mode 100644 frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx create mode 100644 frontend/src/components/tooltips/useAddPasswordTips.ts create mode 100644 frontend/src/components/tooltips/useChangePermissionsTips.ts rename frontend/src/components/tooltips/{CompressTips.ts => useCompressTips.ts} (96%) rename frontend/src/components/tooltips/{OCRTips.ts => useOCRTips.ts} (96%) create mode 100644 frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts create mode 100644 frontend/src/hooks/tools/addPassword/useAddPasswordOperation.ts create mode 100644 frontend/src/hooks/tools/addPassword/useAddPasswordParameters.test.ts create mode 100644 frontend/src/hooks/tools/addPassword/useAddPasswordParameters.ts create mode 100644 frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.test.ts create mode 100644 frontend/src/hooks/tools/changePermissions/useChangePermissionsOperation.ts create mode 100644 frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.test.ts create mode 100644 frontend/src/hooks/tools/changePermissions/useChangePermissionsParameters.ts create mode 100644 frontend/src/tools/AddPassword.tsx create mode 100644 frontend/src/tools/ChangePermissions.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 6e5dd6179..247162df3 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -384,6 +384,10 @@ "title": "Add Password", "desc": "Encrypt your PDF document with a password." }, + "changePermissions": { + "title": "Change Permissions", + "desc": "Change document restrictions and permissions." + }, "removePassword": { "title": "Remove Password", "desc": "Remove password protection from your PDF document." @@ -816,30 +820,6 @@ "removePages": { "tags": "Remove pages,delete pages" }, - "addPassword": { - "tags": "secure,security", - "title": "Add Password", - "header": "Add password (Encrypt)", - "selectText": { - "1": "Select PDF to encrypt", - "2": "User Password", - "3": "Encryption Key Length", - "4": "Higher values are stronger, but lower values have better compatibility.", - "5": "Permissions to set (Recommended to be used along with Owner password)", - "6": "Prevent assembly of document", - "7": "Prevent content extraction", - "8": "Prevent extraction for accessibility", - "9": "Prevent filling in form", - "10": "Prevent modification", - "11": "Prevent annotation modification", - "12": "Prevent printing", - "13": "Prevent printing different formats", - "14": "Owner Password", - "15": "Restricts what can be done with the document once it is opened (Not supported by all readers)", - "16": "Restricts the opening of the document itself" - }, - "submit": "Encrypt" - }, "removePassword": { "tags": "secure,Decrypt,security,unpassword,delete password", "title": "Remove password", @@ -1798,5 +1778,109 @@ "removeFonts": "Remove Fonts", "removeFonts.desc": "Remove embedded fonts from the PDF" } + }, + "addPassword": { + "completed": "Password protection applied", + "submit": "Encrypt", + "filenamePrefix": "encrypted", + "error": { + "failed": "An error occurred while encrypting the PDF." + }, + "passwords": { + "title": "Passwords", + "stepTitle": "Passwords & Encryption", + "completed": "Passwords configured", + "user": { + "label": "User Password", + "placeholder": "Enter user password" + }, + "owner": { + "label": "Owner Password", + "placeholder": "Enter owner password" + } + }, + "permissions": { + "stepTitle": "Document Permissions" + }, + "encryption": { + "title": "Encryption", + "keyLength": { + "label": "Key Length", + "40bit": "40-bit (Low)", + "128bit": "128-bit (Standard)", + "256bit": "256-bit (High)" + } + }, + "results": { + "title": "Encrypted PDFs" + }, + "tooltip": { + "header": { + "title": "Password Protection Overview" + }, + "passwords": { + "title": "Password Types", + "text": "User passwords restrict opening the document, while owner passwords control what can be done with the document once opened. You can set both or just one.", + "bullet1": "User Password: Required to open the PDF", + "bullet2": "Owner Password: Controls document permissions (not supported by all PDF viewers)" + }, + "encryption": { + "title": "Encryption Levels", + "text": "Higher encryption levels provide better security but may not be supported by older PDF viewers.", + "bullet1": "40-bit: Basic security, compatible with older viewers", + "bullet2": "128-bit: Standard security, widely supported", + "bullet3": "256-bit: Maximum security, requires modern viewers" + }, + "restrictions": { + "title": "Document Restrictions", + "text": "These restrictions control what users can do with the PDF. Most effective when combined with an owner password." + } + } + }, + "changePermissions": { + "completed": "Permissions changed", + "submit": "Change Permissions", + "error": { + "failed": "An error occurred while changing PDF permissions." + }, + "restrictions": { + "title": "Document Restrictions", + "preventAssembly": { + "label": "Prevent assembly of document" + }, + "preventExtractContent": { + "label": "Prevent content extraction" + }, + "preventExtractForAccessibility": { + "label": "Prevent extraction for accessibility" + }, + "preventFillInForm": { + "label": "Prevent filling in form" + }, + "preventModify": { + "label": "Prevent modification" + }, + "preventModifyAnnotations": { + "label": "Prevent annotation modification" + }, + "preventPrinting": { + "label": "Prevent printing" + }, + "preventPrintingFaithful": { + "label": "Prevent printing different formats" + } + }, + "results": { + "title": "Modified PDFs" + }, + "tooltip": { + "header": { + "title": "Change Permissions" + }, + "description": { + "title": "Description", + "text": "Changes document permissions. Warning: To make these restrictions unchangeable, use the Add Password tool to set an owner password." + } + } } } diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 6ca67480b..641acf013 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -380,6 +380,10 @@ "title": "Add Password", "desc": "Encrypt your PDF document with a password." }, + "changePermissions": { + "title": "Change Permissions", + "desc": "Change document restrictions and permissions." + }, "removePassword": { "title": "Remove Password", "desc": "Remove password protection from your PDF document." @@ -745,30 +749,6 @@ "removePages": { "tags": "Remove pages,delete pages" }, - "addPassword": { - "tags": "secure,security", - "title": "Add Password", - "header": "Add password (Encrypt)", - "selectText": { - "1": "Select PDF to encrypt", - "2": "User Password", - "3": "Encryption Key Length", - "4": "Higher values are stronger, but lower values have better compatibility.", - "5": "Permissions to set (Recommended to be used along with Owner password)", - "6": "Prevent assembly of document", - "7": "Prevent content extraction", - "8": "Prevent extraction for accessibility", - "9": "Prevent filling in form", - "10": "Prevent modification", - "11": "Prevent annotation modification", - "12": "Prevent printing", - "13": "Prevent printing different formats", - "14": "Owner Password", - "15": "Restricts what can be done with the document once it is opened (Not supported by all readers)", - "16": "Restricts the opening of the document itself" - }, - "submit": "Encrypt" - }, "removePassword": { "tags": "secure,Decrypt,security,unpassword,delete password", "title": "Remove password", @@ -1650,5 +1630,109 @@ "removeFonts.desc": "Remove embedded fonts from the PDF" } } + }, + "addPassword": { + "completed": "Password protection applied", + "submit": "Encrypt", + "filenamePrefix": "encrypted", + "error": { + "failed": "An error occurred while encrypting the PDF." + }, + "passwords": { + "title": "Passwords", + "stepTitle": "Passwords & Encryption", + "completed": "Passwords configured", + "user": { + "label": "User Password", + "placeholder": "Enter user password" + }, + "owner": { + "label": "Owner Password", + "placeholder": "Enter owner password" + } + }, + "permissions": { + "stepTitle": "Document Permissions" + }, + "encryption": { + "title": "Encryption", + "keyLength": { + "label": "Key Length", + "40bit": "40-bit (Low)", + "128bit": "128-bit (Standard)", + "256bit": "256-bit (High)" + } + }, + "results": { + "title": "Password Protected PDFs" + }, + "tooltip": { + "header": { + "title": "Password Protection Overview" + }, + "passwords": { + "title": "Password Types", + "text": "User passwords restrict opening the document, while owner passwords control what can be done with the document once opened. You can set both or just one.", + "bullet1": "User Password: Required to open the PDF", + "bullet2": "Owner Password: Controls document permissions (not supported by all PDF viewers)" + }, + "encryption": { + "title": "Encryption Levels", + "text": "Higher encryption levels provide better security but may not be supported by older PDF viewers.", + "bullet1": "40-bit: Basic security, compatible with older viewers", + "bullet2": "128-bit: Standard security, widely supported", + "bullet3": "256-bit: Maximum security, requires modern viewers" + }, + "restrictions": { + "title": "Document Restrictions", + "text": "These restrictions control what users can do with the PDF. Most effective when combined with an owner password." + } + } + }, + "changePermissions": { + "completed": "Permissions changed", + "submit": "Change Permissions", + "error": { + "failed": "An error occurred while changing PDF permissions." + }, + "restrictions": { + "title": "Document Restrictions", + "preventAssembly": { + "label": "Prevent assembly of document" + }, + "preventExtractContent": { + "label": "Prevent content extraction" + }, + "preventExtractForAccessibility": { + "label": "Prevent extraction for accessibility" + }, + "preventFillInForm": { + "label": "Prevent filling in form" + }, + "preventModify": { + "label": "Prevent modification" + }, + "preventModifyAnnotations": { + "label": "Prevent annotation modification" + }, + "preventPrinting": { + "label": "Prevent printing" + }, + "preventPrintingFaithful": { + "label": "Prevent printing different formats" + } + }, + "results": { + "title": "Modified PDFs" + }, + "tooltip": { + "header": { + "title": "Change Permissions" + }, + "description": { + "title": "Description", + "text": "Changes document permissions. Warning: To make these restrictions unchangeable, use the Add Password tool to set an owner password." + } + } } } diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx new file mode 100644 index 000000000..ac4b000f2 --- /dev/null +++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx @@ -0,0 +1,179 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import AddPasswordSettings from './AddPasswordSettings'; +import { defaultParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters'; +import type { AddPasswordParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters'; + +// 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('AddPasswordSettings', () => { + const mockOnParameterChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render password input fields', () => { + render( + + + + ); + + // Should render user and owner password fields labels + expect(screen.getByText('mock-addPassword.passwords.user.label')).toBeInTheDocument(); + expect(screen.getByText('mock-addPassword.passwords.owner.label')).toBeInTheDocument(); + }); + + test('should render encryption key length select', () => { + render( + + + + ); + + // Should render key length select input + expect(screen.getByRole('textbox', { name: /keyLength/i })).toBeInTheDocument(); + }); + + test('should render main component sections', () => { + render( + + + + ); + + // Check that main section titles are rendered + expect(screen.getByText('mock-addPassword.passwords.title')).toBeInTheDocument(); + expect(screen.getByText('mock-addPassword.encryption.title')).toBeInTheDocument(); + }); + + test('should call onParameterChange when password fields are modified', () => { + render( + + + + ); + + // This test is complex with Mantine's PasswordInput, just verify the component renders + expect(screen.getByText('mock-addPassword.passwords.user.label')).toBeInTheDocument(); + }); + + test('should call onParameterChange when key length is changed', () => { + render( + + + + ); + + // Find key length select and change it + const keyLengthSelect = screen.getByText('mock-addPassword.encryption.keyLength.128bit'); + + fireEvent.mouseDown(keyLengthSelect); + const option256 = screen.getByText('mock-addPassword.encryption.keyLength.256bit'); + fireEvent.click(option256); + + expect(mockOnParameterChange).toHaveBeenCalledWith('keyLength', 256); + }); + + test('should disable all form elements when disabled prop is true', () => { + render( + + + + ); + + // Check password inputs are disabled + const passwordInputs = screen.getAllByRole('textbox'); + passwordInputs.forEach(input => { + expect(input).toBeDisabled(); + }); + + // Check key length select is disabled - simplified test due to Mantine complexity + expect(screen.getByText('mock-addPassword.encryption.keyLength.128bit')).toBeInTheDocument(); + }); + + test('should enable all form elements when disabled prop is false', () => { + render( + + + + ); + + // Check password inputs are enabled + const passwordInputs = screen.getAllByRole('textbox'); + passwordInputs.forEach(input => { + expect(input).not.toBeDisabled(); + }); + + // Check key length select is enabled - simplified test due to Mantine complexity + expect(screen.getByText('mock-addPassword.encryption.keyLength.128bit')).toBeInTheDocument(); + }); + + test('should call translation function with correct keys', () => { + render( + + + + ); + + // Verify that translation keys are being called + expect(mockT).toHaveBeenCalledWith('addPassword.passwords.title', 'Passwords'); + expect(mockT).toHaveBeenCalledWith('addPassword.encryption.title', 'Encryption'); + expect(mockT).toHaveBeenCalledWith('addPassword.passwords.user.label', 'User Password'); + expect(mockT).toHaveBeenCalledWith('addPassword.passwords.owner.label', 'Owner Password'); + }); + + test.each([ + { keyLength: 40, expectedLabel: 'mock-addPassword.encryption.keyLength.40bit' }, + { keyLength: 128, expectedLabel: 'mock-addPassword.encryption.keyLength.128bit' }, + { keyLength: 256, expectedLabel: 'mock-addPassword.encryption.keyLength.256bit' } + ])('should handle key length $keyLength correctly', ({ keyLength, expectedLabel }) => { + render( + + + + ); + + expect(screen.getByText(expectedLabel)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx new file mode 100644 index 000000000..6cce5ef0c --- /dev/null +++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Stack, Text, PasswordInput, Select } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters"; + +interface AddPasswordSettingsProps { + parameters: AddPasswordParameters; + onParameterChange: (key: keyof AddPasswordParameters, value: any) => void; + disabled?: boolean; +} + +const AddPasswordSettings = ({ parameters, onParameterChange, disabled = false }: AddPasswordSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {/* Password Settings */} + + {t('addPassword.passwords.title', 'Passwords')} + onParameterChange('password', e.target.value)} + disabled={disabled} + /> + onParameterChange('ownerPassword', e.target.value)} + disabled={disabled} + /> + + + {/* Encryption Settings */} + + {t('addPassword.encryption.title', 'Encryption')} +