From f3739231707269623fc31b5ed6ef5ae1a787bfb1 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 1 Apr 2026 18:20:52 +0200 Subject: [PATCH] Encrypt pdf data when merging the same pdf (bug 2028369) --- src/core/editor/pdf_editor.js | 15 +++++++++++-- test/unit/api_spec.js | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 585c2eaf4..cd867aab3 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -108,8 +108,16 @@ class XRefWrapper { } class PDFEditor { + // Whether the edited PDF contains only one file. This is used to determine if + // we can handle some potential duplications. + // For example, there are no obvious way to dedup page labels when merging + // multiple PDF files. hasSingleFile = false; + // Whether the edited PDF contains multiple files. This is used to determine + // if we can preserve some information such as passwords. + isSingleFile = false; + #newAnnotationsParams = null; currentDocument = null; @@ -563,6 +571,9 @@ class PDFEditor { async extractPages(pageInfos, annotationStorage, handler, task) { const promises = []; let newIndex = 0; + this.isSingleFile = + pageInfos.length === 1 || + pageInfos.every(info => info.document === pageInfos[0].document); this.hasSingleFile = pageInfos.length === 1; const allDocumentData = []; @@ -2323,7 +2334,7 @@ class PDFEditor { */ #makeInfo() { const infoMap = new Map(); - if (this.hasSingleFile) { + if (this.isSingleFile) { const { xref: { trailer }, } = this.oldPages[0].documentData.document; @@ -2356,7 +2367,7 @@ class PDFEditor { * @returns {Promise<[Dict|null, CipherTransformFactory|null, Array|null]>} */ async #makeEncrypt() { - if (!this.hasSingleFile) { + if (!this.isSingleFile) { return [null, null, null]; } const { documentData } = this.oldPages[0]; diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 217bd677d..27ca2ed5c 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -6982,5 +6982,46 @@ small scripts as well as for`); await newLoadingTask.destroy(); }); }); + + describe("extract pages from password-protected PDF (bug 2028369)", function () { + it("should preserve password protection when merging the same pdf", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("pr6531_2.pdf", { password: "asdfasdf" }) + ); + const pdfDoc = await loadingTask.promise; + + const data = await pdfDoc.extractPages([ + { document: null }, + { document: null }, + ]); + await loadingTask.destroy(); + + // Opening the result without a password must fail. + // Use a copy so the underlying ArrayBuffer is not detached before the + // second getDocument call below. + const passwordNeededLoadingTask = getDocument(data.slice()); + await passwordNeededLoadingTask.promise.then( + function () { + // Shouldn't get here. + expect(false).toEqual(true); + }, + function (err) { + expect(err).toBeInstanceOf(PasswordException); + expect(err.code).toEqual(PasswordResponses.NEED_PASSWORD); + } + ); + await passwordNeededLoadingTask.destroy(); + + // Opening the result with the correct password must succeed. + const passwordAcceptedLoadingTask = getDocument({ + data, + password: "asdfasdf", + }); + const newPdfDoc = await passwordAcceptedLoadingTask.promise; + expect(newPdfDoc).toBeInstanceOf(PDFDocumentProxy); + expect(newPdfDoc.numPages).toEqual(2); + await passwordAcceptedLoadingTask.destroy(); + }); + }); }); });