Bug 1943650 - Command-line --help output misformatted after --dbus-service. r=emilio
[gecko.git] / toolkit / components / pdfjs / content / PdfStreamConverter.sys.mjs
blobf049a3824c13431c38f23494bd3b39eb92e56d3c
1 /* Copyright 2012 Mozilla Foundation
2  *
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
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
14  */
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";
29 const lazy = {};
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",
37 });
39 var Svc = {};
40 XPCOMUtils.defineLazyServiceGetter(
41   Svc,
42   "mime",
43   "@mozilla.org/mime;1",
44   "nsIMIMEService"
46 XPCOMUtils.defineLazyServiceGetter(
47   Svc,
48   "handlers",
49   "@mozilla.org/uriloader/handler-service;1",
50   "nsIHandlerService"
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") {
57     while (file) {
58       if (/\.app\/?$/i.test(file.leafName)) {
59         break;
60       }
61       file = file.parent;
62     }
63   }
64   return file;
65 });
67 function log(aMsg) {
68   if (!Services.prefs.getBoolPref("pdfjs.pdfBugEnabled", false)) {
69     return;
70   }
71   var msg = "PdfStreamConverter.js: " + (aMsg.join ? aMsg.join("") : aMsg);
72   Services.console.logStringMessage(msg);
73   dump(msg + "\n");
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)) {
83     return null;
84   }
85   return win;
88 function getActor(window) {
89   try {
90     const actorName =
91       AppConstants.platform === "android" ? "GeckoViewPdfjs" : "Pdfjs";
92     return window.windowGlobalChild.getActor(actorName);
93   } catch (ex) {
94     return null;
95   }
98 function isValidMatchesCount(data) {
99   if (typeof data !== "object" || data === null) {
100     return false;
101   }
102   const { current, total } = data;
103   if (
104     typeof total !== "number" ||
105     total < 0 ||
106     typeof current !== "number" ||
107     current < 0 ||
108     current > total
109   ) {
110     return false;
111   }
112   return true;
115 // PDF data storage
116 function PdfDataListener(length) {
117   this.length = length; // less than 0, if length is unknown
118   this.buffers = [];
119   this.loaded = 0;
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
130     }
131     this.onprogress(this.loaded, this.length >= 0 ? this.length : void 0);
132   },
133   readData: function PdfDataListener_readData() {
134     if (this.buffers.length === 0) {
135       return null;
136     }
137     if (this.buffers.length === 1) {
138       return this.buffers.pop();
139     }
140     // There are multiple buffers that need to be combined into a single
141     // buffer.
142     let combinedLength = 0;
143     for (let buffer of this.buffers) {
144       combinedLength += buffer.length;
145     }
146     let combinedArray = new Uint8Array(combinedLength);
147     let writeOffset = 0;
148     while (this.buffers.length) {
149       let buffer = this.buffers.shift();
150       combinedArray.set(buffer, writeOffset);
151       writeOffset += buffer.length;
152     }
153     return combinedArray;
154   },
155   get isDone() {
156     return !!this.isDataReady;
157   },
158   finish: function PdfDataListener_finish() {
159     this.isDataReady = true;
160     if (this.oncompleteCallback) {
161       this.oncompleteCallback(this.readData());
162     }
163   },
164   error: function PdfDataListener_error(errorCode) {
165     this.errorCode = errorCode;
166     if (this.oncompleteCallback) {
167       this.oncompleteCallback(null, errorCode);
168     }
169   },
170   onprogress() {},
171   get oncomplete() {
172     return this.oncompleteCallback;
173   },
174   set oncomplete(value) {
175     this.oncompleteCallback = value;
176     if (this.isDataReady) {
177       value(this.readData());
178     }
179     if (this.errorCode) {
180       value(null, this.errorCode);
181     }
182   },
185 class PrefObserver {
186   #domWindow;
188   #prefs = new Map([
189     [
190       caretBrowsingModePref,
191       {
192         name: "supportsCaretBrowsingMode",
193         type: "bool",
194         dispatchToContent: true,
195       },
196     ],
197   ]);
199   constructor(domWindow, isMobile) {
200     this.#domWindow = domWindow;
201     this.#init(isMobile);
202   }
204   #init(isMobile) {
205     if (!isMobile) {
206       this.#prefs.set(toolbarDensityPref, {
207         name: "toolbarDensity",
208         type: "int",
209         dispatchToContent: true,
210       });
211       this.#prefs.set("pdfjs.enableGuessAltText", {
212         name: "enableGuessAltText",
213         type: "bool",
214         dispatchToContent: true,
215         dispatchToParent: true,
216       });
217       this.#prefs.set("pdfjs.enableAltTextModelDownload", {
218         name: "enableAltTextModelDownload",
219         type: "bool",
220         dispatchToContent: true,
221         dispatchToParent: true,
222       });
224       // Once the experiment for new alt-text stuff is removed, we can remove this.
225       this.#prefs.set("pdfjs.enableAltText", {
226         name: "enableAltText",
227         type: "bool",
228         dispatchToParent: true,
229       });
230       this.#prefs.set("pdfjs.enableNewAltTextWhenAddingImage", {
231         name: "enableNewAltTextWhenAddingImage",
232         type: "bool",
233         dispatchToParent: true,
234       });
235       this.#prefs.set("browser.ml.enable", {
236         name: "browser.ml.enable",
237         type: "bool",
238         dispatchToParent: true,
239       });
240     }
241     for (const pref of this.#prefs.keys()) {
242       Services.prefs.addObserver(pref, this, /* aHoldWeak = */ true);
243     }
244   }
246   observe(_aSubject, aTopic, aPrefName) {
247     if (aTopic != "nsPref:changed") {
248       return;
249     }
251     const actor = getActor(this.#domWindow);
252     if (!actor) {
253       return;
254     }
255     const { name, type, dispatchToContent, dispatchToParent } =
256       this.#prefs.get(aPrefName) || {};
257     if (!name) {
258       return;
259     }
260     let value;
261     switch (type) {
262       case "bool": {
263         value = Services.prefs.getBoolPref(aPrefName);
264         break;
265       }
266       case "int": {
267         value = Services.prefs.getIntPref(aPrefName);
268         break;
269       }
270     }
271     const data = { name, value };
272     if (dispatchToContent) {
273       actor.dispatchEvent("updatedPreference", data);
274     }
275     if (dispatchToParent) {
276       actor.sendAsyncMessage("PDFJS:Parent:updatedPreference", data);
277     }
278   }
280   QueryInterface = ChromeUtils.generateQI([Ci.nsISupportsWeakReference]);
284  * All the privileged actions.
285  */
286 class ChromeActions {
287   #allowedGlobalEvents = new Set([
288     "documentloaded",
289     "pagesloaded",
290     "layersloaded",
291     "outlineloaded",
292   ]);
294   constructor(domWindow, contentDispositionFilename) {
295     this.domWindow = domWindow;
296     this.contentDispositionFilename = contentDispositionFilename;
297     this.sandbox = null;
298     this.unloadListener = null;
299     this.observer = new PrefObserver(domWindow, this.isMobile());
300   }
302   createSandbox(data, sendResponse) {
303     function sendResp(res) {
304       if (sendResponse) {
305         sendResponse(res);
306       }
307       return res;
308     }
310     if (!Services.prefs.getBoolPref("pdfjs.enableScripting", false)) {
311       return sendResp(false);
312     }
314     if (this.sandbox !== null) {
315       return sendResp(true);
316     }
318     try {
319       this.sandbox = new lazy.PdfSandbox(this.domWindow, data);
320     } catch (err) {
321       // If there's an error here, it means that something is really wrong
322       // on pdf.js side during sandbox initialization phase.
323       console.error(err);
324       return sendResp(false);
325     }
327     this.unloadListener = () => {
328       this.destroySandbox();
329     };
330     this.domWindow.addEventListener("unload", this.unloadListener);
332     return sendResp(true);
333   }
335   dispatchEventInSandbox(event) {
336     if (this.sandbox) {
337       this.sandbox.dispatchEvent(event);
338     }
339   }
341   dispatchAsyncEventInSandbox(event, sendResponse) {
342     this.dispatchEventInSandbox(event);
343     sendResponse();
344   }
346   destroySandbox() {
347     if (this.sandbox) {
348       this.domWindow.removeEventListener("unload", this.unloadListener);
349       this.sandbox.destroy();
350       this.sandbox = null;
351     }
352   }
354   isInPrivateBrowsing() {
355     return lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow);
356   }
358   getWindowOriginAttributes() {
359     try {
360       return this.domWindow.document.nodePrincipal.originAttributes;
361     } catch (err) {
362       return {};
363     }
364   }
366   async mlDelete(data, sendResponse) {
367     const actor = getActor(this.domWindow);
368     if (!actor) {
369       sendResponse(null);
370       return;
371     }
372     const response = await actor.sendQuery("PDFJS:Parent:mlDelete", data);
373     sendResponse(response);
374   }
376   async mlGuess(data, sendResponse) {
377     const actor = getActor(this.domWindow);
378     if (!actor) {
379       sendResponse(null);
380       return;
381     }
382     const response = await actor.sendQuery("PDFJS:Parent:mlGuess", data);
383     sendResponse(response);
384   }
386   async loadAIEngine(data, sendResponse) {
387     const actor = getActor(this.domWindow);
388     if (!actor) {
389       sendResponse(null);
390       return;
391     }
392     sendResponse(await actor.sendQuery("PDFJS:Parent:loadAIEngine", data));
393   }
395   download(data) {
396     const { originalUrl } = data;
397     const blobUrl = data.blobUrl || originalUrl;
398     let { filename } = data;
399     if (
400       typeof filename !== "string" ||
401       (!/\.pdf$/i.test(filename) && !data.isAttachment)
402     ) {
403       filename = "document.pdf";
404     }
405     const actor = getActor(this.domWindow);
406     actor.sendAsyncMessage("PDFJS:Parent:saveURL", {
407       blobUrl,
408       originalUrl,
409       filename,
410     });
411   }
413   getLocaleProperties() {
414     const { requestedLocale, defaultLocale, isAppLocaleRTL } = Services.locale;
415     return {
416       lang: requestedLocale || defaultLocale,
417       isRTL: isAppLocaleRTL,
418     };
419   }
421   supportsIntegratedFind() {
422     // Integrated find is only supported when we're not in a frame
423     return this.domWindow.windowGlobalChild.browsingContext.parent === null;
424   }
426   async getBrowserPrefs() {
427     const isMobile = this.isMobile();
428     const nimbusDataStr = isMobile
429       ? await this.getNimbusExperimentData()
430       : null;
432     return {
433       allowedGlobalEvents: this.#allowedGlobalEvents,
434       canvasMaxAreaInBytes: Services.prefs.getIntPref("gfx.max-alloc-size"),
435       isInAutomation: Cu.isInAutomation,
436       localeProperties: this.getLocaleProperties(),
437       nimbusDataStr,
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
449       ),
450       toolbarDensity: Services.prefs.getIntPref(toolbarDensityPref, 0),
451     };
452   }
454   isMobile() {
455     return AppConstants.platform === "android";
456   }
458   async getNimbusExperimentData() {
459     if (!this.isMobile()) {
460       return null;
461     }
462     const { promise, resolve } = Promise.withResolvers();
464     const actor = getActor(this.domWindow);
465     actor.sendAsyncMessage("PDFJS:Parent:getNimbus");
466     Services.obs.addObserver(
467       {
468         observe(aSubject, aTopic) {
469           if (aTopic === "pdfjs-getNimbus") {
470             Services.obs.removeObserver(this, aTopic);
471             resolve(aSubject && JSON.stringify(aSubject.wrappedJSObject));
472           }
473         },
474       },
475       "pdfjs-getNimbus"
476     );
477     return promise;
478   }
480   async dispatchGlobalEvent({ eventName, detail }) {
481     if (!this.#allowedGlobalEvents.has(eventName)) {
482       return;
483     }
484     const windowUtils = this.domWindow.windowUtils;
485     if (!windowUtils) {
486       return;
487     }
488     const event = new CustomEvent(eventName, {
489       bubbles: true,
490       cancelable: false,
491       detail,
492     });
493     windowUtils.dispatchEventToChromeOnly(this.domWindow, event);
494   }
496   reportTelemetry(data) {
497     const actor = getActor(this.domWindow);
498     actor?.sendAsyncMessage("PDFJS:Parent:reportTelemetry", data);
499   }
501   updateFindControlState(data) {
502     if (!this.supportsIntegratedFind()) {
503       return;
504     }
505     // Verify what we're sending to the findbar.
506     var result = data.result;
507     var findPrevious = data.findPrevious;
508     var findPreviousType = typeof findPrevious;
509     if (
510       typeof result !== "number" ||
511       result < 0 ||
512       result > 3 ||
513       (findPreviousType !== "undefined" && findPreviousType !== "boolean")
514     ) {
515       return;
516     }
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;
522     }
523     // Same for the `rawQuery` property.
524     let rawQuery = null;
525     if (typeof data.rawQuery === "string") {
526       rawQuery = data.rawQuery;
527     }
528     // Same for the `entireWord` property.
529     let entireWord = false;
530     if (typeof data.entireWord === "boolean") {
531       entireWord = data.entireWord;
532     }
534     let actor = getActor(this.domWindow);
535     actor?.sendAsyncMessage("PDFJS:Parent:updateControlState", {
536       result,
537       findPrevious,
538       entireWord,
539       matchesCount,
540       rawQuery,
541     });
542   }
544   updateFindMatchesCount(data) {
545     if (!this.supportsIntegratedFind()) {
546       return;
547     }
548     // Verify what we're sending to the findbar.
549     if (!isValidMatchesCount(data)) {
550       return;
551     }
553     let actor = getActor(this.domWindow);
554     actor?.sendAsyncMessage("PDFJS:Parent:updateMatchesCount", data);
555   }
557   async getPreferences(prefs, sendResponse) {
558     const browserPrefs = await this.getBrowserPrefs();
560     var defaultBranch = Services.prefs.getDefaultBranch("pdfjs.");
561     var currentPrefs = {},
562       numberOfPrefs = 0;
563     for (var key in prefs) {
564       if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
565         log(
566           "getPreferences - Exceeded the maximum number of preferences " +
567             "that is allowed to be fetched at once."
568         );
569         break;
570       } else if (!defaultBranch.getPrefType(key)) {
571         continue;
572       }
573       const prefName = `pdfjs.${key}`,
574         prefValue = prefs[key];
575       switch (typeof prefValue) {
576         case "boolean":
577           currentPrefs[key] = Services.prefs.getBoolPref(prefName, prefValue);
578           break;
579         case "number":
580           currentPrefs[key] = Services.prefs.getIntPref(prefName, prefValue);
581           break;
582         case "string":
583           // The URL contains some dynamic values (%VERSION%, ...), so we need to
584           // format it.
585           currentPrefs[key] =
586             key === "altTextLearnMoreUrl"
587               ? Services.urlFormatter.formatURLPref(prefName)
588               : Services.prefs.getStringPref(prefName, prefValue);
589           break;
590       }
591     }
593     sendResponse({
594       browserPrefs,
595       prefs: currentPrefs,
596     });
597   }
599   async setPreferences(data, sendResponse) {
600     const actor = getActor(this.domWindow);
601     await actor?.sendQuery("PDFJS:Parent:setPreferences", data);
603     sendResponse(null);
604   }
606   /**
607    * Set the different editor states in order to be able to update the context
608    * menu.
609    * @param {Object} details
610    */
611   updateEditorStates({ details }) {
612     const doc = this.domWindow.document;
613     if (!doc.editorStates) {
614       doc.editorStates = {
615         isEditing: false,
616         isEmpty: true,
617         hasSomethingToUndo: false,
618         hasSomethingToRedo: false,
619         hasSelectedEditor: false,
620         hasSelectedText: false,
621       };
622     }
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;
627       }
628     }
629   }
633  * This is for range requests.
634  */
635 class RangedChromeActions extends ChromeActions {
636   constructor(
637     domWindow,
638     contentDispositionFilename,
639     originalRequest,
640     rangeEnabled,
641     streamingEnabled,
642     dataListener
643   ) {
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 = {
655       headers: {},
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.
662           return;
663         }
664         this.headers[aHeader] = aValue;
665       },
666     };
667     if (originalRequest.visitRequestHeaders) {
668       originalRequest.visitRequestHeaders(httpHeaderVisitor);
669     }
671     var self = this;
672     var xhr_onreadystatechange = function xhr_onreadystatechange() {
673       if (this.readyState === 1) {
674         // LOADING
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());
679         if (
680           "nsIPrivateBrowsingChannel" in Ci &&
681           netChannel instanceof Ci.nsIPrivateBrowsingChannel
682         ) {
683           var docIsPrivate = self.isInPrivateBrowsing();
684           netChannel.setPrivate(docIsPrivate);
685         }
686       }
687     };
688     var getXhr = function getXhr() {
689       var xhr = new XMLHttpRequest({ mozAnon: false });
690       xhr.addEventListener("readystatechange", xhr_onreadystatechange);
691       return xhr;
692     };
694     this.networkManager = new lazy.NetworkManager(this.pdfUrl, {
695       httpHeaders: httpHeaderVisitor.headers,
696       getXhr,
697     });
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);
703       self.abortLoading();
704     });
705   }
707   initPassiveLoading() {
708     let data, done;
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;
715     } else {
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(
723           {
724             pdfjsLoadAction: "progressiveRead",
725             loaded,
726             total,
727             chunk,
728           },
729           PDF_VIEWER_ORIGIN,
730           chunk ? [chunk.buffer] : undefined
731         );
732       };
733       this.dataListener.oncomplete = () => {
734         if (!done && this.dataListener.isDone) {
735           this.domWindow.postMessage(
736             {
737               pdfjsLoadAction: "progressiveDone",
738             },
739             PDF_VIEWER_ORIGIN
740           );
741         }
742         this.dataListener = null;
743       };
744     }
746     this.domWindow.postMessage(
747       {
748         pdfjsLoadAction: "supportsRangedLoading",
749         rangeEnabled: this.rangeEnabled,
750         streamingEnabled: this.streamingEnabled,
751         pdfUrl: this.pdfUrl,
752         length: this.contentLength,
753         data,
754         done,
755         filename: this.contentDispositionFilename,
756       },
757       PDF_VIEWER_ORIGIN,
758       data ? [data.buffer] : undefined
759     );
761     return true;
762   }
764   requestDataRange(args) {
765     if (!this.rangeEnabled) {
766       return;
767     }
769     var begin = args.begin;
770     var end = args.end;
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
774     // seem high-pri
775     this.networkManager.requestRange(begin, end, {
776       onDone: function RangedChromeActions_onDone({ begin, chunk }) {
777         domWindow.postMessage(
778           {
779             pdfjsLoadAction: "range",
780             begin,
781             chunk,
782           },
783           PDF_VIEWER_ORIGIN,
784           chunk ? [chunk.buffer] : undefined
785         );
786       },
787       onProgress: function RangedChromeActions_onProgress(evt) {
788         domWindow.postMessage(
789           {
790             pdfjsLoadAction: "rangeProgress",
791             loaded: evt.loaded,
792           },
793           PDF_VIEWER_ORIGIN
794         );
795       },
796     });
797   }
799   abortLoading() {
800     this.networkManager.abortAllRequests();
801     if (this.originalRequest) {
802       this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
803       this.originalRequest = null;
804     }
805     this.dataListener = null;
806   }
810  * This is for a single network stream.
811  */
812 class StandardChromeActions extends ChromeActions {
813   constructor(
814     domWindow,
815     contentDispositionFilename,
816     originalRequest,
817     dataListener
818   ) {
819     super(domWindow, contentDispositionFilename);
820     this.originalRequest = originalRequest;
821     this.dataListener = dataListener;
822   }
824   initPassiveLoading() {
825     if (!this.dataListener) {
826       return false;
827     }
829     this.dataListener.onprogress = (loaded, total) => {
830       this.domWindow.postMessage(
831         {
832           pdfjsLoadAction: "progress",
833           loaded,
834           total,
835         },
836         PDF_VIEWER_ORIGIN
837       );
838     };
840     this.dataListener.oncomplete = (data, errorCode) => {
841       this.domWindow.postMessage(
842         {
843           pdfjsLoadAction: "complete",
844           data,
845           errorCode,
846           filename: this.contentDispositionFilename,
847         },
848         PDF_VIEWER_ORIGIN,
849         data ? [data.buffer] : undefined
850       );
852       this.dataListener = null;
853       this.originalRequest = null;
854     };
856     return true;
857   }
859   abortLoading() {
860     if (this.originalRequest) {
861       this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
862       this.originalRequest = null;
863     }
864     this.dataListener = null;
865   }
869  * Event listener to trigger chrome privileged code.
870  */
871 class RequestListener {
872   constructor(actions) {
873     this.actions = actions;
874   }
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];
882     if (!actionFn) {
883       log("Unknown action: " + action);
884       return;
885     }
886     let response = null;
888     if (!responseExpected) {
889       doc.documentElement.removeChild(target);
890     } else {
891       response = function (aResponse) {
892         try {
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);
897         } catch (e) {
898           // doc is no longer accessible because the requestor is already
899           // gone. unloaded content cannot receive the response anyway.
900           return false;
901         }
902       };
903     }
904     actionFn.call(this.actions, data, response);
905   }
908 export function PdfStreamConverter() {}
910 PdfStreamConverter.prototype = {
911   QueryInterface: ChromeUtils.generateQI([
912     "nsIStreamConverter",
913     "nsIStreamListener",
914     "nsIRequestObserver",
915   ]),
917   /*
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
923    *           range requests.
924    *
925    *    If range rquests are not supported:
926    *      3.1. Read the stream as it's loaded in onDataAvailable to send
927    *           to the viewer
928    *
929    * The convert function just returns the stream, it's just the synchronous
930    * version of asyncConvertData.
931    */
933   // nsIStreamConverter::convert
934   convert() {
935     throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
936   },
938   // nsIStreamConverter::asyncConvertData
939   asyncConvertData(aFromType, aToType, aListener, aCtxt) {
940     if (aCtxt && aCtxt instanceof Ci.nsIChannel) {
941       aCtxt.QueryInterface(Ci.nsIChannel);
942     }
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;
949   },
951   _usableHandler(handlerInfo) {
952     let { preferredApplicationHandler } = handlerInfo;
953     if (
954       !preferredApplicationHandler ||
955       !(preferredApplicationHandler instanceof Ci.nsILocalHandlerApp)
956     ) {
957       return false;
958     }
959     preferredApplicationHandler.QueryInterface(Ci.nsILocalHandlerApp);
960     // We have an app, grab the executable
961     let { executable } = preferredApplicationHandler;
962     if (!executable) {
963       return false;
964     }
965     return !executable.equals(lazy.gOurBinary);
966   },
968   /*
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
971    * processes.
972    *
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 .
976    *
977    */
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 };
983     }
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.
992     if (!mime) {
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 };
996     }
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) {
1005       return rv;
1006     }
1007     // If we have usable helper app info, don't use PDF.js
1008     if (preferredAction == useHelperApp && this._usableHandler(mime)) {
1009       return rv;
1010     }
1011     // If we want the OS default and that's not Firefox, don't use PDF.js
1012     if (preferredAction == useSystemDefault && !mime.isCurrentAppOSDefault()) {
1013       return rv;
1014     }
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);
1023     return true;
1024   },
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
1031       );
1032     }
1034     const HTML = "text/html";
1035     let channelURI = aChannel?.URI;
1036     // We can be invoked for application/octet-stream; check if we want the
1037     // channel first:
1038     if (aFromType != "application/pdf") {
1039       // Check if the filename has a PDF extension.
1040       let isPDF = false;
1041       try {
1042         isPDF = aChannel.contentDispositionFilename.endsWith(".pdf");
1043       } catch (ex) {}
1044       if (!isPDF) {
1045         isPDF =
1046           channelURI?.QueryInterface(Ci.nsIURL).fileExtension.toLowerCase() ==
1047           "pdf";
1048       }
1050       let browsingContext = aChannel?.loadInfo.targetBrowsingContext;
1051       let toplevelOctetStream =
1052         aFromType == "application/octet-stream" &&
1053         browsingContext &&
1054         !browsingContext.parent;
1055       if (
1056         !isPDF ||
1057         !toplevelOctetStream ||
1058         !Services.prefs.getBoolPref("pdfjs.handleOctetStream", false)
1059       ) {
1060         throw new Components.Exception(
1061           "Ignore PDF.js for this download.",
1062           Cr.NS_ERROR_FAILURE
1063         );
1064       }
1065       // fall through, this appears to be a pdf.
1066     }
1068     let { alwaysAskBeforeHandling, shouldOpen } =
1069       this._validateAndMaybeUpdatePDFPrefs();
1071     if (shouldOpen) {
1072       return HTML;
1073     }
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) {
1081         return HTML;
1082       }
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) {
1093         return HTML;
1094       }
1095     }
1097     throw new Components.Exception("Can't use PDF.js", Cr.NS_ERROR_FAILURE);
1098   },
1100   // nsIStreamListener::onDataAvailable
1101   onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
1102     if (!this.dataListener) {
1103       return;
1104     }
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));
1111   },
1113   // nsIRequestObserver::onStartRequest
1114   onStartRequest(aRequest) {
1115     // Setup the request so we can use it below.
1116     var isHttpRequest = false;
1117     try {
1118       aRequest.QueryInterface(Ci.nsIHttpChannel);
1119       isHttpRequest = true;
1120     } catch (e) {}
1122     var rangeRequest = false;
1123     var streamRequest = false;
1124     if (isHttpRequest) {
1125       var contentEncoding = "identity";
1126       try {
1127         contentEncoding = aRequest.getResponseHeader("Content-Encoding");
1128       } catch (e) {}
1130       var acceptRanges;
1131       try {
1132         acceptRanges = aRequest.getResponseHeader("Accept-Ranges");
1133       } catch (e) {}
1135       var hash = aRequest.URI.ref;
1136       const isPDFBugEnabled = Services.prefs.getBoolPref(
1137         "pdfjs.pdfBugEnabled",
1138         false
1139       );
1140       rangeRequest =
1141         contentEncoding === "identity" &&
1142         acceptRanges === "bytes" &&
1143         aRequest.contentLength >= 0 &&
1144         !Services.prefs.getBoolPref("pdfjs.disableRange", false) &&
1145         (!isPDFBugEnabled || !hash.toLowerCase().includes("disablerange=true"));
1146       streamRequest =
1147         contentEncoding === "identity" &&
1148         aRequest.contentLength >= 0 &&
1149         !Services.prefs.getBoolPref("pdfjs.disableStream", false) &&
1150         (!isPDFBugEnabled ||
1151           !hash.toLowerCase().includes("disablestream=true"));
1152     }
1154     aRequest.QueryInterface(Ci.nsIChannel);
1156     aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
1158     var contentDispositionFilename;
1159     try {
1160       contentDispositionFilename = aRequest.contentDispositionFilename;
1161     } catch (e) {}
1163     if (
1164       contentDispositionFilename &&
1165       !/\.pdf$/i.test(contentDispositionFilename)
1166     ) {
1167       contentDispositionFilename += ".pdf";
1168     }
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",
1178         "",
1179         false
1180       );
1181       // The viewer does not need to handle HTTP Refresh header.
1182       aRequest.setResponseHeader("Refresh", "", false);
1183     }
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
1199     );
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,
1205     });
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.
1213     var proxy = {
1214       onStartRequest() {
1215         listener.onStartRequest(aRequest);
1216       },
1217       onDataAvailable(request, inputStream, offset, count) {
1218         listener.onDataAvailable(aRequest, inputStream, offset, count);
1219       },
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);
1226           return;
1227         }
1228         var actions;
1229         if (rangeRequest || streamRequest) {
1230           actions = new RangedChromeActions(
1231             domWindow,
1232             contentDispositionFilename,
1233             aRequest,
1234             rangeRequest,
1235             streamRequest,
1236             dataListener
1237           );
1238         } else {
1239           actions = new StandardChromeActions(
1240             domWindow,
1241             contentDispositionFilename,
1242             aRequest,
1243             dataListener
1244           );
1245         }
1247         var requestListener = new RequestListener(actions);
1248         domWindow.document.addEventListener(
1249           PDFJS_EVENT_ID,
1250           function (event) {
1251             requestListener.receive(event);
1252           },
1253           false,
1254           true
1255         );
1257         let actor = getActor(domWindow);
1258         actor?.init(actions.supportsIntegratedFind());
1259         actor?.sendAsyncMessage("PDFJS:Parent:recordExposure");
1260         listener.onStopRequest(aRequest, statusCode);
1261       },
1262     };
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(
1275         uri,
1276         aRequest.loadInfo.originAttributes
1277       );
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);
1285   },
1287   // nsIRequestObserver::onStopRequest
1288   onStopRequest(aRequest, aStatusCode) {
1289     if (!this.dataListener) {
1290       // Do nothing
1291       return;
1292     }
1294     if (Components.isSuccessCode(aStatusCode)) {
1295       this.dataListener.finish();
1296     } else {
1297       this.dataListener.error(aStatusCode);
1298     }
1299     delete this.dataListener;
1300     delete this.binaryStream;
1301   },