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.
This commit is contained in:
Jonas Jenwald 2026-01-30 15:51:39 +01:00
parent ff7f87fc21
commit 839c257f87
10 changed files with 176 additions and 188 deletions

View File

@ -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";

View File

@ -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.

View File

@ -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<boolean>} [hasJSActionsPromise]

View File

@ -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 };

View File

@ -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 };

View File

@ -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;
}
// <a> 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;
}
// <a> 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;
}
}

View File

@ -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;
}
}

View File

@ -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 };

View File

@ -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;
}

View File

@ -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.