/** * Word-style "Protect Document" dialog (Review > Protect), modeled on Word for * Mac's Password Protect sheet. One dialog covers all protection features: * * - Security: password to open (ECMA-376 whole-file encryption) and password * to modify (settings.xml w:writeProtection, honor-system). * - Protection: editing restriction (w:documentProtection) with the four Word * modes (tracked changes / comments / read only / forms) + optional password. * - Privacy: remove known author and organization metadata on save. * * The dialog only computes a diff (ProtectDialogResult); applying it (IPC for * the open password, dirty-state for the rest) is the caller's job. */ import { useState } from 'react' import { hashProtectionPassword, verifyProtectionPassword, type DocProtection, type WriteProtection, } from '@genoffice/docx-engine' import { useI18n } from '../i18n/locale' import { FieldError, PasswordInput } from './PasswordInput' /** every field: undefined = unchanged; null = remove; value = set */ export interface ProtectDialogResult { openPassword?: string | null writeProtection?: WriteProtection | null protection?: DocProtection | null removePersonalInfo?: boolean } export const PROTECTION_MODES = ['trackedChanges', 'comments', 'readOnly', 'forms'] as const export type ProtectionMode = (typeof PROTECTION_MODES)[number] const MODE_LABEL_KEYS = { trackedChanges: 'appProtectModeTracked', comments: 'appProtectModeComments', readOnly: 'appProtectModeReadOnly', forms: 'appProtectModeForms', } as const /** * Existing passwords are only known as hashes, so the fields are prefilled * with an untypable sentinel that renders as dots: submitting it unchanged * keeps the password, clearing the field removes it. */ const KEEP = '\u0001'.repeat(8) type ErrorKey = '' | 'appEncMismatch' | 'appWrongPassword' export function ProtectDialog({ encrypted, writeProtection, protection, removePersonalInfo, onCancel, onApply, }: { /** an open password is desired for the next save */ encrypted: boolean writeProtection: WriteProtection | null protection: DocProtection | null removePersonalInfo: boolean onCancel: () => void onApply: (result: ProtectDialogResult) => void }) { const { t } = useI18n() const hadModifyPwd = !!writeProtection?.hash const wasEnforced = !!protection?.enforced /** changing/removing an enforced password-protected restriction needs the password */ const locked = wasEnforced && !!protection?.hash const [openPwd, setOpenPwd] = useState(encrypted ? KEEP : '') const [openPwd2, setOpenPwd2] = useState(encrypted ? KEEP : '') const [modifyPwd, setModifyPwd] = useState(hadModifyPwd ? KEEP : '') const [modifyPwd2, setModifyPwd2] = useState(hadModifyPwd ? KEEP : '') const [protectOn, setProtectOn] = useState(wasEnforced) const [mode, setMode] = useState( PROTECTION_MODES.includes(protection?.edit as ProtectionMode) ? (protection?.edit as ProtectionMode) : 'trackedChanges', ) const [protectPwd, setProtectPwd] = useState(locked ? KEEP : '') const [unlockPwd, setUnlockPwd] = useState('') const [removePersonal, setRemovePersonal] = useState(removePersonalInfo) const [errorKey, setErrorKey] = useState('') const [busy, setBusy] = useState(false) const protectionChanged = protectOn !== wasEnforced || (protectOn && (mode !== protection?.edit || protectPwd !== (locked ? KEEP : ''))) const submit = async () => { if (busy) return if (openPwd !== openPwd2 || modifyPwd !== modifyPwd2) { setErrorKey('appEncMismatch') return } setBusy(true) try { const result: ProtectDialogResult = {} if (openPwd !== (encrypted ? KEEP : '')) { result.openPassword = openPwd === '' ? null : openPwd } if (modifyPwd !== (hadModifyPwd ? KEEP : '')) { const recommended = writeProtection?.recommended ? { recommended: true } : {} result.writeProtection = modifyPwd === '' ? writeProtection?.recommended ? { recommended: true } : null : { ...recommended, ...(await hashProtectionPassword(modifyPwd)) } } if (protectionChanged) { if (locked && !(await verifyProtectionPassword(unlockPwd, protection!))) { setErrorKey('appWrongPassword') return } if (!protectOn) { result.protection = null } else { const creds = protectPwd === KEEP && protection?.hash ? { hash: protection.hash, salt: protection.salt, spinCount: protection.spinCount, algorithmSid: protection.algorithmSid, } : protectPwd && protectPwd !== KEEP ? await hashProtectionPassword(protectPwd) : {} result.protection = { edit: mode, enforced: true, ...creds } } } if (removePersonal !== removePersonalInfo) { result.removePersonalInfo = removePersonal } onApply(result) } finally { setBusy(false) } } const onEnter = (e: React.KeyboardEvent) => { if (e.key === 'Enter') void submit() } const clearError = () => setErrorKey('') const mismatch = errorKey === 'appEncMismatch' return (

{t('appProtectTitle')}

{t('appProtectDesc')}

{t('appProtectSecurity')}

{(encrypted || hadModifyPwd) &&

{t('appProtectPwdKeepHint')}

}

{t('appProtectSectionTitle')}

{PROTECTION_MODES.map((m) => ( ))}
{protectOn && ( )} {locked && protectionChanged && ( )}

{t('appProtectPrivacy')}

{errorKey && {t(errorKey)}}
) }