- `"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.
- `merge`
- No additional `parameters` required.
- The ordering of the merged document is determined by the list of IDs.
<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 (requiresPassword || password) {
+ <button ngbDropdownItem (click)="removePassword()" [disabled]="!userIsOwner || !password">
+ <i-bs name="unlock"></i-bs> <ng-container i18n>Remove Password</ng-container>
+ </button>
+ }
</div>
</div>
})
}
+ removePassword() {
+ if (this.requiresPassword || !this.password) {
+ this.toastService.showError(
+ $localize`Please enter the current password before attempting to remove it.`
+ )
+ return
+ }
+ this.networkActive = true
+ this.documentsService
+ .bulkEdit([this.document.id], 'remove_password', {
+ password: this.password,
+ })
+ .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
+ this.openDocumentService.refreshDocument(this.documentId)
+ },
+ error: (error) => {
+ this.networkActive = false
+ this.toastService.showError(
+ $localize`Error executing password removal operation`,
+ error
+ )
+ },
+ })
+ }
+
printDocument() {
const printUrl = this.documentsService.getDownloadUrl(
this.document.id,
threeDotsVertical,
trash,
uiRadios,
+ unlock,
upcScan,
windowStack,
x,
threeDotsVertical,
trash,
uiRadios,
+ unlock,
upcScan,
windowStack,
x,
return "OK"
+def remove_password(
+ doc_ids: list[int],
+ password: str,
+ *,
+ delete_original: bool = False,
+ update_document: 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,
"split",
"delete_pages",
"edit_pdf",
+ "remove_password",
],
label="Method",
write_only=True,
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.")
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"]
"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
"merge": None,
"edit_pdf": "checksum",
"reprocess": "checksum",
+ "remove_password": "checksum",
}
permission_classes = (IsAuthenticated,)
bulk_edit.split,
bulk_edit.merge,
bulk_edit.edit_pdf,
+ bulk_edit.remove_password,
]:
parameters["user"] = user
bulk_edit.rotate,
bulk_edit.delete_pages,
bulk_edit.edit_pdf,
+ bulk_edit.remove_password,
]
)
or (
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"]
)
)