/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* jshint esnext:true */
/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
           dump, NetworkManager, PdfJsTelemetry, PdfjsContentUtils */

'use strict';

var EXPORTED_SYMBOLS = ['PdfStreamConverter'];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
// True only if this is the version of pdf.js that is included with firefox.
const MOZ_CENTRAL = JSON.parse('true');
const PDFJS_EVENT_ID = 'pdf.js.message';
const PDF_CONTENT_TYPE = 'application/pdf';
const PREF_PREFIX = 'pdfjs';
const PDF_VIEWER_ORIGIN = "resource://pdf.js";
const PDF_VIEWER_WEB_PAGE = "resource://pdf.js/web/viewer.html";
const MAX_NUMBER_OF_PREFS = 50;
const MAX_STRING_PREF_LENGTH = 128;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/NetUtil.jsm');

XPCOMUtils.defineLazyModuleGetter(this, 'NetworkManager',
  'resource://pdf.js/PdfJsNetwork.jsm');

XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
  'resource://gre/modules/PrivateBrowsingUtils.jsm');

XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry',
  'resource://pdf.js/PdfJsTelemetry.jsm');

XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsContentUtils',
  'resource://pdf.js/PdfjsContentUtils.jsm');

var Svc = {};
XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
                                   '@mozilla.org/mime;1',
                                   'nsIMIMEService');

function getContainingBrowser(domWindow) {
  return domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIWebNavigation)
                  .QueryInterface(Ci.nsIDocShell)
                  .chromeEventHandler;
}

function getFindBar(domWindow) {
  if (PdfjsContentUtils.isRemote) {
    throw new Error('FindBar is not accessible from the content process.');
  }
  try {
    var browser = getContainingBrowser(domWindow);
    var tabbrowser = browser.getTabBrowser();
    var tab = tabbrowser.getTabForBrowser(browser);
    return tabbrowser.getFindBar(tab);
  } catch (e) {
    // Suppress errors for PDF files opened in the bookmark sidebar, see
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1248959.
    return null;
  }
}

function getBoolPref(pref, def) {
  try {
    return Services.prefs.getBoolPref(pref);
  } catch (ex) {
    return def;
  }
}

function getIntPref(pref, def) {
  try {
    return Services.prefs.getIntPref(pref);
  } catch (ex) {
    return def;
  }
}

function getStringPref(pref, def) {
  try {
    return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
  } catch (ex) {
    return def;
  }
}

function log(aMsg) {
  if (!getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false)) {
    return;
  }
  var msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
  Services.console.logStringMessage(msg);
  dump(msg + '\n');
}

function getDOMWindow(aChannel, aPrincipal) {
  var requestor = aChannel.notificationCallbacks ?
                  aChannel.notificationCallbacks :
                  aChannel.loadGroup.notificationCallbacks;
  var win = requestor.getInterface(Components.interfaces.nsIDOMWindow);
  // Ensure the window wasn't navigated to something that is not PDF.js.
  if (!win.document.nodePrincipal.equals(aPrincipal)) {
    return null;
  }
  return win;
}

function getLocalizedStrings(path) {
  var stringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
      getService(Ci.nsIStringBundleService).
      createBundle('chrome://pdf.js/locale/' + path);

  var map = {};
  var enumerator = stringBundle.getSimpleEnumeration();
  while (enumerator.hasMoreElements()) {
    var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
    var key = string.key, property = 'textContent';
    var i = key.lastIndexOf('.');
    if (i >= 0) {
      property = key.substring(i + 1);
      key = key.substring(0, i);
    }
    if (!(key in map)) {
      map[key] = {};
    }
    map[key][property] = string.value;
  }
  return map;
}
function getLocalizedString(strings, id, property) {
  property = property || 'textContent';
  if (id in strings) {
    return strings[id][property];
  }
  return id;
}

function createNewChannel(uri, node) {
  return NetUtil.newChannel({
    uri: uri,
    loadUsingSystemPrincipal: true,
  });
}

function asyncOpenChannel(channel, listener, context) {
  return channel.asyncOpen2(listener);
}

function asyncFetchChannel(channel, callback) {
  return NetUtil.asyncFetch(channel, callback);
}

