forked from mirrors/gecko-dev
		
	 80327d3561
			
		
	
	
		80327d3561
		
	
	
	
	
		
			
			Differential Revision: https://phabricator.services.mozilla.com/D3729 --HG-- extra : rebase_source : e187b8e9a6b6db7ebc762adda5e489b25c7a7e43 extra : histedit_source : 868cb99d09954a51d6be321fcb516475ef70eb33
		
			
				
	
	
		
			987 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			987 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* Copyright 2012 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.
 | |
|  */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ["PdfStreamConverter"];
 | |
| 
 | |
| const PDFJS_EVENT_ID = "pdf.js.message";
 | |
| const PREF_PREFIX = "pdfjs";
 | |
| const PDF_VIEWER_ORIGIN = "resource://pdf.js";
 | |
| const PDF_VIEWER_WEB_PAGE = "resource://pdf.js/web/viewer.html";
 | |
| const MAX_NUMBER_OF_PREFS = 50;
 | |
| const MAX_STRING_PREF_LENGTH = 128;
 | |
| 
 | |
| ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "NetUtil",
 | |
|   "resource://gre/modules/NetUtil.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "NetworkManager",
 | |
|   "resource://pdf.js/PdfJsNetwork.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
 | |
|   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "PdfJsTelemetry",
 | |
|   "resource://pdf.js/PdfJsTelemetry.jsm");
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "PdfjsContentUtils",
 | |
|   "resource://pdf.js/PdfjsContentUtils.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest"]);
 | |
| 
 | |
| var Svc = {};
 | |
| XPCOMUtils.defineLazyServiceGetter(Svc, "mime",
 | |
|                                    "@mozilla.org/mime;1",
 | |
|                                    "nsIMIMEService");
 | |
| 
 | |
