1 /* Copyright 2012 Mozilla Foundation
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
16 const PDFJS_EVENT_ID = "pdf.js.message";
17 const PDF_VIEWER_ORIGIN = "resource://pdf.js";
18 const PDF_VIEWER_WEB_PAGE = "resource://pdf.js/web/viewer.html";
19 const MAX_NUMBER_OF_PREFS = 50;
20 const PDF_CONTENT_TYPE = "application/pdf";
22 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
23 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
25 // Non-pdfjs preferences to get when the viewer is created and to observe.
26 const toolbarDensityPref = "browser.uidensity";
27 const caretBrowsingModePref = "accessibility.browsewithcaret";
30 ChromeUtils.defineESModuleGetters(lazy, {
31 NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
32 NetworkManager: "resource://pdf.js/PdfJsNetwork.sys.mjs",
33 PdfJs: "resource://pdf.js/PdfJs.sys.mjs",
34 PdfJsTelemetryContent: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
35 PdfSandbox: "resource://pdf.js/PdfSandbox.sys.mjs",
36 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
40 XPCOMUtils.defineLazyServiceGetter(
43 "@mozilla.org/mime;1",
46 XPCOMUtils.defineLazyServiceGetter(
49 "@mozilla.org/uriloader/handler-service;1",
53 ChromeUtils.defineLazyGetter(lazy, "gOurBinary", () => {
54 let file = Services.dirsvc.get("XREExeF", Ci.nsIFile);
55 // Make sure to get the .app on macOS
56 if (AppConstants.platform == "macosx") {
58 if (/\.app\/?$/i.test(file.leafName)) {
68 if (!Services.prefs.getBoolPref("pdfjs.pdfBugEnabled", false)) {
71 var msg = "PdfStreamConverter.js: " + (aMsg.join ? aMsg.join("") : aMsg);
72 Services.console.logStringMessage(msg);
76 function getDOMWindow(aChannel, aPrincipal) {
77 var requestor = aChannel.notificationCallbacks
78 ? aChannel.notificationCallbacks
79 : aChannel.loadGroup.notificationCallbacks;
80 var win = requestor.getInterface(Ci.nsIDOMWindow);
81 // Ensure the window wasn't navigated to something that is not PDF.js.
82 if (!win.document.nodePrincipal.equals(aPrincipal)) {
88 function getActor(window) {
91 AppConstants.platform === "android" ? "GeckoViewPdfjs" : "Pdfjs";
92 return window.windowGlobalChild.getActor(actorName);
98 function isValidMatchesCount(data) {
99 if (typeof data !== "object" || data === null) {
102 const { current, total } = data;
104 typeof total !== "number" ||
106 typeof current !== "number" ||
116 function PdfDataListener(length) {
117 this.length = length; // less than 0, if length is unknown
122 PdfDataListener.prototype = {
123 append: function PdfDataListener_append(chunk) {
124 // In most of the cases we will pass data as we receive it, but at the
125 // beginning of the loading we may accumulate some data.
126 this.buffers.push(chunk);
127 this.loaded += chunk.length;
128 if (this.length >= 0 && this.length < this.loaded) {
129 this.length = -1; // reset the length, server is giving incorrect one
131 this.onprogress(this.loaded, this.length >= 0 ? this.length : void 0);
133 readData: function PdfDataListener_readData() {
134 if (this.buffers.length === 0) {
137 if (this.buffers.length === 1) {
138 return this.buffers.pop();
140 // There are multiple buffers that need to be combined into a single
142 let combinedLength = 0;
143 for (let buffer of this.buffers) {
144 combinedLength += buffer.length;
146 let combinedArray = new Uint8Array(combinedLength);
148 while (this.buffers.length) {
149 let buffer = this.buffers.shift();
150 combinedArray.set(buffer, writeOffset);
151 writeOffset += buffer.length;
153 return combinedArray;
156 return !!this.isDataReady;
158 finish: function PdfDataListener_finish() {
159 this.isDataReady = true;
160 if (this.oncompleteCallback) {
161 this.oncompleteCallback(this.readData());
164 error: function PdfDataListener_error(errorCode) {
165 this.errorCode = errorCode;
166 if (this.oncompleteCallback) {
167 this.oncompleteCallback(null, errorCode);
172 return this.oncompleteCallback;
174 set oncomplete(value) {
175 this.oncompleteCallback = value;
176 if (this.isDataReady) {
177 value(this.readData());
179 if (this.errorCode) {
180 value(null, this.errorCode);
190 caretBrowsingModePref,
192 name: "supportsCaretBrowsingMode",
194 dispatchToContent: true,
199 constructor(domWindow, isMobile) {
200 this.#domWindow = domWindow;
201 this.#init(isMobile);
206 this.#prefs.set(toolbarDensityPref, {
207 name: "toolbarDensity",
209 dispatchToContent: true,
211 this.#prefs.set("pdfjs.enableGuessAltText", {
212 name: "enableGuessAltText",
214 dispatchToContent: true,
215 dispatchToParent: true,
217 this.#prefs.set("pdfjs.enableAltTextModelDownload", {
218 name: "enableAltTextModelDownload",
220 dispatchToContent: true,
221 dispatchToParent: true,
224 // Once the experiment for new alt-text stuff is removed, we can remove this.
225 this.#prefs.set("pdfjs.enableAltText", {
226 name: "enableAltText",
228 dispatchToParent: true,
230 this.#prefs.set("pdfjs.enableNewAltTextWhenAddingImage", {
231 name: "enableNewAltTextWhenAddingImage",
233 dispatchToParent: true,
235 this.#prefs.set("browser.ml.enable", {
236 name: "browser.ml.enable",
238 dispatchToParent: true,
241 for (const pref of this.#prefs.keys()) {
242 Services.prefs.addObserver(pref, this, /* aHoldWeak = */ true);
246 observe(_aSubject, aTopic, aPrefName) {
247 if (aTopic != "nsPref:changed") {
251 const actor = getActor(this.#domWindow);
255 const { name, type, dispatchToContent, dispatchToParent } =
256 this.#prefs.get(aPrefName) || {};
263 value = Services.prefs.getBoolPref(aPrefName);
267 value = Services.prefs.getIntPref(aPrefName);
271 const data = { name, value };
272 if (dispatchToContent) {
273 actor.dispatchEvent("updatedPreference", data);
275 if (dispatchToParent) {
276 actor.sendAsyncMessage("PDFJS:Parent:updatedPreference", data);
280 QueryInterface = ChromeUtils.generateQI([Ci.nsISupportsWeakReference]);
284 * All the privileged actions.
286 class ChromeActions {
287 #allowedGlobalEvents = new Set([
294 constructor(domWindow, contentDispositionFilename) {
295 this.domWindow = domWindow;
296 this.contentDispositionFilename = contentDispositionFilename;
298 this.unloadListener = null;
299 this.observer = new PrefObserver(domWindow, this.isMobile());
302 createSandbox(data, sendResponse) {
303 function sendResp(res) {
310 if (!Services.prefs.getBoolPref("pdfjs.enableScripting", false)) {
311 return sendResp(false);
314 if (this.sandbox !== null) {
315 return sendResp(true);
319 this.sandbox = new lazy.PdfSandbox(this.domWindow, data);
321 // If there's an error here, it means that something is really wrong
322 // on pdf.js side during sandbox initialization phase.
324 return sendResp(false);
327 this.unloadListener = () => {
328 this.destroySandbox();
330 this.domWindow.addEventListener("unload", this.unloadListener);
332 return sendResp(true);
335 dispatchEventInSandbox(event) {
337 this.sandbox.dispatchEvent(event);
341 dispatchAsyncEventInSandbox(event, sendResponse) {
342 this.dispatchEventInSandbox(event);
348 this.domWindow.removeEventListener("unload", this.unloadListener);
349 this.sandbox.destroy();
354 isInPrivateBrowsing() {
355 return lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow);
358 getWindowOriginAttributes() {
360 return this.domWindow.document.nodePrincipal.originAttributes;
366 async mlDelete(data, sendResponse) {
367 const actor = getActor(this.domWindow);
372 const response = await actor.sendQuery("PDFJS:Parent:mlDelete", data);
373 sendResponse(response);
376 async mlGuess(data, sendResponse) {
377 const actor = getActor(this.domWindow);
382 const response = await actor.sendQuery("PDFJS:Parent:mlGuess", data);
383 sendResponse(response);
386 async loadAIEngine(data, sendResponse) {
387 const actor = getActor(this.domWindow);
392 sendResponse(await actor.sendQuery("PDFJS:Parent:loadAIEngine", data));
396 const { originalUrl } = data;
397 const blobUrl = data.blobUrl || originalUrl;
398 let { filename } = data;
400 typeof filename !== "string" ||
401 (!/\.pdf$/i.test(filename) && !data.isAttachment)
403 filename = "document.pdf";
405 const actor = getActor(this.domWindow);
406 actor.sendAsyncMessage("PDFJS:Parent:saveURL", {
413 getLocaleProperties() {
414 const { requestedLocale, defaultLocale, isAppLocaleRTL } = Services.locale;
416 lang: requestedLocale || defaultLocale,
417 isRTL: isAppLocaleRTL,
421 supportsIntegratedFind() {
422 // Integrated find is only supported when we're not in a frame
423 return this.domWindow.windowGlobalChild.browsingContext.parent === null;
426 async getBrowserPrefs() {
427 const isMobile = this.isMobile();
428 const nimbusDataStr = isMobile
429 ? await this.getNimbusExperimentData()
433 allowedGlobalEvents: this.#allowedGlobalEvents,
434 canvasMaxAreaInBytes: Services.prefs.getIntPref("gfx.max-alloc-size"),
435 isInAutomation: Cu.isInAutomation,
436 localeProperties: this.getLocaleProperties(),
438 supportsDocumentFonts:
439 !!Services.prefs.getIntPref("browser.display.use_document_fonts") &&
440 Services.prefs.getBoolPref("gfx.downloadable_fonts.enabled"),
441 supportsIntegratedFind: this.supportsIntegratedFind(),
442 supportsMouseWheelZoomCtrlKey:
443 Services.prefs.getIntPref("mousewheel.with_control.action") === 3,
444 supportsMouseWheelZoomMetaKey:
445 Services.prefs.getIntPref("mousewheel.with_meta.action") === 3,
446 supportsPinchToZoom: Services.prefs.getBoolPref("apz.allow_zooming"),
447 supportsCaretBrowsingMode: Services.prefs.getBoolPref(
448 caretBrowsingModePref
450 toolbarDensity: Services.prefs.getIntPref(toolbarDensityPref, 0),
455 return AppConstants.platform === "android";
458 async getNimbusExperimentData() {
459 if (!this.isMobile()) {
462 const { promise, resolve } = Promise.withResolvers();
464 const actor = getActor(this.domWindow);
465 actor.sendAsyncMessage("PDFJS:Parent:getNimbus");
466 Services.obs.addObserver(
468 observe(aSubject, aTopic) {
469 if (aTopic === "pdfjs-getNimbus") {
470 Services.obs.removeObserver(this, aTopic);
471 resolve(aSubject && JSON.stringify(aSubject.wrappedJSObject));
480 async dispatchGlobalEvent({ eventName, detail }) {
481 if (!this.#allowedGlobalEvents.has(eventName)) {
484 const windowUtils = this.domWindow.windowUtils;
488 const event = new CustomEvent(eventName, {
493 windowUtils.dispatchEventToChromeOnly(this.domWindow, event);
496 reportTelemetry(data) {
497 const actor = getActor(this.domWindow);
498 actor?.sendAsyncMessage("PDFJS:Parent:reportTelemetry", data);
501 updateFindControlState(data) {
502 if (!this.supportsIntegratedFind()) {
505 // Verify what we're sending to the findbar.
506 var result = data.result;
507 var findPrevious = data.findPrevious;
508 var findPreviousType = typeof findPrevious;
510 typeof result !== "number" ||
513 (findPreviousType !== "undefined" && findPreviousType !== "boolean")
517 // Allow the `matchesCount` property to be optional, and ensure that
518 // it's valid before including it in the data sent to the findbar.
519 let matchesCount = null;
520 if (isValidMatchesCount(data.matchesCount)) {
521 matchesCount = data.matchesCount;
523 // Same for the `rawQuery` property.
525 if (typeof data.rawQuery === "string") {
526 rawQuery = data.rawQuery;
528 // Same for the `entireWord` property.
529 let entireWord = false;
530 if (typeof data.entireWord === "boolean") {
531 entireWord = data.entireWord;
534 let actor = getActor(this.domWindow);
535 actor?.sendAsyncMessage("PDFJS:Parent:updateControlState", {
544 updateFindMatchesCount(data) {
545 if (!this.supportsIntegratedFind()) {
548 // Verify what we're sending to the findbar.
549 if (!isValidMatchesCount(data)) {
553 let actor = getActor(this.domWindow);
554 actor?.sendAsyncMessage("PDFJS:Parent:updateMatchesCount", data);
557 async getPreferences(prefs, sendResponse) {
558 const browserPrefs = await this.getBrowserPrefs();
560 var defaultBranch = Services.prefs.getDefaultBranch("pdfjs.");
561 var currentPrefs = {},
563 for (var key in prefs) {
564 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
566 "getPreferences - Exceeded the maximum number of preferences " +
567 "that is allowed to be fetched at once."
570 } else if (!defaultBranch.getPrefType(key)) {
573 const prefName = `pdfjs.${key}`,
574 prefValue = prefs[key];
575 switch (typeof prefValue) {
577 currentPrefs[key] = Services.prefs.getBoolPref(prefName, prefValue);
580 currentPrefs[key] = Services.prefs.getIntPref(prefName, prefValue);
583 // The URL contains some dynamic values (%VERSION%, ...), so we need to
586 key === "altTextLearnMoreUrl"
587 ? Services.urlFormatter.formatURLPref(prefName)
588 : Services.prefs.getStringPref(prefName, prefValue);
599 async setPreferences(data, sendResponse) {
600 const actor = getActor(this.domWindow);
601 await actor?.sendQuery("PDFJS:Parent:setPreferences", data);
607 * Set the different editor states in order to be able to update the context
609 * @param {Object} details
611 updateEditorStates({ details }) {
612 const doc = this.domWindow.document;
613 if (!doc.editorStates) {
617 hasSomethingToUndo: false,
618 hasSomethingToRedo: false,
619 hasSelectedEditor: false,
620 hasSelectedText: false,
623 const { editorStates } = doc;
624 for (const [key, value] of Object.entries(details)) {
625 if (typeof value === "boolean" && key in editorStates) {
626 editorStates[key] = value;
633 * This is for range requests.
635 class RangedChromeActions extends ChromeActions {
638 contentDispositionFilename,
644 super(domWindow, contentDispositionFilename);
645 this.dataListener = dataListener;
646 this.originalRequest = originalRequest;
647 this.rangeEnabled = rangeEnabled;
648 this.streamingEnabled = streamingEnabled;
650 this.pdfUrl = originalRequest.URI.spec;
651 this.contentLength = originalRequest.contentLength;
653 // Pass all the headers from the original request through
654 var httpHeaderVisitor = {
656 visitHeader(aHeader, aValue) {
657 if (aHeader === "Range") {
658 // When loading the PDF from cache, firefox seems to set the Range
659 // request header to fetch only the unfetched portions of the file
660 // (e.g. 'Range: bytes=1024-'). However, we want to set this header
661 // manually to fetch the PDF in chunks.
664 this.headers[aHeader] = aValue;
667 if (originalRequest.visitRequestHeaders) {
668 originalRequest.visitRequestHeaders(httpHeaderVisitor);
672 var xhr_onreadystatechange = function xhr_onreadystatechange() {
673 if (this.readyState === 1) {
675 var netChannel = this.channel;
676 // override this XMLHttpRequest's OriginAttributes with our cached parent window's
677 // OriginAttributes, as we are currently running under the SystemPrincipal
678 this.setOriginAttributes(self.getWindowOriginAttributes());
680 "nsIPrivateBrowsingChannel" in Ci &&
681 netChannel instanceof Ci.nsIPrivateBrowsingChannel
683 var docIsPrivate = self.isInPrivateBrowsing();
684 netChannel.setPrivate(docIsPrivate);
688 var getXhr = function getXhr() {
689 var xhr = new XMLHttpRequest({ mozAnon: false });
690 xhr.addEventListener("readystatechange", xhr_onreadystatechange);
694 this.networkManager = new lazy.NetworkManager(this.pdfUrl, {
695 httpHeaders: httpHeaderVisitor.headers,
699 // If we are in range request mode, this means we manually issued xhr
700 // requests, which we need to abort when we leave the page
701 domWindow.addEventListener("unload", function unload(e) {
702 domWindow.removeEventListener(e.type, unload);
707 initPassiveLoading() {
709 if (!this.streamingEnabled) {
710 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
711 this.originalRequest = null;
712 data = this.dataListener.readData();
713 done = this.dataListener.isDone;
714 this.dataListener = null;
716 data = this.dataListener.readData();
717 done = this.dataListener.isDone;
719 this.dataListener.onprogress = (loaded, total) => {
720 const chunk = this.dataListener.readData();
722 this.domWindow.postMessage(
724 pdfjsLoadAction: "progressiveRead",
730 chunk ? [chunk.buffer] : undefined
733 this.dataListener.oncomplete = () => {
734 if (!done && this.dataListener.isDone) {
735 this.domWindow.postMessage(
737 pdfjsLoadAction: "progressiveDone",
742 this.dataListener = null;
746 this.domWindow.postMessage(
748 pdfjsLoadAction: "supportsRangedLoading",
749 rangeEnabled: this.rangeEnabled,
750 streamingEnabled: this.streamingEnabled,
752 length: this.contentLength,
755 filename: this.contentDispositionFilename,
758 data ? [data.buffer] : undefined
764 requestDataRange(args) {
765 if (!this.rangeEnabled) {
769 var begin = args.begin;
771 var domWindow = this.domWindow;
772 // TODO(mack): Support error handler. We're not currently not handling
773 // errors from chrome code for non-range requests, so this doesn't
775 this.networkManager.requestRange(begin, end, {
776 onDone: function RangedChromeActions_onDone({ begin, chunk }) {
777 domWindow.postMessage(
779 pdfjsLoadAction: "range",
784 chunk ? [chunk.buffer] : undefined
787 onProgress: function RangedChromeActions_onProgress(evt) {
788 domWindow.postMessage(
790 pdfjsLoadAction: "rangeProgress",
800 this.networkManager.abortAllRequests();
801 if (this.originalRequest) {
802 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
803 this.originalRequest = null;
805 this.dataListener = null;
810 * This is for a single network stream.
812 class StandardChromeActions extends ChromeActions {
815 contentDispositionFilename,
819 super(domWindow, contentDispositionFilename);
820 this.originalRequest = originalRequest;
821 this.dataListener = dataListener;
824 initPassiveLoading() {
825 if (!this.dataListener) {
829 this.dataListener.onprogress = (loaded, total) => {
830 this.domWindow.postMessage(
832 pdfjsLoadAction: "progress",
840 this.dataListener.oncomplete = (data, errorCode) => {
841 this.domWindow.postMessage(
843 pdfjsLoadAction: "complete",
846 filename: this.contentDispositionFilename,
849 data ? [data.buffer] : undefined
852 this.dataListener = null;
853 this.originalRequest = null;
860 if (this.originalRequest) {
861 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
862 this.originalRequest = null;
864 this.dataListener = null;
869 * Event listener to trigger chrome privileged code.
871 class RequestListener {
872 constructor(actions) {
873 this.actions = actions;
876 // Receive an event and (optionally) asynchronously responds.
877 receive({ target, detail }) {
878 const doc = target.ownerDocument;
879 const { action, data, responseExpected } = detail;
881 const actionFn = this.actions[action];
883 log("Unknown action: " + action);
888 if (!responseExpected) {
889 doc.documentElement.removeChild(target);
891 response = function (aResponse) {
893 const listener = doc.createEvent("CustomEvent");
894 const detail = Cu.cloneInto({ response: aResponse }, doc.defaultView);
895 listener.initCustomEvent("pdf.js.response", true, false, detail);
896 return target.dispatchEvent(listener);
898 // doc is no longer accessible because the requestor is already
899 // gone. unloaded content cannot receive the response anyway.
904 actionFn.call(this.actions, data, response);
908 export function PdfStreamConverter() {}
910 PdfStreamConverter.prototype = {
911 QueryInterface: ChromeUtils.generateQI([
912 "nsIStreamConverter",
914 "nsIRequestObserver",
918 * This component works as such:
919 * 1. asyncConvertData stores the listener
920 * 2. onStartRequest creates a new channel, streams the viewer
921 * 3. If range requests are supported:
922 * 3.1. Leave the request open until the viewer is ready to switch to
925 * If range rquests are not supported:
926 * 3.1. Read the stream as it's loaded in onDataAvailable to send
929 * The convert function just returns the stream, it's just the synchronous
930 * version of asyncConvertData.
933 // nsIStreamConverter::convert
935 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
938 // nsIStreamConverter::asyncConvertData
939 asyncConvertData(aFromType, aToType, aListener, aCtxt) {
940 if (aCtxt && aCtxt instanceof Ci.nsIChannel) {
941 aCtxt.QueryInterface(Ci.nsIChannel);
943 // We need to check if we're supposed to convert here, because not all
944 // asyncConvertData consumers will call getConvertedType first:
945 this.getConvertedType(aFromType, aCtxt);
947 // Store the listener passed to us
948 this.listener = aListener;
951 _usableHandler(handlerInfo) {
952 let { preferredApplicationHandler } = handlerInfo;
954 !preferredApplicationHandler ||
955 !(preferredApplicationHandler instanceof Ci.nsILocalHandlerApp)
959 preferredApplicationHandler.QueryInterface(Ci.nsILocalHandlerApp);
960 // We have an app, grab the executable
961 let { executable } = preferredApplicationHandler;
965 return !executable.equals(lazy.gOurBinary);
969 * Check if the user wants to use PDF.js. Returns true if PDF.js should
970 * handle PDFs, and false if not. Will always return true on non-parent
973 * If the user has selected to open PDFs with a helper app, and we are that
974 * helper app, or if the user has selected the OS default, and we are that
975 * OS default, reset the preference back to pdf.js .
978 _validateAndMaybeUpdatePDFPrefs() {
979 let { processType, PROCESS_TYPE_DEFAULT } = Services.appinfo;
980 // If we're not in the parent, or are the default, then just say yes.
981 if (processType != PROCESS_TYPE_DEFAULT || lazy.PdfJs.cachedIsDefault()) {
982 return { shouldOpen: true };
985 // OK, PDF.js might not be the default. Find out if we've misled the user
986 // into making Firefox an external handler or if we're the OS default and
987 // Firefox is set to use the OS default:
988 let mime = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, "pdf");
989 // The above might throw errors. We're deliberately letting those bubble
990 // back up, where they'll tell the stream converter not to use us.
993 // This shouldn't happen, but we can't fix what isn't there. Assume
994 // we're OK to handle with PDF.js
995 return { shouldOpen: true };
998 const { saveToDisk, useHelperApp, useSystemDefault } = Ci.nsIHandlerInfo;
999 let { preferredAction, alwaysAskBeforeHandling } = mime;
1000 // return this info so getConvertedType can use it.
1001 let rv = { alwaysAskBeforeHandling, shouldOpen: false };
1002 // If the user has indicated they want to be asked or want to save to
1003 // disk, we shouldn't render inline immediately:
1004 if (alwaysAskBeforeHandling || preferredAction == saveToDisk) {
1007 // If we have usable helper app info, don't use PDF.js
1008 if (preferredAction == useHelperApp && this._usableHandler(mime)) {
1011 // If we want the OS default and that's not Firefox, don't use PDF.js
1012 if (preferredAction == useSystemDefault && !mime.isCurrentAppOSDefault()) {
1015 rv.shouldOpen = true;
1016 // Log that we're doing this to help debug issues if people end up being
1017 // surprised by this behaviour.
1018 console.error("Found unusable PDF preferences. Fixing back to PDF.js");
1020 mime.preferredAction = Ci.nsIHandlerInfo.handleInternally;
1021 mime.alwaysAskBeforeHandling = false;
1022 Svc.handlers.store(mime);
1026 getConvertedType(aFromType, aChannel) {
1027 if (aChannel instanceof Ci.nsIMultiPartChannel) {
1028 throw new Components.Exception(
1029 "PDF.js doesn't support multipart responses.",
1030 Cr.NS_ERROR_NOT_IMPLEMENTED
1034 const HTML = "text/html";
1035 let channelURI = aChannel?.URI;
1036 // We can be invoked for application/octet-stream; check if we want the
1038 if (aFromType != "application/pdf") {
1039 // Check if the filename has a PDF extension.
1042 isPDF = aChannel.contentDispositionFilename.endsWith(".pdf");
1046 channelURI?.QueryInterface(Ci.nsIURL).fileExtension.toLowerCase() ==
1050 let browsingContext = aChannel?.loadInfo.targetBrowsingContext;
1051 let toplevelOctetStream =
1052 aFromType == "application/octet-stream" &&
1054 !browsingContext.parent;
1057 !toplevelOctetStream ||
1058 !Services.prefs.getBoolPref("pdfjs.handleOctetStream", false)
1060 throw new Components.Exception(
1061 "Ignore PDF.js for this download.",
1065 // fall through, this appears to be a pdf.
1068 let { alwaysAskBeforeHandling, shouldOpen } =
1069 this._validateAndMaybeUpdatePDFPrefs();
1074 // Hm, so normally, no pdfjs. However... if this is a file: channel there
1075 // are some edge-cases.
1076 if (channelURI?.schemeIs("file")) {
1077 // If we're loaded with system principal, we were likely handed the PDF
1078 // by the OS or directly from the URL bar. Assume we should load it:
1079 let triggeringPrincipal = aChannel.loadInfo?.triggeringPrincipal;
1080 if (triggeringPrincipal?.isSystemPrincipal) {
1084 // If we're loading from a file: link, load it in PDF.js unless the user
1085 // has told us they always want to open/save PDFs.
1086 // This is because handing off the choice to open in Firefox itself
1087 // through the dialog doesn't work properly and making it work is
1088 // non-trivial (see https://bugzilla.mozilla.org/show_bug.cgi?id=1680147#c3 )
1089 // - and anyway, opening the file is what we do for *all*
1090 // other file types we handle internally (and users can then use other UI
1091 // to save or open it with other apps from there).
1092 if (triggeringPrincipal?.schemeIs("file") && alwaysAskBeforeHandling) {
1097 throw new Components.Exception("Can't use PDF.js", Cr.NS_ERROR_FAILURE);
1100 // nsIStreamListener::onDataAvailable
1101 onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
1102 if (!this.dataListener) {
1106 var binaryStream = this.binaryStream;
1107 binaryStream.setInputStream(aInputStream);
1108 let chunk = new ArrayBuffer(aCount);
1109 binaryStream.readArrayBuffer(aCount, chunk);
1110 this.dataListener.append(new Uint8Array(chunk));
1113 // nsIRequestObserver::onStartRequest
1114 onStartRequest(aRequest) {
1115 // Setup the request so we can use it below.
1116 var isHttpRequest = false;
1118 aRequest.QueryInterface(Ci.nsIHttpChannel);
1119 isHttpRequest = true;
1122 var rangeRequest = false;
1123 var streamRequest = false;
1124 if (isHttpRequest) {
1125 var contentEncoding = "identity";
1127 contentEncoding = aRequest.getResponseHeader("Content-Encoding");
1132 acceptRanges = aRequest.getResponseHeader("Accept-Ranges");
1135 var hash = aRequest.URI.ref;
1136 const isPDFBugEnabled = Services.prefs.getBoolPref(
1137 "pdfjs.pdfBugEnabled",
1141 contentEncoding === "identity" &&
1142 acceptRanges === "bytes" &&
1143 aRequest.contentLength >= 0 &&
1144 !Services.prefs.getBoolPref("pdfjs.disableRange", false) &&
1145 (!isPDFBugEnabled || !hash.toLowerCase().includes("disablerange=true"));
1147 contentEncoding === "identity" &&
1148 aRequest.contentLength >= 0 &&
1149 !Services.prefs.getBoolPref("pdfjs.disableStream", false) &&
1150 (!isPDFBugEnabled ||
1151 !hash.toLowerCase().includes("disablestream=true"));
1154 aRequest.QueryInterface(Ci.nsIChannel);
1156 aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
1158 var contentDispositionFilename;
1160 contentDispositionFilename = aRequest.contentDispositionFilename;
1164 contentDispositionFilename &&
1165 !/\.pdf$/i.test(contentDispositionFilename)
1167 contentDispositionFilename += ".pdf";
1170 // Change the content type so we don't get stuck in a loop.
1171 aRequest.setProperty("contentType", aRequest.contentType);
1172 aRequest.contentType = "text/html";
1173 if (isHttpRequest) {
1174 // We trust PDF viewer, using no CSP
1175 aRequest.setResponseHeader("Content-Security-Policy", "", false);
1176 aRequest.setResponseHeader(
1177 "Content-Security-Policy-Report-Only",
1181 // The viewer does not need to handle HTTP Refresh header.
1182 aRequest.setResponseHeader("Refresh", "", false);
1185 lazy.PdfJsTelemetryContent.onViewerIsUsed();
1187 // The document will be loaded via the stream converter as html,
1188 // but since we may have come here via a download or attachment
1189 // that was opened directly, force the content disposition to be
1190 // inline so that the html document will be loaded normally instead
1191 // of going to the helper service.
1192 aRequest.contentDisposition = Ci.nsIChannel.DISPOSITION_FORCE_INLINE;
1194 // Creating storage for PDF data
1195 var contentLength = aRequest.contentLength;
1196 this.dataListener = new PdfDataListener(contentLength);
1197 this.binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
1198 Ci.nsIBinaryInputStream
1201 // Create a new channel that is viewer loaded as a resource.
1202 var channel = lazy.NetUtil.newChannel({
1203 uri: PDF_VIEWER_WEB_PAGE,
1204 loadUsingSystemPrincipal: true,
1207 var listener = this.listener;
1208 var dataListener = this.dataListener;
1209 // Proxy all the request observer calls, when it gets to onStopRequest
1210 // we can get the dom window. We also intentionally pass on the original
1211 // request(aRequest) below so we don't overwrite the original channel and
1212 // trigger an assertion.
1215 listener.onStartRequest(aRequest);
1217 onDataAvailable(request, inputStream, offset, count) {
1218 listener.onDataAvailable(aRequest, inputStream, offset, count);
1220 onStopRequest(request, statusCode) {
1221 var domWindow = getDOMWindow(channel, resourcePrincipal);
1222 if (!Components.isSuccessCode(statusCode) || !domWindow) {
1223 // The request may have been aborted and the document may have been
1224 // replaced with something that is not PDF.js, abort attaching.
1225 listener.onStopRequest(aRequest, statusCode);
1229 if (rangeRequest || streamRequest) {
1230 actions = new RangedChromeActions(
1232 contentDispositionFilename,
1239 actions = new StandardChromeActions(
1241 contentDispositionFilename,
1247 var requestListener = new RequestListener(actions);
1248 domWindow.document.addEventListener(
1251 requestListener.receive(event);
1257 let actor = getActor(domWindow);
1258 actor?.init(actions.supportsIntegratedFind());
1259 actor?.sendAsyncMessage("PDFJS:Parent:recordExposure");
1260 listener.onStopRequest(aRequest, statusCode);
1264 // Keep the URL the same so the browser sees it as the same.
1265 channel.originalURI = aRequest.URI;
1266 channel.loadGroup = aRequest.loadGroup;
1267 channel.loadInfo.originAttributes = aRequest.loadInfo.originAttributes;
1269 // We can use the resource principal when data is fetched by the chrome,
1270 // e.g. useful for NoScript. Make make sure we reuse the origin attributes
1271 // from the request channel to keep isolation consistent.
1272 var uri = lazy.NetUtil.newURI(PDF_VIEWER_WEB_PAGE);
1273 var resourcePrincipal =
1274 Services.scriptSecurityManager.createContentPrincipal(
1276 aRequest.loadInfo.originAttributes
1278 // Remember the principal we would have had before we mess with it.
1279 let originalPrincipal =
1280 Services.scriptSecurityManager.getChannelResultPrincipal(aRequest);
1281 aRequest.owner = resourcePrincipal;
1282 aRequest.setProperty("noPDFJSPrincipal", originalPrincipal);
1284 channel.asyncOpen(proxy);
1287 // nsIRequestObserver::onStopRequest
1288 onStopRequest(aRequest, aStatusCode) {
1289 if (!this.dataListener) {
1294 if (Components.isSuccessCode(aStatusCode)) {
1295 this.dataListener.finish();
1297 this.dataListener.error(aStatusCode);
1299 delete this.dataListener;
1300 delete this.binaryStream;