// PDF data storage
function PdfDataListener(length) {
  this.length = length; // less than 0, if length is unknown
  this.buffer = null;
  this.loaded = 0;
}

PdfDataListener.prototype = {
  append: function PdfDataListener_append(chunk) {
    // In most of the cases we will pass data as we receive it, but at the
    // beginning of the loading we may accumulate some data.
    if (!this.buffer) {
      this.buffer = new Uint8Array(chunk);
    } else {
      var buffer = this.buffer;
      var newBuffer = new Uint8Array(buffer.length + chunk.length);
      newBuffer.set(buffer);
      newBuffer.set(chunk, buffer.length);
      this.buffer = newBuffer;
    }
    this.loaded += chunk.length;
    if (this.length >= 0 && this.length < this.loaded) {
      this.length = -1; // reset the length, server is giving incorrect one
    }
    this.onprogress(this.loaded, this.length >= 0 ? this.length : void(0));
  },
  readData: function PdfDataListener_readData() {
    var result = this.buffer;
    this.buffer = null;
    return result;
  },
  finish: function PdfDataListener_finish() {
    this.isDataReady = true;
    if (this.oncompleteCallback) {
      this.oncompleteCallback(this.readData());
    }
  },
  error: function PdfDataListener_error(errorCode) {
    this.errorCode = errorCode;
    if (this.oncompleteCallback) {
      this.oncompleteCallback(null, errorCode);
    }
  },
  onprogress: function() {},
  get oncomplete() {
    return this.oncompleteCallback;
  },
  set oncomplete(value) {
    this.oncompleteCallback = value;
    if (this.isDataReady) {
      value(this.readData());
    }
    if (this.errorCode) {
      value(null, this.errorCode);
    }
  }
};

// All the priviledged actions.
function ChromeActions(domWindow, contentDispositionFilename) {
  this.domWindow = domWindow;
  this.contentDispositionFilename = contentDispositionFilename;
  this.telemetryState = {
    documentInfo: false,
    firstPageInfo: false,
    streamTypesUsed: [],
    fontTypesUsed: [],
    startAt: Date.now()
  };
}

