mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
Fix encrypted pdf handling (#6088)
Fix and improve encrypted pdf handling
This commit is contained in:
69
frontend/package-lock.json
generated
69
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 = ({
|
||||
<Button variant="light" color="var(--mantine-color-gray-8)" onClick={onSkip} disabled={isProcessing}>
|
||||
{t("encryptedPdfUnlock.skip", "Skip for now")}
|
||||
</Button>
|
||||
<Button onClick={onUnlock} loading={isProcessing} disabled={password.trim().length === 0}>
|
||||
{t("encryptedPdfUnlock.unlock", "Unlock & Continue")}
|
||||
</Button>
|
||||
<Group gap="xs">
|
||||
{remainingCount > 0 && (
|
||||
<Button variant="light" onClick={onUnlockAll} loading={isProcessing} disabled={password.trim().length === 0}>
|
||||
{t("encryptedPdfUnlock.unlockAll", "Use for all ({{count}})", { count: remainingCount + 1 })}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onUnlock} loading={isProcessing} disabled={password.trim().length === 0}>
|
||||
{t("encryptedPdfUnlock.unlock", "Unlock & Continue")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -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 = ({
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text c="red">Error: No file provided to viewer</Text>
|
||||
</Center>
|
||||
) : isCurrentFileEncrypted ? (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Stack align="center" gap="md">
|
||||
<LockIcon style={{ fontSize: 48, opacity: 0.5 }} />
|
||||
<Text fw={500}>This PDF is password-protected</Text>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
if (currentFile && isStirlingFile(currentFile)) {
|
||||
actions.openEncryptedUnlockPrompt(currentFile.fileId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
{/* EmbedPDF Viewer */}
|
||||
|
||||
@@ -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<void> => {
|
||||
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}
|
||||
/>
|
||||
</FileActionsContext.Provider>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -116,6 +116,25 @@ export const useToolOperation = <TParams>(config: ToolOperationConfig<TParams>):
|
||||
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
|
||||
|
||||
@@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
BIN
frontend/src/core/tests/test-fixtures/encrypted.pdf
Normal file
BIN
frontend/src/core/tests/test-fixtures/encrypted.pdf
Normal file
Binary file not shown.
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user