Bug 1360406 - Remove context menu sync IPC. r=felipe
authorPerry Jiang <jiangperry@gmail.com>
Mon, 21 Aug 2017 15:42:37 -0700
changeset 429129 70dca88c5cc3cfb927d49068645456e913e5e7ff
parent 429128 604fd3302562dc2059a8de3231818a1b618b8ffe
child 429130 bf7793529f82ab9cb885a62446ed0f3e18c51634
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe
bugs1360406
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1360406 - Remove context menu sync IPC. r=felipe MozReview-Commit-ID: 1oCwC283Suv
browser/base/content/content.js
browser/base/content/nsContextMenu.js
browser/base/content/pageinfo/pageInfo.js
browser/base/content/test/general/browser_addKeywordSearch.js
browser/base/content/test/general/browser_contextmenu.js
browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
browser/modules/ContextMenu.jsm
browser/modules/PluginContent.jsm
browser/modules/moz.build
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -9,57 +9,45 @@
 /* eslint-env mozilla/frame-script */
 
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  E10SUtils: "resource:///modules/E10SUtils.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   ContentLinkHandler: "resource:///modules/ContentLinkHandler.jsm",
   ContentWebRTC: "resource:///modules/ContentWebRTC.jsm",
-  SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
   InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
   LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
   LoginFormFactory: "resource://gre/modules/LoginManagerContent.jsm",
   InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm",
   PluginContent: "resource:///modules/PluginContent.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   FormSubmitObserver: "resource:///modules/FormSubmitObserver.jsm",
   PageMetadata: "resource://gre/modules/PageMetadata.jsm",
   PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   Feeds: "resource:///modules/Feeds.jsm",
-  findCssSelector: "resource://gre/modules/css-selector.js",
+  ContextMenu: "resource:///modules/ContextMenu.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
-  let tmp = {};
-  Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
-  return new tmp.PageMenuChild();
-});
-
-Cu.importGlobalProperties(["URL"]);
-
 // TabChildGlobal
 var global = this;
 
+var contextMenu = this.contextMenu = new ContextMenu(global);
+
 // Load the form validation popup handler
 var formSubmitObserver = new FormSubmitObserver(content, this);
 
-addMessageListener("ContextMenu:DoCustomCommand", function(message) {
-  E10SUtils.wrapHandlingUserInput(
-    content, message.data.handlingUserInput,
-    () => PageMenuChild.executeMenu(message.data.generatedItemId));
-});
-
 addMessageListener("RemoteLogins:fillForm", function(message) {
+  // intercept if ContextMenu.jsm had sent a plain object for remote targets
+  message.objects.inputElement = contextMenu.getTarget(message, "inputElement");
   LoginManagerContent.receiveMessage(message, content);
 });
 addEventListener("DOMFormHasPassword", function(event) {
   LoginManagerContent.onDOMFormHasPassword(event, content);
   let formLike = LoginFormFactory.createFromForm(event.target);
   InsecurePasswordUtils.reportInsecurePasswords(formLike);
 });
 addEventListener("DOMInputPasswordAdded", function(event) {
@@ -72,154 +60,16 @@ addEventListener("pageshow", function(ev
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 
-var handleContentContextMenu = function(event) {
-  let defaultPrevented = event.defaultPrevented;
-  if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
-    let plugin = null;
-    try {
-      plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent);
-    } catch (e) {}
-    if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
-      // Don't open a context menu for plugins.
-      return;
-    }
-
-    defaultPrevented = false;
-  }
-
-  if (defaultPrevented)
-    return;
-
-  let addonInfo = {};
-  let subject = {
-    event,
-    addonInfo,
-  };
-  subject.wrappedJSObject = subject;
-  Services.obs.notifyObservers(subject, "content-contextmenu");
-
-  let doc = event.target.ownerDocument;
-  let docLocation = doc.mozDocumentURIIfNotForErrorPages;
-  docLocation = docLocation && docLocation.spec;
-  let charSet = doc.characterSet;
-  let baseURI = doc.baseURI;
-  let referrer = doc.referrer;
-  let referrerPolicy = doc.referrerPolicy;
-  let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
-  let loginFillInfo = LoginManagerContent.getFieldContext(event.target);
-
-  // The same-origin check will be done in nsContextMenu.openLinkInTab.
-  let parentAllowsMixedContent = !!docShell.mixedContentChannel;
-
-  // get referrer attribute from clicked link and parse it
-  let referrerAttrValue = Services.netUtils.parseAttributePolicyString(event.target.
-                          getAttribute("referrerpolicy"));
-  if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
-    referrerPolicy = referrerAttrValue;
-  }
-
-  let disableSetDesktopBg = null;
-  // Media related cache info parent needs for saving
-  let contentType = null;
-  let contentDisposition = null;
-  if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
-      event.target instanceof Ci.nsIImageLoadingContent &&
-      event.target.currentURI) {
-    disableSetDesktopBg = disableSetDesktopBackground(event.target);
-
-    try {
-      let imageCache =
-        Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
-                                        .getImgCacheForDocument(doc);
-      let props =
-        imageCache.findEntryProperties(event.target.currentURI, doc);
-      try {
-        contentType = props.get("type", Ci.nsISupportsCString).data;
-      } catch (e) {}
-      try {
-        contentDisposition =
-          props.get("content-disposition", Ci.nsISupportsCString).data;
-      } catch (e) {}
-    } catch (e) {}
-  }
-
-  let selectionInfo = BrowserUtils.getSelectionDetails(content);
-
-  let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
-  let userContextId = loadContext.originAttributes.userContextId;
-  let popupNodeSelectors = getNodeSelectors(event.target);
-
-  if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-    let editFlags = SpellCheckHelper.isEditable(event.target, content);
-    let spellInfo;
-    if (editFlags &
-        (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
-      spellInfo =
-        InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
-    }
-
-    // Set the event target first as the copy image command needs it to
-    // determine what was context-clicked on. Then, update the state of the
-    // commands on the context menu.
-    docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
-            .setCommandNode(event.target);
-    event.target.ownerGlobal.updateCommands("contentcontextmenu");
-
-    let customMenuItems = PageMenuChild.build(event.target);
-    let principal = doc.nodePrincipal;
-
-    sendRpcMessage("contextmenu",
-                   { editFlags, spellInfo, customMenuItems, addonInfo,
-                     principal, docLocation, charSet, baseURI, referrer,
-                     referrerPolicy, contentType, contentDisposition,
-                     frameOuterWindowID, selectionInfo, disableSetDesktopBg,
-                     loginFillInfo, parentAllowsMixedContent, userContextId,
-                     popupNodeSelectors,
-                   }, {
-                     event,
-                     popupNode: event.target,
-                   });
-  } else {
-    // Break out to the parent window and pass the add-on info along
-    let browser = docShell.chromeEventHandler;
-    let mainWin = browser.ownerGlobal;
-    mainWin.setContextMenuContentData({
-      isRemote: false,
-      event,
-      popupNode: event.target,
-      popupNodeSelectors,
-      browser,
-      addonInfo,
-      documentURIObject: doc.documentURIObject,
-      docLocation,
-      charSet,
-      referrer,
-      referrerPolicy,
-      contentType,
-      contentDisposition,
-      selectionInfo,
-      disableSetDesktopBackground: disableSetDesktopBg,
-      loginFillInfo,
-      parentAllowsMixedContent,
-      userContextId,
-    });
-  }
-}
-
-Cc["@mozilla.org/eventlistenerservice;1"]
-  .getService(Ci.nsIEventListenerService)
-  .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
-
 // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
 const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
 const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1;
 const TLS_ERROR_REPORT_TELEMETRY_SUCCESS  = 6;
 const TLS_ERROR_REPORT_TELEMETRY_FAILURE  = 7;
 
 const SEC_ERROR_BASE          = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
 const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
@@ -232,39 +82,16 @@ const SEC_ERROR_OCSP_OLD_RESPONSE       
 const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 5;
 const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 6;
 
 const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds";
 
 const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
 
 
-/**
- * Retrieve the array of CSS selectors corresponding to the provided node. The first item
- * of the array is the selector of the node in its owner document. Additional items are
- * used if the node is inside a frame, each representing the CSS selector for finding the
- * frame element in its parent document.
- *
- * This format is expected by DevTools in order to handle the Inspect Node context menu
- * item.
- *
- * @param  {Node}
- *         The node for which the CSS selectors should be computed
- * @return {Array} array of css selectors (strings).
- */
-function getNodeSelectors(node) {
-  let selectors = [];
-  while (node) {
-    selectors.push(findCssSelector(node));
-    node = node.ownerGlobal.frameElement;
-  }
-
-  return selectors;
-}
-
 function getSerializedSecurityInfo(docShell) {
   let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                     .getService(Ci.nsISerializationHelper);
 
   let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo;
   if (!securityInfo) {
     return "";
   }
@@ -805,175 +632,32 @@ addEventListener("pagehide", function(ev
 var PageMetadataMessenger = {
   init() {
     addMessageListener("PageMetadata:GetPageData", this);
     addMessageListener("PageMetadata:GetMicroformats", this);
   },
   receiveMessage(message) {
     switch (message.name) {
       case "PageMetadata:GetPageData": {
-        let target = message.objects.target;
+        let target = contextMenu.getTarget(message);
         let result = PageMetadata.getData(content.document, target);
         sendAsyncMessage("PageMetadata:PageDataResult", result);
         break;
       }
       case "PageMetadata:GetMicroformats": {
-        let target = message.objects.target;
+        let target = contextMenu.getTarget(message);
         let result = PageMetadata.getMicroformats(content.document, target);
         sendAsyncMessage("PageMetadata:MicroformatsResult", result);
         break;
       }
     }
   }
 }
 PageMetadataMessenger.init();
 
-addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
-  let video = message.objects.target;
-  let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-  canvas.width = video.videoWidth;
-  canvas.height = video.videoHeight;
-
-  let ctxDraw = canvas.getContext("2d");
-  ctxDraw.drawImage(video, 0, 0);
-  sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
-    dataURL: canvas.toDataURL("image/jpeg", ""),
-  });
-});
-
-addMessageListener("ContextMenu:MediaCommand", (message) => {
-  E10SUtils.wrapHandlingUserInput(
-    content, message.data.handlingUserInput,
-    () => {
-      let media = message.objects.element;
-      switch (message.data.command) {
-        case "play":
-          media.play();
-          break;
-        case "pause":
-          media.pause();
-          break;
-        case "loop":
-          media.loop = !media.loop;
-          break;
-        case "mute":
-          media.muted = true;
-          break;
-        case "unmute":
-          media.muted = false;
-          break;
-        case "playbackRate":
-          media.playbackRate = message.data.data;
-          break;
-        case "hidecontrols":
-          media.removeAttribute("controls");
-          break;
-        case "showcontrols":
-          media.setAttribute("controls", "true");
-          break;
-        case "fullscreen":
-          if (content.document.fullscreenEnabled)
-            media.requestFullscreen();
-          break;
-      }
-    });
-});
-
-addMessageListener("ContextMenu:Canvas:ToBlobURL", (message) => {
-  message.objects.target.toBlob((blob) => {
-    let blobURL = URL.createObjectURL(blob);
-    sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
-  });
-});
-
-addMessageListener("ContextMenu:ReloadFrame", (message) => {
-  message.objects.target.ownerDocument.location.reload();
-});
-
-addMessageListener("ContextMenu:ReloadImage", (message) => {
-  let image = message.objects.target;
-  if (image instanceof Ci.nsIImageLoadingContent)
-    image.forceReload();
-});
-
-addMessageListener("ContextMenu:BookmarkFrame", (message) => {
-  let frame = message.objects.target.ownerDocument;
-  sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
-                   { title: frame.title,
-                     description: PlacesUIUtils.getDescriptionFromDocument(frame) });
-});
-
-addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => {
-  let node = message.objects.target;
-
-  let charset = node.ownerDocument.characterSet;
-
-  let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
-
-  let formURI = Services.io.newURI(node.form.getAttribute("action"), charset,
-                                   formBaseURI);
-
-  let spec = formURI.spec;
-
-  let isURLEncoded =
-               (node.form.method.toUpperCase() == "POST"
-                && (node.form.enctype == "application/x-www-form-urlencoded" ||
-                    node.form.enctype == ""));
-
-  let title = node.ownerDocument.title;
-  let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
-
-  let formData = [];
-
-  function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
-    if (aIsFormUrlEncoded) {
-      return escape(aName + "=" + aValue);
-    }
-    return escape(aName) + "=" + escape(aValue);
-  }
-
-  for (let el of node.form.elements) {
-    if (!el.type) // happens with fieldsets
-      continue;
-
-    if (el == node) {
-      formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
-                                     // Don't escape "%s", just append
-                                     escapeNameValuePair(el.name, "", false) + "%s");
-      continue;
-    }
-
-    let type = el.type.toLowerCase();
-
-    if (((el instanceof content.HTMLInputElement && el.mozIsTextField(true)) ||
-        type == "hidden" || type == "textarea") ||
-        ((type == "checkbox" || type == "radio") && el.checked)) {
-      formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
-    } else if (el instanceof content.HTMLSelectElement && el.selectedIndex >= 0) {
-      for (let j = 0; j < el.options.length; j++) {
-        if (el.options[j].selected)
-          formData.push(escapeNameValuePair(el.name, el.options[j].value,
-                                            isURLEncoded));
-      }
-    }
-  }
-
-  let postData;
-
-  if (isURLEncoded)
-    postData = formData.join("&");
-  else {
-    let separator = spec.includes("?") ? "&" : "?";
-    spec += separator + formData.join("&");
-  }
-
-  sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
-                   { spec, title, description, postData, charset });
-});
-
 addMessageListener("Bookmarks:GetPageDetails", (message) => {
   let doc = content.document;
   let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI);
   sendAsyncMessage("Bookmarks:GetPageDetails:Result",
                    { isErrorPage,
                      description: PlacesUIUtils.getDescriptionFromDocument(doc) });
 });
 
