mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-02 14:28:14 -06:00
Compare commits
22 Commits
dependabot
...
feature-pw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02e590c70c | ||
|
|
3c53e4bab1 | ||
|
|
880b3e6d15 | ||
|
|
f7a6f79c8b | ||
|
|
87dc22fbf6 | ||
|
|
2332b3f6ad | ||
|
|
5fbc985b67 | ||
|
|
7f95160a63 | ||
|
|
1aaf128bcb | ||
|
|
10db1e6405 | ||
|
|
0e2611163b | ||
|
|
b917db44ed | ||
|
|
bca409d932 | ||
|
|
07d67b3299 | ||
|
|
5fca9bac50 | ||
|
|
b21df970fd | ||
|
|
833890d0ca | ||
|
|
eb1708420e | ||
|
|
3bb74772a9 | ||
|
|
402c9af81b | ||
|
|
c1de78162b | ||
|
|
f888722a73 |
4
.github/workflows/translate-strings.yml
vendored
4
.github/workflows/translate-strings.yml
vendored
@@ -12,11 +12,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
||||
with:
|
||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||
ref: ${{ env.GH_REF }}
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -294,6 +294,13 @@ The following methods are supported:
|
||||
- `"delete_original": true` to delete the original documents after editing.
|
||||
- `"update_document": true` to update the existing document with the edited PDF.
|
||||
- `"include_metadata": true` to copy metadata from the original document to the edited document.
|
||||
- `remove_password`
|
||||
- Requires `parameters`:
|
||||
- `"password": "PASSWORD_STRING"` The password to remove from the PDF documents.
|
||||
- Optional `parameters`:
|
||||
- `"update_document": true` to replace the existing document with the password-less PDF.
|
||||
- `"delete_original": true` to delete the original document after editing.
|
||||
- `"include_metadata": true` to copy metadata from the original document to the new password-less document.
|
||||
- `merge`
|
||||
- No additional `parameters` required.
|
||||
- The ordering of the merged document is determined by the list of IDs.
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"jest": "30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "^16.0.0",
|
||||
"jest-preset-angular": "^15.0.3",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"ts-node": "~10.9.1",
|
||||
|
||||
338
src-ui/pnpm-lock.yaml
generated
338
src-ui/pnpm-lock.yaml
generated
@@ -160,8 +160,8 @@ importers:
|
||||
specifier: ^16.0.0
|
||||
version: 16.0.0
|
||||
jest-preset-angular:
|
||||
specifier: ^16.0.0
|
||||
version: 16.0.0(e164a15bea80c603eb581d5724a352ec)
|
||||
specifier: ^15.0.3
|
||||
version: 15.0.3(e164a15bea80c603eb581d5724a352ec)
|
||||
jest-websocket-mock:
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
@@ -1282,8 +1282,8 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||
'@esbuild/aix-ppc64@0.27.0':
|
||||
resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
@@ -1300,8 +1300,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||
'@esbuild/android-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
@@ -1318,8 +1318,8 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||
'@esbuild/android-arm@0.27.0':
|
||||
resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
@@ -1336,8 +1336,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||
'@esbuild/android-x64@0.27.0':
|
||||
resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
@@ -1354,8 +1354,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||
'@esbuild/darwin-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@@ -1372,8 +1372,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||
'@esbuild/darwin-x64@0.27.0':
|
||||
resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@@ -1390,8 +1390,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||
'@esbuild/freebsd-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
@@ -1408,8 +1408,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||
'@esbuild/freebsd-x64@0.27.0':
|
||||
resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
@@ -1426,8 +1426,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||
'@esbuild/linux-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@@ -1444,8 +1444,8 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||
'@esbuild/linux-arm@0.27.0':
|
||||
resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
@@ -1462,8 +1462,8 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||
'@esbuild/linux-ia32@0.27.0':
|
||||
resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
@@ -1480,8 +1480,8 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||
'@esbuild/linux-loong64@0.27.0':
|
||||
resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
@@ -1498,8 +1498,8 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||
'@esbuild/linux-mips64el@0.27.0':
|
||||
resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
@@ -1516,8 +1516,8 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||
'@esbuild/linux-ppc64@0.27.0':
|
||||
resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
@@ -1534,8 +1534,8 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||
'@esbuild/linux-riscv64@0.27.0':
|
||||
resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
@@ -1552,8 +1552,8 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||
'@esbuild/linux-s390x@0.27.0':
|
||||
resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
@@ -1570,8 +1570,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||
'@esbuild/linux-x64@0.27.0':
|
||||
resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
@@ -1588,8 +1588,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||
'@esbuild/netbsd-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
@@ -1606,8 +1606,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||
'@esbuild/netbsd-x64@0.27.0':
|
||||
resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
@@ -1624,8 +1624,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||
'@esbuild/openbsd-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
@@ -1642,8 +1642,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||
'@esbuild/openbsd-x64@0.27.0':
|
||||
resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
@@ -1654,8 +1654,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
|
||||
'@esbuild/openharmony-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
@@ -1672,8 +1672,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||
'@esbuild/sunos-x64@0.27.0':
|
||||
resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
@@ -1690,8 +1690,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||
'@esbuild/win32-arm64@0.27.0':
|
||||
resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@@ -1708,8 +1708,8 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||
'@esbuild/win32-ia32@0.27.0':
|
||||
resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
@@ -1726,8 +1726,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||
'@esbuild/win32-x64@0.27.0':
|
||||
resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -2833,8 +2833,8 @@ packages:
|
||||
'@sinclair/typebox@0.27.8':
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
|
||||
'@sinclair/typebox@0.34.46':
|
||||
resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==}
|
||||
'@sinclair/typebox@0.34.41':
|
||||
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
|
||||
@@ -3991,13 +3991,18 @@ packages:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild-wasm@0.25.10:
|
||||
resolution: {integrity: sha512-IyyfrTA2iiOh/uhlaJj0aUDgW42lFhr29ZeKouVNOz/8mLyuqWbEuVst+B4RBH18pb3AcOHnaOgyskAbsVOe3A==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild-wasm@0.25.5:
|
||||
resolution: {integrity: sha512-V/rbdOws2gDcnCAECfPrajhuafI0WY4WumUgc8ZHwOLnvmM0doLQ+dqvVFI2qkVxQsvo6880aC9IjpyDqcwwTw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild-wasm@0.27.2:
|
||||
resolution: {integrity: sha512-eUTnl8eh+v8UZIZh4MrMOKDAc8Lm7+NqP3pyuTORGFY1s/o9WoiJgKnwXy+te2J3hX7iRbFSHEyig7GsPeeJyw==}
|
||||
esbuild-wasm@0.27.0:
|
||||
resolution: {integrity: sha512-4XpLDOY4sOzqgiezPXDkHTn25cBA1MKN5OTotehoFR7/wTpSYCK76OA8rQfpiuz12G27JpGxxdh+/D9GcNl61w==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -4011,8 +4016,8 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.2:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
esbuild@0.27.0:
|
||||
resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -4817,14 +4822,14 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
jest-preset-angular@16.0.0:
|
||||
resolution: {integrity: sha512-FVo98EZiJ9cwHeteJozCCIkgJecytt1tu0t8DrAMTyyQ4x/seeZmctkWXP0J9uGyARS0Kcwd+f2YeKqKQOB2yA==}
|
||||
jest-preset-angular@15.0.3:
|
||||
resolution: {integrity: sha512-W1OpeQ/tq72ZSGXRvUDktL02QBAWIM5SZPK4KI0zoXNvRGiGhNMbxuaFF7anLi27cojmWHm+TewJQI1Dsm9s6A==}
|
||||
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0}
|
||||
peerDependencies:
|
||||
'@angular/compiler-cli': '>=19.0.0 <22.0.0'
|
||||
'@angular/core': '>=19.0.0 <22.0.0'
|
||||
'@angular/platform-browser': '>=19.0.0 <22.0.0'
|
||||
'@angular/platform-browser-dynamic': '>=19.0.0 <22.0.0'
|
||||
'@angular/compiler-cli': '>=18.0.0 <21.0.0'
|
||||
'@angular/core': '>=18.0.0 <21.0.0'
|
||||
'@angular/platform-browser': '>=18.0.0 <21.0.0'
|
||||
'@angular/platform-browser-dynamic': '>=18.0.0 <21.0.0'
|
||||
jest: ^30.0.0
|
||||
jsdom: '>=26.0.0'
|
||||
typescript: '>=5.5'
|
||||
@@ -6386,8 +6391,35 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-jest@29.4.6:
|
||||
resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==}
|
||||
ts-jest@29.4.4:
|
||||
resolution: {integrity: sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0-beta.0 <8'
|
||||
'@jest/transform': ^29.0.0 || ^30.0.0
|
||||
'@jest/types': ^29.0.0 || ^30.0.0
|
||||
babel-jest: ^29.0.0 || ^30.0.0
|
||||
esbuild: '*'
|
||||
jest: ^29.0.0 || ^30.0.0
|
||||
jest-util: ^29.0.0 || ^30.0.0
|
||||
typescript: '>=4.3 <6'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@jest/transform':
|
||||
optional: true
|
||||
'@jest/types':
|
||||
optional: true
|
||||
babel-jest:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jest-util:
|
||||
optional: true
|
||||
|
||||
ts-jest@29.4.5:
|
||||
resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -6762,12 +6794,10 @@ packages:
|
||||
whatwg-encoding@2.0.0:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-mimetype@3.0.0:
|
||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||
@@ -8458,7 +8488,7 @@ snapshots:
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
'@esbuild/aix-ppc64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.5':
|
||||
@@ -8467,7 +8497,7 @@ snapshots:
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
'@esbuild/android-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.5':
|
||||
@@ -8476,7 +8506,7 @@ snapshots:
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
'@esbuild/android-arm@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.5':
|
||||
@@ -8485,7 +8515,7 @@ snapshots:
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
'@esbuild/android-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.5':
|
||||
@@ -8494,7 +8524,7 @@ snapshots:
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
'@esbuild/darwin-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.5':
|
||||
@@ -8503,7 +8533,7 @@ snapshots:
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
'@esbuild/darwin-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.5':
|
||||
@@ -8512,7 +8542,7 @@ snapshots:
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
'@esbuild/freebsd-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.5':
|
||||
@@ -8521,7 +8551,7 @@ snapshots:
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
'@esbuild/freebsd-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.5':
|
||||
@@ -8530,7 +8560,7 @@ snapshots:
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
'@esbuild/linux-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.5':
|
||||
@@ -8539,7 +8569,7 @@ snapshots:
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
'@esbuild/linux-arm@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.5':
|
||||
@@ -8548,7 +8578,7 @@ snapshots:
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
'@esbuild/linux-ia32@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.5':
|
||||
@@ -8557,7 +8587,7 @@ snapshots:
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
'@esbuild/linux-loong64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.5':
|
||||
@@ -8566,7 +8596,7 @@ snapshots:
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
'@esbuild/linux-mips64el@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.5':
|
||||
@@ -8575,7 +8605,7 @@ snapshots:
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
'@esbuild/linux-ppc64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.5':
|
||||
@@ -8584,7 +8614,7 @@ snapshots:
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
'@esbuild/linux-riscv64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.5':
|
||||
@@ -8593,7 +8623,7 @@ snapshots:
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
'@esbuild/linux-s390x@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.5':
|
||||
@@ -8602,7 +8632,7 @@ snapshots:
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
'@esbuild/linux-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.5':
|
||||
@@ -8611,7 +8641,7 @@ snapshots:
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
'@esbuild/netbsd-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.5':
|
||||
@@ -8620,7 +8650,7 @@ snapshots:
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
'@esbuild/netbsd-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.5':
|
||||
@@ -8629,7 +8659,7 @@ snapshots:
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
'@esbuild/openbsd-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.5':
|
||||
@@ -8638,13 +8668,13 @@ snapshots:
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
'@esbuild/openbsd-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
'@esbuild/openharmony-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.5':
|
||||
@@ -8653,7 +8683,7 @@ snapshots:
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
'@esbuild/sunos-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.5':
|
||||
@@ -8662,7 +8692,7 @@ snapshots:
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
'@esbuild/win32-arm64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.5':
|
||||
@@ -8671,7 +8701,7 @@ snapshots:
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
'@esbuild/win32-ia32@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.5':
|
||||
@@ -8680,7 +8710,7 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
'@esbuild/win32-x64@0.27.0':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))':
|
||||
@@ -9069,7 +9099,7 @@ snapshots:
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.34.46
|
||||
'@sinclair/typebox': 0.34.41
|
||||
|
||||
'@jest/snapshot-utils@30.2.0':
|
||||
dependencies:
|
||||
@@ -9423,7 +9453,7 @@ snapshots:
|
||||
|
||||
'@npmcli/fs@4.0.0':
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
'@npmcli/git@6.0.3':
|
||||
dependencies:
|
||||
@@ -9433,7 +9463,7 @@ snapshots:
|
||||
npm-pick-manifest: 10.0.0
|
||||
proc-log: 5.0.0
|
||||
promise-retry: 2.0.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
which: 5.0.0
|
||||
|
||||
'@npmcli/installed-package-contents@3.0.0':
|
||||
@@ -9450,7 +9480,7 @@ snapshots:
|
||||
hosted-git-info: 8.1.0
|
||||
json-parse-even-better-errors: 4.0.0
|
||||
proc-log: 5.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
'@npmcli/promise-spawn@8.0.3':
|
||||
@@ -9768,7 +9798,7 @@ snapshots:
|
||||
|
||||
'@sinclair/typebox@0.27.8': {}
|
||||
|
||||
'@sinclair/typebox@0.34.46': {}
|
||||
'@sinclair/typebox@0.34.41': {}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
dependencies:
|
||||
@@ -10790,7 +10820,7 @@ snapshots:
|
||||
postcss-modules-scope: 3.2.1(postcss@8.5.3)
|
||||
postcss-modules-values: 4.0.0(postcss@8.5.3)
|
||||
postcss-value-parser: 4.2.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
webpack: 5.99.8(esbuild@0.25.5)
|
||||
|
||||
@@ -10998,9 +11028,11 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
esbuild-wasm@0.25.10: {}
|
||||
|
||||
esbuild-wasm@0.25.5: {}
|
||||
|
||||
esbuild-wasm@0.27.2: {}
|
||||
esbuild-wasm@0.27.0: {}
|
||||
|
||||
esbuild@0.25.5:
|
||||
optionalDependencies:
|
||||
@@ -11059,34 +11091,34 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.25.9
|
||||
'@esbuild/win32-x64': 0.25.9
|
||||
|
||||
esbuild@0.27.2:
|
||||
esbuild@0.27.0:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
'@esbuild/android-arm': 0.27.2
|
||||
'@esbuild/android-arm64': 0.27.2
|
||||
'@esbuild/android-x64': 0.27.2
|
||||
'@esbuild/darwin-arm64': 0.27.2
|
||||
'@esbuild/darwin-x64': 0.27.2
|
||||
'@esbuild/freebsd-arm64': 0.27.2
|
||||
'@esbuild/freebsd-x64': 0.27.2
|
||||
'@esbuild/linux-arm': 0.27.2
|
||||
'@esbuild/linux-arm64': 0.27.2
|
||||
'@esbuild/linux-ia32': 0.27.2
|
||||
'@esbuild/linux-loong64': 0.27.2
|
||||
'@esbuild/linux-mips64el': 0.27.2
|
||||
'@esbuild/linux-ppc64': 0.27.2
|
||||
'@esbuild/linux-riscv64': 0.27.2
|
||||
'@esbuild/linux-s390x': 0.27.2
|
||||
'@esbuild/linux-x64': 0.27.2
|
||||
'@esbuild/netbsd-arm64': 0.27.2
|
||||
'@esbuild/netbsd-x64': 0.27.2
|
||||
'@esbuild/openbsd-arm64': 0.27.2
|
||||
'@esbuild/openbsd-x64': 0.27.2
|
||||
'@esbuild/openharmony-arm64': 0.27.2
|
||||
'@esbuild/sunos-x64': 0.27.2
|
||||
'@esbuild/win32-arm64': 0.27.2
|
||||
'@esbuild/win32-ia32': 0.27.2
|
||||
'@esbuild/win32-x64': 0.27.2
|
||||
'@esbuild/aix-ppc64': 0.27.0
|
||||
'@esbuild/android-arm': 0.27.0
|
||||
'@esbuild/android-arm64': 0.27.0
|
||||
'@esbuild/android-x64': 0.27.0
|
||||
'@esbuild/darwin-arm64': 0.27.0
|
||||
'@esbuild/darwin-x64': 0.27.0
|
||||
'@esbuild/freebsd-arm64': 0.27.0
|
||||
'@esbuild/freebsd-x64': 0.27.0
|
||||
'@esbuild/linux-arm': 0.27.0
|
||||
'@esbuild/linux-arm64': 0.27.0
|
||||
'@esbuild/linux-ia32': 0.27.0
|
||||
'@esbuild/linux-loong64': 0.27.0
|
||||
'@esbuild/linux-mips64el': 0.27.0
|
||||
'@esbuild/linux-ppc64': 0.27.0
|
||||
'@esbuild/linux-riscv64': 0.27.0
|
||||
'@esbuild/linux-s390x': 0.27.0
|
||||
'@esbuild/linux-x64': 0.27.0
|
||||
'@esbuild/netbsd-arm64': 0.27.0
|
||||
'@esbuild/netbsd-x64': 0.27.0
|
||||
'@esbuild/openbsd-arm64': 0.27.0
|
||||
'@esbuild/openbsd-x64': 0.27.0
|
||||
'@esbuild/openharmony-arm64': 0.27.0
|
||||
'@esbuild/sunos-x64': 0.27.0
|
||||
'@esbuild/win32-arm64': 0.27.0
|
||||
'@esbuild/win32-ia32': 0.27.0
|
||||
'@esbuild/win32-x64': 0.27.0
|
||||
optional: true
|
||||
|
||||
escalade@3.2.0: {}
|
||||
@@ -11333,10 +11365,6 @@ snapshots:
|
||||
dependencies:
|
||||
bser: 2.1.1
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
@@ -12087,15 +12115,15 @@ snapshots:
|
||||
'@angular/core': 20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||
'@angular/platform-browser-dynamic': 20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.15)(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)))
|
||||
bs-logger: 0.2.6
|
||||
esbuild-wasm: 0.27.2
|
||||
esbuild-wasm: 0.25.10
|
||||
jest: 30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))
|
||||
jest-environment-jsdom: 29.7.0(canvas@3.0.0)
|
||||
jest-util: 29.7.0
|
||||
pretty-format: 29.7.0
|
||||
ts-jest: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
ts-jest: 29.4.4(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
esbuild: 0.27.0
|
||||
jsdom: 26.1.0(canvas@3.0.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -12107,7 +12135,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
jest-preset-angular@16.0.0(e164a15bea80c603eb581d5724a352ec):
|
||||
jest-preset-angular@15.0.3(e164a15bea80c603eb581d5724a352ec):
|
||||
dependencies:
|
||||
'@angular/compiler-cli': 20.3.15(@angular/compiler@20.3.15)(typescript@5.8.3)
|
||||
'@angular/core': 20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)
|
||||
@@ -12115,15 +12143,15 @@ snapshots:
|
||||
'@angular/platform-browser-dynamic': 20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.15)(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)))
|
||||
'@jest/environment-jsdom-abstract': 30.2.0(canvas@3.0.0)(jsdom@26.1.0(canvas@3.0.0))
|
||||
bs-logger: 0.2.6
|
||||
esbuild-wasm: 0.27.2
|
||||
esbuild-wasm: 0.27.0
|
||||
jest: 30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))
|
||||
jest-util: 30.2.0
|
||||
jsdom: 26.1.0(canvas@3.0.0)
|
||||
pretty-format: 30.2.0
|
||||
ts-jest: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
ts-jest: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
esbuild: 0.27.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@jest/transform'
|
||||
@@ -12853,7 +12881,7 @@ snapshots:
|
||||
make-fetch-happen: 14.0.3
|
||||
nopt: 8.1.0
|
||||
proc-log: 5.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
tar: 7.5.2
|
||||
tinyglobby: 0.2.15
|
||||
which: 5.0.0
|
||||
@@ -12878,7 +12906,7 @@ snapshots:
|
||||
|
||||
npm-install-checks@7.1.2:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
npm-normalize-package-bin@4.0.0: {}
|
||||
|
||||
@@ -12886,7 +12914,7 @@ snapshots:
|
||||
dependencies:
|
||||
hosted-git-info: 8.1.0
|
||||
proc-log: 5.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
validate-npm-package-name: 6.0.2
|
||||
|
||||
npm-package-arg@13.0.0:
|
||||
@@ -12906,7 +12934,7 @@ snapshots:
|
||||
npm-install-checks: 7.1.2
|
||||
npm-normalize-package-bin: 4.0.0
|
||||
npm-package-arg: 12.0.2
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
npm-registry-fetch@18.0.2:
|
||||
dependencies:
|
||||
@@ -13146,7 +13174,7 @@ snapshots:
|
||||
cosmiconfig: 9.0.0(typescript@5.8.3)
|
||||
jiti: 1.21.7
|
||||
postcss: 8.5.3
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
webpack: 5.99.8(esbuild@0.25.5)
|
||||
transitivePeerDependencies:
|
||||
@@ -13943,8 +13971,8 @@ snapshots:
|
||||
|
||||
tinyglobby@0.2.13:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tinyglobby@0.2.14:
|
||||
dependencies:
|
||||
@@ -13999,7 +14027,7 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
ts-jest@29.4.4(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
@@ -14017,10 +14045,10 @@ snapshots:
|
||||
'@jest/transform': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
babel-jest: 30.2.0(@babel/core@7.28.5)
|
||||
esbuild: 0.27.2
|
||||
esbuild: 0.27.0
|
||||
jest-util: 29.7.0
|
||||
|
||||
ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
@@ -14038,7 +14066,7 @@ snapshots:
|
||||
'@jest/transform': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
babel-jest: 30.2.0(@babel/core@7.28.5)
|
||||
esbuild: 0.27.2
|
||||
esbuild: 0.27.0
|
||||
jest-util: 30.2.0
|
||||
|
||||
ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3):
|
||||
@@ -14216,9 +14244,9 @@ snapshots:
|
||||
|
||||
vite@6.3.5(@types/node@24.10.1)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(yaml@2.7.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
fdir: 6.5.0(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
esbuild: 0.25.9
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.52.3
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (message) {
|
||||
<p class="mb-3" [innerHTML]="message"></p>
|
||||
}
|
||||
<div class="btn-group mb-3" role="group">
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="passwordRemoveMode"
|
||||
id="removeReplace"
|
||||
[(ngModel)]="updateDocument"
|
||||
[value]="true"
|
||||
(ngModelChange)="onUpdateDocumentChange($event)"
|
||||
/>
|
||||
<label class="btn btn-outline-primary btn-sm" for="removeReplace">
|
||||
<i-bs name="pencil"></i-bs>
|
||||
<span class="ms-2" i18n>Replace current document</span>
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="passwordRemoveMode"
|
||||
id="removeCreate"
|
||||
[(ngModel)]="updateDocument"
|
||||
[value]="false"
|
||||
(ngModelChange)="onUpdateDocumentChange($event)"
|
||||
/>
|
||||
<label class="btn btn-outline-primary btn-sm" for="removeCreate">
|
||||
<i-bs name="plus"></i-bs>
|
||||
<span class="ms-2" i18n>Create new document</span>
|
||||
</label>
|
||||
</div>
|
||||
@if (!updateDocument) {
|
||||
<div class="d-flex flex-column flex-md-row w-100 gap-3 align-items-center">
|
||||
<div class="form-group d-flex">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="copyMetaRemove" [(ngModel)]="includeMetadata" />
|
||||
<label class="form-check-label" for="copyMetaRemove" i18n> Copy metadata
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check ms-3">
|
||||
<input class="form-check-input" type="checkbox" id="deleteOriginalRemove" [(ngModel)]="deleteOriginal" />
|
||||
<label class="form-check-label" for="deleteOriginalRemove" i18n> Delete original</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer flex-nowrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
[class]="cancelBtnClass"
|
||||
(click)="cancel()"
|
||||
[disabled]="!buttonsEnabled"
|
||||
>
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">
|
||||
{{cancelBtnCaption}}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
[class]="btnClass"
|
||||
(click)="confirm()"
|
||||
[disabled]="!confirmButtonEnabled || !buttonsEnabled"
|
||||
>
|
||||
{{btnCaption}}
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component'
|
||||
|
||||
describe('PasswordRemovalConfirmDialogComponent', () => {
|
||||
let component: PasswordRemovalConfirmDialogComponent
|
||||
let fixture: ComponentFixture<PasswordRemovalConfirmDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
PasswordRemovalConfirmDialogComponent,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should default to replacing the document', () => {
|
||||
expect(component.updateDocument).toBe(true)
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('#removeReplace')).nativeElement.checked
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow creating a new document with metadata and delete toggle', () => {
|
||||
component.onUpdateDocumentChange(false)
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(component.updateDocument).toBe(false)
|
||||
expect(fixture.debugElement.query(By.css('#copyMetaRemove'))).not.toBeNull()
|
||||
|
||||
component.includeMetadata = false
|
||||
component.deleteOriginal = true
|
||||
component.onUpdateDocumentChange(true)
|
||||
expect(component.updateDocument).toBe(true)
|
||||
expect(component.includeMetadata).toBe(true)
|
||||
expect(component.deleteOriginal).toBe(false)
|
||||
})
|
||||
|
||||
it('should emit confirm when confirmed', () => {
|
||||
let confirmed = false
|
||||
component.confirmClicked.subscribe(() => (confirmed = true))
|
||||
component.confirm()
|
||||
expect(confirmed).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-password-removal-confirm-dialog',
|
||||
templateUrl: './password-removal-confirm-dialog.component.html',
|
||||
styleUrls: ['./password-removal-confirm-dialog.component.scss'],
|
||||
imports: [FormsModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
updateDocument: boolean = true
|
||||
includeMetadata: boolean = true
|
||||
deleteOriginal: boolean = false
|
||||
|
||||
@Input()
|
||||
override title = $localize`Remove password protection`
|
||||
|
||||
@Input()
|
||||
override message =
|
||||
$localize`Create an unprotected copy or replace the existing file.`
|
||||
|
||||
@Input()
|
||||
override btnCaption = $localize`Start`
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
onUpdateDocumentChange(updateDocument: boolean) {
|
||||
this.updateDocument = updateDocument
|
||||
if (this.updateDocument) {
|
||||
this.deleteOriginal = false
|
||||
this.includeMetadata = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,6 +430,24 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case (WorkflowActionType.PasswordRemoval) {
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="small" i18n>
|
||||
One or more passwords separated by commas or new lines. The workflow will try them in order until one succeeds.
|
||||
</p>
|
||||
<pngx-input-textarea
|
||||
i18n-title
|
||||
title="Passwords"
|
||||
formControlName="passwords"
|
||||
rows="4"
|
||||
[error]="error?.actions?.[i]?.passwords"
|
||||
hint="Passwords are stored in plain text. Use with caution."
|
||||
i18n-hint
|
||||
></pngx-input-textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -139,6 +139,10 @@ export const WORKFLOW_ACTION_OPTIONS = [
|
||||
id: WorkflowActionType.Webhook,
|
||||
name: $localize`Webhook`,
|
||||
},
|
||||
{
|
||||
id: WorkflowActionType.PasswordRemoval,
|
||||
name: $localize`Password removal`,
|
||||
},
|
||||
]
|
||||
|
||||
export enum TriggerFilterType {
|
||||
@@ -1133,6 +1137,7 @@ export class WorkflowEditDialogComponent
|
||||
headers: new FormControl(action.webhook?.headers),
|
||||
include_document: new FormControl(!!action.webhook?.include_document),
|
||||
}),
|
||||
passwords: new FormControl(action.passwords),
|
||||
}),
|
||||
{ emitEvent }
|
||||
)
|
||||
|
||||
@@ -65,6 +65,12 @@
|
||||
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
||||
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
||||
</button>
|
||||
|
||||
@if (userIsOwner && (requiresPassword || password)) {
|
||||
<button ngbDropdownItem (click)="removePassword()" [disabled]="!password">
|
||||
<i-bs name="unlock"></i-bs> <ng-container i18n>Remove Password</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import {
|
||||
DocumentDetailComponent,
|
||||
@@ -1209,6 +1210,88 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support removing password protection from pdfs', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
initNormally()
|
||||
component.password = 'secret'
|
||||
component.removePassword()
|
||||
const dialog =
|
||||
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||
dialog.updateDocument = false
|
||||
dialog.includeMetadata = false
|
||||
dialog.deleteOriginal = true
|
||||
dialog.confirm()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [doc.id],
|
||||
method: 'remove_password',
|
||||
parameters: {
|
||||
password: 'secret',
|
||||
update_document: false,
|
||||
include_metadata: false,
|
||||
delete_original: true,
|
||||
},
|
||||
})
|
||||
req.flush(true)
|
||||
})
|
||||
|
||||
it('should require the current password before removing it', () => {
|
||||
initNormally()
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
component.requiresPassword = true
|
||||
component.password = ''
|
||||
|
||||
component.removePassword()
|
||||
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle failures when removing password protection', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
initNormally()
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
component.password = 'secret'
|
||||
|
||||
component.removePassword()
|
||||
const dialog =
|
||||
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||
dialog.confirm()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.error(new ErrorEvent('failed'))
|
||||
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
expect(component.networkActive).toBe(false)
|
||||
expect(dialog.buttonsEnabled).toBe(true)
|
||||
})
|
||||
|
||||
it('should refresh the document when removing password in update mode', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
const refreshSpy = jest.spyOn(openDocumentsService, 'refreshDocument')
|
||||
initNormally()
|
||||
component.password = 'secret'
|
||||
|
||||
component.removePassword()
|
||||
const dialog =
|
||||
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||
dialog.confirm()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledWith(doc.id)
|
||||
})
|
||||
|
||||
it('should support keyboard shortcuts', () => {
|
||||
initNormally()
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ import { getFilenameFromContentDisposition } from 'src/app/utils/http'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import * as UTIF from 'utif'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
@@ -175,6 +176,7 @@ export enum ZoomSetting {
|
||||
NgxBootstrapIconsModule,
|
||||
PdfViewerModule,
|
||||
TextAreaComponent,
|
||||
PasswordRemovalConfirmDialogComponent,
|
||||
],
|
||||
})
|
||||
export class DocumentDetailComponent
|
||||
@@ -1428,6 +1430,63 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
removePassword() {
|
||||
if (this.requiresPassword || !this.password) {
|
||||
this.toastService.showError(
|
||||
$localize`Please enter the current password before attempting to remove it.`
|
||||
)
|
||||
return
|
||||
}
|
||||
const modal = this.modalService.open(
|
||||
PasswordRemovalConfirmDialogComponent,
|
||||
{
|
||||
backdrop: 'static',
|
||||
}
|
||||
)
|
||||
modal.componentInstance.title = $localize`Remove password protection`
|
||||
modal.componentInstance.message = $localize`Create an unprotected copy or replace the existing file.`
|
||||
modal.componentInstance.btnCaption = $localize`Start`
|
||||
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
const dialog =
|
||||
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||
dialog.buttonsEnabled = false
|
||||
this.networkActive = true
|
||||
this.documentsService
|
||||
.bulkEdit([this.document.id], 'remove_password', {
|
||||
password: this.password,
|
||||
update_document: dialog.updateDocument,
|
||||
include_metadata: dialog.includeMetadata,
|
||||
delete_original: dialog.deleteOriginal,
|
||||
})
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Password removal operation for "${this.document.title}" will begin in the background.`
|
||||
)
|
||||
this.networkActive = false
|
||||
modal.close()
|
||||
if (!dialog.updateDocument && dialog.deleteOriginal) {
|
||||
this.openDocumentService.closeDocument(this.document)
|
||||
} else if (dialog.updateDocument) {
|
||||
this.openDocumentService.refreshDocument(this.documentId)
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
dialog.buttonsEnabled = true
|
||||
this.networkActive = false
|
||||
this.toastService.showError(
|
||||
$localize`Error executing password removal operation`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
printDocument() {
|
||||
const printUrl = this.documentsService.getDownloadUrl(
|
||||
this.document.id,
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum WorkflowActionType {
|
||||
Removal = 2,
|
||||
Email = 3,
|
||||
Webhook = 4,
|
||||
PasswordRemoval = 5,
|
||||
}
|
||||
|
||||
export interface WorkflowActionEmail extends ObjectWithId {
|
||||
@@ -97,4 +98,6 @@ export interface WorkflowAction extends ObjectWithId {
|
||||
email?: WorkflowActionEmail
|
||||
|
||||
webhook?: WorkflowActionWebhook
|
||||
|
||||
passwords?: string
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ import {
|
||||
threeDotsVertical,
|
||||
trash,
|
||||
uiRadios,
|
||||
unlock,
|
||||
upcScan,
|
||||
windowStack,
|
||||
x,
|
||||
@@ -348,6 +349,7 @@ const icons = {
|
||||
threeDotsVertical,
|
||||
trash,
|
||||
uiRadios,
|
||||
unlock,
|
||||
upcScan,
|
||||
windowStack,
|
||||
x,
|
||||
|
||||
@@ -646,6 +646,77 @@ def edit_pdf(
|
||||
return "OK"
|
||||
|
||||
|
||||
def remove_password(
|
||||
doc_ids: list[int],
|
||||
password: str,
|
||||
*,
|
||||
update_document: bool = False,
|
||||
delete_original: bool = False,
|
||||
include_metadata: bool = True,
|
||||
user: User | None = None,
|
||||
) -> Literal["OK"]:
|
||||
"""
|
||||
Remove password protection from PDF documents.
|
||||
"""
|
||||
import pikepdf
|
||||
|
||||
for doc_id in doc_ids:
|
||||
doc = Document.objects.get(id=doc_id)
|
||||
try:
|
||||
logger.info(
|
||||
f"Attempting password removal from document {doc_ids[0]}",
|
||||
)
|
||||
with pikepdf.open(doc.source_path, password=password) as pdf:
|
||||
temp_path = doc.source_path.with_suffix(".tmp.pdf")
|
||||
pdf.remove_unreferenced_resources()
|
||||
pdf.save(temp_path)
|
||||
|
||||
if update_document:
|
||||
# replace the original document with the unprotected one
|
||||
temp_path.replace(doc.source_path)
|
||||
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
||||
doc.page_count = len(pdf.pages)
|
||||
doc.save()
|
||||
update_document_content_maybe_archive_file.delay(document_id=doc.id)
|
||||
else:
|
||||
consume_tasks = []
|
||||
overrides = (
|
||||
DocumentMetadataOverrides().from_document(doc)
|
||||
if include_metadata
|
||||
else DocumentMetadataOverrides()
|
||||
)
|
||||
if user is not None:
|
||||
overrides.owner_id = user.id
|
||||
|
||||
filepath: Path = (
|
||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||
/ f"{doc.id}_unprotected.pdf"
|
||||
)
|
||||
temp_path.replace(filepath)
|
||||
consume_tasks.append(
|
||||
consume_file.s(
|
||||
ConsumableDocument(
|
||||
source=DocumentSource.ConsumeFolder,
|
||||
original_file=filepath,
|
||||
),
|
||||
overrides,
|
||||
),
|
||||
)
|
||||
|
||||
if delete_original:
|
||||
chord(header=consume_tasks, body=delete.si([doc.id])).delay()
|
||||
else:
|
||||
group(consume_tasks).delay()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error removing password from document {doc.id}: {e}")
|
||||
raise ValueError(
|
||||
f"An error occurred while removing the password: {e}",
|
||||
) from e
|
||||
|
||||
return "OK"
|
||||
|
||||
|
||||
def reflect_doclinks(
|
||||
document: Document,
|
||||
field: CustomField,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.7 on 2025-12-29 03:56
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="workflowaction",
|
||||
name="passwords",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="Passwords to try when removing PDF protection. Separate with commas or new lines.",
|
||||
null=True,
|
||||
verbose_name="passwords",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="workflowaction",
|
||||
name="type",
|
||||
field=models.PositiveIntegerField(
|
||||
choices=[
|
||||
(1, "Assignment"),
|
||||
(2, "Removal"),
|
||||
(3, "Email"),
|
||||
(4, "Webhook"),
|
||||
(5, "Password removal"),
|
||||
],
|
||||
default=1,
|
||||
verbose_name="Workflow Action Type",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1287,6 +1287,10 @@ class WorkflowAction(models.Model):
|
||||
4,
|
||||
_("Webhook"),
|
||||
)
|
||||
PASSWORD_REMOVAL = (
|
||||
5,
|
||||
_("Password removal"),
|
||||
)
|
||||
|
||||
type = models.PositiveIntegerField(
|
||||
_("Workflow Action Type"),
|
||||
@@ -1514,6 +1518,15 @@ class WorkflowAction(models.Model):
|
||||
verbose_name=_("webhook"),
|
||||
)
|
||||
|
||||
passwords = models.TextField(
|
||||
_("passwords"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Passwords to try when removing PDF protection. Separate with commas or new lines.",
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("workflow action")
|
||||
verbose_name_plural = _("workflow actions")
|
||||
|
||||
@@ -18,8 +18,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.core.validators import DecimalValidator
|
||||
from django.core.validators import EmailValidator
|
||||
from django.core.validators import MaxLengthValidator
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.validators import RegexValidator
|
||||
from django.core.validators import integer_validator
|
||||
from django.db.models import Count
|
||||
@@ -877,13 +875,6 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
||||
uri_validator(data["value"])
|
||||
elif field.data_type == CustomField.FieldDataType.INT:
|
||||
integer_validator(data["value"])
|
||||
try:
|
||||
value_int = int(data["value"])
|
||||
except (TypeError, ValueError):
|
||||
raise serializers.ValidationError("Enter a valid integer.")
|
||||
# Keep values within the PostgreSQL integer range
|
||||
MinValueValidator(-2147483648)(value_int)
|
||||
MaxValueValidator(2147483647)(value_int)
|
||||
elif (
|
||||
field.data_type == CustomField.FieldDataType.MONETARY
|
||||
and data["value"] != ""
|
||||
@@ -1430,6 +1421,7 @@ class BulkEditSerializer(
|
||||
"split",
|
||||
"delete_pages",
|
||||
"edit_pdf",
|
||||
"remove_password",
|
||||
],
|
||||
label="Method",
|
||||
write_only=True,
|
||||
@@ -1505,6 +1497,8 @@ class BulkEditSerializer(
|
||||
return bulk_edit.delete_pages
|
||||
elif method == "edit_pdf":
|
||||
return bulk_edit.edit_pdf
|
||||
elif method == "remove_password":
|
||||
return bulk_edit.remove_password
|
||||
else: # pragma: no cover
|
||||
# This will never happen as it is handled by the ChoiceField
|
||||
raise serializers.ValidationError("Unsupported method.")
|
||||
@@ -1701,6 +1695,12 @@ class BulkEditSerializer(
|
||||
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
|
||||
)
|
||||
|
||||
def validate_parameters_remove_password(self, parameters):
|
||||
if "password" not in parameters:
|
||||
raise serializers.ValidationError("password not specified")
|
||||
if not isinstance(parameters["password"], str):
|
||||
raise serializers.ValidationError("password must be a string")
|
||||
|
||||
def validate(self, attrs):
|
||||
method = attrs["method"]
|
||||
parameters = attrs["parameters"]
|
||||
@@ -1741,6 +1741,8 @@ class BulkEditSerializer(
|
||||
"Edit PDF method only supports one document",
|
||||
)
|
||||
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
|
||||
elif method == bulk_edit.remove_password:
|
||||
self.validate_parameters_remove_password(parameters)
|
||||
|
||||
return attrs
|
||||
|
||||
@@ -2438,6 +2440,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
"remove_change_groups",
|
||||
"email",
|
||||
"webhook",
|
||||
"passwords",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
@@ -2494,6 +2497,20 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
"Webhook data is required for webhook actions",
|
||||
)
|
||||
|
||||
if (
|
||||
"type" in attrs
|
||||
and attrs["type"] == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL
|
||||
):
|
||||
passwords = attrs.get("passwords")
|
||||
if passwords is None or not isinstance(passwords, str):
|
||||
raise serializers.ValidationError(
|
||||
"Passwords are required for password removal actions",
|
||||
)
|
||||
if not passwords.strip():
|
||||
raise serializers.ValidationError(
|
||||
"Passwords are required for password removal actions",
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from documents.permissions import get_objects_for_user_owner_aware
|
||||
from documents.templating.utils import convert_format_str_to_template_format
|
||||
from documents.workflows.actions import build_workflow_action_context
|
||||
from documents.workflows.actions import execute_email_action
|
||||
from documents.workflows.actions import execute_password_removal_action
|
||||
from documents.workflows.actions import execute_webhook_action
|
||||
from documents.workflows.mutations import apply_assignment_to_document
|
||||
from documents.workflows.mutations import apply_assignment_to_overrides
|
||||
@@ -792,6 +793,8 @@ def run_workflows(
|
||||
logging_group,
|
||||
original_file,
|
||||
)
|
||||
elif action.type == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL:
|
||||
execute_password_removal_action(action, document, logging_group)
|
||||
|
||||
if not use_overrides:
|
||||
# limit title to 128 characters
|
||||
|
||||
@@ -1582,6 +1582,58 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn(b"out of bounds", response.content)
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.remove_password")
|
||||
def test_remove_password(self, m):
|
||||
self.setup_mock(m, "remove_password")
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "remove_password",
|
||||
"parameters": {"password": "secret", "update_document": True},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertCountEqual(args[0], [self.doc2.id])
|
||||
self.assertEqual(kwargs["password"], "secret")
|
||||
self.assertTrue(kwargs["update_document"])
|
||||
self.assertEqual(kwargs["user"], self.user)
|
||||
|
||||
def test_remove_password_invalid_params(self):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "remove_password",
|
||||
"parameters": {},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn(b"password not specified", response.content)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "remove_password",
|
||||
"parameters": {"password": 123},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn(b"password must be a string", response.content)
|
||||
|
||||
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
||||
"""
|
||||
|
||||
@@ -1664,44 +1664,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
|
||||
self.consume_file_mock.assert_not_called()
|
||||
|
||||
def test_patch_document_integer_custom_field_out_of_range(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- An integer custom field
|
||||
- A document
|
||||
WHEN:
|
||||
- Patching the document with an integer value exceeding PostgreSQL's range
|
||||
THEN:
|
||||
- HTTP 400 is returned (validation catches the overflow)
|
||||
- No custom field instance is created
|
||||
"""
|
||||
cf_int = CustomField.objects.create(
|
||||
name="intfield",
|
||||
data_type=CustomField.FieldDataType.INT,
|
||||
)
|
||||
doc = Document.objects.create(
|
||||
title="Doc",
|
||||
checksum="123",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
response = self.client.patch(
|
||||
f"/api/documents/{doc.pk}/",
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"field": cf_int.pk,
|
||||
"value": 2**31, # overflow for PostgreSQL integer fields
|
||||
},
|
||||
],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("custom_fields", response.data)
|
||||
self.assertEqual(CustomFieldInstance.objects.count(), 0)
|
||||
|
||||
def test_upload_with_webui_source(self):
|
||||
"""
|
||||
GIVEN: A document with a source file
|
||||
|
||||
@@ -808,3 +808,57 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.action.refresh_from_db()
|
||||
self.assertEqual(self.action.assign_title, "Patched Title")
|
||||
|
||||
def test_password_action_passwords_field(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Nothing
|
||||
WHEN:
|
||||
- A workflow password removal action is created with passwords set
|
||||
THEN:
|
||||
- The passwords field is correctly stored and retrieved
|
||||
"""
|
||||
passwords = "password1,password2\npassword3"
|
||||
response = self.client.post(
|
||||
"/api/workflow_actions/",
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
"passwords": passwords,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data["passwords"], passwords)
|
||||
|
||||
def test_password_action_no_passwords_field(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Nothing
|
||||
WHEN:
|
||||
- A workflow password removal action is created with no passwords set
|
||||
- A workflow password removal action is created with passwords set to empty string
|
||||
THEN:
|
||||
- The required validation error is raised
|
||||
"""
|
||||
response = self.client.post(
|
||||
"/api/workflow_actions/",
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn(
|
||||
"Passwords are required",
|
||||
str(response.data["non_field_errors"][0]),
|
||||
)
|
||||
response = self.client.post(
|
||||
"/api/workflow_actions/",
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
"passwords": "",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn(
|
||||
"Passwords are required",
|
||||
str(response.data["non_field_errors"][0]),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import shutil
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
@@ -1066,3 +1067,147 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
bulk_edit.edit_pdf(doc_ids, operations, update_document=True)
|
||||
mock_group.assert_not_called()
|
||||
mock_consume_file.assert_not_called()
|
||||
|
||||
@mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file.delay")
|
||||
@mock.patch("pikepdf.open")
|
||||
def test_remove_password_update_document(self, mock_open, mock_update_document):
|
||||
doc = self.doc1
|
||||
original_checksum = doc.checksum
|
||||
|
||||
fake_pdf = mock.MagicMock()
|
||||
fake_pdf.pages = [mock.Mock(), mock.Mock(), mock.Mock()]
|
||||
|
||||
def save_side_effect(target_path):
|
||||
Path(target_path).write_bytes(b"new pdf content")
|
||||
|
||||
fake_pdf.save.side_effect = save_side_effect
|
||||
mock_open.return_value.__enter__.return_value = fake_pdf
|
||||
|
||||
result = bulk_edit.remove_password(
|
||||
[doc.id],
|
||||
password="secret",
|
||||
update_document=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result, "OK")
|
||||
mock_open.assert_called_once_with(doc.source_path, password="secret")
|
||||
fake_pdf.remove_unreferenced_resources.assert_called_once()
|
||||
doc.refresh_from_db()
|
||||
self.assertNotEqual(doc.checksum, original_checksum)
|
||||
expected_checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
||||
self.assertEqual(doc.checksum, expected_checksum)
|
||||
self.assertEqual(doc.page_count, len(fake_pdf.pages))
|
||||
mock_update_document.assert_called_once_with(document_id=doc.id)
|
||||
|
||||
@mock.patch("documents.bulk_edit.chord")
|
||||
@mock.patch("documents.bulk_edit.group")
|
||||
@mock.patch("documents.tasks.consume_file.s")
|
||||
@mock.patch("documents.bulk_edit.tempfile.mkdtemp")
|
||||
@mock.patch("pikepdf.open")
|
||||
def test_remove_password_creates_consumable_document(
|
||||
self,
|
||||
mock_open,
|
||||
mock_mkdtemp,
|
||||
mock_consume_file,
|
||||
mock_group,
|
||||
mock_chord,
|
||||
):
|
||||
doc = self.doc2
|
||||
temp_dir = self.dirs.scratch_dir / "remove-password"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
mock_mkdtemp.return_value = str(temp_dir)
|
||||
|
||||
fake_pdf = mock.MagicMock()
|
||||
fake_pdf.pages = [mock.Mock(), mock.Mock()]
|
||||
|
||||
def save_side_effect(target_path):
|
||||
Path(target_path).write_bytes(b"password removed")
|
||||
|
||||
fake_pdf.save.side_effect = save_side_effect
|
||||
mock_open.return_value.__enter__.return_value = fake_pdf
|
||||
mock_group.return_value.delay.return_value = None
|
||||
|
||||
user = User.objects.create(username="owner")
|
||||
|
||||
result = bulk_edit.remove_password(
|
||||
[doc.id],
|
||||
password="secret",
|
||||
include_metadata=False,
|
||||
update_document=False,
|
||||
delete_original=False,
|
||||
user=user,
|
||||
)
|
||||
|
||||
self.assertEqual(result, "OK")
|
||||
mock_open.assert_called_once_with(doc.source_path, password="secret")
|
||||
mock_consume_file.assert_called_once()
|
||||
consume_args, _ = mock_consume_file.call_args
|
||||
consumable_document = consume_args[0]
|
||||
overrides = consume_args[1]
|
||||
expected_path = temp_dir / f"{doc.id}_unprotected.pdf"
|
||||
self.assertTrue(expected_path.exists())
|
||||
self.assertEqual(
|
||||
Path(consumable_document.original_file).resolve(),
|
||||
expected_path.resolve(),
|
||||
)
|
||||
self.assertEqual(overrides.owner_id, user.id)
|
||||
mock_group.assert_called_once_with([mock_consume_file.return_value])
|
||||
mock_group.return_value.delay.assert_called_once()
|
||||
mock_chord.assert_not_called()
|
||||
|
||||
@mock.patch("documents.bulk_edit.delete")
|
||||
@mock.patch("documents.bulk_edit.chord")
|
||||
@mock.patch("documents.bulk_edit.group")
|
||||
@mock.patch("documents.tasks.consume_file.s")
|
||||
@mock.patch("documents.bulk_edit.tempfile.mkdtemp")
|
||||
@mock.patch("pikepdf.open")
|
||||
def test_remove_password_deletes_original(
|
||||
self,
|
||||
mock_open,
|
||||
mock_mkdtemp,
|
||||
mock_consume_file,
|
||||
mock_group,
|
||||
mock_chord,
|
||||
mock_delete,
|
||||
):
|
||||
doc = self.doc2
|
||||
temp_dir = self.dirs.scratch_dir / "remove-password-delete"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
mock_mkdtemp.return_value = str(temp_dir)
|
||||
|
||||
fake_pdf = mock.MagicMock()
|
||||
fake_pdf.pages = [mock.Mock(), mock.Mock()]
|
||||
|
||||
def save_side_effect(target_path):
|
||||
Path(target_path).write_bytes(b"password removed")
|
||||
|
||||
fake_pdf.save.side_effect = save_side_effect
|
||||
mock_open.return_value.__enter__.return_value = fake_pdf
|
||||
mock_chord.return_value.delay.return_value = None
|
||||
|
||||
result = bulk_edit.remove_password(
|
||||
[doc.id],
|
||||
password="secret",
|
||||
include_metadata=False,
|
||||
update_document=False,
|
||||
delete_original=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result, "OK")
|
||||
mock_open.assert_called_once_with(doc.source_path, password="secret")
|
||||
mock_consume_file.assert_called_once()
|
||||
mock_group.assert_not_called()
|
||||
mock_chord.assert_called_once()
|
||||
mock_chord.return_value.delay.assert_called_once()
|
||||
mock_delete.si.assert_called_once_with([doc.id])
|
||||
|
||||
@mock.patch("pikepdf.open")
|
||||
def test_remove_password_open_failure(self, mock_open):
|
||||
mock_open.side_effect = RuntimeError("wrong password")
|
||||
|
||||
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
|
||||
with self.assertRaises(ValueError) as exc:
|
||||
bulk_edit.remove_password([self.doc1.id], password="secret")
|
||||
|
||||
self.assertIn("wrong password", str(exc.exception))
|
||||
self.assertIn("Error removing password from document", cm.output[0])
|
||||
|
||||
@@ -2,6 +2,7 @@ import datetime
|
||||
import json
|
||||
import shutil
|
||||
import socket
|
||||
import tempfile
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -60,6 +61,7 @@ from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import DummyProgressManager
|
||||
from documents.tests.utils import FileSystemAssertsMixin
|
||||
from documents.tests.utils import SampleDirMixin
|
||||
from documents.workflows.actions import execute_password_removal_action
|
||||
from paperless_mail.models import MailAccount
|
||||
from paperless_mail.models import MailRule
|
||||
|
||||
@@ -3548,6 +3550,196 @@ class TestWorkflows(
|
||||
|
||||
mock_post.assert_called_once()
|
||||
|
||||
@mock.patch("documents.bulk_edit.remove_password")
|
||||
def test_password_removal_action_attempts_multiple_passwords(
|
||||
self,
|
||||
mock_remove_password,
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow password removal action
|
||||
- Multiple passwords provided
|
||||
WHEN:
|
||||
- Document updated triggering the workflow
|
||||
THEN:
|
||||
- Password removal is attempted until one succeeds
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="Protected",
|
||||
checksum="pw-checksum",
|
||||
)
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
passwords="wrong, right\n extra ",
|
||||
)
|
||||
workflow = Workflow.objects.create(name="Password workflow")
|
||||
workflow.triggers.add(trigger)
|
||||
workflow.actions.add(action)
|
||||
|
||||
mock_remove_password.side_effect = [
|
||||
ValueError("wrong password"),
|
||||
"OK",
|
||||
]
|
||||
|
||||
run_workflows(trigger.type, doc)
|
||||
|
||||
assert mock_remove_password.call_count == 2
|
||||
mock_remove_password.assert_has_calls(
|
||||
[
|
||||
mock.call(
|
||||
[doc.id],
|
||||
password="wrong",
|
||||
update_document=True,
|
||||
user=doc.owner,
|
||||
),
|
||||
mock.call(
|
||||
[doc.id],
|
||||
password="right",
|
||||
update_document=True,
|
||||
user=doc.owner,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@mock.patch("documents.bulk_edit.remove_password")
|
||||
def test_password_removal_action_fails_without_correct_password(
|
||||
self,
|
||||
mock_remove_password,
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow password removal action
|
||||
- No correct password provided
|
||||
WHEN:
|
||||
- Document updated triggering the workflow
|
||||
THEN:
|
||||
- Password removal is attempted for all passwords and fails
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="Protected",
|
||||
checksum="pw-checksum-2",
|
||||
)
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
passwords=" \n , ",
|
||||
)
|
||||
workflow = Workflow.objects.create(name="Password workflow missing passwords")
|
||||
workflow.triggers.add(trigger)
|
||||
workflow.actions.add(action)
|
||||
|
||||
run_workflows(trigger.type, doc)
|
||||
|
||||
mock_remove_password.assert_not_called()
|
||||
|
||||
@mock.patch("documents.bulk_edit.remove_password")
|
||||
def test_password_removal_action_skips_without_passwords(
|
||||
self,
|
||||
mock_remove_password,
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow password removal action with no passwords
|
||||
WHEN:
|
||||
- Workflow is run
|
||||
THEN:
|
||||
- Password removal is not attempted
|
||||
"""
|
||||
doc = Document.objects.create(
|
||||
title="Protected",
|
||||
checksum="pw-checksum-2",
|
||||
)
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
passwords="",
|
||||
)
|
||||
workflow = Workflow.objects.create(name="Password workflow missing passwords")
|
||||
workflow.triggers.add(trigger)
|
||||
workflow.actions.add(action)
|
||||
|
||||
run_workflows(trigger.type, doc)
|
||||
|
||||
mock_remove_password.assert_not_called()
|
||||
|
||||
@mock.patch("documents.bulk_edit.remove_password")
|
||||
def test_password_removal_consumable_document_deferred(
|
||||
self,
|
||||
mock_remove_password,
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow password removal action
|
||||
- Simulated consumption trigger (a ConsumableDocument is used)
|
||||
WHEN:
|
||||
- Document consumption is finished
|
||||
THEN:
|
||||
- Password removal is attempted
|
||||
"""
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||
passwords="first, second",
|
||||
)
|
||||
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
original_file = temp_dir / "file.pdf"
|
||||
original_file.write_bytes(b"pdf content")
|
||||
consumable = ConsumableDocument(
|
||||
source=DocumentSource.ApiUpload,
|
||||
original_file=original_file,
|
||||
)
|
||||
|
||||
execute_password_removal_action(action, consumable, logging_group=None)
|
||||
|
||||
mock_remove_password.assert_not_called()
|
||||
|
||||
mock_remove_password.side_effect = [
|
||||
ValueError("bad password"),
|
||||
"OK",
|
||||
]
|
||||
|
||||
doc = Document.objects.create(
|
||||
checksum="pw-checksum-consumed",
|
||||
title="Protected",
|
||||
)
|
||||
|
||||
document_consumption_finished.send(
|
||||
sender=self.__class__,
|
||||
document=doc,
|
||||
)
|
||||
|
||||
assert mock_remove_password.call_count == 2
|
||||
mock_remove_password.assert_has_calls(
|
||||
[
|
||||
mock.call(
|
||||
[doc.id],
|
||||
password="first",
|
||||
update_document=True,
|
||||
user=doc.owner,
|
||||
),
|
||||
mock.call(
|
||||
[doc.id],
|
||||
password="second",
|
||||
update_document=True,
|
||||
user=doc.owner,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# ensure handler disconnected after first run
|
||||
document_consumption_finished.send(
|
||||
sender=self.__class__,
|
||||
document=doc,
|
||||
)
|
||||
assert mock_remove_password.call_count == 2
|
||||
|
||||
|
||||
class TestWebhookSend:
|
||||
def test_send_webhook_data_or_json(
|
||||
|
||||
@@ -1504,6 +1504,7 @@ class BulkEditView(PassUserMixin):
|
||||
"merge": None,
|
||||
"edit_pdf": "checksum",
|
||||
"reprocess": "checksum",
|
||||
"remove_password": "checksum",
|
||||
}
|
||||
|
||||
permission_classes = (IsAuthenticated,)
|
||||
@@ -1522,6 +1523,7 @@ class BulkEditView(PassUserMixin):
|
||||
bulk_edit.split,
|
||||
bulk_edit.merge,
|
||||
bulk_edit.edit_pdf,
|
||||
bulk_edit.remove_password,
|
||||
]:
|
||||
parameters["user"] = user
|
||||
|
||||
@@ -1550,6 +1552,7 @@ class BulkEditView(PassUserMixin):
|
||||
bulk_edit.rotate,
|
||||
bulk_edit.delete_pages,
|
||||
bulk_edit.edit_pdf,
|
||||
bulk_edit.remove_password,
|
||||
]
|
||||
)
|
||||
or (
|
||||
@@ -1566,7 +1569,7 @@ class BulkEditView(PassUserMixin):
|
||||
and (
|
||||
method in [bulk_edit.split, bulk_edit.merge]
|
||||
or (
|
||||
method == bulk_edit.edit_pdf
|
||||
method in [bulk_edit.edit_pdf, bulk_edit.remove_password]
|
||||
and not parameters["update_document"]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
@@ -14,6 +15,7 @@ from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import WorkflowAction
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.signals import document_consumption_finished
|
||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||
from documents.workflows.webhooks import send_webhook
|
||||
|
||||
@@ -259,3 +261,74 @@ def execute_webhook_action(
|
||||
f"Error occurred sending webhook: {e}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
|
||||
def execute_password_removal_action(
|
||||
action: WorkflowAction,
|
||||
document: Document | ConsumableDocument,
|
||||
logging_group,
|
||||
) -> None:
|
||||
"""
|
||||
Try to remove a password from a document using the configured list.
|
||||
"""
|
||||
passwords = action.passwords
|
||||
if not passwords:
|
||||
logger.warning(
|
||||
"Password removal action %s has no passwords configured",
|
||||
action.pk,
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
return
|
||||
|
||||
passwords = [
|
||||
password.strip()
|
||||
for password in re.split(r"[,\n]", passwords)
|
||||
if password.strip()
|
||||
]
|
||||
|
||||
if isinstance(document, ConsumableDocument):
|
||||
# hook the consumption-finished signal to attempt password removal later
|
||||
def handler(sender, **kwargs):
|
||||
consumed_document: Document = kwargs.get("document")
|
||||
if consumed_document is not None:
|
||||
execute_password_removal_action(
|
||||
action,
|
||||
consumed_document,
|
||||
logging_group,
|
||||
)
|
||||
document_consumption_finished.disconnect(handler)
|
||||
|
||||
document_consumption_finished.connect(handler, weak=False)
|
||||
return
|
||||
|
||||
# import here to avoid circular dependency
|
||||
from documents.bulk_edit import remove_password
|
||||
|
||||
for password in passwords:
|
||||
try:
|
||||
remove_password(
|
||||
[document.id],
|
||||
password=password,
|
||||
update_document=True,
|
||||
user=document.owner,
|
||||
)
|
||||
logger.info(
|
||||
"Removed password from document %s using workflow action %s",
|
||||
document.pk,
|
||||
action.pk,
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
return
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
"Password removal failed for document %s with supplied password: %s",
|
||||
document.pk,
|
||||
e,
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
logger.error(
|
||||
"Password removal failed for document %s after trying all provided passwords",
|
||||
document.pk,
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 14:49+0000\n"
|
||||
"POT-Creation-Date: 2025-12-24 05:27+0000\n"
|
||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@@ -1219,35 +1219,35 @@ msgstr ""
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:642
|
||||
#: documents/serialisers.py:640
|
||||
msgid "Invalid color."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1835
|
||||
#: documents/serialisers.py:1826
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1879
|
||||
#: documents/serialisers.py:1870
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1886
|
||||
#: documents/serialisers.py:1877
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1903 documents/serialisers.py:1913
|
||||
#: documents/serialisers.py:1894 documents/serialisers.py:1904
|
||||
msgid ""
|
||||
"Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1908
|
||||
#: documents/serialisers.py:1899
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2023
|
||||
#: documents/serialisers.py:2014
|
||||
msgid "Invalid variable detected."
|
||||
msgstr ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user