From 839c257f87c77999152be1f12716e214c1ce724c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Jan 2026 15:51:39 +0100 Subject: [PATCH] Replace the `IDownloadManager` interface with an abstract `BaseDownloadManager` class This should help reduce the maintenance burden of the code, since you no longer need to remember to update separate code when touching the different `DownloadManager` classes. --- gulpfile.mjs | 2 +- src/display/annotation_layer.js | 6 +- web/annotation_layer_builder.js | 3 +- web/base_download_manager.js | 103 ++++++++++++++++++++++++++ web/chromecom.js | 22 +++++- web/download_manager.js | 123 ++++++++------------------------ web/firefoxcom.js | 71 ++++-------------- web/interfaces.js | 29 +------- web/pdf_link_service.js | 2 +- web/pdf_viewer.js | 3 +- 10 files changed, 176 insertions(+), 188 deletions(-) create mode 100644 web/base_download_manager.js diff --git a/gulpfile.mjs b/gulpfile.mjs index 7b036c530..c758770be 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -231,7 +231,7 @@ function createWebpackAlias(defines) { libraryAlias["display-fetch_stream"] = "src/display/fetch_stream.js"; libraryAlias["display-network"] = "src/display/network.js"; - viewerAlias["web-download_manager"] = "web/download_manager.js"; + viewerAlias["web-download_manager"] = "web/chromecom.js"; viewerAlias["web-external_services"] = "web/chromecom.js"; viewerAlias["web-null_l10n"] = "web/l10n.js"; viewerAlias["web-preferences"] = "web/chromecom.js"; diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 947e686e8..d289fbd18 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -18,8 +18,6 @@ // eslint-disable-next-line max-len /** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ // eslint-disable-next-line max-len -/** @typedef {import("../../web/interfaces").IDownloadManager} IDownloadManager */ -// eslint-disable-next-line max-len /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ // eslint-disable-next-line max-len /** @typedef {import("../../web/struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */ @@ -57,7 +55,7 @@ const TIMEZONE_OFFSET = new Date().getTimezoneOffset() * 60 * 1000; * @property {Object} data * @property {HTMLDivElement} layer * @property {PDFLinkService} linkService - * @property {IDownloadManager} [downloadManager] + * @property {BaseDownloadManager} [downloadManager] * @property {AnnotationStorage} [annotationStorage] * @property {string} [imageResourcesPath] - Path for image resources, mainly * for annotation icons. Include trailing slash. @@ -3736,7 +3734,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement { * @property {Array} annotations * @property {PDFPageProxy} page * @property {PDFLinkService} linkService - * @property {IDownloadManager} [downloadManager] + * @property {BaseDownloadManager} [downloadManager] * @property {AnnotationStorage} [annotationStorage] * @property {string} [imageResourcesPath] - Path for image resources, mainly * for annotation icons. Include trailing slash. diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index eef2fca33..974db622e 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -18,7 +18,6 @@ /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */ // eslint-disable-next-line max-len /** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */ -/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */ // eslint-disable-next-line max-len /** @typedef {import("./struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */ // eslint-disable-next-line max-len @@ -43,7 +42,7 @@ import { PresentationModeState } from "./ui_utils.js"; * for annotation icons. Include trailing slash. * @property {boolean} renderForms * @property {PDFLinkService} linkService - * @property {IDownloadManager} [downloadManager] + * @property {BaseDownloadManager} [downloadManager] * @property {boolean} [enableComment] * @property {boolean} [enableScripting] * @property {Promise} [hasJSActionsPromise] diff --git a/web/base_download_manager.js b/web/base_download_manager.js new file mode 100644 index 000000000..7caa616af --- /dev/null +++ b/web/base_download_manager.js @@ -0,0 +1,103 @@ +/* Copyright 2013 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isPdfFile } from "pdfjs-lib"; + +class BaseDownloadManager { + #openBlobUrls = new WeakMap(); + + constructor() { + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && + this.constructor === BaseDownloadManager + ) { + throw new Error("Cannot initialize BaseDownloadManager."); + } + } + + _triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) { + throw new Error("Not implemented: _triggerDownload"); + } + + _getOpenDataUrl(blobUrl, filename, dest = null) { + throw new Error("Not implemented: _getOpenDataUrl"); + } + + /** + * @param {Uint8Array} data + * @param {string} filename + * @param {string} [contentType] + */ + downloadData(data, filename, contentType) { + const blobUrl = URL.createObjectURL( + new Blob([data], { type: contentType }) + ); + + this._triggerDownload( + blobUrl, + /* originalUrl = */ blobUrl, + filename, + /* isAttachment = */ true + ); + } + + /** + * @param {Uint8Array} data + * @param {string} filename + * @param {string | null} [dest] + * @returns {boolean} Indicating if the data was opened. + */ + openOrDownloadData(data, filename, dest = null) { + const isPdfData = isPdfFile(filename); + const contentType = isPdfData ? "application/pdf" : ""; + + if (isPdfData) { + let blobUrl; + try { + blobUrl = this.#openBlobUrls.getOrInsertComputed(data, () => + URL.createObjectURL(new Blob([data], { type: contentType })) + ); + const viewerUrl = this._getOpenDataUrl(blobUrl, filename, dest); + + window.open(viewerUrl); + return true; + } catch (ex) { + console.error("openOrDownloadData:", ex); + // Release the `blobUrl`, since opening it failed, and fallback to + // downloading the PDF file. + URL.revokeObjectURL(blobUrl); + this.#openBlobUrls.delete(data); + } + } + + this.downloadData(data, filename, contentType); + return false; + } + + /** + * @param {Uint8Array} data + * @param {string} url + * @param {string} filename + */ + download(data, url, filename) { + const blobUrl = data + ? URL.createObjectURL(new Blob([data], { type: "application/pdf" })) + : null; + + this._triggerDownload(blobUrl, /* originalUrl = */ url, filename); + } +} + +export { BaseDownloadManager }; diff --git a/web/chromecom.js b/web/chromecom.js index 6129fa2dd..96231d5de 100644 --- a/web/chromecom.js +++ b/web/chromecom.js @@ -17,6 +17,7 @@ import { AppOptions } from "./app_options.js"; import { BaseExternalServices } from "./external_services.js"; import { BasePreferences } from "./preferences.js"; +import { DownloadManager as GenericDownloadManager } from "./download_manager.js"; import { GenericL10n } from "./genericl10n.js"; import { GenericScripting } from "./generic_scripting.js"; import { SignatureStorage } from "./generic_signature_storage.js"; @@ -310,6 +311,25 @@ function setReferer(url, callback) { } } +/** + * This "should" really extend the `BaseDownloadManager` class, + * however doing it this way instead reduces code duplication. + */ +class DownloadManager extends GenericDownloadManager { + _getOpenDataUrl(blobUrl, filename, dest = null) { + // In the Chrome extension, the URL is rewritten using the history API + // in viewer.js, so an absolute URL must be generated. + let url = + chrome.runtime.getURL("/content/web/viewer.html") + + "?file=" + + encodeURIComponent(blobUrl + "#" + filename); + if (dest) { + url += `#${escape(dest)}`; + } + return url; + } +} + // chrome.storage.sync is not supported in every Chromium-derivate. // Note: The background page takes care of migrating values from // chrome.storage.local to chrome.storage.sync when needed. @@ -437,4 +457,4 @@ class MLManager { } } -export { ExternalServices, initCom, MLManager, Preferences }; +export { DownloadManager, ExternalServices, initCom, MLManager, Preferences }; diff --git a/web/download_manager.js b/web/download_manager.js index 26d6200b3..7a2bbf111 100644 --- a/web/download_manager.js +++ b/web/download_manager.js @@ -13,9 +13,8 @@ * limitations under the License. */ -/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */ - -import { createValidAbsoluteUrl, isPdfFile } from "pdfjs-lib"; +import { BaseDownloadManager } from "./base_download_manager.js"; +import { createValidAbsoluteUrl } from "pdfjs-lib"; if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { throw new Error( @@ -24,101 +23,41 @@ if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("CHROME || GENERIC")) { ); } -function download(blobUrl, filename) { - const a = document.createElement("a"); - if (!a.click) { - throw new Error('DownloadManager: "a.click()" is not supported.'); - } - a.href = blobUrl; - a.target = "_parent"; - // Use a.download if available. This increases the likelihood that - // the file is downloaded instead of opened by another PDF plugin. - if ("download" in a) { - a.download = filename; - } - // must be in the document for recent Firefox versions, - // otherwise .click() is ignored. - (document.body || document.documentElement).append(a); - a.click(); - a.remove(); -} - -/** - * @implements {IDownloadManager} - */ -class DownloadManager { - #openBlobUrls = new WeakMap(); - - downloadData(data, filename, contentType) { - const blobUrl = URL.createObjectURL( - new Blob([data], { type: contentType }) - ); - download(blobUrl, filename); - } - - /** - * @returns {boolean} Indicating if the data was opened. - */ - openOrDownloadData(data, filename, dest = null) { - const isPdfData = isPdfFile(filename); - const contentType = isPdfData ? "application/pdf" : ""; - - if ( - (typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")) && - isPdfData - ) { - let blobUrl = this.#openBlobUrls.get(data); - if (!blobUrl) { - blobUrl = URL.createObjectURL(new Blob([data], { type: contentType })); - this.#openBlobUrls.set(data, blobUrl); - } - let viewerUrl; - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - // The current URL is the viewer, let's use it and append the file. - viewerUrl = "?file=" + encodeURIComponent(blobUrl + "#" + filename); - } else if (PDFJSDev.test("CHROME")) { - // In the Chrome extension, the URL is rewritten using the history API - // in viewer.js, so an absolute URL must be generated. - viewerUrl = - // eslint-disable-next-line no-undef - chrome.runtime.getURL("/content/web/viewer.html") + - "?file=" + - encodeURIComponent(blobUrl + "#" + filename); - } - if (dest) { - viewerUrl += `#${escape(dest)}`; - } - - try { - window.open(viewerUrl); - return true; - } catch (ex) { - console.error("openOrDownloadData:", ex); - // Release the `blobUrl`, since opening it failed, and fallback to - // downloading the PDF file. - URL.revokeObjectURL(blobUrl); - this.#openBlobUrls.delete(data); +class DownloadManager extends BaseDownloadManager { + _triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) { + if (!blobUrl && !isAttachment) { + // Fallback to downloading non-attachments by their URL. + if (!createValidAbsoluteUrl(originalUrl, "http://example.com")) { + throw new Error(`_triggerDownload - not a valid URL: ${originalUrl}`); } + blobUrl = originalUrl + "#pdfjs.action=download"; } - this.downloadData(data, filename, contentType); - return false; + const a = document.createElement("a"); + a.href = blobUrl; + a.target = "_parent"; + // Use a.download if available. This increases the likelihood that + // the file is downloaded instead of opened by another PDF plugin. + if ("download" in a) { + a.download = filename; + } + // must be in the document for recent Firefox versions, + // otherwise .click() is ignored. + (document.body || document.documentElement).append(a); + a.click(); + a.remove(); } - download(data, url, filename) { - let blobUrl; - if (data) { - blobUrl = URL.createObjectURL( - new Blob([data], { type: "application/pdf" }) - ); - } else { - if (!createValidAbsoluteUrl(url, "http://example.com")) { - console.error(`download - not a valid URL: ${url}`); - return; - } - blobUrl = url + "#pdfjs.action=download"; + _getOpenDataUrl(blobUrl, filename, dest = null) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COMPONENTS")) { + throw new Error("Opening data is not supported in `COMPONENTS` builds."); } - download(blobUrl, filename); + // The current URL is the viewer, let's use it and append the file. + let url = "?file=" + encodeURIComponent(blobUrl + "#" + filename); + if (dest) { + url += `#${escape(dest)}`; + } + return url; } } diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 8db4d8965..2b93e82dd 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -13,8 +13,9 @@ * limitations under the License. */ -import { isPdfFile, MathClamp, PDFDataRangeTransport } from "pdfjs-lib"; +import { MathClamp, PDFDataRangeTransport } from "pdfjs-lib"; import { AppOptions } from "./app_options.js"; +import { BaseDownloadManager } from "./base_download_manager.js"; import { BaseExternalServices } from "./external_services.js"; import { BasePreferences } from "./preferences.js"; import { DEFAULT_SCALE_VALUE } from "./ui_utils.js"; @@ -80,69 +81,25 @@ class FirefoxCom { } } -class DownloadManager { - #openBlobUrls = new WeakMap(); - - downloadData(data, filename, contentType) { - const blobUrl = URL.createObjectURL( - new Blob([data], { type: contentType }) - ); - +class DownloadManager extends BaseDownloadManager { + _triggerDownload(blobUrl, originalUrl, filename, isAttachment = false) { FirefoxCom.request("download", { blobUrl, - originalUrl: blobUrl, + originalUrl, filename, - isAttachment: true, + isAttachment, }); } - /** - * @returns {boolean} Indicating if the data was opened. - */ - openOrDownloadData(data, filename, dest = null) { - const isPdfData = isPdfFile(filename); - const contentType = isPdfData ? "application/pdf" : ""; - - if (isPdfData) { - let blobUrl = this.#openBlobUrls.get(data); - if (!blobUrl) { - blobUrl = URL.createObjectURL(new Blob([data], { type: contentType })); - this.#openBlobUrls.set(data, blobUrl); - } - // Let Firefox's content handler catch the URL and display the PDF. - // NOTE: This cannot use a query string for the filename, see - // https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5 - let viewerUrl = blobUrl + "#filename=" + encodeURIComponent(filename); - if (dest) { - viewerUrl += `&filedest=${escape(dest)}`; - } - - try { - window.open(viewerUrl); - return true; - } catch (ex) { - console.error("openOrDownloadData:", ex); - // Release the `blobUrl`, since opening it failed, and fallback to - // downloading the PDF file. - URL.revokeObjectURL(blobUrl); - this.#openBlobUrls.delete(data); - } + _getOpenDataUrl(blobUrl, filename, dest = null) { + // Let Firefox's content handler catch the URL and display the PDF. + // NOTE: This cannot use a query string for the filename, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1632644#c5 + let url = blobUrl + "#filename=" + encodeURIComponent(filename); + if (dest) { + url += `&filedest=${escape(dest)}`; } - - this.downloadData(data, filename, contentType); - return false; - } - - download(data, url, filename) { - const blobUrl = data - ? URL.createObjectURL(new Blob([data], { type: "application/pdf" })) - : null; - - FirefoxCom.request("download", { - blobUrl, - originalUrl: url, - filename, - }); + return url; } } diff --git a/web/interfaces.js b/web/interfaces.js index c798eba3a..428c5de58 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -44,31 +44,4 @@ class IRenderableView { async draw() {} } -/** - * @interface - */ -class IDownloadManager { - /** - * @param {Uint8Array} data - * @param {string} filename - * @param {string} [contentType] - */ - downloadData(data, filename, contentType) {} - - /** - * @param {Uint8Array} data - * @param {string} filename - * @param {string | null} [dest] - * @returns {boolean} Indicating if the data was opened. - */ - openOrDownloadData(data, filename, dest = null) {} - - /** - * @param {Uint8Array} data - * @param {string} url - * @param {string} filename - */ - download(data, url, filename) {} -} - -export { IDownloadManager, IRenderableView }; +export { IRenderableView }; diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index c57be4873..1b04f457a 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -423,7 +423,7 @@ class PDFLinkService { } // Support opening of PDF attachments in the Firefox PDF Viewer, // which uses a couple of non-standard hash parameters; refer to - // `DownloadManager.openOrDownloadData` in the firefoxcom.js file. + // `DownloadManager._getOpenDataUrl` in the firefoxcom.js file. if (!params.has("filename") || !params.has("filedest")) { return; } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 6235f8752..525a8a839 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -20,7 +20,6 @@ // eslint-disable-next-line max-len /** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */ /** @typedef {import("./event_utils").EventBus} EventBus */ -/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */ // eslint-disable-next-line max-len /** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */ // eslint-disable-next-line max-len @@ -89,7 +88,7 @@ function isValidAnnotationEditorMode(mode) { * @property {HTMLDivElement} [viewer] - The viewer element. * @property {EventBus} eventBus - The application event bus. * @property {PDFLinkService} [linkService] - The navigation/linking service. - * @property {IDownloadManager} [downloadManager] - The download manager + * @property {BaseDownloadManager} [downloadManager] - The download manager * component. * @property {PDFFindController} [findController] - The find controller * component.