browser/extensions/pdfjs/content/PdfStreamConverter.jsm

changeset 0
6474c204b198
     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 +};

mercurial