From 76aa5c7e2f2606662a3695795fc665f2c4e155d8 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:20:43 +0100 Subject: [PATCH] Fix encrypted pdf handling (#6088) Fix and improve encrypted pdf handling --- frontend/package-lock.json | 69 ++--- .../public/locales/en-GB/translation.toml | 5 + .../shared/EncryptedPdfUnlockModal.tsx | 17 +- .../core/components/viewer/EmbedPdfViewer.tsx | 29 +- frontend/src/core/contexts/FileContext.tsx | 62 ++++ .../src/core/contexts/file/fileActions.ts | 16 + .../hooks/tools/shared/useToolOperation.ts | 19 ++ frontend/src/core/services/fileAnalyzer.ts | 62 +++- .../EncryptedPdfUnlockE2E.spec.ts | 285 ++++++++++++++++++ .../core/tests/test-fixtures/encrypted.pdf | Bin 0 -> 50417 bytes frontend/src/core/utils/thumbnailUtils.ts | 9 +- frontend/src/core/utils/toolErrorHandler.ts | 21 +- 12 files changed, 527 insertions(+), 67 deletions(-) create mode 100644 frontend/src/core/tests/encryptedPdf/EncryptedPdfUnlockE2E.spec.ts create mode 100644 frontend/src/core/tests/test-fixtures/encrypted.pdf diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d6a59cbed7..d6328aa1c9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -477,7 +477,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -526,7 +525,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -589,7 +587,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.9.1.tgz", "integrity": "sha512-DlFV2o+tv9S+j4TeBVkRaIjjE9o3Tq3+hvJNoIOFtl87cR77UVQqEIRqOf61yk85Y+T2LfmnVPWjNcMuiKUh8w==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/engines": "2.9.1", "@embedpdf/models": "2.9.1" @@ -685,7 +682,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.9.1.tgz", "integrity": "sha512-aNtXjI3NUwz7kdmWsQIWzuS1QdZmuHXGCc+Kwl9u5O0PAgoj74OLsgoNEcFzz9m1rljyq3WPVnLczO6ByiifpQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1", "@embedpdf/utils": "2.9.1" @@ -775,7 +771,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.9.1.tgz", "integrity": "sha512-3AcvSTT7fmqe1ve/FvR3lJ5q7t5JYmnnAg8LKc9ATsDjS9J5b0WE03Omz9a8/sL19iKq8xeR1+W28phgvlcKNw==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -793,7 +788,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.9.1.tgz", "integrity": "sha512-/wpdStr1NeyMCvAEMVSCPC0a3zaMd+TSK4u8INsIo3b1RoFfb9iTlBB+qW/aaxvZJ/C7MChQ7cLX6VSKXK/6JQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -869,7 +863,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.9.1.tgz", "integrity": "sha512-mtfu6uDxlz3+j0xPXfKyvuu8iCFjapPkbnx8vGQ0z2PBNAMm+05hsNIzxJSGMP2VCFo09SOz2zCs7ch9J6NeNg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -904,7 +897,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.9.1.tgz", "integrity": "sha512-+U3PSIUuNlIOTXzRhnPBP+Rx20sFOd3OPiowyI2EP/Kx/j5R/amgL/t2rjrpw9gjXEMEGsli9Fn4UqnVgMrPaQ==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -940,7 +932,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.9.1.tgz", "integrity": "sha512-dVLjiLGnZDo0xO7lZulLGl3cJ/mO7BcA3PGO2uMdhqSWK4tAF/DrakvwXdD581VBwXD/C25EJhxiNa2L7mU4wg==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1", "@embedpdf/utils": "2.9.1" @@ -1015,7 +1006,6 @@ "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.9.1.tgz", "integrity": "sha512-bVhBuZHTppKV+OB5lBLqXQv+5oW1A7kAIc5UzsImBwl6NpwH+2PdVkelfrF37yEqnEF/mdxobriWSP0aOVl93w==", "license": "MIT", - "peer": true, "dependencies": { "@embedpdf/models": "2.9.1" }, @@ -1118,7 +1108,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1162,7 +1151,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2021,7 +2009,6 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", @@ -2072,7 +2059,6 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -2149,7 +2135,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", "@mui/core-downloads-tracker": "^7.3.9", @@ -2597,7 +2582,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3513,7 +3497,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -3609,6 +3592,7 @@ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "license": "MIT", + "peer": true, "peerDependencies": { "acorn": "^8.9.0" } @@ -4436,7 +4420,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5024,7 +5007,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5035,7 +5017,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5116,7 +5097,6 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -5565,6 +5545,7 @@ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", + "peer": true, "dependencies": { "@vue/shared": "3.5.30" } @@ -5574,6 +5555,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.30", "@vue/shared": "3.5.30" @@ -5584,6 +5566,7 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/reactivity": "3.5.30", "@vue/runtime-core": "3.5.30", @@ -5596,6 +5579,7 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" @@ -5622,7 +5606,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5883,6 +5866,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -6164,7 +6148,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7076,7 +7059,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7539,15 +7521,15 @@ "version": "5.6.4", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/devtools-protocol": { "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7880,7 +7862,6 @@ "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -7990,7 +7971,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/espree": { "version": "11.2.0", @@ -8055,6 +8037,7 @@ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@typescript-eslint/types": "^8.2.0" @@ -8932,7 +8915,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.29.2" }, @@ -9213,6 +9195,7 @@ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.6" } @@ -9388,7 +9371,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -9950,7 +9932,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -10970,7 +10953,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11231,7 +11213,6 @@ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.3.tgz", "integrity": "sha512-j1+MTbHO17kKXJMGDnaiW1EMOiA4AprE8EML6QnbSds+XbqHR2CdHa8T+/zIriZSoXlkZH4R+A4gY29lb5hdlA==", "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", @@ -11253,7 +11234,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -11645,7 +11625,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11655,7 +11634,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11738,8 +11716,7 @@ "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-number-format": { "version": "5.4.5", @@ -11756,7 +11733,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12138,8 +12114,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12989,6 +12964,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">= 0.4" } @@ -13261,7 +13237,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13455,7 +13430,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -13523,7 +13497,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13774,7 +13747,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13949,7 +13921,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13963,7 +13934,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -14474,7 +14444,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/zod": { "version": "3.25.76", diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 693144ffd6..94248c215e 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -44,6 +44,8 @@ downloadPdf = "Download PDF" downloadUnavailable = "Download unavailable for this item" edit = "Edit" editYourNewFiles = "Edit your new file(s)" +encryptedFileBlocked = "File is password-protected. Unlock it first." +encryptedFilesBlocked = "{{count}} files are password-protected. Unlock them first." exportAndContinue = "Export & Continue" false = "False" fileSavedToDisk = "File saved to disk" @@ -3341,6 +3343,9 @@ successBodyWithName = "Password removed from {{fileName}}" successTitle = "Password removed" title = "Remove password to continue" unlock = "Unlock & Continue" +unlockAll = "Use for all ({{count}})" +unlockAllPartialFail = "Wrong password for: {{names}}" +unlockAllSuccess = "Unlocked {{count}} file(s)." unlockPrompt = "Unlock PDF to continue" [encryptedPdfUnlock.password] diff --git a/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx b/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx index 765f90704b..e726e10823 100644 --- a/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx +++ b/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx @@ -9,8 +9,10 @@ interface EncryptedPdfUnlockModalProps { password: string; errorMessage?: string | null; isProcessing: boolean; + remainingCount: number; onPasswordChange: (value: string) => void; onUnlock: () => void; + onUnlockAll: () => void; onSkip: () => void; } @@ -20,8 +22,10 @@ const EncryptedPdfUnlockModal = ({ password, errorMessage, isProcessing, + remainingCount, onPasswordChange, onUnlock, + onUnlockAll, onSkip, }: EncryptedPdfUnlockModalProps) => { const { t } = useTranslation(); @@ -75,9 +79,16 @@ const EncryptedPdfUnlockModal = ({ - + + {remainingCount > 0 && ( + + )} + + diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index 7d40b2b4d7..2df8bd351a 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Box, Center, Text, ActionIcon } from "@mantine/core"; +import { Box, Center, Text, ActionIcon, Button, Stack } from "@mantine/core"; import CloseIcon from "@mui/icons-material/Close"; +import LockIcon from "@mui/icons-material/Lock"; import { useFileState, useFileActions } from "@app/contexts/FileContext"; import { useFileWithUrl } from "@app/hooks/useFileWithUrl"; @@ -350,6 +351,13 @@ const EmbedPdfViewerContent = ({ } }, [previewFile, fileWithUrl]); + // Check if the current file is encrypted (gate the viewer to prevent PDFium crash) + const isCurrentFileEncrypted = React.useMemo(() => { + if (!currentFile || !isStirlingFile(currentFile)) return false; + const stub = selectors.getStirlingFileStub(currentFile.fileId); + return stub?.processedFile?.isEncrypted === true; + }, [currentFile, selectors]); + const bookmarkCacheKey = React.useMemo(() => { if (currentFile && isStirlingFile(currentFile)) { return currentFile.fileId; @@ -1045,7 +1053,7 @@ const EmbedPdfViewerContent = ({ console.log("[FormFill] Fetching form fields for:", currentFileId); fetchFormFields(currentFile, currentFileId ?? undefined); } - }, [isFormFillToolActive, currentFile, currentFileId, fetchFormFields]); + }, [isFormFillToolActive, currentFile, currentFileId, fetchFormFields, isCurrentFileEncrypted]); const sidebarWidthRem = 15; const commentsSidebarWidthRem = 18; @@ -1087,6 +1095,23 @@ const EmbedPdfViewerContent = ({
Error: No file provided to viewer
+ ) : isCurrentFileEncrypted ? ( +
+ + + This PDF is password-protected + + +
) : ( <> {/* EmbedPDF Viewer */} diff --git a/frontend/src/core/contexts/FileContext.tsx b/frontend/src/core/contexts/FileContext.tsx index a4ec2ec1e1..88f43690cd 100644 --- a/frontend/src/core/contexts/FileContext.tsx +++ b/frontend/src/core/contexts/FileContext.tsx @@ -371,6 +371,66 @@ function FileContextInner({ children, enablePersistence = true }: FileContextPro } }, [activeEncryptedFileId, unlockPassword, runAutomaticPasswordRemoval, t]); + const handleUnlockAll = useCallback(async () => { + if (!activeEncryptedFileId) return; + const pw = unlockPassword.trim(); + if (!pw) { + setUnlockError(t("encryptedPdfUnlock.required", "Enter the password to continue.")); + return; + } + + setIsUnlocking(true); + setUnlockError(null); + + const allIds = [activeEncryptedFileId, ...encryptedQueue]; + let successCount = 0; + const failedNames: string[] = []; + + for (const fileId of allIds) { + try { + await runAutomaticPasswordRemoval(fileId, pw); + dismissedEncryptedFilesRef.current.delete(fileId); + successCount++; + } catch { + const name = stateRef.current.files.byId[fileId]?.name ?? fileId; + failedNames.push(name); + } + } + + if (successCount > 0) { + alert({ + alertType: "success", + title: t("encryptedPdfUnlock.successTitle", "Password removed"), + body: t("encryptedPdfUnlock.unlockAllSuccess", { + defaultValue: "Unlocked {{count}} file(s).", + count: successCount, + }), + expandable: false, + isPersistentPopup: false, + }); + } + + if (failedNames.length > 0) { + setUnlockError( + t("encryptedPdfUnlock.unlockAllPartialFail", { + defaultValue: "Wrong password for: {{names}}", + names: failedNames.join(", "), + }), + ); + const failedIds = allIds.filter((id) => { + const name = stateRef.current.files.byId[id]?.name ?? id; + return failedNames.includes(name); + }); + setEncryptedQueue(failedIds.slice(1)); + setActiveEncryptedFileId(failedIds[0]); + } else { + setEncryptedQueue([]); + setActiveEncryptedFileId(null); + } + + setIsUnlocking(false); + }, [activeEncryptedFileId, encryptedQueue, unlockPassword, runAutomaticPasswordRemoval, t]); + const undoConsumeFilesWrapper = useCallback( async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise => { return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, indexedDB); @@ -520,8 +580,10 @@ function FileContextInner({ children, enablePersistence = true }: FileContextPro password={unlockPassword} errorMessage={unlockError} isProcessing={isUnlocking} + remainingCount={encryptedQueue.length} onPasswordChange={setUnlockPassword} onUnlock={handleUnlockSubmit} + onUnlockAll={handleUnlockAll} onSkip={handleUnlockSkip} /> diff --git a/frontend/src/core/contexts/file/fileActions.ts b/frontend/src/core/contexts/file/fileActions.ts index 17caafbb38..ac6d71d691 100644 --- a/frontend/src/core/contexts/file/fileActions.ts +++ b/frontend/src/core/contexts/file/fileActions.ts @@ -19,6 +19,7 @@ import { buildQuickKeySet } from "@app/contexts/file/fileSelectors"; import { StirlingFile } from "@app/types/fileContext"; import { fileStorage } from "@app/services/fileStorage"; import { zipFileService } from "@app/services/zipFileService"; +import { FileAnalyzer } from "@app/services/fileAnalyzer"; const DEBUG = process.env.NODE_ENV === "development"; const HYDRATION_CONCURRENCY = 2; let activeHydrations = 0; @@ -328,6 +329,21 @@ export async function addFiles( // Create new filestub with minimal metadata; hydrate thumbnails/processedFile asynchronously const fileStub = createNewStirlingFileStub(file, fileId); + // Early encryption detection for PDFs — set the flag before dispatch so the + // viewer gate and modal queue pick it up immediately instead of after hydration + if (file.type === "application/pdf") { + try { + if (await FileAnalyzer.isPDFUserPasswordProtected(file)) { + fileStub.processedFile = (fileStub.processedFile || { pages: [] }) as any; + fileStub.processedFile!.isEncrypted = true; + } + } catch (error) { + // Never block upload on analysis failure — but log so it's debuggable + // if an unencrypted file later appears to "hang" during processing. + console.warn("[FileActions] Early encryption detection failed for", file.name, error); + } + } + // Check for pending file path mapping from Tauri file dialog (desktop only) try { const { pendingFilePathMappings } = await import("@app/services/pendingFilePathMappings"); diff --git a/frontend/src/core/hooks/tools/shared/useToolOperation.ts b/frontend/src/core/hooks/tools/shared/useToolOperation.ts index 064dc3aa9f..e92dd874f1 100644 --- a/frontend/src/core/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/core/hooks/tools/shared/useToolOperation.ts @@ -116,6 +116,25 @@ export const useToolOperation = (config: ToolOperationConfig): return; } + // Block encrypted files from being sent to backend tools + const encryptedFiles = validFiles.filter((f) => { + const stub = selectors.getStirlingFileStub(f.fileId); + return stub?.processedFile?.isEncrypted === true; + }); + if (encryptedFiles.length > 0) { + for (const ef of encryptedFiles) { + fileActions.openEncryptedUnlockPrompt(ef.fileId); + } + actions.setError( + encryptedFiles.length === 1 + ? t("encryptedFileBlocked", "File is password-protected. Unlock it first.") + : t("encryptedFilesBlocked", "{{count}} files are password-protected. Unlock them first.", { + count: encryptedFiles.length, + }), + ); + return; + } + // Resolve the runtime endpoint from params (static string or function result). // Custom processors may omit endpoint entirely — result is undefined in that case. const runtimeEndpoint: string | undefined = config.endpoint diff --git a/frontend/src/core/services/fileAnalyzer.ts b/frontend/src/core/services/fileAnalyzer.ts index 7c8e62ee3c..f2ac0e6405 100644 --- a/frontend/src/core/services/fileAnalyzer.ts +++ b/frontend/src/core/services/fileAnalyzer.ts @@ -1,5 +1,24 @@ import { FileAnalysis, ProcessingStrategy } from "@app/types/processing"; import { pdfWorkerManager } from "@app/services/pdfWorkerManager"; +import type { PDFDocumentProxy } from "pdfjs-dist"; + +// Scan the last ~8KB of the PDF for an /Encrypt entry. The trailer lives near +// the tail of the file, so this is enough in practice while staying cheap. +// For files smaller than the window, the whole file is scanned. +function hasEncryptMarker(buffer: ArrayBuffer): boolean { + const TAIL_BYTES = 8 * 1024; + const offset = Math.max(0, buffer.byteLength - TAIL_BYTES); + const view = new Uint8Array(buffer, offset); + // "/Encrypt" as ASCII bytes + const needle = [0x2f, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74]; + outer: for (let i = 0; i <= view.length - needle.length; i++) { + for (let j = 0; j < needle.length; j++) { + if (view[i + j] !== needle[j]) continue outer; + } + return true; + } + return false; +} export class FileAnalyzer { private static readonly SIZE_THRESHOLDS = { @@ -55,30 +74,59 @@ export class FileAnalyzer { /** * Quick PDF analysis without full processing */ + /** + * Cheap encryption-only probe for the upload-time detection path. + * + * Looks for a /Encrypt entry in the last 8KB of the file (where the PDF + * trailer lives). If absent, the file is definitely not encrypted and we + * can skip a full pdf.js parse. If present, falls back to pdf.js so we can + * distinguish user-password (blocks open) from owner-password-only (opens + * fine) — only the former should prompt. + */ + static async isPDFUserPasswordProtected(file: File): Promise { + const arrayBuffer = await file.arrayBuffer(); + if (!hasEncryptMarker(arrayBuffer)) return false; + + let pdf: PDFDocumentProxy | undefined; + try { + pdf = await pdfWorkerManager.createDocument(arrayBuffer, { + stopAtErrors: false, + verbosity: 0, + }); + // pdf.js opened it — owner-password-only case, no prompt needed. + return false; + } catch (error) { + const errorMessage = error instanceof Error ? error.message.toLowerCase() : ""; + return errorMessage.includes("password") || errorMessage.includes("encrypted"); + } finally { + if (pdf) pdfWorkerManager.destroyDocument(pdf); + } + } + static async quickPDFAnalysis(file: File): Promise<{ pageCount: number; isEncrypted: boolean; isCorrupted: boolean; }> { + let pdf: PDFDocumentProxy | undefined; try { // For small files, read the whole file // For large files, try the whole file first (PDF.js needs the complete structure) const arrayBuffer = await file.arrayBuffer(); - const pdf = await pdfWorkerManager.createDocument(arrayBuffer, { + pdf = await pdfWorkerManager.createDocument(arrayBuffer, { stopAtErrors: false, // Don't stop at minor errors verbosity: 0, // Suppress PDF.js warnings }); const pageCount = pdf.numPages; - const isEncrypted = (pdf as any).isEncrypted; - - // Clean up using worker manager - pdfWorkerManager.destroyDocument(pdf); + // If pdf.js opened the document successfully, the user can view it — even if + // the PDF carries encryption dictionaries (owner-password-only case). We only + // flag isEncrypted when pdf.js *fails* to open the file (caught below). return { pageCount, - isEncrypted, + isEncrypted: false, isCorrupted: false, }; } catch (error) { @@ -91,6 +139,8 @@ export class FileAnalyzer { isEncrypted, isCorrupted: !isEncrypted, // If not encrypted, probably corrupted }; + } finally { + if (pdf) pdfWorkerManager.destroyDocument(pdf); } } diff --git a/frontend/src/core/tests/encryptedPdf/EncryptedPdfUnlockE2E.spec.ts b/frontend/src/core/tests/encryptedPdf/EncryptedPdfUnlockE2E.spec.ts new file mode 100644 index 0000000000..2bce945f24 --- /dev/null +++ b/frontend/src/core/tests/encryptedPdf/EncryptedPdfUnlockE2E.spec.ts @@ -0,0 +1,285 @@ +/** + * End-to-End Tests for Encrypted PDF Password Prompting + * + * Tests the EncryptedPdfUnlockModal flow when uploading password-protected PDFs. + * All backend API calls are mocked via page.route() — no real backend required. + * The Vite dev server must be running (handled by playwright.config.ts webServer). + */ + +import { test, expect, type Page } from "@playwright/test"; +import path from "path"; +import fs from "fs"; + +const FIXTURES_DIR = path.join(__dirname, "../test-fixtures"); +const ENCRYPTED_PDF = path.join(FIXTURES_DIR, "encrypted.pdf"); +const SAMPLE_PDF = path.join(FIXTURES_DIR, "sample.pdf"); + +// Minimal valid PDF returned by the mocked remove-password endpoint +const FAKE_UNLOCKED_PDF = Buffer.from( + "%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" + + "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" + + "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" + + "xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n" + + "0000000115 00000 n \ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF", +); + +// --------------------------------------------------------------------------- +// Helper: mock all standard app APIs needed to load the main UI +// --------------------------------------------------------------------------- +async function mockAppApis(page: Page) { + await page.route("**/api/v1/info/status", (route) => route.fulfill({ json: { status: "UP" } })); + + await page.route("**/api/v1/config/app-config", (route) => + route.fulfill({ + json: { enableLogin: false, languages: ["en-GB"], defaultLocale: "en-GB" }, + }), + ); + + await page.route("**/api/v1/auth/me", (route) => + route.fulfill({ + json: { id: 1, username: "testuser", email: "test@example.com", roles: ["ROLE_USER"] }, + }), + ); + + await page.route("**/api/v1/config/endpoints-availability", (route) => route.fulfill({ json: {} })); + + await page.route("**/api/v1/config/endpoint-enabled*", (route) => route.fulfill({ json: true })); + + await page.route("**/api/v1/config/group-enabled*", (route) => route.fulfill({ json: true })); + + await page.route("**/api/v1/ui-data/footer-info", (route) => route.fulfill({ json: {} })); + + await page.route("**/api/v1/proprietary/**", (route) => route.fulfill({ json: {} })); +} + +// --------------------------------------------------------------------------- +// Helper: mock the remove-password endpoint to succeed +// --------------------------------------------------------------------------- +function mockRemovePasswordSuccess(page: Page) { + return page.route("**/api/v1/security/remove-password", (route) => + route.fulfill({ + status: 200, + contentType: "application/pdf", + headers: { "Content-Disposition": 'attachment; filename="encrypted.pdf"' }, + body: FAKE_UNLOCKED_PDF, + }), + ); +} + +// --------------------------------------------------------------------------- +// Helper: mock the remove-password endpoint to fail with wrong password +// --------------------------------------------------------------------------- +function mockRemovePasswordWrongPassword(page: Page) { + return page.route("**/api/v1/security/remove-password", (route) => + route.fulfill({ + status: 400, + contentType: "application/problem+json", + body: JSON.stringify({ + type: "/errors/pdf-password", + title: "PDF password incorrect", + status: 400, + detail: "The PDF is passworded and requires the correct password to open.", + }), + }), + ); +} + +// --------------------------------------------------------------------------- +// Helper: upload a file through the Files modal and wait for it to close +// --------------------------------------------------------------------------- +async function uploadFile(page: Page, filePath: string) { + await page.getByTestId("files-button").click(); + await page.waitForSelector(".mantine-Modal-overlay", { state: "visible", timeout: 5000 }); + await page.locator('[data-testid="file-input"]').setInputFiles(filePath); + // Modal auto-closes after file is selected + await page.waitForSelector(".mantine-Modal-overlay", { state: "hidden", timeout: 10000 }); +} + +// --------------------------------------------------------------------------- +// Helper: upload encrypted file — the Files modal closes, then the unlock +// modal should appear on top. We don't wait for the Files modal to vanish +// since the unlock modal may appear while it is still closing. +// --------------------------------------------------------------------------- +async function uploadEncryptedFile(page: Page, filePath: string) { + await page.getByTestId("files-button").click(); + await page.waitForSelector(".mantine-Modal-overlay", { state: "visible", timeout: 5000 }); + await page.locator('[data-testid="file-input"]').setInputFiles(filePath); +} + +// --------------------------------------------------------------------------- +// Selectors for the unlock modal (Mantine Modal with known text content) +// --------------------------------------------------------------------------- +const MODAL_TITLE = "Remove password to continue"; +const PASSWORD_PLACEHOLDER = "Enter the PDF password"; +const UNLOCK_BUTTON_TEXT = "Unlock & Continue"; +const SKIP_BUTTON_TEXT = "Skip for now"; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- +test.describe.configure({ mode: "serial" }); + +test.describe("Encrypted PDF Unlock Modal", () => { + test.beforeEach(async ({ page }) => { + await mockAppApis(page); + await page.goto("/?bypassOnboarding=true"); + await page.waitForSelector('[data-testid="files-button"]', { timeout: 10000 }); + + // Dismiss onboarding tooltip if it appears (can block clicks in Firefox/WebKit) + const tooltip = page.locator('button:has-text("Close tooltip")'); + if (await tooltip.isVisible({ timeout: 1000 }).catch(() => false)) { + await tooltip.click(); + } + }); + + test("uploading an encrypted PDF shows the unlock modal", async ({ page }) => { + await uploadEncryptedFile(page, ENCRYPTED_PDF); + + // The unlock modal should appear with the expected title + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + await expect(page.getByPlaceholder(PASSWORD_PLACEHOLDER)).toBeVisible(); + await expect(page.getByRole("button", { name: UNLOCK_BUTTON_TEXT })).toBeVisible(); + await expect(page.getByRole("button", { name: SKIP_BUTTON_TEXT })).toBeVisible(); + }); + + test("unlock button is disabled when password field is empty", async ({ page }) => { + await uploadEncryptedFile(page, ENCRYPTED_PDF); + + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + const unlockBtn = page.getByRole("button", { name: UNLOCK_BUTTON_TEXT }); + await expect(unlockBtn).toBeDisabled(); + }); + + test("unlock button becomes enabled after entering a password", async ({ page }) => { + await uploadEncryptedFile(page, ENCRYPTED_PDF); + + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + const passwordInput = page.getByPlaceholder(PASSWORD_PLACEHOLDER); + await passwordInput.fill("somepassword"); + + const unlockBtn = page.getByRole("button", { name: UNLOCK_BUTTON_TEXT }); + await expect(unlockBtn).toBeEnabled(); + }); + + test("successful unlock removes the modal and shows success alert", async ({ page }) => { + await mockRemovePasswordSuccess(page); + + await uploadEncryptedFile(page, ENCRYPTED_PDF); + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + await page.getByPlaceholder(PASSWORD_PLACEHOLDER).fill("testpass123"); + await page.getByRole("button", { name: UNLOCK_BUTTON_TEXT }).click(); + + // Modal should close after successful unlock + await expect(page.getByText(MODAL_TITLE)).toBeHidden({ timeout: 10000 }); + + // Success alert should appear + await expect(page.getByText("Password removed", { exact: true })).toBeVisible({ timeout: 5000 }); + }); + + test("incorrect password shows error message in modal", async ({ page }) => { + await mockRemovePasswordWrongPassword(page); + + await uploadEncryptedFile(page, ENCRYPTED_PDF); + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + await page.getByPlaceholder(PASSWORD_PLACEHOLDER).fill("wrongpassword"); + await page.getByRole("button", { name: UNLOCK_BUTTON_TEXT }).click(); + + // Error message should appear within the modal + await expect(page.getByText("Incorrect password")).toBeVisible({ timeout: 5000 }); + + // Modal should remain open + await expect(page.getByText(MODAL_TITLE)).toBeVisible(); + }); + + test("skip button closes the modal without unlocking", async ({ page }) => { + await uploadEncryptedFile(page, ENCRYPTED_PDF); + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + await page.getByRole("button", { name: SKIP_BUTTON_TEXT }).click(); + + // Modal should close + await expect(page.getByText(MODAL_TITLE)).toBeHidden({ timeout: 5000 }); + }); + + test("pressing Enter in password field triggers unlock", async ({ page }) => { + await mockRemovePasswordSuccess(page); + + await uploadEncryptedFile(page, ENCRYPTED_PDF); + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + const passwordInput = page.getByPlaceholder(PASSWORD_PLACEHOLDER); + await passwordInput.fill("testpass123"); + await passwordInput.press("Enter"); + + // Modal should close after successful unlock via Enter key + await expect(page.getByText(MODAL_TITLE)).toBeHidden({ timeout: 10000 }); + }); + + test("uploading a normal PDF does not show the unlock modal", async ({ page }) => { + await uploadFile(page, SAMPLE_PDF); + + // Wait for the file to finish processing, then verify no unlock modal appeared + await page.waitForTimeout(3000); + await expect(page.getByText(MODAL_TITLE)).toBeHidden(); + }); + + test("unlock all button is hidden with only one encrypted file", async ({ page }) => { + await uploadEncryptedFile(page, ENCRYPTED_PDF); + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + + // The "Use for all" button should NOT appear with only one file + await expect(page.getByRole("button", { name: /Use for all/ })).toBeHidden(); + }); + + test("unlock all button appears with multiple encrypted files and unlocks all", async ({ page }) => { + await mockRemovePasswordSuccess(page); + + // Upload two encrypted files at once (different names to avoid deduplication) + await page.getByTestId("files-button").click(); + await page.waitForSelector(".mantine-Modal-overlay", { state: "visible", timeout: 5000 }); + await page.locator('[data-testid="file-input"]').setInputFiles([ + { name: "encrypted-a.pdf", mimeType: "application/pdf", buffer: fs.readFileSync(ENCRYPTED_PDF) }, + { name: "encrypted-b.pdf", mimeType: "application/pdf", buffer: fs.readFileSync(ENCRYPTED_PDF) }, + ]); + + // The unlock modal should appear for the first file with "Use for all" visible + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + const unlockAllBtn = page.getByRole("button", { name: /Use for all/ }); + await expect(unlockAllBtn).toBeVisible({ timeout: 10000 }); + + // Enter password and click unlock all + await page.getByPlaceholder(PASSWORD_PLACEHOLDER).fill("testpass123"); + await unlockAllBtn.click(); + + // Modal should close — all files unlocked + await expect(page.getByText(MODAL_TITLE)).toBeHidden({ timeout: 10000 }); + }); + + test("unlock all with wrong password shows which files failed", async ({ page }) => { + await mockRemovePasswordWrongPassword(page); + + // Upload two encrypted files at once (different names to avoid deduplication) + await page.getByTestId("files-button").click(); + await page.waitForSelector(".mantine-Modal-overlay", { state: "visible", timeout: 5000 }); + await page.locator('[data-testid="file-input"]').setInputFiles([ + { name: "encrypted-a.pdf", mimeType: "application/pdf", buffer: fs.readFileSync(ENCRYPTED_PDF) }, + { name: "encrypted-b.pdf", mimeType: "application/pdf", buffer: fs.readFileSync(ENCRYPTED_PDF) }, + ]); + + // The unlock modal should appear with "Use for all" + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 10000 }); + const unlockAllBtn = page.getByRole("button", { name: /Use for all/ }); + await expect(unlockAllBtn).toBeVisible({ timeout: 10000 }); + + await page.getByPlaceholder(PASSWORD_PLACEHOLDER).fill("wrongpassword"); + await unlockAllBtn.click(); + + // Modal should remain open with error about failed files + await expect(page.getByText(MODAL_TITLE)).toBeVisible({ timeout: 5000 }); + await expect(page.getByText(/Wrong password for/)).toBeVisible({ timeout: 5000 }); + }); +}); diff --git a/frontend/src/core/tests/test-fixtures/encrypted.pdf b/frontend/src/core/tests/test-fixtures/encrypted.pdf new file mode 100644 index 0000000000000000000000000000000000000000..28fb757bd2e15c6b6f9862a407b8f20e39c36f55 GIT binary patch literal 50417 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 literal 0 HcmV?d00001 diff --git a/frontend/src/core/utils/thumbnailUtils.ts b/frontend/src/core/utils/thumbnailUtils.ts index c7970ed643..da25483d04 100644 --- a/frontend/src/core/utils/thumbnailUtils.ts +++ b/frontend/src/core/utils/thumbnailUtils.ts @@ -554,11 +554,8 @@ async function generatePDFThumbnail(arrayBuffer: ArrayBuffer, file: File, scale: pdfWorkerManager.destroyDocument(pdf); return thumbnail; } catch (error) { - if (error instanceof Error) { - // Check if PDF is encrypted - if (error.name === "PasswordException") { - return generateEncryptedPDFThumbnail(file); - } + if (error && typeof error === "object" && (error as any).name === "PasswordException") { + return generateEncryptedPDFThumbnail(file); } throw error; // Not an encryption issue, re-throw } @@ -693,7 +690,7 @@ export async function generateThumbnailWithMetadata( pdfWorkerManager.destroyDocument(pdf); return { thumbnail, pageCount, pageRotations, pageDimensions }; } catch (error) { - if (error instanceof Error && error.name === "PasswordException") { + if (error && typeof error === "object" && (error as any).name === "PasswordException") { // Handle encrypted PDFs const thumbnail = generateEncryptedPDFThumbnail(file); return { thumbnail, pageCount: 1, isEncrypted: true }; diff --git a/frontend/src/core/utils/toolErrorHandler.ts b/frontend/src/core/utils/toolErrorHandler.ts index 355033c290..95a6a8f063 100644 --- a/frontend/src/core/utils/toolErrorHandler.ts +++ b/frontend/src/core/utils/toolErrorHandler.ts @@ -98,10 +98,29 @@ export const handlePasswordError = async ( const status = error?.response?.status; // Handle specific error cases with user-friendly messages + // Backend returns 400 with PdfPasswordException for incorrect/missing PDF passwords if (status === 500) { - // 500 typically means incorrect password for encrypted PDFs return incorrectPasswordMessage; } + if (status === 400) { + const data = error?.response?.data; + // ProblemDetail JSON has type "/errors/pdf-password", blob needs parsing + const isPasswordError = await (async () => { + if (data instanceof Blob) { + try { + const text = await data.text(); + return text.includes("pdf-password") || text.includes("passworded"); + } catch { + return false; + } + } + const type = data?.type ?? ""; + return type.includes("pdf-password"); + })(); + if (isPasswordError) { + return incorrectPasswordMessage; + } + } // For other errors, try to extract the message const normalizedData = await normalizeAxiosErrorData(error?.response?.data);