1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,978 @@ 1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 1.6 +/* Copyright 2012 Mozilla Foundation 1.7 + * 1.8 + * Licensed under the Apache License, Version 2.0 (the "License"); 1.9 + * you may not use this file except in compliance with the License. 1.10 + * You may obtain a copy of the License at 1.11 + * 1.12 + * http://www.apache.org/licenses/LICENSE-2.0 1.13 + * 1.14 + * Unless required by applicable law or agreed to in writing, software 1.15 + * distributed under the License is distributed on an "AS IS" BASIS, 1.16 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1.17 + * See the License for the specific language governing permissions and 1.18 + * limitations under the License. 1.19 + */ 1.20 +/* jshint esnext:true */ 1.21 +/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils, 1.22 + dump, NetworkManager, PdfJsTelemetry */ 1.23 + 1.24 +'use strict'; 1.25 + 1.26 +var EXPORTED_SYMBOLS = ['PdfStreamConverter']; 1.27 + 1.28 +const Cc = Components.classes; 1.29 +const Ci = Components.interfaces; 1.30 +const Cr = Components.results; 1.31 +const Cu = Components.utils; 1.32 +// True only if this is the version of pdf.js that is included with firefox. 1.33 +const MOZ_CENTRAL = JSON.parse('true'); 1.34 +const PDFJS_EVENT_ID = 'pdf.js.message'; 1.35 +const PDF_CONTENT_TYPE = 'application/pdf'; 1.36 +const PREF_PREFIX = 'pdfjs'; 1.37 +const PDF_VIEWER_WEB_PAGE = 'resource://pdf.js/web/viewer.html'; 1.38 +const MAX_NUMBER_OF_PREFS = 50; 1.39 +const MAX_STRING_PREF_LENGTH = 128; 1.40 + 1.41 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.42 +Cu.import('resource://gre/modules/Services.jsm'); 1.43 +Cu.import('resource://gre/modules/NetUtil.jsm'); 1.44 + 1.45 +XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager', 1.46 + 'resource://pdf.js/network.js'); 1.47 + 1.48 +XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils', 1.49 + 'resource://gre/modules/PrivateBrowsingUtils.jsm'); 1.50 + 1.51 +XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry', 1.52 + 'resource://pdf.js/PdfJsTelemetry.jsm'); 1.53 + 1.54 +var Svc = {}; 1.55 +XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', 1.56 + '@mozilla.org/mime;1', 1.57 + 'nsIMIMEService'); 1.58 + 1.59 +function getContainingBrowser(domWindow) { 1.60 + return domWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.61 + .getInterface(Ci.nsIWebNavigation) 1.62 + .QueryInterface(Ci.nsIDocShell) 1.63 + .chromeEventHandler; 1.64 +} 1.65 + 1.66 +function getChromeWindow(domWindow) { 1.67 + return getContainingBrowser(domWindow).ownerDocument.defaultView; 1.68 +} 1.69 + 1.70 +function getFindBar(domWindow) { 1.71 + var browser = getContainingBrowser(domWindow); 1.72 + try { 1.73 + var tabbrowser = browser.getTabBrowser(); 1.74 + var tab = tabbrowser._getTabForBrowser(browser); 1.75 + return tabbrowser.getFindBar(tab); 1.76 + } catch (e) { 1.77 + // FF22 has no _getTabForBrowser, and FF24 has no getFindBar 1.78 + var chromeWindow = browser.ownerDocument.defaultView; 1.79 + return chromeWindow.gFindBar; 1.80 + } 1.81 +} 1.82 + 1.83 +function setBoolPref(pref, value) { 1.84 + Services.prefs.setBoolPref(pref, value); 1.85 +} 1.86 + 1.87 +function getBoolPref(pref, def) { 1.88 + try { 1.89 + return Services.prefs.getBoolPref(pref); 1.90 + } catch (ex) { 1.91 + return def; 1.92 + } 1.93 +} 1.94 + 1.95 +function setIntPref(pref, value) { 1.96 + Services.prefs.setIntPref(pref, value); 1.97 +} 1.98 + 1.99 +function getIntPref(pref, def) { 1.100 + try { 1.101 + return Services.prefs.getIntPref(pref); 1.102 + } catch (ex) { 1.103 + return def; 1.104 + } 1.105 +} 1.106 + 1.107 +function setStringPref(pref, value) { 1.108 + var str = Cc['@mozilla.org/supports-string;1'] 1.109 + .createInstance(Ci.nsISupportsString); 1.110 + str.data = value; 1.111 + Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str); 1.112 +} 1.113 + 1.114 +function getStringPref(pref, def) { 1.115 + try { 1.116 + return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data; 1.117 + } catch (ex) { 1.118 + return def; 1.119 + } 1.120 +} 1.121 + 1.122 +function log(aMsg) { 1.123 + if (!getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false)) 1.124 + return; 1.125 + var msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg); 1.126 + Services.console.logStringMessage(msg); 1.127 + dump(msg + '\n'); 1.128 +} 1.129 + 1.130 +function getDOMWindow(aChannel) { 1.131 + var requestor = aChannel.notificationCallbacks ? 1.132 + aChannel.notificationCallbacks : 1.133 + aChannel.loadGroup.notificationCallbacks; 1.134 + var win = requestor.getInterface(Components.interfaces.nsIDOMWindow); 1.135 + return win; 1.136 +} 1.137 + 1.138 +function getLocalizedStrings(path) { 1.139 + var stringBundle = Cc['@mozilla.org/intl/stringbundle;1']. 1.140 + getService(Ci.nsIStringBundleService). 1.141 + createBundle('chrome://pdf.js/locale/' + path); 1.142 + 1.143 + var map = {}; 1.144 + var enumerator = stringBundle.getSimpleEnumeration(); 1.145 + while (enumerator.hasMoreElements()) { 1.146 + var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); 1.147 + var key = string.key, property = 'textContent'; 1.148 + var i = key.lastIndexOf('.'); 1.149 + if (i >= 0) { 1.150 + property = key.substring(i + 1); 1.151 + key = key.substring(0, i); 1.152 + } 1.153 + if (!(key in map)) 1.154 + map[key] = {}; 1.155 + map[key][property] = string.value; 1.156 + } 1.157 + return map; 1.158 +} 1.159 +function getLocalizedString(strings, id, property) { 1.160 + property = property || 'textContent'; 1.161 + if (id in strings) 1.162 + return strings[id][property]; 1.163 + return id; 1.164 +} 1.165 + 1.166 +// PDF data storage 1.167 +function PdfDataListener(length) { 1.168 + this.length = length; // less than 0, if length is unknown 1.169 + this.data = new Uint8Array(length >= 0 ? length : 0x10000); 1.170 + this.loaded = 0; 1.171 +} 1.172 + 1.173 +PdfDataListener.prototype = { 1.174 + append: function PdfDataListener_append(chunk) { 1.175 + var willBeLoaded = this.loaded + chunk.length; 1.176 + if (this.length >= 0 && this.length < willBeLoaded) { 1.177 + this.length = -1; // reset the length, server is giving incorrect one 1.178 + } 1.179 + if (this.length < 0 && this.data.length < willBeLoaded) { 1.180 + // data length is unknown and new chunk will not fit in the existing 1.181 + // buffer, resizing the buffer by doubling the its last length 1.182 + var newLength = this.data.length; 1.183 + for (; newLength < willBeLoaded; newLength *= 2) {} 1.184 + var newData = new Uint8Array(newLength); 1.185 + newData.set(this.data); 1.186 + this.data = newData; 1.187 + } 1.188 + this.data.set(chunk, this.loaded); 1.189 + this.loaded = willBeLoaded; 1.190 + this.onprogress(this.loaded, this.length >= 0 ? this.length : void(0)); 1.191 + }, 1.192 + getData: function PdfDataListener_getData() { 1.193 + var data = this.data; 1.194 + if (this.loaded != data.length) 1.195 + data = data.subarray(0, this.loaded); 1.196 + delete this.data; // releasing temporary storage 1.197 + return data; 1.198 + }, 1.199 + finish: function PdfDataListener_finish() { 1.200 + this.isDataReady = true; 1.201 + if (this.oncompleteCallback) { 1.202 + this.oncompleteCallback(this.getData()); 1.203 + } 1.204 + }, 1.205 + error: function PdfDataListener_error(errorCode) { 1.206 + this.errorCode = errorCode; 1.207 + if (this.oncompleteCallback) { 1.208 + this.oncompleteCallback(null, errorCode); 1.209 + } 1.210 + }, 1.211 + onprogress: function() {}, 1.212 + get oncomplete() { 1.213 + return this.oncompleteCallback; 1.214 + }, 1.215 + set oncomplete(value) { 1.216 + this.oncompleteCallback = value; 1.217 + if (this.isDataReady) { 1.218 + value(this.getData()); 1.219 + } 1.220 + if (this.errorCode) { 1.221 + value(null, this.errorCode); 1.222 + } 1.223 + } 1.224 +}; 1.225 + 1.226 +// All the priviledged actions. 1.227 +function ChromeActions(domWindow, contentDispositionFilename) { 1.228 + this.domWindow = domWindow; 1.229 + this.contentDispositionFilename = contentDispositionFilename; 1.230 + this.telemetryState = { 1.231 + documentInfo: false, 1.232 + firstPageInfo: false, 1.233 + streamTypesUsed: [], 1.234 + startAt: Date.now() 1.235 + }; 1.236 +} 1.237 + 1.238 +ChromeActions.prototype = { 1.239 + isInPrivateBrowsing: function() { 1.240 + return PrivateBrowsingUtils.isWindowPrivate(this.domWindow); 1.241 + }, 1.242 + download: function(data, sendResponse) { 1.243 + var self = this; 1.244 + var originalUrl = data.originalUrl; 1.245 + // The data may not be downloaded so we need just retry getting the pdf with 1.246 + // the original url. 1.247 + var originalUri = NetUtil.newURI(data.originalUrl); 1.248 + var filename = data.filename; 1.249 + if (typeof filename !== 'string' || 1.250 + (!/\.pdf$/i.test(filename) && !data.isAttachment)) { 1.251 + filename = 'document.pdf'; 1.252 + } 1.253 + var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri; 1.254 + var extHelperAppSvc = 1.255 + Cc['@mozilla.org/uriloader/external-helper-app-service;1']. 1.256 + getService(Ci.nsIExternalHelperAppService); 1.257 + var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1']. 1.258 + getService(Ci.nsIWindowWatcher).activeWindow; 1.259 + 1.260 + var docIsPrivate = this.isInPrivateBrowsing(); 1.261 + var netChannel = NetUtil.newChannel(blobUri); 1.262 + if ('nsIPrivateBrowsingChannel' in Ci && 1.263 + netChannel instanceof Ci.nsIPrivateBrowsingChannel) { 1.264 + netChannel.setPrivate(docIsPrivate); 1.265 + } 1.266 + NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) { 1.267 + if (!Components.isSuccessCode(aResult)) { 1.268 + if (sendResponse) 1.269 + sendResponse(true); 1.270 + return; 1.271 + } 1.272 + // Create a nsIInputStreamChannel so we can set the url on the channel 1.273 + // so the filename will be correct. 1.274 + var channel = Cc['@mozilla.org/network/input-stream-channel;1']. 1.275 + createInstance(Ci.nsIInputStreamChannel); 1.276 + channel.QueryInterface(Ci.nsIChannel); 1.277 + try { 1.278 + // contentDisposition/contentDispositionFilename is readonly before FF18 1.279 + channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT; 1.280 + if (self.contentDispositionFilename) { 1.281 + channel.contentDispositionFilename = self.contentDispositionFilename; 1.282 + } else { 1.283 + channel.contentDispositionFilename = filename; 1.284 + } 1.285 + } catch (e) {} 1.286 + channel.setURI(originalUri); 1.287 + channel.contentStream = aInputStream; 1.288 + if ('nsIPrivateBrowsingChannel' in Ci && 1.289 + channel instanceof Ci.nsIPrivateBrowsingChannel) { 1.290 + channel.setPrivate(docIsPrivate); 1.291 + } 1.292 + 1.293 + var listener = { 1.294 + extListener: null, 1.295 + onStartRequest: function(aRequest, aContext) { 1.296 + this.extListener = extHelperAppSvc.doContent((data.isAttachment ? '' : 1.297 + 'application/pdf'), 1.298 + aRequest, frontWindow, false); 1.299 + this.extListener.onStartRequest(aRequest, aContext); 1.300 + }, 1.301 + onStopRequest: function(aRequest, aContext, aStatusCode) { 1.302 + if (this.extListener) 1.303 + this.extListener.onStopRequest(aRequest, aContext, aStatusCode); 1.304 + // Notify the content code we're done downloading. 1.305 + if (sendResponse) 1.306 + sendResponse(false); 1.307 + }, 1.308 + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, 1.309 + aCount) { 1.310 + this.extListener.onDataAvailable(aRequest, aContext, aInputStream, 1.311 + aOffset, aCount); 1.312 + } 1.313 + }; 1.314 + 1.315 + channel.asyncOpen(listener, null); 1.316 + }); 1.317 + }, 1.318 + getLocale: function() { 1.319 + return getStringPref('general.useragent.locale', 'en-US'); 1.320 + }, 1.321 + getStrings: function(data) { 1.322 + try { 1.323 + // Lazy initialization of localizedStrings 1.324 + if (!('localizedStrings' in this)) 1.325 + this.localizedStrings = getLocalizedStrings('viewer.properties'); 1.326 + 1.327 + var result = this.localizedStrings[data]; 1.328 + return JSON.stringify(result || null); 1.329 + } catch (e) { 1.330 + log('Unable to retrive localized strings: ' + e); 1.331 + return 'null'; 1.332 + } 1.333 + }, 1.334 + pdfBugEnabled: function() { 1.335 + return getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false); 1.336 + }, 1.337 + supportsIntegratedFind: function() { 1.338 + // Integrated find is only supported when we're not in a frame 1.339 + if (this.domWindow.frameElement !== null) { 1.340 + return false; 1.341 + } 1.342 + // ... and when the new find events code exists. 1.343 + var findBar = getFindBar(this.domWindow); 1.344 + return findBar && ('updateControlState' in findBar); 1.345 + }, 1.346 + supportsDocumentFonts: function() { 1.347 + var prefBrowser = getIntPref('browser.display.use_document_fonts', 1); 1.348 + var prefGfx = getBoolPref('gfx.downloadable_fonts.enabled', true); 1.349 + return (!!prefBrowser && prefGfx); 1.350 + }, 1.351 + supportsDocumentColors: function() { 1.352 + return getBoolPref('browser.display.use_document_colors', true); 1.353 + }, 1.354 + reportTelemetry: function (data) { 1.355 + var probeInfo = JSON.parse(data); 1.356 + switch (probeInfo.type) { 1.357 + case 'documentInfo': 1.358 + if (!this.telemetryState.documentInfo) { 1.359 + PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0); 1.360 + PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0); 1.361 + if (probeInfo.formType) { 1.362 + PdfJsTelemetry.onForm(probeInfo.formType === 'acroform'); 1.363 + } 1.364 + this.telemetryState.documentInfo = true; 1.365 + } 1.366 + break; 1.367 + case 'pageInfo': 1.368 + if (!this.telemetryState.firstPageInfo) { 1.369 + var duration = Date.now() - this.telemetryState.startAt; 1.370 + PdfJsTelemetry.onTimeToView(duration); 1.371 + this.telemetryState.firstPageInfo = true; 1.372 + } 1.373 + break; 1.374 + case 'streamInfo': 1.375 + if (!Array.isArray(probeInfo.streamTypes)) { 1.376 + break; 1.377 + } 1.378 + for (var i = 0; i < probeInfo.streamTypes.length; i++) { 1.379 + var streamTypeId = probeInfo.streamTypes[i] | 0; 1.380 + if (streamTypeId >= 0 && streamTypeId < 10 && 1.381 + !this.telemetryState.streamTypesUsed[streamTypeId]) { 1.382 + PdfJsTelemetry.onStreamType(streamTypeId); 1.383 + this.telemetryState.streamTypesUsed[streamTypeId] = true; 1.384 + } 1.385 + } 1.386 + break; 1.387 + } 1.388 + }, 1.389 + fallback: function(args, sendResponse) { 1.390 + var featureId = args.featureId; 1.391 + var url = args.url; 1.392 + 1.393 + var self = this; 1.394 + var domWindow = this.domWindow; 1.395 + var strings = getLocalizedStrings('chrome.properties'); 1.396 + var message; 1.397 + if (featureId === 'forms') { 1.398 + message = getLocalizedString(strings, 'unsupported_feature_forms'); 1.399 + } else { 1.400 + message = getLocalizedString(strings, 'unsupported_feature'); 1.401 + } 1.402 + 1.403 + PdfJsTelemetry.onFallback(); 1.404 + 1.405 + var notificationBox = null; 1.406 + try { 1.407 + // Based on MDN's "Working with windows in chrome code" 1.408 + var mainWindow = domWindow 1.409 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.410 + .getInterface(Components.interfaces.nsIWebNavigation) 1.411 + .QueryInterface(Components.interfaces.nsIDocShellTreeItem) 1.412 + .rootTreeItem 1.413 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.414 + .getInterface(Components.interfaces.nsIDOMWindow); 1.415 + var browser = mainWindow.gBrowser 1.416 + .getBrowserForDocument(domWindow.top.document); 1.417 + notificationBox = mainWindow.gBrowser.getNotificationBox(browser); 1.418 + } catch (e) { 1.419 + log('Unable to get a notification box for the fallback message'); 1.420 + return; 1.421 + } 1.422 + 1.423 + // Flag so we don't call the response callback twice, since if the user 1.424 + // clicks open with different viewer both the button callback and 1.425 + // eventCallback will be called. 1.426 + var sentResponse = false; 1.427 + var buttons = [{ 1.428 + label: getLocalizedString(strings, 'open_with_different_viewer'), 1.429 + accessKey: getLocalizedString(strings, 'open_with_different_viewer', 1.430 + 'accessKey'), 1.431 + callback: function() { 1.432 + sentResponse = true; 1.433 + sendResponse(true); 1.434 + } 1.435 + }]; 1.436 + notificationBox.appendNotification(message, 'pdfjs-fallback', null, 1.437 + notificationBox.PRIORITY_INFO_LOW, 1.438 + buttons, 1.439 + function eventsCallback(eventType) { 1.440 + // Currently there is only one event "removed" but if there are any other 1.441 + // added in the future we still only care about removed at the moment. 1.442 + if (eventType !== 'removed') 1.443 + return; 1.444 + // Don't send a response again if we already responded when the button was 1.445 + // clicked. 1.446 + if (!sentResponse) 1.447 + sendResponse(false); 1.448 + }); 1.449 + }, 1.450 + updateFindControlState: function(data) { 1.451 + if (!this.supportsIntegratedFind()) 1.452 + return; 1.453 + // Verify what we're sending to the findbar. 1.454 + var result = data.result; 1.455 + var findPrevious = data.findPrevious; 1.456 + var findPreviousType = typeof findPrevious; 1.457 + if ((typeof result !== 'number' || result < 0 || result > 3) || 1.458 + (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) { 1.459 + return; 1.460 + } 1.461 + getFindBar(this.domWindow).updateControlState(result, findPrevious); 1.462 + }, 1.463 + setPreferences: function(prefs, sendResponse) { 1.464 + var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.'); 1.465 + var numberOfPrefs = 0; 1.466 + var prefValue, prefName; 1.467 + for (var key in prefs) { 1.468 + if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) { 1.469 + log('setPreferences - Exceeded the maximum number of preferences ' + 1.470 + 'that is allowed to be set at once.'); 1.471 + break; 1.472 + } else if (!defaultBranch.getPrefType(key)) { 1.473 + continue; 1.474 + } 1.475 + prefValue = prefs[key]; 1.476 + prefName = (PREF_PREFIX + '.' + key); 1.477 + switch (typeof prefValue) { 1.478 + case 'boolean': 1.479 + setBoolPref(prefName, prefValue); 1.480 + break; 1.481 + case 'number': 1.482 + setIntPref(prefName, prefValue); 1.483 + break; 1.484 + case 'string': 1.485 + if (prefValue.length > MAX_STRING_PREF_LENGTH) { 1.486 + log('setPreferences - Exceeded the maximum allowed length ' + 1.487 + 'for a string preference.'); 1.488 + } else { 1.489 + setStringPref(prefName, prefValue); 1.490 + } 1.491 + break; 1.492 + } 1.493 + } 1.494 + if (sendResponse) { 1.495 + sendResponse(true); 1.496 + } 1.497 + }, 1.498 + getPreferences: function(prefs, sendResponse) { 1.499 + var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.'); 1.500 + var currentPrefs = {}, numberOfPrefs = 0; 1.501 + var prefValue, prefName; 1.502 + for (var key in prefs) { 1.503 + if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) { 1.504 + log('getPreferences - Exceeded the maximum number of preferences ' + 1.505 + 'that is allowed to be fetched at once.'); 1.506 + break; 1.507 + } else if (!defaultBranch.getPrefType(key)) { 1.508 + continue; 1.509 + } 1.510 + prefValue = prefs[key]; 1.511 + prefName = (PREF_PREFIX + '.' + key); 1.512 + switch (typeof prefValue) { 1.513 + case 'boolean': 1.514 + currentPrefs[key] = getBoolPref(prefName, prefValue); 1.515 + break; 1.516 + case 'number': 1.517 + currentPrefs[key] = getIntPref(prefName, prefValue); 1.518 + break; 1.519 + case 'string': 1.520 + currentPrefs[key] = getStringPref(prefName, prefValue); 1.521 + break; 1.522 + } 1.523 + } 1.524 + if (sendResponse) { 1.525 + sendResponse(JSON.stringify(currentPrefs)); 1.526 + } else { 1.527 + return JSON.stringify(currentPrefs); 1.528 + } 1.529 + } 1.530 +}; 1.531 + 1.532 +var RangedChromeActions = (function RangedChromeActionsClosure() { 1.533 + /** 1.534 + * This is for range requests 1.535 + */ 1.536 + function RangedChromeActions( 1.537 + domWindow, contentDispositionFilename, originalRequest, 1.538 + dataListener) { 1.539 + 1.540 + ChromeActions.call(this, domWindow, contentDispositionFilename); 1.541 + this.dataListener = dataListener; 1.542 + this.originalRequest = originalRequest; 1.543 + 1.544 + this.pdfUrl = originalRequest.URI.spec; 1.545 + this.contentLength = originalRequest.contentLength; 1.546 + 1.547 + // Pass all the headers from the original request through 1.548 + var httpHeaderVisitor = { 1.549 + headers: {}, 1.550 + visitHeader: function(aHeader, aValue) { 1.551 + if (aHeader === 'Range') { 1.552 + // When loading the PDF from cache, firefox seems to set the Range 1.553 + // request header to fetch only the unfetched portions of the file 1.554 + // (e.g. 'Range: bytes=1024-'). However, we want to set this header 1.555 + // manually to fetch the PDF in chunks. 1.556 + return; 1.557 + } 1.558 + this.headers[aHeader] = aValue; 1.559 + } 1.560 + }; 1.561 + originalRequest.visitRequestHeaders(httpHeaderVisitor); 1.562 + 1.563 + var self = this; 1.564 + var xhr_onreadystatechange = function xhr_onreadystatechange() { 1.565 + if (this.readyState === 1) { // LOADING 1.566 + var netChannel = this.channel; 1.567 + if ('nsIPrivateBrowsingChannel' in Ci && 1.568 + netChannel instanceof Ci.nsIPrivateBrowsingChannel) { 1.569 + var docIsPrivate = self.isInPrivateBrowsing(); 1.570 + netChannel.setPrivate(docIsPrivate); 1.571 + } 1.572 + } 1.573 + }; 1.574 + var getXhr = function getXhr() { 1.575 + const XMLHttpRequest = Components.Constructor( 1.576 + '@mozilla.org/xmlextras/xmlhttprequest;1'); 1.577 + var xhr = new XMLHttpRequest(); 1.578 + xhr.addEventListener('readystatechange', xhr_onreadystatechange); 1.579 + return xhr; 1.580 + }; 1.581 + 1.582 + this.networkManager = new NetworkManager(this.pdfUrl, { 1.583 + httpHeaders: httpHeaderVisitor.headers, 1.584 + getXhr: getXhr 1.585 + }); 1.586 + 1.587 + // If we are in range request mode, this means we manually issued xhr 1.588 + // requests, which we need to abort when we leave the page 1.589 + domWindow.addEventListener('unload', function unload(e) { 1.590 + self.networkManager.abortAllRequests(); 1.591 + domWindow.removeEventListener(e.type, unload); 1.592 + }); 1.593 + } 1.594 + 1.595 + RangedChromeActions.prototype = Object.create(ChromeActions.prototype); 1.596 + var proto = RangedChromeActions.prototype; 1.597 + proto.constructor = RangedChromeActions; 1.598 + 1.599 + proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() { 1.600 + this.originalRequest.cancel(Cr.NS_BINDING_ABORTED); 1.601 + this.originalRequest = null; 1.602 + this.domWindow.postMessage({ 1.603 + pdfjsLoadAction: 'supportsRangedLoading', 1.604 + pdfUrl: this.pdfUrl, 1.605 + length: this.contentLength, 1.606 + data: this.dataListener.getData() 1.607 + }, '*'); 1.608 + this.dataListener = null; 1.609 + 1.610 + return true; 1.611 + }; 1.612 + 1.613 + proto.requestDataRange = function RangedChromeActions_requestDataRange(args) { 1.614 + var begin = args.begin; 1.615 + var end = args.end; 1.616 + var domWindow = this.domWindow; 1.617 + // TODO(mack): Support error handler. We're not currently not handling 1.618 + // errors from chrome code for non-range requests, so this doesn't 1.619 + // seem high-pri 1.620 + this.networkManager.requestRange(begin, end, { 1.621 + onDone: function RangedChromeActions_onDone(args) { 1.622 + domWindow.postMessage({ 1.623 + pdfjsLoadAction: 'range', 1.624 + begin: args.begin, 1.625 + chunk: args.chunk 1.626 + }, '*'); 1.627 + }, 1.628 + onProgress: function RangedChromeActions_onProgress(evt) { 1.629 + domWindow.postMessage({ 1.630 + pdfjsLoadAction: 'rangeProgress', 1.631 + loaded: evt.loaded, 1.632 + }, '*'); 1.633 + } 1.634 + }); 1.635 + }; 1.636 + 1.637 + return RangedChromeActions; 1.638 +})(); 1.639 + 1.640 +var StandardChromeActions = (function StandardChromeActionsClosure() { 1.641 + 1.642 + /** 1.643 + * This is for a single network stream 1.644 + */ 1.645 + function StandardChromeActions(domWindow, contentDispositionFilename, 1.646 + dataListener) { 1.647 + 1.648 + ChromeActions.call(this, domWindow, contentDispositionFilename); 1.649 + this.dataListener = dataListener; 1.650 + } 1.651 + 1.652 + StandardChromeActions.prototype = Object.create(ChromeActions.prototype); 1.653 + var proto = StandardChromeActions.prototype; 1.654 + proto.constructor = StandardChromeActions; 1.655 + 1.656 + proto.initPassiveLoading = 1.657 + function StandardChromeActions_initPassiveLoading() { 1.658 + 1.659 + if (!this.dataListener) { 1.660 + return false; 1.661 + } 1.662 + 1.663 + var self = this; 1.664 + 1.665 + this.dataListener.onprogress = function ChromeActions_dataListenerProgress( 1.666 + loaded, total) { 1.667 + self.domWindow.postMessage({ 1.668 + pdfjsLoadAction: 'progress', 1.669 + loaded: loaded, 1.670 + total: total 1.671 + }, '*'); 1.672 + }; 1.673 + 1.674 + this.dataListener.oncomplete = function ChromeActions_dataListenerComplete( 1.675 + data, errorCode) { 1.676 + self.domWindow.postMessage({ 1.677 + pdfjsLoadAction: 'complete', 1.678 + data: data, 1.679 + errorCode: errorCode 1.680 + }, '*'); 1.681 + 1.682 + delete self.dataListener; 1.683 + }; 1.684 + 1.685 + return true; 1.686 + }; 1.687 + 1.688 + return StandardChromeActions; 1.689 +})(); 1.690 + 1.691 +// Event listener to trigger chrome privedged code. 1.692 +function RequestListener(actions) { 1.693 + this.actions = actions; 1.694 +} 1.695 +// Receive an event and synchronously or asynchronously responds. 1.696 +RequestListener.prototype.receive = function(event) { 1.697 + var message = event.target; 1.698 + var doc = message.ownerDocument; 1.699 + var action = event.detail.action; 1.700 + var data = event.detail.data; 1.701 + var sync = event.detail.sync; 1.702 + var actions = this.actions; 1.703 + if (!(action in actions)) { 1.704 + log('Unknown action: ' + action); 1.705 + return; 1.706 + } 1.707 + if (sync) { 1.708 + var response = actions[action].call(this.actions, data); 1.709 + var detail = event.detail; 1.710 + detail.__exposedProps__ = {response: 'r'}; 1.711 + detail.response = response; 1.712 + } else { 1.713 + var response; 1.714 + if (!event.detail.callback) { 1.715 + doc.documentElement.removeChild(message); 1.716 + response = null; 1.717 + } else { 1.718 + response = function sendResponse(response) { 1.719 + try { 1.720 + var listener = doc.createEvent('CustomEvent'); 1.721 + listener.initCustomEvent('pdf.js.response', true, false, 1.722 + {response: response, 1.723 + __exposedProps__: {response: 'r'}}); 1.724 + return message.dispatchEvent(listener); 1.725 + } catch (e) { 1.726 + // doc is no longer accessible because the requestor is already 1.727 + // gone. unloaded content cannot receive the response anyway. 1.728 + return false; 1.729 + } 1.730 + }; 1.731 + } 1.732 + actions[action].call(this.actions, data, response); 1.733 + } 1.734 +}; 1.735 + 1.736 +// Forwards events from the eventElement to the contentWindow only if the 1.737 +// content window matches the currently selected browser window. 1.738 +function FindEventManager(eventElement, contentWindow, chromeWindow) { 1.739 + this.types = ['find', 1.740 + 'findagain', 1.741 + 'findhighlightallchange', 1.742 + 'findcasesensitivitychange']; 1.743 + this.chromeWindow = chromeWindow; 1.744 + this.contentWindow = contentWindow; 1.745 + this.eventElement = eventElement; 1.746 +} 1.747 + 1.748 +FindEventManager.prototype.bind = function() { 1.749 + var unload = function(e) { 1.750 + this.unbind(); 1.751 + this.contentWindow.removeEventListener(e.type, unload); 1.752 + }.bind(this); 1.753 + this.contentWindow.addEventListener('unload', unload); 1.754 + 1.755 + for (var i = 0; i < this.types.length; i++) { 1.756 + var type = this.types[i]; 1.757 + this.eventElement.addEventListener(type, this, true); 1.758 + } 1.759 +}; 1.760 + 1.761 +FindEventManager.prototype.handleEvent = function(e) { 1.762 + var chromeWindow = this.chromeWindow; 1.763 + var contentWindow = this.contentWindow; 1.764 + // Only forward the events if they are for our dom window. 1.765 + if (chromeWindow.gBrowser.selectedBrowser.contentWindow === contentWindow) { 1.766 + var detail = e.detail; 1.767 + detail.__exposedProps__ = { 1.768 + query: 'r', 1.769 + caseSensitive: 'r', 1.770 + highlightAll: 'r', 1.771 + findPrevious: 'r' 1.772 + }; 1.773 + var forward = contentWindow.document.createEvent('CustomEvent'); 1.774 + forward.initCustomEvent(e.type, true, true, detail); 1.775 + contentWindow.dispatchEvent(forward); 1.776 + e.preventDefault(); 1.777 + } 1.778 +}; 1.779 + 1.780 +FindEventManager.prototype.unbind = function() { 1.781 + for (var i = 0; i < this.types.length; i++) { 1.782 + var type = this.types[i]; 1.783 + this.eventElement.removeEventListener(type, this, true); 1.784 + } 1.785 +}; 1.786 + 1.787 +function PdfStreamConverter() { 1.788 +} 1.789 + 1.790 +PdfStreamConverter.prototype = { 1.791 + 1.792 + // properties required for XPCOM registration: 1.793 + classID: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2291}'), 1.794 + classDescription: 'pdf.js Component', 1.795 + contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*', 1.796 + 1.797 + QueryInterface: XPCOMUtils.generateQI([ 1.798 + Ci.nsISupports, 1.799 + Ci.nsIStreamConverter, 1.800 + Ci.nsIStreamListener, 1.801 + Ci.nsIRequestObserver 1.802 + ]), 1.803 + 1.804 + /* 1.805 + * This component works as such: 1.806 + * 1. asyncConvertData stores the listener 1.807 + * 2. onStartRequest creates a new channel, streams the viewer 1.808 + * 3. If range requests are supported: 1.809 + * 3.1. Leave the request open until the viewer is ready to switch to 1.810 + * range requests. 1.811 + * 1.812 + * If range rquests are not supported: 1.813 + * 3.1. Read the stream as it's loaded in onDataAvailable to send 1.814 + * to the viewer 1.815 + * 1.816 + * The convert function just returns the stream, it's just the synchronous 1.817 + * version of asyncConvertData. 1.818 + */ 1.819 + 1.820 + // nsIStreamConverter::convert 1.821 + convert: function(aFromStream, aFromType, aToType, aCtxt) { 1.822 + throw Cr.NS_ERROR_NOT_IMPLEMENTED; 1.823 + }, 1.824 + 1.825 + // nsIStreamConverter::asyncConvertData 1.826 + asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { 1.827 + // Store the listener passed to us 1.828 + this.listener = aListener; 1.829 + }, 1.830 + 1.831 + // nsIStreamListener::onDataAvailable 1.832 + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { 1.833 + if (!this.dataListener) { 1.834 + return; 1.835 + } 1.836 + 1.837 + var binaryStream = this.binaryStream; 1.838 + binaryStream.setInputStream(aInputStream); 1.839 + var chunk = binaryStream.readByteArray(aCount); 1.840 + this.dataListener.append(chunk); 1.841 + }, 1.842 + 1.843 + // nsIRequestObserver::onStartRequest 1.844 + onStartRequest: function(aRequest, aContext) { 1.845 + // Setup the request so we can use it below. 1.846 + var isHttpRequest = false; 1.847 + try { 1.848 + aRequest.QueryInterface(Ci.nsIHttpChannel); 1.849 + isHttpRequest = true; 1.850 + } catch (e) {} 1.851 + 1.852 + var rangeRequest = false; 1.853 + if (isHttpRequest) { 1.854 + var contentEncoding = 'identity'; 1.855 + try { 1.856 + contentEncoding = aRequest.getResponseHeader('Content-Encoding'); 1.857 + } catch (e) {} 1.858 + 1.859 + var acceptRanges; 1.860 + try { 1.861 + acceptRanges = aRequest.getResponseHeader('Accept-Ranges'); 1.862 + } catch (e) {} 1.863 + 1.864 + var hash = aRequest.URI.ref; 1.865 + rangeRequest = contentEncoding === 'identity' && 1.866 + acceptRanges === 'bytes' && 1.867 + aRequest.contentLength >= 0 && 1.868 + hash.indexOf('disableRange=true') < 0; 1.869 + } 1.870 + 1.871 + aRequest.QueryInterface(Ci.nsIChannel); 1.872 + 1.873 + aRequest.QueryInterface(Ci.nsIWritablePropertyBag); 1.874 + 1.875 + var contentDispositionFilename; 1.876 + try { 1.877 + contentDispositionFilename = aRequest.contentDispositionFilename; 1.878 + } catch (e) {} 1.879 + 1.880 + // Change the content type so we don't get stuck in a loop. 1.881 + aRequest.setProperty('contentType', aRequest.contentType); 1.882 + aRequest.contentType = 'text/html'; 1.883 + if (isHttpRequest) { 1.884 + // We trust PDF viewer, using no CSP 1.885 + aRequest.setResponseHeader('Content-Security-Policy', '', false); 1.886 + aRequest.setResponseHeader('Content-Security-Policy-Report-Only', '', 1.887 + false); 1.888 + aRequest.setResponseHeader('X-Content-Security-Policy', '', false); 1.889 + aRequest.setResponseHeader('X-Content-Security-Policy-Report-Only', '', 1.890 + false); 1.891 + } 1.892 + 1.893 + PdfJsTelemetry.onViewerIsUsed(); 1.894 + PdfJsTelemetry.onDocumentSize(aRequest.contentLength); 1.895 + 1.896 + 1.897 + // Creating storage for PDF data 1.898 + var contentLength = aRequest.contentLength; 1.899 + this.dataListener = new PdfDataListener(contentLength); 1.900 + this.binaryStream = Cc['@mozilla.org/binaryinputstream;1'] 1.901 + .createInstance(Ci.nsIBinaryInputStream); 1.902 + 1.903 + // Create a new channel that is viewer loaded as a resource. 1.904 + var ioService = Services.io; 1.905 + var channel = ioService.newChannel( 1.906 + PDF_VIEWER_WEB_PAGE, null, null); 1.907 + 1.908 + var listener = this.listener; 1.909 + var dataListener = this.dataListener; 1.910 + // Proxy all the request observer calls, when it gets to onStopRequest 1.911 + // we can get the dom window. We also intentionally pass on the original 1.912 + // request(aRequest) below so we don't overwrite the original channel and 1.913 + // trigger an assertion. 1.914 + var proxy = { 1.915 + onStartRequest: function(request, context) { 1.916 + listener.onStartRequest(aRequest, context); 1.917 + }, 1.918 + onDataAvailable: function(request, context, inputStream, offset, count) { 1.919 + listener.onDataAvailable(aRequest, context, inputStream, offset, count); 1.920 + }, 1.921 + onStopRequest: function(request, context, statusCode) { 1.922 + // We get the DOM window here instead of before the request since it 1.923 + // may have changed during a redirect. 1.924 + var domWindow = getDOMWindow(channel); 1.925 + var actions; 1.926 + if (rangeRequest) { 1.927 + actions = new RangedChromeActions( 1.928 + domWindow, contentDispositionFilename, aRequest, dataListener); 1.929 + } else { 1.930 + actions = new StandardChromeActions( 1.931 + domWindow, contentDispositionFilename, dataListener); 1.932 + } 1.933 + var requestListener = new RequestListener(actions); 1.934 + domWindow.addEventListener(PDFJS_EVENT_ID, function(event) { 1.935 + requestListener.receive(event); 1.936 + }, false, true); 1.937 + if (actions.supportsIntegratedFind()) { 1.938 + var chromeWindow = getChromeWindow(domWindow); 1.939 + var findBar = getFindBar(domWindow); 1.940 + var findEventManager = new FindEventManager(findBar, 1.941 + domWindow, 1.942 + chromeWindow); 1.943 + findEventManager.bind(); 1.944 + } 1.945 + listener.onStopRequest(aRequest, context, statusCode); 1.946 + } 1.947 + }; 1.948 + 1.949 + // Keep the URL the same so the browser sees it as the same. 1.950 + channel.originalURI = aRequest.URI; 1.951 + channel.loadGroup = aRequest.loadGroup; 1.952 + 1.953 + // We can use resource principal when data is fetched by the chrome 1.954 + // e.g. useful for NoScript 1.955 + var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1'] 1.956 + .getService(Ci.nsIScriptSecurityManager); 1.957 + var uri = ioService.newURI(PDF_VIEWER_WEB_PAGE, null, null); 1.958 + // FF16 and below had getCodebasePrincipal, it was replaced by 1.959 + // getNoAppCodebasePrincipal (bug 758258). 1.960 + var resourcePrincipal = 'getNoAppCodebasePrincipal' in securityManager ? 1.961 + securityManager.getNoAppCodebasePrincipal(uri) : 1.962 + securityManager.getCodebasePrincipal(uri); 1.963 + aRequest.owner = resourcePrincipal; 1.964 + channel.asyncOpen(proxy, aContext); 1.965 + }, 1.966 + 1.967 + // nsIRequestObserver::onStopRequest 1.968 + onStopRequest: function(aRequest, aContext, aStatusCode) { 1.969 + if (!this.dataListener) { 1.970 + // Do nothing 1.971 + return; 1.972 + } 1.973 + 1.974 + if (Components.isSuccessCode(aStatusCode)) 1.975 + this.dataListener.finish(); 1.976 + else 1.977 + this.dataListener.error(aStatusCode); 1.978 + delete this.dataListener; 1.979 + delete this.binaryStream; 1.980 + } 1.981 +};