| function getBoolPref(pref, def) {
 | |
|   try {
 | |
|     return Services.prefs.getBoolPref(pref);
 | |
|   } catch (ex) {
 | |
|     return def;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getIntPref(pref, def) {
 | |
|   try {
 | |
|     return Services.prefs.getIntPref(pref);
 | |
|   } catch (ex) {
 | |
|     return def;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getStringPref(pref, def) {
 | |
|   try {
 | |
|     return Services.prefs.getStringPref(pref);
 | |
|   } catch (ex) {
 | |
|     return def;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function log(aMsg) {
 | |
|   if (!getBoolPref(PREF_PREFIX + ".pdfBugEnabled", false)) {
 | |
|     return;
 | |
|   }
 | |
|   var msg = "PdfStreamConverter.js: " + (aMsg.join ? aMsg.join("") : aMsg);
 | |
|   Services.console.logStringMessage(msg);
 | |
|   dump(msg + "\n");
 | |
| }
 | |
| 
 | |
| function getDOMWindow(aChannel, aPrincipal) {
 | |
|   var requestor = aChannel.notificationCallbacks ?
 | |
|                   aChannel.notificationCallbacks :
 | |
|                   aChannel.loadGroup.notificationCallbacks;
 | |
|   var win = requestor.getInterface(Ci.nsIDOMWindow);
 | |
|   // Ensure the window wasn't navigated to something that is not PDF.js.
 | |
|   if (!win.document.nodePrincipal.equals(aPrincipal)) {
 | |
|     return null;
 | |
|   }
 | |
|   return win;
 | |
| }
 | |
| 
 | |
| function getLocalizedStrings(path) {
 | |
|   var stringBundle =
 | |
|     Services.strings.createBundle("chrome://pdf.js/locale/" + path);
 | |
| 
 | |
|   var map = {};
 | |
|   for (let string of stringBundle.getSimpleEnumeration()) {
 | |
|     var key = string.key, property = "textContent";
 | |
|     var i = key.lastIndexOf(".");
 | |
|     if (i >= 0) {
 | |
|       property = key.substring(i + 1);
 | |
|       key = key.substring(0, i);
 | |
|     }
 | |
|     if (!(key in map)) {
 | |
|       map[key] = {};
 | |
|     }
 | |
|     map[key][property] = string.value;
 | |
|   }
 | |
|   return map;
 | |
| }
 | |
| function getLocalizedString(strings, id, property) {
 | |
|   property = property || "textContent";
 | |
|   if (id in strings) {
 | |
|     return strings[id][property];
 | |
|   }
 | |
|   return id;
 | |
| }
 | |
| 
 | |
| // PDF data storage
 | |
| function PdfDataListener(length) {
 | |
|   this.length = length; // less than 0, if length is unknown
 | |
|   this.buffers = [];
 | |
|   this.loaded = 0;
 | |
| }
 | |
| 
 | |
| PdfDataListener.prototype = {
 | |
|   append: function PdfDataListener_append(chunk) {
 | |
|     // In most of the cases we will pass data as we receive it, but at the
 | |
|     // beginning of the loading we may accumulate some data.
 | |
|     this.buffers.push(chunk);
 | |
|     this.loaded += chunk.length;
 | |
|     if (this.length >= 0 && this.length < this.loaded) {
 | |
|       this.length = -1; // reset the length, server is giving incorrect one
 | |
|     }
 | |
|     this.onprogress(this.loaded, this.length >= 0 ? this.length : void 0);
 | |
|   },
 | |
|   readData: function PdfDataListener_readData() {
 | |
|     if (this.buffers.length === 0) {
 | |
|       return null;
 | |
|     }
 | |
|     if (this.buffers.length === 1) {
 | |
|       return this.buffers.pop();
 | |
|     }
 | |
|     // There are multiple buffers that need to be combined into a single
 | |
|     // buffer.
 | |
|     let combinedLength = 0;
 | |
|     for (let buffer of this.buffers) {
 | |
|       combinedLength += buffer.length;
 | |
|     }
 | |
|     let combinedArray = new Uint8Array(combinedLength);
 | |
|     let writeOffset = 0;
 | |
|     while (this.buffers.length) {
 | |
|       let buffer = this.buffers.shift();
 | |
|       combinedArray.set(buffer, writeOffset);
 | |
|       writeOffset += buffer.length;
 | |
|     }
 | |
|     return combinedArray;
 | |
|   },
 | |
|   finish: function PdfDataListener_finish() {
 | |
|     this.isDataReady = true;
 | |
|     if (this.oncompleteCallback) {
 | |
|       this.oncompleteCallback(this.readData());
 | |
|     }
 | |
|   },
 | |
|   error: function PdfDataListener_error(errorCode) {
 | |
|     this.errorCode = errorCode;
 | |
|     if (this.oncompleteCallback) {
 | |
|       this.oncompleteCallback(null, errorCode);
 | |
|     }
 | |
|   },
 | |
|   onprogress() {},
 | |
|   get oncomplete() {
 | |
|     return this.oncompleteCallback;
 | |
|   },
 | |
|   set oncomplete(value) {
 | |
|     this.oncompleteCallback = value;
 | |
|     if (this.isDataReady) {
 | |
|       value(this.readData());
 | |
|     }
 | |
|     if (this.errorCode) {
 | |
|       value(null, this.errorCode);
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * All the privileged actions.
 | |
|  */
 | |
| class ChromeActions {
 | |
|   constructor(domWindow, contentDispositionFilename) {
 | |
|     this.domWindow = domWindow;
 | |
|     this.contentDispositionFilename = contentDispositionFilename;
 | |
|     this.telemetryState = {
 | |
|       documentInfo: false,
 | |
|       firstPageInfo: false,
 | |
|       streamTypesUsed: [],
 | |
|       fontTypesUsed: [],
 | |
|       startAt: Date.now(),
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   isInPrivateBrowsing() {
 | |
|     return PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow);
 | |
|   }
 | |
| 
 | |
|   download(data, sendResponse) {
 | |
|     var self = this;
 | |
|     var originalUrl = data.originalUrl;
 | |
|     var blobUrl = data.blobUrl || originalUrl;
 | |
|     // The data may not be downloaded so we need just retry getting the pdf with
 | |
|     // the original url.
 | |
|     var originalUri = NetUtil.newURI(originalUrl);
 | |
|     var filename = data.filename;
 | |
|     if (typeof filename !== "string" ||
 | |
|         (!/\.pdf$/i.test(filename) && !data.isAttachment)) {
 | |
|       filename = "document.pdf";
 | |
|     }
 | |
|     var blobUri = NetUtil.newURI(blobUrl);
 | |
|     var extHelperAppSvc =
 | |
|           Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
 | |
|              getService(Ci.nsIExternalHelperAppService);
 | |
| 
 | |
|     var docIsPrivate = this.isInPrivateBrowsing();
 | |
|     var netChannel = NetUtil.newChannel({
 | |
|       uri: blobUri,
 | |
|       loadUsingSystemPrincipal: true,
 | |
|     });
 | |
|     if ("nsIPrivateBrowsingChannel" in Ci &&
 | |
|         netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
 | |
|       netChannel.setPrivate(docIsPrivate);
 | |
|     }
 | |
|     NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) {
 | |
|       if (!Components.isSuccessCode(aResult)) {
 | |
|         if (sendResponse) {
 | |
|           sendResponse(true);
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
|       // Create a nsIInputStreamChannel so we can set the url on the channel
 | |
|       // so the filename will be correct.
 | |
|       var channel = Cc["@mozilla.org/network/input-stream-channel;1"].
 | |
|                        createInstance(Ci.nsIInputStreamChannel);
 | |
|       channel.QueryInterface(Ci.nsIChannel);
 | |
|       try {
 | |
|         // contentDisposition/contentDispositionFilename is readonly before FF18
 | |
|         channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
 | |
|         if (self.contentDispositionFilename && !data.isAttachment) {
 | |
|           channel.contentDispositionFilename = self.contentDispositionFilename;
 | |
|         } else {
 | |
|           channel.contentDispositionFilename = filename;
 | |
|         }
 | |
|       } catch (e) {}
 | |
|       channel.setURI(originalUri);
 | |
|       channel.loadInfo = netChannel.loadInfo;
 | |
|       channel.contentStream = aInputStream;
 | |
|       if ("nsIPrivateBrowsingChannel" in Ci &&
 | |
|           channel instanceof Ci.nsIPrivateBrowsingChannel) {
 | |
|         channel.setPrivate(docIsPrivate);
 | |
|       }
 | |
| 
 | |
|       var listener = {
 | |
|         extListener: null,
 | |
|         onStartRequest(aRequest, aContext) {
 | |
|           var loadContext = self.domWindow.docShell
 | |
|                                 .QueryInterface(Ci.nsILoadContext);
 | |
|           this.extListener = extHelperAppSvc.doContent(
 | |
|             (data.isAttachment ? "application/octet-stream" :
 | |
|                                  "application/pdf"),
 | |
|             aRequest, loadContext, false);
 | |
|           this.extListener.onStartRequest(aRequest, aContext);
 | |
|         },
 | |
|         onStopRequest(aRequest, aContext, aStatusCode) {
 | |
|           if (this.extListener) {
 | |
|             this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
 | |
|           }
 | |
|           // Notify the content code we're done downloading.
 | |
|           if (sendResponse) {
 | |
|             sendResponse(false);
 | |
|           }
 | |
|         },
 | |
|         onDataAvailable(aRequest, aContext, aDataInputStream, aOffset, aCount) {
 | |
|           this.extListener.onDataAvailable(aRequest, aContext, aDataInputStream,
 | |
|                                            aOffset, aCount);
 | |
|         },
 | |
|       };
 | |
| 
 | |
|       channel.asyncOpen2(listener);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   getLocale() {
 | |
|     return Services.locale.getRequestedLocale() || "en-US";
 | |
|   }
 | |
| 
 | |
|   getStrings(data) {
 | |
|     try {
 | |
|       // Lazy initialization of localizedStrings
 | |
|       if (!("localizedStrings" in this)) {
 | |
|         this.localizedStrings = getLocalizedStrings("viewer.properties");
 | |
|       }
 | |
|       var result = this.localizedStrings[data];
 | |
|       return JSON.stringify(result || null);
 | |
|     } catch (e) {
 | |
|       log("Unable to retrieve localized strings: " + e);
 | |
|       return "null";
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   supportsIntegratedFind() {
 | |
|     // Integrated find is only supported when we're not in a frame
 | |
|     return this.domWindow.frameElement === null;
 | |
|   }
 | |
| 
 | |
|   supportsDocumentFonts() {
 | |
|     var prefBrowser = getIntPref("browser.display.use_document_fonts", 1);
 | |
|     var prefGfx = getBoolPref("gfx.downloadable_fonts.enabled", true);
 | |
|     return (!!prefBrowser && prefGfx);
 | |
|   }
 | |
| 
 | |
|   supportsDocumentColors() {
 | |
|     return getIntPref("browser.display.document_color_use", 0) !== 2;
 | |
|   }
 | |
| 
 | |
|   supportedMouseWheelZoomModifierKeys() {
 | |
|     return {
 | |
|       ctrlKey: getIntPref("mousewheel.with_control.action", 3) === 3,
 | |
|       metaKey: getIntPref("mousewheel.with_meta.action", 1) === 3,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   reportTelemetry(data) {
 | |
|     var probeInfo = JSON.parse(data);
 | |
|     switch (probeInfo.type) {
 | |
|       case "documentInfo":
 | |
|         if (!this.telemetryState.documentInfo) {
 | |
|           PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0);
 | |
|           PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0);
 | |
|           if (probeInfo.formType) {
 | |
|             PdfJsTelemetry.onForm(probeInfo.formType === "acroform");
 | |
|           }
 | |
|           this.telemetryState.documentInfo = true;
 | |
|         }
 | |
|         break;
 | |
|       case "pageInfo":
 | |
|         if (!this.telemetryState.firstPageInfo) {
 | |
|           var duration = Date.now() - this.telemetryState.startAt;
 | |
|           PdfJsTelemetry.onTimeToView(duration);
 | |
|           this.telemetryState.firstPageInfo = true;
 | |
|         }
 | |
|         break;
 | |
|       case "documentStats":
 | |
|         // documentStats can be called several times for one documents.
 | |
|         // if stream/font types are reported, trying not to submit the same
 | |
|         // enumeration value multiple times.
 | |
|         var documentStats = probeInfo.stats;
 | |
|         if (!documentStats || typeof documentStats !== "object") {
 | |
|           break;
 | |
|         }
 | |
|         var i, streamTypes = documentStats.streamTypes;
 | |
|         if (Array.isArray(streamTypes)) {
 | |
|           var STREAM_TYPE_ID_LIMIT = 20;
 | |
|           for (i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
 | |
|             if (streamTypes[i] &&
 | |
|                 !this.telemetryState.streamTypesUsed[i]) {
 | |
|               PdfJsTelemetry.onStreamType(i);
 | |
|               this.telemetryState.streamTypesUsed[i] = true;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         var fontTypes = documentStats.fontTypes;
 | |
|         if (Array.isArray(fontTypes)) {
 | |
|           var FONT_TYPE_ID_LIMIT = 20;
 | |
|           for (i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
 | |
|             if (fontTypes[i] &&
 | |
|                 !this.telemetryState.fontTypesUsed[i]) {
 | |
|               PdfJsTelemetry.onFontType(i);
 | |
|               this.telemetryState.fontTypesUsed[i] = true;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
|       case "print":
 | |
|         PdfJsTelemetry.onPrint();
 | |
|         break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {Object} args - Object with `featureId` and `url` properties.
 | |
|    * @param {function} sendResponse - Callback function.
 | |
|    */
 | |
|   fallback(args, sendResponse) {
 | |
|     var featureId = args.featureId;
 | |
| 
 | |
|     var domWindow = this.domWindow;
 | |
|     var strings = getLocalizedStrings("chrome.properties");
 | |
|     var message;
 | |
|     if (featureId === "forms") {
 | |
|       message = getLocalizedString(strings, "unsupported_feature_forms");
 | |
|     } else {
 | |
|       message = getLocalizedString(strings, "unsupported_feature");
 | |
|     }
 | |
|     PdfJsTelemetry.onFallback();
 | |
|     PdfjsContentUtils.displayWarning(domWindow, message,
 | |
|       getLocalizedString(strings, "open_with_different_viewer"),
 | |
|       getLocalizedString(strings, "open_with_different_viewer", "accessKey"));
 | |
| 
 | |
|     let winmm = domWindow.docShell.messageManager;
 | |
| 
 | |
|     winmm.addMessageListener("PDFJS:Child:fallbackDownload",
 | |
|       function fallbackDownload(msg) {
 | |
|         let data = msg.data;
 | |
|         sendResponse(data.download);
 | |
| 
 | |
|         winmm.removeMessageListener("PDFJS:Child:fallbackDownload",
 | |
|                                     fallbackDownload);
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   updateFindControlState(data) {
 | |
|     if (!this.supportsIntegratedFind()) {
 | |
|       return;
 | |
|     }
 | |
|     // Verify what we're sending to the findbar.
 | |
|     var result = data.result;
 | |
|     var findPrevious = data.findPrevious;
 | |
|     var findPreviousType = typeof findPrevious;
 | |
|     if ((typeof result !== "number" || result < 0 || result > 3) ||
 | |
|         (findPreviousType !== "undefined" && findPreviousType !== "boolean")) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var winmm = this.domWindow.docShell.messageManager;
 | |
| 
 | |
|     winmm.sendAsyncMessage("PDFJS:Parent:updateControlState", data);
 | |
|   }
 | |
| 
 | |
|   setPreferences(prefs, sendResponse) {
 | |
|     var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + ".");
 | |
|     var numberOfPrefs = 0;
 | |
|     var prefValue, prefName;
 | |
|     for (var key in prefs) {
 | |
|       if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
 | |
|         log("setPreferences - Exceeded the maximum number of preferences " +
 | |
|             "that is allowed to be set at once.");
 | |
|         break;
 | |
|       } else if (!defaultBranch.getPrefType(key)) {
 | |
|         continue;
 | |
|       }
 | |
|       prefValue = prefs[key];
 | |
|       prefName = (PREF_PREFIX + "." + key);
 | |
|       switch (typeof prefValue) {
 | |
|         case "boolean":
 | |
|           PdfjsContentUtils.setBoolPref(prefName, prefValue);
 | |
|           break;
 | |
|         case "number":
 | |
|           PdfjsContentUtils.setIntPref(prefName, prefValue);
 | |
|           break;
 | |
|         case "string":
 | |
|           if (prefValue.length > MAX_STRING_PREF_LENGTH) {
 | |
|             log("setPreferences - Exceeded the maximum allowed length " +
 | |
|                 "for a string preference.");
 | |
|           } else {
 | |
|             PdfjsContentUtils.setStringPref(prefName, prefValue);
 | |
|           }
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|     if (sendResponse) {
 | |
|       sendResponse(true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   getPreferences(prefs, sendResponse) {
 | |
|     var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + ".");
 | |
|     var currentPrefs = {}, numberOfPrefs = 0;
 | |
|     var prefValue, prefName;
 | |
|     for (var key in prefs) {
 | |
|       if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
 | |
|         log("getPreferences - Exceeded the maximum number of preferences " +
 | |
|             "that is allowed to be fetched at once.");
 | |
|         break;
 | |
|       } else if (!defaultBranch.getPrefType(key)) {
 | |
|         continue;
 | |
|       }
 | |
|       prefValue = prefs[key];
 | |
|       prefName = (PREF_PREFIX + "." + key);
 | |
|       switch (typeof prefValue) {
 | |
|         case "boolean":
 | |
|           currentPrefs[key] = getBoolPref(prefName, prefValue);
 | |
|           break;
 | |
|         case "number":
 | |
|           currentPrefs[key] = getIntPref(prefName, prefValue);
 | |
|           break;
 | |
|         case "string":
 | |
|           currentPrefs[key] = getStringPref(prefName, prefValue);
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|     let result = JSON.stringify(currentPrefs);
 | |
|     if (sendResponse) {
 | |
|       sendResponse(result);
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This is for range requests.
 | |
|  */
 | |
| class RangedChromeActions extends ChromeActions {
 | |
|   constructor(domWindow, contentDispositionFilename, originalRequest,
 | |
|               rangeEnabled, streamingEnabled, dataListener) {
 | |
| 
 | |
|     super(domWindow, contentDispositionFilename);
 | |
|     this.dataListener = dataListener;
 | |
|     this.originalRequest = originalRequest;
 | |
|     this.rangeEnabled = rangeEnabled;
 | |
|     this.streamingEnabled = streamingEnabled;
 | |
| 
 | |
|     this.pdfUrl = originalRequest.URI.spec;
 | |
|     this.contentLength = originalRequest.contentLength;
 | |
| 
 | |
|     // Pass all the headers from the original request through
 | |
|     var httpHeaderVisitor = {
 | |
|       headers: {},
 | |
|       visitHeader(aHeader, aValue) {
 | |
|         if (aHeader === "Range") {
 | |
|           // When loading the PDF from cache, firefox seems to set the Range
 | |
|           // request header to fetch only the unfetched portions of the file
 | |
|           // (e.g. 'Range: bytes=1024-'). However, we want to set this header
 | |
|           // manually to fetch the PDF in chunks.
 | |
|           return;
 | |
|         }
 | |
|         this.headers[aHeader] = aValue;
 | |
|       },
 | |
|     };
 | |
|     if (originalRequest.visitRequestHeaders) {
 | |
|       originalRequest.visitRequestHeaders(httpHeaderVisitor);
 | |
|     }
 | |
| 
 | |
|     var self = this;
 | |
|     var xhr_onreadystatechange = function xhr_onreadystatechange() {
 | |
|       if (this.readyState === 1) { // LOADING
 | |
|         var netChannel = this.channel;
 | |
|         if ("nsIPrivateBrowsingChannel" in Ci &&
 | |
|             netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
 | |
|           var docIsPrivate = self.isInPrivateBrowsing();
 | |
|           netChannel.setPrivate(docIsPrivate);
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     var getXhr = function getXhr() {
 | |
|       var xhr = new XMLHttpRequest();
 | |
|       xhr.addEventListener("readystatechange", xhr_onreadystatechange);
 | |
|       return xhr;
 | |
|     };
 | |
| 
 | |
|     this.networkManager = new NetworkManager(this.pdfUrl, {
 | |
|       httpHeaders: httpHeaderVisitor.headers,
 | |
|       getXhr,
 | |
|     });
 | |
| 
 | |
|     // If we are in range request mode, this means we manually issued xhr
 | |
|     // requests, which we need to abort when we leave the page
 | |
|     domWindow.addEventListener("unload", function unload(e) {
 | |
|       domWindow.removeEventListener(e.type, unload);
 | |
|       self.abortLoading();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   initPassiveLoading() {
 | |
|     var data;
 | |
|     if (!this.streamingEnabled) {
 | |
|       this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
 | |
|       this.originalRequest = null;
 | |
|       data = this.dataListener.readData();
 | |
|       this.dataListener = null;
 | |
|     } else {
 | |
|       data = this.dataListener.readData();
 | |
| 
 | |
|       this.dataListener.onprogress = (loaded, total) => {
 | |
|         this.domWindow.postMessage({
 | |
|           pdfjsLoadAction: "progressiveRead",
 | |
|           loaded,
 | |
|           total,
 | |
|           chunk: this.dataListener.readData(),
 | |
|         }, PDF_VIEWER_ORIGIN);
 | |
|       };
 | |
|       this.dataListener.oncomplete = () => {
 | |
|         this.dataListener = null;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     this.domWindow.postMessage({
 | |
|       pdfjsLoadAction: "supportsRangedLoading",
 | |
|       rangeEnabled: this.rangeEnabled,
 | |
|       streamingEnabled: this.streamingEnabled,
 | |
|       pdfUrl: this.pdfUrl,
 | |
|       length: this.contentLength,
 | |
|       data,
 | |
|     }, PDF_VIEWER_ORIGIN);
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   requestDataRange(args) {
 | |
|     if (!this.rangeEnabled) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var begin = args.begin;
 | |
|     var end = args.end;
 | |
|     var domWindow = this.domWindow;
 | |
|     // TODO(mack): Support error handler. We're not currently not handling
 | |
|     // errors from chrome code for non-range requests, so this doesn't
 | |
|     // seem high-pri
 | |
|     this.networkManager.requestRange(begin, end, {
 | |
|       onDone: function RangedChromeActions_onDone(aArgs) {
 | |
|         domWindow.postMessage({
 | |
|           pdfjsLoadAction: "range",
 | |
|           begin: aArgs.begin,
 | |
|           chunk: aArgs.chunk,
 | |
|         }, PDF_VIEWER_ORIGIN);
 | |
|       },
 | |
|       onProgress: function RangedChromeActions_onProgress(evt) {
 | |
|         domWindow.postMessage({
 | |
|           pdfjsLoadAction: "rangeProgress",
 | |
|           loaded: evt.loaded,
 | |
|         }, PDF_VIEWER_ORIGIN);
 | |
|       },
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   abortLoading() {
 | |
|     this.networkManager.abortAllRequests();
 | |
|     if (this.originalRequest) {
 | |
|       this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
 | |
|       this.originalRequest = null;
 | |
|     }
 | |
|     this.dataListener = null;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This is for a single network stream.
 | |
|  */
 | |
| class StandardChromeActions extends ChromeActions {
 | |
|   constructor(domWindow, contentDispositionFilename, originalRequest,
 | |
|               dataListener) {
 | |
|     super(domWindow, contentDispositionFilename);
 | |
|     this.originalRequest = originalRequest;
 | |
|     this.dataListener = dataListener;
 | |
|   }
 | |
| 
 | |
|   initPassiveLoading() {
 | |
|     if (!this.dataListener) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     this.dataListener.onprogress = (loaded, total) => {
 | |
|       this.domWindow.postMessage({
 | |
|         pdfjsLoadAction: "progress",
 | |
|         loaded,
 | |
|         total,
 | |
|       }, PDF_VIEWER_ORIGIN);
 | |
|     };
 | |
| 
 | |
|     this.dataListener.oncomplete = (data, errorCode) => {
 | |
|       this.domWindow.postMessage({
 | |
|         pdfjsLoadAction: "complete",
 | |
|         data,
 | |
|         errorCode,
 | |
|       }, PDF_VIEWER_ORIGIN);
 | |
| 
 | |
|       this.dataListener = null;
 | |
|       this.originalRequest = null;
 | |
|     };
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   abortLoading() {
 | |
|     if (this.originalRequest) {
 | |
|       this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
 | |
|       this.originalRequest = null;
 | |
|     }
 | |
|     this.dataListener = null;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Event listener to trigger chrome privileged code.
 | |
|  */
 | |
| class RequestListener {
 | |
|   constructor(actions) {
 | |
|     this.actions = actions;
 | |
|   }
 | |
| 
 | |
|   // Receive an event and synchronously or asynchronously responds.
 | |
|   receive(event) {
 | |
|     var message = event.target;
 | |
|     var doc = message.ownerDocument;
 | |
|     var action = event.detail.action;
 | |
|     var data = event.detail.data;
 | |
|     var sync = event.detail.sync;
 | |
|     var actions = this.actions;
 | |
|     if (!(action in actions)) {
 | |
|       log("Unknown action: " + action);
 | |
|       return;
 | |
|     }
 | |
|     var response;
 | |
|     if (sync) {
 | |
|       response = actions[action].call(this.actions, data);
 | |
|       event.detail.response = Cu.cloneInto(response, doc.defaultView);
 | |
|     } else {
 | |
|       if (!event.detail.responseExpected) {
 | |
|         doc.documentElement.removeChild(message);
 | |
|         response = null;
 | |
|       } else {
 | |
|         response = function sendResponse(aResponse) {
 | |
|           try {
 | |
|             var listener = doc.createEvent("CustomEvent");
 | |
|             let detail = Cu.cloneInto({ response: aResponse, },
 | |
|                                       doc.defaultView);
 | |
|             listener.initCustomEvent("pdf.js.response", true, false, detail);
 | |
|             return message.dispatchEvent(listener);
 | |
|           } catch (e) {
 | |
|             // doc is no longer accessible because the requestor is already
 | |
|             // gone. unloaded content cannot receive the response anyway.
 | |
|             return false;
 | |
|           }
 | |
|         };
 | |
|       }
 | |
|       actions[action].call(this.actions, data, response);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Forwards events from the eventElement to the contentWindow only if the
 | |
|  * content window matches the currently selected browser window.
 | |
|  */
 | |
| class FindEventManager {
 | |
|   constructor(contentWindow) {
 | |
|     this.contentWindow = contentWindow;
 | |
|     this.winmm = contentWindow.docShell.messageManager;
 | |
|   }
 | |
| 
 | |
|   bind() {
 | |
|     var unload = (evt) => {
 | |
|       this.unbind();
 | |
|       this.contentWindow.removeEventListener(evt.type, unload);
 | |
|     };
 | |
|     this.contentWindow.addEventListener("unload", unload);
 | |
| 
 | |
|     // We cannot directly attach listeners to for the find events
 | |
|     // since the FindBar is in the parent process. Instead we're
 | |
|     // asking the PdfjsChromeUtils to do it for us and forward
 | |
|     // all the find events to us.
 | |
|     this.winmm.sendAsyncMessage("PDFJS:Parent:addEventListener");
 | |
|     this.winmm.addMessageListener("PDFJS:Child:handleEvent", this);
 | |
|   }
 | |
| 
 | |
|   receiveMessage(msg) {
 | |
|     var detail = msg.data.detail;
 | |
|     var type = msg.data.type;
 | |
|     var contentWindow = this.contentWindow;
 | |
| 
 | |
|     detail = Cu.cloneInto(detail, contentWindow);
 | |
|     var forward = contentWindow.document.createEvent("CustomEvent");
 | |
|     forward.initCustomEvent(type, true, true, detail);
 | |
|     contentWindow.dispatchEvent(forward);
 | |
|   }
 | |
| 
 | |
|   unbind() {
 | |
|     this.winmm.sendAsyncMessage("PDFJS:Parent:removeEventListener");
 | |
|   }
 | |
| }
 | |
| 
 | |
| function PdfStreamConverter() {
 | |
| }
 | |
| 
 | |
| PdfStreamConverter.prototype = {
 | |
|   QueryInterface: ChromeUtils.generateQI([Ci.nsIStreamConverter, Ci.nsIStreamListener, Ci.nsIRequestObserver]),
 | |
| 
 | |
|   /*
 | |
|    * This component works as such:
 | |
|    * 1. asyncConvertData stores the listener
 | |
|    * 2. onStartRequest creates a new channel, streams the viewer
 | |
|    * 3. If range requests are supported:
 | |
|    *      3.1. Leave the request open until the viewer is ready to switch to
 | |
|    *           range requests.
 | |
|    *
 | |
|    *    If range rquests are not supported:
 | |
|    *      3.1. Read the stream as it's loaded in onDataAvailable to send
 | |
|    *           to the viewer
 | |
|    *
 | |
|    * The convert function just returns the stream, it's just the synchronous
 | |
|    * version of asyncConvertData.
 | |
|    */
 | |
| 
 | |
|   // nsIStreamConverter::convert
 | |
|   convert(aFromStream, aFromType, aToType, aCtxt) {
 | |
|     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
 | |
|   },
 | |
| 
 | |
|   // nsIStreamConverter::asyncConvertData
 | |
|   asyncConvertData(aFromType, aToType, aListener, aCtxt) {
 | |
|     // Store the listener passed to us
 | |
|     this.listener = aListener;
 | |
|   },
 | |
| 
 | |
|   // nsIStreamListener::onDataAvailable
 | |
|   onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
 | |
|     if (!this.dataListener) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var binaryStream = this.binaryStream;
 | |
|     binaryStream.setInputStream(aInputStream);
 | |
|     let chunk = new ArrayBuffer(aCount);
 | |
|     binaryStream.readArrayBuffer(aCount, chunk);
 | |
|     this.dataListener.append(new Uint8Array(chunk));
 | |
|   },
 | |
| 
 | |
|   // nsIRequestObserver::onStartRequest
 | |
|   onStartRequest(aRequest, aContext) {
 | |
|     // Setup the request so we can use it below.
 | |
|     var isHttpRequest = false;
 | |
|     try {
 | |
|       aRequest.QueryInterface(Ci.nsIHttpChannel);
 | |
|       isHttpRequest = true;
 | |
|     } catch (e) {}
 | |
| 
 | |
|     var rangeRequest = false;
 | |
|     var streamRequest = false;
 | |
|     if (isHttpRequest) {
 | |
|       var contentEncoding = "identity";
 | |
|       try {
 | |
|         contentEncoding = aRequest.getResponseHeader("Content-Encoding");
 | |
|       } catch (e) {}
 | |
| 
 | |
|       var acceptRanges;
 | |
|       try {
 | |
|         acceptRanges = aRequest.getResponseHeader("Accept-Ranges");
 | |
|       } catch (e) {}
 | |
| 
 | |
|       var hash = aRequest.URI.ref;
 | |
|       var isPDFBugEnabled = getBoolPref(PREF_PREFIX + ".pdfBugEnabled", false);
 | |
|       rangeRequest = contentEncoding === "identity" &&
 | |
|                      acceptRanges === "bytes" &&
 | |
|                      aRequest.contentLength >= 0 &&
 | |
|                      !getBoolPref(PREF_PREFIX + ".disableRange", false) &&
 | |
|                      (!isPDFBugEnabled ||
 | |
|                       !hash.toLowerCase().includes("disablerange=true"));
 | |
|       streamRequest = contentEncoding === "identity" &&
 | |
|                       aRequest.contentLength >= 0 &&
 | |
|                       !getBoolPref(PREF_PREFIX + ".disableStream", false) &&
 | |
|                       (!isPDFBugEnabled ||
 | |
|                        !hash.toLowerCase().includes("disablestream=true"));
 | |
|     }
 | |
| 
 | |
|     aRequest.QueryInterface(Ci.nsIChannel);
 | |
| 
 | |
|     aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
 | |
| 
 | |
|     var contentDispositionFilename;
 | |
|     try {
 | |
|       contentDispositionFilename = aRequest.contentDispositionFilename;
 | |
|     } catch (e) {}
 | |
| 
 | |
|     // Change the content type so we don't get stuck in a loop.
 | |
|     aRequest.setProperty("contentType", aRequest.contentType);
 | |
|     aRequest.contentType = "text/html";
 | |
|     if (isHttpRequest) {
 | |
|       // We trust PDF viewer, using no CSP
 | |
|       aRequest.setResponseHeader("Content-Security-Policy", "", false);
 | |
|       aRequest.setResponseHeader("Content-Security-Policy-Report-Only", "",
 | |
|                                  false);
 | |
|       // The viewer does not need to handle HTTP Refresh header.
 | |
|       aRequest.setResponseHeader("Refresh", "", false);
 | |
|     }
 | |
| 
 | |
|     PdfJsTelemetry.onViewerIsUsed();
 | |
|     PdfJsTelemetry.onDocumentSize(aRequest.contentLength);
 | |
| 
 | |
|     // Creating storage for PDF data
 | |
|     var contentLength = aRequest.contentLength;
 | |
|     this.dataListener = new PdfDataListener(contentLength);
 | |
|     this.binaryStream = Cc["@mozilla.org/binaryinputstream;1"]
 | |
|                         .createInstance(Ci.nsIBinaryInputStream);
 | |
| 
 | |
|     // Create a new channel that is viewer loaded as a resource.
 | |
|     var channel = NetUtil.newChannel({
 | |
|       uri: PDF_VIEWER_WEB_PAGE,
 | |
|       loadUsingSystemPrincipal: true,
 | |
|     });
 | |
| 
 | |
|     var listener = this.listener;
 | |
|     var dataListener = this.dataListener;
 | |
|     // Proxy all the request observer calls, when it gets to onStopRequest
 | |
|     // we can get the dom window.  We also intentionally pass on the original
 | |
|     // request(aRequest) below so we don't overwrite the original channel and
 | |
|     // trigger an assertion.
 | |
|     var proxy = {
 | |
|       onStartRequest(request, context) {
 | |
|         listener.onStartRequest(aRequest, aContext);
 | |
|       },
 | |
|       onDataAvailable(request, context, inputStream, offset, count) {
 | |
|         listener.onDataAvailable(aRequest, aContext, inputStream,
 | |
|                                  offset, count);
 | |
|       },
 | |
|       onStopRequest(request, context, statusCode) {
 | |
|         var domWindow = getDOMWindow(channel, resourcePrincipal);
 | |
|         if (!Components.isSuccessCode(statusCode) || !domWindow) {
 | |
|           // The request may have been aborted and the document may have been
 | |
|           // replaced with something that is not PDF.js, abort attaching.
 | |
|           listener.onStopRequest(aRequest, context, statusCode);
 | |
|           return;
 | |
|         }
 | |
|         var actions;
 | |
|         if (rangeRequest || streamRequest) {
 | |
|           actions = new RangedChromeActions(
 | |
|             domWindow, contentDispositionFilename, aRequest,
 | |
|             rangeRequest, streamRequest, dataListener);
 | |
|         } else {
 | |
|           actions = new StandardChromeActions(
 | |
|             domWindow, contentDispositionFilename, aRequest, dataListener);
 | |
|         }
 | |
|         var requestListener = new RequestListener(actions);
 | |
|         domWindow.document.addEventListener(PDFJS_EVENT_ID, function(event) {
 | |
|           requestListener.receive(event);
 | |
|         }, false, true);
 | |
|         if (actions.supportsIntegratedFind()) {
 | |
|           var findEventManager = new FindEventManager(domWindow);
 | |
|           findEventManager.bind();
 | |
|         }
 | |
|         listener.onStopRequest(aRequest, aContext, statusCode);
 | |
| 
 | |
|         if (domWindow.frameElement) {
 | |
|           var isObjectEmbed = domWindow.frameElement.tagName !== "IFRAME" ||
 | |
|             domWindow.frameElement.className === "previewPluginContentFrame";
 | |
|           PdfJsTelemetry.onEmbed(isObjectEmbed);
 | |
|         }
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     // Keep the URL the same so the browser sees it as the same.
 | |
|     channel.originalURI = aRequest.URI;
 | |
|     channel.loadGroup = aRequest.loadGroup;
 | |
|     channel.loadInfo.originAttributes = aRequest.loadInfo.originAttributes;
 | |
| 
 | |
|     // We can use the resource principal when data is fetched by the chrome,
 | |
|     // e.g. useful for NoScript. Make make sure we reuse the origin attributes
 | |
|     // from the request channel to keep isolation consistent.
 | |
|     var uri = NetUtil.newURI(PDF_VIEWER_WEB_PAGE);
 | |
|     var resourcePrincipal =
 | |
|       Services.scriptSecurityManager.createCodebasePrincipal(uri,
 | |
|         aRequest.loadInfo.originAttributes);
 | |
|     aRequest.owner = resourcePrincipal;
 | |
| 
 | |
|     channel.asyncOpen2(proxy);
 | |
|   },
 | |
| 
 | |
|   // nsIRequestObserver::onStopRequest
 | |
|   onStopRequest(aRequest, aContext, aStatusCode) {
 | |
|     if (!this.dataListener) {
 | |
|       // Do nothing
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (Components.isSuccessCode(aStatusCode)) {
 | |
|       this.dataListener.finish();
 | |
|     } else {
 | |
|       this.dataListener.error(aStatusCode);
 | |
|     }
 | |
|     delete this.dataListener;
 | |
|     delete this.binaryStream;
 | |
|   },
 | |
| };
 | |
| 
 |