/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

dump("######################## BrowserElementChildPreload.js loaded\n");

var BrowserElementIsReady = false;

var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu }  = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/BrowserElementPromptService.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Microformats.js");
Cu.import("resource://gre/modules/ExtensionContent.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "acs",
                                   "@mozilla.org/audiochannel/service;1",
                                   "nsIAudioChannelService");
XPCOMUtils.defineLazyModuleGetter(this, "ManifestFinder",
                                  "resource://gre/modules/ManifestFinder.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ManifestObtainer",
                                  "resource://gre/modules/ManifestObtainer.jsm");


var kLongestReturnedString = 128;

function debug(msg) {
  //dump("BrowserElementChildPreload - " + msg + "\n");
}

function sendAsyncMsg(msg, data) {
  // Ensure that we don't send any messages before BrowserElementChild.js
  // finishes loading.
  if (!BrowserElementIsReady)
    return;

  if (!data) {
    data = { };
  }

  data.msg_name = msg;
  sendAsyncMessage('browser-element-api:call', data);
}

function sendSyncMsg(msg, data) {
  // Ensure that we don't send any messages before BrowserElementChild.js
  // finishes loading.
  if (!BrowserElementIsReady)
    return;

  if (!data) {
    data = { };
  }

  data.msg_name = msg;
  return sendSyncMessage('browser-element-api:call', data);
}

var CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page';

const OBSERVED_EVENTS = [
  'xpcom-shutdown',
  'audio-playback',
  'activity-done',
  'invalid-widget'
];

const COMMAND_MAP = {
  'cut': 'cmd_cut',
  'copy': 'cmd_copyAndCollapseToEnd',
  'copyImage': 'cmd_copyImage',
  'copyLink': 'cmd_copyLink',
  'paste': 'cmd_paste',
  'selectall': 'cmd_selectAll'
};

/**
 * The BrowserElementChild implements one half of <iframe mozbrowser>.
 * (The other half is, unsurprisingly, BrowserElementParent.)
 *
 * This script is injected into an <iframe mozbrowser> via
 * nsIMessageManager::LoadFrameScript().
 *
 * Our job here is to listen for events within this frame and bubble them up to
 * the parent process.
 */

var global = this;

function BrowserElementProxyForwarder() {
}

BrowserElementProxyForwarder.prototype = {
  init: function() {
    Services.obs.addObserver(this, "browser-element-api:proxy-call", false);
    addMessageListener("browser-element-api:proxy", this);
  },

  uninit: function() {
    Services.obs.removeObserver(this, "browser-element-api:proxy-call", false);
    removeMessageListener("browser-element-api:proxy", this);
  },

  // Observer callback receives messages from BrowserElementProxy.js
  observe: function(subject, topic, stringifedData) {
    if (subject !== content) {
      return;
    }

    // Forward it to BrowserElementParent.js
    sendAsyncMessage(topic, JSON.parse(stringifedData));
  },

  // Message manager callback receives messages from BrowserElementParent.js
  receiveMessage: function(mmMsg) {
    // Forward it to BrowserElementProxy.js
    Services.obs.notifyObservers(
      content, mmMsg.name, JSON.stringify(mmMsg.json));
  }
};

function BrowserElementChild() {
  // Maps outer window id --> weak ref to window.  Used by modal dialog code.
  this._windowIDDict = {};

  // _forcedVisible corresponds to the visibility state our owner has set on us
  // (via iframe.setVisible).  ownerVisible corresponds to whether the docShell
  // whose window owns this element is visible.
  //
  // Our docShell is visible iff _forcedVisible and _ownerVisible are both
  // true.
  this._forcedVisible = true;
  this._ownerVisible = true;

  this._nextPaintHandler = null;

  this._isContentWindowCreated = false;
  this._pendingSetInputMethodActive = [];
  this._selectionStateChangedTarget = null;

  this.forwarder = new BrowserElementProxyForwarder();

  this._init();
};

