browser/extensions/pdfjs/content/PdfStreamConverter.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
michael@0 3 /* Copyright 2012 Mozilla Foundation
michael@0 4 *
michael@0 5 * Licensed under the Apache License, Version 2.0 (the "License");
michael@0 6 * you may not use this file except in compliance with the License.
michael@0 7 * You may obtain a copy of the License at
michael@0 8 *
michael@0 9 * http://www.apache.org/licenses/LICENSE-2.0
michael@0 10 *
michael@0 11 * Unless required by applicable law or agreed to in writing, software
michael@0 12 * distributed under the License is distributed on an "AS IS" BASIS,
michael@0 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0 14 * See the License for the specific language governing permissions and
michael@0 15 * limitations under the License.
michael@0 16 */
michael@0 17 /* jshint esnext:true */
michael@0 18 /* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
michael@0 19 dump, NetworkManager, PdfJsTelemetry */
michael@0 20
michael@0 21 'use strict';
michael@0 22
michael@0 23 var EXPORTED_SYMBOLS = ['PdfStreamConverter'];
michael@0 24
michael@0 25 const Cc = Components.classes;
michael@0 26 const Ci = Components.interfaces;
michael@0 27 const Cr = Components.results;
michael@0 28 const Cu = Components.utils;
michael@0 29 // True only if this is the version of pdf.js that is included with firefox.
michael@0 30 const MOZ_CENTRAL = JSON.parse('true');
michael@0 31 const PDFJS_EVENT_ID = 'pdf.js.message';
michael@0 32 const PDF_CONTENT_TYPE = 'application/pdf';
michael@0 33 const PREF_PREFIX = 'pdfjs';
michael@0 34 const PDF_VIEWER_WEB_PAGE = 'resource://pdf.js/web/viewer.html';
michael@0 35 const MAX_NUMBER_OF_PREFS = 50;
michael@0 36 const MAX_STRING_PREF_LENGTH = 128;
michael@0 37
michael@0 38 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
michael@0 39 Cu.import('resource://gre/modules/Services.jsm');
michael@0 40 Cu.import('resource://gre/modules/NetUtil.jsm');
michael@0 41
michael@0 42 XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager',
michael@0 43 'resource://pdf.js/network.js');
michael@0 44
michael@0 45 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
michael@0 46 'resource://gre/modules/PrivateBrowsingUtils.jsm');
michael@0 47
michael@0 48 XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry',
michael@0 49 'resource://pdf.js/PdfJsTelemetry.jsm');
michael@0 50
michael@0 51 var Svc = {};
michael@0 52 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
michael@0 53 '@mozilla.org/mime;1',
michael@0 54 'nsIMIMEService');
michael@0 55
michael@0 56 function getContainingBrowser(domWindow) {
michael@0 57 return domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 58 .getInterface(Ci.nsIWebNavigation)
michael@0 59 .QueryInterface(Ci.nsIDocShell)
michael@0 60 .chromeEventHandler;
michael@0 61 }
michael@0 62
michael@0 63 function getChromeWindow(domWindow) {
michael@0 64 return getContainingBrowser(domWindow).ownerDocument.defaultView;
michael@0 65 }
michael@0 66
michael@0 67 function getFindBar(domWindow) {
michael@0 68 var browser = getContainingBrowser(domWindow);
michael@0 69 try {
michael@0 70 var tabbrowser = browser.getTabBrowser();
michael@0 71 var tab = tabbrowser._getTabForBrowser(browser);
michael@0 72 return tabbrowser.getFindBar(tab);
michael@0 73 } catch (e) {
michael@0 74 // FF22 has no _getTabForBrowser, and FF24 has no getFindBar
michael@0 75 var chromeWindow = browser.ownerDocument.defaultView;
michael@0 76 return chromeWindow.gFindBar;
michael@0 77 }
michael@0 78 }
michael@0 79
michael@0 80 function setBoolPref(pref, value) {
michael@0 81 Services.prefs.setBoolPref(pref, value);
michael@0 82 }
michael@0 83
michael@0 84 function getBoolPref(pref, def) {
michael@0 85 try {
michael@0 86 return Services.prefs.getBoolPref(pref);
michael@0 87 } catch (ex) {
michael@0 88 return def;
michael@0 89 }
michael@0 90 }
michael@0 91
michael@0 92 function setIntPref(pref, value) {
michael@0 93 Services.prefs.setIntPref(pref, value);
michael@0 94 }
michael@0 95
michael@0 96 function getIntPref(pref, def) {
michael@0 97 try {
michael@0 98 return Services.prefs.getIntPref(pref);
michael@0 99 } catch (ex) {
michael@0 100 return def;
michael@0 101 }
michael@0 102 }
michael@0 103
michael@0 104 function setStringPref(pref, value) {
michael@0 105 var str = Cc['@mozilla.org/supports-string;1']
michael@0 106 .createInstance(Ci.nsISupportsString);
michael@0 107 str.data = value;
michael@0 108 Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str);
michael@0 109 }
michael@0 110
michael@0 111 function getStringPref(pref, def) {
michael@0 112 try {
michael@0 113 return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
michael@0 114 } catch (ex) {
michael@0 115 return def;
michael@0 116 }
michael@0 117 }
michael@0 118
michael@0 119 function log(aMsg) {
michael@0 120 if (!getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false))
michael@0 121 return;
michael@0 122 var msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
michael@0 123 Services.console.logStringMessage(msg);
michael@0 124 dump(msg + '\n');
michael@0 125 }
michael@0 126
michael@0 127 function getDOMWindow(aChannel) {
michael@0 128 var requestor = aChannel.notificationCallbacks ?
michael@0 129 aChannel.notificationCallbacks :
michael@0 130 aChannel.loadGroup.notificationCallbacks;
michael@0 131 var win = requestor.getInterface(Components.interfaces.nsIDOMWindow);
michael@0 132 return win;
michael@0 133 }
michael@0 134
michael@0 135 function getLocalizedStrings(path) {
michael@0 136 var stringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
michael@0 137 getService(Ci.nsIStringBundleService).
michael@0 138 createBundle('chrome://pdf.js/locale/' + path);
michael@0 139
michael@0 140 var map = {};
michael@0 141 var enumerator = stringBundle.getSimpleEnumeration();
michael@0 142 while (enumerator.hasMoreElements()) {
michael@0 143 var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
michael@0 144 var key = string.key, property = 'textContent';
michael@0 145 var i = key.lastIndexOf('.');
michael@0 146 if (i >= 0) {
michael@0 147 property = key.substring(i + 1);
michael@0 148 key = key.substring(0, i);
michael@0 149 }
michael@0 150 if (!(key in map))
michael@0 151 map[key] = {};
michael@0 152 map[key][property] = string.value;
michael@0 153 }
michael@0 154 return map;
michael@0 155 }
michael@0 156 function getLocalizedString(strings, id, property) {
michael@0 157 property = property || 'textContent';
michael@0 158 if (id in strings)
michael@0 159 return strings[id][property];
michael@0 160 return id;
michael@0 161 }
michael@0 162
michael@0 163 // PDF data storage
michael@0 164 function PdfDataListener(length) {
michael@0 165 this.length = length; // less than 0, if length is unknown
michael@0 166 this.data = new Uint8Array(length >= 0 ? length : 0x10000);
michael@0 167 this.loaded = 0;
michael@0 168 }
michael@0 169
michael@0 170 PdfDataListener.prototype = {
michael@0 171 append: function PdfDataListener_append(chunk) {
michael@0 172 var willBeLoaded = this.loaded + chunk.length;
michael@0 173 if (this.length >= 0 && this.length < willBeLoaded) {
michael@0 174 this.length = -1; // reset the length, server is giving incorrect one
michael@0 175 }
michael@0 176 if (this.length < 0 && this.data.length < willBeLoaded) {
michael@0 177 // data length is unknown and new chunk will not fit in the existing
michael@0 178 // buffer, resizing the buffer by doubling the its last length
michael@0 179 var newLength = this.data.length;
michael@0 180 for (; newLength < willBeLoaded; newLength *= 2) {}
michael@0 181 var newData = new Uint8Array(newLength);
michael@0 182 newData.set(this.data);
michael@0 183 this.data = newData;
michael@0 184 }
michael@0 185 this.data.set(chunk, this.loaded);
michael@0 186 this.loaded = willBeLoaded;
michael@0 187 this.onprogress(this.loaded, this.length >= 0 ? this.length : void(0));
michael@0 188 },
michael@0 189 getData: function PdfDataListener_getData() {
michael@0 190 var data = this.data;
michael@0 191 if (this.loaded != data.length)
michael@0 192 data = data.subarray(0, this.loaded);
michael@0 193 delete this.data; // releasing temporary storage
michael@0 194 return data;
michael@0 195 },
michael@0 196 finish: function PdfDataListener_finish() {
michael@0 197 this.isDataReady = true;
michael@0 198 if (this.oncompleteCallback) {
michael@0 199 this.oncompleteCallback(this.getData());
michael@0 200 }
michael@0 201 },
michael@0 202 error: function PdfDataListener_error(errorCode) {
michael@0 203 this.errorCode = errorCode;
michael@0 204 if (this.oncompleteCallback) {
michael@0 205 this.oncompleteCallback(null, errorCode);
michael@0 206 }
michael@0 207 },
michael@0 208 onprogress: function() {},
michael@0 209 get oncomplete() {
michael@0 210 return this.oncompleteCallback;
michael@0 211 },
michael@0 212 set oncomplete(value) {
michael@0 213 this.oncompleteCallback = value;
michael@0 214 if (this.isDataReady) {
michael@0 215 value(this.getData());
michael@0 216 }
michael@0 217 if (this.errorCode) {
michael@0 218 value(null, this.errorCode);
michael@0 219 }
michael@0 220 }
michael@0 221 };
michael@0 222
michael@0 223 // All the priviledged actions.
michael@0 224 function ChromeActions(domWindow, contentDispositionFilename) {
michael@0 225 this.domWindow = domWindow;
michael@0 226 this.contentDispositionFilename = contentDispositionFilename;
michael@0 227 this.telemetryState = {
michael@0 228 documentInfo: false,
michael@0 229 firstPageInfo: false,
michael@0 230 streamTypesUsed: [],
michael@0 231 startAt: Date.now()
michael@0 232 };
michael@0 233 }
michael@0 234
michael@0 235 ChromeActions.prototype = {
michael@0 236 isInPrivateBrowsing: function() {
michael@0 237 return PrivateBrowsingUtils.isWindowPrivate(this.domWindow);
michael@0 238 },
michael@0 239 download: function(data, sendResponse) {
michael@0 240 var self = this;
michael@0 241 var originalUrl = data.originalUrl;
michael@0 242 // The data may not be downloaded so we need just retry getting the pdf with
michael@0 243 // the original url.
michael@0 244 var originalUri = NetUtil.newURI(data.originalUrl);
michael@0 245 var filename = data.filename;
michael@0 246 if (typeof filename !== 'string' ||
michael@0 247 (!/\.pdf$/i.test(filename) && !data.isAttachment)) {
michael@0 248 filename = 'document.pdf';
michael@0 249 }
michael@0 250 var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri;
michael@0 251 var extHelperAppSvc =
michael@0 252 Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
michael@0 253 getService(Ci.nsIExternalHelperAppService);
michael@0 254 var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1'].
michael@0 255 getService(Ci.nsIWindowWatcher).activeWindow;
michael@0 256
michael@0 257 var docIsPrivate = this.isInPrivateBrowsing();
michael@0 258 var netChannel = NetUtil.newChannel(blobUri);
michael@0 259 if ('nsIPrivateBrowsingChannel' in Ci &&
michael@0 260 netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
michael@0 261 netChannel.setPrivate(docIsPrivate);
michael@0 262 }
michael@0 263 NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) {
michael@0 264 if (!Components.isSuccessCode(aResult)) {
michael@0 265 if (sendResponse)
michael@0 266 sendResponse(true);
michael@0 267 return;
michael@0 268 }
michael@0 269 // Create a nsIInputStreamChannel so we can set the url on the channel
michael@0 270 // so the filename will be correct.
michael@0 271 var channel = Cc['@mozilla.org/network/input-stream-channel;1'].
michael@0 272 createInstance(Ci.nsIInputStreamChannel);
michael@0 273 channel.QueryInterface(Ci.nsIChannel);
michael@0 274 try {
michael@0 275 // contentDisposition/contentDispositionFilename is readonly before FF18
michael@0 276 channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
michael@0 277 if (self.contentDispositionFilename) {
michael@0 278 channel.contentDispositionFilename = self.contentDispositionFilename;
michael@0 279 } else {
michael@0 280 channel.contentDispositionFilename = filename;
michael@0 281 }
michael@0 282 } catch (e) {}
michael@0 283 channel.setURI(originalUri);
michael@0 284 channel.contentStream = aInputStream;
michael@0 285 if ('nsIPrivateBrowsingChannel' in Ci &&
michael@0 286 channel instanceof Ci.nsIPrivateBrowsingChannel) {
michael@0 287 channel.setPrivate(docIsPrivate);
michael@0 288 }
michael@0 289
michael@0 290 var listener = {
michael@0 291 extListener: null,
michael@0 292 onStartRequest: function(aRequest, aContext) {
michael@0 293 this.extListener = extHelperAppSvc.doContent((data.isAttachment ? '' :
michael@0 294 'application/pdf'),
michael@0 295 aRequest, frontWindow, false);
michael@0 296 this.extListener.onStartRequest(aRequest, aContext);
michael@0 297 },
michael@0 298 onStopRequest: function(aRequest, aContext, aStatusCode) {
michael@0 299 if (this.extListener)
michael@0 300 this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
michael@0 301 // Notify the content code we're done downloading.
michael@0 302 if (sendResponse)
michael@0 303 sendResponse(false);
michael@0 304 },
michael@0 305 onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
michael@0 306 aCount) {
michael@0 307 this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
michael@0 308 aOffset, aCount);
michael@0 309 }
michael@0 310 };
michael@0 311
michael@0 312 channel.asyncOpen(listener, null);
michael@0 313 });
michael@0 314 },
michael@0 315 getLocale: function() {
michael@0 316 return getStringPref('general.useragent.locale', 'en-US');
michael@0 317 },
michael@0 318 getStrings: function(data) {
michael@0 319 try {
michael@0 320 // Lazy initialization of localizedStrings
michael@0 321 if (!('localizedStrings' in this))
michael@0 322 this.localizedStrings = getLocalizedStrings('viewer.properties');
michael@0 323
michael@0 324 var result = this.localizedStrings[data];
michael@0 325 return JSON.stringify(result || null);
michael@0 326 } catch (e) {
michael@0 327 log('Unable to retrive localized strings: ' + e);
michael@0 328 return 'null';
michael@0 329 }
michael@0 330 },
michael@0 331 pdfBugEnabled: function() {
michael@0 332 return getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false);
michael@0 333 },
michael@0 334 supportsIntegratedFind: function() {
michael@0 335 // Integrated find is only supported when we're not in a frame
michael@0 336 if (this.domWindow.frameElement !== null) {
michael@0 337 return false;
michael@0 338 }
michael@0 339 // ... and when the new find events code exists.
michael@0 340 var findBar = getFindBar(this.domWindow);
michael@0 341 return findBar && ('updateControlState' in findBar);
michael@0 342 },
michael@0 343 supportsDocumentFonts: function() {
michael@0 344 var prefBrowser = getIntPref('browser.display.use_document_fonts', 1);
michael@0 345 var prefGfx = getBoolPref('gfx.downloadable_fonts.enabled', true);
michael@0 346 return (!!prefBrowser && prefGfx);
michael@0 347 },
michael@0 348 supportsDocumentColors: function() {
michael@0 349 return getBoolPref('browser.display.use_document_colors', true);
michael@0 350 },
michael@0 351 reportTelemetry: function (data) {
michael@0 352 var probeInfo = JSON.parse(data);
michael@0 353 switch (probeInfo.type) {
michael@0 354 case 'documentInfo':
michael@0 355 if (!this.telemetryState.documentInfo) {
michael@0 356 PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0);
michael@0 357 PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0);
michael@0 358 if (probeInfo.formType) {
michael@0 359 PdfJsTelemetry.onForm(probeInfo.formType === 'acroform');
michael@0 360 }
michael@0 361 this.telemetryState.documentInfo = true;
michael@0 362 }
michael@0 363 break;
michael@0 364 case 'pageInfo':
michael@0 365 if (!this.telemetryState.firstPageInfo) {
michael@0 366 var duration = Date.now() - this.telemetryState.startAt;
michael@0 367 PdfJsTelemetry.onTimeToView(duration);
michael@0 368 this.telemetryState.firstPageInfo = true;
michael@0 369 }
michael@0 370 break;
michael@0 371 case 'streamInfo':
michael@0 372 if (!Array.isArray(probeInfo.streamTypes)) {
michael@0 373 break;
michael@0 374 }
michael@0 375 for (var i = 0; i < probeInfo.streamTypes.length; i++) {
michael@0 376 var streamTypeId = probeInfo.streamTypes[i] | 0;
michael@0 377 if (streamTypeId >= 0 && streamTypeId < 10 &&
michael@0 378 !this.telemetryState.streamTypesUsed[streamTypeId]) {
michael@0 379 PdfJsTelemetry.onStreamType(streamTypeId);
michael@0 380 this.telemetryState.streamTypesUsed[streamTypeId] = true;
michael@0 381 }
michael@0 382 }
michael@0 383 break;
michael@0 384 }
michael@0 385 },
michael@0 386 fallback: function(args, sendResponse) {
michael@0 387 var featureId = args.featureId;
michael@0 388 var url = args.url;
michael@0 389
michael@0 390 var self = this;
michael@0 391 var domWindow = this.domWindow;
michael@0 392 var strings = getLocalizedStrings('chrome.properties');
michael@0 393 var message;
michael@0 394 if (featureId === 'forms') {
michael@0 395 message = getLocalizedString(strings, 'unsupported_feature_forms');
michael@0 396 } else {
michael@0 397 message = getLocalizedString(strings, 'unsupported_feature');
michael@0 398 }
michael@0 399
michael@0 400 PdfJsTelemetry.onFallback();
michael@0 401
michael@0 402 var notificationBox = null;
michael@0 403 try {
michael@0 404 // Based on MDN's "Working with windows in chrome code"
michael@0 405 var mainWindow = domWindow
michael@0 406 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
michael@0 407 .getInterface(Components.interfaces.nsIWebNavigation)
michael@0 408 .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
michael@0 409 .rootTreeItem
michael@0 410 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
michael@0 411 .getInterface(Components.interfaces.nsIDOMWindow);
michael@0 412 var browser = mainWindow.gBrowser
michael@0 413 .getBrowserForDocument(domWindow.top.document);
michael@0 414 notificationBox = mainWindow.gBrowser.getNotificationBox(browser);
michael@0 415 } catch (e) {
michael@0 416 log('Unable to get a notification box for the fallback message');
michael@0 417 return;
michael@0 418 }
michael@0 419
michael@0 420 // Flag so we don't call the response callback twice, since if the user
michael@0 421 // clicks open with different viewer both the button callback and
michael@0 422 // eventCallback will be called.
michael@0 423 var sentResponse = false;
michael@0 424 var buttons = [{
michael@0 425 label: getLocalizedString(strings, 'open_with_different_viewer'),
michael@0 426 accessKey: getLocalizedString(strings, 'open_with_different_viewer',
michael@0 427 'accessKey'),
michael@0 428 callback: function() {
michael@0 429 sentResponse = true;
michael@0 430 sendResponse(true);
michael@0 431 }
michael@0 432 }];
michael@0 433 notificationBox.appendNotification(message, 'pdfjs-fallback', null,
michael@0 434 notificationBox.PRIORITY_INFO_LOW,
michael@0 435 buttons,
michael@0 436 function eventsCallback(eventType) {
michael@0 437 // Currently there is only one event "removed" but if there are any other
michael@0 438 // added in the future we still only care about removed at the moment.
michael@0 439 if (eventType !== 'removed')
michael@0 440 return;
michael@0 441 // Don't send a response again if we already responded when the button was
michael@0 442 // clicked.
michael@0 443 if (!sentResponse)
michael@0 444 sendResponse(false);
michael@0 445 });
michael@0 446 },
michael@0 447 updateFindControlState: function(data) {
michael@0 448 if (!this.supportsIntegratedFind())
michael@0 449 return;
michael@0 450 // Verify what we're sending to the findbar.
michael@0 451 var result = data.result;
michael@0 452 var findPrevious = data.findPrevious;
michael@0 453 var findPreviousType = typeof findPrevious;
michael@0 454 if ((typeof result !== 'number' || result < 0 || result > 3) ||
michael@0 455 (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) {
michael@0 456 return;
michael@0 457 }
michael@0 458 getFindBar(this.domWindow).updateControlState(result, findPrevious);
michael@0 459 },
michael@0 460 setPreferences: function(prefs, sendResponse) {
michael@0 461 var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
michael@0 462 var numberOfPrefs = 0;
michael@0 463 var prefValue, prefName;
michael@0 464 for (var key in prefs) {
michael@0 465 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
michael@0 466 log('setPreferences - Exceeded the maximum number of preferences ' +
michael@0 467 'that is allowed to be set at once.');
michael@0 468 break;
michael@0 469 } else if (!defaultBranch.getPrefType(key)) {
michael@0 470 continue;
michael@0 471 }
michael@0 472 prefValue = prefs[key];
michael@0 473 prefName = (PREF_PREFIX + '.' + key);
michael@0 474 switch (typeof prefValue) {
michael@0 475 case 'boolean':
michael@0 476 setBoolPref(prefName, prefValue);
michael@0 477 break;
michael@0 478 case 'number':
michael@0 479 setIntPref(prefName, prefValue);
michael@0 480 break;
michael@0 481 case 'string':
michael@0 482 if (prefValue.length > MAX_STRING_PREF_LENGTH) {
michael@0 483 log('setPreferences - Exceeded the maximum allowed length ' +
michael@0 484 'for a string preference.');
michael@0 485 } else {
michael@0 486 setStringPref(prefName, prefValue);
michael@0 487 }
michael@0 488 break;
michael@0 489 }
michael@0 490 }
michael@0 491 if (sendResponse) {
michael@0 492 sendResponse(true);
michael@0 493 }
michael@0 494 },
michael@0 495 getPreferences: function(prefs, sendResponse) {
michael@0 496 var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
michael@0 497 var currentPrefs = {}, numberOfPrefs = 0;
michael@0 498 var prefValue, prefName;
michael@0 499 for (var key in prefs) {
michael@0 500 if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
michael@0 501 log('getPreferences - Exceeded the maximum number of preferences ' +
michael@0 502 'that is allowed to be fetched at once.');
michael@0 503 break;
michael@0 504 } else if (!defaultBranch.getPrefType(key)) {
michael@0 505 continue;
michael@0 506 }
michael@0 507 prefValue = prefs[key];
michael@0 508 prefName = (PREF_PREFIX + '.' + key);
michael@0 509 switch (typeof prefValue) {
michael@0 510 case 'boolean':
michael@0 511 currentPrefs[key] = getBoolPref(prefName, prefValue);
michael@0 512 break;
michael@0 513 case 'number':
michael@0 514 currentPrefs[key] = getIntPref(prefName, prefValue);
michael@0 515 break;
michael@0 516 case 'string':
michael@0 517 currentPrefs[key] = getStringPref(prefName, prefValue);
michael@0 518 break;
michael@0 519 }
michael@0 520 }
michael@0 521 if (sendResponse) {
michael@0 522 sendResponse(JSON.stringify(currentPrefs));
michael@0 523 } else {
michael@0 524 return JSON.stringify(currentPrefs);
michael@0 525 }
michael@0 526 }
michael@0 527 };
michael@0 528
michael@0 529 var RangedChromeActions = (function RangedChromeActionsClosure() {
michael@0 530 /**
michael@0 531 * This is for range requests
michael@0 532 */
michael@0 533 function RangedChromeActions(
michael@0 534 domWindow, contentDispositionFilename, originalRequest,
michael@0 535 dataListener) {
michael@0 536
michael@0 537 ChromeActions.call(this, domWindow, contentDispositionFilename);
michael@0 538 this.dataListener = dataListener;
michael@0 539 this.originalRequest = originalRequest;
michael@0 540
michael@0 541 this.pdfUrl = originalRequest.URI.spec;
michael@0 542 this.contentLength = originalRequest.contentLength;
michael@0 543
michael@0 544 // Pass all the headers from the original request through
michael@0 545 var httpHeaderVisitor = {
michael@0 546 headers: {},
michael@0 547 visitHeader: function(aHeader, aValue) {
michael@0 548 if (aHeader === 'Range') {
michael@0 549 // When loading the PDF from cache, firefox seems to set the Range
michael@0 550 // request header to fetch only the unfetched portions of the file
michael@0 551 // (e.g. 'Range: bytes=1024-'). However, we want to set this header
michael@0 552 // manually to fetch the PDF in chunks.
michael@0 553 return;
michael@0 554 }
michael@0 555 this.headers[aHeader] = aValue;
michael@0 556 }
michael@0 557 };
michael@0 558 originalRequest.visitRequestHeaders(httpHeaderVisitor);
michael@0 559
michael@0 560 var self = this;
michael@0 561 var xhr_onreadystatechange = function xhr_onreadystatechange() {
michael@0 562 if (this.readyState === 1) { // LOADING
michael@0 563 var netChannel = this.channel;
michael@0 564 if ('nsIPrivateBrowsingChannel' in Ci &&
michael@0 565 netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
michael@0 566 var docIsPrivate = self.isInPrivateBrowsing();
michael@0 567 netChannel.setPrivate(docIsPrivate);
michael@0 568 }
michael@0 569 }
michael@0 570 };
michael@0 571 var getXhr = function getXhr() {
michael@0 572 const XMLHttpRequest = Components.Constructor(
michael@0 573 '@mozilla.org/xmlextras/xmlhttprequest;1');
michael@0 574 var xhr = new XMLHttpRequest();
michael@0 575 xhr.addEventListener('readystatechange', xhr_onreadystatechange);
michael@0 576 return xhr;
michael@0 577 };
michael@0 578
michael@0 579 this.networkManager = new NetworkManager(this.pdfUrl, {
michael@0 580 httpHeaders: httpHeaderVisitor.headers,
michael@0 581 getXhr: getXhr
michael@0 582 });
michael@0 583
michael@0 584 // If we are in range request mode, this means we manually issued xhr
michael@0 585 // requests, which we need to abort when we leave the page
michael@0 586 domWindow.addEventListener('unload', function unload(e) {
michael@0 587 self.networkManager.abortAllRequests();
michael@0 588 domWindow.removeEventListener(e.type, unload);
michael@0 589 });
michael@0 590 }
michael@0 591
michael@0 592 RangedChromeActions.prototype = Object.create(ChromeActions.prototype);
michael@0 593 var proto = RangedChromeActions.prototype;
michael@0 594 proto.constructor = RangedChromeActions;
michael@0 595
michael@0 596 proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() {
michael@0 597 this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
michael@0 598 this.originalRequest = null;
michael@0 599 this.domWindow.postMessage({
michael@0 600 pdfjsLoadAction: 'supportsRangedLoading',
michael@0 601 pdfUrl: this.pdfUrl,
michael@0 602 length: this.contentLength,
michael@0 603 data: this.dataListener.getData()
michael@0 604 }, '*');
michael@0 605 this.dataListener = null;
michael@0 606
michael@0 607 return true;
michael@0 608 };
michael@0 609
michael@0 610 proto.requestDataRange = function RangedChromeActions_requestDataRange(args) {
michael@0 611 var begin = args.begin;
michael@0 612 var end = args.end;
michael@0 613 var domWindow = this.domWindow;
michael@0 614 // TODO(mack): Support error handler. We're not currently not handling
michael@0 615 // errors from chrome code for non-range requests, so this doesn't
michael@0 616 // seem high-pri
michael@0 617 this.networkManager.requestRange(begin, end, {
michael@0 618 onDone: function RangedChromeActions_onDone(args) {
michael@0 619 domWindow.postMessage({
michael@0 620 pdfjsLoadAction: 'range',
michael@0 621 begin: args.begin,
michael@0 622 chunk: args.chunk
michael@0 623 }, '*');
michael@0 624 },
michael@0 625 onProgress: function RangedChromeActions_onProgress(evt) {
michael@0 626 domWindow.postMessage({
michael@0 627 pdfjsLoadAction: 'rangeProgress',
michael@0 628 loaded: evt.loaded,
michael@0 629 }, '*');
michael@0 630 }
michael@0 631 });
michael@0 632 };
michael@0 633
michael@0 634 return RangedChromeActions;
michael@0 635 })();
michael@0 636
michael@0 637 var StandardChromeActions = (function StandardChromeActionsClosure() {
michael@0 638
michael@0 639 /**
michael@0 640 * This is for a single network stream
michael@0 641 */
michael@0 642 function StandardChromeActions(domWindow, contentDispositionFilename,
michael@0 643 dataListener) {
michael@0 644
michael@0 645 ChromeActions.call(this, domWindow, contentDispositionFilename);
michael@0 646 this.dataListener = dataListener;
michael@0 647 }
michael@0 648
michael@0 649 StandardChromeActions.prototype = Object.create(ChromeActions.prototype);
michael@0 650 var proto = StandardChromeActions.prototype;
michael@0 651 proto.constructor = StandardChromeActions;
michael@0 652
michael@0 653 proto.initPassiveLoading =
michael@0 654 function StandardChromeActions_initPassiveLoading() {
michael@0 655
michael@0 656 if (!this.dataListener) {
michael@0 657 return false;
michael@0 658 }
michael@0 659
michael@0 660 var self = this;
michael@0 661
michael@0 662 this.dataListener.onprogress = function ChromeActions_dataListenerProgress(
michael@0 663 loaded, total) {
michael@0 664 self.domWindow.postMessage({
michael@0 665 pdfjsLoadAction: 'progress',
michael@0 666 loaded: loaded,
michael@0 667 total: total
michael@0 668 }, '*');
michael@0 669 };
michael@0 670
michael@0 671 this.dataListener.oncomplete = function ChromeActions_dataListenerComplete(
michael@0 672 data, errorCode) {
michael@0 673 self.domWindow.postMessage({
michael@0 674 pdfjsLoadAction: 'complete',
michael@0 675 data: data,
michael@0 676 errorCode: errorCode
michael@0 677 }, '*');
michael@0 678
michael@0 679 delete self.dataListener;
michael@0 680 };
michael@0 681
michael@0 682 return true;
michael@0 683 };
michael@0 684
michael@0 685 return StandardChromeActions;
michael@0 686 })();
michael@0 687
michael@0 688 // Event listener to trigger chrome privedged code.
michael@0 689 function RequestListener(actions) {
michael@0 690 this.actions = actions;
michael@0 691 }
michael@0 692 // Receive an event and synchronously or asynchronously responds.
michael@0 693 RequestListener.prototype.receive = function(event) {
michael@0 694 var message = event.target;
michael@0 695 var doc = message.ownerDocument;
michael@0 696 var action = event.detail.action;
michael@0 697 var data = event.detail.data;
michael@0 698 var sync = event.detail.sync;
michael@0 699 var actions = this.actions;
michael@0 700 if (!(action in actions)) {
michael@0 701 log('Unknown action: ' + action);
michael@0 702 return;
michael@0 703 }
michael@0 704 if (sync) {
michael@0 705 var response = actions[action].call(this.actions, data);
michael@0 706 var detail = event.detail;
michael@0 707 detail.__exposedProps__ = {response: 'r'};
michael@0 708 detail.response = response;
michael@0 709 } else {
michael@0 710 var response;
michael@0 711 if (!event.detail.callback) {
michael@0 712 doc.documentElement.removeChild(message);
michael@0 713 response = null;
michael@0 714 } else {
michael@0 715 response = function sendResponse(response) {
michael@0 716 try {
michael@0 717 var listener = doc.createEvent('CustomEvent');
michael@0 718 listener.initCustomEvent('pdf.js.response', true, false,
michael@0 719 {response: response,
michael@0 720 __exposedProps__: {response: 'r'}});
michael@0 721 return message.dispatchEvent(listener);
michael@0 722 } catch (e) {
michael@0 723 // doc is no longer accessible because the requestor is already
michael@0 724 // gone. unloaded content cannot receive the response anyway.
michael@0 725 return false;
michael@0 726 }
michael@0 727 };
michael@0 728 }
michael@0 729 actions[action].call(this.actions, data, response);
michael@0 730 }
michael@0 731 };
michael@0 732
michael@0 733 // Forwards events from the eventElement to the contentWindow only if the
michael@0 734 // content window matches the currently selected browser window.
michael@0 735 function FindEventManager(eventElement, contentWindow, chromeWindow) {
michael@0 736 this.types = ['find',
michael@0 737 'findagain',
michael@0 738 'findhighlightallchange',
michael@0 739 'findcasesensitivitychange'];
michael@0 740 this.chromeWindow = chromeWindow;
michael@0 741 this.contentWindow = contentWindow;
michael@0 742 this.eventElement = eventElement;
michael@0 743 }
michael@0 744
michael@0 745 FindEventManager.prototype.bind = function() {
michael@0 746 var unload = function(e) {
michael@0 747 this.unbind();
michael@0 748 this.contentWindow.removeEventListener(e.type, unload);
michael@0 749 }.bind(this);
michael@0 750 this.contentWindow.addEventListener('unload', unload);
michael@0 751
michael@0 752 for (var i = 0; i < this.types.length; i++) {
michael@0 753 var type = this.types[i];
michael@0 754 this.eventElement.addEventListener(type, this, true);
michael@0 755 }
michael@0 756 };
michael@0 757
michael@0 758 FindEventManager.prototype.handleEvent = function(e) {
michael@0 759 var chromeWindow = this.chromeWindow;
michael@0 760 var contentWindow = this.contentWindow;
michael@0 761 // Only forward the events if they are for our dom window.
michael@0 762 if (chromeWindow.gBrowser.selectedBrowser.contentWindow === contentWindow) {
michael@0 763 var detail = e.detail;
michael@0 764 detail.__exposedProps__ = {
michael@0 765 query: 'r',
michael@0 766 caseSensitive: 'r',
michael@0 767 highlightAll: 'r',
michael@0 768 findPrevious: 'r'
michael@0 769 };
michael@0 770 var forward = contentWindow.document.createEvent('CustomEvent');
michael@0 771 forward.initCustomEvent(e.type, true, true, detail);
michael@0 772 contentWindow.dispatchEvent(forward);
michael@0 773 e.preventDefault();
michael@0 774 }
michael@0 775 };
michael@0 776
michael@0 777 FindEventManager.prototype.unbind = function() {
michael@0 778 for (var i = 0; i < this.types.length; i++) {
michael@0 779 var type = this.types[i];
michael@0 780 this.eventElement.removeEventListener(type, this, true);
michael@0 781 }
michael@0 782 };
michael@0 783
michael@0 784 function PdfStreamConverter() {
michael@0 785 }
michael@0 786
michael@0 787 PdfStreamConverter.prototype = {
michael@0 788
michael@0 789 // properties required for XPCOM registration:
michael@0 790 classID: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2291}'),
michael@0 791 classDescription: 'pdf.js Component',
michael@0 792 contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*',
michael@0 793
michael@0 794 QueryInterface: XPCOMUtils.generateQI([
michael@0 795 Ci.nsISupports,
michael@0 796 Ci.nsIStreamConverter,
michael@0 797 Ci.nsIStreamListener,
michael@0 798 Ci.nsIRequestObserver
michael@0 799 ]),
michael@0 800
michael@0 801 /*
michael@0 802 * This component works as such:
michael@0 803 * 1. asyncConvertData stores the listener
michael@0 804 * 2. onStartRequest creates a new channel, streams the viewer
michael@0 805 * 3. If range requests are supported:
michael@0 806 * 3.1. Leave the request open until the viewer is ready to switch to
michael@0 807 * range requests.
michael@0 808 *
michael@0 809 * If range rquests are not supported:
michael@0 810 * 3.1. Read the stream as it's loaded in onDataAvailable to send
michael@0 811 * to the viewer
michael@0 812 *
michael@0 813 * The convert function just returns the stream, it's just the synchronous
michael@0 814 * version of asyncConvertData.
michael@0 815 */
michael@0 816
michael@0 817 // nsIStreamConverter::convert
michael@0 818 convert: function(aFromStream, aFromType, aToType, aCtxt) {
michael@0 819 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
michael@0 820 },
michael@0 821
michael@0 822 // nsIStreamConverter::asyncConvertData
michael@0 823 asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
michael@0 824 // Store the listener passed to us
michael@0 825 this.listener = aListener;
michael@0 826 },
michael@0 827
michael@0 828 // nsIStreamListener::onDataAvailable
michael@0 829 onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
michael@0 830 if (!this.dataListener) {
michael@0 831 return;
michael@0 832 }
michael@0 833
michael@0 834 var binaryStream = this.binaryStream;
michael@0 835 binaryStream.setInputStream(aInputStream);
michael@0 836 var chunk = binaryStream.readByteArray(aCount);
michael@0 837 this.dataListener.append(chunk);
michael@0 838 },
michael@0 839
michael@0 840 // nsIRequestObserver::onStartRequest
michael@0 841 onStartRequest: function(aRequest, aContext) {
michael@0 842 // Setup the request so we can use it below.
michael@0 843 var isHttpRequest = false;
michael@0 844 try {
michael@0 845 aRequest.QueryInterface(Ci.nsIHttpChannel);
michael@0 846 isHttpRequest = true;
michael@0 847 } catch (e) {}
michael@0 848
michael@0 849 var rangeRequest = false;
michael@0 850 if (isHttpRequest) {
michael@0 851 var contentEncoding = 'identity';
michael@0 852 try {
michael@0 853 contentEncoding = aRequest.getResponseHeader('Content-Encoding');
michael@0 854 } catch (e) {}
michael@0 855
michael@0 856 var acceptRanges;
michael@0 857 try {
michael@0 858 acceptRanges = aRequest.getResponseHeader('Accept-Ranges');
michael@0 859 } catch (e) {}
michael@0 860
michael@0 861 var hash = aRequest.URI.ref;
michael@0 862 rangeRequest = contentEncoding === 'identity' &&
michael@0 863 acceptRanges === 'bytes' &&
michael@0 864 aRequest.contentLength >= 0 &&
michael@0 865 hash.indexOf('disableRange=true') < 0;
michael@0 866 }
michael@0 867
michael@0 868 aRequest.QueryInterface(Ci.nsIChannel);
michael@0 869
michael@0 870 aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
michael@0 871
michael@0 872 var contentDispositionFilename;
michael@0 873 try {
michael@0 874 contentDispositionFilename = aRequest.contentDispositionFilename;
michael@0 875 } catch (e) {}
michael@0 876
michael@0 877 // Change the content type so we don't get stuck in a loop.
michael@0 878 aRequest.setProperty('contentType', aRequest.contentType);
michael@0 879 aRequest.contentType = 'text/html';
michael@0 880 if (isHttpRequest) {
michael@0 881 // We trust PDF viewer, using no CSP
michael@0 882 aRequest.setResponseHeader('Content-Security-Policy', '', false);
michael@0 883 aRequest.setResponseHeader('Content-Security-Policy-Report-Only', '',
michael@0 884 false);
michael@0 885 aRequest.setResponseHeader('X-Content-Security-Policy', '', false);
michael@0 886 aRequest.setResponseHeader('X-Content-Security-Policy-Report-Only', '',
michael@0 887 false);
michael@0 888 }
michael@0 889
michael@0 890 PdfJsTelemetry.onViewerIsUsed();
michael@0 891 PdfJsTelemetry.onDocumentSize(aRequest.contentLength);
michael@0 892
michael@0 893
michael@0 894 // Creating storage for PDF data
michael@0 895 var contentLength = aRequest.contentLength;
michael@0 896 this.dataListener = new PdfDataListener(contentLength);
michael@0 897 this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
michael@0 898 .createInstance(Ci.nsIBinaryInputStream);
michael@0 899
michael@0 900 // Create a new channel that is viewer loaded as a resource.
michael@0 901 var ioService = Services.io;
michael@0 902 var channel = ioService.newChannel(
michael@0 903 PDF_VIEWER_WEB_PAGE, null, null);
michael@0 904
michael@0 905 var listener = this.listener;
michael@0 906 var dataListener = this.dataListener;
michael@0 907 // Proxy all the request observer calls, when it gets to onStopRequest
michael@0 908 // we can get the dom window. We also intentionally pass on the original
michael@0 909 // request(aRequest) below so we don't overwrite the original channel and
michael@0 910 // trigger an assertion.
michael@0 911 var proxy = {
michael@0 912 onStartRequest: function(request, context) {
michael@0 913 listener.onStartRequest(aRequest, context);
michael@0 914 },
michael@0 915 onDataAvailable: function(request, context, inputStream, offset, count) {
michael@0 916 listener.onDataAvailable(aRequest, context, inputStream, offset, count);
michael@0 917 },
michael@0 918 onStopRequest: function(request, context, statusCode) {
michael@0 919 // We get the DOM window here instead of before the request since it
michael@0 920 // may have changed during a redirect.
michael@0 921 var domWindow = getDOMWindow(channel);
michael@0 922 var actions;
michael@0 923 if (rangeRequest) {
michael@0 924 actions = new RangedChromeActions(
michael@0 925 domWindow, contentDispositionFilename, aRequest, dataListener);
michael@0 926 } else {
michael@0 927 actions = new StandardChromeActions(
michael@0 928 domWindow, contentDispositionFilename, dataListener);
michael@0 929 }
michael@0 930 var requestListener = new RequestListener(actions);
michael@0 931 domWindow.addEventListener(PDFJS_EVENT_ID, function(event) {
michael@0 932 requestListener.receive(event);
michael@0 933 }, false, true);
michael@0 934 if (actions.supportsIntegratedFind()) {
michael@0 935 var chromeWindow = getChromeWindow(domWindow);
michael@0 936 var findBar = getFindBar(domWindow);
michael@0 937 var findEventManager = new FindEventManager(findBar,
michael@0 938 domWindow,
michael@0 939 chromeWindow);
michael@0 940 findEventManager.bind();
michael@0 941 }
michael@0 942 listener.onStopRequest(aRequest, context, statusCode);
michael@0 943 }
michael@0 944 };
michael@0 945
michael@0 946 // Keep the URL the same so the browser sees it as the same.
michael@0 947 channel.originalURI = aRequest.URI;
michael@0 948 channel.loadGroup = aRequest.loadGroup;
michael@0 949
michael@0 950 // We can use resource principal when data is fetched by the chrome
michael@0 951 // e.g. useful for NoScript
michael@0 952 var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1']
michael@0 953 .getService(Ci.nsIScriptSecurityManager);
michael@0 954 var uri = ioService.newURI(PDF_VIEWER_WEB_PAGE, null, null);
michael@0 955 // FF16 and below had getCodebasePrincipal, it was replaced by
michael@0 956 // getNoAppCodebasePrincipal (bug 758258).
michael@0 957 var resourcePrincipal = 'getNoAppCodebasePrincipal' in securityManager ?
michael@0 958 securityManager.getNoAppCodebasePrincipal(uri) :
michael@0 959 securityManager.getCodebasePrincipal(uri);
michael@0 960 aRequest.owner = resourcePrincipal;
michael@0 961 channel.asyncOpen(proxy, aContext);
michael@0 962 },
michael@0 963
michael@0 964 // nsIRequestObserver::onStopRequest
michael@0 965 onStopRequest: function(aRequest, aContext, aStatusCode) {
michael@0 966 if (!this.dataListener) {
michael@0 967 // Do nothing
michael@0 968 return;
michael@0 969 }
michael@0 970
michael@0 971 if (Components.isSuccessCode(aStatusCode))
michael@0 972 this.dataListener.finish();
michael@0 973 else
michael@0 974 this.dataListener.error(aStatusCode);
michael@0 975 delete this.dataListener;
michael@0 976 delete this.binaryStream;
michael@0 977 }
michael@0 978 };

mercurial