diff options
Diffstat (limited to 'browser/extensions/pdfjs/content/PdfStreamConverter.jsm')
-rw-r--r-- | browser/extensions/pdfjs/content/PdfStreamConverter.jsm | 1045 |
1 files changed, 1045 insertions, 0 deletions
diff --git a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm new file mode 100644 index 000000000..3b9f9de26 --- /dev/null +++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm @@ -0,0 +1,1045 @@ +/* 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. + */ +/* jshint esnext:true */ +/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils, + dump, NetworkManager, PdfJsTelemetry, PdfjsContentUtils */ + +'use strict'; + +var EXPORTED_SYMBOLS = ['PdfStreamConverter']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +// True only if this is the version of pdf.js that is included with firefox. +const MOZ_CENTRAL = JSON.parse('true'); +const PDFJS_EVENT_ID = 'pdf.js.message'; +const PDF_CONTENT_TYPE = 'application/pdf'; +const PREF_PREFIX = 'pdfjs'; +const PDF_VIEWER_WEB_PAGE = 'resource://pdf.js/web/viewer.html'; +const MAX_NUMBER_OF_PREFS = 50; +const MAX_STRING_PREF_LENGTH = 128; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/NetUtil.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager', + 'resource://pdf.js/PdfJsNetwork.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils', + 'resource://gre/modules/PrivateBrowsingUtils.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry', + 'resource://pdf.js/PdfJsTelemetry.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsContentUtils', + 'resource://pdf.js/PdfjsContentUtils.jsm'); + +var Svc = {}; +XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', + '@mozilla.org/mime;1', + 'nsIMIMEService'); + +function getContainingBrowser(domWindow) { + return domWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; +} + +function getFindBar(domWindow) { + if (PdfjsContentUtils.isRemote) { + throw new Error('FindBar is not accessible from the content process.'); + } + try { + var browser = getContainingBrowser(domWindow); + var tabbrowser = browser.getTabBrowser(); + var tab = tabbrowser.getTabForBrowser(browser); + return tabbrowser.getFindBar(tab); + } catch (e) { + // Suppress errors for PDF files opened in the bookmark sidebar, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1248959. + return null; + } +} + +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.getComplexValue(pref, Ci.nsISupportsString).data; + } 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) { + var requestor = aChannel.notificationCallbacks ? + aChannel.notificationCallbacks : + aChannel.loadGroup.notificationCallbacks; + var win = requestor.getInterface(Components.interfaces.nsIDOMWindow); + return win; +} + +function getLocalizedStrings(path) { + var stringBundle = Cc['@mozilla.org/intl/stringbundle;1']. + getService(Ci.nsIStringBundleService). + createBundle('chrome://pdf.js/locale/' + path); + + var map = {}; + var enumerator = stringBundle.getSimpleEnumeration(); + while (enumerator.hasMoreElements()) { + var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); + 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; +} + +function createNewChannel(uri, node) { + return NetUtil.newChannel({ + uri: uri, + loadUsingSystemPrincipal: true, + }); +} + +function asyncOpenChannel(channel, listener, context) { + return channel.asyncOpen2(listener); +} + +function asyncFetchChannel(channel, callback) { + return NetUtil.asyncFetch(channel, callback); +} + +// PDF data storage +function PdfDataListener(length) { + this.length = length; // less than 0, if length is unknown + this.buffer = null; + 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. + if (!this.buffer) { + this.buffer = new Uint8Array(chunk); + } else { + var buffer = this.buffer; + var newBuffer = new Uint8Array(buffer.length + chunk.length); + newBuffer.set(buffer); + newBuffer.set(chunk, buffer.length); + this.buffer = newBuffer; + } + 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() { + var result = this.buffer; + this.buffer = null; + return result; + }, + 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: function() {}, + 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 priviledged actions. +function ChromeActions(domWindow, contentDispositionFilename) { + this.domWindow = domWindow; + this.contentDispositionFilename = contentDispositionFilename; + this.telemetryState = { + documentInfo: false, + firstPageInfo: false, + streamTypesUsed: [], + fontTypesUsed: [], + startAt: Date.now() + }; +} + +ChromeActions.prototype = { + isInPrivateBrowsing: function() { + return PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow); + }, + download: function(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 = createNewChannel(blobUri, this.domWindow.document); + if ('nsIPrivateBrowsingChannel' in Ci && + netChannel instanceof Ci.nsIPrivateBrowsingChannel) { + netChannel.setPrivate(docIsPrivate); + } + asyncFetchChannel(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: function(aRequest, aContext) { + var loadContext = self.domWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); + this.extListener = extHelperAppSvc.doContent( + (data.isAttachment ? 'application/octet-stream' : + 'application/pdf'), + aRequest, loadContext, false); + this.extListener.onStartRequest(aRequest, aContext); + }, + onStopRequest: function(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: function(aRequest, aContext, aInputStream, aOffset, + aCount) { + this.extListener.onDataAvailable(aRequest, aContext, aInputStream, + aOffset, aCount); + } + }; + + asyncOpenChannel(channel, listener, null); + }); + }, + getLocale: function() { + return getStringPref('general.useragent.locale', 'en-US'); + }, + getStrings: function(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: function() { + // Integrated find is only supported when we're not in a frame + if (this.domWindow.frameElement !== null) { + return false; + } + + // ... and we are in a child process + if (PdfjsContentUtils.isRemote) { + return true; + } + + // ... or when the new find events code exists. + var findBar = getFindBar(this.domWindow); + return !!findBar && ('updateControlState' in findBar); + }, + supportsDocumentFonts: function() { + var prefBrowser = getIntPref('browser.display.use_document_fonts', 1); + var prefGfx = getBoolPref('gfx.downloadable_fonts.enabled', true); + return (!!prefBrowser && prefGfx); + }, + supportsDocumentColors: function() { + return getIntPref('browser.display.document_color_use', 0) !== 2; + }, + supportedMouseWheelZoomModifierKeys: function() { + return { + ctrlKey: getIntPref('mousewheel.with_control.action', 3) === 3, + metaKey: getIntPref('mousewheel.with_meta.action', 1) === 3, + }; + }, + reportTelemetry: function (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; + } + }, + fallback: function(args, sendResponse) { + var featureId = args.featureId; + var url = args.url; + + var self = this; + 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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + + winmm.addMessageListener('PDFJS:Child:fallbackDownload', + function fallbackDownload(msg) { + let data = msg.data; + sendResponse(data.download); + + winmm.removeMessageListener('PDFJS:Child:fallbackDownload', + fallbackDownload); + }); + }, + updateFindControlState: function(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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + + winmm.sendAsyncMessage('PDFJS:Parent:updateControlState', data); + }, + setPreferences: function(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: function(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; + } + } + if (sendResponse) { + sendResponse(JSON.stringify(currentPrefs)); + } else { + return JSON.stringify(currentPrefs); + } + } +}; + +var RangedChromeActions = (function RangedChromeActionsClosure() { + /** + * This is for range requests + */ + function RangedChromeActions( + domWindow, contentDispositionFilename, originalRequest, + rangeEnabled, streamingEnabled, dataListener) { + + ChromeActions.call(this, 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: function(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() { + const XMLHttpRequest = Components.Constructor( + '@mozilla.org/xmlextras/xmlhttprequest;1'); + var xhr = new XMLHttpRequest(); + xhr.addEventListener('readystatechange', xhr_onreadystatechange); + return xhr; + }; + + this.networkManager = new NetworkManager(this.pdfUrl, { + httpHeaders: httpHeaderVisitor.headers, + getXhr: 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(); + }); + } + + RangedChromeActions.prototype = Object.create(ChromeActions.prototype); + var proto = RangedChromeActions.prototype; + proto.constructor = RangedChromeActions; + + proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() { + var self = this; + 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 = function (loaded, total) { + self.domWindow.postMessage({ + pdfjsLoadAction: 'progressiveRead', + loaded: loaded, + total: total, + chunk: self.dataListener.readData() + }, '*'); + }; + this.dataListener.oncomplete = function () { + self.dataListener = null; + }; + } + + this.domWindow.postMessage({ + pdfjsLoadAction: 'supportsRangedLoading', + rangeEnabled: this.rangeEnabled, + streamingEnabled: this.streamingEnabled, + pdfUrl: this.pdfUrl, + length: this.contentLength, + data: data + }, '*'); + + return true; + }; + + proto.requestDataRange = function RangedChromeActions_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(args) { + domWindow.postMessage({ + pdfjsLoadAction: 'range', + begin: args.begin, + chunk: args.chunk + }, '*'); + }, + onProgress: function RangedChromeActions_onProgress(evt) { + domWindow.postMessage({ + pdfjsLoadAction: 'rangeProgress', + loaded: evt.loaded, + }, '*'); + } + }); + }; + + proto.abortLoading = function RangedChromeActions_abortLoading() { + this.networkManager.abortAllRequests(); + if (this.originalRequest) { + this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); + this.originalRequest = null; + } + this.dataListener = null; + }; + + return RangedChromeActions; +})(); + +var StandardChromeActions = (function StandardChromeActionsClosure() { + + /** + * This is for a single network stream + */ + function StandardChromeActions(domWindow, contentDispositionFilename, + originalRequest, dataListener) { + + ChromeActions.call(this, domWindow, contentDispositionFilename); + this.originalRequest = originalRequest; + this.dataListener = dataListener; + } + + StandardChromeActions.prototype = Object.create(ChromeActions.prototype); + var proto = StandardChromeActions.prototype; + proto.constructor = StandardChromeActions; + + proto.initPassiveLoading = + function StandardChromeActions_initPassiveLoading() { + + if (!this.dataListener) { + return false; + } + + var self = this; + + this.dataListener.onprogress = function ChromeActions_dataListenerProgress( + loaded, total) { + self.domWindow.postMessage({ + pdfjsLoadAction: 'progress', + loaded: loaded, + total: total + }, '*'); + }; + + this.dataListener.oncomplete = + function StandardChromeActions_dataListenerComplete(data, errorCode) { + self.domWindow.postMessage({ + pdfjsLoadAction: 'complete', + data: data, + errorCode: errorCode + }, '*'); + + self.dataListener = null; + self.originalRequest = null; + }; + + return true; + }; + + proto.abortLoading = function StandardChromeActions_abortLoading() { + if (this.originalRequest) { + this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); + this.originalRequest = null; + } + this.dataListener = null; + }; + + return StandardChromeActions; +})(); + +// Event listener to trigger chrome privileged code. +function RequestListener(actions) { + this.actions = actions; +} +// Receive an event and synchronously or asynchronously responds. +RequestListener.prototype.receive = function(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(response) { + try { + var listener = doc.createEvent('CustomEvent'); + let detail = Cu.cloneInto({ response: response }, 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. +function FindEventManager(contentWindow) { + this.contentWindow = contentWindow; + this.winmm = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); +} + +FindEventManager.prototype.bind = function() { + var unload = function(e) { + this.unbind(); + this.contentWindow.removeEventListener(e.type, unload); + }.bind(this); + 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); +}; + +FindEventManager.prototype.receiveMessage = function(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); +}; + +FindEventManager.prototype.unbind = function() { + this.winmm.sendAsyncMessage('PDFJS:Parent:removeEventListener'); +}; + +function PdfStreamConverter() { +} + +PdfStreamConverter.prototype = { + + // properties required for XPCOM registration: + classID: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2291}'), + classDescription: 'pdf.js Component', + contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*', + + classID2: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2292}'), + contractID2: '@mozilla.org/streamconv;1?from=application/pdf&to=text/html', + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISupports, + 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: function(aFromStream, aFromType, aToType, aCtxt) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + // nsIStreamConverter::asyncConvertData + asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { + // Store the listener passed to us + this.listener = aListener; + }, + + // nsIStreamListener::onDataAvailable + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { + if (!this.dataListener) { + return; + } + + var binaryStream = this.binaryStream; + binaryStream.setInputStream(aInputStream); + var chunk = binaryStream.readByteArray(aCount); + this.dataListener.append(chunk); + }, + + // nsIRequestObserver::onStartRequest + onStartRequest: function(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().indexOf('disablerange=true') < 0); + streamRequest = contentEncoding === 'identity' && + aRequest.contentLength >= 0 && + !getBoolPref(PREF_PREFIX + '.disableStream', false) && + (!isPDFBugEnabled || + hash.toLowerCase().indexOf('disablestream=true') < 0); + } + + 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 = createNewChannel(PDF_VIEWER_WEB_PAGE, null); + + 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: function(request, context) { + listener.onStartRequest(aRequest, aContext); + }, + onDataAvailable: function(request, context, inputStream, offset, count) { + listener.onDataAvailable(aRequest, aContext, inputStream, + offset, count); + }, + onStopRequest: function(request, context, statusCode) { + // We get the DOM window here instead of before the request since it + // may have changed during a redirect. + var domWindow = getDOMWindow(channel); + 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.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 ssm = Cc['@mozilla.org/scriptsecuritymanager;1'] + .getService(Ci.nsIScriptSecurityManager); + var uri = NetUtil.newURI(PDF_VIEWER_WEB_PAGE, null, null); + var resourcePrincipal; + resourcePrincipal = + ssm.createCodebasePrincipal(uri, aRequest.loadInfo.originAttributes); + aRequest.owner = resourcePrincipal; + asyncOpenChannel(channel, proxy, aContext); + }, + + // nsIRequestObserver::onStopRequest + onStopRequest: function(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; + } +}; + |