michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ michael@0: /* Copyright 2012 Mozilla Foundation michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: /* jshint esnext:true */ michael@0: /* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils, michael@0: dump, NetworkManager, PdfJsTelemetry */ michael@0: michael@0: 'use strict'; michael@0: michael@0: var EXPORTED_SYMBOLS = ['PdfStreamConverter']; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: // True only if this is the version of pdf.js that is included with firefox. michael@0: const MOZ_CENTRAL = JSON.parse('true'); michael@0: const PDFJS_EVENT_ID = 'pdf.js.message'; michael@0: const PDF_CONTENT_TYPE = 'application/pdf'; michael@0: const PREF_PREFIX = 'pdfjs'; michael@0: const PDF_VIEWER_WEB_PAGE = 'resource://pdf.js/web/viewer.html'; michael@0: const MAX_NUMBER_OF_PREFS = 50; michael@0: const MAX_STRING_PREF_LENGTH = 128; michael@0: michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: Cu.import('resource://gre/modules/Services.jsm'); michael@0: Cu.import('resource://gre/modules/NetUtil.jsm'); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager', michael@0: 'resource://pdf.js/network.js'); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils', michael@0: 'resource://gre/modules/PrivateBrowsingUtils.jsm'); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry', michael@0: 'resource://pdf.js/PdfJsTelemetry.jsm'); michael@0: michael@0: var Svc = {}; michael@0: XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', michael@0: '@mozilla.org/mime;1', michael@0: 'nsIMIMEService'); michael@0: michael@0: function getContainingBrowser(domWindow) { michael@0: return domWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler; michael@0: } michael@0: michael@0: function getChromeWindow(domWindow) { michael@0: return getContainingBrowser(domWindow).ownerDocument.defaultView; michael@0: } michael@0: michael@0: function getFindBar(domWindow) { michael@0: var browser = getContainingBrowser(domWindow); michael@0: try { michael@0: var tabbrowser = browser.getTabBrowser(); michael@0: var tab = tabbrowser._getTabForBrowser(browser); michael@0: return tabbrowser.getFindBar(tab); michael@0: } catch (e) { michael@0: // FF22 has no _getTabForBrowser, and FF24 has no getFindBar michael@0: var chromeWindow = browser.ownerDocument.defaultView; michael@0: return chromeWindow.gFindBar; michael@0: } michael@0: } michael@0: michael@0: function setBoolPref(pref, value) { michael@0: Services.prefs.setBoolPref(pref, value); michael@0: } michael@0: michael@0: function getBoolPref(pref, def) { michael@0: try { michael@0: return Services.prefs.getBoolPref(pref); michael@0: } catch (ex) { michael@0: return def; michael@0: } michael@0: } michael@0: michael@0: function setIntPref(pref, value) { michael@0: Services.prefs.setIntPref(pref, value); michael@0: } michael@0: michael@0: function getIntPref(pref, def) { michael@0: try { michael@0: return Services.prefs.getIntPref(pref); michael@0: } catch (ex) { michael@0: return def; michael@0: } michael@0: } michael@0: michael@0: function setStringPref(pref, value) { michael@0: var str = Cc['@mozilla.org/supports-string;1'] michael@0: .createInstance(Ci.nsISupportsString); michael@0: str.data = value; michael@0: Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str); michael@0: } michael@0: michael@0: function getStringPref(pref, def) { michael@0: try { michael@0: return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data; michael@0: } catch (ex) { michael@0: return def; michael@0: } michael@0: } michael@0: michael@0: function log(aMsg) { michael@0: if (!getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false)) michael@0: return; michael@0: var msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg); michael@0: Services.console.logStringMessage(msg); michael@0: dump(msg + '\n'); michael@0: } michael@0: michael@0: function getDOMWindow(aChannel) { michael@0: var requestor = aChannel.notificationCallbacks ? michael@0: aChannel.notificationCallbacks : michael@0: aChannel.loadGroup.notificationCallbacks; michael@0: var win = requestor.getInterface(Components.interfaces.nsIDOMWindow); michael@0: return win; michael@0: } michael@0: michael@0: function getLocalizedStrings(path) { michael@0: var stringBundle = Cc['@mozilla.org/intl/stringbundle;1']. michael@0: getService(Ci.nsIStringBundleService). michael@0: createBundle('chrome://pdf.js/locale/' + path); michael@0: michael@0: var map = {}; michael@0: var enumerator = stringBundle.getSimpleEnumeration(); michael@0: while (enumerator.hasMoreElements()) { michael@0: var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); michael@0: var key = string.key, property = 'textContent'; michael@0: var i = key.lastIndexOf('.'); michael@0: if (i >= 0) { michael@0: property = key.substring(i + 1); michael@0: key = key.substring(0, i); michael@0: } michael@0: if (!(key in map)) michael@0: map[key] = {}; michael@0: map[key][property] = string.value; michael@0: } michael@0: return map; michael@0: } michael@0: function getLocalizedString(strings, id, property) { michael@0: property = property || 'textContent'; michael@0: if (id in strings) michael@0: return strings[id][property]; michael@0: return id; michael@0: } michael@0: michael@0: // PDF data storage michael@0: function PdfDataListener(length) { michael@0: this.length = length; // less than 0, if length is unknown michael@0: this.data = new Uint8Array(length >= 0 ? length : 0x10000); michael@0: this.loaded = 0; michael@0: } michael@0: michael@0: PdfDataListener.prototype = { michael@0: append: function PdfDataListener_append(chunk) { michael@0: var willBeLoaded = this.loaded + chunk.length; michael@0: if (this.length >= 0 && this.length < willBeLoaded) { michael@0: this.length = -1; // reset the length, server is giving incorrect one michael@0: } michael@0: if (this.length < 0 && this.data.length < willBeLoaded) { michael@0: // data length is unknown and new chunk will not fit in the existing michael@0: // buffer, resizing the buffer by doubling the its last length michael@0: var newLength = this.data.length; michael@0: for (; newLength < willBeLoaded; newLength *= 2) {} michael@0: var newData = new Uint8Array(newLength); michael@0: newData.set(this.data); michael@0: this.data = newData; michael@0: } michael@0: this.data.set(chunk, this.loaded); michael@0: this.loaded = willBeLoaded; michael@0: this.onprogress(this.loaded, this.length >= 0 ? this.length : void(0)); michael@0: }, michael@0: getData: function PdfDataListener_getData() { michael@0: var data = this.data; michael@0: if (this.loaded != data.length) michael@0: data = data.subarray(0, this.loaded); michael@0: delete this.data; // releasing temporary storage michael@0: return data; michael@0: }, michael@0: finish: function PdfDataListener_finish() { michael@0: this.isDataReady = true; michael@0: if (this.oncompleteCallback) { michael@0: this.oncompleteCallback(this.getData()); michael@0: } michael@0: }, michael@0: error: function PdfDataListener_error(errorCode) { michael@0: this.errorCode = errorCode; michael@0: if (this.oncompleteCallback) { michael@0: this.oncompleteCallback(null, errorCode); michael@0: } michael@0: }, michael@0: onprogress: function() {}, michael@0: get oncomplete() { michael@0: return this.oncompleteCallback; michael@0: }, michael@0: set oncomplete(value) { michael@0: this.oncompleteCallback = value; michael@0: if (this.isDataReady) { michael@0: value(this.getData()); michael@0: } michael@0: if (this.errorCode) { michael@0: value(null, this.errorCode); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // All the priviledged actions. michael@0: function ChromeActions(domWindow, contentDispositionFilename) { michael@0: this.domWindow = domWindow; michael@0: this.contentDispositionFilename = contentDispositionFilename; michael@0: this.telemetryState = { michael@0: documentInfo: false, michael@0: firstPageInfo: false, michael@0: streamTypesUsed: [], michael@0: startAt: Date.now() michael@0: }; michael@0: } michael@0: michael@0: ChromeActions.prototype = { michael@0: isInPrivateBrowsing: function() { michael@0: return PrivateBrowsingUtils.isWindowPrivate(this.domWindow); michael@0: }, michael@0: download: function(data, sendResponse) { michael@0: var self = this; michael@0: var originalUrl = data.originalUrl; michael@0: // The data may not be downloaded so we need just retry getting the pdf with michael@0: // the original url. michael@0: var originalUri = NetUtil.newURI(data.originalUrl); michael@0: var filename = data.filename; michael@0: if (typeof filename !== 'string' || michael@0: (!/\.pdf$/i.test(filename) && !data.isAttachment)) { michael@0: filename = 'document.pdf'; michael@0: } michael@0: var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri; michael@0: var extHelperAppSvc = michael@0: Cc['@mozilla.org/uriloader/external-helper-app-service;1']. michael@0: getService(Ci.nsIExternalHelperAppService); michael@0: var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1']. michael@0: getService(Ci.nsIWindowWatcher).activeWindow; michael@0: michael@0: var docIsPrivate = this.isInPrivateBrowsing(); michael@0: var netChannel = NetUtil.newChannel(blobUri); michael@0: if ('nsIPrivateBrowsingChannel' in Ci && michael@0: netChannel instanceof Ci.nsIPrivateBrowsingChannel) { michael@0: netChannel.setPrivate(docIsPrivate); michael@0: } michael@0: NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) { michael@0: if (!Components.isSuccessCode(aResult)) { michael@0: if (sendResponse) michael@0: sendResponse(true); michael@0: return; michael@0: } michael@0: // Create a nsIInputStreamChannel so we can set the url on the channel michael@0: // so the filename will be correct. michael@0: var channel = Cc['@mozilla.org/network/input-stream-channel;1']. michael@0: createInstance(Ci.nsIInputStreamChannel); michael@0: channel.QueryInterface(Ci.nsIChannel); michael@0: try { michael@0: // contentDisposition/contentDispositionFilename is readonly before FF18 michael@0: channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT; michael@0: if (self.contentDispositionFilename) { michael@0: channel.contentDispositionFilename = self.contentDispositionFilename; michael@0: } else { michael@0: channel.contentDispositionFilename = filename; michael@0: } michael@0: } catch (e) {} michael@0: channel.setURI(originalUri); michael@0: channel.contentStream = aInputStream; michael@0: if ('nsIPrivateBrowsingChannel' in Ci && michael@0: channel instanceof Ci.nsIPrivateBrowsingChannel) { michael@0: channel.setPrivate(docIsPrivate); michael@0: } michael@0: michael@0: var listener = { michael@0: extListener: null, michael@0: onStartRequest: function(aRequest, aContext) { michael@0: this.extListener = extHelperAppSvc.doContent((data.isAttachment ? '' : michael@0: 'application/pdf'), michael@0: aRequest, frontWindow, false); michael@0: this.extListener.onStartRequest(aRequest, aContext); michael@0: }, michael@0: onStopRequest: function(aRequest, aContext, aStatusCode) { michael@0: if (this.extListener) michael@0: this.extListener.onStopRequest(aRequest, aContext, aStatusCode); michael@0: // Notify the content code we're done downloading. michael@0: if (sendResponse) michael@0: sendResponse(false); michael@0: }, michael@0: onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, michael@0: aCount) { michael@0: this.extListener.onDataAvailable(aRequest, aContext, aInputStream, michael@0: aOffset, aCount); michael@0: } michael@0: }; michael@0: michael@0: channel.asyncOpen(listener, null); michael@0: }); michael@0: }, michael@0: getLocale: function() { michael@0: return getStringPref('general.useragent.locale', 'en-US'); michael@0: }, michael@0: getStrings: function(data) { michael@0: try { michael@0: // Lazy initialization of localizedStrings michael@0: if (!('localizedStrings' in this)) michael@0: this.localizedStrings = getLocalizedStrings('viewer.properties'); michael@0: michael@0: var result = this.localizedStrings[data]; michael@0: return JSON.stringify(result || null); michael@0: } catch (e) { michael@0: log('Unable to retrive localized strings: ' + e); michael@0: return 'null'; michael@0: } michael@0: }, michael@0: pdfBugEnabled: function() { michael@0: return getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false); michael@0: }, michael@0: supportsIntegratedFind: function() { michael@0: // Integrated find is only supported when we're not in a frame michael@0: if (this.domWindow.frameElement !== null) { michael@0: return false; michael@0: } michael@0: // ... and when the new find events code exists. michael@0: var findBar = getFindBar(this.domWindow); michael@0: return findBar && ('updateControlState' in findBar); michael@0: }, michael@0: supportsDocumentFonts: function() { michael@0: var prefBrowser = getIntPref('browser.display.use_document_fonts', 1); michael@0: var prefGfx = getBoolPref('gfx.downloadable_fonts.enabled', true); michael@0: return (!!prefBrowser && prefGfx); michael@0: }, michael@0: supportsDocumentColors: function() { michael@0: return getBoolPref('browser.display.use_document_colors', true); michael@0: }, michael@0: reportTelemetry: function (data) { michael@0: var probeInfo = JSON.parse(data); michael@0: switch (probeInfo.type) { michael@0: case 'documentInfo': michael@0: if (!this.telemetryState.documentInfo) { michael@0: PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0); michael@0: PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0); michael@0: if (probeInfo.formType) { michael@0: PdfJsTelemetry.onForm(probeInfo.formType === 'acroform'); michael@0: } michael@0: this.telemetryState.documentInfo = true; michael@0: } michael@0: break; michael@0: case 'pageInfo': michael@0: if (!this.telemetryState.firstPageInfo) { michael@0: var duration = Date.now() - this.telemetryState.startAt; michael@0: PdfJsTelemetry.onTimeToView(duration); michael@0: this.telemetryState.firstPageInfo = true; michael@0: } michael@0: break; michael@0: case 'streamInfo': michael@0: if (!Array.isArray(probeInfo.streamTypes)) { michael@0: break; michael@0: } michael@0: for (var i = 0; i < probeInfo.streamTypes.length; i++) { michael@0: var streamTypeId = probeInfo.streamTypes[i] | 0; michael@0: if (streamTypeId >= 0 && streamTypeId < 10 && michael@0: !this.telemetryState.streamTypesUsed[streamTypeId]) { michael@0: PdfJsTelemetry.onStreamType(streamTypeId); michael@0: this.telemetryState.streamTypesUsed[streamTypeId] = true; michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: fallback: function(args, sendResponse) { michael@0: var featureId = args.featureId; michael@0: var url = args.url; michael@0: michael@0: var self = this; michael@0: var domWindow = this.domWindow; michael@0: var strings = getLocalizedStrings('chrome.properties'); michael@0: var message; michael@0: if (featureId === 'forms') { michael@0: message = getLocalizedString(strings, 'unsupported_feature_forms'); michael@0: } else { michael@0: message = getLocalizedString(strings, 'unsupported_feature'); michael@0: } michael@0: michael@0: PdfJsTelemetry.onFallback(); michael@0: michael@0: var notificationBox = null; michael@0: try { michael@0: // Based on MDN's "Working with windows in chrome code" michael@0: var mainWindow = domWindow michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsIDocShellTreeItem) michael@0: .rootTreeItem michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindow); michael@0: var browser = mainWindow.gBrowser michael@0: .getBrowserForDocument(domWindow.top.document); michael@0: notificationBox = mainWindow.gBrowser.getNotificationBox(browser); michael@0: } catch (e) { michael@0: log('Unable to get a notification box for the fallback message'); michael@0: return; michael@0: } michael@0: michael@0: // Flag so we don't call the response callback twice, since if the user michael@0: // clicks open with different viewer both the button callback and michael@0: // eventCallback will be called. michael@0: var sentResponse = false; michael@0: var buttons = [{ michael@0: label: getLocalizedString(strings, 'open_with_different_viewer'), michael@0: accessKey: getLocalizedString(strings, 'open_with_different_viewer', michael@0: 'accessKey'), michael@0: callback: function() { michael@0: sentResponse = true; michael@0: sendResponse(true); michael@0: } michael@0: }]; michael@0: notificationBox.appendNotification(message, 'pdfjs-fallback', null, michael@0: notificationBox.PRIORITY_INFO_LOW, michael@0: buttons, michael@0: function eventsCallback(eventType) { michael@0: // Currently there is only one event "removed" but if there are any other michael@0: // added in the future we still only care about removed at the moment. michael@0: if (eventType !== 'removed') michael@0: return; michael@0: // Don't send a response again if we already responded when the button was michael@0: // clicked. michael@0: if (!sentResponse) michael@0: sendResponse(false); michael@0: }); michael@0: }, michael@0: updateFindControlState: function(data) { michael@0: if (!this.supportsIntegratedFind()) michael@0: return; michael@0: // Verify what we're sending to the findbar. michael@0: var result = data.result; michael@0: var findPrevious = data.findPrevious; michael@0: var findPreviousType = typeof findPrevious; michael@0: if ((typeof result !== 'number' || result < 0 || result > 3) || michael@0: (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) { michael@0: return; michael@0: } michael@0: getFindBar(this.domWindow).updateControlState(result, findPrevious); michael@0: }, michael@0: setPreferences: function(prefs, sendResponse) { michael@0: var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.'); michael@0: var numberOfPrefs = 0; michael@0: var prefValue, prefName; michael@0: for (var key in prefs) { michael@0: if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) { michael@0: log('setPreferences - Exceeded the maximum number of preferences ' + michael@0: 'that is allowed to be set at once.'); michael@0: break; michael@0: } else if (!defaultBranch.getPrefType(key)) { michael@0: continue; michael@0: } michael@0: prefValue = prefs[key]; michael@0: prefName = (PREF_PREFIX + '.' + key); michael@0: switch (typeof prefValue) { michael@0: case 'boolean': michael@0: setBoolPref(prefName, prefValue); michael@0: break; michael@0: case 'number': michael@0: setIntPref(prefName, prefValue); michael@0: break; michael@0: case 'string': michael@0: if (prefValue.length > MAX_STRING_PREF_LENGTH) { michael@0: log('setPreferences - Exceeded the maximum allowed length ' + michael@0: 'for a string preference.'); michael@0: } else { michael@0: setStringPref(prefName, prefValue); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: if (sendResponse) { michael@0: sendResponse(true); michael@0: } michael@0: }, michael@0: getPreferences: function(prefs, sendResponse) { michael@0: var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.'); michael@0: var currentPrefs = {}, numberOfPrefs = 0; michael@0: var prefValue, prefName; michael@0: for (var key in prefs) { michael@0: if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) { michael@0: log('getPreferences - Exceeded the maximum number of preferences ' + michael@0: 'that is allowed to be fetched at once.'); michael@0: break; michael@0: } else if (!defaultBranch.getPrefType(key)) { michael@0: continue; michael@0: } michael@0: prefValue = prefs[key]; michael@0: prefName = (PREF_PREFIX + '.' + key); michael@0: switch (typeof prefValue) { michael@0: case 'boolean': michael@0: currentPrefs[key] = getBoolPref(prefName, prefValue); michael@0: break; michael@0: case 'number': michael@0: currentPrefs[key] = getIntPref(prefName, prefValue); michael@0: break; michael@0: case 'string': michael@0: currentPrefs[key] = getStringPref(prefName, prefValue); michael@0: break; michael@0: } michael@0: } michael@0: if (sendResponse) { michael@0: sendResponse(JSON.stringify(currentPrefs)); michael@0: } else { michael@0: return JSON.stringify(currentPrefs); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var RangedChromeActions = (function RangedChromeActionsClosure() { michael@0: /** michael@0: * This is for range requests michael@0: */ michael@0: function RangedChromeActions( michael@0: domWindow, contentDispositionFilename, originalRequest, michael@0: dataListener) { michael@0: michael@0: ChromeActions.call(this, domWindow, contentDispositionFilename); michael@0: this.dataListener = dataListener; michael@0: this.originalRequest = originalRequest; michael@0: michael@0: this.pdfUrl = originalRequest.URI.spec; michael@0: this.contentLength = originalRequest.contentLength; michael@0: michael@0: // Pass all the headers from the original request through michael@0: var httpHeaderVisitor = { michael@0: headers: {}, michael@0: visitHeader: function(aHeader, aValue) { michael@0: if (aHeader === 'Range') { michael@0: // When loading the PDF from cache, firefox seems to set the Range michael@0: // request header to fetch only the unfetched portions of the file michael@0: // (e.g. 'Range: bytes=1024-'). However, we want to set this header michael@0: // manually to fetch the PDF in chunks. michael@0: return; michael@0: } michael@0: this.headers[aHeader] = aValue; michael@0: } michael@0: }; michael@0: originalRequest.visitRequestHeaders(httpHeaderVisitor); michael@0: michael@0: var self = this; michael@0: var xhr_onreadystatechange = function xhr_onreadystatechange() { michael@0: if (this.readyState === 1) { // LOADING michael@0: var netChannel = this.channel; michael@0: if ('nsIPrivateBrowsingChannel' in Ci && michael@0: netChannel instanceof Ci.nsIPrivateBrowsingChannel) { michael@0: var docIsPrivate = self.isInPrivateBrowsing(); michael@0: netChannel.setPrivate(docIsPrivate); michael@0: } michael@0: } michael@0: }; michael@0: var getXhr = function getXhr() { michael@0: const XMLHttpRequest = Components.Constructor( michael@0: '@mozilla.org/xmlextras/xmlhttprequest;1'); michael@0: var xhr = new XMLHttpRequest(); michael@0: xhr.addEventListener('readystatechange', xhr_onreadystatechange); michael@0: return xhr; michael@0: }; michael@0: michael@0: this.networkManager = new NetworkManager(this.pdfUrl, { michael@0: httpHeaders: httpHeaderVisitor.headers, michael@0: getXhr: getXhr michael@0: }); michael@0: michael@0: // If we are in range request mode, this means we manually issued xhr michael@0: // requests, which we need to abort when we leave the page michael@0: domWindow.addEventListener('unload', function unload(e) { michael@0: self.networkManager.abortAllRequests(); michael@0: domWindow.removeEventListener(e.type, unload); michael@0: }); michael@0: } michael@0: michael@0: RangedChromeActions.prototype = Object.create(ChromeActions.prototype); michael@0: var proto = RangedChromeActions.prototype; michael@0: proto.constructor = RangedChromeActions; michael@0: michael@0: proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() { michael@0: this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); michael@0: this.originalRequest = null; michael@0: this.domWindow.postMessage({ michael@0: pdfjsLoadAction: 'supportsRangedLoading', michael@0: pdfUrl: this.pdfUrl, michael@0: length: this.contentLength, michael@0: data: this.dataListener.getData() michael@0: }, '*'); michael@0: this.dataListener = null; michael@0: michael@0: return true; michael@0: }; michael@0: michael@0: proto.requestDataRange = function RangedChromeActions_requestDataRange(args) { michael@0: var begin = args.begin; michael@0: var end = args.end; michael@0: var domWindow = this.domWindow; michael@0: // TODO(mack): Support error handler. We're not currently not handling michael@0: // errors from chrome code for non-range requests, so this doesn't michael@0: // seem high-pri michael@0: this.networkManager.requestRange(begin, end, { michael@0: onDone: function RangedChromeActions_onDone(args) { michael@0: domWindow.postMessage({ michael@0: pdfjsLoadAction: 'range', michael@0: begin: args.begin, michael@0: chunk: args.chunk michael@0: }, '*'); michael@0: }, michael@0: onProgress: function RangedChromeActions_onProgress(evt) { michael@0: domWindow.postMessage({ michael@0: pdfjsLoadAction: 'rangeProgress', michael@0: loaded: evt.loaded, michael@0: }, '*'); michael@0: } michael@0: }); michael@0: }; michael@0: michael@0: return RangedChromeActions; michael@0: })(); michael@0: michael@0: var StandardChromeActions = (function StandardChromeActionsClosure() { michael@0: michael@0: /** michael@0: * This is for a single network stream michael@0: */ michael@0: function StandardChromeActions(domWindow, contentDispositionFilename, michael@0: dataListener) { michael@0: michael@0: ChromeActions.call(this, domWindow, contentDispositionFilename); michael@0: this.dataListener = dataListener; michael@0: } michael@0: michael@0: StandardChromeActions.prototype = Object.create(ChromeActions.prototype); michael@0: var proto = StandardChromeActions.prototype; michael@0: proto.constructor = StandardChromeActions; michael@0: michael@0: proto.initPassiveLoading = michael@0: function StandardChromeActions_initPassiveLoading() { michael@0: michael@0: if (!this.dataListener) { michael@0: return false; michael@0: } michael@0: michael@0: var self = this; michael@0: michael@0: this.dataListener.onprogress = function ChromeActions_dataListenerProgress( michael@0: loaded, total) { michael@0: self.domWindow.postMessage({ michael@0: pdfjsLoadAction: 'progress', michael@0: loaded: loaded, michael@0: total: total michael@0: }, '*'); michael@0: }; michael@0: michael@0: this.dataListener.oncomplete = function ChromeActions_dataListenerComplete( michael@0: data, errorCode) { michael@0: self.domWindow.postMessage({ michael@0: pdfjsLoadAction: 'complete', michael@0: data: data, michael@0: errorCode: errorCode michael@0: }, '*'); michael@0: michael@0: delete self.dataListener; michael@0: }; michael@0: michael@0: return true; michael@0: }; michael@0: michael@0: return StandardChromeActions; michael@0: })(); michael@0: michael@0: // Event listener to trigger chrome privedged code. michael@0: function RequestListener(actions) { michael@0: this.actions = actions; michael@0: } michael@0: // Receive an event and synchronously or asynchronously responds. michael@0: RequestListener.prototype.receive = function(event) { michael@0: var message = event.target; michael@0: var doc = message.ownerDocument; michael@0: var action = event.detail.action; michael@0: var data = event.detail.data; michael@0: var sync = event.detail.sync; michael@0: var actions = this.actions; michael@0: if (!(action in actions)) { michael@0: log('Unknown action: ' + action); michael@0: return; michael@0: } michael@0: if (sync) { michael@0: var response = actions[action].call(this.actions, data); michael@0: var detail = event.detail; michael@0: detail.__exposedProps__ = {response: 'r'}; michael@0: detail.response = response; michael@0: } else { michael@0: var response; michael@0: if (!event.detail.callback) { michael@0: doc.documentElement.removeChild(message); michael@0: response = null; michael@0: } else { michael@0: response = function sendResponse(response) { michael@0: try { michael@0: var listener = doc.createEvent('CustomEvent'); michael@0: listener.initCustomEvent('pdf.js.response', true, false, michael@0: {response: response, michael@0: __exposedProps__: {response: 'r'}}); michael@0: return message.dispatchEvent(listener); michael@0: } catch (e) { michael@0: // doc is no longer accessible because the requestor is already michael@0: // gone. unloaded content cannot receive the response anyway. michael@0: return false; michael@0: } michael@0: }; michael@0: } michael@0: actions[action].call(this.actions, data, response); michael@0: } michael@0: }; michael@0: michael@0: // Forwards events from the eventElement to the contentWindow only if the michael@0: // content window matches the currently selected browser window. michael@0: function FindEventManager(eventElement, contentWindow, chromeWindow) { michael@0: this.types = ['find', michael@0: 'findagain', michael@0: 'findhighlightallchange', michael@0: 'findcasesensitivitychange']; michael@0: this.chromeWindow = chromeWindow; michael@0: this.contentWindow = contentWindow; michael@0: this.eventElement = eventElement; michael@0: } michael@0: michael@0: FindEventManager.prototype.bind = function() { michael@0: var unload = function(e) { michael@0: this.unbind(); michael@0: this.contentWindow.removeEventListener(e.type, unload); michael@0: }.bind(this); michael@0: this.contentWindow.addEventListener('unload', unload); michael@0: michael@0: for (var i = 0; i < this.types.length; i++) { michael@0: var type = this.types[i]; michael@0: this.eventElement.addEventListener(type, this, true); michael@0: } michael@0: }; michael@0: michael@0: FindEventManager.prototype.handleEvent = function(e) { michael@0: var chromeWindow = this.chromeWindow; michael@0: var contentWindow = this.contentWindow; michael@0: // Only forward the events if they are for our dom window. michael@0: if (chromeWindow.gBrowser.selectedBrowser.contentWindow === contentWindow) { michael@0: var detail = e.detail; michael@0: detail.__exposedProps__ = { michael@0: query: 'r', michael@0: caseSensitive: 'r', michael@0: highlightAll: 'r', michael@0: findPrevious: 'r' michael@0: }; michael@0: var forward = contentWindow.document.createEvent('CustomEvent'); michael@0: forward.initCustomEvent(e.type, true, true, detail); michael@0: contentWindow.dispatchEvent(forward); michael@0: e.preventDefault(); michael@0: } michael@0: }; michael@0: michael@0: FindEventManager.prototype.unbind = function() { michael@0: for (var i = 0; i < this.types.length; i++) { michael@0: var type = this.types[i]; michael@0: this.eventElement.removeEventListener(type, this, true); michael@0: } michael@0: }; michael@0: michael@0: function PdfStreamConverter() { michael@0: } michael@0: michael@0: PdfStreamConverter.prototype = { michael@0: michael@0: // properties required for XPCOM registration: michael@0: classID: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2291}'), michael@0: classDescription: 'pdf.js Component', michael@0: contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*', michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsISupports, michael@0: Ci.nsIStreamConverter, michael@0: Ci.nsIStreamListener, michael@0: Ci.nsIRequestObserver michael@0: ]), michael@0: michael@0: /* michael@0: * This component works as such: michael@0: * 1. asyncConvertData stores the listener michael@0: * 2. onStartRequest creates a new channel, streams the viewer michael@0: * 3. If range requests are supported: michael@0: * 3.1. Leave the request open until the viewer is ready to switch to michael@0: * range requests. michael@0: * michael@0: * If range rquests are not supported: michael@0: * 3.1. Read the stream as it's loaded in onDataAvailable to send michael@0: * to the viewer michael@0: * michael@0: * The convert function just returns the stream, it's just the synchronous michael@0: * version of asyncConvertData. michael@0: */ michael@0: michael@0: // nsIStreamConverter::convert michael@0: convert: function(aFromStream, aFromType, aToType, aCtxt) { michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: // nsIStreamConverter::asyncConvertData michael@0: asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { michael@0: // Store the listener passed to us michael@0: this.listener = aListener; michael@0: }, michael@0: michael@0: // nsIStreamListener::onDataAvailable michael@0: onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { michael@0: if (!this.dataListener) { michael@0: return; michael@0: } michael@0: michael@0: var binaryStream = this.binaryStream; michael@0: binaryStream.setInputStream(aInputStream); michael@0: var chunk = binaryStream.readByteArray(aCount); michael@0: this.dataListener.append(chunk); michael@0: }, michael@0: michael@0: // nsIRequestObserver::onStartRequest michael@0: onStartRequest: function(aRequest, aContext) { michael@0: // Setup the request so we can use it below. michael@0: var isHttpRequest = false; michael@0: try { michael@0: aRequest.QueryInterface(Ci.nsIHttpChannel); michael@0: isHttpRequest = true; michael@0: } catch (e) {} michael@0: michael@0: var rangeRequest = false; michael@0: if (isHttpRequest) { michael@0: var contentEncoding = 'identity'; michael@0: try { michael@0: contentEncoding = aRequest.getResponseHeader('Content-Encoding'); michael@0: } catch (e) {} michael@0: michael@0: var acceptRanges; michael@0: try { michael@0: acceptRanges = aRequest.getResponseHeader('Accept-Ranges'); michael@0: } catch (e) {} michael@0: michael@0: var hash = aRequest.URI.ref; michael@0: rangeRequest = contentEncoding === 'identity' && michael@0: acceptRanges === 'bytes' && michael@0: aRequest.contentLength >= 0 && michael@0: hash.indexOf('disableRange=true') < 0; michael@0: } michael@0: michael@0: aRequest.QueryInterface(Ci.nsIChannel); michael@0: michael@0: aRequest.QueryInterface(Ci.nsIWritablePropertyBag); michael@0: michael@0: var contentDispositionFilename; michael@0: try { michael@0: contentDispositionFilename = aRequest.contentDispositionFilename; michael@0: } catch (e) {} michael@0: michael@0: // Change the content type so we don't get stuck in a loop. michael@0: aRequest.setProperty('contentType', aRequest.contentType); michael@0: aRequest.contentType = 'text/html'; michael@0: if (isHttpRequest) { michael@0: // We trust PDF viewer, using no CSP michael@0: aRequest.setResponseHeader('Content-Security-Policy', '', false); michael@0: aRequest.setResponseHeader('Content-Security-Policy-Report-Only', '', michael@0: false); michael@0: aRequest.setResponseHeader('X-Content-Security-Policy', '', false); michael@0: aRequest.setResponseHeader('X-Content-Security-Policy-Report-Only', '', michael@0: false); michael@0: } michael@0: michael@0: PdfJsTelemetry.onViewerIsUsed(); michael@0: PdfJsTelemetry.onDocumentSize(aRequest.contentLength); michael@0: michael@0: michael@0: // Creating storage for PDF data michael@0: var contentLength = aRequest.contentLength; michael@0: this.dataListener = new PdfDataListener(contentLength); michael@0: this.binaryStream = Cc['@mozilla.org/binaryinputstream;1'] michael@0: .createInstance(Ci.nsIBinaryInputStream); michael@0: michael@0: // Create a new channel that is viewer loaded as a resource. michael@0: var ioService = Services.io; michael@0: var channel = ioService.newChannel( michael@0: PDF_VIEWER_WEB_PAGE, null, null); michael@0: michael@0: var listener = this.listener; michael@0: var dataListener = this.dataListener; michael@0: // Proxy all the request observer calls, when it gets to onStopRequest michael@0: // we can get the dom window. We also intentionally pass on the original michael@0: // request(aRequest) below so we don't overwrite the original channel and michael@0: // trigger an assertion. michael@0: var proxy = { michael@0: onStartRequest: function(request, context) { michael@0: listener.onStartRequest(aRequest, context); michael@0: }, michael@0: onDataAvailable: function(request, context, inputStream, offset, count) { michael@0: listener.onDataAvailable(aRequest, context, inputStream, offset, count); michael@0: }, michael@0: onStopRequest: function(request, context, statusCode) { michael@0: // We get the DOM window here instead of before the request since it michael@0: // may have changed during a redirect. michael@0: var domWindow = getDOMWindow(channel); michael@0: var actions; michael@0: if (rangeRequest) { michael@0: actions = new RangedChromeActions( michael@0: domWindow, contentDispositionFilename, aRequest, dataListener); michael@0: } else { michael@0: actions = new StandardChromeActions( michael@0: domWindow, contentDispositionFilename, dataListener); michael@0: } michael@0: var requestListener = new RequestListener(actions); michael@0: domWindow.addEventListener(PDFJS_EVENT_ID, function(event) { michael@0: requestListener.receive(event); michael@0: }, false, true); michael@0: if (actions.supportsIntegratedFind()) { michael@0: var chromeWindow = getChromeWindow(domWindow); michael@0: var findBar = getFindBar(domWindow); michael@0: var findEventManager = new FindEventManager(findBar, michael@0: domWindow, michael@0: chromeWindow); michael@0: findEventManager.bind(); michael@0: } michael@0: listener.onStopRequest(aRequest, context, statusCode); michael@0: } michael@0: }; michael@0: michael@0: // Keep the URL the same so the browser sees it as the same. michael@0: channel.originalURI = aRequest.URI; michael@0: channel.loadGroup = aRequest.loadGroup; michael@0: michael@0: // We can use resource principal when data is fetched by the chrome michael@0: // e.g. useful for NoScript michael@0: var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1'] michael@0: .getService(Ci.nsIScriptSecurityManager); michael@0: var uri = ioService.newURI(PDF_VIEWER_WEB_PAGE, null, null); michael@0: // FF16 and below had getCodebasePrincipal, it was replaced by michael@0: // getNoAppCodebasePrincipal (bug 758258). michael@0: var resourcePrincipal = 'getNoAppCodebasePrincipal' in securityManager ? michael@0: securityManager.getNoAppCodebasePrincipal(uri) : michael@0: securityManager.getCodebasePrincipal(uri); michael@0: aRequest.owner = resourcePrincipal; michael@0: channel.asyncOpen(proxy, aContext); michael@0: }, michael@0: michael@0: // nsIRequestObserver::onStopRequest michael@0: onStopRequest: function(aRequest, aContext, aStatusCode) { michael@0: if (!this.dataListener) { michael@0: // Do nothing michael@0: return; michael@0: } michael@0: michael@0: if (Components.isSuccessCode(aStatusCode)) michael@0: this.dataListener.finish(); michael@0: else michael@0: this.dataListener.error(aStatusCode); michael@0: delete this.dataListener; michael@0: delete this.binaryStream; michael@0: } michael@0: };