ChromeActions.prototype = {
  isInPrivateBrowsing: function() {
    return PrivateBrowsingUtils.isContentWindowPrivate(this.domWindow);
  },
  download: function(data, sendResponse) {
    var self = this;
    var originalUrl = data.originalUrl;
    var blobUrl = data.blobUrl || originalUrl;
    // The data may not be downloaded so we need just retry getting the pdf with
    // the original url.
    var originalUri = NetUtil.newURI(originalUrl);
    var filename = data.filename;
    if (typeof filename !== 'string' ||
        (!/\.pdf$/i.test(filename) && !data.isAttachment)) {
      filename = 'document.pdf';
    }
    var blobUri = NetUtil.newURI(blobUrl);
    var extHelperAppSvc =
          Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
             getService(Ci.nsIExternalHelperAppService);

    var docIsPrivate = this.isInPrivateBrowsing();
    var netChannel = createNewChannel(blobUri, this.domWindow.document);
    if ('nsIPrivateBrowsingChannel' in Ci &&
        netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
      netChannel.setPrivate(docIsPrivate);
    }
    asyncFetchChannel(netChannel, function(aInputStream, aResult) {
      if (!Components.isSuccessCode(aResult)) {
        if (sendResponse) {
          sendResponse(true);
        }
        return;
      }
      // Create a nsIInputStreamChannel so we can set the url on the channel
      // so the filename will be correct.
      var channel = Cc['@mozilla.org/network/input-stream-channel;1'].
                       createInstance(Ci.nsIInputStreamChannel);
      channel.QueryInterface(Ci.nsIChannel);
      try {
        // contentDisposition/contentDispositionFilename is readonly before FF18
        channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
        if (self.contentDispositionFilename && !data.isAttachment) {
          channel.contentDispositionFilename = self.contentDispositionFilename;
        } else {
          channel.contentDispositionFilename = filename;
        }
      } catch (e) {}
      channel.setURI(originalUri);
      channel.loadInfo = netChannel.loadInfo;
      channel.contentStream = aInputStream;
      if ('nsIPrivateBrowsingChannel' in Ci &&
          channel instanceof Ci.nsIPrivateBrowsingChannel) {
        channel.setPrivate(docIsPrivate);
      }

      var listener = {
        extListener: null,
        onStartRequest: function(aRequest, aContext) {
          var loadContext = self.domWindow
                                .QueryInterface(Ci.nsIInterfaceRequestor)
                                .getInterface(Ci.nsIWebNavigation)
                                .QueryInterface(Ci.nsILoadContext);
          this.extListener = extHelperAppSvc.doContent(
            (data.isAttachment ? 'application/octet-stream' :
                                 'application/pdf'),
            aRequest, loadContext, false);
          this.extListener.onStartRequest(aRequest, aContext);
        },
        onStopRequest: function(aRequest, aContext, aStatusCode) {
          if (this.extListener) {
            this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
          }
          // Notify the content code we're done downloading.
          if (sendResponse) {
            sendResponse(false);
          }
        },
        onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
                                  aCount) {
          this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
                                           aOffset, aCount);
        }
      };

      asyncOpenChannel(channel, listener, null);
    });
  },
  getLocale: function() {
    return getStringPref('general.useragent.locale', 'en-US');
  },
  getStrings: function(data) {
    try {
      // Lazy initialization of localizedStrings
      if (!('localizedStrings' in this)) {
        this.localizedStrings = getLocalizedStrings('viewer.properties');
      }
      var result = this.localizedStrings[data];
      return JSON.stringify(result || null);
    } catch (e) {
      log('Unable to retrieve localized strings: ' + e);
      return 'null';
    }
  },
  supportsIntegratedFind: function() {
    // Integrated find is only supported when we're not in a frame
    if (this.domWindow.frameElement !== null) {
      return false;
    }

    // ... and we are in a child process
    if (PdfjsContentUtils.isRemote) {
      return true;
    }

    // ... or when the new find events code exists.
    var findBar = getFindBar(this.domWindow);
    return !!findBar && ('updateControlState' in findBar);
  },
  supportsDocumentFonts: function() {
    var prefBrowser = getIntPref('browser.display.use_document_fonts', 1);
    var prefGfx = getBoolPref('gfx.downloadable_fonts.enabled', true);
    return (!!prefBrowser && prefGfx);
  },
  supportsDocumentColors: function() {
    return getIntPref('browser.display.document_color_use', 0) !== 2;
  },
  supportedMouseWheelZoomModifierKeys: function() {
    return {
      ctrlKey: getIntPref('mousewheel.with_control.action', 3) === 3,
      metaKey: getIntPref('mousewheel.with_meta.action', 1) === 3,
    };
  },
  reportTelemetry: function (data) {
    var probeInfo = JSON.parse(data);
    switch (probeInfo.type) {
      case 'documentInfo':
        if (!this.telemetryState.documentInfo) {
          PdfJsTelemetry.onDocumentVersion(probeInfo.version | 0);
          PdfJsTelemetry.onDocumentGenerator(probeInfo.generator | 0);
          if (probeInfo.formType) {
            PdfJsTelemetry.onForm(probeInfo.formType === 'acroform');
          }
          this.telemetryState.documentInfo = true;
        }
        break;
      case 'pageInfo':
        if (!this.telemetryState.firstPageInfo) {
          var duration = Date.now() - this.telemetryState.startAt;
          PdfJsTelemetry.onTimeToView(duration);
          this.telemetryState.firstPageInfo = true;
        }
        break;
      case 'documentStats':
        // documentStats can be called several times for one documents.
        // if stream/font types are reported, trying not to submit the same
        // enumeration value multiple times.
        var documentStats = probeInfo.stats;
        if (!documentStats || typeof documentStats !== 'object') {
          break;
        }
        var i, streamTypes = documentStats.streamTypes;
        if (Array.isArray(streamTypes)) {
          var STREAM_TYPE_ID_LIMIT = 20;
          for (i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
            if (streamTypes[i] &&
                !this.telemetryState.streamTypesUsed[i]) {
              PdfJsTelemetry.onStreamType(i);
              this.telemetryState.streamTypesUsed[i] = true;
            }
          }
        }
        var fontTypes = documentStats.fontTypes;
        if (Array.isArray(fontTypes)) {
          var FONT_TYPE_ID_LIMIT = 20;
          for (i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
            if (fontTypes[i] &&
                !this.telemetryState.fontTypesUsed[i]) {
              PdfJsTelemetry.onFontType(i);
              this.telemetryState.fontTypesUsed[i] = true;
            }
          }
        }
        break;
      case 'print':
        PdfJsTelemetry.onPrint();
        break;
    }
  },
  fallback: function(args, sendResponse) {
    var featureId = args.featureId;
    var url = args.url;

    var self = this;
    var domWindow = this.domWindow;
    var strings = getLocalizedStrings('chrome.properties');
    var message;
    if (featureId === 'forms') {
      message = getLocalizedString(strings, 'unsupported_feature_forms');
    } else {
      message = getLocalizedString(strings, 'unsupported_feature');
    }
    PdfJsTelemetry.onFallback();
    PdfjsContentUtils.displayWarning(domWindow, message,
      getLocalizedString(strings, 'open_with_different_viewer'),
      getLocalizedString(strings, 'open_with_different_viewer', 'accessKey'));

    let winmm = domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDocShell)
                         .QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIContentFrameMessageManager);

    winmm.addMessageListener('PDFJS:Child:fallbackDownload',
      function fallbackDownload(msg) {
        let data = msg.data;
        sendResponse(data.download);

        winmm.removeMessageListener('PDFJS:Child:fallbackDownload',
                                    fallbackDownload);
      });
  },
  updateFindControlState: function(data) {
    if (!this.supportsIntegratedFind()) {
      return;
    }
    // Verify what we're sending to the findbar.
    var result = data.result;
    var findPrevious = data.findPrevious;
    var findPreviousType = typeof findPrevious;
    if ((typeof result !== 'number' || result < 0 || result > 3) ||
        (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) {
      return;
    }

    var winmm = this.domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIDocShell)
                              .QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIContentFrameMessageManager);

    winmm.sendAsyncMessage('PDFJS:Parent:updateControlState', data);
  },
  setPreferences: function(prefs, sendResponse) {
    var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
    var numberOfPrefs = 0;
    var prefValue, prefName;
    for (var key in prefs) {
      if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
        log('setPreferences - Exceeded the maximum number of preferences ' +
            'that is allowed to be set at once.');
        break;
      } else if (!defaultBranch.getPrefType(key)) {
        continue;
      }
      prefValue = prefs[key];
      prefName = (PREF_PREFIX + '.' + key);
      switch (typeof prefValue) {
        case 'boolean':
          PdfjsContentUtils.setBoolPref(prefName, prefValue);
          break;
        case 'number':
          PdfjsContentUtils.setIntPref(prefName, prefValue);
          break;
        case 'string':
          if (prefValue.length > MAX_STRING_PREF_LENGTH) {
            log('setPreferences - Exceeded the maximum allowed length ' +
                'for a string preference.');
          } else {
            PdfjsContentUtils.setStringPref(prefName, prefValue);
          }
          break;
      }
    }
    if (sendResponse) {
      sendResponse(true);
    }
  },
  getPreferences: function(prefs, sendResponse) {
    var defaultBranch = Services.prefs.getDefaultBranch(PREF_PREFIX + '.');
    var currentPrefs = {}, numberOfPrefs = 0;
    var prefValue, prefName;
    for (var key in prefs) {
      if (++numberOfPrefs > MAX_NUMBER_OF_PREFS) {
        log('getPreferences - Exceeded the maximum number of preferences ' +
            'that is allowed to be fetched at once.');
        break;
      } else if (!defaultBranch.getPrefType(key)) {
        continue;
      }
      prefValue = prefs[key];
      prefName = (PREF_PREFIX + '.' + key);
      switch (typeof prefValue) {
        case 'boolean':
          currentPrefs[key] = getBoolPref(prefName, prefValue);
          break;
        case 'number':
          currentPrefs[key] = getIntPref(prefName, prefValue);
          break;
        case 'string':
          currentPrefs[key] = getStringPref(prefName, prefValue);
          break;
      }
    }
    if (sendResponse) {
      sendResponse(JSON.stringify(currentPrefs));
    } else {
      return JSON.stringify(currentPrefs);
    }
  }
};