@@ -1023,66 +707,16 @@ var LightWeightThemeWebInstallListener =
   _resetPreviewWindow() {
     this._previewWindow.removeEventListener("pagehide", this, true);
     this._previewWindow = null;
   }
 };
 
 LightWeightThemeWebInstallListener.init();
 
-function disableSetDesktopBackground(aTarget) {
-  // Disable the Set as Desktop Background menu item if we're still trying
-  // to load the image or the load failed.
-  if (!(aTarget instanceof Ci.nsIImageLoadingContent))
-    return true;
-
-  if (("complete" in aTarget) && !aTarget.complete)
-    return true;
-
-  if (aTarget.currentURI.schemeIs("javascript"))
-    return true;
-
-  let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
-                       .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
-  if (!request)
-    return true;
-
-  return false;
-}
-
-addMessageListener("ContextMenu:SetAsDesktopBackground", (message) => {
-  let target = message.objects.target;
-
-  // Paranoia: check disableSetDesktopBackground again, in case the
-  // image changed since the context menu was initiated.
-  let disable = disableSetDesktopBackground(target);
-
-  if (!disable) {
-    try {
-      BrowserUtils.urlSecurityCheck(target.currentURI.spec, target.ownerDocument.nodePrincipal);
-      let canvas = content.document.createElement("canvas");
-      canvas.width = target.naturalWidth;
-      canvas.height = target.naturalHeight;
-      let ctx = canvas.getContext("2d");
-      ctx.drawImage(target, 0, 0);
-      let dataUrl = canvas.toDataURL();
-      let url = (new URL(target.ownerDocument.location.href)).pathname;
-      let imageName = url.substr(url.lastIndexOf("/") + 1);
-      sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
-                       { dataUrl, imageName });
-    } catch (e) {
-      Cu.reportError(e);
-      disable = true;
-    }
-  }
-
-  if (disable)
-    sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable });
-});
-
 var PageInfoListener = {
 
   init() {
     addMessageListener("PageInfo:getData", this);
   },
 
   receiveMessage(message) {
     let strings = message.data.strings;
@@ -1095,43 +729,27 @@ var PageInfoListener = {
     if (frameOuterWindowID != undefined) {
       window = Services.wm.getOuterWindowWithId(frameOuterWindowID);
       document = window.document;
     } else {
       window = content.window;
       document = content.document;
     }
 
-    let imageElement = message.objects.imageElement;
-
     let pageInfoData = {metaViewRows: this.getMetaInfo(document),
                         docInfo: this.getDocumentInfo(document),
                         feeds: this.getFeedsInfo(document, strings),
-                        windowInfo: this.getWindowInfo(window),
-                        imageInfo: this.getImageInfo(imageElement)};
+                        windowInfo: this.getWindowInfo(window)};
 
     sendAsyncMessage("PageInfo:data", pageInfoData);
 
     // Separate step so page info dialog isn't blank while waiting for this to finish.
     this.getMediaInfo(document, window, strings);
   },
 
-  getImageInfo(imageElement) {
-    let imageInfo = null;
-    if (imageElement) {
-      imageInfo = {
-        currentSrc: imageElement.currentSrc,
-        width: imageElement.width,
-        height: imageElement.height,
-        imageText: imageElement.title || imageElement.alt
-      };
-    }
-    return imageInfo;
-  },
-
   getMetaInfo(document) {
     let metaViewRows = [];
 
     // Get the meta tags from the page.
     let metaNodes = document.getElementsByTagName("meta");
 
     for (let metaNode of metaNodes) {
       metaViewRows.push([metaNode.name || metaNode.httpEquiv || metaNode.getAttribute("property"),
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -22,26 +22,27 @@ var gContextMenuContentData = null;
 
 function setContextMenuContentData(data) {
   gContextMenuContentData = data;
 }
 
 function openContextMenu(aMessage) {
   let data = aMessage.data;
   let browser = aMessage.target;
+  let spellInfo = data.spellInfo;
 
-  let spellInfo = data.spellInfo;
-  if (spellInfo)
+  if (spellInfo) {
     spellInfo.target = aMessage.target.messageManager;
+  }
+
   let documentURIObject = makeURI(data.docLocation,
                                   data.charSet,
                                   makeURI(data.baseURI));
-  gContextMenuContentData = { isRemote: true,
-                              event: aMessage.objects.event,
-                              popupNode: aMessage.objects.popupNode,
+  gContextMenuContentData = { context: data.context,
+                              isRemote: data.isRemote,
                               popupNodeSelectors: data.popupNodeSelectors,
                               browser,
                               editFlags: data.editFlags,
                               spellInfo,
                               principal: data.principal,
                               customMenuItems: data.customMenuItems,
                               addonInfo: data.addonInfo,
                               documentURIObject,
@@ -53,40 +54,41 @@ function openContextMenu(aMessage) {
                               contentDisposition: data.contentDisposition,
                               frameOuterWindowID: data.frameOuterWindowID,
                               selectionInfo: data.selectionInfo,
                               disableSetDesktopBackground: data.disableSetDesktopBg,
                               loginFillInfo: data.loginFillInfo,
                               parentAllowsMixedContent: data.parentAllowsMixedContent,
                               userContextId: data.userContextId,
                             };
+
   let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
-  let event = gContextMenuContentData.event;
+  let context = gContextMenuContentData.context;
 
   // The event is a CPOW that can't be passed into the native openPopupAtScreen
   // function. Therefore we synthesize a new MouseEvent to propagate the
   // inputSource to the subsequently triggered popupshowing event.
   var newEvent = document.createEvent("MouseEvent");
-  newEvent.initNSMouseEvent("contextmenu", true, true, null, 0, event.screenX, event.screenY,
-                            0, 0, false, false, false, false, 0, null, 0, event.mozInputSource);
+  newEvent.initNSMouseEvent("contextmenu", true, true, null, 0, context.screenX, context.screenY,
+                            0, 0, false, false, false, false, 0, null, 0, context.mozInputSource);
 
   popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
 }
 
 function nsContextMenu(aXulMenu, aIsShift) {
   this.shouldDisplay = true;
   this.initMenu(aXulMenu, aIsShift);
 }
 
 // Prototype for nsContextMenu "class."
 nsContextMenu.prototype = {
   initMenu: function CM_initMenu(aXulMenu, aIsShift) {
     // Get contextual info.
-    this.setTarget(document.popupNode, document.popupRangeParent,
-                   document.popupRangeOffset);
+    this.setContext();
+
     if (!this.shouldDisplay)
       return;
 
     this.hasPageMenu = false;
     this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
     if (!aIsShift) {
       if (this.isRemote) {
         this.hasPageMenu =
@@ -140,17 +142,144 @@ nsContextMenu.prototype = {
 
     // Initialize (disable/remove) menu items.
     this.initItems();
 
     // Register this opening of the menu with telemetry:
     this._checkTelemetryForMenu(aXulMenu);
   },
 
+  setContext() {
+    let context = Object.create(null);
+    this.isRemote = false;
+
+    if (gContextMenuContentData) {
+      context = gContextMenuContentData.context;
+      gContextMenuContentData.context = null;
+      this.isRemote = gContextMenuContentData.isRemote;
+    }
+
+    this.shouldDisplay = context.shouldDisplay;
+
+    // Assign what's _possibly_ needed from `context` sent by ContextMenu.jsm
+    // Keep this consistent with the similar code in ContextMenu's _setContext
+    this.bgImageURL          = context.bgImageURL;
+    this.imageDescURL        = context.imageDescURL;
+    this.imageInfo           = context.imageInfo;
+    this.mediaURL            = context.mediaURL;
+    this.webExtBrowserType   = context.webExtBrowserType;
+
+    this.canSpellCheck       = context.canSpellCheck;
+    this.hasBGImage          = context.hasBGImage;
+    this.hasMultipleBGImages = context.hasMultipleBGImages;
+    this.isDesignMode        = context.isDesignMode;
+    this.inFrame             = context.inFrame;
+    this.inSrcdocFrame       = context.inSrcdocFrame;
+    this.inSyntheticDoc      = context.inSyntheticDoc;
+    this.inTabBrowser        = context.inTabBrowser;
+    this.inWebExtBrowser     = context.inWebExtBrowser;
+
+    this.link                = context.link;
+    this.linkDownload        = context.linkDownload;
+    this.linkHasNoReferrer   = context.linkHasNoReferrer;
+    this.linkProtocol        = context.linkProtocol;
+    this.linkTextStr         = context.linkTextStr;
+    this.linkURL             = context.linkURL;
+    this.linkURI             = this.getLinkURI();  // can't send; regenerate
+
+    this.onAudio             = context.onAudio;
+    this.onCanvas            = context.onCanvas;
+    this.onCompletedImage    = context.onCompletedImage;
+    this.onCTPPlugin         = context.onCTPPlugin;
+    this.onDRMMedia          = context.onDRMMedia;
+    this.onEditableArea      = context.onEditableArea;
+    this.onImage             = context.onImage;
+    this.onKeywordField      = context.onKeywordField;
+    this.onLink              = context.onLink;
+    this.onLoadedImage       = context.onLoadedImage;
+    this.onMailtoLink        = context.onMailtoLink;
+    this.onMathML            = context.onMathML;
+    this.onMozExtLink        = context.onMozExtLink;
+    this.onNumeric           = context.onNumeric;
+    this.onPassword          = context.onPassword;
+    this.onSaveableLink      = context.onSaveableLink;
+    this.onTextInput         = context.onTextInput;
+    this.onVideo             = context.onVideo;
+
+    this.target = this.isRemote ? context.target : document.popupNode;
+
+    this.principal = context.principal;
+    this.frameOuterWindowID = context.frameOuterWindowID;
+
+    this.inSyntheticDoc = context.inSyntheticDoc;
+
+    // Everything after this isn't sent directly from ContextMenu
+    this.ownerDoc = this.target.ownerDocument;
+
+    // Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
+    // can be null if the menu was triggered by tests in which case use an empty array.
+    this.targetSelectors = gContextMenuContentData
+                           ? gContextMenuContentData.popupNodeSelectors
+                           : [];
+
+    if (this.isRemote) {
+      this.browser = gContextMenuContentData.browser;
+      this.selectionInfo = gContextMenuContentData.selectionInfo;
+    } else {
+      this.browser = this.ownerDoc.defaultView
+                         .QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation)
+                         .QueryInterface(Ci.nsIDocShell)
+                         .chromeEventHandler;
+      this.selectionInfo = BrowserUtils.getSelectionDetails(window);
+    }
+
+    this.textSelected      = this.selectionInfo.text;
+    this.isTextSelected    = this.textSelected.length != 0;
+    this.webExtBrowserType = this.browser.getAttribute("webextension-view-type");
+    this.inWebExtBrowser   = !!this.webExtBrowserType;
+    this.inTabBrowser      = this.browser.ownerGlobal.gBrowser ?
+      !!this.browser.ownerGlobal.gBrowser.getTabForBrowser(this.browser) : false;
+
+    if (context.shouldInitInlineSpellCheckerUINoChildren) {
+      if (this.isRemote) {
+        InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+      } else {
+        InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
+        InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
+                                           document.popupRangeOffset);
+      }
+    }
+
+    if (context.shouldInitInlineSpellCheckerUIWithChildren) {
+      if (this.isRemote) {
+        InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
+      } else {
+        var targetWin = this.ownerDoc.defaultView;
+        var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                                      .getInterface(Ci.nsIWebNavigation)
+                                      .QueryInterface(Ci.nsIInterfaceRequestor)
+                                      .getInterface(Ci.nsIEditingSession);
+
+        InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
+        InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
+                                           document.popupRangeOffset);
+      }
+
+      let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
+      this.showItem("spell-check-enabled", canSpell);
+      this.showItem("spell-separator", canSpell);
+    }
+  },  // setContext
+
   hiding: function CM_hiding() {
+    if (this.browser) {
+      this.browser.messageManager.sendAsyncMessage("ContextMenu:Hiding");
+    }
+
     gContextMenuContentData = null;
     InlineSpellCheckerUI.clearSuggestionsFromMenu();
     InlineSpellCheckerUI.clearDictionaryListFromMenu();
     InlineSpellCheckerUI.uninit();
     if (Cu.isModuleLoaded("resource://gre/modules/LoginManagerContextMenu.jsm")) {
       LoginManagerContextMenu.clearLoginsFromMenu(document);
     }
 
@@ -273,31 +402,20 @@ nsContextMenu.prototype = {
     this.showItem("context-savevideo", this.onVideo);
     this.showItem("context-saveaudio", this.onAudio);
     this.showItem("context-video-saveimage", this.onVideo);
     this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
     this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
     // Send media URL (but not for canvas, since it's a big data: URL)
     this.showItem("context-sendimage", this.onImage);
     this.showItem("context-sendvideo", this.onVideo);
-    this.showItem("context-castvideo", this.onVideo);
     this.showItem("context-sendaudio", this.onAudio);
     let mediaIsBlob = this.mediaURL.startsWith("blob:");
     this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL || mediaIsBlob);
     this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL || mediaIsBlob);
-    let shouldShowCast = Services.prefs.getBoolPref("browser.casting.enabled");
-    // getServicesForVideo alone would be sufficient here (it depends on
-    // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed
-    // to be already loaded, since we load it on startup in nsBrowserGlue,
-    // and CastingApps isn't, so check SimpleServiceDiscovery.services first
-    // to avoid needing to load CastingApps.jsm if we don't need to.
-    shouldShowCast = shouldShowCast && this.mediaURL &&
-                     SimpleServiceDiscovery.services.length > 0 &&
-                     CastingApps.getServicesForVideo(this.target).length > 0;
-    this.setItemAttr("context-castvideo", "disabled", !shouldShowCast);
   },
 
   initViewItems: function CM_initViewItems() {
     // View source is always OK, unless in directory listing.
     this.showItem("context-viewpartialsource-selection",
                   this.isContentSelected);
     this.showItem("context-viewpartialsource-mathml",
                   this.onMathML && !this.isContentSelected);
@@ -349,20 +467,20 @@ nsContextMenu.prototype = {
 
     // View video depends on not having a standalone video.
     this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame));
     this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
 
     // View background image depends on whether there is one, but don't make
     // background images of a stand-alone media document available.
     this.showItem("context-viewbgimage", shouldShow &&
-                                         !this._hasMultipleBGImages &&
+                                         !this.hasMultipleBGImages &&
                                          !this.inSyntheticDoc);
     this.showItem("context-sep-viewbgimage", shouldShow &&
-                                             !this._hasMultipleBGImages &&
+                                             !this.hasMultipleBGImages &&
                                              !this.inSyntheticDoc);
     document.getElementById("context-viewbgimage")
             .disabled = !this.hasBGImage;
 
     this.showItem("context-viewimageinfo", this.onImage);
     // The image info popup is broken for WebExtension popups, since the browser
     // is destroyed when the popup is closed.
     this.setItemAttr("context-viewimageinfo", "disabled", this.webExtBrowserType === "popup");
@@ -619,437 +737,16 @@ nsContextMenu.prototype = {
   openPasswordManager() {
     LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
   },
 
   inspectNode() {
     return DevToolsShim.inspectNode(gBrowser.selectedTab, this.targetSelectors);
   },
 
-  /**
-   * Set various context menu attributes based on the state of the world.
-   * Note: If the context menu is on a remote process the supplied parameters
-   * will be overwritten with data from gContextMenuContentData.
-   *
-   * @param {Object} aNode The node that this menu is being opened on.
-   * @param {nsIDOMNode} aRangeParent The parent node for where the selection ends.
-   * @param {Integer} aRangeOffset The end position of where the selction ends.
-   */
-  setTarget(aNode, aRangeParent, aRangeOffset) {
-    // gContextMenuContentData.isRemote tells us if the event came from a remote
-    // process. gContextMenuContentData can be null if something (like tests)
-    // opens the context menu directly.
-    this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
-    if (this.isRemote) {
-      aNode = gContextMenuContentData.event.target;
-      aRangeParent = gContextMenuContentData.event.rangeParent;
-      aRangeOffset = gContextMenuContentData.event.rangeOffset;
-    }
-
-    const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    if (aNode.nodeType == Node.DOCUMENT_NODE ||
-        // Not display on XUL element but relax for <label class="text-link">
-        (aNode.namespaceURI == xulNS && !this._isXULTextLinkLabel(aNode))) {
-      this.shouldDisplay = false;
-      return;
-    }
-
-    // Initialize contextual info.
-    this.onImage           = false;
-    this.onLoadedImage     = false;
-    this.onCompletedImage  = false;
-    this.imageDescURL      = "";
-    this.onCanvas          = false;
-    this.onVideo           = false;
-    this.onAudio           = false;
-    this.onDRMMedia        = false;
-    this.onTextInput       = false;
-    this.onNumeric         = false;
-    this.onKeywordField    = false;
-    this.mediaURL          = "";
-    this.onLink            = false;
-    this.onMailtoLink      = false;
-    this.onSaveableLink    = false;
-    this.link              = null;
-    this.linkURL           = "";
-    this.linkURI           = null;
-    this.linkTextStr       = "";
-    this.linkProtocol      = "";
-    this.linkDownload      = "";
-    this.linkHasNoReferrer = false;
-    this.onMathML          = false;
-    this.inFrame           = false;
-    this.inSrcdocFrame     = false;
-    this.inSyntheticDoc    = false;
-    this.hasBGImage        = false;
-    this.bgImageURL        = "";
-    this.onEditableArea    = false;
-    this.isDesignMode      = false;
-    this.onCTPPlugin       = false;
-    this.canSpellCheck     = false;
-    this.onPassword        = false;
-    this.webExtBrowserType = "";
-    this.inWebExtBrowser   = false;
-    this.inTabBrowser      = true;
-    this.onMozExtLink      = false;
-
-    if (this.isRemote) {
-      this.selectionInfo = gContextMenuContentData.selectionInfo;
-    } else {
-      this.selectionInfo = BrowserUtils.getSelectionDetails(window);
-    }
-
-    this.textSelected      = this.selectionInfo.text;
-    this.isTextSelected    = this.textSelected.length != 0;
-
-    // Remember the node that was clicked.
-    this.target = aNode;
-
-    // Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
-    // can be null if the menu was triggered by tests in which case use an empty array.
-    this.targetSelectors = gContextMenuContentData
-                              ? gContextMenuContentData.popupNodeSelectors
-                              : [];
-
-    let ownerDoc = this.target.ownerDocument;
-    this.ownerDoc = ownerDoc;
-
-    let editFlags;
-
-    // If this is a remote context menu event, use the information from
-    // gContextMenuContentData instead.
-    if (this.isRemote) {
-      this.browser = gContextMenuContentData.browser;
-      this.principal = gContextMenuContentData.principal;
-      this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID;
-      editFlags = gContextMenuContentData.editFlags;
-    } else {
-      editFlags = SpellCheckHelper.isEditable(this.target, window);
-      this.browser = ownerDoc.defaultView
-                             .QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIWebNavigation)
-                             .QueryInterface(Ci.nsIDocShell)
-                             .chromeEventHandler;
-      this.principal = ownerDoc.nodePrincipal;
-      this.frameOuterWindowID = WebNavigationFrames.getFrameId(ownerDoc.defaultView);
-    }
-    this.webExtBrowserType = this.browser.getAttribute("webextension-view-type");
-    this.inWebExtBrowser = !!this.webExtBrowserType;
-    this.inTabBrowser = this.browser.ownerGlobal.gBrowser ?
-      !!this.browser.ownerGlobal.gBrowser.getTabForBrowser(this.browser) : false;
-
-    // Check if we are in a synthetic document (stand alone image, video, etc.).
-    this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
-
-    this._setTargetForNodesNoChildren(editFlags, aRangeParent, aRangeOffset);
-
-    this._setTargetForNodesWithChildren(editFlags, aRangeParent, aRangeOffset);
-  },
-
-  /**
-   * Sets up the parts of the context menu for when when nodes have no children.
-   *
-   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
-   *                            for the details.
-   * @param {nsIDOMNode} rangeParent The parent node for where the selection ends.
-   * @param {Integer} rangeOffset The end position of where the selction ends.
-   */
-  _setTargetForNodesNoChildren(editFlags, rangeParent, rangeOffset) {
-    if (this.target.nodeType == Node.TEXT_NODE) {
-      // For text nodes, look at the parent node to determine the spellcheck attribute.
-      this.canSpellCheck = this.target.parentNode &&
-                           this._isSpellCheckEnabled(this.target);
-      return;
-    }
-
-    // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
-    // early if we don't have one.
-    if (this.target.nodeType != Node.ELEMENT_NODE) {
-      return;
-    }
-    // See if the user clicked on an image. This check mirrors
-    // nsDocumentViewer::GetInImage. Make sure to update both if this is
-    // changed.
-    if (this.target instanceof Ci.nsIImageLoadingContent &&
-        this.target.currentURI) {
-      this.onImage = true;
-
-      var request =
-        this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
-      if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
-        this.onLoadedImage = true;
-      if (request &&
-          (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
-          !(request.imageStatus & request.STATUS_ERROR)) {
-        this.onCompletedImage = true;
-      }
-
-      this.mediaURL = this.target.currentURI.spec;
-
-      var descURL = this.target.getAttribute("longdesc");
-      if (descURL) {
-        this.imageDescURL = makeURLAbsolute(this.ownerDoc.body.baseURI, descURL);
-      }
-    } else if (this.target instanceof HTMLCanvasElement) {
-      this.onCanvas = true;
-    } else if (this.target instanceof HTMLVideoElement) {
-      let mediaURL = this.target.currentSrc || this.target.src;
-      if (this.isMediaURLReusable(mediaURL)) {
-        this.mediaURL = mediaURL;
-      }
-      if (this._isProprietaryDRM()) {
-        this.onDRMMedia = true;
-      }
-      // Firefox always creates a HTMLVideoElement when loading an ogg file
-      // directly. If the media is actually audio, be smarter and provide a
-      // context menu with audio operations.
-      if (this.target.readyState >= this.target.HAVE_METADATA &&
-          (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
-        this.onAudio = true;
-      } else {
-        this.onVideo = true;
-      }
-    } else if (this.target instanceof HTMLAudioElement) {
-      this.onAudio = true;
-      let mediaURL = this.target.currentSrc || this.target.src;
-      if (this.isMediaURLReusable(mediaURL)) {
-        this.mediaURL = mediaURL;
-      }
-      if (this._isProprietaryDRM()) {
-        this.onDRMMedia = true;
-      }
-    } else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
-      this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
-      this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
-      this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
-      this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
-      if (this.onEditableArea) {
-        if (this.isRemote) {
-          InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
-        } else {
-          InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
-          InlineSpellCheckerUI.initFromEvent(rangeParent, rangeOffset);
-        }
-      }
-      this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
-    } else if (this.target instanceof HTMLHtmlElement) {
-      var bodyElt = this.ownerDoc.body;
-      if (bodyElt) {
-        let computedURL;
-        try {
-          computedURL = this.getComputedURL(bodyElt, "background-image");
-          this._hasMultipleBGImages = false;
-        } catch (e) {
-          this._hasMultipleBGImages = true;
-        }
-        if (computedURL) {
-          this.hasBGImage = true;
-          this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
-                                            computedURL);
-        }
-      }
-    } else if ((this.target instanceof HTMLEmbedElement ||
-              this.target instanceof HTMLObjectElement) &&
-             this.target.displayedType == HTMLObjectElement.TYPE_NULL &&
-             this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
-      this.onCTPPlugin = true;
-    }
-
-    this.canSpellCheck = this._isSpellCheckEnabled(this.target);
-  },
-
-  /**
-   * Sets up the parts of the context menu for when when nodes have children.
-   *
-   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
-   *                            for the details.
-   * @param {nsIDOMNode} rangeParent The parent node for where the selection ends.
-   * @param {Integer} rangeOffset The end position of where the selction ends.
-   */
-  _setTargetForNodesWithChildren(editFlags, rangeParent, rangeOffset) {
-    // Second, bubble out, looking for items of interest that can have childen.
-    // Always pick the innermost link, background image, etc.
-    var elem = this.target;
-    while (elem) {
-      if (elem.nodeType == Node.ELEMENT_NODE) {
-        // Link?
-        const XLINKNS = "http://www.w3.org/1999/xlink";
-        if (!this.onLink &&
-            // Be consistent with what hrefAndLinkNodeForClickEvent
-            // does in browser.js
-             (this._isXULTextLinkLabel(elem) ||
-              (elem instanceof HTMLAnchorElement && elem.href) ||
-              (elem instanceof SVGAElement &&
-               (elem.href || elem.hasAttributeNS(XLINKNS, "href"))) ||
-              (elem instanceof HTMLAreaElement && elem.href) ||
-              elem instanceof HTMLLinkElement ||
-              elem.getAttributeNS(XLINKNS, "type") == "simple")) {
-
-          // Target is a link or a descendant of a link.
-          this.onLink = true;
-
-          // Remember corresponding element.
-          this.link = elem;
-          this.linkURL = this.getLinkURL();
-          this.linkURI = this.getLinkURI();
-          this.linkTextStr = this.getLinkText();
-          this.linkProtocol = this.getLinkProtocol();
-          this.onMailtoLink = (this.linkProtocol == "mailto");
-          this.onMozExtLink = (this.linkProtocol == "moz-extension");
-          this.onSaveableLink = this.isLinkSaveable( this.link );
-          this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
-          try {
-            if (elem.download) {
-              // Ignore download attribute on cross-origin links
-              this.principal.checkMayLoad(this.linkURI, false, true);
-              this.linkDownload = elem.download;
-            }
-          } catch (ex) {}
-        }
-
-        // Background image?  Don't bother if we've already found a
-        // background image further down the hierarchy.  Otherwise,
-        // we look for the computed background-image style.
-        if (!this.hasBGImage &&
-            !this._hasMultipleBGImages) {
-          let bgImgUrl;
-          try {
-            bgImgUrl = this.getComputedURL(elem, "background-image");
-            this._hasMultipleBGImages = false;
-          } catch (e) {
-            this._hasMultipleBGImages = true;
-          }
-          if (bgImgUrl) {
-            this.hasBGImage = true;
-            this.bgImageURL = makeURLAbsolute(elem.baseURI,
-                                              bgImgUrl);
-          }
-        }
-      }
-
-      elem = elem.parentNode;
-    }
-
-    // See if the user clicked on MathML
-    const NS_MathML = "http://www.w3.org/1998/Math/MathML";
-    if ((this.target.nodeType == Node.TEXT_NODE &&
-         this.target.parentNode.namespaceURI == NS_MathML)
-         || (this.target.namespaceURI == NS_MathML))
-      this.onMathML = true;
-
-    // See if the user clicked in a frame.
-    var docDefaultView = this.ownerDoc.defaultView;
-    if (docDefaultView != docDefaultView.top) {
-      this.inFrame = true;
-
-      if (this.ownerDoc.isSrcdocDocument) {
-          this.inSrcdocFrame = true;
-      }
-    }
-
-    // if the document is editable, show context menu like in text inputs
-    if (!this.onEditableArea) {
-      if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
-        // If this.onEditableArea is false but editFlags is CONTENTEDITABLE, then
-        // the document itself must be editable.
-        this.onTextInput       = true;
-        this.onKeywordField    = false;
-        this.onImage           = false;
-        this.onLoadedImage     = false;
-        this.onCompletedImage  = false;
-        this.onMathML          = false;
-        this.inFrame           = false;
-        this.inSrcdocFrame     = false;
-        this.hasBGImage        = false;
-        this.isDesignMode      = true;
-        this.onEditableArea = true;
-        if (this.isRemote) {
-          InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
-        } else {
-          var targetWin = this.ownerDoc.defaultView;
-          var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
-                                        .getInterface(Ci.nsIWebNavigation)
-                                        .QueryInterface(Ci.nsIInterfaceRequestor)
-                                        .getInterface(Ci.nsIEditingSession);
-          InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
-          InlineSpellCheckerUI.initFromEvent(rangeParent, rangeOffset);
-        }
-        var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
-        this.showItem("spell-check-enabled", canSpell);
-        this.showItem("spell-separator", canSpell);
-      }
-    }
-  },
-
-  /**
-   * Determines if a node is a XUL Text link.
-   *
-   * @param {Object} node The object to test.
-   * @returns {Boolean} true if the object is a XUL text link.
-   */
-  _isXULTextLinkLabel(node) {
-    const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-    return node.namespaceURI == xulNS &&
-           node.tagName == "label" &&
-           node.classList.contains("text-link") &&
-           node.href;
-  },
-
-  // Returns the computed style attribute for the given element.
-  getComputedStyle(aElem, aProp) {
-    return aElem.ownerGlobal
-                .getComputedStyle(aElem).getPropertyValue(aProp);
-  },
-
-  // Returns a "url"-type computed style attribute value, with the url() stripped.
-  getComputedURL(aElem, aProp) {
-    var url = aElem.ownerGlobal.getComputedStyle(aElem)
-                   .getPropertyCSSValue(aProp);
-    if (url instanceof CSSValueList) {
-      if (url.length != 1)
-        throw "found multiple URLs";
-      url = url[0];
-    }
-    return url.primitiveType == CSSPrimitiveValue.CSS_URI ?
-           url.getStringValue() : null;
-  },
-
-  // Returns true if clicked-on link targets a resource that can be saved.
-  isLinkSaveable(aLink) {
-    // We don't do the Right Thing for news/snews yet, so turn them off
-    // until we do.
-    return this.linkProtocol && !(
-             this.linkProtocol == "mailto" ||
-             this.linkProtocol == "javascript" ||
-             this.linkProtocol == "news" ||
-             this.linkProtocol == "snews");
-  },
-
-  _isSpellCheckEnabled(aNode) {
-    // We can always force-enable spellchecking on textboxes
-    if (this.isTargetATextBox(aNode)) {
-      return true;
-    }
-    // We can never spell check something which is not content editable
-    var editable = aNode.isContentEditable;
-    if (!editable && aNode.ownerDocument) {
-      editable = aNode.ownerDocument.designMode == "on";
-    }
-    if (!editable) {
-      return false;
-    }
-    // Otherwise make sure that nothing in the parent chain disables spellchecking
-    return aNode.spellcheck;
-  },
-
-  _isProprietaryDRM() {
-    return this.target.isEncrypted && this.target.mediaKeys &&
-           this.target.mediaKeys.keySystem != "org.w3.clearkey";
-  },
-
   _openLinkInParameters(extra) {
     let params = { charset: gContextMenuContentData.charSet,
                    originPrincipal: this.principal,
                    triggeringPrincipal: this.principal,
                    referrerURI: gContextMenuContentData.documentURIObject,
                    referrerPolicy: gContextMenuContentData.referrerPolicy,
                    frameOuterWindowID: gContextMenuContentData.frameOuterWindowID,
                    noReferrer: this.linkHasNoReferrer };
@@ -1189,17 +886,17 @@ nsContextMenu.prototype = {
   },
 
   viewInfo() {
     BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser);
   },
 
   viewImageInfo() {
     BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
-                    this.target, null, this.browser);
+                    this.imageInfo, null, this.browser);
   },
 
   viewImageDesc(e) {
     urlSecurityCheck(this.imageDescURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
                                        referrerURI: gContextMenuContentData.documentURIObject });
@@ -1343,17 +1040,17 @@ nsContextMenu.prototype = {
   // Save URL of clicked-on frame.
   saveFrame() {
     saveBrowser(this.browser, false, this.frameOuterWindowID);
   },
 
   // Helper function to wait for appropriate MIME-type headers and
   // then prompt the user with a file picker
   saveHelper(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
-             windowID, linkDownload) {
+             windowID, linkDownload, isContentWindowPrivate) {
     // canonical def in nsURILoader.h
     const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
 
     // an object to proxy the data through to
     // nsIExternalHelperAppService.doContent, which will wait for the
     // appropriate MIME-type headers and then prompt the user with a
     // file picker
     function saveAsListener() {}
@@ -1402,17 +1099,17 @@ nsContextMenu.prototype = {
       },
 
       onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
                                                        aStatusCode) {
         if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
           // do it the old fashioned way, which will pick the best filename
           // it can without waiting.
           saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI,
-                  doc);
+                  doc, isContentWindowPrivate);
         }
         if (this.extListener)
           this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
       },
 
       onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
                                                            aInputStream,
                                                            aOffset, aCount) {
@@ -1491,31 +1188,35 @@ nsContextMenu.prototype = {
 
     // kick off the channel with our proxy object as the listener
     channel.asyncOpen2(new saveAsListener());
   },
 
   // Save URL of clicked-on link.
   saveLink() {
     urlSecurityCheck(this.linkURL, this.principal);
+
+    let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
     this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
                     gContextMenuContentData.documentURIObject,
                     this.frameOuterWindowID,
-                    this.linkDownload);
+                    this.linkDownload,
+                    isContentWindowPrivate);
   },
 
   // Backwards-compatibility wrapper
   saveImage() {
     if (this.onCanvas || this.onImage)
         this.saveMedia();
   },
 
   // Save URL of the clicked upon image, video, or audio.
   saveMedia() {
     let doc = this.ownerDoc;
+    let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
     let referrerURI = gContextMenuContentData.documentURIObject;
     let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
     if (this.onCanvas) {
       // Bypass cache, since it's a data: URL.
       this._canvasToBlobURL(this.target).then(function(blobURL) {
         saveImageURL(blobURL, "canvas.png", "SaveImageTitle",
                      true, false, referrerURI, null, null, null,
                      isPrivate);
@@ -1524,49 +1225,30 @@ nsContextMenu.prototype = {
       urlSecurityCheck(this.mediaURL, this.principal);
       saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, referrerURI, null, gContextMenuContentData.contentType,
                    gContextMenuContentData.contentDisposition, isPrivate);
     } else if (this.onVideo || this.onAudio) {
       urlSecurityCheck(this.mediaURL, this.principal);
       var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
       this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
-                      this.frameOuterWindowID, "");
+                      this.frameOuterWindowID, "", isContentWindowPrivate);
     }
   },
 
   // Backwards-compatibility wrapper
   sendImage() {
     if (this.onCanvas || this.onImage)
         this.sendMedia();
   },
 
   sendMedia() {
     MailIntegration.sendMessage(this.mediaURL, "");
   },
 
