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 zcmY!laBdQsl{CS&iSQzB?^XI`rer-#R}01CJF`$K?<=*T8%NZ zIwzJS=H#byLCjD9nH_|r0b~ToPLR8t^Ycnl^GZM_n?g<2cP`2=aLTVxh*mJrGXP^V zLn8%qOCyC?E`8tBl*~k&vO%fE`K3k4sl^IrAk(!YXTCbXXm7qrKXvEsDfe%x z-#YOt?La-h;Xbw-u2-jSxOL%sOL%;NqgT^L@4dOHtK?PQ-8r{s@uS35vwIa&o5gbr zA8_)_y7Rq!KX=CQ^WT<83V5pt-VL)epDfaHG3!BaNoCPzCrg3L53Wcp{QBi^u{XEj z)o69sr>AaRo0A?V@agxy{{C4m_xtYb5p&<=Z@a-}we$rpHQkG=TpjMlxmfw8Ocv|# zFKcK_^}b`*yl>W~#1~cvwq^CIHEy|QqA*Qxp0up+rzwBRr{0L{u;G}TFa0v^(k2rn zg-KyIZT|^*Ei#ClV6l&*!Qx!{Lxa~YZ;q^7?!?gihj&ud^;>UN@^$S`zEa$(@y^S2 zQ&#u619NKT@3wb)GGBd7L+IvRLciXq$Ly&89Al%xmI=06Z+b%v@Fe1E7FTc`{=JDbH4B$v`mb=6)Vcq z`qSyys#@QBYM(;;wg%XqAM#sZqE*7bn}LxZmdvTM0dTd!by>d*SK4 ze^YKxbFmmP(BxaT+;dOCS=-Cj-`KEim)ntdG1vE9?_y!xu5jxF zGY4O&@R^uRhXmb!-2bR_h5M%ek-3?NDy@?5@^qT4Gd^me|7!mfJLZbna$%V=2fi1j zFQ`dxyzO;U%j;p<#no$gBKtOL+43FPZ)L3eSi<4vW4})sq6#jc!%$770x+cHxx}_g)u# zWc6>mit?w_god9x;w}-;du=t#sb!b#)9gO@ zzIec!sC7rTbboh|#7fRfZ{AxeOn!Bw$W89or}N&|`vCx#t{Q z`+va`!KJK|L=qmEefzy{cHhPWOWWBmsYX3ZPCT*yjy>~!f!?4gr{`RmE4-BdzVnhz z;(ptmthpE^E5)`Z>UiosD4&*fLiO*tm}tw(lbsj3^KNu~$o}xLo$cLsj(TS#R$j`f zZb&(0nfc$wujkaPz*p6Mf0qb~#0Lk?^A%ZRaqWoMlJw})ipD<|MbG@#_^a}QN7&q* z^(UMDHe3?ClyuGUr19>~8(UxVG;2>;GKcq(0)v9oj;Jk+g9Ddtpi)32;D>x}L zOFihG&Xu~~cl00WaICcdm+|e|L-P!i6M5%6bPw)LKlT2;{!#15IdW5*Jl9s~&-;35 z(zjcRssD_VB>B#~<}vi&C&FJmCq+R0NJ7^#lZC;vBvsrux-oT~m^o+W{r$HtpZxZ~ zyVT9?jKX_|?3no7!rP9Wy0>(BnDBh&<2h@E?s4@m_0(ED`9-rqw&eexY5Ue6;LJUv z+x+BS)bb7MTjsU?nBX{V%>mY*49Aq~UzWDqUG3c%p}ru(#p7M{^rkH#fk*2oVw#@R^7|oes)u* zz{_v5Y+~fUc8O>nzqP3#g7@hBB>`3Mj&}I$JCNFbCHuc6MC)5&lV8smTxt&)mfHR6_+rQ0)P(<`A_iNCV6ur^Mn^H;`fa z&YmtHhPIm_m%dACadK*2N@89KNC;F7g6zVs*4fh~xU#q;HP z1q-mt^n+5kTf_9JWCRDGLs$i(sRHSb#O^) zZkU20q}2eewn1tU4g`B8xHPE*+)#lAC#Zfmf>rY%^<4Vl3WlJ@2`ExPO$XP!%pZDHwR<@sGX1>nwJTx?iCC{4V54iBN0&x3UP4M8Yib&7+NGJ8l@T-rI?sl z!kbi}H~_0KNj6F~F)_3-Ldb!G2qYP;ple}j3=T>K5W~{gLLnBEB0-YiU;{BgX#!7! z#8AQ1%+wO@`Cv%11lC$HGBL75Zmt~Q6ZpP+ooUL~Y!eQdp?E&K{Dr ztk|+D?5DnBoW5k5;&;oM>{3~yzCT7zZZbC{7PlQq67uKcd7`)a#t-ctGI`hVY_Rs8T3T%)3AW&iKi%(jDn?mjfS8>8_0 zm(dc}=Z*<^I!Bh4dFF~u@x1nuv&)40k=K$(3-&*DyYSm_+P>X}c?S;^3ubZ!Eec5M z@Lo`S{D;)73T=bLiPKD43)b(snO@^KhjZ~$UK^E&LzTzFLKFnezg zwWo^5!z|;^=scZ#q5Y}+!Hs7$t9=yrSnzDvWBB3jpWaK%J##`knYAvjN^d-`#md;b z_Hyg@xM#CeZ}+OjM6AEdH{;IhYMnjOGHH|7UQrNA`|fo2qHFs~3&wWaB~r1Ik6E1A zVQzb$P5et}{4T#tW$#xWJY4&&WXxW0Zhd(qJ|!UkX-VReGoNhzY^!&j*_~B-TC(nZ zfJ+Ti@7CEWn)7u}eX1@wBGN4KHGR494vzKKpWm+&Jh*kk*ZTQY2k&lr;x+l;i>9j_ zo8Ni=l$oWsVM2N5yPcbyAI}$k{cnedD!ZzkSv`{;zrmWQmj-29i+F$c2_3lq)$!E3 zx~qB-n)}Tz&OIjlkS$o2+vl6=K|RhVXBGzTYMXU%Mb+~kb8oH``FbaF#f=wD=@yIA z%Y^M@7d^L+mN%G~$F+LuY0mxIdE6ImC{^Zdo-4gY$ss#YHv7n$yo|RGRQtbAF?-$k zHh$#^s}&iyZh3myoff&9zMawg-JBn5`A>HI{=ZtSH21>pi|(_YgkJv6G^N4+=L64c z(%TvwoR69tYZhISIg)jLYuJLvH+l6|&p25Hh)sl=iKCLtqW~q#p-6|*2~SRJiJh{>eh6dQ%s_=7ea+~dXyMSwkoyXdO4>z z^Is!}!K7@{oXn56WnRsddR#5O`0c68liyX#+*ft7w-rk)|L{j<-$bj_{`8lvk>dBl z&lD~_bvZ}GUuJ8$(MHL>)1J{QW-L(;I1;!i^Tnt7^VXfaE?ukCy0CDM@tpVbm?GuB zw51+KZE7RgJVzE-kh-3@?F9c*PUBk7)-?yzU1)CLv|A_@x}Y^@4L_NjqjH9R>dXj zLs({9v$@JBqF>Ls`cvPY&of%~2Um5te~?vNQt~JD%EEI_f)oEP3G->ppPtn@>$QKF zukCZT;&qD`X~w(wurL_ zcZ9I~$-3!3`+~LO#jY}W|L-h|Jc0|Fq;H&QYu%i%qoDrRIYqPiK4HsuM}7@nY_{09 zcB;#|TA7Yw)&3@n^64V)WbQoEeVcWkD_7+LU&y796ojQ;$#jm1Wt9_LW8^3QRTJPaQISuC#NnUfJ=O<5+HE!MByFS*riuM0ZWBdHmkD zoxh~$i_*h2?01wT88x@AG}C%sl($TO%gJvqt}WZT*hDqIyCUYplZB7BH5@RI`(rKg z(ZlA$BaMmi1_hU|^d5Xw<@sfG*x_TobxIs-x2>?~OXR5dC%*G-%j-(+AemN zFLtNc%~cJZ=J&+Xuh#0CYvr5lyP#Y7L$E)3hAl_V#cwm6_s4C2cPsj)I@2u6g80tt zdu7Y~Y+r1!UG8R6C#(85P>RKHPq$6dPU{nMOuzqpx@w2N-=WSAR~Td0ru=Z1_^YDL zxg@zqEj{q7%YvR$yALQjuAh2ZOmSmWz_N>TTDMI$|1f!N@SWb-9}Zr+a z4`y%m{j8cGxugHv^Sz+~#;-m9iaU3m%-wN4bj_5lZ!E+PP34N#IVLeX^jzhUaL>iH zo3@;;EU}s9Tm8r~cbj0&LqB?tf5bUnrm2`t7%x+S(l%@(T|)zuD|?`1h_$ z??em4`{%E*U}kMzqSzL_pe$ zVVRS^&vZQX_eo&(o-!uccrLg1;(7AbGiGgBR$zLOiEnMxcK*6q9zXP+oRm9%SNHJ8 zgNJIxk6(G>zQB;fd`j>c+lcp47k(MkDE)CSRhz2ovcb%+FU-oTd+~MOfCI1QuT%8Z zNt`s{>aE{Cjv9>*z6;!KOWt=^%75mH60v{Yg*Qbldkh5<&$|72@Kd9pB=tmXn(LAu zH}x;BjNg&#aoci!tm?|&)8+m$?E2Oz>lNSqwHS?bY^`F4%l_rQdH$$sMbS)1K%#@C%(F= zTr@LY{&4jYhr7);f6n!6d+{dKv!;kQy);DQ>1BsYuVd$GXa(&w58O3LC+1Lhxl~Yj zq)M@NryxpV)5-T9|=do^SD1Dc#;x67TdlR1*zow@wCBu5#0%i_`= zkF_?-{?%3ew!P`;>DnHJ_C&i?-EoftB$f0f9+a?@u3WQuHEUc1PtHw0BhGcl4z7uP zH05ySh0u^*>Av3ue$Sp6?5=cbco=f`<&2!=A+N{f&W2|0i`qoLTCCTZ^TbT-!1{cL!*=c+HJdK;=YJ}l zDk&#EO@11umh-{wjKj>~z1zWHZ?|CAv6|0)$FDm-azA*b{aG`6TC3pKu9_+4 zQ}s54Mk*ZHtQ#nGX6?^ZgI%lHa%OJ5tj#1TsJ7K5Nb8Bua?y# z1Ww7raV>aY&Yg_ocvmJ$L&bY+TT%^CMhe;SlMp8=#$qwH4EmtZLe+#zS>-+`>^NK z0hSAEZu#y!arM4^xnqyx-h0dbIy(3lh>I)U(`jp89A0}nS@Nxf>~zl;x~+_FzCHFf z4*7R*rJN4Kj5|#=>yFxSZY=PeHJ78j{n6wLhKu&y_wCy=XHCQ-?;@eej#lS3&ds^H ze(5Q_GJ#{ae;TL%zq4g+Xw2bG4fQ}}-DRTRdL+3xEcZt{PFue2qYjst z+TN8`7yol_`CV@v=}| z-^4!6vpPoN<=|$0!L0kxS!<2pOs1PQ+2Run{7+Ry33wk6IP&O9!=>DR z;p%q+zD!r%d%EU<@tNYw?)r!M{>sWE7R=bP%z|MPm)IrwAHF-DM;}qOah>t8=JGlX zcjK}pvc^2c#+)wMmql3;B9>OwebW{4I=X~4>Zw6P-2X+h&n>L$J$W?Az}0Tihdq|+ zCH;$E*FB954#~GaVR>?iQogp?=2vUi-dO!-?}2{&UEB0o|)t0l0S`Sk<5!{EmvIkDi+QQ zyHO-EK~E%9)h0QT^WL%tHugsS|0{QJJnnk<#jId|Y`$}wQtOg}YirN`-zVgzCY!A) zY4-ce9sB8%9f|_i*a_TvswG(E+$(g?emTN&j$%TcUdxT(rWAIb zE9dK)rCnZkU-Q>aySjg_7VnZ9r9GVqvm16yf3%R*`)jN8FMsKfPeH!ZqyBEU_O04_ z@|McsB({pnU$VAqSPC1hRSXOZOOalz&&T=y5`*(yuEUMpQ*ZDr+Nintm?774(LT)+ zo^6iH{O{#Em7ROVr{`C<{gQjx#`d`tZ>6Lev{lnyaPH!B)i`8UdYpOU9<57--7MGj zjGON#oq5mcJ@wV)>5{xt%{PB*=CF{QmN%oo`Te02?yn@bOv*LTZ2I`jC~%evx4@#! zXCKA=+fY;U`@7Dw`on2Fb-J2scQl$d*vb_xTEAlThpmzs-9ei}&5x`1?f(<@KYX@V z$JXh0m_L{DF$s&zPmBKecgy=MiFUnk_a+6MU&j2~9P^JnV4d)#{({8Kw5^3Ja~t&k z39UIb$2miPYGI!L^GXMw$`6O1^j^A|zI=~V@!OmCnJ%|>y}r0oziV-FmG0|RSDfe6 zZrS-x`CNvRnXiZZiLYAceW#@SX))-QyuDbI!JF;zgVRb2S_@cMYZOCnd@-&F5SS&W zdTHjGZTE~Awf@HR>-J2&a3cS#mOuf2(*IktFUbFScX7fDhBL30blWw3GyQP$mrlr5 zE53spR(+{5P~SgeuK0WR)9v?s^GnQTifD2le7w8O>H69m*Vc-wnDUqV2r1vR7Yfs} zYMMIN@!_>&%sFd+SKh2&_mTbPk6qn~48LDmY!EPds4SATsm&yi`DOjvs>t6Bm!G8E zEqQTd=G>drDhuYG+J59Y=emhjg3{JX&!>N`d>*&Tm?z|~M&I2jA~U1z{8{6(L;TP5 z`n4MW)I;7@ojIM%=URRBp7rAgrTtFbji1V1-OR5LusEM=aC`Ey{L;+!wK4jqm_#Gb zCZ)HYo_vx~P1iUhy0ux8tGw|3#v&_@D~nPVtv})tCQ!82RwtQj(KE5f{Hu z%D=obO_#|}XgpOW&iuRfuT+8araA4qe&y_*^}yFVrr-U_t@!MtDhq0R{;uo1vwQnK z6TXLa>(#oY8#)#Sc3urUe)wmBp6-vYp(i^yH@)u=Gw=wtZ~v5ccHwK~n@l(Tbf4b; ztvvOaP}hC;siu54m)=}7`@v-nuZMwtm$p^h`f*A)b*AaYfH_4ICx{;U^y_eQ_uB`r zoO%ilwO5?HYM!d7nG{v_oh7K(^nhYHr}y_47g-J*=3HBQ%34L9cSe+jnQ`%*T%QvO zQ9CX!F?BAANu6W=uHa_n+at%C%vrxZ2ua|d_egi5!l6LxO4g8>6I)gqu35i%X-w~{ zA9JG5PKq`R<^1_x_rdL541xOsk19oR91!nhUVJ+F%gqmR+m5?Uck_8vnh^8l`PI1Z zs(imXL`}Q73Ic4Wg{y92b(VP19RIk*sMwv4edh9()djAbr!G9;Gy83J=E z3{5{!%-6?qQ<3FGk`up^L(^CJm{2LHiCZ@tZ8dDV*Z(bAM1?1z&cJh_n9ITLyBoRm zT3_$Y?3Z=)N!z=BV^uTr^?1oMFLxh)R5#(xim8(ILMx-{^Zph}Dsfvsx!Hc&b9?`k zpgoV&KO}d5tVlla>CUW~3;8PN+sMaloRs`uzVdtn!}W>BkL-T7MfS`Cr{5Nz{w!V8 zad*DOq=^!_DKQJ)sZTw8-|?T33be8X4T;CXgs14_h;wimMhL|J1rT7*1ptM{=wV5=}?zU`DZbX zqD@n!m(RR^*<|--nT?lR{IW`??$mFH67JjPexWe-+S))nS@FU*dm81-X3o1S@bGES z3+EI6xBZd!`xJF?&dg~`lZ&zv7F#c9@4Wu&LVQk@u-nN@`G@xFRT3AR3%u0uZ_h3^ zC8-TR=ce7ie}3Jw`le?S&Q1&TwcaVb_o47o8;-tDvX;-M{dK*aP?~3=SUr{k^_2TEF_i779e*DmC}neWHzVH5mB(B1if z8<+Qi8l%ngKNY__yPEGosj|-DtpR?=cmC{42LFHQhi5e1%enc{tJi$GX}A1S>8_JeqE}fu_(Kl< zUn})v2E*b1x=$^ASpUcWPS&ZF+`IAk3J3c$XOF6Gdc`Sr_dcV6jOX{(uJ51IPIJ2L z^p?ETx=qG_Id651v(>EE*B{Y}U$dlb zuCzWBe-U%kwK4Ws{+r6rm+ki$m26X-^T%nI;ZwWPcl+N8q`G*^ygFBvZf&O~4Zr897`EaW}dpx%FNB5-XKRWp0&e=e_ z^X@tiMFllfJLjv+ulqM=i?VFGr6=RA`HI&pW>#u%VOO7STJ-y+G7HlLi!c3xzEL|^ zg_bU!&8}zrY`$Lc)hWgg;{UhLTKhf3I)(MhA=}F;vCEr1gMPXsF>Y5XF z?{24W|~Rn?JYC6{!`ObF}B#gav+Y^nYEr5z6RN|YQ>K;C!8B)*^?q|cb-f;`Z)0G`t-hEQR-Q@t*$VuKIT}rD6Q~zeN%qm?i&+# zGDUfpG^S4~W^TVVL-CQytUoj7Yx*}IULgLvNIP2fUv--2yJ?HJr3wB%tF67|)@MH< z{Y2TnAAeCK{WbQ@o|XKacQ5O=d}Q3p zZ}7oL@S?q#mu&01s+;O7C!Up+^NRUu&y%RtC|r5hq4QKg>ie@|rMy4GFNU6f`?6<~ zOUdt94_}n(E;g7XB=1}M;^I-4$c*2SvH`EZ^vh{`Y@cc=Wjj0XkGRF3BVk|Vo-*#< zK3n1VlG`7ybxc~=usr;25bs3!VB2D=x&=kt9MQpio6@=5cU>0X-5aNP=+e3?t@m#q zJ5pNqMqbW9i|6+GN<&H0m+J4t4(y#Nw@!RtnM%&e)tcKvKiqh$yl?T*R&lTR2and* z&zV%(61u7GdP;G*i_;sWNv><$R!*Dr`jOgn2Kj_8tJ|zCX%|1~IU1YX-?8|x)?MYg zZ%bENtz%d4EW5}Pyk31)$9vUv!Dg2KJ9P^WXl*$CldbHgPMY^U%`owI!awafBi9{y z{K`aqdi9glR`$I7nk^v;8IM=J-(s-AR)3RJ(bMuhDSJJ?FW7G3wbsntL+lOfdB!(Y z&JV-?Wo??Y*VSwLoOk>y1iPl45{!FTZIh_H)Qg?z*Ut*YfMqfsNz2RwuWo*_`BmDi zc&6RQ{k~fsY%Eb$X|K8-Hg!@$VAOPZbNg$qdRFsyF#NP|FR?W1YB(sI-!fCb?Kta7 z;XcEH_y>w01~l9sZoKY_Wx}#pR$~=_RF8zTDh5;kxl5CJu`& zChMYF`?q-|ao!WyYHGjyTf3T#=Qh3HuM7^Du5i8+r)r@qH2-*P&rX%EA8J&K_GA@> zFR9$AFC$Xy-nqy+Y^e?fqcJEt*?0X#oE;{PWK7Hc3 ze`xHDY12)4C)&<4`nzUsL0P%XFDGN*gA1QK_ZKggm)8)Pu3X!@?W{_|#2&8imlEdKCw?f9 z@#u>Aqo(Jne6*eQyTf#LeV)?yky5`H1afyA5_c@NTc*q^?|I6`WdFl$?ChV5wQt!T zmiAvM#g^AP?^l)2tz%V(UhM7Lu%+4j>FbzK=k7PT{P=)~mrqX?hq;5{Uk>f< z+gG=}&8V*bTlly1nELrWqSa0|?^Tq_`cF^fWPEQF&;GPw){HF=S1%D>%rp0^MAQ8o>#1iyN0u!0vtC&6?9eBki0tIAb;8qSS9hJV zZnJ&(?SjYep4W%3wK(YN%y(~>=bWLOvhk0UDdXe9AD^dAKR-v~+ykZtL6NkxCXs6+ z8;>5i8y1!LJ$;#=%U=(F8%bAby(jODLeej){I=M;A>Vu2jpWbx#{T=3~MUB*NWvR!U@|r2I@6;oy$os+-S7z$EwI07$wMQo_`9SRCTQUD7 zKib;|I3!)^QV!X7qqw5uhx^xGD%#w|(NSK{f?eISb=eQMR|{uOC|^DQ!xn~|f2wJ=Y|DxfgovEcf^jb?0>UMaB@fjG3$c_8FY|e{lEr4Q{_{ zC0sHhgA7WgBFt7~vob#X-OX|5XL5kg?DrZ%A`%m}wi+%;f3-|xsm8xL>65wM&Y_#^ z5=$F$ZkMSV^FFVLJNc1capT9?KP_jJ+U5U|NZj3h>3G@Jlt{l%1<60XthhP-R@AHH z%+H&AUsx%v;eJMO{KQ8G-UWX5VlR|mku$}n&nF2i$)9o~N|4d@ z#;++d?=04pt=_9$v9Gq)Jipo|E`;kntEbuHWxO{9)@QCx(%<>((z;bMI5x$peH|LRwM_P+Sb3o9n~YP`M7@padNd2Qa3Y-W7x ztF9!r>t3>1GeO(Sr}w!1qrgDJNt2JMe|wkW`7Udz!#qXni(xNb^M{JGOo=*ImZPfq zUEOupuLk|K?{`MqGH8ut8!u5)mUJnPq~OO`v%+5E00ZHW(ea!mNyrJ36w>00c$ zd4MCMK=}ZNlJ(VB%XoIGEzrrBd@VXe@9iD7|9j_U$exvF-?(Dixvt3fpQO4q+pqol z@h#n7Q=;ZsPg-8ALBl7mPqQ7_Y_`<%*A}E*m^xkeZ0xT(2^}x>9bJ2*B=0KTdJ%YL zPPCqPVU1^NTJ_T={S{?X=6==}Q1p1ePWf|d`=*8^dpEsiwF}YCZ*dH64V`K#(V-AI zW1q|OKG~fC6)Sd6e|1~vsISPoX;Ep;|F_vPaE0=D|E~~ua_XqcgDp$a-u!?2=}h9c zZ&}YwHogq~(Aht4i(`Qj+Xbo4n|^h=Ojr9v#UL+o>uqySeLo3&F$9043QJ>pBg+#5!u;2pY!Df7k<`j zF*2S{#Mj4s-&>)dI{kpy!Qyh0q-l2z*0S!@vD9K+*PV8?s>4L@apReF)z?+SzulgG zwyQXI?dFrF=B+%hRf|pDzcJn%6(aEC_ncRkg;$)sDr9HR5?@#vrDQd;W?E98b^l`i zNsg(@nB?C0EL&c>i&aSX}zyFlUF9OR_U-+KY{8n{WQ` zT35PLci$?Tf>m>~CNt0ORo(6X|I5>PYh}AR(xt?m7af_)6VINoUee&3V3gdx8qp8z z+GUPEpRs;C62hbR|HaX-8%`WiJoRYOy5j0ADf+e`S7-XhKYu;Sxa+b&NcnHT$Fp?`o}S|4})`=nqA#|1v!3Bhm6{BHQZ$iAZW>qgh@mOD9Jr%NAZ9gI7y zY`;XM;>z>VJBwoZ!lszs-KU%GWxT}NYtO&)8{PSY?w?^JROSpvIWD4G8=G`yefTJWhiQLaLa zty@9tx0r*(ndbhN%`-3OP76A3*vmOnR^z&QvaWkoNqa1>+w*dH!I`yR!^|_* zU#Z)(`+KeQz4;q$*6--td%)q{_PsSTmP}gN7nKudc|PK=t8Cc^uQ2C(x9Vo`9aJp( zRNqkbP_1U}$HErv%9D3?-;3`a-Bjy&)k;k(-SXj4m309tkJwmw z*{^q4$QI)pe#q17c5&jl8`o~wJ~-`Mx7ce!ujEDFQ|dFViwrF5_jSbScJer=e-VA> zti`MvF*izJ0dqSP6nBT)pu1+y zB)4hHi1oI-yuLEr(#+$S$vF?NErRR5LgWrDZuUkmpJj1b) z)jXZmIsC|J+?Y1#QfnWfBy zPk61hT}QW`#La(JZpR<0$F*;M#DC9;W&g3aibp+5CFUPY6kijxxa)(PX)mWM)B4)g z|B{>PLfKt%1np<-tbPA;+hjNWYsGpjlE*@)eb5hJkMX{6Y4)kVJ?^{9?k$+o_O5x# zl#P>0`Dab_TUxN)$-3s(+&d#r_TUT`oda^SgU)@AOezme_x-YT2ew zhBM!9)K^{p{^Ey+RpFD*De_p~`~Oe=^dz&8a|Y{fr$}j^@?9q&@P6NcWS0JC87me| zHn~%~J@x(Z=1Jma7t*#k`fGM7<(5s)%y@aKyz%|^-Z$H#TVt#@-?(dYbKQ!KlQasi z>AwtkH!HyX=fwrTlj3LXyS3ie+sJc)?C0|>LYGgc>K(6>_wu!_$YzS3bZyJxzv4kR zyB;iBm9VC!w*C3P!wK9>_G;TTLX-pxf6344diKDGvAVS1aQ55F(c66bwh8~9u;IzJ zdC|vI8*j~*@!IYuxY@$+R8qqWjURpa{oGKqB>^4a~?Umflt3R4HEcx9(tIO!t^{Vt0Hbv9E-`nV7 zxp?=Mmp4^RPbEqxrA+&^v&zP7*OaC|CFk;Df?hWCGQSVg+;l_zSfo#S&&HDP5&Syt zXZg$u_b?k*O?7hqY^`+ZbMkNhq*?ERXRK1cEb?>h^v$ZGvVxjb_V#>9za~C5TcZDd zQp5JtR--mH#eWM9Syl6RMKKmx+~#QsK5k$$H)Q+q#M?gtyqH$0i*41n|2Fear*G`W z)hnz26l;cec-ob0WYO}O7iO|BlJoYAsgKSjMpn%JCeYnbw)ESwU3!g67F{)~6Yi91 z`?9_1l|D=2@5h{9XWu={&>Qw>m*?V*PhTb+k!aEq+N{o*b$>-VL;mT^^F_hCe!t(} zdF0@ygsw^jh6NS z?>}t&ykOhP*n2y*?|*ZfJNxLjY4sCD@7{K6S;!zJntAj}(!vi*=2*=*x9)d8|KlA$ z?Jj>*pHyD)@k%eV_p)<4J^D8<3)prp@cqi4()TAlSflRh2PvxFS}ghb#mc`k&GjscCO!MZDgo^Mzesw0+ z%4BnGF!MWu$G783)fi?KOunAQQ~to!{*Tq^i$~8H-K|JjQa*pCBZq))`rO2&o z(v}oWVE_L6L{e{I(=BV4Lf!kxO?%bev^YLdpE38( zW!b-1jc)(UPtrT1q%2Y7@mGIw=XBQj^+m~^mmbLfzv-qBtShYnhDlhlNEyZX2Jvvh(`A-_0_` z^31^>Iyy=Z-N-m@Y7?&_|J_^og3~9)Z*uQuB^(S-=A81#s%w?Rqcd^2S1)Son5s`` zP(9o?<>Ju^tjRhNj<4obFK`j-`LC0Z=lp7GLC?ESM>mviWRU&dxZ|Sco5vohNA8q# zm*~|zHRFC8o40Q3!fBly2l_R|R<=I7RcpE{_Kap&`J3GuPYT&5OZJK~8e}+!$<^2; z21uVw^6THiy+@+F;KHHT+#$}J)b{$jEpVT+`4pc~HTRc$i}$v+JbSdkZ10)N8?S8V z`+fh(j0nx`d*vVSE%Vj-_Uxn+dukZxovcUfdz%cd2rplH|Bb`!;}^o;W;_pEX~;0= zfk=GC1(s5+Ho<~?o#5v;-*Miyx#$pg|HZdR!#Q2^)2|%a=O5srb7bG4WvFDNLL5MLx+S&vD>0T6t_6zfn;b)7A1l<_aoG znZZfvewNEN23oi}nM7^2P<_2*j@PG@S-P^Tj!)LSnX~o4s{bX$QT3u9d6UaEUow?G z=v|^6&bLxgW8pT=CZ^c6MjPssRz5CTBHiA^ZT41sm%Q|)<9_yUv(`6NTwZfGxl}Pn zJj`ItfkO7Pu~RReeO0&4`r-*m$>!~sSAAJ5BcIfCefiD#-fJ|17Zr5L-eQja;(Smd zc*R1`&u5PEq`vomKVwgjmV~CUM1gai3GK^u?(7ReF_iX8QRrRa$qqc1A z%)4xNwDrNpSF=s~-hA04Yof7i;(>{xn&o#M>E#!++*R@XvL~*J_3`2R(+cMasR$Y` z|M)Ynw<Z$(i>q{71U@v2u=k z*?imoyyZP}Q`C<8f?55Fx3?=qt#;kD$gj!Tvf7!iE#;Z#n`bwl#LjwTU)=KG`OE{6 zUw5Z}4PdC|p7>g5_On|W519XdsdL+~LHdxZh0ndJ6;7>ru}m)<*zFft-B9qcDY#WA z}dy>Q}$y_JdWri?eg+eKW=sorATbYtQDhE+~X zmtVf^(ys}g$LX23YL(3I)sj|oHXMC)tT^*khJ}lN&*gv2A6+I{XQwWCyKw0{mEeq|ve~omGI?a2o;kS{KpV`afQ{x>Su4xrJm>Sjoteavy zG3C_e`RZMluQ{$sKd`LDI`M=iqCQ^GN=Lp!irJ?(U(*s&)! zji=Y1W?XsE|03`5zD`!jy6zhpM~h#T1id~Zxl(mi{uhR0vJZbI-W3XYsLr5w=C#E+ zNfpoQyB(E^e=e@R{Qqb5LRE(8J!WyQV}z_&GLF?PR}s;8kUuX?@!5lD38C;yU!F!> zGWucYc(dMEy3~sK{FIA)3gJuFZvD@1l^5qvg9sD-uGylJ+Eh$}-AFA%Ryw01b5?X)!x!?1`yE6(lEz>Av=;e9W zC$vZ>wdO)+4wr-c^&>|g$y6%1|2wjFJ)d^dd9&2?)Vto3J+8|w;O;o*AwD@oY?^NU zx{h5Z`R{qV*UoK~dt+_p?env55q2J?@54w`SDj9X7ge+42&eQw1 zrf-+!rwb-8N;FrS?Ofdz?siHtdH3Z#8II;1XRS6z*z(ETm>ux3deV)TEDQHV-CgRY zu*_n)kI>!p5LH=?_&Jfjo-3y>6_ssx`FF|PRdx!$9;$fH)bH7RM&>?ybI6Z+rPFWS zCzm`@w&ybLDqnrUCQbig>8cyLJ$Lkb`ukJo-T#&FsgFNxzbfmA>#^?qB~j0LTsn{G zeNwjmBQUc_ymDJ(^TT`pR?9oQXLubMF@Nbl&&UI4H=#^1^+t z;-0;)oI|(#*>+r~=6}8E->?N|r&(P|kLS14k+r;_Q2XfE>~9+uF?(MWdfWG9L;9i* zNiH`w-72(c{CdvG_3)){v+svo<$O8I_v_oa(vsbeF3uB(Ws2=vY4i1Ygr;St;lHO$ zwL5;*-hVFpwdspAukB8*75g53{Je0dH(R&uhdCOuDbZ@57OhIi@!8m=KbL_mS^e+M zbxDCg+qC13n#Za{Gb-ktmM#n5C1#G+&Ii(wszI=sJ`{{Fc4!jLdNekiLGVRDe?qsoJrf%-bq?&%7yFM?3qhZFh z5A4>fBQ!$KFN-*2pUB>=8oex`FW}9OT`b4`&lOew5m9*J>*K@crkoXxw2JxFv@ZXQ zPbl|}m#e;ugz_kRyl|YnA>@Gk-@@~o);6B`m%HGyPIA!iUz47_od3!}+THC$E6ex$ z%KDajIIHjdj=6DcrOy4F%~xcrk8f?ZUKqT<K z=lCLReo0Eo&n#=n)5f);v8%ZKPp#U0(e#!exBH%#l>(iCB99%8-8rc9vww@ghHDD3 zeIg}Kc1_x=SiE-Wy1hoX_7|}Qrp_;SR}Rj(CCTis8#T2>s-8=2>EFUt4ZB=t=&{xY%1nuwcim+v9p;kGcMDW2Nl5+%cMOdhF~Us{i9PX4+a+n0rgokK_3ogOxX9_Mfoa z+?yJp*>hE7RZiRqCf3ul7C#CqYL8_-@=f8h>+Vl2H7S-GS;YiWUTS9v{W_SYaZhB~ zW1fu80FL&x>5Li~7kD;0NW9GtukLiPQTEz>{Mo#Mf1Ghs*Kdqntgw?kZ3gS>?;d-f z@}!^HE4Me}?-2p5oSn+sHlC4R#kOLH@-N8)x}{e#)8kA^<~_44S7j1*)SBXRCTsJz zzzcUBHgB~^;y!M0i1(*%g6yfObu5-Y7F<|<|HPFY7tVf|8rv#$@IZVl*AAI!wr|p( z?=XvUGW=zeR4L(Rm^!J4_hL-4%JgcX6*HF!dUgMw)}UA%Z@q)R;FMp_*#M>ZKD%Hh zl``?{iXA#{@}9k{F}3-zaB}ATtds9`1GGYte}r4JF8Ls_|Ha*zTQ0fUHrGb(-qEbWe7j@{b#}77iaG2y|A(i`Tu9B`JpGl z|8gG2J(HSVa@O$snU~CKdQQv=d(?k==C4aPm*p34*cCjbqhe|U$Bh@;cQLf|im^Ox zJh3p#$aCc@_WZPsPG?k>O*_`ISwHfqenZXv3$AY_-e1#pxn}bJ%OW0Kp8Yefd{=JT z<(?9puwD5`eCuJ6qPr(|O}}*LNT05J{q6MXHgoOZb93uI&-1!CsqMn~fKtx4IsOJ$ ztgq~0)KtC^DO7f@TTlID>3xRAN@>51_5YKm%HMsweb&jzn)CI$wEyPiJY|s=ZhOnk zD5rZ;E2iSho2FlC8SFRpX1nApoF$cW;k5bfSF^4qHaYSq6{q|AwOk5VEArXwVtDqs z-|Hu_waa}FU+W>c=jbaPj)JovLIRYhW}Y~xZ>5pDN@eve1v^{Qjb35uVzza$3LkuK z8y<)i5uRu{;s2BKQgf|~9xObR`TlU!3YlJWUEdQiQzzR=F9`c7;Z^rrOhxAI)~99G zk!7c>HUx&Jx?TOpHcy^;$?JCo&+44HwrXEpzv!<0d+z%an@?sqM4y(;VB+5QXV+GB zPPgt&;-4zsUKRhcdvcbwz`yL-4Juc(1J9qn^)2>X=~te}?)3PG({ui>nD2gXmB;(o zJ$E@oqxnBv?6ymgXm*dAQQGzA>x08LS)S+FtYvfEUwq!c`_c6W=av8Le0lZop6bo1 zQz{sK>U%w%vnQ|h?`cP=T83iNlyJ@)lAGQ-1$$rPm&!0#KecV)$Py zceeGEcdHe13JQ|h++Qm+vu>&ReQ1N9&_4TAt952}YVVGSF6Vyutybyx#0Sm} zUFMC?Q;MdCSy#(6t2q1Axb@52v_G(XR(pPbnp;J{1@&FU)vv<0-}zp3$^A~>t;L+D zJP$40ZM^s3iH8?@Yv-+(TdT{Lzhch${jHxfD;vTtsW}$BYd3Jcs++&b?`p;x;k!yn zn~gT`cIq5XRpod4lPR1ex!6ZQPH5IfVZJl3{ETkxn8g#FeqfUV)6MzTb55jmpIN+4 zs(oQ~N#&Z}W9BCn7xFF8Z#nu_E^w(A--M})d#`vW+@I<+nOI= z{r#`*dvp4f&h4`F=J~4UHk{H8y4&b{K0EEac}Sso_=+Ks6KniijAUu=#unD)}L;LZZS&G~m+{9PAr*Z5);VIP0^F`r_< z#p|Mr!veP0$}#&KJ#c`piT(4_iC=t8Q?$hHU%Iq+KCkfC1uraG+EcYl>QmjaE*`y@ zA9}Tq@uOE~^^dX@O>KWqswtR8sp))s)b;!<>0sZ)i=y979cW6+(Vc6)Fr`+@KllEQ=-U0e8iQE*t(UOh z43ZVRJ})WaC~ISEz!L@6?d=z31-w=EfBhdpP|}kDmyFD zFz{RLeDfHloUBikoL3@BMQ%RwTVJa1{?d$k=3S+$OTO>EEbJ<{dY(OdHWr)ycz%nRQ2>6vW2Q#W!qmp4b%+Gs}F)J3uBMwY76 z<||&#d2&YW`X4U0R~=Eo-+Fyc9dJp`oNcyBUhAh*dUCt;gu~lRkMjloxz5eRBOPSR z;a|y8bNICU-;XSomu3petylQZcK-g$paj)`S9`L)zCC+wV;8ekW73Vw zJPF>H*;rq8(<~s#xz(!M)r#k#a2sd+GZux?-j);Wy>o3AKilD$K9j+>`>yP*b>H_S zJYAf1>+_KtCC~TEvWRy*OPR(P#`F2F#)jWs`zMz_Z{>WpxBXCTclM(<<&`JB4Mq0O z__fkawC+9Ejb-(w5{jS{lB`L9Y zD!3j_lb`guE%R8vUE7~%!wq{%1T`<&8eGm2|C{vo;HoC>TCiU@Sm*~rnm!7V#e?8l*@`_+y zN&vsap_MG!`Qh8#yMD*TiYW-z$L8(f4(z*eUS+PFWK??Q{fUoy|1PLp5n{3C%elbi z*K!2YBDe4R7`CIndjWI%?uT4jR!qnE50obEsS5nDv(r}fN1V*Lb8Rv6BKFLUtGpPX z+b-<2fBj;ksRBRl$Fqg+?OSzA`o5t`K*qv^{cDW^Lig@n^x&1Zb@bvCpTBCcmrQ=# z7n*P(;Q4|i3wPlSol!^kE9&~+k=Bk}<@jQ<= zU%L{!uGilCsm6P7LYv%##7fJwIsek`mOI~yoSK#Sq*3RqfLW~i8M^{$#)^P?hhj<_3$Bzt0Mf3YE-H*7RMF z81&Kks*lj?H*FFJ7Ek_mf8I?8HuV)UQTi1s0TVY}ZFtE)LE_P3S>*{w-ZAim_1s-K z>HGC~E`K}ofaK~~`&KAKm&|8xe)PRvrnLOlV!ObwHIrixK45LrkNU*ze{{l^AeCl= zEmPJ!*YaItv0{N|U2-v-%6y)xeLoH3TrIL@^t0bz#gR4t!{b03H8+-9GlGLG{g=5r zywshdDgOU+!ms6dK`mD$Yg?i;dR{Xcv#1$MRtH*r@2wHM`mBqw|J1&#Y-PXNdY`N4 z8vZ|2c=vjWM*X{zB@-@glrWs~E|Al?=BvP@oJZ$#56n?7;hP`2VDs8jyTew_zdLWU zsph}!eW8=*B;Trx_#_m(C$;COR$?#K*UrS?A^U3H2qu-?B_n zygS5`dG@os|93=DsBvbT0>>8quDs1b3hVS@KE(A`#DpA_ zT5;stT%ErTbssquwjRy=n#*+LW7~li(Z}i@cgzBANNjtTkvH|qUX%I7wwl4RqMo}Q z=XY%G6xWHK`Ca1jldm(nShtoe(XemI_gdg1S}}c}$QkvRtkm?q)fc%6Y9-&?u;Ui$ zJ@QJC@ql4w`yuw)$2YnUe7b3~orT-`+9dhyVOh5fP8!}Z>9%(9?3Um0Z{mq6MZ4$; z`SaxW>)PFmt8&;cwfW5139XukA03eTRZzq|`Kfc*?^-Sy=d&DTGRXqFl0NM{nDJ`Q z(#1!<6#hPK*xW8UNmHUx>(txl7cVtN3#F9vrY#8g-gtIp>&v3Hoxi?%BptB)^UaIF zv`74Du}Z!3UDk>5uZP=LBIpe{S{?)w##}-wdT$!tMag(${_m=x%fzt~V z>`y8_3|hXO<80o--i3YbSFZYe4A&^^zVTyA%w)x1EmD(|qC2@Rf8TbMFEZt3Ez_yl zZL`|jwskHvbc#+D5^C9Bz5xFR@w0XWZH(}o6l<8*gq`Ow>o`T*~Bh%Q|;Y9l8@SF{*&P|Ro`EE zSM`=QN4vp^7~xvxrJ>q#_NV(Z9xvXMS?X}$So{C9_N`@!N29p+|ND3U_IrzdljZKW z&d>J=aJ5?8F@AEzTp4RM0wBL~W zC8+O`un^B)u1)4S5D_&=$r5FkzE;WS(^3Kp#J|h!6RA`U(c{PHU7CbH8SXL@aJbv zUoLA-*~pmrH#Tox!=+O(tJs2;mMxp)sCF|q)?8Gj=##|yrIl&Fu2q=WI4xQ1@afc1 z<1<;RYCkr6^G`~dwKV5NcI(SiQ~#XV_%YX@xmKfAggM&&v&XzFe>VTd$~oajrt&vT zEzOPM?6WBRZoZT`nd3>a_#f-K_|Ww2S(fij&RtvgYpqU|V$U+|fMqK+@A3)SOc(Ll zxX*1t=54P^tMe!Rx%_|gVKTSKM$dvnn$|z2&1p*B@am&<{JMjkFIZ1B{bLK!dE|7c zETwgon{x5p1sh`T_AER7!DQO^${BfOQ*SX}d~q&JjA5si&wJ0Thh+-C89EJiZw}@= zI&VV7mw7MdKM0w`XZHWkd(~HyBRb|KYGsPK*+h1(317Obsb_^`<}{nAf-^7V_D!ww z&XiBw)3ZAM?^5P;sSeJwQ%gz>uC?$#U#+sSnQyQ5p(Yt!d)4k2f!Bm;@@I0MFLRYO zIc(3eEHq?xeoRYrjIsN_jQJmD1kL}r@5}FiDbY;Vt6xTU-R3<1(Q3}cH_sMLi=V6V za9!xe&p&MawA}eU^mP4xC+_=ka{9LEEX#VOCFcGK`z2|8*mY~pO+ot|u9tUIv$*Bn z?R+d#d~Vyuf00MePD+?>!YU}ib7QHvf5@>Vr={8~rhKZ4zsztuNKw?rb$5r&kMD~c zL>I2Uca%A*`27q|>k~Rvo##KYOxVSsrpaZZe$QjQ!12}}A2SrQ=Ds=nki9JU+fAma z(U&9(FZgW~Wl>maw3U@(&1En5#p#pG&C6WA2m4qkT)Vk6gWYBG^`l4P#1FbHofx{v zwDjcb37OKIo5f2ltix+nZ#DSvL5^G)*=v4O80d*D3O;W7FBgE?yH3-@MNEl;Op%q@zo7L>E77 zzC3H%{rSf)I^10-b@HSo+ZV+RyHtJ8#2sgy@2ql^ck#5oqy7qQE7)5nIaMriwm36U zRW75%YW0K3GiL9%K9>|(viz${|K!@}ocerqmb~|7i)JiXDr@yjk*#&>!iUxqYnGjv zl-Bxnclk@_l}@7W7te_onT4AuHD~U*mHFhMz6JjlgQL%8o|$R?KxN-1B~!!g8Zr|; z$u*xeia4mhR(XzX;-`#9jBA%(J!aFSe$mPBvhY^#FJ=2;AHHT?@+MR-`}C`hE+`KsN?B1WNlI7&41$fV`I$CD*p~P+FJ^58fa~!t5_l%!yA-z8N zdzjVw6-=^!^8=^O|D^Z6vSDWDOYdJI2WOt>zY^#&pR>iodTr+3)t^%ypW4OYz$EB< ztt#ZfVT;=dN4{##bP}DY^v(4_Sk$cW3p4rW`W>uO`C2fu*ZAbC1r3UKM1Nj1pJTb8 zHuynvnNFNsSMcQvFCQN46fZn#oVSp}Q0`XI-u3;$6*Bjm9g^w`a=jHHo-(^x08cr!+|B)K`DM>-JzH|HAnQ`Hv2Q#@U{-v#ojnH%Y!85PIyGy<*6vaplch6u+>B(? zwv+lId@WW^eI_6ZU^;o3+vHR$Ki7Bck3d+j>vukJR!zYtuK2RWIT( zKJ1deS;_uNO^^TI`fENnHTO))t^CPtskF-X$X%1ea=U-3u{!)I5^vTKmOOcuZKcPI z_7}ekvpyWZqB-MsYEqeThXwyZK9&-du$PghOIpmQ?ol$4f*6 z6wlcu%{6)1R(g~xoa6Hg)dR6%2fg1u+7ce^6gRK3u*~&pVBf9CwGY+5B+AbcG`v^1 zm%mAE5yR;X+v=ST*UM}PHavWNdrL<=HNd3BPGnij&IhtSXNgf_8AgKYu?SQRQ=k>U@3Y+ zFNtYm)2CCaKHqQrJ;R4Gxf*)i!<0oXWH5HH_qGrk6$^WtXrerd4j;>HIj>V1S_94k>ZWHyWyYy3T-xB z^$>@+HZexYKf*V5nb!YfkZp`vIGdfFeVebpmBL)!)y&R@RqObs>;Dv*w{NlDmL*+( zpZwuJekytY@#C>q7C&gv?9+Z;qMJ55r-t{DJF`=)vt43I!O_cleSah(XLcw&EZ1ze z{QGs9R{Z~yzK<3(GRxdI{jK!j^vBKRMy#7Y>!zNMwojLf)JQPCtTkKs&Z)gGnpQdboBOBgzdPTs;E4w5;s-HYf+}9%;T`tF;xwOYF zYVG1<)hjnGJ`>S3>!JMfIQvPqFMe`Ia?h}Jc)Gt&zJ6a?wNBib=)>U$S&BC1v3wPd zZ+%x&k+I(7r+@a3`p4!fC0T-BHjKHWBy z?2vrmnUlHlZba)g=RPytB<+`z+f;X~$~`3aRaD%nNVCmYdY9e0TZhWlTFYz`&Azz# z_PLu61QqwQ7p-6VPh52q*9%K#<$^WJyTeW&WZ<}Jo!GN`*Di;4) z#rk}S?#%TH2;*swz%bk#q|4nIm*x8ERLK}-1lx#!={3nGxt4p={dfg zIsa}&xzyBI2@h}Hv-;b9TsQt{WW-VP@&i@u`x=XGpFVRiOriZ6$M1JR(mP`8Vk;)U zS(_4aL9s!z)2c*0Nxi&_@%))mQMb28xho3_z2vu={J6>Dce8Zb=Yl()u}o51%tAl; z7sst|Y1;LlgKfFkSsot4z^w-X=@Q$rg>2LXeH1)4n`S7*SZi?1d zJH1l}g4Qh)+a^1?yw|eWCklT@b z_|2mGCPo(*9Xl{@@|}rZYweyr+Ow3!$?L?{?4up;4zs;J-{i99>HInO?cV%YZGJNA z^u5KmcW>w4q<3mX@vJhz4L6m)&eQpS`sW;#`|i{;xtn_Coe^GnDrz{5pB}_&-nU6}|6z+TL&2bLrKO zhcYi`WUwD8tc&sA*kPMxZ2E{T;_wTds)<71suw1oU|Rd$o}*Nm|3#3Bm7~PVg?-Nd z9{DVC+0QH+HUG0+>gQ0EDsJJ4FVebqMkceE*<0LL^8KVomr(AeCaYzq_kG&BxVisl z%hcDh9%ZJuD_fx)%R?%@EX%prTEexzH)Zy+YW`RJ}6Q>wsF6CW`e`i$LG=uf#3hQ$dpL)-l&$%ftP2$(Sc&*iXecWbl zy?6glGU+*fVVb+2+Qr04LB9E2K1=p)y>M46Q(}$$%BfP9Yr_uJ$!zm@vA67+SKT4E zW4_haZd(>!Ui$Qh%%{|87jAUDsr#TD5hi!s`O|?D!KFVY+AAdTT$mlsCuU%E@~)AZ zhGUq~tqUjA=hR<0D7}hxQTlqdC7TrHB<(TSYZ>|S-{-&o?sHdmweHsQzw>g&WuE_a z`@Pn${Ls8ka@SVQTy{s^*a<%i)Nb=NTX5HuFN#o8=+sqk5sR_46^=;LlUe_y?Q7n_ zC0Z5_0`1w=1=SXZdrZj=!#6fWe*n5^ZI7C!)D33_jfO-&QjM|-S;iT?%~fD zr!xbVIv-@3&-aD%?bf4K8cn-D&vF*=mQoR)ux;|~0R2jjIjpC}elR$kOEGUHz5c(_dF5dS49SRLwr)^LLCPFt0mEXEDe%UUKkE>{RYeUQ!ZJ9E+@uMX%@c zOpuuNB7*-={?fda3Fkb2&TCCnu}O;SIl`2R&s8<*>)s;o8p&!FQ$hq zt7F@kwP^9Owo@fJlh$0ESzoLgHX7Ob|rR+Q~r65(dJ$>7&E z+l@0!7)`uxolA?Vx8YhPK4;#KMBR>y2L#_9W8ATz&o|H_(w#FozgSKc>n`!w#a-WoA>-(H=OWor(s{}TIN z@MXtWOV510x2yagY)GiO6%er8rMXPx*UdE>ILtM3elE=IIxg|~xfP%>tOG@UnluOz8YyJ9lB6ZFYr$8{;smSb2-h-!u*)kw|6jm zuv)tb@FIN*G0=^u@pV$VYnb7S~TT`!+GaNSWkML(i^?i__@QA<~AFD}@j&pd<0 zr~jqTX3l3$syE78&bD>M{5*5pIs8}9%~h5ME=)TTGGF6rT^V0|W9RRzq6x|?-t4ZM zBYR%0L;P*1@LrvTv2xx^X4*{IVX(jCI)4}AB2D$0rFWQX3pN}P?&;?gShlhK=O4Y) zn{)oi?2&8#pL=w{?q~1*O!_)=+8?Knla3Z8tS;wx!H_%{BBCQaZ-Azd`S(&!z)AZkw!*smysM*1JIGeyaOM zrbCGt-yC?)r^jUR?qybZFn7D8+1!7hPZziuM62p3&AxS_;Cs`zPmBNRadk*vOF7)V zYl{EYzSEpX{5Chm7y8A1;L>mrF0=SoBcpETaUh}9=YvnekN@j;^u@0|$Gzdg#3-MA zGjFo0JT1**=QNH|ue-}9)3uCe(Q*UkJxk>N9#QU(jQGQvs3Y{Z#&glSUiC!oJ}t{5 zl4tZ{CyK4^S@_7>^$FjjixZ1o+5dn4VkG)rBYMM!%HVeg-UUB7IdPp%rCtV4sb$l? zM?1f{)$Mw4Uua?c^J}-9LLP{iy}t8%nlk^K?Vp#mX-vp}_u|{hPkZ|>Owuvn_MZ4* z57=M}{XSDsGuLz5eN2Z=;RUJ)f@UYMQ0(>s?h;yU3xb=nL%ZAWj(?cVxoD*LR!XRKBecQ#6j{Pde(aAw~ z`Y?aSN%NaBPdu9*U651lyg&cIq-R;;r;V5bS}u7sELk5>r!wKqg8e~j#F-y^^1e=Z zu|=8v_7mwleIMSNGAZ+w^KA_Kx$@y!{;S;#Mh#m1hdDRbp5(Bu)#{u%d0qUezN$kE z7Dw!I6;HjInYHc0%q&UKqtkrZ{?EKW-Jw|(mEC7rk5W_Dk-H%}M3s;F|~%Z;m37ykU5 zl>F$hU)n<6LXX953l-i?7YOM4xGG29d-LPow-3CX=1h^^zy6)1jNgMxzIQ7t&pK>2 zoF=GyCFa$f&w2imwmCUFCe2^Ob#_UpY5WGu)~O%u{;SqA@7K)PXLRo2hwqDc&wW^R ze%({?CWSh#ZyXLUTLcvTANeKTExqw#PPY1N<#+jEjf(BMo8GSPJ;f6-17BF9bap^=s=S{4=&u*SpZ_8ZBxa|tF@(1ZCk?l+DkIJglNSb@^pDCW; z6}DRQ@Qr<|XR$h3Z;5@&CGbm)>0SJ9JI2R~S_NWTXWK5_{@&C%eA&j@syxZhh7-T2 z{>=EY&c0FHLU+FLjcGqbW+?@HO8Ru#b?O7D3~k;B+rU}>a|=&YXhu|~ZAoL;vS?b_ z*F{{aZN7Ggyc;s_UddvW{qZf+H1*Nx{7An44)2z2z9Ms?vdefv&&PLZ)5MnT`_3aT zaqiZm6B<+VroK4d^jmeCO0b^F%g>(M*XeZM**!g8ZPhv@e_ef{v$l8Vto-@suEg%! zfggN2yYsZoWqdAcZMnNZ@7}!+mnH@V|Bw~DdUWZ_OPq|c7N2~3q?iA3k3Sy1YSL|O zJ4qpViCc1)Zu@!pPjfpmZ{cYV#)kH{Z4-I7#J&t_baAfze9cA2VQX6%8{fRC)4X@@ zFRwJ&`pw2WZ1$GL-1~J7-P=|pD6u{9l}5HrsEg%i_V|P1x%ofdrLH+LU38@+|JIkw zy(}~t-JaZ=Dx2jg@k9QjDetdS>mTo9K6IKfqS(d!#VivGWld3@+g9`SCOkf4v3J${ zeYY><`lPf?%yxfjCEmq&b6>KRN5kyYo^?^dRWF&o{?^~1$Zzn0e}9wl<`d7n8t!%d z-D)Anm714Q3^@fqH4l8h5%@X*&{6sprp6#y5YrHJIRJj7`if6pRc^!PwN; zP{GL92#igQOcacaEx_2+%s|1&!~~2@jm#8`%*?^q*uYT1*uWHwK}Y!;8<;78u&Jq` zg0Z0q7=zg+CJM$NYz(3;6pT$Q6ih5k6^u>IA=t=R!Pv|Qj7?1~6pSs5!Pwl$K*7?& z6pW1wjFGMhFi}8U^aQ=4$kf;rbV~v>5VF4|R2uGfc98z)J$>S|Z-*EPJ6+|blvp&| z+a{fixw*;h(~M;A_pi(M?|A5~d|g;gO()^Y9LtXO_sfqyTDqcZ$vmT}i;VxJ2DvHU z@;3T8D^Ag;D`dB6udo2?#49~-c+Hx1E3C1^9uCq1zX2|sCgXM4YV=tPTr!C@-FJ5+5_1StJn>y2?)qgI9zYK0~cQ|n{ zMq=5KhAvl${G-v5*AILP+Ihs`eDeFQ$J`yuG9&yJBpVopE%b=oKR^H9zgr6%oMp;x zq&z>t(7w}hWxz9Go*Jv0rUp?=mAg3>ZDD`2bkTjE2dCv__N|USIaTi3A;Sk7CrkP> zE)p-)Td8yKPfzLJ>Z8Xq{9aVv`o4X$)p893H*2?`d5=Hkop)KVrubx(!qV84g)hYK z_x?U(aHXfia$;eq^Tb%r#N^-8Yfk%YW+`D`E_69e`ze2F{-Ym3Z0-38^BvdbJUk|O z|o)>Cvdk0+SkPYD$G0?G)`6avd8pi4|ZSpsz9gi~foaX@O3bAE0?ejex!5ztj2 z`8oMT!3BxQsS5fosb!hTsX^{eT>7xfDO{XGV0UFfZwxUuFhaUx1AJEs>_!dfB`RF{ z;h8BV846|~mC)pCWN2;zzOe-Ayc^N&#S2|JH(1-xS+GuRs?tOG+XWB*>CS)kfU)XG z#!TY{txP4Sd75Wm3vvFbW11w~<;ZpQ!>@bW?B=bx{Zn>dM3ZZkfw5J$&its{kMSHo zAJDV%R#P`P#yUxSkAV;=_A|3*K8Q$(8L80{}V-hO-XveYw?z9%cbx12v%u4kmQ zaPFBmaY^rv`4w8TZ>ro75^7a(itTEb;m*~XIWep=Z|Mc8MCwf1ultef=AuKBU(Wb( zx#Rg)rA<0TJggZzF9$7B;H#-!5WVf;s^e~wpG_0r#I2}}x*n=-^GKp{_LFljRy&)@ zrYkHL^3A+I|89tPug{OFD(h6gx5iaom3CTrawcBZPv&k3QXbS5L)BZTf+p{+k!htq*HC&d}C7_moavtntHXato%;s@Np8cuv=3 zm*si+O>3+8-NMR)RJn4d3kAuGW^8pjb&mb06vvhGYg4B@`I*2n;kI%6ow>fMF1I9Z zO2?dXc)k74q-AFWgWXhKZc{Lcs9%s`aX``f`_HM-`(`!CDj8J$e`;lK(COr=w(?l~6k7OUtz!+p(z?|jYkrQ82az0K%$&)+|}?nCo|+cS6y=GHE0 z(>wC->xa;;T!V@r?32XY-G{$iifCNUO70&y-)x;L}<`bEuB&ghEyinWJRm;{#x z1ZmyaQoZ2Y%q{x+-~PNB`0I7RntPHW2A5f;on`5AesM#(TR82ly2P^YR}YBi3je>= z`GzTWt&Ssm&&NjIo8}W`?q%h1+}^nO48zh7&o0zX+Mx6P;^`Av-r47lZ=3Z0ZlXYY z-pbb-d$w-(nEEoTec}@BI9;I+ddADgsSoB0?rS)7IM-n_8uTiLeyV71lv{Q~ED{MfbZ?YH!@sUJ(OM6UAM zcaouMf~((^H?D{NyUjj2VMkE$;`bA~)>}_yzI-fv&TG!||5w}-Us84PqN<&2^M;u_ zb?)U#+)7>lMCFWUU4$|J^hBY10&c1jb62x>R^P0cbjEh}k0UE8yG7Pi7*wb!8F4HU zsXlaM)mM*ZzGjQc|LMU{cNrI*w@yW=ZV zw)RX`Ja^MIHFSx+rf}cWrH)cFf2wc>Rd8`!Jin)tO*oX3GgwV*p?s1A-}^uxnRSJw z&!1iG_|JZ5NnZ)$zP-UZakfG4f#4_oZx39Y=h17;UgVO@8QqkoQ~Y}M z>KnqlJ)ci(ZPwYT{ju#@#?310+j+uriANRlFL}(oedW^puRnsnuDjWGfxq?dd49>l z-KrDj7Io`vk~Vf+z4vSQnZ|<>w>d8JM$JhFfcQVbS^vj@#`$LH^Smml&@&^&P|-}SwFwSY+J=+ zj|!D(dT&E>rcQI`nItvcvNFq!nZxr>$%hUznTBN>pDnc79hjGT)jL@I=kvo8-uY^} zu+8jfH}st3{&J%E2l>SCf5j8ev>61u<)>zxn8h*IJMC3)NQ1@8rYS}rcf5%D5$|Qt zbZW*E<*Rd7YS`s=UR$-GDE*q?q;qWU(>041t=lOgYP|2@eQv?*Z-LE5Vh!vobK|F4 zNY^d%6__rV`MfT7(zHYW+c$6V;_2u)N(YX2zrLxw+$8uW-*&v9<}f z4HUK}?XJ$VI$~YvoDeYW{rak|75BV!j9UFBq^^9Booy&f}I5)GSDa~+`X50C#rR!xj)vfz1`7;cJ}#i3{ppCoMX~&7c4&1v=oGO#$k*5=T^aN+CkGj|5wIo!&#_rT{L`DS+cv%gQc>dGiO)84Y? z{8o`?`=ZR&FtFU>yLVHsyS8_2SB>2VUdN|@4dz|=XZ69GS#$NPQ1u=*8Rl)(R}8dV zo;F<+^jVQxCA$Brx4c@(JJb0|i{*r?xDQ2#PZX5Ss1Ew>`~F1Q;xlg-2mY`N|G3(S zXM2B2I`>y!UiNs;Okeif&5DiR!w!FC5!jnJYu%3k@n{#J)QYlOtF`)a!{jv7H^s*p z{MFpDEm?x^=v)oXCpQug-)In5c`a~a^Zk=07yp~x)cGF2>E}k%$;zvC?J*HPzy0N! z)2l)@)m$q6(KmN}p;g|OXd&(1nw{7GE|K#47wg2+*R8qSS|Zcy!^@dF)GjQpEaoD|yt#S=P*O#!f9oMNvVy=qEc%xn2kE{3H}(vZ7(X z^6fQTMQp(?x2uyUuTc(J?-Bay7qOOf5A2rZ0Wtxc;k8d$jkm zJ$tg~YwC`6OXWWzc@K;aGgwWYvvAe@Pk-;)6)(JC{Qp*S;ghb8hd%?SWZ%{`7CLZj zZ|arfhaLy5eG<*TZ_3rsJAq}qzX}g6)H-b1Eb}f}WAnr}Y!`%fc}OE92K!i63g^`*t0a)3T%xR#oR{+QBoEw8J2zs0d`^Op(>PnV<}Hxaj6XvS<4BhDP{vSPZ*_vw%L zW-iZ;t$oS8|JLOlhn61fxhr_Sth}Tuw9BEmWYab0V-x2b3%-=$T_3n})0ACZZ6>e3 zxUE*x{5|=^f*tvP(t&N1~|dq4iwv7nvv%K}fQ|LA#G9qRSk%I}f8sF_~*mg$A7 zb`|zqndZbio!{B(ol?@S@)ajCkN@phE1^Ckd&9*go(#Jt-(PEP7#LbKGc96yQ$icN zP0g$5V-*_?7#x^;TZPTX=Ik^3g0q3kmz;UXO<~;Mk?*Lix-|=l-}U*CZXg&ajw>e`?=4So~kacO-Ya46r{6W|9Sk=U%aJv z7<(hVCRQBio!D?|m%*=F9}_a$g`_x-yy|+p#GQeqPSmt@^1a3f>$iPsDSo8SH1$es z^6^DZIX6x&R;_2KX>w3YcX*jS|480ZCH=(}ef!l`91xIQgp?Z?Y%{j1!c@-WW&`7FTc$IOjOb&kn!*6rQZTK}*4 zz+%;m3v=S`h}iR}M%deP@4mh~BlU^YJ7ujke2*;iNB9+ebt z^;r6QZuN?i3H_WbZN0L7ZdaRr)!16U_D#Hec-!JW}cNN~@5OOPK`MDddwftA(!dLuz_90+5=ZV)3uDi~;`i4JrlKI^94!}u?62^jh)pvYgBjGyuhlnB_YK=ou;9OBwE3o*+Lno)`x-bW<@N_0 zT3wg0%Kk~pI`8$qMzWt?TC(JZEKYSjH0zhYn0)ioIoB6-+&WyJP1n@xc=LUJ+1eBUD8fg%Sh*Z(teT@@!-+v?@@19 zB+t$aTixKR->ddQ*E*o`UwP8=o~4q>|1N9S_;0j{6sS%qd~<1&t4^rxv~aebL&p5! z*TXF~e3*RhR{Mr(*&BN{bUH?-{SfZc(S4aHrLne`vreGVYl(&LoSgF|jW;cq*>(sg zf2k0YWA9r2;Lpvgm+KE%Z&s+9;qmWvPL#vl?-L&`@l9b}W7T93%_cPb`Fc71-YnyGp74b#)hm9yEu^g8qM-?P$wtpDs$ zs@%RzyLAQ2ruvhVl(01`le{}oTpA4QYrni%{u;%SU4tPe@@Y zKd`P|;kiRacUjh7qn-K(JGb4vq{Ymk=6AY~HK#71P42{u7rXNe!lE}G{&gxf;okm3 zw-uHI9IPyuaM|a{yIePU1wK*DPw(0fiw5pj%F-`(G7Sa4_oJv(V2= zo_696%iE0Waqk`OAKD%>N1;}&>6)T&zt$^(->lPCT3Y`OJ}1@2aq+&{U9SypR}Nk) z-@My_are)N0!4?_l|lEm8Q2INEVk#jUGZ*-N^|rnFV_#rJ9;@Lye-vspBQ@J<&wFR z81sMp-Z#xLqe6E1xu2#fhwn^R*Yz`(;WasZohd(In&}&1wTwLbBAe1Z3%eY*m#x`3 z^(N!)gW;(+|Nr(l&}(YLm>_cV@;B8VyH^?qn9T5hd^%m}?bFBIVqb5IZYgRp2}!Kk zV;`REkfgIu?fWgYl=PDL;Mdu1SvF^*qe~SIDtK-BT_;h*^k%(!YuNmmRZ%VPBb2R?XoL7 zUt4?)i2eCq*+JmMX~Aem`P=HsU#@R>u%l&$iS#rl_F~@rhc>5H{8#GfnQ=*&OG=PW zWbu_p`;V(sp9)@b?7a8xZAAt5mt8&Jb^7c=Gt)En6D^O#WwJj`%wNJCTz%>A8TNEu z!}&gEb{(@kwY0Uxe)Wm^qBkCmy77tcOjepGTE@w|+9Je|=g+$E)$Mh%Hq(VJ?0uXe zyO4ilk?+Q9%ecR)cwOIqLOAX7>uClB69vD1m>+mCg1Nrj?ti>^R+28)qSj8eCzDrR zbg=vo!SS)AFo`exTKa6>vg-VjG!}=IftlZ;%)aK$(RwuL#@_Ei^J_j^CNe6T%-M6R zJ3+x&{k5CO;eAWae6d)3*!O6!N9xk+x*Ochr<)YbSkPR!zns-XMfC8&6KV69+zVtc zy{ld@_f~D*#?{Rch3<>n8(;Z-eV_gNlD3Xd=3&*0_SeFfmtMYL`|7gd$JS)Iwt4Y7 z-c9B?;>SPh-aG#C?thO?quQU1+iyf3DXc$w!=lz=&6$!M?=v;)1G~g-HZ#6jnD$Bb z#j;bYtg_EK&fCM_sX6!3di8lnMYmMx@xx%oqs5+zn-9f`395GdB)gXlX8=*jSLpJkDIP4ePm%^t2-R4Dj0RbGAYGc^`V4Xx8I%&?i62(AQ`*4HTU+H+DLQB zzW?~^)90BRmTg(oGwc52q=xsm0>3`H`tfS7`L*J$_n0pC-R*3#DQuduNACZD-9MCn z)qOhVbl92U$YnjD-&w!6y!^1?U+Y;lrgZ{||FvSHzS`z~%(;K|W`>dK8UK)NGk2}M z5XIRZtZtr}c&;%wW!f#b^3vCT?@VEkS<4$;-C}3Lwx>;<*ZzCQVPhQ;#UnTNo?rRT zO(|K2Ux4Fr=vkAR4gF4cPNlrv#pu%+7jaBW{>ZT%>F>`xkB_j>F-;A0Dmh{6JiBUA zRTyWBw~fLRYtbd_vIpjzS$H#T-qTA;ckc+t?n_y%RI$T0J?_D7vy*9@K82SOL`;+8 zEtfN2e|~zU#JNiyOTVRDzUs5)glWR`)Ih`6mhn@UU759bZeLuqZ-VNjhYMWxGBfzC zcArzwd{}<&{}aBp4}Fa-W6x$7sOJ0azs9pPJMi_@Z;dmImvWbVO|ve!sCoXx<7*{{ z>U6disINLyEyC>4r2~qU(lb8Y6nyVt~bnF`b@R-+Ec!`%hIz>eHXg! ztI~E@Y!}~uSApd1>*wUVg>q}`c^I3s=hExq_skRZ;zO31tX%xoVeZFj#)R+IRrNc) z?=oFGBz%zLboIThe0rzs&)=+H607j#N8z+%4GS+Tvi}U@zJ2G><+wvxlUR?-*zTA; zS(&fXDR_s;JMmLFmM0wM9oeyXwRVEcuKH(omSG#j1a`?Q+|a)|*;VM%8{U-8J`S(7(W zA}l-CP<7(N4gEn6Z*{#AX}C6%`?ko_OEc>y=L8>Pm0#@BsCD{m3P;~Ak==GbBtGwX zb~f;zq}YqZwZFFfd3ird;kkq2HCus~%3m)1mYQ*Do9}7R_MO4MxaOri~di&?a>!!~%*Wc)OrO)n7^kkbb zg|rh&Yl^GeOCnm|`@B3;q})68*W;V}CfK)GuW(>@tRUPMc)R)am5WZZ@{_xt&N(Hj+5YNDlR&}RNQWycSL|bG30&t^U2@@D zgrQ~NpNOvkP3!)~J?;K{t>9c6%cOVG0lSZ1UH7H9@a(nj;0xDVCZ(i2KNmP<`JzY6 z`L_fUgC5@x`~CZ!c4g<@+r>QrEzvhat~1cs)qxP;!O8DA}e@$<+INX?hg6}k+yd)y|fg`>|=49hsCtLrw z`+N929L^WMk2Alr_dxkci$BVXr#wBIr+GKwMU!}k;bBj^Le21^4-#Lur%n^x&?nhh z)&D|vGUv>zZvwoxt~ESd=VaaDUEHAM-Xhez``4O!r=Ig6H)gZRCsf_Mx9rYg<9#Pm z)ixP+{#8Hy^6k03`E37{ELbd)>NSP>-FTlrR0`HzyE^{Iv30Izon_eHNUQ3azKt$R za&o`@cmH2j2H8uU%*J*}PPGNBhdi1$EWIIq<#h3du(B_v^H;w=8zh!^yK29>*j4Uz zB@DMGRw$hCKeYJ7+S#QU3!_`_ER18|_F5!&Y|)Q{C#7|pbMMB&&HN`QuZk|K?OJ{`FqSfAJ(gG4p4e#FLK87L_-=KETPG`6tZ!{Pg?rrKRaX zn=a1|54txk@b3vTdHuK`;ipSe{dax~Sh#1=D#@gcxqr9=oUUa_r#-*t#I{a^wdly} zH9wCDo0TrE+NN)j6()5!c4v_A_8@K@zt0`lQfu0nMGNc07cR8A?pL)muIbV@U(4wM zzuVrt+GbnFEOhip?J+Bt?_s5;q1%1m?P#;QH~0NF|DCUEJQE5twp@yH-rk+c^quLi zeMEg!)dtn4TAcS^Ug2dfxVm`qr>(ykE~Z~v@+UX#@Rt*=EbD^mR2H1%nAQHUBHs6V zd`8QSPn%EN=$d$I(k{DuhYcC{w|}wOtJXSwOAcc= z3r;thoMoB6eZd+2CE_{{{Dj1qUGg}WJ1x6Xx8Zt(|Na{p+46kb;`<0pBgq(#BYW*TgJ~>S1wKSn9jN@ z@ImSPHLv;VPg$m|yV&~gRxopHl#|<${X#P&WsZovpY-GW2_Ye`RT5XX?X|f6UE6!{ zt7H!u*RMj~o32KuFH<^nY3aQ{#w{g^Z|?apHR;>!vfZaL>+R1E&#t6&tUZ3vVzd4X z(Ln9P(_AY5=@}?aSaewSfvirFo8*t9ED;A=dutDc|8&g~)7h=b6H{6-_w9)n3mQT; zMTo4ok-f{>DY*QL%mmfRG2#V#yCxQG4iYOoTk~Vxlb84O)(32MbG{y)C(3!^hhlZp zhuQ3EUJiu?vAyAc=jk4oR%~CVesTNk{O^wax1I{^eqq8EVrR5lPN>-Lms(RX4|j)G z$m!G!WgBH1qfLyKUuD*3uh}+b)~;o#k5ifc=`Fodb&KcLv9~w9Z&j{1k?#T0LwyX5NSl-J;st$hDAxZm1{!WORwYy!`ZQ{zeH~l7`I5B6snaHAVE4qUI zd0b-)6pPBebb_tCO7Xu?wtt%4wx*ja!VZ5aU-r%}`T4Q(iuQ9N+fJV>;+oo#k+x1K zP-~snTKSty8>gr{&0N`Jxbc(oqPdW0(#!fqI`k?Kh zp3vu#w-yJr&u*W)M?-UlO%adNA=iEVr{`)uTQl81$!$u_y{J2L4G#IPlZuFIioNU9 z(6GTpc;dnTd=)GIY~|T=wNfraSLn3UA3m3pyvsZGI(GyJ{d>uIP^Iqb)Vvm%zq@h| z8N3LQj7{mhCHcdA=knF(-~9Mr+;cZ?qS*qsJ15rjPgP-UTUM@gYOCGZv}Ma~K36`u z<{C%)=8D&5-x>ttw*N^8dB?(-`(>H_g>@F6_oX=RSj)7Aq3B2MyMo_*?`jV%_7mlf zo|(R%_u-Wnp-SF|CS>)Ulh1WDyX(5@P57EN{foSlcQsz;O4j&4<$Ai-huON{7IZ|f zuwV4VYpFu;_vshfZmo%ov)vo4_2HwBh*&zSU2SvUH8v+!rHw^i6sylV< zn+C!2vc`LT%UsR|msuV%%rrZv{&~(_Dev?0ec_EWCtJF1xTB;fX8X%opyQR(70IJv zvMc#_9XCw<;hgz(b=Bkt#g64ieRn1^8{Sn7c%Az0o@ema*h!l@HuG>wrJW2Y6e_h9 z?RnVrW2Rk#kDc=?#+;g(s)}RsPOY{r;xaG1>DqoY%rUE-(DFQt{2ra?1-fS0?4%z8AsC zz5AG0_!TRY85`~`ez;`$iocK9f}g3@y-Vw^6HH5b-}fL&jcdbnnahvNqEqh1_NpG7 z^wxXQZTnlMx2><(ha5l5ms9hpD?HboH+TJX{(rLi>wRb5J$grV)*hzLj>-S(+$Pno zVm|U~%SUae;#-lv|AIPXzdm{W;$@+U@Pi!XS>hiyv8v2k71VbzZtAq1A0I5?y>sN4 z=&mf2j+;LUr`7xXl%KTpjMRjPlI(MhMK|7Zlq7_1I5&S~Uap%%iJalwdqU@zG|&EW z$d!#@v3UD;^~ZmO*Ef3l_-&9|{b%c$)tk%ro}TG^K+JP)w|wf0FWl=qi~e4)R^wgy z`N9#~=jO7Pc0Aj&(@F0e;}e$sPmdIE|5dmVd+cJgYMspD`C=|+fk7qrK1|3eUmf>x zk>&N9wNFmRU3*jPoBJX~@bzZilDtEfBH2Pl9D*DBV|V<1#iSa0MnR43=c&w_8UJ1` zZazNaMO)^llT+OPRzG`kV~xdsu(%#00KezMy&c1r0Xw|8mgUFVRycB41UVdNN&e+r!_y zt+VesmgOpZYi4D*>7M&vwM8b1GxT@M zo9EiuH}&&Y-8Fo`D${VNr{T5h*4i{>=Z1w7%L+cFzWivu>&C6)K2O(onr(2=G2Z|G zo4x_>-cUu7Pr+Tu&U)X#pdw#fE&2%90D@o>7c@c)*c z`-(9Wjp+RHPP~J$Mjobll-iIYwVp_B^S1O&$dNpyiLnn%RlUHDw@`3Wc2pf z`DNelE-yW}KKyrT+QmD@jQz$O-{f=mdp&Z=>h>%3sdt?9TI$~+7yG2!**n=jguh8U zb8_o$8D&oIl}o=(V0`|?W9#B~{DQ{4U%g!_Ox}KTT2u4#!h%fUsohbUN%ulN70os` zEBSZ*k>tBWp-E0BSe7oazanDe`#ayxOmDKw(xqtv^W&%HT@qV+l6Q6P*?&iurzobT zGcoJQVWc;$5rncl4c~$J!@ZuIFwfsoFg= zFPE{-(7Dm+(fnyzhmR=V%<1QqKQ3LeY-M4Qld7z#1$Wb#MQx`bV~ZRf;*S{X6C5)H@rk7T*=;6Ruk-F}+FYalc`3!5!C`4mO95PiyS2 z?LN0};haMy(hn!_TGpPwwsP*nhcnMvF>6+K*u7O~SNZgNdUO=u^2!AQhgsED>wT=V z>{FSRbvJEO2s0D&j#D8%?_C|VL>jw-I2ALFd~aI2W>(w211{$`SV^pzIXl2ijwO3T z9K-Ef=BB9z0Ut#YCjD^CVdCYo)_JjVPEYgy<#~*gH^&>79-6&Gaz*d`49*YKFpK;|4pS8`Tyz7?N`KO56&g=H@=AF>oAgj#fTKIY1WaX*$ zDZLW=-@Hq|8+0t;k@V-9mYU!Fg|Q4fK74bXf7(g%+0#9l$FF;TIo`MQb?SoOhA$U$ zSc*-SQjeSR)5xoivB_Hph1D|b&nZQy=;KREHT|g5?sv&DQsr+B7AWIqr`%gOg$Q zyX;jfu86M*U)8wGzw%n+@_lm>?=1RuD%2>BFIO;g>f#40F3l=W=eclN3q z{`(VDEhoEv*{gm|;uk|{YijYb&NZy>?9c4n{8ybL&Ty@Xdk;^{-cQ}X+24B|x*sTU z)N|`w(ZH>%Bpz;5SmU}h=6CnvxqlBoW{lzH64TvLEZoQ--<3E=uFZ5??}wm2=T@H( zo~;-@qi_4UQsrIT*DP19xpT$-U)!;F?Gv)|7kJ-XlD$&4Jg-9|e`kK_w;8-s{vO%% z_UMz&pYoc=M4}g5{Sv>I_nY6poZHH)jk;rJK2g}!v&sloS6L-E$wm%;7?m=Pk zGv(PC=8jxVqRWkqxvCi3f`7|5*jG%s)3oII&(o{6oUJwa=(Kvf=xgq8hGBl%=c?^$ z1FUoNe>Rm#32dIGe1xr{>(x!kXi+w;yn_$kFS_hGZ;9SDOO1Tz7u{V-e0ph*FIu(T zopi$VY=Arqlg+EQ{=e;KUS_jsnfik(RwBk~@9W_1gx&j0md7wCG{38FkaXWs5_)iI zol#`gzf@DUqgo$U-FUI~eBj5GmkoNNWR1-I0@PjFwM9-Ud-+RGikR!>KPzLxWVyP8 zgGz@AMJK4_*j?X0ljZc1gML~!D;_8czV^G#p7g{w^zY`LM926UTf}mYs_D8Gd~f@` z=Yz!8BF)o_pU6yW5Dm`kJzANno*gAHsqRtbm5Yo;N)__cln&&-XelcfZ*e}&Q6v?5 z=o|DSdJ&1pBj|Gs`FF5H=A`SN+q zZjrXT9;;8xVK}lhgsMfSK+w^DLUj!%fA7EV&$y-_lc8|Q-c7bM{g+&u@nNHF z?XI~I?|c?}E^Duy*7^B^j@JLZ3@UPWXFUnb*6X_eIV>e~V~Of!-dDAco_<`@$hze8 z2Pq-rhj{@ePbwF(vb*RZWX|`J~*C`O{;i#Zpas+!Gw*dQ|;yu!Y(y-s36}2>re2IgNhNJ*eq;5Mt$|~ck?>6|DH8lC)@e@`ue~B9$$RtQn}SF;nyR%l{@yG zn)BH+WmAzrMZ=zBzQ5W$njAl$WDAjgr@QWf+i&kwzDv&xc>3j6@vW#S@bo(2IOB0u z18CSjnLVB&|!4&%p?wd*R z%qEdup@EqUW*c&tTi89bOs4Q`Zt?qaZ;?LRtmnty7OB3Nrrj%ObI*KwqH(#eRe0W( z6fe_7AEzcwPqVW;ec|Jk>#SFqWF!9ev`>j@oys$1x~lX4Jh^$b)AZ-vR-WNyqTHAD zRGd{~#x%DTN!%A7rYA5f*(+3ss`+O5?Rn@rd$EIizoGCxi`Fe)I^G%1xw7m6XQ$DY zGwVObUEn;Vnft6_|2e&Hw-h$lx8D(H_`Y3@d%}S!(Y*f4&imiCS?{Z9nj8OJs^?q# zxgXAQ$GI+^n7&#fd)f@=M9#}{7jOPPab?-IoBrX=%Uc(#eS790VsL&+y~^EIgPx7u ztNb57Z0HtkIOEtpfBvr1`8R*CHl$5wZYz-!p09aG@524n9^GqFRf?5P2yII4GBPP% z#q{lg%(^>FcFKjeo{dWd%SHDD*m@*yS&`y5qrXRNDQAg?>nj~c@%T;8y&lZXI{tn2 z-J+$5>F3!N-zvToYv!ceUt|BmK{1(AgKut->(3p_1W!%f(s4ndW?M^w=QdqO?#L&8 z-S(?A@~*vYRhT3oZE&l==a7n6^?zxzd^781%zEydwjGFMiJu(v&s^(5iq7465^K)p zE}g?QIW{i*8H?(jhCThyW~|FUe2hcH;r;R#dp<=e#T>Mp7BKPU`?HoFM*p~uEKprc-%*xiXzIA)_)_EP7v-tVBDsz%!lxF$dN1ZLD+n+&9-_(W=_FD{M*@G+bNxIYN75GV>vx0GTg+PR0kdId89t`17M0-@&SLpO>_8-gq__;5w2$CzikzZ^-^QpkW&?lNH`xMT(2%lpu z)#%w8BdFTHSxut6z-z9ksZ$?QwXpJ1zMg-K+YYy3-|{8xz^}tyihgT%lM_BEZ0C*Nd1tak5=caq7{JZjugCT04JC;ac2e5PbYa zXb0bWC3%jyOBCYrDo#u9vR2By7irfYYVkaN@in_kSI$2u^A5att8AO&$5mWYk1^b= zj$3%=?&`%R%ROuJ7mNIscZ@YMeR)@{U#^#3CVhpa<8O|$XFZcf?X=P^m8$i> z!-Tr6RTSBtT^A{0Vv1h6a?@n%Uvp~~)UPd*n&|LTXWwLTl}C}gWM}@o5jOeYj8B0d zQV(3cVWq+P_RWc+t;Y-RZMf1i^Y62iWApAs%rH2@_v0qZq7S!fKOFORNDw_aA**U9 z?~;Ib`&^5*Gg)^1Udp-s%+mD};os+54Hpf(X$rNppHq1-6INJPK`TIc@9gby- zpA=TO@$NpjgZ;8i>)LO6-wyFF*}eXb+Eas<7uC1yKl;q{!+XK=f8X|Y?lhed@^$?h zK0Ssx#S261C*3`GE|YJ@Gt0b3o^`CEz7v9{JW823IdJKo>aM-ZP90sr$$r^nO3ATP zXD@EN7xT>`Z%4~6gKG7y`zQbGdBk8j_j!o$4~u676Q<4dZEh(qHrpZp*4Q=jN=;{R z-4Dm~^YiPhH-_GIaS~m&B>AIR`OY<5K2|~#9A#8~%KW}mQsl(kALd-MS4?6Znc3wq`ri<4l_ zfwaAu^?>yYJ(NI@OJrXZt?WynKCOcGaWWWci2T7dY~A^Ok*S5?#CC%e@rd$5rl| z^a^k8nyA?Gp|_spiN~uhzRC{@S5G;0Xu8~=YuT2qSh3sFKR9a}OY;e@x!%X$sIk40 za&oHZxOG-7wCbN&qVrV&rx)#tn-fn=jyCg5Q+pQT_L*E+*%3tz1{^9rhmI~3G?5ykNNoU=SJ>k@!I$K-(K7)Pud5&fG znr>X;%JX6uI{N29)}NK3CLu{m{y{Svl$f%wvwp6MN>h5hE0pPSfLitInLdAlbNud9 zt2g_EXzB3u^axEmBT?a^EyNRyOWPHl_5NLV zlG(m24BEVauUp2)Yu4^vnjTv|C#DO{RnG6tT96PZEp_7OkL^KcrZz6Je!R(P>(`6- z^Eog7ZrXinSJRv2A5YKF4_j;Nw6-?o#9rTvi64a~>U0D>tlawZPOfSYC7Bbk%5KIR{4+PPov z*pusbZ$FY0eI$NudzhhA)tk`$Lb0EJ{a*bz!10-3SM1UavZwE*B|W+m<}&49=)&!j zv}EQ=&R1&c{CoMpnzLs999NU4#JxSu+ftdwk*6*H^+@5KpLU|xW(v(XXrFvw(TdvU zFC51!t*o!Liahu{`;z*rFAO685AARIs>))zUMqUC%CCz*j&h%OXuiw!$!uqN)|A=i z0Z-cxX4tP=I^l0i={hl%*S8LubM5)H`L}fIVU{g#ygwBjtz(wu-Ty&niG|>aR`sPE z*-I^tez4hSwPoYvLn|1J=J#&;li&Vjr&frSq>B2aE}#7CwY&dnZ#wC=i>v8Fr{bIw zQzX}|wqg)guRSC%^HZSz*DS`je%rP!d>uAr0i#tF7n}Uk*Nuf;yF@A{&zd5 zt|UjlC`PmE@(NcwlslY`+Wg!Y6m;p~Y+)s}*)I`_V1M>|r^0QU8y^K^CZB%0+2bmor=E99X;As?i?WP6 zFSu^b%JX+y-Ec2_#`&4Q=I8q?@2g9T&Vkp;9phk0ILnY*PB=bbyo0x*kt@{;aB0fH!Czm z!uIm*=-rvNW#QYJsJtZAQlIv+3)5DyDE8P)c8<-f+TEtbq8h!Re%^+!w%*0E&i+w} zjYdKRXAD$aOX6MkzwGXZgSC)T+crrc!Q&uy66l9{F53 zwJ+(M%=G@HyfbQBv!{Gp-nGZ|(dEv^Y44XA-6(3c3AB$9iT$MZkJT@D{+;LF^2`4( ztjcR>Pn6+Z^t9!|#@R2w+Zk)fMZdYV@~=_+$*cYF%CfMYuKzc@ZKl<&WqHyZj@JYFcj6 z`JuD?y}yO&*0wsWnf6@o*Y5RyY_c>WP-gNEtEu^}tl?~Tj8r}qh&_23>)2e^Jr>{LZbZSLa_3 z-=)0#TgKGw5zH0ka`KlmOcU1{{*%#~W~dm}V6gS?Gs$~86Pw@cui0hMctOzUne08M zn3n>-l&`)`JYS!|d-e1oR>QAM4!@VZmHxeK%LFBtw9D@=)SBoPML)2ZA(Y8KTP*ZKKe0Vf6S%nU4f5YMq@+uH?mT(>GVzhgY- zRh>5N@C}2Xne(`wI(%ea-+X_@!gYsa`omSPPfvW;I%mRy7Uq0M;dN5a`{hOV%E_#X zG%bDfx=2!RO4o`b8`tW4rmTo-Yx5Vs8oP#xb@O$1o#;T>ZS9lJhsOL{wbxJHtzI_W zb9K%GZ3)))+NN?A(VR`o*cK_3DZcrnWvU!^(r?D1>{pw(*@7J%Z)B?fsR3A#q7;ivd1)Ut%=^QURY}(&!95v$DjE3*Bst?=@xC=;T#coBbIM!V(_wX+h7xg zCcb4;rXNleFMGFPxhCJvc8?`|e<$qS^uEyaa<5wb)U^V=^I89Gi45H~t*AiP{KDlH zjcwLE9Ut~yQaP<ggl%0O51Aj&`EUOvCTELrR{(FZ zH`hkXwBJ+I^@Pq%U10U}$?LsyRZQ)c{vPXXmB4Bpo}uE4#(XSFMDiNR(sjtZ~ALRq3g5EeO2?q z-TB+BBxgjc9DL}#TiwI$?S}=89?Z%fJ4_qq>a1}4Z+>OTp>D;vYTi3XmTgh}bg(Wl zp7Fk#+bXuA+4tf<3)bD-aqOaa!68-i*?ZHt)Q-ik)Mq&G)MN66(s!*>Wfmvzy%RiN zR;_9I#1+N-u8esFoE`O%p)KD&g&ebQoiEN^pe4#4AaOVB?a~+D=DpCCUwe|JKxUuL zt;`)Wd-)5tizxq2-}56VMx-E2e8R%XzUh}|>$e~CD7Gk2S~#mmy{a#Go%))@E5iD^ z(Z}+||8U;@|KjBRtt*>b6y|U%3uevll0O#s_~FW{8m$vOSGdI3$cJ-JZpxJ}f4zH= z7r7BNqO%?JS+3x=TM=g>k;;)?FQK#tVESDwC=&RtEgboB1W` z%~N0BGiA>4_a-$9?qFCkb?@Xl=O$V0h~;6MclT;0yWZkn+q;uviKy~~qaIKs@i2QW6UpB#pKtY^mQdMRJ>~V0&r&z@*00fIc>G>Dy|N?xqm#r% z-~UVgSKM-2d9moT0+&|L+XwH@M`T#OP&j+;ePEq%y7tC(w=WBIIJry~{L<>zCMx_s zh<}$)&=DsKcB^Qy1ru(qG5RoNn!m4Rz?-HmchsiL*8Q`3;-YO<>koGN>Fl#{dG>Ms zDhat)x9q0Q`dRxl;=bXwUuBUY!pe!)x13Fw<&fd*dEr)UUxAv*$}e|+_O+j7lC|8# zRMo}d`6M{cZ}Gdvb!i?`b8gAsj8fjWBt~Ih;dv%Dk7!r!8m^oSm()wY-gjr}=O;AP z>^U$?^!4lBu>F^}C_HW1u*Fp7$pM*+$y??sTv6Z&^-cM?b#~5wi;Ho$-ad?xflHh8olHgdtM8I95p_KCfo zvO?k3I`6fM>};1kQ0zNj;P8Cwl;<2ulNnYtv`$~T@ehx~tLIl|Zz*;vobpore*W3A z$14_g>F)Nqb&E@_;n+r_%$c?JmS>hUGK=Rphdq0?4mmDOYY; zYkaw%+$NoI_OQcF4bh0HwN(Kt?(|KxRx_TXedsj3E&(5Q}pFG%etmH^ZzSG({$Ljm%o#l+MSU*Gh zVPoj3k4t*qUaxG_;k?6SAZfHbuZ(` z-3t~RtqhufxU4NDy?E-ms*v?f0SAA4m3026|w8GIQ?)>%*7=~rmqRudR}}R7pL&0eEqrC zA4mEs2373hkT~YLbV1M(zBNqUY!|&A-BT$n-1JMO=lNx3O#yGoHJ5X?DD|(8F@60n z|H|{UGg-!Y0{@rYmWcg0fAUY?*5{#zO3Q9H%4UR}DDHgzW4GEwuRYRNLr;1M|Nr?Z zQA4cgVT|w%bvKz$@y2Hvf;VY+$VwW$h%~nrop4s7^O%Nc!m))Gjn^ajdHz+*OFGi; z^G0Q-=dW#lSeC1(D^wZXX{~PF;5OmvXx9HIMb*moqe2w_; zVxad!(KhbD%Q@${w%wfnpXHg_!Z}YSONNO}d47g@wqLT|2D5iU?++`~;2z83-*$UeG|T>*^Q0^w?CzHNn?Eg8|L&2m^z7KX*=NjC zRVuU=h;p_ZynJpM|BvVPU#x^zcj?NymX~MGezGEKU)HhwuU>9Z9d|flFP@5yIJQp{l9;%dUH|i$!$GzUmnfla)11!I{BS`LixYdey>h+EDCsj=v<9E*DH&h z9BxtPOb4$!v(}n#3=lir)3D~$Y2JqJvD^Y~E>*$TU9ffG z@o8H$-+#7veRbmHl9Mmjelxjpi(7}~^y#^xmHQg|-WyC_Qn0S`&HGd84-&VO@>;kB zr?lU@UniFR`BB@g_lo8%PNDnxO-{btAr-bZ_}!6yZJ}h- zhil(3$P0eGW&c0yOi)VuI`cDAS3F`i4b_M!NW02?r>e+&{lh(5PA%QrQt>IN^bE_B z7Y%AbIj4RHX=)4fp65+il&;^X_DVCsH2g_-prUxu!lho<*T>qFW$&1LHmboypjXT0 z|AZGCr!(xWxfdzEMC2yIi}Ih zY1@+W&C7DWrDY~cNk9IXb$9voyYcEzoQu9YXf52O5cp`H?<_al$?^h!AL*-JvD0{x zpLZ%-ZvKChq%Kp!YQ@f&RrA*IE!<(c|FyxdBHa&7$2}fi zP;n?Kuh8u zb+}slaGk`X1;=ymN>(;b`6|Q_#Amiz{+X2XdB&J;o$H;WG;RJmn`(8uv0wJo>}avE z=~}Kuv8BN$tbT1j%5y5@)VJT9Ih~9=OIzCx-?-=&t$VcURPJ=0&en%|lMm}pogeTs z&oB!Fls%4bjNGS{5jg*gx=8Lqg;zdG!RHbsKbC6kxach0JloR7cZ)zBS2J%} zbamIBx$AUhX{}JzTv#W*=U{Qx(%>&!Z3|5^pS>(rKeN5&>ka(`3#N91DW(Db*RRyt zxo{`h%?o?eu=Q$B-G(%`EzKOpUb_z&f7y1xD#CNO(gc>5mp^dKyE|7$RM>60%Cm!7 zn~W!DCN{2@D9_|CTQGg`1J8(tS^Zg}eboiVHMrh zKhNK|So>zanDxKw*zxPWyOtMeuL@?HZf5;g@BHeIs$c4NX#V~4Z-4S*yF42a1ilp>2alO6So62cg)?bmKKI1H>I3a!=`X@t3N8_e01qkNrlZrO?jqEx})U zre&zFDLM9JhQXJ(v(oIZ9eX>U{X4u_=EB@!|9@RZI@Wp-_iGQyx^pO>Q+{wgrcr?L z*n=tk$F<9^UVF@Kl9xG`_1c4eal^-4%EFuP-SHH-5Tvy5kDR=Wvf@3-lC4rO@3G08 zd3yUc=d(9lttb7pKD1P`tNB34ny&E<6&glMgzkYl|_~iSI zx~dAkp2yfrx3g%~KIi_Z^h?6(u!)cP({$dmt$i7`i}UIJ$S+D?S90uX zcbw$OE%vg!wf*Pmn@`u8?bPnLw3;I^^j!FE4gXLXKf77|qHN9AE^PW69rgcZ!rRd9 zuxl4gF9*ENJ-OsX?70XHZT;3$$29)a^Uf|9r-y2PGR;zx`ys&?M%S>~}wj zgCh2`FPF9j9@lt~*YQxn-}zt7t0(=(&u{tBnmR9EcX>zD%RTP5_gP(OXnywf|Nnwx zynXMNPrsCso@(tdJ%+)07kky~Jedy@MVI!}@zp#06S%uaLI3tu^@9f{X7oJjUlgo+ zB_v@d%a&G?-LJLwOx?5QUp?oun>)WWx9@(JzU|vC^^CeHefc>lvi4o?C%ivVtGswp zjVp55xQymxcelAkZy78s`%1=U}!wg2iq?bV%k@iPyqzF#y;C&fjn z%?a_$|H$C`|>aZVwl=?0BEpu{NMc%WjW1c=oWa}+& zW?1WspWhKW_xaYWbF;;hZd~x_S}|FY?arw~0jF!9Hm%M*S^nd_?4Hmq-^I@S^@Acnj9{sDo`r?x{A7*TuF>iv3?y*&ybhu)LoLe;Ah4!ed=3_cnwz+jy%g>lE z6-VZVn*}yEYUZpp%w?TzdsQIm_U0#lm_+5?x=!VPnZGLR_eF6V=b2j{Jz^8{y5zPXI{Ui_0C3^gjUDq{X$NXm$s zH7)Rbiaf{K+bYg(^(lX9Dt@0iCoUe%E4WC0hsf>9C~Jv1rvx6_oO^L>rS+T-O^#mM zu9UqpxIgL4l@CRdstp#rUfhLim|Q)|v!o)PoJsI!6yKF_;Appf^A4HVw{ohXo%T^4 zUP-d87K@WaW(9`+DRc>1V6(sV$^-j%PA$nokGQrhi_e}HGRfdTjYY_GC@nj1kuzeKe^ zowPLnezl&bbyuC_aZTALGVXiD_=E4(*V{UNpKQdo?`!c3w4deKvBm%Q-fY`HU0Fke=4ZtKlsOCB!$ z+WNuCLNn|t!^8DCA0H^1^57(`G;`#G#3d^$&#s2eOJ?5U4 zlkIsMEBb=t#M|=$ecFa9TB{CJincbUJZfH8R?KE@yGPLKK#$v1znc9#a^`dRN_~lCJ~It|ir6{lCA3 zqft?r_2vQRMK4R9M3%U|%DB5KnK$iZvgzyMyEFSgPEFEH*}>!>c>2TaZKa{Fx~-#s z83YziORT&xQOm2gbK3mp&gw;PPoFp;^G%a+m+#T}FMdy|epbKA>_qd*14<=E?=z=J zHaUF0y_Ty=@5dvn$b*)*Hq`xi=@hoA|K{?mFWx8b-5+LK!Xv+w=}fYzg3k_rCPUpN zSr<1Ze}1sJR@(0C*>lzVe`b5-ob5~sc>c&jSI{k0b@o2iUC$0Av(m_^1+A)xy#-ASftj@?r;1S)op$6)-k;mGZk05=b65F zws!GzkLO+g_O|```t0F8)ms_U&zO7q{_nWO`J1=eqeT7s+|XZULsP?Y=Q@}Ik|(<~f1Hwzr@dvsjWXhvvF+J2es zj8k8DcwbU^pOZOducN*hhswf)h+Os+9S5tzb~E!;6w4W3Hu@Ki6K>IHcykE%|@SPhl;V z=#0nOx)SRjtXdTE=Ea0}-XHco_~RzGCgXjc(Z7@CJHI=ZO;bAhjCV_8`{S#9+~_@~`ZZmxUCvjfJDOf{3Z5}pYs{Nu%n>NTcj#RE9EnCv zAMr{@^{0v2aZ}Vfcx#o**YouSnmSlZD0DYne_zcdah!X@)SU}^n^w$T8`I{QJRvBC zf4%Q#H|#L6IZ~p#DkA7 zNR>#teLH-@efx5$dQVwV{w2QipKdv`TdVx_1LuP2nwvVeG0eDohLx|?C?~;pezt5}_ zSels|SsEdZV+}4z%u7iuO5xJ?SFo`#Hb_cLOtdsGvNTFdHZn0Ww@fomPD(N~GfK5I zOEa)cOiDFROS3eyFitZ}vrI{~G&V{~wMb1$N=&shGBdN|(hpUzNj5S}HB7clu}n5i zG)^%wF-b}`HAyxxH#SeSFf=wbFtSKWGf6c~HAyu$wMaBEG&3j&lMmneYFy$<5i_smPnS1?pC zPyn%A6rvSu%#6*%@me>W}wWrpKL 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);