var RangedChromeActions = (function RangedChromeActionsClosure() {
  /**
   * This is for range requests
   */
  function RangedChromeActions(
              domWindow, contentDispositionFilename, originalRequest,
              rangeEnabled, streamingEnabled, dataListener) {

    ChromeActions.call(this, domWindow, contentDispositionFilename);
    this.dataListener = dataListener;
    this.originalRequest = originalRequest;
    this.rangeEnabled = rangeEnabled;
    this.streamingEnabled = streamingEnabled;

    this.pdfUrl = originalRequest.URI.spec;
    this.contentLength = originalRequest.contentLength;

    // Pass all the headers from the original request through
    var httpHeaderVisitor = {
      headers: {},
      visitHeader: function(aHeader, aValue) {
        if (aHeader === 'Range') {
          // When loading the PDF from cache, firefox seems to set the Range
          // request header to fetch only the unfetched portions of the file
          // (e.g. 'Range: bytes=1024-'). However, we want to set this header
          // manually to fetch the PDF in chunks.
          return;
        }
        this.headers[aHeader] = aValue;
      }
    };
    if (originalRequest.visitRequestHeaders) {
      originalRequest.visitRequestHeaders(httpHeaderVisitor);
    }

    var self = this;
    var xhr_onreadystatechange = function xhr_onreadystatechange() {
      if (this.readyState === 1) { // LOADING
        var netChannel = this.channel;
        if ('nsIPrivateBrowsingChannel' in Ci &&
            netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
          var docIsPrivate = self.isInPrivateBrowsing();
          netChannel.setPrivate(docIsPrivate);
        }
      }
    };
    var getXhr = function getXhr() {
      const XMLHttpRequest = Components.Constructor(
          '@mozilla.org/xmlextras/xmlhttprequest;1');
      var xhr = new XMLHttpRequest();
      xhr.addEventListener('readystatechange', xhr_onreadystatechange);
      return xhr;
    };

    this.networkManager = new NetworkManager(this.pdfUrl, {
      httpHeaders: httpHeaderVisitor.headers,
      getXhr: getXhr
    });

    // If we are in range request mode, this means we manually issued xhr
    // requests, which we need to abort when we leave the page
    domWindow.addEventListener('unload', function unload(e) {
      domWindow.removeEventListener(e.type, unload);
      self.abortLoading();
    });
  }

  RangedChromeActions.prototype = Object.create(ChromeActions.prototype);
  var proto = RangedChromeActions.prototype;
  proto.constructor = RangedChromeActions;

  proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() {
    var self = this;
    var data;
    if (!this.streamingEnabled) {
      this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
      this.originalRequest = null;
      data = this.dataListener.readData();
      this.dataListener = null;
    } else {
      data = this.dataListener.readData();

      this.dataListener.onprogress = function (loaded, total) {
        self.domWindow.postMessage({
          pdfjsLoadAction: 'progressiveRead',
          loaded: loaded,
          total: total,
          chunk: self.dataListener.readData()
        }, PDF_VIEWER_ORIGIN);
      };
      this.dataListener.oncomplete = function () {
        self.dataListener = null;
      };
    }

    this.domWindow.postMessage({
      pdfjsLoadAction: 'supportsRangedLoading',
      rangeEnabled: this.rangeEnabled,
      streamingEnabled: this.streamingEnabled,
      pdfUrl: this.pdfUrl,
      length: this.contentLength,
      data: data
    }, PDF_VIEWER_ORIGIN);

    return true;
  };

  proto.requestDataRange = function RangedChromeActions_requestDataRange(args) {
    if (!this.rangeEnabled) {
      return;
    }

    var begin = args.begin;
    var end = args.end;
    var domWindow = this.domWindow;
    // TODO(mack): Support error handler. We're not currently not handling
    // errors from chrome code for non-range requests, so this doesn't
    // seem high-pri
    this.networkManager.requestRange(begin, end, {
      onDone: function RangedChromeActions_onDone(args) {
        domWindow.postMessage({
          pdfjsLoadAction: 'range',
          begin: args.begin,
          chunk: args.chunk
        }, PDF_VIEWER_ORIGIN);
      },
      onProgress: function RangedChromeActions_onProgress(evt) {
        domWindow.postMessage({
          pdfjsLoadAction: 'rangeProgress',
          loaded: evt.loaded,
        }, PDF_VIEWER_ORIGIN);
      }
    });
  };

  proto.abortLoading = function RangedChromeActions_abortLoading() {
    this.networkManager.abortAllRequests();
    if (this.originalRequest) {
      this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
      this.originalRequest = null;
    }
    this.dataListener = null;
  };

  return RangedChromeActions;
})();

