diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index feaceea1cc3f4..a7ff8b7af758d 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -1455,6 +1455,72 @@ describe("PDF viewer", () => { }); }); + describe("Save/download disabled when supportsDownloading is false", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".textLayer .endOfContent", + null, + null, + { supportsDownloading: false } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must hide the download buttons and skip save/download", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForSelector("#downloadButton", { hidden: true }); + await waitAndClick(page, "#secondaryToolbarToggleButton"); + await page.waitForSelector("#secondaryDownload", { hidden: true }); + + const triggered = await page.evaluate(async () => { + const app = window.PDFViewerApplication; + const calls = []; + const saveDocument = app.pdfDocument.saveDocument.bind( + app.pdfDocument + ); + app.pdfDocument.saveDocument = (...args) => { + calls.push("saveDocument"); + return saveDocument(...args); + }; + + // Each bail-out path dispatches a TESTING-only "downloadskipped" + // event, so we can deterministically wait for all four attempts to + // run to completion. + let skipped = 0; + const allSkipped = new Promise(resolve => { + app.eventBus.on("downloadskipped", function listener() { + if (++skipped === 4) { + app.eventBus.off("downloadskipped", listener); + resolve(); + } + }); + }); + + await app.download(); + await app.save(); + await app.downloadOrSave(); + app.eventBus.dispatch("download", { source: null }); + await allSkipped; + + return { calls, skipped, downloadManager: app.downloadManager }; + }); + expect(triggered.downloadManager) + .withContext(`In ${browserName}`) + .toBeNull(); + expect(triggered.calls).withContext(`In ${browserName}`).toEqual([]); + expect(triggered.skipped).withContext(`In ${browserName}`).toBe(4); + }) + ); + }); + }); + describe("Pinch-zoom", () => { let pages; diff --git a/web/app.js b/web/app.js index e3d9fb9feb03d..2f5a504347734 100644 --- a/web/app.js +++ b/web/app.js @@ -385,6 +385,7 @@ const PDFViewerApplication = { maxCanvasPixels: x => parseInt(x, 10), spreadModeOnLoad: x => parseInt(x, 10), supportsCaretBrowsingMode: x => x === "true", + supportsDownloading: x => x === "true", viewerCssTheme: x => parseInt(x, 10), forcePageColors: x => x === "true", pageColorsBackground: x => x, @@ -434,7 +435,16 @@ const PDFViewerApplication = { ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"), })); - const downloadManager = (this.downloadManager = new DownloadManager()); + const supportsDownloading = AppOptions.get("supportsDownloading"); + const downloadManager = (this.downloadManager = supportsDownloading + ? new DownloadManager() + : null); + if (appConfig.secondaryToolbar?.downloadButton) { + appConfig.secondaryToolbar.downloadButton.hidden = !supportsDownloading; + } + if (appConfig.toolbar?.download) { + appConfig.toolbar.download.hidden = !supportsDownloading; + } const findController = (this.findController = new PDFFindController({ linkService, @@ -1311,6 +1321,13 @@ const PDFViewerApplication = { }, async download() { + if (!this.downloadManager) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + this.eventBus.dispatch("downloadskipped", { source: this }); + } + return; + } + let data; try { data = await (this.pdfDocument @@ -1323,6 +1340,13 @@ const PDFViewerApplication = { }, async save() { + if (!this.downloadManager) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + this.eventBus.dispatch("downloadskipped", { source: this }); + } + return; + } + if (this._saveInProgress) { return; } @@ -1354,6 +1378,13 @@ const PDFViewerApplication = { }, async downloadOrSave() { + if (!this.downloadManager) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + this.eventBus.dispatch("downloadskipped", { source: this }); + } + return; + } + // In the Firefox case, this method MUST always trigger a download. // When the user is closing a modified and unsaved document, we display a // prompt asking for saving or not. In case they save, we must wait for @@ -2442,6 +2473,9 @@ const PDFViewerApplication = { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { return; } + if (!this.downloadManager) { + return; + } if (!this.pdfDocument) { return; } diff --git a/web/app_options.js b/web/app_options.js index 804f2dc237d53..157adccd02d69 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -102,6 +102,11 @@ const defaultOptions = { value: true, kind: OptionKind.BROWSER, }, + supportsDownloading: { + /** @type {boolean} */ + value: true, + kind: OptionKind.BROWSER, + }, supportsIntegratedFind: { /** @type {boolean} */ value: false, diff --git a/web/pdf_attachment_viewer.js b/web/pdf_attachment_viewer.js index 30ab2d2d1c13a..5be3a01858dec 100644 --- a/web/pdf_attachment_viewer.js +++ b/web/pdf_attachment_viewer.js @@ -126,7 +126,7 @@ class PDFAttachmentViewer extends BaseTreeViewer { : fallbackContent; if (content) { - this.downloadManager.openOrDownloadData(content, filename); + this.downloadManager?.openOrDownloadData(content, filename); } }; diff --git a/web/pdf_outline_viewer.js b/web/pdf_outline_viewer.js index bdbcb07b0785e..ef59fc84c58a5 100644 --- a/web/pdf_outline_viewer.js +++ b/web/pdf_outline_viewer.js @@ -155,7 +155,10 @@ class PDFOutlineViewer extends BaseTreeViewer { const content = await linkService.getAttachmentContent(attachmentId); if (content) { - this.downloadManager.openOrDownloadData(content, attachment.filename); + this.downloadManager?.openOrDownloadData( + content, + attachment.filename + ); } };