-  castVideo() {
-    CastingApps.openExternal(this.target, window);
-  },
-
-  populateCastVideoMenu(popup) {
-    let videoEl = this.target;
-    popup.innerHTML = null;
-    let doc = popup.ownerDocument;
-    let services = CastingApps.getServicesForVideo(videoEl);
-    services.forEach(service => {
-      let item = doc.createElement("menuitem");
-      item.setAttribute("label", service.friendlyName);
-      item.addEventListener("command", event => {
-        CastingApps.sendVideoToService(videoEl, service);
-      });
-      popup.appendChild(item);
-    });
-  },
-
   playPlugin() {
     gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
   },
 
   hidePlugin() {
     gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
   },
 
@@ -1662,99 +1344,31 @@ nsContextMenu.prototype = {
       var attr = attrs.item(i);
       node.setAttribute(attr.nodeName, attr.nodeValue);
     }
 
     // Voila!
     return node;
   },
 
-  // Generate fully qualified URL for clicked-on link.
-  getLinkURL() {
-    var href = this.link.href;
-    if (href) {
-      // Handle SVG links:
-      if (typeof href == "object" && href.animVal) {
-        return href.animVal;
-      }
-      return href;
-    }
-
-    href = this.link.getAttribute("href") ||
-           this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
-
-    if (!href || !href.match(/\S/)) {
-      // Without this we try to save as the current doc,
-      // for example, HTML case also throws if empty
-      throw "Empty href";
-    }
-
-    return makeURLAbsolute(this.link.baseURI, href);
-  },
-
   getLinkURI() {
     try {
       return makeURI(this.linkURL);
     } catch (ex) {
      // e.g. empty URL string
     }
 
     return null;
   },
 