var StandardChromeActions = (function StandardChromeActionsClosure() {

  /**
   * This is for a single network stream
   */
  function StandardChromeActions(domWindow, contentDispositionFilename,
                                 originalRequest, dataListener) {

    ChromeActions.call(this, domWindow, contentDispositionFilename);
    this.originalRequest = originalRequest;
    this.dataListener = dataListener;
  }

  StandardChromeActions.prototype = Object.create(ChromeActions.prototype);
  var proto = StandardChromeActions.prototype;
  proto.constructor = StandardChromeActions;

  proto.initPassiveLoading =
      function StandardChromeActions_initPassiveLoading() {

    if (!this.dataListener) {
      return false;
    }

    var self = this;

    this.dataListener.onprogress = function ChromeActions_dataListenerProgress(
                                      loaded, total) {
      self.domWindow.postMessage({
        pdfjsLoadAction: 'progress',
        loaded: loaded,
        total: total
      }, PDF_VIEWER_ORIGIN);
    };

    this.dataListener.oncomplete =
        function StandardChromeActions_dataListenerComplete(data, errorCode) {
      self.domWindow.postMessage({
        pdfjsLoadAction: 'complete',
        data: data,
        errorCode: errorCode
      }, PDF_VIEWER_ORIGIN);

      self.dataListener = null;
      self.originalRequest = null;
    };

    return true;
  };

  proto.abortLoading = function StandardChromeActions_abortLoading() {
    if (this.originalRequest) {
      this.originalRequest.cancel(Cr.NS_BINDING_ABORTED);
      this.originalRequest = null;
    }
    this.dataListener = null;
  };

  return StandardChromeActions;
})();