BrowserElementChild.prototype = {

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                         Ci.nsISupportsWeakReference]),

  _init: function() {
    debug("Starting up.");

    BrowserElementPromptService.mapWindowToBrowserElementChild(content, this);

    docShell.QueryInterface(Ci.nsIWebProgress)
            .addProgressListener(this._progressListener,
                                 Ci.nsIWebProgress.NOTIFY_LOCATION |
                                 Ci.nsIWebProgress.NOTIFY_SECURITY |
                                 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);

    docShell.QueryInterface(Ci.nsIWebNavigation)
            .sessionHistory = Cc["@mozilla.org/browser/shistory;1"]
                                .createInstance(Ci.nsISHistory);

    // This is necessary to get security web progress notifications.
    var securityUI = Cc['@mozilla.org/secure_browser_ui;1']
                       .createInstance(Ci.nsISecureBrowserUI);
    securityUI.init(content);

    // A cache of the menuitem dom objects keyed by the id we generate
    // and pass to the embedder
    this._ctxHandlers = {};
    // Counter of contextmenu events fired
    this._ctxCounter = 0;

    this._shuttingDown = false;

    addEventListener('DOMTitleChanged',
                     this._titleChangedHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('DOMLinkAdded',
                     this._linkAddedHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('MozScrolledAreaChanged',
                     this._mozScrollAreaChanged.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener("MozDOMFullscreen:Request",
                     this._mozRequestedDOMFullscreen.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener("MozDOMFullscreen:NewOrigin",
                     this._mozFullscreenOriginChange.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener("MozDOMFullscreen:Exit",
                     this._mozExitDomFullscreen.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('DOMMetaAdded',
                     this._metaChangedHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('DOMMetaChanged',
                     this._metaChangedHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('DOMMetaRemoved',
                     this._metaChangedHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('mozselectionstatechanged',
                     this._selectionStateChangedHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('scrollviewchange',
                     this._ScrollViewChangeHandler.bind(this),
                     /* useCapture = */ true,
                     /* wantsUntrusted = */ false);

    addEventListener('click',
                     this._ClickHandler.bind(this),
                     /* useCapture = */ false,
                     /* wantsUntrusted = */ false);

    // This listens to unload events from our message manager, but /not/ from
    // the |content| window.  That's because the window's unload event doesn't
    // bubble, and we're not using a capturing listener.  If we'd used
    // useCapture == true, we /would/ hear unload events from the window, which
    // is not what we want!
    addEventListener('unload',
                     this._unloadHandler.bind(this),
                     /* useCapture = */ false,
                     /* wantsUntrusted = */ false);

    // Registers a MozAfterPaint handler for the very first paint.
    this._addMozAfterPaintHandler(function () {
      sendAsyncMsg('firstpaint');
    });

    let self = this;

    let mmCalls = {
      "purge-history": this._recvPurgeHistory,
      "get-screenshot": this._recvGetScreenshot,
      "get-contentdimensions": this._recvGetContentDimensions,
      "set-visible": this._recvSetVisible,
      "get-visible": this._recvVisible,
      "send-mouse-event": this._recvSendMouseEvent,
      "send-touch-event": this._recvSendTouchEvent,
      "get-can-go-back": this._recvCanGoBack,
      "get-can-go-forward": this._recvCanGoForward,
      "mute": this._recvMute.bind(this),
      "unmute": this._recvUnmute.bind(this),
      "get-muted": this._recvGetMuted.bind(this),
      "set-volume": this._recvSetVolume.bind(this),
      "get-volume": this._recvGetVolume.bind(this),
      "go-back": this._recvGoBack,
      "go-forward": this._recvGoForward,
      "reload": this._recvReload,
      "stop": this._recvStop,
      "zoom": this._recvZoom,
      "unblock-modal-prompt": this._recvStopWaiting,
      "fire-ctx-callback": this._recvFireCtxCallback,
      "owner-visibility-change": this._recvOwnerVisibilityChange,
      "entered-fullscreen": this._recvEnteredFullscreen,
      "exit-fullscreen": this._recvExitFullscreen.bind(this),
      "activate-next-paint-listener": this._activateNextPaintListener.bind(this),
      "set-input-method-active": this._recvSetInputMethodActive.bind(this),
      "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this),
      "do-command": this._recvDoCommand,
      "find-all": this._recvFindAll.bind(this),
      "find-next": this._recvFindNext.bind(this),
      "clear-match": this._recvClearMatch.bind(this),
      "execute-script": this._recvExecuteScript,
      "get-audio-channel-volume": this._recvGetAudioChannelVolume,
      "set-audio-channel-volume": this._recvSetAudioChannelVolume,
      "get-audio-channel-muted": this._recvGetAudioChannelMuted,
      "set-audio-channel-muted": this._recvSetAudioChannelMuted,
      "get-is-audio-channel-active": this._recvIsAudioChannelActive,
      "get-structured-data": this._recvGetStructuredData,
      "get-web-manifest": this._recvGetWebManifest,
    }

    addMessageListener("browser-element-api:call", function(aMessage) {
      if (aMessage.data.msg_name in mmCalls) {
        return mmCalls[aMessage.data.msg_name].apply(self, arguments);
      }
    });

    let els = Cc["@mozilla.org/eventlistenerservice;1"]
                .getService(Ci.nsIEventListenerService);

    // We are using the system group for those events so if something in the
    // content called .stopPropagation() this will still be called.
    els.addSystemEventListener(global, 'DOMWindowClose',
                               this._windowCloseHandler.bind(this),
                               /* useCapture = */ false);
    els.addSystemEventListener(global, 'DOMWindowCreated',
                               this._windowCreatedHandler.bind(this),
                               /* useCapture = */ true);
    els.addSystemEventListener(global, 'DOMWindowResize',
                               this._windowResizeHandler.bind(this),
                               /* useCapture = */ false);
    els.addSystemEventListener(global, 'contextmenu',
                               this._contextmenuHandler.bind(this),
                               /* useCapture = */ false);
    els.addSystemEventListener(global, 'scroll',
                               this._scrollEventHandler.bind(this),
                               /* useCapture = */ false);

    OBSERVED_EVENTS.forEach((aTopic) => {
      Services.obs.addObserver(this, aTopic, false);
    });

    this.forwarder.init();
  },

  observe: function(subject, topic, data) {
    // Ignore notifications not about our document.  (Note that |content| /can/
    // be null; see bug 874900.)

    if (topic !== 'activity-done' && topic !== 'audio-playback' &&
        (!content || subject !== content.document)) {
      return;
    }
    if (topic == 'activity-done' && docShell !== subject)
      return;
    switch (topic) {
      case 'activity-done':
        sendAsyncMsg('activitydone', { success: (data == 'activity-success') });
        break;
      case 'audio-playback':
        if (subject === content) {
          sendAsyncMsg('audioplaybackchange', { _payload_: data });
        }
        break;
      case 'xpcom-shutdown':
        this._shuttingDown = true;
        break;
      case 'invalid-widget':
        sendAsyncMsg('error', { type: 'invalid-widget' });
        break;
    }
  },

  /**
   * Called when our TabChildGlobal starts to die.  This is not called when the
   * page inside |content| unloads.
   */
  _unloadHandler: function() {
    this._shuttingDown = true;
    OBSERVED_EVENTS.forEach((aTopic) => {
      Services.obs.removeObserver(this, aTopic);
    });

    this.forwarder.uninit();
    this.forwarder = null;
  },

  get _windowUtils() {
    return content.document.defaultView
                  .QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindowUtils);
  },

  _tryGetInnerWindowID: function(win) {
    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);
    try {
      return utils.currentInnerWindowID;
    }
    catch(e) {
      return null;
    }
  },

  /**
   * Show a modal prompt.  Called by BrowserElementPromptService.
   */
  showModalPrompt: function(win, args) {
    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);

    args.windowID = { outer: utils.outerWindowID,
                      inner: this._tryGetInnerWindowID(win) };
    sendAsyncMsg('showmodalprompt', args);

    let returnValue = this._waitForResult(win);

    Services.obs.notifyObservers(null, 'BEC:ShownModalPrompt', null);

    if (args.promptType == 'prompt' ||
        args.promptType == 'confirm' ||
        args.promptType == 'custom-prompt') {
      return returnValue;
    }
  },

  _isCommandEnabled: function(cmd) {
    let command = COMMAND_MAP[cmd];
    if (!command) {
      return false;
    }

    return docShell.isCommandEnabled(command);
  },

  /**
   * Spin in a nested event loop until we receive a unblock-modal-prompt message for
   * this window.
   */
  _waitForResult: function(win) {
    debug("_waitForResult(" + win + ")");
    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);

    let outerWindowID = utils.outerWindowID;
    let innerWindowID = this._tryGetInnerWindowID(win);
    if (innerWindowID === null) {
      // I have no idea what waiting for a result means when there's no inner
      // window, so let's just bail.
      debug("_waitForResult: No inner window. Bailing.");
      return;
    }

    this._windowIDDict[outerWindowID] = Cu.getWeakReference(win);

    debug("Entering modal state (outerWindowID=" + outerWindowID + ", " +
                                "innerWindowID=" + innerWindowID + ")");

    utils.enterModalState();

    // We'll decrement win.modalDepth when we receive a unblock-modal-prompt message
    // for the window.
    if (!win.modalDepth) {
      win.modalDepth = 0;
    }
    win.modalDepth++;
    let origModalDepth = win.modalDepth;

    let thread = Services.tm.currentThread;
    debug("Nested event loop - begin");
    while (win.modalDepth == origModalDepth && !this._shuttingDown) {
      // Bail out of the loop if the inner window changed; that means the
      // window navigated.  Bail out when we're shutting down because otherwise
      // we'll leak our window.
      if (this._tryGetInnerWindowID(win) !== innerWindowID) {
        debug("_waitForResult: Inner window ID changed " +
              "while in nested event loop.");
        break;
      }

      thread.processNextEvent(/* mayWait = */ true);
    }
    debug("Nested event loop - finish");

    if (win.modalDepth == 0) {
      delete this._windowIDDict[outerWindowID];
    }

    // If we exited the loop because the inner window changed, then bail on the
    // modal prompt.
    if (innerWindowID !== this._tryGetInnerWindowID(win)) {
      throw Components.Exception("Modal state aborted by navigation",
                                 Cr.NS_ERROR_NOT_AVAILABLE);
    }

    let returnValue = win.modalReturnValue;
    delete win.modalReturnValue;

    if (!this._shuttingDown) {
      utils.leaveModalState();
    }

    debug("Leaving modal state (outerID=" + outerWindowID + ", " +
                               "innerID=" + innerWindowID + ")");
    return returnValue;
  },

  _recvStopWaiting: function(msg) {
    let outerID = msg.json.windowID.outer;
    let innerID = msg.json.windowID.inner;
    let returnValue = msg.json.returnValue;
    debug("recvStopWaiting(outer=" + outerID + ", inner=" + innerID +
          ", returnValue=" + returnValue + ")");

    if (!this._windowIDDict[outerID]) {
      debug("recvStopWaiting: No record of outer window ID " + outerID);
      return;
    }

    let win = this._windowIDDict[outerID].get();

    if (!win) {
      debug("recvStopWaiting, but window is gone\n");
      return;
    }

    if (innerID !== this._tryGetInnerWindowID(win)) {
      debug("recvStopWaiting, but inner ID has changed\n");
      return;
    }

    debug("recvStopWaiting " + win);
    win.modalReturnValue = returnValue;
    win.modalDepth--;
  },

  _recvEnteredFullscreen: function() {
    if (!this._windowUtils.handleFullscreenRequests() &&
        !content.document.mozFullScreen) {
      // If we don't actually have any pending fullscreen request
      // to handle, neither we have been in fullscreen, tell the
      // parent to just exit.
      sendAsyncMsg("exit-dom-fullscreen");
    }
  },

  _recvExitFullscreen: function() {
    this._windowUtils.exitFullscreen();
  },

  _titleChangedHandler: function(e) {
    debug("Got titlechanged: (" + e.target.title + ")");
    var win = e.target.defaultView;

    // Ignore titlechanges which don't come from the top-level
    // <iframe mozbrowser> window.
    if (win == content) {
      sendAsyncMsg('titlechange', { _payload_: e.target.title });
    }
    else {
      debug("Not top level!");
    }
  },

  _maybeCopyAttribute: function(src, target, attribute) {
    if (src.getAttribute(attribute)) {
      target[attribute] = src.getAttribute(attribute);
    }
  },

  _iconChangedHandler: function(e) {
    debug('Got iconchanged: (' + e.target.href + ')');
    let icon = { href: e.target.href };
    this._maybeCopyAttribute(e.target, icon, 'sizes');
    this._maybeCopyAttribute(e.target, icon, 'rel');
    sendAsyncMsg('iconchange', icon);
  },

  _openSearchHandler: function(e) {
    debug('Got opensearch: (' + e.target.href + ')');

    if (e.target.type !== "application/opensearchdescription+xml") {
      return;
    }

    sendAsyncMsg('opensearch', { title: e.target.title,
                                 href: e.target.href });

  },

  _manifestChangedHandler: function(e) {
    debug('Got manifestchanged: (' + e.target.href + ')');
    let manifest = { href: e.target.href };
    sendAsyncMsg('manifestchange', manifest);

  },

  // Processes the "rel" field in <link> tags and forward to specific handlers.
  _linkAddedHandler: function(e) {
    let win = e.target.ownerDocument.defaultView;
    // Ignore links which don't come from the top-level
    // <iframe mozbrowser> window.
    if (win != content) {
      debug('Not top level!');
      return;
    }

    let handlers = {
      'icon': this._iconChangedHandler.bind(this),
      'apple-touch-icon': this._iconChangedHandler.bind(this),
      'search': this._openSearchHandler,
      'manifest': this._manifestChangedHandler
    };

    debug('Got linkAdded: (' + e.target.href + ') ' + e.target.rel);
    e.target.rel.split(' ').forEach(function(x) {
      let token = x.toLowerCase();
      if (handlers[token]) {
        handlers[token](e);
      }
    }, this);
  },

  _metaChangedHandler: function(e) {
    let win = e.target.ownerDocument.defaultView;
    // Ignore metas which don't come from the top-level
    // <iframe mozbrowser> window.
    if (win != content) {
      debug('Not top level!');
      return;
    }

    var name = e.target.name;
    var property = e.target.getAttributeNS(null, "property");

    if (!name && !property) {
      return;
    }

    debug('Got metaChanged: (' + (name || property) + ') ' +
          e.target.content);

    let handlers = {
      'viewmode': this._genericMetaHandler,
      'theme-color': this._genericMetaHandler,
      'theme-group': this._genericMetaHandler,
      'application-name': this._applicationNameChangedHandler
    };
    let handler = handlers[name];

    if ((property || name).match(/^og:/)) {
      name = property || name;
      handler = this._genericMetaHandler;
    }

    if (handler) {
      handler(name, e.type, e.target);
    }
  },

  _applicationNameChangedHandler: function(name, eventType, target) {
    if (eventType !== 'DOMMetaAdded') {
      // Bug 1037448 - Decide what to do when <meta name="application-name">
      // changes
      return;
    }

    let meta = { name: name,
                 content: target.content };

    let lang;
    let elm;

    for (elm = target;
         !lang && elm && elm.nodeType == target.ELEMENT_NODE;
         elm = elm.parentNode) {
      if (elm.hasAttribute('lang')) {
        lang = elm.getAttribute('lang');
        continue;
      }

      if (elm.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')) {
        lang = elm.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang');
        continue;
      }
    }

    // No lang has been detected.
    if (!lang && elm.nodeType == target.DOCUMENT_NODE) {
      lang = elm.contentLanguage;
    }

    if (lang) {
      meta.lang = lang;
    }

    sendAsyncMsg('metachange', meta);
  },

  _ScrollViewChangeHandler: function(e) {
    e.stopPropagation();
    let detail = {
      state: e.state,
    };
    sendAsyncMsg('scrollviewchange', detail);
  },

  _ClickHandler: function(e) {

    let isHTMLLink = node =>
      ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
       (node instanceof Ci.nsIDOMHTMLAreaElement && node.href) ||
        node instanceof Ci.nsIDOMHTMLLinkElement);

    // Open in a new tab if middle click or ctrl/cmd-click,
    // and e.target is a link or inside a link.
    if ((Services.appinfo.OS == 'Darwin' && e.metaKey) ||
        (Services.appinfo.OS != 'Darwin' && e.ctrlKey) ||
         e.button == 1) {

      let node = e.target;
      while (node && !isHTMLLink(node)) {
        node = node.parentNode;
      }

      if (node) {
        sendAsyncMsg('opentab', {url: node.href});
      }
    }
  },

  _selectionStateChangedHandler: function(e) {
    e.stopPropagation();

    if (!this._isContentWindowCreated) {
      return;
    }

    let boundingClientRect = e.boundingClientRect;

    let isCollapsed = (e.selectedText.length == 0);
    let isMouseUp = (e.states.indexOf('mouseup') == 0);
    let canPaste = this._isCommandEnabled("paste");

    if (this._selectionStateChangedTarget != e.target) {
      // SelectionStateChanged events with the following states are not
      // necessary to trigger the text dialog, bypass these events
      // by default.
      //
      if(e.states.length == 0 ||
         e.states.indexOf('drag') == 0 ||
         e.states.indexOf('keypress') == 0 ||
         e.states.indexOf('mousedown') == 0) {
        return;
      }

      // The collapsed SelectionStateChanged event is unnecessary to dispatch,
      // bypass this event by default, but here comes some exceptional cases
      if (isCollapsed) {
        if (isMouseUp && canPaste) {
          // Always dispatch to support shortcut mode which can paste previous
          // copied content easily
        } else if (e.states.indexOf('blur') == 0) {
          // Always dispatch to notify the blur for the focus content
        } else if (e.states.indexOf('taponcaret') == 0) {
          // Always dispatch to notify the caret be touched
        } else {
          return;
        }
      }
    }

    // If we select something and selection range is visible, we cache current
    // event's target to selectionStateChangedTarget.
    // And dispatch the next SelectionStateChagne event if target is matched, so
    // that the parent side can hide the text dialog.
    // We clear selectionStateChangedTarget if selection carets are invisible.
    if (e.visible && !isCollapsed) {
      this._selectionStateChangedTarget = e.target;
    } else if (canPaste && isCollapsed) {
      this._selectionStateChangedTarget = e.target;
    } else {
      this._selectionStateChangedTarget = null;
    }

    let zoomFactor = content.screen.width / content.innerWidth;

    let detail = {
      rect: {
        width: boundingClientRect ? boundingClientRect.width : 0,
        height: boundingClientRect ? boundingClientRect.height : 0,
        top: boundingClientRect ? boundingClientRect.top : 0,
        bottom: boundingClientRect ? boundingClientRect.bottom : 0,
        left: boundingClientRect ? boundingClientRect.left : 0,
        right: boundingClientRect ? boundingClientRect.right : 0,
      },
      commands: {
        canSelectAll: this._isCommandEnabled("selectall"),
        canCut: this._isCommandEnabled("cut"),
        canCopy: this._isCommandEnabled("copy"),
        canPaste: this._isCommandEnabled("paste"),
      },
      zoomFactor: zoomFactor,
      states: e.states,
      isCollapsed: (e.selectedText.length == 0),
      visible: e.visible,
    };

    // Get correct geometry information if we have nested iframe.
    let currentWindow = e.target.defaultView;
    while (currentWindow.realFrameElement) {
      let currentRect = currentWindow.realFrameElement.getBoundingClientRect();
      detail.rect.top += currentRect.top;
      detail.rect.bottom += currentRect.top;
      detail.rect.left += currentRect.left;
      detail.rect.right += currentRect.left;
      currentWindow = currentWindow.realFrameElement.ownerDocument.defaultView;
    }

    sendAsyncMsg('selectionstatechanged', detail);
  },

  _genericMetaHandler: function(name, eventType, target) {
    let meta = {
      name: name,
      content: target.content,
      type: eventType.replace('DOMMeta', '').toLowerCase()
    };
    sendAsyncMsg('metachange', meta);
  },

  _addMozAfterPaintHandler: function(callback) {
    function onMozAfterPaint() {
      let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
      if (uri.spec != "about:blank") {
        debug("Got afterpaint event: " + uri.spec);
        removeEventListener('MozAfterPaint', onMozAfterPaint,
                            /* useCapture = */ true);
        callback();
      }
    }

    addEventListener('MozAfterPaint', onMozAfterPaint, /* useCapture = */ true);
    return onMozAfterPaint;
  },

  _removeMozAfterPaintHandler: function(listener) {
    removeEventListener('MozAfterPaint', listener,
                        /* useCapture = */ true);
  },

  _activateNextPaintListener: function(e) {
    if (!this._nextPaintHandler) {
      this._nextPaintHandler = this._addMozAfterPaintHandler(function () {
        this._nextPaintHandler = null;
        sendAsyncMsg('nextpaint');
      }.bind(this));
    }
  },

  _deactivateNextPaintListener: function(e) {
    if (this._nextPaintHandler) {
      this._removeMozAfterPaintHandler(this._nextPaintHandler);
      this._nextPaintHandler = null;
    }
  },

  _windowCloseHandler: function(e) {
    let win = e.target;
    if (win != content || e.defaultPrevented) {
      return;
    }

    debug("Closing window " + win);
    sendAsyncMsg('close');

    // Inform the window implementation that we handled this close ourselves.
    e.preventDefault();
  },

  _windowCreatedHandler: function(e) {
    let targetDocShell = e.target.defaultView
          .QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIWebNavigation);
    if (targetDocShell != docShell) {
      return;
    }

    let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
    debug("Window created: " + uri.spec);
    if (uri.spec != "about:blank") {
      this._addMozAfterPaintHandler(function () {
        sendAsyncMsg('documentfirstpaint');
      });
      this._isContentWindowCreated = true;
      // Handle pending SetInputMethodActive request.
      while (this._pendingSetInputMethodActive.length > 0) {
        this._recvSetInputMethodActive(this._pendingSetInputMethodActive.shift());
      }
    }
  },

  _windowResizeHandler: function(e) {
    let win = e.target;
    if (win != content || e.defaultPrevented) {
      return;
    }

    debug("resizing window " + win);
    sendAsyncMsg('resize', { width: e.detail.width, height: e.detail.height });

    // Inform the window implementation that we handled this resize ourselves.
    e.preventDefault();
  },

  _contextmenuHandler: function(e) {
    debug("Got contextmenu");

    if (e.defaultPrevented) {
      return;
    }

    this._ctxCounter++;
    this._ctxHandlers = {};

    var elem = e.target;
    var menuData = {systemTargets: [], contextmenu: null};
    var ctxMenuId = null;
    var clipboardPlainTextOnly = Services.prefs.getBoolPref('clipboard.plainTextOnly');
    var copyableElements = {
      image: false,
      link: false,
      hasElements: function() {
        return this.image || this.link;
      }
    };

    // Set the event target as the copy image command needs it to
    // determine what was context-clicked on.
    docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit).setCommandNode(elem);

    while (elem && elem.parentNode) {
      var ctxData = this._getSystemCtxMenuData(elem);
      if (ctxData) {
        menuData.systemTargets.push({
          nodeName: elem.nodeName,
          data: ctxData
        });
      }

      if (!ctxMenuId && 'hasAttribute' in elem && elem.hasAttribute('contextmenu')) {
        ctxMenuId = elem.getAttribute('contextmenu');
      }

      // Enable copy image/link option
      if (elem.nodeName == 'IMG') {
        copyableElements.image = !clipboardPlainTextOnly;
      } else if (elem.nodeName == 'A') {
        copyableElements.link = true;
      }

      elem = elem.parentNode;
    }

    if (ctxMenuId || copyableElements.hasElements()) {
      var menu = null;
      if (ctxMenuId) {
        menu = e.target.ownerDocument.getElementById(ctxMenuId);
      }
      menuData.contextmenu = this._buildMenuObj(menu, '', copyableElements);
    }

    // Pass along the position where the context menu should be located
    menuData.clientX = e.clientX;
    menuData.clientY = e.clientY;
    menuData.screenX = e.screenX;
    menuData.screenY = e.screenY;

    // The value returned by the contextmenu sync call is true if the embedder
    // called preventDefault() on its contextmenu event.
    //
    // We call preventDefault() on our contextmenu event if the embedder called
    // preventDefault() on /its/ contextmenu event.  This way, if the embedder
    // ignored the contextmenu event, TabChild will fire a click.
    if (sendSyncMsg('contextmenu', menuData)[0]) {
      e.preventDefault();
    } else {
      this._ctxHandlers = {};
    }
  },

  _getSystemCtxMenuData: function(elem) {
    let documentURI =
      docShell.QueryInterface(Ci.nsIWebNavigation).currentURI.spec;
    if ((elem instanceof Ci.nsIDOMHTMLAnchorElement && elem.href) ||
        (elem instanceof Ci.nsIDOMHTMLAreaElement && elem.href)) {
      return {uri: elem.href,
              documentURI: documentURI,
              text: elem.textContent.substring(0, kLongestReturnedString)};
    }
    if (elem instanceof Ci.nsIImageLoadingContent && elem.currentURI) {
      return {uri: elem.currentURI.spec, documentURI: documentURI};
    }
    if (elem instanceof Ci.nsIDOMHTMLImageElement) {
      return {uri: elem.src, documentURI: documentURI};
    }
    if (elem instanceof Ci.nsIDOMHTMLMediaElement) {
      let hasVideo = !(elem.readyState >= elem.HAVE_METADATA &&
                       (elem.videoWidth == 0 || elem.videoHeight == 0));
      return {uri: elem.currentSrc || elem.src,
              hasVideo: hasVideo,
              documentURI: documentURI};
    }
    if (elem instanceof Ci.nsIDOMHTMLInputElement &&
        elem.hasAttribute("name")) {
      // For input elements, we look for a parent <form> and if there is
      // one we return the form's method and action uri.
      let parent = elem.parentNode;
      while (parent) {
        if (parent instanceof Ci.nsIDOMHTMLFormElement &&
            parent.hasAttribute("action")) {
          let actionHref = docShell.QueryInterface(Ci.nsIWebNavigation)
                                   .currentURI
                                   .resolve(parent.getAttribute("action"));
          let method = parent.hasAttribute("method")
            ? parent.getAttribute("method").toLowerCase()
            : "get";
          return {
            documentURI: documentURI,
            action: actionHref,
            method: method,
            name: elem.getAttribute("name"),
          }
        }
        parent = parent.parentNode;
      }
    }
    return false;
  },

  _scrollEventHandler: function(e) {
    let win = e.target.defaultView;
    if (win != content) {
      return;
    }

    debug("scroll event " + win);
    sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX });
  },

  _recvPurgeHistory: function(data) {
    debug("Received purgeHistory message: (" + data.json.id + ")");

    let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;

    try {
      if (history && history.count) {
        history.PurgeHistory(history.count);
      }
    } catch(e) {}

    sendAsyncMsg('got-purge-history', { id: data.json.id, successRv: true });
  },

  _recvGetScreenshot: function(data) {
    debug("Received getScreenshot message: (" + data.json.id + ")");

    let self = this;
    let maxWidth = data.json.args.width;
    let maxHeight = data.json.args.height;
    let mimeType = data.json.args.mimeType;
    let domRequestID = data.json.id;

    let takeScreenshotClosure = function() {
      self._takeScreenshot(maxWidth, maxHeight, mimeType, domRequestID);
    };

    let maxDelayMS = 2000;
    try {
      maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS');
    }
    catch(e) {}

    // Try to wait for the event loop to go idle before we take the screenshot,
    // but once we've waited maxDelayMS milliseconds, go ahead and take it
    // anyway.
    Cc['@mozilla.org/message-loop;1'].getService(Ci.nsIMessageLoop).postIdleTask(
      takeScreenshotClosure, maxDelayMS);
  },

  _recvExecuteScript: function(data) {
    debug("Received executeScript message: (" + data.json.id + ")");

    let domRequestID = data.json.id;

    let sendError = errorMsg => sendAsyncMsg("execute-script-done", {
      errorMsg,
      id: domRequestID
    });

    let sendSuccess = successRv => sendAsyncMsg("execute-script-done", {
      successRv,
      id: domRequestID
    });

    let isJSON = obj => {
      try {
        JSON.stringify(obj);
      } catch(e) {
        return false;
      }
      return true;
    }

    let expectedOrigin = data.json.args.options.origin;
    let expectedUrl = data.json.args.options.url;

    if (expectedOrigin) {
      if (expectedOrigin != content.location.origin) {
        sendError("Origin mismatches");
        return;
      }
    }

    if (expectedUrl) {
      let expectedURI
      try {
       expectedURI = Services.io.newURI(expectedUrl, null, null);
      } catch(e) {
        sendError("Malformed URL");
        return;
      }
      let currentURI = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
      if (!currentURI.equalsExceptRef(expectedURI)) {
        sendError("URL mismatches");
        return;
      }
    }

    let sandbox = new Cu.Sandbox([content], {
      sandboxPrototype: content,
      sandboxName: "browser-api-execute-script",
      allowWaivers: false,
      sameZoneAs: content
    });

    try {
      let sandboxRv = Cu.evalInSandbox(data.json.args.script, sandbox, "1.8");
      if (sandboxRv instanceof Promise) {
        sandboxRv.then(rv => {
          if (isJSON(rv)) {
            sendSuccess(rv);
          } else {
            sendError("Value returned (resolve) by promise is not a valid JSON object");
          }
        }, error => {
          if (isJSON(error)) {
            sendError(error);
          } else {
            sendError("Value returned (reject) by promise is not a valid JSON object");
          }
        });
      } else {
        if (isJSON(sandboxRv)) {
          sendSuccess(sandboxRv);
        } else {
          sendError("Script last expression must be a promise or a JSON object");
        }
      }
    } catch(e) {
      sendError(e.toString());
    }
  },

  _recvGetContentDimensions: function(data) {
    debug("Received getContentDimensions message: (" + data.json.id + ")");
    sendAsyncMsg('got-contentdimensions', {
      id: data.json.id,
      successRv: this._getContentDimensions()
    });
  },

  _mozScrollAreaChanged: function(e) {
    sendAsyncMsg('scrollareachanged', {
      width: e.width,
      height: e.height
    });
  },

  _mozRequestedDOMFullscreen: function(e) {
    sendAsyncMsg("requested-dom-fullscreen");
  },

  _mozFullscreenOriginChange: function(e) {
    sendAsyncMsg("fullscreen-origin-change", {
      originNoSuffix: e.target.nodePrincipal.originNoSuffix
    });
  },

  _mozExitDomFullscreen: function(e) {
    sendAsyncMsg("exit-dom-fullscreen");
  },

  _getContentDimensions: function() {
    return {
      width: content.document.body.scrollWidth,
      height: content.document.body.scrollHeight
    }
  },

  /**
   * Actually take a screenshot and foward the result up to our parent, given
   * the desired maxWidth and maxHeight (in CSS pixels), and given the
   * DOMRequest ID associated with the request from the parent.
   */
  _takeScreenshot: function(maxWidth, maxHeight, mimeType, domRequestID) {
    // You can think of the screenshotting algorithm as carrying out the
    // following steps:
    //
    // - Calculate maxWidth, maxHeight, and viewport's width and height in the
    //   dimension of device pixels by multiply the numbers with
    //   window.devicePixelRatio.
    //
    // - Let scaleWidth be the factor by which we'd need to downscale the
    //   viewport pixel width so it would fit within maxPixelWidth.
    //   (If the viewport's pixel width is less than maxPixelWidth, let
    //   scaleWidth be 1.) Compute scaleHeight the same way.
    //
    // - Scale the viewport by max(scaleWidth, scaleHeight).  Now either the
    //   viewport's width is no larger than maxWidth, the viewport's height is
    //   no larger than maxHeight, or both.
    //
    // - Crop the viewport so its width is no larger than maxWidth and its
    //   height is no larger than maxHeight.
    //
    // - Set mozOpaque to true and background color to solid white
    //   if we are taking a JPEG screenshot, keep transparent if otherwise.
    //
    // - Return a screenshot of the page's viewport scaled and cropped per
    //   above.
    debug("Taking a screenshot: maxWidth=" + maxWidth +
          ", maxHeight=" + maxHeight +
          ", mimeType=" + mimeType +
          ", domRequestID=" + domRequestID + ".");

    if (!content) {
      // If content is not loaded yet, bail out since even sendAsyncMessage
      // fails...
      debug("No content yet!");
      return;
    }

    let devicePixelRatio = content.devicePixelRatio;

    let maxPixelWidth = Math.round(maxWidth * devicePixelRatio);
    let maxPixelHeight = Math.round(maxHeight * devicePixelRatio);

    let contentPixelWidth = content.innerWidth * devicePixelRatio;
    let contentPixelHeight = content.innerHeight * devicePixelRatio;

    let scaleWidth = Math.min(1, maxPixelWidth / contentPixelWidth);
    let scaleHeight = Math.min(1, maxPixelHeight / contentPixelHeight);

    let scale = Math.max(scaleWidth, scaleHeight);

    let canvasWidth =
      Math.min(maxPixelWidth, Math.round(contentPixelWidth * scale));
    let canvasHeight =
      Math.min(maxPixelHeight, Math.round(contentPixelHeight * scale));

    let transparent = (mimeType !== 'image/jpeg');

    var canvas = content.document
      .createElementNS("http://www.w3.org/1999/xhtml", "canvas");
    if (!transparent)
      canvas.mozOpaque = true;
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;

    let ctx = canvas.getContext("2d", { willReadFrequently: true });
    ctx.scale(scale * devicePixelRatio, scale * devicePixelRatio);

    let flags = ctx.DRAWWINDOW_DRAW_VIEW |
                ctx.DRAWWINDOW_USE_WIDGET_LAYERS |
                ctx.DRAWWINDOW_DO_NOT_FLUSH |
                ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES;
    ctx.drawWindow(content, 0, 0, content.innerWidth, content.innerHeight,
                   transparent ? "rgba(255,255,255,0)" : "rgb(255,255,255)",
                   flags);

    // Take a JPEG screenshot by default instead of PNG with alpha channel.
    // This requires us to unpremultiply the alpha channel, which
    // is expensive on ARM processors because they lack a hardware integer
    // division instruction.
    canvas.toBlob(function(blob) {
      sendAsyncMsg('got-screenshot', {
        id: domRequestID,
        successRv: blob
      });
    }, mimeType);
  },

  _recvFireCtxCallback: function(data) {
    debug("Received fireCtxCallback message: (" + data.json.menuitem + ")");

    if (data.json.menuitem == 'copy-image') {
      // Set command
      data.json.command = 'copyImage';
      this._recvDoCommand(data);
    } else if (data.json.menuitem == 'copy-link') {
      // Set command
      data.json.command = 'copyLink';
      this._recvDoCommand(data);
    } else if (data.json.menuitem in this._ctxHandlers) {
      this._ctxHandlers[data.json.menuitem].click();
      this._ctxHandlers = {};
    } else {
      // We silently ignore if the embedder uses an incorrect id in the callback
      debug("Ignored invalid contextmenu invocation");
    }
  },

  _buildMenuObj: function(menu, idPrefix, copyableElements) {
    var menuObj = {type: 'menu', customized: false, items: []};
    // Customized context menu
    if (menu) {
      this._maybeCopyAttribute(menu, menuObj, 'label');

      for (var i = 0, child; child = menu.children[i++];) {
        if (child.nodeName === 'MENU') {
          menuObj.items.push(this._buildMenuObj(child, idPrefix + i + '_', false));
        } else if (child.nodeName === 'MENUITEM') {
          var id = this._ctxCounter + '_' + idPrefix + i;
          var menuitem = {id: id, type: 'menuitem'};
          this._maybeCopyAttribute(child, menuitem, 'label');
          this._maybeCopyAttribute(child, menuitem, 'icon');
          this._ctxHandlers[id] = child;
          menuObj.items.push(menuitem);
        }
      }

      if (menuObj.items.length > 0) {
        menuObj.customized = true;
      }
    }
    // Note: Display "Copy Link" first in order to make sure "Copy Image" is
    //       put together with other image options if elem is an image link.
    // "Copy Link" menu item
    if (copyableElements.link) {
      menuObj.items.push({id: 'copy-link'});
    }
    // "Copy Image" menu item
    if (copyableElements.image) {
      menuObj.items.push({id: 'copy-image'});
    }

    return menuObj;
  },

  _recvSetVisible: function(data) {
    debug("Received setVisible message: (" + data.json.visible + ")");
    if (this._forcedVisible == data.json.visible) {
      return;
    }

    this._forcedVisible = data.json.visible;
    this._updateVisibility();
  },

  _recvVisible: function(data) {
    sendAsyncMsg('got-visible', {
      id: data.json.id,
      successRv: docShell.isActive
    });
  },

  /**
   * Called when the window which contains this iframe becomes hidden or
   * visible.
   */
  _recvOwnerVisibilityChange: function(data) {
    debug("Received ownerVisibilityChange: (" + data.json.visible + ")");
    this._ownerVisible = data.json.visible;
    this._updateVisibility();
  },

  _updateVisibility: function() {
    var visible = this._forcedVisible && this._ownerVisible;
    if (docShell && docShell.isActive !== visible) {
      docShell.isActive = visible;
      sendAsyncMsg('visibilitychange', {visible: visible});
    }
  },

  _recvSendMouseEvent: function(data) {
    let json = data.json;
    let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
    utils.sendMouseEventToWindow(json.type, json.x, json.y, json.button,
                                 json.clickCount, json.modifiers);
  },

  _recvSendTouchEvent: function(data) {
    let json = data.json;
    let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
    utils.sendTouchEventToWindow(json.type, json.identifiers, json.touchesX,
                                 json.touchesY, json.radiisX, json.radiisY,
                                 json.rotationAngles, json.forces, json.count,
                                 json.modifiers);
  },

  _recvCanGoBack: function(data) {
    var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
    sendAsyncMsg('got-can-go-back', {
      id: data.json.id,
      successRv: webNav.canGoBack
    });
  },

  _recvCanGoForward: function(data) {
    var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
    sendAsyncMsg('got-can-go-forward', {
      id: data.json.id,
      successRv: webNav.canGoForward
    });
  },

  _recvMute: function(data) {
    this._windowUtils.audioMuted = true;
  },

  _recvUnmute: function(data) {
    this._windowUtils.audioMuted = false;
  },

  _recvGetMuted: function(data) {
    sendAsyncMsg('got-muted', {
      id: data.json.id,
      successRv: this._windowUtils.audioMuted
    });
  },

  _recvSetVolume: function(data) {
    this._windowUtils.audioVolume = data.json.volume;
  },

  _recvGetVolume: function(data) {
    sendAsyncMsg('got-volume', {
      id: data.json.id,
      successRv: this._windowUtils.audioVolume
    });
  },

  _recvGoBack: function(data) {
    try {
      docShell.QueryInterface(Ci.nsIWebNavigation).goBack();
    } catch(e) {
      // Silently swallow errors; these happen when we can't go back.
    }
  },

  _recvGoForward: function(data) {
    try {
      docShell.QueryInterface(Ci.nsIWebNavigation).goForward();
    } catch(e) {
      // Silently swallow errors; these happen when we can't go forward.
    }
  },

  _recvReload: function(data) {
    let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
    let reloadFlags = data.json.hardReload ?
      webNav.LOAD_FLAGS_BYPASS_PROXY | webNav.LOAD_FLAGS_BYPASS_CACHE :
      webNav.LOAD_FLAGS_NONE;
    try {
      webNav.reload(reloadFlags);
    } catch(e) {
      // Silently swallow errors; these can happen if a used cancels reload
    }
  },

  _recvStop: function(data) {
    let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
    webNav.stop(webNav.STOP_NETWORK);
  },

  _recvZoom: function(data) {
    docShell.contentViewer.fullZoom = data.json.zoom;
  },

  _recvDoCommand: function(data) {
    if (this._isCommandEnabled(data.json.command)) {
      this._selectionStateChangedTarget = null;
      docShell.doCommand(COMMAND_MAP[data.json.command]);
    }
  },

  _recvGetAudioChannelVolume: function(data) {
    debug("Received getAudioChannelVolume message: (" + data.json.id + ")");

    let volume = acs.getAudioChannelVolume(content,
                                           data.json.args.audioChannel);
    sendAsyncMsg('got-audio-channel-volume', {
      id: data.json.id, successRv: volume
    });
  },

  _recvSetAudioChannelVolume: function(data) {
    debug("Received setAudioChannelVolume message: (" + data.json.id + ")");

    acs.setAudioChannelVolume(content,
                              data.json.args.audioChannel,
                              data.json.args.volume);
    sendAsyncMsg('got-set-audio-channel-volume', {
      id: data.json.id, successRv: true
    });
  },

  _recvGetAudioChannelMuted: function(data) {
    debug("Received getAudioChannelMuted message: (" + data.json.id + ")");

    let muted = acs.getAudioChannelMuted(content, data.json.args.audioChannel);
    sendAsyncMsg('got-audio-channel-muted', {
      id: data.json.id, successRv: muted
    });
  },

  _recvSetAudioChannelMuted: function(data) {
    debug("Received setAudioChannelMuted message: (" + data.json.id + ")");

    acs.setAudioChannelMuted(content, data.json.args.audioChannel,
                             data.json.args.muted);
    sendAsyncMsg('got-set-audio-channel-muted', {
      id: data.json.id, successRv: true
    });
  },

  _recvIsAudioChannelActive: function(data) {
    debug("Received isAudioChannelActive message: (" + data.json.id + ")");

    let active = acs.isAudioChannelActive(content, data.json.args.audioChannel);
    sendAsyncMsg('got-is-audio-channel-active', {
      id: data.json.id, successRv: active
    });
  },
  _recvGetWebManifest: Task.async(function* (data) {
    debug(`Received GetWebManifest message: (${data.json.id})`);
    let manifest = null;
    let hasManifest = ManifestFinder.contentHasManifestLink(content);
    if (hasManifest) {
      try {
        manifest = yield ManifestObtainer.contentObtainManifest(content);
      } catch (e) {
        sendAsyncMsg('got-web-manifest', {
          id: data.json.id,
          errorMsg: `Error fetching web manifest: ${e}.`,
        });
        return;
      }
    }
    sendAsyncMsg('got-web-manifest', {
      id: data.json.id,
      successRv: manifest
    });
  }),
  _initFinder: function() {
    if (!this._finder) {
      try {
        this._findLimit = Services.prefs.getIntPref("accessibility.typeaheadfind.matchesCountLimit");
      } catch (e) {
        // Pref not available, assume 0, no match counting.
        this._findLimit = 0;
      }

      let {Finder} = Components.utils.import("resource://gre/modules/Finder.jsm", {});
      this._finder = new Finder(docShell);
      this._finder.addResultListener({
        onMatchesCountResult: (data) => {
          sendAsyncMsg('findchange', {
            active: true,
            searchString: this._finder.searchString,
            searchLimit: this._findLimit,
            activeMatchOrdinal: data.current,
            numberOfMatches: data.total
          });
        }
      });
    }
  },

  _recvFindAll: function(data) {
    this._initFinder();
    let searchString = data.json.searchString;
    this._finder.caseSensitive = data.json.caseSensitive;
    this._finder.fastFind(searchString, false, false);
    this._finder.requestMatchesCount(searchString, this._findLimit, false);
  },

  _recvFindNext: function(data) {
    if (!this._finder) {
      debug("findNext() called before findAll()");
      return;
    }
    this._finder.findAgain(data.json.backward, false, false);
    this._finder.requestMatchesCount(this._finder.searchString, this._findLimit, false);
  },

  _recvClearMatch: function(data) {
    if (!this._finder) {
      debug("clearMach() called before findAll()");
      return;
    }
    this._finder.removeSelection();
    sendAsyncMsg('findchange', {active: false});
  },

  _recvSetInputMethodActive: function(data) {
    let msgData = { id: data.json.id };
    if (!this._isContentWindowCreated) {
      if (data.json.args.isActive) {
        // To activate the input method, we should wait before the content
        // window is ready.
        this._pendingSetInputMethodActive.push(data);
        return;
      }
      msgData.successRv = null;
      sendAsyncMsg('got-set-input-method-active', msgData);
      return;
    }
    // Unwrap to access webpage content.
    let nav = XPCNativeWrapper.unwrap(content.document.defaultView.navigator);
    if (nav.mozInputMethod) {
      // Wrap to access the chrome-only attribute setActive.
      new XPCNativeWrapper(nav.mozInputMethod).setActive(data.json.args.isActive);
      msgData.successRv = null;
    } else {
      msgData.errorMsg = 'Cannot access mozInputMethod.';
    }
    sendAsyncMsg('got-set-input-method-active', msgData);
  },

  _processMicroformatValue(field, value) {
    if (['node', 'resolvedNode', 'semanticType'].includes(field)) {
      return null;
    } else if (Array.isArray(value)) {
      var result = value.map(i => this._processMicroformatValue(field, i))
                        .filter(i => i !== null);
      return result.length ? result : null;
    } else if (typeof value == 'string') {
      return value;
    } else if (typeof value == 'object' && value !== null) {
      return this._processMicroformatItem(value);
    }
    return null;
  },

  // This function takes legacy Microformat data (hCard and hCalendar)
  // and produces the same result that the equivalent Microdata data
  // would produce.
  _processMicroformatItem(microformatData) {
    var result = {};

    if (microformatData.semanticType == 'geo') {
      return microformatData.latitude + ';' + microformatData.longitude;
    }

    if (microformatData.semanticType == 'hCard') {
      result.type = ["http://microformats.org/profile/hcard"];
    } else if (microformatData.semanticType == 'hCalendar') {
      result.type = ["http://microformats.org/profile/hcalendar#vevent"];
    }

    for (let field of Object.getOwnPropertyNames(microformatData)) {
      var processed = this._processMicroformatValue(field, microformatData[field]);
      if (processed === null) {
        continue;
      }
      if (!result.properties) {
        result.properties = {};
      }
      if (Array.isArray(processed)) {
        result.properties[field] = processed;
      } else {
        result.properties[field] = [processed];
      }
    }

    return result;
  },

  _findItemProperties: function(node, properties, alreadyProcessed) {
    if (node.itemProp) {
      var value;

      if (node.itemScope) {
        value = this._processItem(node, alreadyProcessed);
      } else {
        value = node.itemValue;
      }

      for (let i = 0; i < node.itemProp.length; ++i) {
        var property = node.itemProp[i];
        if (!properties[property]) {
          properties[property] = [];
        }

        properties[property].push(value);
      }
    }

    if (!node.itemScope) {
      var childNodes = node.childNodes;
      for (var childNode of childNodes) {
        this._findItemProperties(childNode, properties, alreadyProcessed);
      }
    }
  },

  _processItem: function(node, alreadyProcessed = []) {
    if (alreadyProcessed.includes(node)) {
      return "ERROR";
    }

    alreadyProcessed.push(node);

    var result = {};

    if (node.itemId) {
      result.id = node.itemId;
    }
    if (node.itemType) {
      result.type = [];
      for (let i = 0; i < node.itemType.length; ++i) {
        result.type.push(node.itemType[i]);
      }
    }

    var properties = {};

    var childNodes = node.childNodes;
    for (var childNode of childNodes) {
      this._findItemProperties(childNode, properties, alreadyProcessed);
    }

    if (node.itemRef) {
      for (let i = 0; i < node.itemRef.length; ++i) {
        var refNode = content.document.getElementById(node.itemRef[i]);
        this._findItemProperties(refNode, properties, alreadyProcessed);
      }
    }

    result.properties = properties;
    return result;
  },

  _recvGetStructuredData: function(data) {
    var result = {
      items: []
    };

    var microdataItems = content.document.getItems();

    for (let microdataItem of microdataItems) {
      result.items.push(this._processItem(microdataItem));
    }

    var hCardItems = Microformats.get("hCard", content.document);
    for (let hCardItem of hCardItems) {
      if (!hCardItem.node.itemScope) {  // If it's also marked with Microdata, ignore the Microformat
        result.items.push(this._processMicroformatItem(hCardItem));
      }
    }

    var hCalendarItems = Microformats.get("hCalendar", content.document);
    for (let hCalendarItem of hCalendarItems) {
      if (!hCalendarItem.node.itemScope) {  // If it's also marked with Microdata, ignore the Microformat
        result.items.push(this._processMicroformatItem(hCalendarItem));
      }
    }

    var resultString = JSON.stringify(result);

    sendAsyncMsg('got-structured-data', {
      id: data.json.id,
      successRv: resultString
    });
  },

  _processMicroformatValue(field, value) {
    if (['node', 'resolvedNode', 'semanticType'].includes(field)) {
      return null;
    } else if (Array.isArray(value)) {
      var result = value.map(i => this._processMicroformatValue(field, i))
                        .filter(i => i !== null);
      return result.length ? result : null;
    } else if (typeof value == 'string') {
      return value;
    } else if (typeof value == 'object' && value !== null) {
      return this._processMicroformatItem(value);
    }
    return null;
  },

  // This function takes legacy Microformat data (hCard and hCalendar)
  // and produces the same result that the equivalent Microdata data
  // would produce.
  _processMicroformatItem(microformatData) {
    var result = {};

    if (microformatData.semanticType == 'geo') {
      return microformatData.latitude + ';' + microformatData.longitude;
    }

    if (microformatData.semanticType == 'hCard') {
      result.type = ["http://microformats.org/profile/hcard"];
    } else if (microformatData.semanticType == 'hCalendar') {
      result.type = ["http://microformats.org/profile/hcalendar#vevent"];
    }

    for (let field of Object.getOwnPropertyNames(microformatData)) {
      var processed = this._processMicroformatValue(field, microformatData[field]);
      if (processed === null) {
        continue;
      }
      if (!result.properties) {
        result.properties = {};
      }
      if (Array.isArray(processed)) {
        result.properties[field] = processed;
      } else {
        result.properties[field] = [processed];
      }
    }

    return result;
  },

  _findItemProperties: function(node, properties, alreadyProcessed) {
    if (node.itemProp) {
      var value;

      if (node.itemScope) {
        value = this._processItem(node, alreadyProcessed);
      } else {
        value = node.itemValue;
      }

      for (let i = 0; i < node.itemProp.length; ++i) {
        var property = node.itemProp[i];
        if (!properties[property]) {
          properties[property] = [];
        }

        properties[property].push(value);
      }
    }

    if (!node.itemScope) {
      var childNodes = node.childNodes;
      for (var childNode of childNodes) {
        this._findItemProperties(childNode, properties, alreadyProcessed);
      }
    }
  },

  _processItem: function(node, alreadyProcessed = []) {
    if (alreadyProcessed.includes(node)) {
      return "ERROR";
    }

    alreadyProcessed.push(node);

    var result = {};

    if (node.itemId) {
      result.id = node.itemId;
    }
    if (node.itemType) {
      result.type = [];
      for (let i = 0; i < node.itemType.length; ++i) {
        result.type.push(node.itemType[i]);
      }
    }

    var properties = {};

    var childNodes = node.childNodes;
    for (var childNode of childNodes) {
      this._findItemProperties(childNode, properties, alreadyProcessed);
    }

    if (node.itemRef) {
      for (let i = 0; i < node.itemRef.length; ++i) {
        var refNode = content.document.getElementById(node.itemRef[i]);
        this._findItemProperties(refNode, properties, alreadyProcessed);
      }
    }

    result.properties = properties;
    return result;
  },

  _recvGetStructuredData: function(data) {
    var result = {
      items: []
    };

    var microdataItems = content.document.getItems();

    for (let microdataItem of microdataItems) {
      result.items.push(this._processItem(microdataItem));
    }

    var hCardItems = Microformats.get("hCard", content.document);
    for (let hCardItem of hCardItems) {
      if (!hCardItem.node.itemScope) {  // If it's also marked with Microdata, ignore the Microformat
        result.items.push(this._processMicroformatItem(hCardItem));
      }
    }

    var hCalendarItems = Microformats.get("hCalendar", content.document);
    for (let hCalendarItem of hCalendarItems) {
      if (!hCalendarItem.node.itemScope) {  // If it's also marked with Microdata, ignore the Microformat
        result.items.push(this._processMicroformatItem(hCalendarItem));
      }
    }

    var resultString = JSON.stringify(result);

    sendAsyncMsg('got-structured-data', {
      id: data.json.id,
      successRv: resultString
    });
  },

  // The docShell keeps a weak reference to the progress listener, so we need
  // to keep a strong ref to it ourselves.
  _progressListener: {
    QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                           Ci.nsISupportsWeakReference]),
    _seenLoadStart: false,

    onLocationChange: function(webProgress, request, location, flags) {
      // We get progress events from subshells here, which is kind of weird.
      if (webProgress != docShell) {
        return;
      }

      // Ignore locationchange events which occur before the first loadstart.
      // These are usually about:blank loads we don't care about.
      if (!this._seenLoadStart) {
        return;
      }

      // Remove password and wyciwyg from uri.
      location = Cc["@mozilla.org/docshell/urifixup;1"]
        .getService(Ci.nsIURIFixup).createExposableURI(location);

      sendAsyncMsg('locationchange', { _payload_: location.spec });
    },

    onStateChange: function(webProgress, request, stateFlags, status) {
      if (webProgress != docShell) {
        return;
      }

      if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
        this._seenLoadStart = true;
        sendAsyncMsg('loadstart');
      }

      if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
        let bgColor = 'transparent';
        try {
          bgColor = content.getComputedStyle(content.document.body)
                           .getPropertyValue('background-color');
        } catch (e) {}
        sendAsyncMsg('loadend', {backgroundColor: bgColor});

        switch (status) {
          case Cr.NS_OK :
          case Cr.NS_BINDING_ABORTED :
            // Ignoring NS_BINDING_ABORTED, which is set when loading page is
            // stopped.
            return;

          // TODO See nsDocShell::DisplayLoadError to see what extra
          // information we should be annotating this first block of errors
          // with. Bug 1107091.
          case Cr.NS_ERROR_UNKNOWN_PROTOCOL :
            sendAsyncMsg('error', { type: 'unknownProtocolFound' });
            return;
          case Cr.NS_ERROR_FILE_NOT_FOUND :
            sendAsyncMsg('error', { type: 'fileNotFound' });
            return;
          case Cr.NS_ERROR_UNKNOWN_HOST :
            sendAsyncMsg('error', { type: 'dnsNotFound' });
            return;
          case Cr.NS_ERROR_CONNECTION_REFUSED :
            sendAsyncMsg('error', { type: 'connectionFailure' });
            return;
          case Cr.NS_ERROR_NET_INTERRUPT :
            sendAsyncMsg('error', { type: 'netInterrupt' });
            return;
          case Cr.NS_ERROR_NET_TIMEOUT :
            sendAsyncMsg('error', { type: 'netTimeout' });
            return;
          case Cr.NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION :
            sendAsyncMsg('error', { type: 'cspBlocked' });
            return;
          case Cr.NS_ERROR_PHISHING_URI :
            sendAsyncMsg('error', { type: 'phishingBlocked' });
            return;
          case Cr.NS_ERROR_MALWARE_URI :
            sendAsyncMsg('error', { type: 'malwareBlocked' });
            return;
          case Cr.NS_ERROR_UNWANTED_URI :
            sendAsyncMsg('error', { type: 'unwantedBlocked' });
            return;
          case Cr.NS_ERROR_FORBIDDEN_URI :
            sendAsyncMsg('error', { type: 'forbiddenBlocked' });
            return;

          case Cr.NS_ERROR_OFFLINE :
            sendAsyncMsg('error', { type: 'offline' });
            return;
          case Cr.NS_ERROR_MALFORMED_URI :
            sendAsyncMsg('error', { type: 'malformedURI' });
            return;
          case Cr.NS_ERROR_REDIRECT_LOOP :
            sendAsyncMsg('error', { type: 'redirectLoop' });
            return;
          case Cr.NS_ERROR_UNKNOWN_SOCKET_TYPE :
            sendAsyncMsg('error', { type: 'unknownSocketType' });
            return;
          case Cr.NS_ERROR_NET_RESET :
            sendAsyncMsg('error', { type: 'netReset' });
            return;
          case Cr.NS_ERROR_DOCUMENT_NOT_CACHED :
            sendAsyncMsg('error', { type: 'notCached' });
            return;
          case Cr.NS_ERROR_DOCUMENT_IS_PRINTMODE :
            sendAsyncMsg('error', { type: 'isprinting' });
            return;
          case Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED :
            sendAsyncMsg('error', { type: 'deniedPortAccess' });
            return;
          case Cr.NS_ERROR_UNKNOWN_PROXY_HOST :
            sendAsyncMsg('error', { type: 'proxyResolveFailure' });
            return;
          case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED :
            sendAsyncMsg('error', { type: 'proxyConnectFailure' });
            return;
          case Cr.NS_ERROR_INVALID_CONTENT_ENCODING :
            sendAsyncMsg('error', { type: 'contentEncodingFailure' });
            return;
          case Cr.NS_ERROR_REMOTE_XUL :
            sendAsyncMsg('error', { type: 'remoteXUL' });
            return;
          case Cr.NS_ERROR_UNSAFE_CONTENT_TYPE :
            sendAsyncMsg('error', { type: 'unsafeContentType' });
            return;
          case Cr.NS_ERROR_CORRUPTED_CONTENT :
            sendAsyncMsg('error', { type: 'corruptedContentError' });
            return;

          default:
            // getErrorClass() will throw if the error code passed in is not a NSS
            // error code.
            try {
              let nssErrorsService = Cc['@mozilla.org/nss_errors_service;1']
                                       .getService(Ci.nsINSSErrorsService);
              if (nssErrorsService.getErrorClass(status)
                    == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
                // XXX Is there a point firing the event if the error page is not
                // certerror? If yes, maybe we should add a property to the
                // event to to indicate whether there is a custom page. That would
                // let the embedder have more control over the desired behavior.
                let errorPage = null;
                try {
                  errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF);
                } catch (e) {}

                if (errorPage == 'certerror') {
                  sendAsyncMsg('error', { type: 'certerror' });
                  return;
                }
              }
            } catch (e) {}

            sendAsyncMsg('error', { type: 'other' });
            return;
        }
      }
    },

    onSecurityChange: function(webProgress, request, state) {
      if (webProgress != docShell) {
        return;
      }

      var securityStateDesc;
      if (state & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
        securityStateDesc = 'secure';
      }
      else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
        securityStateDesc = 'broken';
      }
      else if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
        securityStateDesc = 'insecure';
      }
      else {
        debug("Unexpected securitychange state!");
        securityStateDesc = '???';
      }

      var trackingStateDesc;
      if (state & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) {
        trackingStateDesc = 'loaded_tracking_content';
      }
      else if (state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
        trackingStateDesc = 'blocked_tracking_content';
      }

      var mixedStateDesc;
      if (state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
        mixedStateDesc = 'blocked_mixed_active_content';
      }
      else if (state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
        // Note that STATE_LOADED_MIXED_ACTIVE_CONTENT implies STATE_IS_BROKEN
        mixedStateDesc = 'loaded_mixed_active_content';
      }

      var isEV = !!(state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL);
      var isTrackingContent = !!(state &
        (Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT |
        Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT));
      var isMixedContent = !!(state &
        (Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT |
        Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT));

      sendAsyncMsg('securitychange', {
        state: securityStateDesc,
        trackingState: trackingStateDesc,
        mixedState: mixedStateDesc,
        extendedValidation: isEV,
        trackingContent: isTrackingContent,
        mixedContent: isMixedContent,
      });
    },

    onStatusChange: function(webProgress, request, status, message) {},
    onProgressChange: function(webProgress, request, curSelfProgress,
                               maxSelfProgress, curTotalProgress, maxTotalProgress) {},
  },

  // Expose the message manager for WebApps and others.
  _messageManagerPublic: {
    sendAsyncMessage: global.sendAsyncMessage.bind(global),
    sendSyncMessage: global.sendSyncMessage.bind(global),
    addMessageListener: global.addMessageListener.bind(global),
    removeMessageListener: global.removeMessageListener.bind(global)
  },

  get messageManager() {
    return this._messageManagerPublic;
  }
};

var api = null;
if ('DoPreloadPostfork' in this && typeof this.DoPreloadPostfork === 'function') {
  // If we are preloaded, instantiate BrowserElementChild after a content
  // process is forked.
  this.DoPreloadPostfork(function() {
    api = new BrowserElementChild();
  });
} else {
  api = new BrowserElementChild();
}