-  getLinkProtocol() {
-    if (this.linkURI)
-      return this.linkURI.scheme; // can be |undefined|
-
-    return null;
-  },
-
-  // Get text of link.
-  getLinkText() {
-    var text = gatherTextUnder(this.link);
-    if (!text || !text.match(/\S/)) {
-      text = this.link.getAttribute("title");
-      if (!text || !text.match(/\S/)) {
-        text = this.link.getAttribute("alt");
-        if (!text || !text.match(/\S/))
-          text = this.linkURL;
-      }
-    }
-
-    return text;
-  },
-
   // Kept for addon compat
   linkText() {
     return this.linkTextStr;
   },
 
-  isMediaURLReusable(aURL) {
-    if (aURL.startsWith("blob:")) {
-      return URL.isValidURL(aURL);
-    }
-    return true;
-  },
-
-  toString() {
-    return "contextMenu.target     = " + this.target + "\n" +
-           "contextMenu.onImage    = " + this.onImage + "\n" +
-           "contextMenu.onLink     = " + this.onLink + "\n" +
-           "contextMenu.link       = " + this.link + "\n" +
-           "contextMenu.inFrame    = " + this.inFrame + "\n" +
-           "contextMenu.hasBGImage = " + this.hasBGImage + "\n";
-  },
-
-  isTargetATextBox(node) {
-    if (node instanceof HTMLInputElement)
-      return node.mozIsTextField(false);
-
-    return (node instanceof HTMLTextAreaElement);
-  },
-
   // Determines whether or not the separator with the specified ID should be
   // shown or not by determining if there are any non-hidden items between it
   // and the previous separator.
   shouldShowSeparator(aSeparatorID) {
     var separator = document.getElementById(aSeparatorID);
     if (separator) {
       var sibling = separator.previousSibling;
       while (sibling && sibling.localName != "menuseparator") {
--- a/browser/base/content/pageinfo/pageInfo.js
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -343,34 +343,34 @@ function loadPageInfo(frameOuterWindowID
   let mm = browser.messageManager;
 
   gStrings["application/rss+xml"]  = gBundle.getString("feedRss");
   gStrings["application/atom+xml"] = gBundle.getString("feedAtom");
   gStrings["text/xml"]             = gBundle.getString("feedXML");
   gStrings["application/xml"]      = gBundle.getString("feedXML");
   gStrings["application/rdf+xml"]  = gBundle.getString("feedXML");
 
+  let imageInfo = imageElement;
+
   // Look for pageInfoListener in content.js. Sends message to listener with arguments.
-  mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings,
-                      frameOuterWindowID},
-                      { imageElement });
+  mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings, frameOuterWindowID});
 
   let pageInfoData;
 
   // Get initial pageInfoData needed to display the general, feeds, permission and security tabs.
   mm.addMessageListener("PageInfo:data", function onmessage(message) {
     mm.removeMessageListener("PageInfo:data", onmessage);
     pageInfoData = message.data;
     let docInfo = pageInfoData.docInfo;
     let windowInfo = pageInfoData.windowInfo;
     let uri = makeURI(docInfo.documentURIObject.spec);
     let principal = docInfo.principal;
     gDocInfo = docInfo;
 
-    gImageElement = pageInfoData.imageInfo;
+    gImageElement = imageInfo;
 
     var titleFormat = windowInfo.isTopWindow ? "pageInfo.page.title"
                                              : "pageInfo.frame.title";
     document.title = gBundle.getFormattedString(titleFormat, [docInfo.location]);
 
     document.getElementById("main-window").setAttribute("relatedUrl", docInfo.location);
 
     makeGeneralTab(pageInfoData.metaViewRows, docInfo);
--- a/browser/base/content/test/general/browser_addKeywordSearch.js
+++ b/browser/base/content/test/general/browser_addKeywordSearch.js
@@ -24,17 +24,17 @@ add_task(async function() {
   let count = 0;
   for (let method of ["GET", "POST"]) {
     for (let {desc, action, param } of testData) {
       info(`Running ${method} keyword test '${desc}'`);
       let id = `keyword-form-${count++}`;
       let contextMenu = document.getElementById("contentAreaContextMenu");
       let contextMenuPromise =
         BrowserTestUtils.waitForEvent(contextMenu, "popupshown")
-                        .then(() => gContextMenuContentData.popupNode);
+                        .then(() => gContextMenuContentData.target);
 
       await ContentTask.spawn(tab.linkedBrowser,
                               { action, param, method, id }, async function(args) {
         let doc = content.document;
         let form = doc.createElement("form");
         form.id = args.id;
         form.method = args.method;
         form.action = args.action;
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -18,17 +18,22 @@ const chrome_base = "chrome://mochitests
 
 /* import-globals-from contextmenu_common.js */
 Services.scriptloader.loadSubScript(chrome_base + "contextmenu_common.js", this);
 
 // Below are test cases for XUL element
 add_task(async function test_xul_text_link_label() {
   let url = chrome_base + "subtst_contextmenu_xul.xul";
 
-  await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+  await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url,
+    waitForLoad: true,
+    waitForStateStop: true,
+  });
 
   await test_contextmenu("#test-xul-text-link-label",
     ["context-openlinkintab", true,
      ...(hasContainers ? ["context-openlinkinusercontext-menu", true] : []),
      // We need a blank entry here because the containers submenu is
      // dynamically generated with no ids.
      ...(hasContainers ? ["", null] : []),
      "context-openlink",      true,
--- a/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
+++ b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js
@@ -1,20 +1,28 @@
 /* Make sure that "View Image Info" loads the correct image data */
+function getImageInfo(imageElement) {
+  return {
+    currentSrc: imageElement.currentSrc,
+    width: imageElement.width,
+    height: imageElement.height,
+    imageText: imageElement.title || imageElement.alt
+  };
+}
 
 function test() {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
 
   gBrowser.selectedBrowser.addEventListener("load", function() {
     var doc = gBrowser.contentDocument;
     var testImg = doc.getElementById("test-image");
     var pageInfo = BrowserPageInfo(gBrowser.selectedBrowser.currentURI.spec,
-                                   "mediaTab", testImg);
+                                   "mediaTab", getImageInfo(testImg));
 
     pageInfo.addEventListener("load", function() {
       pageInfo.onFinished.push(function() {
         var pageInfoImg = pageInfo.document.getElementById("thepreviewimage");
         pageInfoImg.addEventListener("loadend", function() {
 
           is(pageInfoImg.src, testImg.src, "selected image has the correct source");
           is(pageInfoImg.width, testImg.width, "selected image has the correct width");
new file mode 100644
--- /dev/null
+++ b/browser/modules/ContextMenu.jsm
@@ -0,0 +1,1037 @@
+/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["ContextMenu"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.importGlobalProperties(["URL"]);
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  E10SUtils: "resource:///modules/E10SUtils.jsm",
+  CastingApps: "resource:///modules/CastingApps.jsm",
+  BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+  PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
+  findCssSelector: "resource://gre/modules/css-selector.js",
+  SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
+  LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
+  WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+  InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "PageMenuChild", () => {
+  let tmp = {};
+  Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
+  return new tmp.PageMenuChild();
+});
+
+const messageListeners = {
+  "ContextMenu:BookmarkFrame": function(aMessage) {
+    let frame = this.getTarget(aMessage).ownerDocument;
+
+    this.global.sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
+                                 { title: frame.title,
+                                 description: PlacesUIUtils.getDescriptionFromDocument(frame) });
+  },
+
+  "ContextMenu:Canvas:ToBlobURL": function(aMessage) {
+    this.getTarget(aMessage).toBlob((blob) => {
+      let blobURL = URL.createObjectURL(blob);
+      this.global.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
+    });
+  },
+
+  "ContextMenu:DoCustomCommand": function(aMessage) {
+    E10SUtils.wrapHandlingUserInput(
+      this.content,
+      aMessage.data.handlingUserInput,
+      () => PageMenuChild.executeMenu(aMessage.data.generatedItemId)
+    );
+  },
+
+  "ContextMenu:Hiding": function() {
+    this.context = null;
+    this.target = null;
+  },
+
+  "ContextMenu:MediaCommand": function(aMessage) {
+    E10SUtils.wrapHandlingUserInput(
+      this.content, aMessage.data.handlingUserInput, () => {
+        let media = this.getTarget(aMessage, "element");
+
+        switch (aMessage.data.command) {
+          case "play":
+            media.play();
+            break;
+          case "pause":
+            media.pause();
+            break;
+          case "loop":
+            media.loop = !media.loop;
+            break;
+          case "mute":
+            media.muted = true;
+            break;
+          case "unmute":
+            media.muted = false;
+            break;
+          case "playbackRate":
+            media.playbackRate = aMessage.data.data;
+            break;
+          case "hidecontrols":
+            media.removeAttribute("controls");
+            break;
+          case "showcontrols":
+            media.setAttribute("controls", "true");
+            break;
+          case "fullscreen":
+            if (this.content.document.fullscreenEnabled) {
+              media.requestFullscreen();
+            }
+
+            break;
+        }
+      }
+    );
+  },
+
+  "ContextMenu:ReloadFrame": function(aMessage) {
+    this.getTarget(aMessage).ownerDocument.location.reload();
+  },
+
+  "ContextMenu:ReloadImage": function(aMessage) {
+    let image = this.getTarget(aMessage);
+
+    if (image instanceof Ci.nsIImageLoadingContent) {
+      image.forceReload();
+    }
+  },
+
+  "ContextMenu:SearchFieldBookmarkData": function(aMessage) {
+    let node = this.getTarget(aMessage);
+    let charset = node.ownerDocument.characterSet;
+    let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
+    let formURI = Services.io.newURI(node.form.getAttribute("action"),
+                                     charset, formBaseURI);
+    let spec = formURI.spec;
+    let isURLEncoded =  (node.form.method.toUpperCase() == "POST" &&
+                         (node.form.enctype == "application/x-www-form-urlencoded" ||
+                          node.form.enctype == ""));
+    let title = node.ownerDocument.title;
+    let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
+    let formData = [];
+
+    function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
+      if (aIsFormUrlEncoded) {
+        return escape(aName + "=" + aValue);
+      }
+
+      return escape(aName) + "=" + escape(aValue);
+    }
+
+    for (let el of node.form.elements) {
+      if (!el.type) // happens with fieldsets
+        continue;
+
+      if (el == node) {
+        formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
+                                       // Don't escape "%s", just append
+                                       escapeNameValuePair(el.name, "", false) + "%s");
+        continue;
+      }
+
+      let type = el.type.toLowerCase();
+
+      if (((el instanceof this.content.HTMLInputElement && el.mozIsTextField(true)) ||
+          type == "hidden" || type == "textarea") ||
+          ((type == "checkbox" || type == "radio") && el.checked)) {
+        formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
+      } else if (el instanceof this.content.HTMLSelectElement && el.selectedIndex >= 0) {
+        for (let j = 0; j < el.options.length; j++) {
+          if (el.options[j].selected)
+            formData.push(escapeNameValuePair(el.name, el.options[j].value,
+                                              isURLEncoded));
+        }
+      }
+    }
+
+    let postData;
+
+    if (isURLEncoded) {
+      postData = formData.join("&");
+    } else {
+      let separator = spec.includes("?") ? "&" : "?";
+      spec += separator + formData.join("&");
+    }
+
+    this.global.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
+                                 { spec, title, description, postData, charset });
+  },
+
+  "ContextMenu:SaveVideoFrameAsImage": function(aMessage) {
+    let video = this.getTarget(aMessage);
+    let canvas = this.content.document.createElementNS("http://www.w3.org/1999/xhtml",
+                                                       "canvas");
+    canvas.width = video.videoWidth;
+    canvas.height = video.videoHeight;
+
+    let ctxDraw = canvas.getContext("2d");
+    ctxDraw.drawImage(video, 0, 0);
+
+    this.global.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
+      dataURL: canvas.toDataURL("image/jpeg", ""),
+    });
+  },
+
+  "ContextMenu:SetAsDesktopBackground": function(aMessage) {
+    let target = this.getTarget(aMessage);
+
+    // Paranoia: check disableSetDesktopBackground again, in case the
+    // image changed since the context menu was initiated.
+    let disable = this._disableSetDesktopBackground(target);
+
+    if (!disable) {
+      try {
+        BrowserUtils.urlSecurityCheck(target.currentURI.spec,
+                                      target.ownerDocument.nodePrincipal);
+        let canvas = this.content.document.createElement("canvas");
+        canvas.width = target.naturalWidth;
+        canvas.height = target.naturalHeight;
+        let ctx = canvas.getContext("2d");
+        ctx.drawImage(target, 0, 0);
+        let dataUrl = canvas.toDataURL();
+        let url = (new URL(target.ownerDocument.location.href)).pathname;
+        let imageName = url.substr(url.lastIndexOf("/") + 1);
+        this.global.sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
+                                     { dataUrl, imageName });
+      } catch (e) {
+        Cu.reportError(e);
+        disable = true;
+      }
+    }
+
+    if (disable) {
+      this.global.sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
+                                   { disable });
+    }
+  },
+};
+
+class ContextMenu {
+  // PUBLIC
+  constructor(global) {
+    this.target = null;
+    this.context = null;
+    this.global = global;
+    this.content = global.content;
+
+    Cc["@mozilla.org/eventlistenerservice;1"]
+      .getService(Ci.nsIEventListenerService)
+      .addSystemEventListener(global, "contextmenu",
+                              this._handleContentContextMenu.bind(this), false);
+
+    Object.keys(messageListeners).forEach(key =>
+      global.addMessageListener(key, messageListeners[key].bind(this))
+    );
+  }
+
+  /**
+   * Returns the event target of the context menu, using a locally stored
+   * reference if possible. If not, and aMessage.objects is defined,
+   * aMessage.objects[aKey] is returned. Otherwise null.
+   * @param  {Object} aMessage Message with a objects property
+   * @param  {String} aKey     Key for the target on aMessage.objects
+   * @return {Object}          Context menu target
+   */
+  getTarget(aMessage, aKey = "target") {
+    return this.target || (aMessage.objects && aMessage.objects[aKey]);
+  }
+
+  // PRIVATE
+  _isXULTextLinkLabel(aNode) {
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+    return aNode.namespaceURI == XUL_NS &&
+           aNode.tagName == "label" &&
+           aNode.classList.contains("text-link") &&
+           aNode.href;
+  }
+
+  // Generate fully qualified URL for clicked-on link.
+  _getLinkURL() {
+    let href = this.context.link.href;
+
+    if (href) {
+      // Handle SVG links:
+      if (typeof href == "object" && href.animVal) {
+        return href.animVal;
+      }
+
+      return href;
+    }
+
+    href = this.context.link.getAttribute("href") ||
+           this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+
+    if (!href || !href.match(/\S/)) {
+      // Without this we try to save as the current doc,
+      // for example, HTML case also throws if empty
+      throw "Empty href";
+    }
+
+    return this._makeURLAbsolute(this.context.link.baseURI, href);
+  }
+
+  _getLinkURI() {
+    try {
+      return Services.io.newURI(this.context.linkURL);
+    } catch (ex) {
+     // e.g. empty URL string
+    }
+
+    return null;
+  }
+
+  // Get text of link.
+  _getLinkText() {
+    let text = this._gatherTextUnder(this.context.link);
+
+    if (!text || !text.match(/\S/)) {
+      text = this.context.link.getAttribute("title");
+      if (!text || !text.match(/\S/)) {
+        text = this.context.link.getAttribute("alt");
+        if (!text || !text.match(/\S/)) {
+          text = this.context.linkURL;
+        }
+      }
+    }
+
+    return text;
+  }
+
+  _getLinkProtocol() {
+    if (this.context.linkURI) {
+      return this.context.linkURI.scheme; // can be |undefined|
+    }
+
+    return null;
+  }
+
+  // Returns true if clicked-on link targets a resource that can be saved.
+  _isLinkSaveable(aLink) {
+    // We don't do the Right Thing for news/snews yet, so turn them off
+    // until we do.
+    return this.context.linkProtocol && !(
+           this.context.linkProtocol == "mailto" ||
+           this.context.linkProtocol == "javascript" ||
+           this.context.linkProtocol == "news" ||
+           this.context.linkProtocol == "snews");
+  }
+
+  // Gather all descendent text under given document node.
+  _gatherTextUnder(root) {
+    let text = "";
+    let node = root.firstChild;
+    let depth = 1;
+    while (node && depth > 0) {
+      // See if this node is text.
+      if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+        // Add this text to our collection.
+        text += " " + node.data;
+      } else if (node instanceof this.content.HTMLImageElement) {
+        // If it has an "alt" attribute, add that.
+        let altText = node.getAttribute( "alt" );
+        if ( altText && altText != "" ) {
+          text += " " + altText;
+        }
+      }
+      // Find next node to test.
+      // First, see if this node has children.
+      if (node.hasChildNodes()) {
+        // Go to first child.
+        node = node.firstChild;
+        depth++;
+      } else {
+        // No children, try next sibling (or parent next sibling).
+        while (depth > 0 && !node.nextSibling) {
+          node = node.parentNode;
+          depth--;
+        }
+        if (node.nextSibling) {
+          node = node.nextSibling;
+        }
+      }
+    }
+
+    // Strip leading and tailing whitespace.
+    text = text.trim();
+    // Compress remaining whitespace.
+    text = text.replace(/\s+/g, " ");
+    return text;
+  }
+
+  // Returns a "url"-type computed style attribute value, with the url() stripped.
+  _getComputedURL(aElem, aProp) {
+    let url = aElem.ownerGlobal.getComputedStyle(aElem).getPropertyCSSValue(aProp);
+
+    if (url instanceof this.content.CSSValueList) {
+      if (url.length != 1) {
+        throw "found multiple URLs";
+      }
+
+      url = url[0];
+    }
+
+    return url.primitiveType == this.content.CSSPrimitiveValue.CSS_URI ?
+           url.getStringValue() : null;
+  }
+
+  _makeURLAbsolute(aBase, aUrl) {
+    return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
+  }
+
+  _isProprietaryDRM() {
+    return this.context.target.isEncrypted && this.context.target.mediaKeys &&
+           this.context.target.mediaKeys.keySystem != "org.w3.clearkey";
+  }
+
+  _isMediaURLReusable(aURL) {
+    if (aURL.startsWith("blob:")) {
+      return URL.isValidURL(aURL);
+    }
+
+    return true;
+  }
+
+  _isTargetATextBox(node) {
+    if (node instanceof this.content.HTMLInputElement) {
+      return node.mozIsTextField(false);
+    }
+
+    return (node instanceof this.content.HTMLTextAreaElement);
+  }
+
+  _isSpellCheckEnabled(aNode) {
+    // We can always force-enable spellchecking on textboxes
+    if (this._isTargetATextBox(aNode)) {
+      return true;
+    }
+
+    // We can never spell check something which is not content editable
+    let editable = aNode.isContentEditable;
+
+    if (!editable && aNode.ownerDocument) {
+      editable = aNode.ownerDocument.designMode == "on";
+    }
+
+    if (!editable) {
+      return false;
+    }
+
+    // Otherwise make sure that nothing in the parent chain disables spellchecking
+    return aNode.spellcheck;
+  }
+
+  _disableSetDesktopBackground(aTarget) {
+    // Disable the Set as Desktop Background menu item if we're still trying
+    // to load the image or the load failed.
+    if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
+      return true;
+    }
+
+    if (("complete" in aTarget) && !aTarget.complete) {
+      return true;
+    }
+
+    if (aTarget.currentURI.schemeIs("javascript")) {
+      return true;
+    }
+
+    let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
+                         .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+
+    if (!request) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Retrieve the array of CSS selectors corresponding to the provided node. The first item
+   * of the array is the selector of the node in its owner document. Additional items are
+   * used if the node is inside a frame, each representing the CSS selector for finding the
+   * frame element in its parent document.
+   *
+   * This format is expected by DevTools in order to handle the Inspect Node context menu
+   * item.
+   *
+   * @param  {aNode}
+   *         The node for which the CSS selectors should be computed
+   * @return {Array} array of css selectors (strings).
+   */
+  _getNodeSelectors(aNode) {
+    let selectors = [];
+    while (aNode) {
+      selectors.push(findCssSelector(aNode));
+      aNode = aNode.ownerGlobal.frameElement;
+    }
+
+    return selectors;
+  }
+
+  _handleContentContextMenu(aEvent) {
+    let defaultPrevented = aEvent.defaultPrevented;
+
+    if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
+      let plugin = null;
+
+      try {
+        plugin = aEvent.target.QueryInterface(Ci.nsIObjectLoadingContent);
+      } catch (e) {}
+
+      if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
+        // Don't open a context menu for plugins.
+        return;
+      }
+
+      defaultPrevented = false;
+    }
+
+    if (defaultPrevented) {
+      return;
+    }
+
+    let addonInfo = Object.create(null);
+    let subject = {
+      aEvent,
+      addonInfo,
+    };
+
+    subject.wrappedJSObject = subject;
+    Services.obs.notifyObservers(subject, "content-contextmenu");
+
+    let doc = aEvent.target.ownerDocument;
+    let {
+      mozDocumentURIIfNotForErrorPages: docLocation,
+      characterSet: charSet,
+      baseURI,
+      referrer,
+      referrerPolicy
+    } = doc;
+    docLocation = docLocation && docLocation.spec;
+    let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
+    let loginFillInfo = LoginManagerContent.getFieldContext(aEvent.target);
+
+    // The same-origin check will be done in nsContextMenu.openLinkInTab.
+    let parentAllowsMixedContent = !!this.global.docShell.mixedContentChannel;
+
+    // Get referrer attribute from clicked link and parse it
+    let referrerAttrValue = Services.netUtils.parseAttributePolicyString(aEvent.target.
+                            getAttribute("referrerpolicy"));
+
+    if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
+      referrerPolicy = referrerAttrValue;
+    }
+
+    let disableSetDesktopBg = null;
+
+    // Media related cache info parent needs for saving
+    let contentType = null;
+    let contentDisposition = null
+    if (aEvent.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+        aEvent.target instanceof Ci.nsIImageLoadingContent &&
+        aEvent.target.currentURI) {
+      disableSetDesktopBg = this._disableSetDesktopBackground(aEvent.target);
+
+      try {
+        let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+                                                         .getImgCacheForDocument(doc);
+        let props = imageCache.findEntryProperties(aEvent.target.currentURI, doc);
+
+        try {
+          contentType = props.get("type", Ci.nsISupportsCString).data;
+        } catch (e) {}
+
+        try {
+          contentDisposition = props.get("content-disposition", Ci.nsISupportsCString).data;
+        } catch (e) {}
+      } catch (e) {}
+    }
+
+    let selectionInfo = BrowserUtils.getSelectionDetails(this.content);
+    let loadContext = this.global.docShell.QueryInterface(Ci.nsILoadContext);
+    let userContextId = loadContext.originAttributes.userContextId;
+    let popupNodeSelectors = this._getNodeSelectors(aEvent.target);
+
+    this._setContext(aEvent);
+    let context = this.context;
+    this.target = context.target;
+
+    let spellInfo = null;
+    let editFlags = null;
+    let principal = null;
+    let customMenuItems = null;
+
+    if (context.target) {
+      this._cleanContext();
+    }
+
+    let isRemote = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+    if (isRemote) {
+      editFlags = SpellCheckHelper.isEditable(aEvent.target, this.content);
+
+      if (editFlags &
+          (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
+        spellInfo = InlineSpellCheckerContent.initContextMenu(aEvent, editFlags, this.global);
+      }
+
+      // Set the event target first as the copy image command needs it to
+      // determine what was context-clicked on. Then, update the state of the
+      // commands on the context menu.
+      this.global.docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
+                          .setCommandNode(aEvent.target);
+      aEvent.target.ownerGlobal.updateCommands("contentcontextmenu");
+
+      customMenuItems = PageMenuChild.build(aEvent.target);
+      principal = doc.nodePrincipal;
+    }
+
+    let data = {
+      context,
+      charSet,
+      baseURI,
+      isRemote,
+      referrer,
+      addonInfo,
+      editFlags,
+      principal,
+      spellInfo,
+      contentType,
+      docLocation,
+      loginFillInfo,
+      selectionInfo,
+      userContextId,
+      referrerPolicy,
+      customMenuItems,
+      contentDisposition,
+      frameOuterWindowID,
+      popupNodeSelectors,
+      disableSetDesktopBg,
+      parentAllowsMixedContent,
+    };
+
+    if (isRemote) {
+      this.global.sendAsyncMessage("contextmenu", data);
+    } else {
+      let browser = this.global.docShell.chromeEventHandler;
+      let mainWin = browser.ownerGlobal;
+
+      data.documentURIObject = doc.documentURIObject;
+      data.disableSetDesktopBackground = data.disableSetDesktopBg;
+      delete data.disableSetDesktopBg;
+
+      mainWin.setContextMenuContentData(data);
+    }
+  }
+
+  /**
+   * Some things are not serializable, so we either have to only send
+   * their needed data or regenerate them in nsContextMenu.js
+   * - target and target.ownerDocument
+   * - link
+   * - linkURI
+   */
+  _cleanContext(aEvent) {
+    const context = this.context;
+    const cleanTarget = Object.create(null);
+
+    cleanTarget.ownerDocument = {
+      // used for nsContextMenu.initLeaveDOMFullScreenItems and
+      // nsContextMenu.initMediaPlayerItems
+      fullscreenElement: context.target.ownerDocument.fullscreenElement,
+
+      // used for nsContextMenu.initMiscItems
+      contentType: context.target.ownerDocument.contentType,
+
+      // used for nsContextMenu.saveLink
+      isPrivate: context.target.ownerDocument.isPrivate,
+    };
+
+    // used for nsContextMenu.initMediaPlayerItems
+    Object.assign(cleanTarget, {
+      ended: context.target.ended,
+      muted: context.target.muted,
+      paused: context.target.paused,
+      controls: context.target.controls,
+      duration: context.target.duration,
+    });
+
+    const onMedia = context.onVideo || context.onAudio;
+
+    if (onMedia) {
+      Object.assign(cleanTarget, {
+        loop: context.target.loop,
+        error: context.target.error,
+        networkState: context.target.networkState,
+        playbackRate: context.target.playbackRate,
+        NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
+      });
+
+      if (context.onVideo) {
+        Object.assign(cleanTarget, {
+          readyState: context.target.readyState,
+          HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
+        });
+      }
+    }
+
+    context.target = cleanTarget;
+
+    if (context.link) {
+      context.link = { href: context.link.href };
+    }
+
+    delete context.linkURI;
+  }
+
+  _setContext(aEvent) {
+    this.context = Object.create(null);
+    const context = this.context;
+
+    context.screenX = aEvent.screenX;
+    context.screenY = aEvent.screenY;
+    context.mozInputSource = aEvent.mozInputSource;
+
+    const node = aEvent.target;
+
+    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+    context.shouldDisplay = true;
+
+    if (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ||
+        // Don't display for XUL element unless <label class="text-link">
+        (node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))) {
+      context.shouldDisplay = false;
+      return;
+    }
+
+    // Initialize context to be sent to nsContextMenu
+    // Keep this consistent with the similar code in nsContextMenu's setContext
+    context.bgImageURL          = "";
+    context.imageDescURL        = "";
+    context.imageInfo           = null;
+    context.mediaURL            = "";
+    context.webExtBrowserType   = "";
+
+    context.canSpellCheck       = false;
+    context.hasBGImage          = false;
+    context.hasMultipleBGImages = false;
+    context.isDesignMode        = false;
+    context.inFrame             = false;
+    context.inSrcdocFrame       = false;
+    context.inSyntheticDoc      = false;
+    context.inTabBrowser        = true;
+    context.inWebExtBrowser     = false;
+
+    context.link                = null;
+    context.linkDownload        = "";
+    context.linkHasNoReferrer   = false;
+    context.linkProtocol        = "";
+    context.linkTextStr         = "";
+    context.linkURL             = "";
+    context.linkURI             = null;
+
+    context.onAudio             = false;
+    context.onCanvas            = false;
+    context.onCompletedImage    = false;
+    context.onCTPPlugin         = false;
+    context.onDRMMedia          = false;
+    context.onEditableArea      = false;
+    context.onImage             = false;
+    context.onKeywordField      = false;
+    context.onLink              = false;
+    context.onLoadedImage       = false;
+    context.onMailtoLink        = false;
+    context.onMathML            = false;
+    context.onMozExtLink        = false;
+    context.onNumeric           = false;
+    context.onPassword          = false;
+    context.onSaveableLink      = false;
+    context.onTextInput         = false;
+    context.onVideo             = false;
+
+    // Remember the node and its owner document that was clicked
+    // This may be modifed before sending to nsContextMenu
+    context.target = node;
+
+    context.principal = context.target.ownerDocument.nodePrincipal;
+    context.frameOuterWindowID = WebNavigationFrames.getFrameId(context.target.ownerGlobal);
+
+    // Check if we are in a synthetic document (stand alone image, video, etc.).
+    context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
+
+    context.shouldInitInlineSpellCheckerUINoChildren = false;
+    context.shouldInitInlineSpellCheckerUIWithChildren = false;
+
+    let editFlags = SpellCheckHelper.isEditable(context.target, this.content);
+    this._setContextForNodesNoChildren(editFlags);
+    this._setContextForNodesWithChildren(editFlags);
+  }
+
+  /**
+   * Sets up the parts of the context menu for when when nodes have no children.
+   *
+   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
+   *                            for the details.
+   */
+  _setContextForNodesNoChildren(editFlags) {
+    const context = this.context;
+
+    if (context.target.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
+      // For text nodes, look at the parent node to determine the spellcheck attribute.
+      context.canSpellCheck = context.target.parentNode &&
+                              this._isSpellCheckEnabled(context.target);
+      return;
+    }
+
+    // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
+    // early if we don't have one.
+    if (context.target.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
+      return;
+    }
+
+    // See if the user clicked on an image. This check mirrors
+    // nsDocumentViewer::GetInImage. Make sure to update both if this is
+    // changed.
+    if (context.target instanceof Ci.nsIImageLoadingContent &&
+        context.target.currentURI) {
+      context.onImage = true;
+
+      context.imageInfo = {
+        currentSrc: context.target.currentSrc,
+        width: context.target.width,
+        height: context.target.height,
+        imageText: context.target.title || context.target.alt
+      };
+
+      const request = context.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+
+      if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) {
+        context.onLoadedImage = true;
+      }
+
+      if (request &&
+          (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
+          !(request.imageStatus & request.STATUS_ERROR)) {
+        context.onCompletedImage = true;
+      }
+
+      context.mediaURL = context.target.currentURI.spec;
+
+      const descURL = context.target.getAttribute("longdesc");
+
+      if (descURL) {
+        context.imageDescURL = this._makeURLAbsolute(context.target.ownerDocument.body.baseURI,
+                                                    descURL);
+      }
+    } else if (context.target instanceof this.content.HTMLCanvasElement) {
+      context.onCanvas = true;
+    } else if (context.target instanceof this.content.HTMLVideoElement) {
+      const mediaURL = context.target.currentSrc || context.target.src;
+
+      if (this._isMediaURLReusable(mediaURL)) {
+        context.mediaURL = mediaURL;
+      }
+
+      if (this._isProprietaryDRM()) {
+        context.onDRMMedia = true;
+      }
+
+      // Firefox always creates a HTMLVideoElement when loading an ogg file
+      // directly. If the media is actually audio, be smarter and provide a
+      // context menu with audio operations.
+      if (context.target.readyState >= context.target.HAVE_METADATA &&
+          (context.target.videoWidth == 0 || context.target.videoHeight == 0)) {
+        context.onAudio = true;
+      } else {
+        context.onVideo = true;
+      }
+    } else if (context.target instanceof this.content.HTMLAudioElement) {
+      context.onAudio = true;
+      const mediaURL = context.target.currentSrc || context.target.src;
+
+      if (this._isMediaURLReusable(mediaURL)) {
+        context.mediaURL = mediaURL;
+      }
+
+      if (this._isProprietaryDRM()) {
+        context.onDRMMedia = true;
+      }
+    } else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
+      context.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
+      context.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
+      context.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
+      context.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
+
+      if (context.onEditableArea) {
+        context.shouldInitInlineSpellCheckerUINoChildren = true;
+      }
+
+      context.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
+    } else if (context.target instanceof this.content.HTMLHtmlElement) {
+      const bodyElt = context.target.ownerDocument.body;
+
+      if (bodyElt) {
+        let computedURL;
+
+        try {
+          computedURL = this._getComputedURL(bodyElt, "background-image");
+          context.hasMultipleBGImages = false;
+        } catch (e) {
+          context.hasMultipleBGImages = true;
+        }
+
+        if (computedURL) {
+          context.hasBGImage = true;
+          context.bgImageURL = this._makeURLAbsolute(bodyElt.baseURI,
+                                                    computedURL);
+        }
+      }
+    } else if ((context.target instanceof this.content.HTMLEmbedElement ||
+               context.target instanceof this.content.HTMLObjectElement) &&
+               context.target.displayedType == this.content.HTMLObjectElement.TYPE_NULL &&
+               context.target.pluginFallbackType == this.content.HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
+      context.onCTPPlugin = true;
+    }
+
+    context.canSpellCheck = this._isSpellCheckEnabled(context.target);
+  }
+
+  /**
+   * Sets up the parts of the context menu for when when nodes have children.
+   *
+   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
+   *                            for the details.
+   */
+  _setContextForNodesWithChildren(editFlags) {
+    const context = this.context;
+
+    // Second, bubble out, looking for items of interest that can have childen.
+    // Always pick the innermost link, background image, etc.
+    let elem = context.target;
+
+    while (elem) {
+      if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+        // Link?
+        const XLINK_NS = "http://www.w3.org/1999/xlink";
+
+        if (!context.onLink &&
+            // Be consistent with what hrefAndLinkNodeForClickEvent
+            // does in browser.js
+            (this._isXULTextLinkLabel(elem) ||
+            (elem instanceof this.content.HTMLAnchorElement && elem.href) ||
+            (elem instanceof this.content.SVGAElement &&
+            (elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
+            (elem instanceof this.content.HTMLAreaElement && elem.href) ||
+            elem instanceof this.content.HTMLLinkElement ||
+            elem.getAttributeNS(XLINK_NS, "type") == "simple")) {
+
+          // Target is a link or a descendant of a link.
+          context.onLink = true;
+
+          // Remember corresponding element.
+          context.link = elem;
+          context.linkURL = this._getLinkURL();
+          context.linkURI = this._getLinkURI();
+          context.linkTextStr = this._getLinkText();
+          context.linkProtocol = this._getLinkProtocol();
+          context.onMailtoLink = (context.linkProtocol == "mailto");
+          context.onMozExtLink = (context.linkProtocol == "moz-extension");
+          context.onSaveableLink = this._isLinkSaveable(context.link);
+          context.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
+
+          try {
+            if (elem.download) {
+              // Ignore download attribute on cross-origin links
+              context.principal.checkMayLoad(context.linkURI, false, true);
+              context.linkDownload = elem.download;
+            }
+          } catch (ex) {}
+        }
+
+        // Background image?  Don't bother if we've already found a
+        // background image further down the hierarchy.  Otherwise,
+        // we look for the computed background-image style.
+        if (!context.hasBGImage &&
+            !context.hasMultipleBGImages) {
+          let bgImgUrl = null;
+
+          try {
+            bgImgUrl = this._getComputedURL(elem, "background-image");
+            context.hasMultipleBGImages = false;
+          } catch (e) {
+            context.hasMultipleBGImages = true;
+          }
+
+          if (bgImgUrl) {
+            context.hasBGImage = true;
+            context.bgImageURL = this._makeURLAbsolute(elem.baseURI,
+                                                      bgImgUrl);
+          }
+        }
+      }
+
+      elem = elem.parentNode;
+    }
+
+    // See if the user clicked on MathML
+    const MathML_NS = "http://www.w3.org/1998/Math/MathML";
+
+    if ((context.target.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
+         context.target.parentNode.namespaceURI == MathML_NS) ||
+         (context.target.namespaceURI == MathML_NS)) {
+      context.onMathML = true;
+    }
+
+    // See if the user clicked in a frame.
+    const docDefaultView = context.target.ownerGlobal;
+
+    if (docDefaultView != docDefaultView.top) {
+      context.inFrame = true;
+
+      if (context.target.ownerDocument.isSrcdocDocument) {
+          context.inSrcdocFrame = true;
+      }
+    }
+
+    // if the document is editable, show context menu like in text inputs
+    if (!context.onEditableArea) {
+      if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
+        // If this._onEditableArea is false but editFlags is CONTENTEDITABLE, then
+        // the document itself must be editable.
+        context.onTextInput       = true;
+        context.onKeywordField    = false;
+        context.onImage           = false;
+        context.onLoadedImage     = false;
+        context.onCompletedImage  = false;
+        context.onMathML          = false;
+        context.inFrame           = false;
+        context.inSrcdocFrame     = false;
+        context.hasBGImage        = false;
+        context.isDesignMode      = true;
+        context.onEditableArea    = true;
+        context.shouldInitInlineSpellCheckerUIWithChildren = true;
+      }
+    }
+  }
+}
--- a/browser/modules/PluginContent.jsm
+++ b/browser/modules/PluginContent.jsm
@@ -20,17 +20,17 @@ XPCOMUtils.defineLazyGetter(this, "gNavi
   return Services.strings.createBundle(url);
 });
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 
 this.PluginContent = function(global) {
   this.init(global);
-}
+};
 
 const FLASH_MIME_TYPE = "application/x-shockwave-flash";
 const REPLACEMENT_STYLE_SHEET = Services.io.newURI("chrome://pluginproblem/content/pluginReplaceBinding.css");
 
 PluginContent.prototype = {
   init(global) {
     this.global = global;
     // Need to hold onto the content window or else it'll get destroyed
@@ -93,22 +93,24 @@ PluginContent.prototype = {
     switch (msg.name) {
       case "BrowserPlugins:ActivatePlugins":
         this.activatePlugins(msg.data.pluginInfo, msg.data.newState);
         break;
       case "BrowserPlugins:NotificationShown":
         setTimeout(() => this.updateNotificationUI(), 0);
         break;
       case "BrowserPlugins:ContextMenuCommand":
+        let contextMenu = this.global.contextMenu;
+
         switch (msg.data.command) {
           case "play":
-            this._showClickToPlayNotification(msg.objects.plugin, true);
+            this._showClickToPlayNotification(contextMenu.getTarget(msg, "plugin"), true);
             break;
           case "hide":
-            this.hideClickToPlayOverlay(msg.objects.plugin);
+            this.hideClickToPlayOverlay(contextMenu.getTarget(msg, "plugin"));
             break;
         }
         break;
       case "BrowserPlugins:NPAPIPluginProcessCrashed":
         this.NPAPIPluginProcessCrashed({
           pluginName: msg.data.pluginName,
           runID: msg.data.runID,
           state: msg.data.state,
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -130,16 +130,17 @@ EXTRA_JS_MODULES += [
     'BrowserUsageTelemetry.jsm',
     'CastingApps.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentObservers.js',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',
+    'ContextMenu.jsm',
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
     'ExtensionsUI.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'LaterRun.jsm',
     'offlineAppCache.jsm',