// Event listener to trigger chrome privileged code.
function RequestListener(actions) {
  this.actions = actions;
}
// Receive an event and synchronously or asynchronously responds.
RequestListener.prototype.receive = function(event) {
  var message = event.target;
  var doc = message.ownerDocument;
  var action = event.detail.action;
  var data = event.detail.data;
  var sync = event.detail.sync;
  var actions = this.actions;
  if (!(action in actions)) {
    log('Unknown action: ' + action);
    return;
  }
  var response;
  if (sync) {
    response = actions[action].call(this.actions, data);
    event.detail.response = Cu.cloneInto(response, doc.defaultView);
  } else {
    if (!event.detail.responseExpected) {
      doc.documentElement.removeChild(message);
      response = null;
    } else {
      response = function sendResponse(response) {
        try {
          var listener = doc.createEvent('CustomEvent');
          let detail = Cu.cloneInto({ response: response }, doc.defaultView);
          listener.initCustomEvent('pdf.js.response', true, false, detail);
          return message.dispatchEvent(listener);
        } catch (e) {
          // doc is no longer accessible because the requestor is already
          // gone. unloaded content cannot receive the response anyway.
          return false;
        }
      };
    }
    actions[action].call(this.actions, data, response);
  }
};

// Forwards events from the eventElement to the contentWindow only if the
// content window matches the currently selected browser window.
function FindEventManager(contentWindow) {
  this.contentWindow = contentWindow;
  this.winmm = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDocShell)
                            .QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIContentFrameMessageManager);
}

FindEventManager.prototype.bind = function() {
  var unload = function(e) {
    this.unbind();
    this.contentWindow.removeEventListener(e.type, unload);
  }.bind(this);
  this.contentWindow.addEventListener('unload', unload);

  // We cannot directly attach listeners to for the find events
  // since the FindBar is in the parent process. Instead we're
  // asking the PdfjsChromeUtils to do it for us and forward
  // all the find events to us.
  this.winmm.sendAsyncMessage('PDFJS:Parent:addEventListener');
  this.winmm.addMessageListener('PDFJS:Child:handleEvent', this);
};

