/* -*- tab-width: 2; 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/. */

ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");

XPCOMUtils.defineLazyModuleGetters(this, {
  SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
  LoginManagerContextMenu: "resource://gre/modules/LoginManagerContextMenu.jsm",
  WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
  ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
  NetUtil: "resource://gre/modules/NetUtil.jsm",
});

var gContextMenuContentData = null;

function setContextMenuContentData(data) {
  gContextMenuContentData = data;
}

function openContextMenu(aMessage) {
  let data = aMessage.data;
  let browser = aMessage.target;
  let spellInfo = data.spellInfo;

  // ContextMenu.jsm sends us the target as a CPOW only so that
  // we can send that CPOW back down to the content process and
  // have it resolve to a DOM node. The parent should not attempt
  // to access any properties on this CPOW (in fact, doing so
  // will throw an exception).
  data.context.targetAsCPOW = aMessage.objects.targetAsCPOW;

  if (spellInfo) {
    spellInfo.target = aMessage.target.messageManager;
  }

  let documentURIObject = makeURI(data.docLocation,
                                  data.charSet,
                                  makeURI(data.baseURI));
  gContextMenuContentData = { context: data.context,
                              isRemote: data.isRemote,
                              popupNodeSelectors: data.popupNodeSelectors,
                              browser,
                              editFlags: data.editFlags,
                              spellInfo,
                              principal: data.principal,
                              customMenuItems: data.customMenuItems,
                              documentURIObject,
                              docLocation: data.docLocation,
                              charSet: data.charSet,
                              referrer: data.referrer,
                              referrerPolicy: data.referrerPolicy,
                              contentType: data.contentType,
                              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 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, 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.setContext();

    if (!this.shouldDisplay)
      return;

    this.hasPageMenu = false;
    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
    if (!aIsShift) {
      if (this.isRemote) {
        this.hasPageMenu =
          PageMenuParent.addToPopup(gContextMenuContentData.customMenuItems,
                                    this.browser, aXulMenu);
      } else {
        this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
      }

      let subject = {
        menu: aXulMenu,
        tab: gBrowser ? gBrowser.getTabForBrowser(this.browser) : undefined,
        isContentSelected: this.isContentSelected,
        inFrame: this.inFrame,
        isTextSelected: this.isTextSelected,
        onTextInput: this.onTextInput,
        onLink: this.onLink,
        onImage: this.onImage,
        onVideo: this.onVideo,
        onAudio: this.onAudio,
        onCanvas: this.onCanvas,
        onEditable: this.onEditable,
        onSpellcheckable: this.onSpellcheckable,
        onPassword: this.onPassword,
        srcUrl: this.mediaURL,
        frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined,
        pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
        linkText: this.linkTextStr,
        linkUrl: this.linkURL,
        selectionText: this.isTextSelected ? this.selectionInfo.fullText : undefined,
        frameId: this.frameOuterWindowID,
      };
      subject.wrappedJSObject = subject;
      Services.obs.notifyObservers(subject, "on-build-contextmenu");
    }

    this.isFrameImage = document.getElementById("isFrameImage");
    this.ellipsis = "\u2026";
    try {
      this.ellipsis = Services.prefs.getComplexValue("intl.ellipsis",
                                                     Ci.nsIPrefLocalizedString).data;
    } catch (e) { }

    // Reset after "on-build-contextmenu" notification in case selection was
    // changed during the notification.
    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
    this.onPlainTextLink = false;

    let bookmarkPage = document.getElementById("context-bookmarkpage");
    if (bookmarkPage)
      BookmarkingUI.onCurrentPageContextPopupShowing();

    // Initialize (disable/remove) menu items.
    this.initItems();
  },

  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.onEditable          = context.onEditable;
    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.onSpellcheckable    = context.onSpellcheckable;
    this.onTextInput         = context.onTextInput;
    this.onVideo             = context.onVideo;

    this.target = this.isRemote ? context.target : document.popupNode;
    this.targetAsCPOW = context.targetAsCPOW;

    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.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);
    }

    // This handler self-deletes, only run it if it is still there:
    if (this._onPopupHiding) {
      this._onPopupHiding();
    }
  },

  initItems: function CM_initItems() {
    this.initPageMenuSeparator();
    this.initOpenItems();
    this.initNavigationItems();
    this.initViewItems();
    this.initMiscItems();
    this.initSpellingItems();
    this.initSaveItems();
    this.initClipboardItems();
    this.initMediaPlayerItems();
    this.initLeaveDOMFullScreenItems();
    this.initClickToPlayItems();
    this.initPasswordManagerItems();
    this.initSyncItems();
  },

  initPageMenuSeparator: function CM_initPageMenuSeparator() {
    this.showItem("page-menu-separator", this.hasPageMenu);
  },

  initOpenItems: function CM_initOpenItems() {
    var isMailtoInternal = false;
    if (this.onMailtoLink) {
      var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
                          getService(Ci.nsIExternalProtocolService).
                          getProtocolHandlerInfo("mailto");
      isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling &&
                          mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
                          (mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp));
    }

    if (this.isTextSelected && !this.onLink &&
        this.selectionInfo && this.selectionInfo.linkURL) {
      this.linkURL = this.selectionInfo.linkURL;
      try {
        this.linkURI = makeURI(this.linkURL);
      } catch (ex) {}

      this.linkTextStr = this.selectionInfo.linkText;
      this.onPlainTextLink = true;
    }

    var inContainer = false;
    if (gContextMenuContentData.userContextId) {
      inContainer = true;
      var item = document.getElementById("context-openlinkincontainertab");

      item.setAttribute("data-usercontextid", gContextMenuContentData.userContextId);

      var label =
        ContextualIdentityService.getUserContextLabel(gContextMenuContentData.userContextId);
      item.setAttribute("label",
         gBrowserBundle.formatStringFromName("userContextOpenLink.label",
                                             [label], 1));
    }

    var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
    var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
    var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
    this.showItem("context-openlink", shouldShow && !isWindowPrivate);
    this.showItem("context-openlinkprivate", shouldShow && PrivateBrowsingUtils.enabled);
    this.showItem("context-openlinkintab", shouldShow && !inContainer);
    this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
    this.showItem("context-openlinkinusercontext-menu", shouldShow && !isWindowPrivate && showContainers);
    this.showItem("context-openlinkincurrent", this.onPlainTextLink);
    this.showItem("context-sep-open", shouldShow);
  },

  initNavigationItems: function CM_initNavigationItems() {
    var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
                       this.onCanvas || this.onVideo || this.onAudio ||
                       this.onTextInput) && this.inTabBrowser;
    this.showItem("context-navigation", shouldShow);
    this.showItem("context-sep-navigation", shouldShow);

    let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";

    let stopReloadItem = "";
    if (shouldShow || !this.inTabBrowser) {
      stopReloadItem = (stopped || !this.inTabBrowser) ? "reload" : "stop";
    }

    this.showItem("context-reload", stopReloadItem == "reload");
    this.showItem("context-stop", stopReloadItem == "stop");

    // XXX: Stop is determined in browser.js; the canStop broadcaster is broken
    // this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
  },

  initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() {
    // only show the option if the user is in DOM fullscreen
    var shouldShow = this.target.ownerDocument.fullscreen;
    this.showItem("context-leave-dom-fullscreen", shouldShow);

    // Explicitly show if in DOM fullscreen, but do not hide it has already been shown
    if (shouldShow)
        this.showItem("context-media-sep-commands", true);
  },

  initSaveItems: function CM_initSaveItems() {
    var shouldShow = !(this.onTextInput || this.onLink ||
                       this.isContentSelected || this.onImage ||
                       this.onCanvas || this.onVideo || this.onAudio);
    this.showItem("context-savepage", shouldShow);

    // Save link depends on whether we're in a link, or selected text matches valid URL pattern.
    this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink);

    // Save image depends on having loaded its content, video and audio don't.
    this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
    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-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);
  },

  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);

    var shouldShow = !(this.isContentSelected ||
                       this.onImage || this.onCanvas ||
                       this.onVideo || this.onAudio ||
                       this.onLink || this.onTextInput);

    var showInspect = this.inTabBrowser &&
                      Services.prefs.getBoolPref("devtools.inspector.enabled", true) &&
                      !Services.prefs.getBoolPref("devtools.policy.disabled", false);

    var showInspectA11Y = showInspect &&
                          // Only when accessibility service started.
                          Services.appinfo.accessibilityEnabled &&
                          this.inTabBrowser &&
                          Services.prefs.getBoolPref("devtools.enabled", true) &&
                          Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
                          !Services.prefs.getBoolPref("devtools.policy.disabled", false);

    this.showItem("context-viewsource", shouldShow);
    this.showItem("context-viewinfo", shouldShow);
    // The page info is broken for WebExtension popups, as the browser is
    // destroyed when the popup is closed.
    this.setItemAttr("context-viewinfo", "disabled", this.webExtBrowserType === "popup");
    this.showItem("inspect-separator", showInspect);
    this.showItem("context-inspect", showInspect);

    this.showItem("context-inspect-a11y", showInspectA11Y);

    this.showItem("context-sep-viewsource", shouldShow);

    // Set as Desktop background depends on whether an image was clicked on,
    // and only works if we have a shell service.
    var haveSetDesktopBackground = false;

    if (AppConstants.HAVE_SHELL_SERVICE &&
        Services.policies.isAllowed("setDesktopBackground")) {
      // Only enable Set as Desktop Background if we can get the shell service.
      var shell = getShellService();
      if (shell)
        haveSetDesktopBackground = shell.canSetDesktopBackground;
    }

    this.showItem("context-setDesktopBackground",
                  haveSetDesktopBackground && this.onLoadedImage);

    if (haveSetDesktopBackground && this.onLoadedImage) {
      document.getElementById("context-setDesktopBackground")
              .disabled = gContextMenuContentData.disableSetDesktopBackground;
    }

    // Reload image depends on an image that's not fully loaded
    this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage));

    // View image depends on having an image that's not standalone
    // (or is in a frame), or a canvas.
    this.showItem("context-viewimage", (this.onImage &&
                  (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas);

    // 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.inSyntheticDoc);
    this.showItem("context-sep-viewbgimage", shouldShow &&
                                             !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");
    this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== "");
  },

  initMiscItems: function CM_initMiscItems() {
    // Use "Bookmark This Link" if on a link.
    let bookmarkPage = document.getElementById("context-bookmarkpage");
    this.showItem(bookmarkPage,
                  !(this.isContentSelected || this.onTextInput || this.onLink ||
                    this.onImage || this.onVideo || this.onAudio ||
                    this.onCanvas || this.inWebExtBrowser));
    bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext"));

    this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
                                           !this.onMozExtLink) || this.onPlainTextLink);
    this.showItem("context-keywordfield",
                  this.onTextInput && this.onKeywordField);
    this.showItem("frame", this.inFrame);

    let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage;
    this.showItem("context-searchselect", showSearchSelect);
    if (showSearchSelect) {
      this.formatSearchContextItem();
    }

    // srcdoc cannot be opened separately due to concerns about web
    // content with about:srcdoc in location bar masquerading as trusted
    // chrome/addon content.
    // No need to also test for this.inFrame as this is checked in the parent
    // submenu.
    this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
    this.showItem("context-openframeintab", !this.inSrcdocFrame);
    this.showItem("context-openframe", !this.inSrcdocFrame);
    this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
    this.showItem("open-frame-sep", !this.inSrcdocFrame);

    this.showItem("frame-sep", this.inFrame && this.isTextSelected);

    // Hide menu entries for images, show otherwise
    if (this.inFrame) {
      if (BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType))
        this.isFrameImage.removeAttribute("hidden");
      else
        this.isFrameImage.setAttribute("hidden", "true");
    }

    // BiDi UI
    this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI);
    this.showItem("context-bidi-text-direction-toggle",
                  this.onTextInput && !this.onNumeric && top.gBidiUI);
    this.showItem("context-bidi-page-direction-toggle",
                  !this.onTextInput && top.gBidiUI);
  },

  initSpellingItems() {
    var canSpell = InlineSpellCheckerUI.canSpellCheck &&
                   !InlineSpellCheckerUI.initialSpellCheckPending &&
                   this.canSpellCheck;
    let showDictionaries = canSpell && InlineSpellCheckerUI.enabled;
    var onMisspelling = InlineSpellCheckerUI.overMisspelling;
    var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
    this.showItem("spell-check-enabled", canSpell);
    this.showItem("spell-separator", canSpell);
    document.getElementById("spell-check-enabled")
            .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);

    this.showItem("spell-add-to-dictionary", onMisspelling);
    this.showItem("spell-undo-add-to-dictionary", showUndo);

    // suggestion list
    this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
    if (onMisspelling) {
      var suggestionsSeparator =
        document.getElementById("spell-add-to-dictionary");
      var numsug =
        InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode,
                                                  suggestionsSeparator, 5);
      this.showItem("spell-no-suggestions", numsug == 0);
    } else {
      this.showItem("spell-no-suggestions", false);
    }

    // dictionary list
    this.showItem("spell-dictionaries", showDictionaries);
    if (canSpell) {
      var dictMenu = document.getElementById("spell-dictionaries-menu");
      var dictSep = document.getElementById("spell-language-separator");
      let count = InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
      this.showItem(dictSep, count > 0);
      this.showItem("spell-add-dictionaries-main", false);
    } else if (this.onSpellcheckable) {
      // when there is no spellchecker but we might be able to spellcheck
      // add the add to dictionaries item. This will ensure that people
      // with no dictionaries will be able to download them
      this.showItem("spell-language-separator", showDictionaries);
      this.showItem("spell-add-dictionaries-main", showDictionaries);
    } else {
      this.showItem("spell-add-dictionaries-main", false);
    }
  },

  initClipboardItems() {
    // Copy depends on whether there is selected text.
    // Enabling this context menu item is now done through the global
    // command updating system
    // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
    goUpdateGlobalEditMenuItems();

    this.showItem("context-undo", this.onTextInput);
    this.showItem("context-sep-undo", this.onTextInput);
    this.showItem("context-cut", this.onTextInput);
    this.showItem("context-copy",
                  this.isContentSelected || this.onTextInput);
    this.showItem("context-paste", this.onTextInput);
    this.showItem("context-delete", this.onTextInput);
    this.showItem("context-sep-paste", this.onTextInput);
    this.showItem("context-selectall", !(this.onLink || this.onImage ||
                                         this.onVideo || this.onAudio ||
                                         this.inSyntheticDoc) ||
                                       this.isDesignMode);
    this.showItem("context-sep-selectall", this.isContentSelected );

    // XXX dr
    // ------
    // nsDocumentViewer.cpp has code to determine whether we're
    // on a link or an image. we really ought to be using that...

    // Copy email link depends on whether we're on an email link.
    this.showItem("context-copyemail", this.onMailtoLink);

    // Copy link location depends on whether we're on a non-mailto link.
    this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
    this.showItem("context-sep-copylink", this.onLink &&
                  (this.onImage || this.onVideo || this.onAudio));

    // Copy image contents depends on whether we're on an image.
    // Note: the element doesn't exist on all platforms, but showItem() takes
    // care of that by itself.
    this.showItem("context-copyimage-contents", this.onImage);

    // Copy image location depends on whether we're on an image.
    this.showItem("context-copyimage", this.onImage);
    this.showItem("context-copyvideourl", this.onVideo);
    this.showItem("context-copyaudiourl", this.onAudio);
    this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
    this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
    this.showItem("context-sep-copyimage", this.onImage ||
                  this.onVideo || this.onAudio);
  },

  initMediaPlayerItems() {
    var onMedia = (this.onVideo || this.onAudio);
    // Several mutually exclusive items... play/pause, mute/unmute, show/hide
    this.showItem("context-media-play", onMedia && (this.target.paused || this.target.ended));
    this.showItem("context-media-pause", onMedia && !this.target.paused && !this.target.ended);
    this.showItem("context-media-mute", onMedia && !this.target.muted);
    this.showItem("context-media-unmute", onMedia && this.target.muted);
    this.showItem("context-media-playbackrate", onMedia && this.target.duration != Number.POSITIVE_INFINITY);
    this.showItem("context-media-loop", onMedia);
    this.showItem("context-media-showcontrols", onMedia && !this.target.controls);
    this.showItem("context-media-hidecontrols", this.target.controls && (this.onVideo || (this.onAudio && !this.inSyntheticDoc)));
    this.showItem("context-video-fullscreen", this.onVideo && !this.target.ownerDocument.fullscreen);
    this.showItem("context-media-eme-learnmore", this.onDRMMedia);
    this.showItem("context-media-eme-separator", this.onDRMMedia);

    // Disable them when there isn't a valid media source loaded.
    if (onMedia) {
      this.setItemAttr("context-media-playbackrate-050x", "checked", this.target.playbackRate == 0.5);
      this.setItemAttr("context-media-playbackrate-100x", "checked", this.target.playbackRate == 1.0);
      this.setItemAttr("context-media-playbackrate-125x", "checked", this.target.playbackRate == 1.25);
      this.setItemAttr("context-media-playbackrate-150x", "checked", this.target.playbackRate == 1.5);
      this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0);
      this.setItemAttr("context-media-loop", "checked", this.target.loop);
      var hasError = this.target.error != null ||
                     this.target.networkState == this.target.NETWORK_NO_SOURCE;
      this.setItemAttr("context-media-play", "disabled", hasError);
      this.setItemAttr("context-media-pause", "disabled", hasError);
      this.setItemAttr("context-media-mute", "disabled", hasError);
      this.setItemAttr("context-media-unmute", "disabled", hasError);
      this.setItemAttr("context-media-playbackrate", "disabled", hasError);
      this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
      this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
      this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError);
      this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
      this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
      this.setItemAttr("context-media-showcontrols", "disabled", hasError);
      this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
      if (this.onVideo) {
        let canSaveSnapshot = !this.onDRMMedia && this.target.readyState >= this.target.HAVE_CURRENT_DATA;
        this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot);
        this.setItemAttr("context-video-fullscreen", "disabled", hasError);
      }
    }
    this.showItem("context-media-sep-commands", onMedia);
  },

  initClickToPlayItems() {
    this.showItem("context-ctp-play", this.onCTPPlugin);
    this.showItem("context-ctp-hide", this.onCTPPlugin);
    this.showItem("context-sep-ctp", this.onCTPPlugin);
  },

  initPasswordManagerItems() {
    let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo;

    // If we could not find a password field we
    // don't want to show the form fill option.
    let showFill = loginFillInfo && loginFillInfo.passwordField.found;

    // Disable the fill option if the user has set a master password
    // or if the password field or target field are disabled.
    let disableFill = !loginFillInfo ||
                      !Services.logins ||
                      !Services.logins.isLoggedIn ||
                      loginFillInfo.passwordField.disabled ||
                      (!this.onPassword && loginFillInfo.usernameField.disabled);

    this.showItem("fill-login-separator", showFill);
    this.showItem("fill-login", showFill);
    this.setItemAttr("fill-login", "disabled", disableFill);

    // Set the correct label for the fill menu
    let fillMenu = document.getElementById("fill-login");
    if (this.onPassword) {
      fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
      fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-password"));
    } else {
      fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
      fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-login"));
    }

    if (!showFill || disableFill) {
      return;
    }
    let documentURI = gContextMenuContentData.documentURIObject;
    let fragment = LoginManagerContextMenu.addLoginsToMenu(this.targetAsCPOW, this.browser, documentURI);

    this.showItem("fill-login-no-logins", !fragment);

    if (!fragment) {
      return;
    }
    let popup = document.getElementById("fill-login-popup");
    let insertBeforeElement = document.getElementById("fill-login-no-logins");
    popup.insertBefore(fragment, insertBeforeElement);
  },

  initSyncItems() {
    gSync.updateContentContextMenu(this);
  },

  openPasswordManager() {
    LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
  },

  inspectNode() {
    return DevToolsShim.inspectNode(gBrowser.selectedTab, this.targetSelectors);
  },

  inspectA11Y() {
    return DevToolsShim.inspectA11Y(gBrowser.selectedTab, this.targetSelectors);
  },

  _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 || this.onPlainTextLink };
    for (let p in extra) {
      params[p] = extra[p];
    }

    if (!this.isRemote) {
      // Propagate the frameOuterWindowID value saved when
      // the context menu has been opened.
      params.frameOuterWindowID = this.frameOuterWindowID;
    }

    // If we want to change userContextId, we must be sure that we don't
    // propagate the referrer.
    if ("userContextId" in params &&
        params.userContextId != gContextMenuContentData.userContextId) {
      params.noReferrer = true;
    }

    return params;
  },

  // Open linked-to URL in a new window.
  openLink() {
    openLinkIn(this.linkURL, "window", this._openLinkInParameters());
  },

  // Open linked-to URL in a new private window.
  openLinkInPrivateWindow() {
    openLinkIn(this.linkURL, "window",
               this._openLinkInParameters({ private: true }));
  },

  // Open linked-to URL in a new tab.
  openLinkInTab(event) {
    let referrerURI = gContextMenuContentData.documentURIObject;

    // if its parent allows mixed content and the referring URI passes
    // a same origin check with the target URI, we can preserve the users
    // decision of disabling MCB on a page for it's child tabs.
    let persistAllowMixedContentInChildTab = false;

    if (gContextMenuContentData.parentAllowsMixedContent) {
      const sm = Services.scriptSecurityManager;
      try {
        let targetURI = this.linkURI;
        sm.checkSameOriginURI(referrerURI, targetURI, false);
        persistAllowMixedContentInChildTab = true;
      } catch (e) { }
    }

    let params = {
      allowMixedContent: persistAllowMixedContentInChildTab,
      userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
    };

    openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
  },

  // open URL in current tab
  openLinkInCurrent() {
    openLinkIn(this.linkURL, "current", this._openLinkInParameters());
  },

  // Open frame in a new tab.
  openFrameInTab() {
    let referrer = gContextMenuContentData.referrer;
    openLinkIn(gContextMenuContentData.docLocation, "tab",
               { charset: gContextMenuContentData.charSet,
                 referrerURI: referrer ? makeURI(referrer) : null });
  },

  // Reload clicked-in frame.
  reloadFrame(aEvent) {
    let forceReload = aEvent.shiftKey;
    this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadFrame",
                                                 null, { target: this.target, forceReload });
  },

  // Open clicked-in frame in its own window.
  openFrame() {
    let referrer = gContextMenuContentData.referrer;
    openLinkIn(gContextMenuContentData.docLocation, "window",
               { charset: gContextMenuContentData.charSet,
                 referrerURI: referrer ? makeURI(referrer) : null });
  },

  // Open clicked-in frame in the same window.
  showOnlyThisFrame() {
    urlSecurityCheck(gContextMenuContentData.docLocation,
                     this.browser.contentPrincipal,
                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
    let referrer = gContextMenuContentData.referrer;
    openWebLinkIn(gContextMenuContentData.docLocation, "current", {
      disallowInheritPrincipal: true,
      referrerURI: referrer ? makeURI(referrer) : null,
      triggeringPrincipal: this.browser.contentPrincipal,
    });
  },

  reload(event) {
    BrowserReloadOrDuplicate(event);
  },

  // View Partial Source
  viewPartialSource(aContext) {
    let {browser} = this;
    let openSelectionFn = function() {
      let tabBrowser = gBrowser;
      // In the case of popups, we need to find a non-popup browser window.
      // We might also not have a tabBrowser reference (if this isn't in a
      // a tabbrowser scope) or might have a fake/stub tabbrowser reference
      // (in the sidebar). Deal with those cases:
      if (!tabBrowser || !tabBrowser.loadOneTab || !window.toolbar.visible) {
        // This returns only non-popup browser windows by default.
        let browserWindow = BrowserWindowTracker.getTopWindow();
        tabBrowser = browserWindow.gBrowser;
      }
      let relatedToCurrent = gBrowser && gBrowser.selectedBrowser == browser;
      let tab = tabBrowser.loadOneTab("about:blank", {
        relatedToCurrent,
        inBackground: false,
        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
      });
      return tabBrowser.getBrowserForTab(tab);
    };

    let target = aContext == "mathml" ? this.target : null;
    top.gViewSourceUtils.viewPartialSourceInBrowser(browser, target, openSelectionFn);
  },

  // Open new "view source" window with the frame's URL.
  viewFrameSource() {
    BrowserViewSourceOfDocument({
      browser: this.browser,
      URL: gContextMenuContentData.docLocation,
      outerWindowID: this.frameOuterWindowID,
    });
  },

  viewInfo() {
    BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser);
  },

  viewImageInfo() {
    BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
                    this.imageInfo, null, this.browser);
  },

  viewImageDesc(e) {
    urlSecurityCheck(this.imageDescURL,
                     this.principal,
                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
    openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
                                       referrerURI: gContextMenuContentData.documentURIObject,
                                       triggeringPrincipal: this.principal,
    });
  },

  viewFrameInfo() {
    BrowserPageInfo(gContextMenuContentData.docLocation, null, null,
                    this.frameOuterWindowID, this.browser);
  },

  reloadImage() {
    urlSecurityCheck(this.mediaURL,
                     this.principal,
                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);

    this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
                                                 null, { target: this.target });
  },

  _canvasToBlobURL(target) {
    let mm = this.browser.messageManager;
    return new Promise(function(resolve) {
      mm.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL", {}, { target });

      let onMessage = (message) => {
        mm.removeMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
        resolve(message.data.blobURL);
      };
      mm.addMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
    });
  },

  // Change current window to the URL of the image, video, or audio.
  viewMedia(e) {
    let referrerURI = gContextMenuContentData.documentURIObject;
    let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
    if (this.onCanvas) {
      this._canvasToBlobURL(this.target).then(function(blobURL) {
        openUILink(blobURL, e, { disallowInheritPrincipal: true,
                                 referrerURI,
                                 triggeringPrincipal: systemPrincipal});
      }, Cu.reportError);
    } else {
      urlSecurityCheck(this.mediaURL,
                       this.principal,
                       Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
      openUILink(this.mediaURL, e, { disallowInheritPrincipal: true,
                                     referrerURI,
                                     forceAllowDataURI: true,
                                     triggeringPrincipal: this.principal,
      });
    }
  },

  saveVideoFrameAsImage() {
    let mm = this.browser.messageManager;
    let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);

    let name = "";
    if (this.mediaURL) {
      try {
        let uri = makeURI(this.mediaURL);
        let url = uri.QueryInterface(Ci.nsIURL);
        if (url.fileBaseName)
          name = decodeURI(url.fileBaseName) + ".jpg";
      } catch (e) { }
    }
    if (!name)
      name = "snapshot.jpg";

    mm.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage", {}, {
      target: this.target,
    });

    let onMessage = (message) => {
      mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
      let dataURL = message.data.dataURL;
      saveImageURL(dataURL, name, "SaveImageTitle", true, false,
                   document.documentURIObject, null, null, null,
                   isPrivate);
    };
    mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
  },

  leaveDOMFullScreen() {
    document.exitFullscreen();
  },

  // Change current window to the URL of the background image.
  viewBGImage(e) {
    urlSecurityCheck(this.bgImageURL,
                     this.principal,
                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);

    openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
                                     referrerURI: gContextMenuContentData.documentURIObject,
                                     triggeringPrincipal: this.principal,
    });
  },

  setDesktopBackground() {
    let mm = this.browser.messageManager;

    mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null,
                        { target: this.target });

    let onMessage = (message) => {
      mm.removeMessageListener("ContextMenu:SetAsDesktopBackground:Result",
                               onMessage);

      if (message.data.disable ||
          !Services.policies.isAllowed("setDesktopBackground")) {
        return;
      }

      let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
      image.src = message.data.dataUrl;
      let imageName = message.data.imageName;

      // Confirm since it's annoying if you hit this accidentally.
      const kDesktopBackgroundURL =
                    "chrome://browser/content/setDesktopBackground.xul";

      if (AppConstants.platform == "macosx") {
        // On Mac, the Set Desktop Background window is not modal.
        // Don't open more than one Set Desktop Background window.
        let dbWin = Services.wm.getMostRecentWindow("Shell:SetDesktopBackground");
        if (dbWin) {
          dbWin.gSetBackground.init(image, imageName);
          dbWin.focus();
        } else {
          openDialog(kDesktopBackgroundURL, "",
                     "centerscreen,chrome,dialog=no,dependent,resizable=no",
                     image, imageName);
        }
      } else {
        // On non-Mac platforms, the Set Wallpaper dialog is modal.
        openDialog(kDesktopBackgroundURL, "",
                   "centerscreen,chrome,dialog,modal,dependent",
                   image, imageName);
      }
    };

    mm.addMessageListener("ContextMenu:SetAsDesktopBackground:Result", onMessage);
  },

  // 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, 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() {}
    saveAsListener.prototype = {
      extListener: null,

      onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {

        // if the timer fired, the error status will have been caused by that,
        // and we'll be restarting in onStopRequest, so no reason to notify
        // the user
        if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
          return;

        timer.cancel();

        // some other error occured; notify the user...
        if (!Components.isSuccessCode(aRequest.status)) {
          try {
            const bundle = Services.strings.createBundle(
                    "chrome://mozapps/locale/downloads/downloads.properties");

            const title = bundle.GetStringFromName("downloadErrorAlertTitle");
            const msg = bundle.GetStringFromName("downloadErrorGeneric");

            let window = Services.wm.getOuterWindowWithId(windowID);
            Services.prompt.alert(window, title, msg);
          } catch (ex) {}
          return;
        }

        let extHelperAppSvc =
          Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
          getService(Ci.nsIExternalHelperAppService);
        let channel = aRequest.QueryInterface(Ci.nsIChannel);
        this.extListener =
          extHelperAppSvc.doContent(channel.contentType, aRequest,
                                    null, true, window);
        this.extListener.onStartRequest(aRequest, aContext);
      },

      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, isContentWindowPrivate);
        }
        if (this.extListener)
          this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
      },

      onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
                                                           aInputStream,
                                                           aOffset, aCount) {
        this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
                                         aOffset, aCount);
      }
    };

    function callbacks() {}
    callbacks.prototype = {
      getInterface: function sLA_callbacks_getInterface(aIID) {
        if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
          // If the channel demands authentication prompt, we must cancel it
          // because the save-as-timer would expire and cancel the channel
          // before we get credentials from user.  Both authentication dialog
          // and save as dialog would appear on the screen as we fall back to
          // the old fashioned way after the timeout.
          timer.cancel();
          channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
        }
        throw Cr.NS_ERROR_NO_INTERFACE;
      }
    };

    // if it we don't have the headers after a short time, the user
    // won't have received any feedback from their click.  that's bad.  so
    // we give up waiting for the filename.
    function timerCallback() {}
    timerCallback.prototype = {
      notify: function sLA_timer_notify(aTimer) {
        channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
      }
    };

    // setting up a new channel for 'right click - save link as ...'
    var channel = NetUtil.newChannel({
                    uri: makeURI(linkURL),
                    loadingPrincipal: this.principal,
                    contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
                    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
                  });

    if (linkDownload)
      channel.contentDispositionFilename = linkDownload;
    if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
      let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
      channel.setPrivate(docIsPrivate);
    }
    channel.notificationCallbacks = new callbacks();

    let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;

    if (bypassCache)
      flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;

    if (channel instanceof Ci.nsICachingChannel)
      flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;

    channel.loadFlags |= flags;

    if (channel instanceof Ci.nsIHttpChannel) {
      channel.referrer = docURI;
      if (channel instanceof Ci.nsIHttpChannelInternal)
        channel.forceAllowThirdPartyCookie = true;
    }

    // fallback to the old way if we don't see the headers quickly
    var timeToWait =
      Services.prefs.getIntPref("browser.download.saveLinkAsFilenameTimeout");
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    timer.initWithCallback(new timerCallback(), timeToWait,
                           timer.TYPE_ONE_SHOT);

    // kick off the channel with our proxy object as the listener
    channel.asyncOpen2(new saveAsListener());
  },

  // Save URL of clicked-on link.
  saveLink() {
    let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
    this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
                    gContextMenuContentData.documentURIObject,
                    this.frameOuterWindowID,
                    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);
      }, Cu.reportError);
    } else if (this.onImage) {
      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) {
      var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
      this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
                      this.frameOuterWindowID, "", isContentWindowPrivate);
    }
  },

  // Backwards-compatibility wrapper
  sendImage() {
    if (this.onCanvas || this.onImage)
        this.sendMedia();
  },

  sendMedia() {
    MailIntegration.sendMessage(this.mediaURL, "");
  },

  playPlugin() {
    gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
  },

  hidePlugin() {
    gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
  },

  // Generate email address and put it on clipboard.
  copyEmail() {
    // Copy the comma-separated list of email addresses only.
    // There are other ways of embedding email addresses in a mailto:
    // link, but such complex parsing is beyond us.
    var url = this.linkURL;
    var qmark = url.indexOf("?");
    var addresses;

    // 7 == length of "mailto:"
    addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);

    // Let's try to unescape it using a character set
    // in case the address is not ASCII.
    try {
      addresses = Services.textToSubURI.unEscapeURIForUI(gContextMenuContentData.charSet,
                                                         addresses);
    } catch (ex) {
      // Do nothing.
    }

    var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
                    getService(Ci.nsIClipboardHelper);
    clipboard.copyString(addresses);
  },

  copyLink() {
    // If we're in a view source tab, remove the view-source: prefix
    let linkURL = this.linkURL.replace(/^view-source:/, "");
    var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
                    getService(Ci.nsIClipboardHelper);
    clipboard.copyString(linkURL);
  },

  /**
   * Utilities
   */

  /**
   * Show/hide one item (specified via name or the item element itself).
   * If the element is not found, then this function finishes silently.
   *
   * @param {Element|String} aItemOrId The item element or the name of the element
   *                                   to show.
   * @param {Boolean} aShow Set to true to show the item, false to hide it.
   */
  showItem(aItemOrId, aShow) {
    var item = aItemOrId.constructor == String ?
      document.getElementById(aItemOrId) : aItemOrId;
    if (item)
      item.hidden = !aShow;
  },

  // Set given attribute of specified context-menu item.  If the
  // value is null, then it removes the attribute (which works
  // nicely for the disabled attribute).
  setItemAttr(aID, aAttr, aVal ) {
    var elem = document.getElementById(aID);
    if (elem) {
      if (aVal == null) {
        // null indicates attr should be removed.
        elem.removeAttribute(aAttr);
      } else {
        // Set attr=val.
        elem.setAttribute(aAttr, aVal);
      }
    }
  },

  // Set context menu attribute according to like attribute of another node
  // (such as a broadcaster).
  setItemAttrFromNode(aItem_id, aAttr, aOther_id) {
    var elem = document.getElementById(aOther_id);
    if (elem && elem.getAttribute(aAttr) == "true")
      this.setItemAttr(aItem_id, aAttr, "true");
    else
      this.setItemAttr(aItem_id, aAttr, null);
  },

  // Temporary workaround for DOM api not yet implemented by XUL nodes.
  cloneNode(aItem) {
    // Create another element like the one we're cloning.
    var node = document.createElement(aItem.tagName);

    // Copy attributes from argument item to the new one.
    var attrs = aItem.attributes;
    for (var i = 0; i < attrs.length; i++) {
      var attr = attrs.item(i);
      node.setAttribute(attr.nodeName, attr.nodeValue);
    }

    // Voila!
    return node;
  },

  getLinkURI() {
    try {
      return makeURI(this.linkURL);
    } catch (ex) {
     // e.g. empty URL string
    }

    return null;
  },

  // Kept for addon compat
  linkText() {
    return this.linkTextStr;
  },

  // 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") {
        if (!sibling.hidden)
          return true;
        sibling = sibling.previousSibling;
      }
    }
    return false;
  },

  addDictionaries() {
    var uri = formatURL("browser.dictionaries.download.url", true);

    var locale = "-";
    try {
      locale = Services.prefs.getComplexValue("intl.accept_languages",
                                              Ci.nsIPrefLocalizedString).data;
    } catch (e) { }

    var version = "-";
    try {
      version = Services.appinfo.version;
    } catch (e) { }

    uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);

    var newWindowPref = Services.prefs.getIntPref("browser.link.open_newwindow");
    var where = newWindowPref == 3 ? "tab" : "window";

    openTrustedLinkIn(uri, where);
  },

  bookmarkThisPage: function CM_bookmarkThisPage() {
    window.top.PlacesCommandHook
              .bookmarkPage(this.browser, true)
              .catch(Cu.reportError);
  },

  bookmarkLink: function CM_bookmarkLink() {
    window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
                                              this.linkURL, this.linkTextStr);
  },

  addBookmarkForFrame: function CM_addBookmarkForFrame() {
    let uri = gContextMenuContentData.documentURIObject;
    let mm = this.browser.messageManager;

    let onMessage = (message) => {
      mm.removeMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);

      window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
                                                uri.spec,
                                                message.data.title)
                                  .catch(Cu.reportError);
    };
    mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);

    mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target });
  },

  savePageAs: function CM_savePageAs() {
    saveBrowser(this.browser);
  },

  printFrame: function CM_printFrame() {
    PrintUtils.printWindow(this.frameOuterWindowID, this.browser);
  },

  switchPageDirection: function CM_switchPageDirection() {
    this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
  },

  mediaCommand: function CM_mediaCommand(command, data) {
    let mm = this.browser.messageManager;
    let win = this.browser.ownerGlobal;
    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDOMWindowUtils);
    mm.sendAsyncMessage("ContextMenu:MediaCommand",
                        {command,
                         data,
                         handlingUserInput: windowUtils.isHandlingUserInput},
                        {element: this.target});
  },

  copyMediaLocation() {
    var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
                    getService(Ci.nsIClipboardHelper);
    clipboard.copyString(this.mediaURL);
  },

  drmLearnMore(aEvent) {
    let drmInfoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
    let dest = whereToOpenLink(aEvent);
    // Don't ever want this to open in the same tab as it'll unload the
    // DRM'd video, which is going to be a bad idea in most cases.
    if (dest == "current") {
      dest = "tab";
    }
    openTrustedLinkIn(drmInfoURL, dest);
  },

  get imageURL() {
    if (this.onImage)
      return this.mediaURL;
    return "";
  },

  // Formats the 'Search <engine> for "<selection or link text>"' context menu.
  formatSearchContextItem() {
    var menuItem = document.getElementById("context-searchselect");
    let selectedText = this.isTextSelected ? this.textSelected : this.linkTextStr;

    // Store searchTerms in context menu item so we know what to search onclick
    menuItem.searchTerms = selectedText;

    // Copied to alert.js' prefillAlertInfo().
    // If the JS character after our truncation point is a trail surrogate,
    // include it in the truncated string to avoid splitting a surrogate pair.
    if (selectedText.length > 15) {
      let truncLength = 15;
      let truncChar = selectedText[15].charCodeAt(0);
      if (truncChar >= 0xDC00 && truncChar <= 0xDFFF)
        truncLength++;
      selectedText = selectedText.substr(0, truncLength) + this.ellipsis;
    }

    // format "Search <engine> for <selection>" string to show in menu
    let engineName = Services.search.currentEngine.name;
    var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
                                                        [engineName,
                                                         selectedText]);
    menuItem.label = menuLabel;
    menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
  },

  createContainerMenu(aEvent) {
    let createMenuOptions = {
      isContextMenu: true,
      excludeUserContextId: gContextMenuContentData.userContextId,
    };
    return createUserContextMenu(aEvent, createMenuOptions);
  },
};