FindEventManager.prototype.receiveMessage = function(msg) {
  var detail = msg.data.detail;
  var type = msg.data.type;
  var contentWindow = this.contentWindow;

  detail = Cu.cloneInto(detail, contentWindow);
  var forward = contentWindow.document.createEvent('CustomEvent');
  forward.initCustomEvent(type, true, true, detail);
  contentWindow.dispatchEvent(forward);
};

FindEventManager.prototype.unbind = function() {
  this.winmm.sendAsyncMessage('PDFJS:Parent:removeEventListener');
};

function PdfStreamConverter() {
}

PdfStreamConverter.prototype = {

  // properties required for XPCOM registration:
  classID: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2291}'),
  classDescription: 'pdf.js Component',
  contractID: '@mozilla.org/streamconv;1?from=application/pdf&to=*/*',

  classID2: Components.ID('{d0c5195d-e798-49d4-b1d3-9324328b2292}'),
  contractID2: '@mozilla.org/streamconv;1?from=application/pdf&to=text/html',

  QueryInterface: XPCOMUtils.generateQI([
      Ci.nsISupports,
      Ci.nsIStreamConverter,
      Ci.nsIStreamListener,
      Ci.nsIRequestObserver
  ]),

  /*
   * This component works as such:
   * 1. asyncConvertData stores the listener
   * 2. onStartRequest creates a new channel, streams the viewer
   * 3. If range requests are supported:
   *      3.1. Leave the request open until the viewer is ready to switch to
   *           range requests.
   *
   *    If range rquests are not supported:
   *      3.1. Read the stream as it's loaded in onDataAvailable to send
   *           to the viewer
   *
   * The convert function just returns the stream, it's just the synchronous
   * version of asyncConvertData.
   */

  // nsIStreamConverter::convert
  convert: function(aFromStream, aFromType, aToType, aCtxt) {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },

  // nsIStreamConverter::asyncConvertData
  asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
    // Store the listener passed to us
    this.listener = aListener;
  },

  // nsIStreamListener::onDataAvailable
  onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
    if (!this.dataListener) {
      return;
    }

    var binaryStream = this.binaryStream;
    binaryStream.setInputStream(aInputStream);
    var chunk = binaryStream.readByteArray(aCount);
    this.dataListener.append(chunk);
  },

  // nsIRequestObserver::onStartRequest
  onStartRequest: function(aRequest, aContext) {
    // Setup the request so we can use it below.
    var isHttpRequest = false;
    try {
      aRequest.QueryInterface(Ci.nsIHttpChannel);
      isHttpRequest = true;
    } catch (e) {}

    var rangeRequest = false;
    var streamRequest = false;
    if (isHttpRequest) {
      var contentEncoding = 'identity';
      try {
        contentEncoding = aRequest.getResponseHeader('Content-Encoding');
      } catch (e) {}

      var acceptRanges;
      try {
        acceptRanges = aRequest.getResponseHeader('Accept-Ranges');
      } catch (e) {}

      var hash = aRequest.URI.ref;
      var isPDFBugEnabled = getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false);
      rangeRequest = contentEncoding === 'identity' &&
                     acceptRanges === 'bytes' &&
                     aRequest.contentLength >= 0 &&
                     !getBoolPref(PREF_PREFIX + '.disableRange', false) &&
                     (!isPDFBugEnabled ||
                      hash.toLowerCase().indexOf('disablerange=true') < 0);
      streamRequest = contentEncoding === 'identity' &&
                      aRequest.contentLength >= 0 &&
                      !getBoolPref(PREF_PREFIX + '.disableStream', false) &&
                      (!isPDFBugEnabled ||
                       hash.toLowerCase().indexOf('disablestream=true') < 0);
    }

    aRequest.QueryInterface(Ci.nsIChannel);

    aRequest.QueryInterface(Ci.nsIWritablePropertyBag);

    var contentDispositionFilename;
    try {
      contentDispositionFilename = aRequest.contentDispositionFilename;
    } catch (e) {}

    // Change the content type so we don't get stuck in a loop.
    aRequest.setProperty('contentType', aRequest.contentType);
    aRequest.contentType = 'text/html';
    if (isHttpRequest) {
      // We trust PDF viewer, using no CSP
      aRequest.setResponseHeader('Content-Security-Policy', '', false);
      aRequest.setResponseHeader('Content-Security-Policy-Report-Only', '',
                                 false);
      // The viewer does not need to handle HTTP Refresh header.
      aRequest.setResponseHeader('Refresh', '', false);
    }

    PdfJsTelemetry.onViewerIsUsed();
    PdfJsTelemetry.onDocumentSize(aRequest.contentLength);

    // Creating storage for PDF data
    var contentLength = aRequest.contentLength;
    this.dataListener = new PdfDataListener(contentLength);
    this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
                        .createInstance(Ci.nsIBinaryInputStream);

    // Create a new channel that is viewer loaded as a resource.
    var channel = createNewChannel(PDF_VIEWER_WEB_PAGE, null);

    var listener = this.listener;
    var dataListener = this.dataListener;
    // Proxy all the request observer calls, when it gets to onStopRequest
    // we can get the dom window.  We also intentionally pass on the original
    // request(aRequest) below so we don't overwrite the original channel and
    // trigger an assertion.
    var proxy = {
      onStartRequest: function(request, context) {
        listener.onStartRequest(aRequest, aContext);
      },
      onDataAvailable: function(request, context, inputStream, offset, count) {
        listener.onDataAvailable(aRequest, aContext, inputStream,
                                 offset, count);
      },
      onStopRequest(request, context, statusCode) {
        var domWindow = getDOMWindow(channel, resourcePrincipal);
        if (!Components.isSuccessCode(statusCode) || !domWindow) {
          // The request may have been aborted and the document may have been
          // replaced with something that is not PDF.js, abort attaching.
          listener.onStopRequest(aRequest, context, statusCode);
          return;
        }
        var actions;
        if (rangeRequest || streamRequest) {
          actions = new RangedChromeActions(
            domWindow, contentDispositionFilename, aRequest,
            rangeRequest, streamRequest, dataListener);
        } else {
          actions = new StandardChromeActions(
            domWindow, contentDispositionFilename, aRequest, dataListener);
        }
        var requestListener = new RequestListener(actions);
        domWindow.document.addEventListener(PDFJS_EVENT_ID, function(event) {
          requestListener.receive(event);
        }, false, true);
        if (actions.supportsIntegratedFind()) {
          var findEventManager = new FindEventManager(domWindow);
          findEventManager.bind();
        }
        listener.onStopRequest(aRequest, aContext, statusCode);

        if (domWindow.frameElement) {
          var isObjectEmbed = domWindow.frameElement.tagName !== 'IFRAME' ||
            domWindow.frameElement.className === 'previewPluginContentFrame';
          PdfJsTelemetry.onEmbed(isObjectEmbed);
        }
      }
    };

    // Keep the URL the same so the browser sees it as the same.
    channel.originalURI = aRequest.URI;
    channel.loadGroup = aRequest.loadGroup;
    channel.loadInfo.originAttributes = aRequest.loadInfo.originAttributes;

    // We can use the resource principal when data is fetched by the chrome,
    // e.g. useful for NoScript. Make make sure we reuse the origin attributes
    // from the request channel to keep isolation consistent.
    var ssm = Cc['@mozilla.org/scriptsecuritymanager;1']
                .getService(Ci.nsIScriptSecurityManager);
    var uri = NetUtil.newURI(PDF_VIEWER_WEB_PAGE, null, null);
    var resourcePrincipal;
    resourcePrincipal =
      ssm.createCodebasePrincipal(uri, aRequest.loadInfo.originAttributes);
    aRequest.owner = resourcePrincipal;
    asyncOpenChannel(channel, proxy, aContext);
  },

  // nsIRequestObserver::onStopRequest
  onStopRequest: function(aRequest, aContext, aStatusCode) {
    if (!this.dataListener) {
      // Do nothing
      return;
    }

    if (Components.isSuccessCode(aStatusCode)) {
      this.dataListener.finish();
    } else {
      this.dataListener.error(aStatusCode);
    }
    delete this.dataListener;
    delete this.binaryStream;
  }
};

