summaryrefslogtreecommitdiffstats
path: root/comm/suite/base/content/nsContextMenu.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/base/content/nsContextMenu.js
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/base/content/nsContextMenu.js')
-rw-r--r--comm/suite/base/content/nsContextMenu.js1676
1 files changed, 1676 insertions, 0 deletions
diff --git a/comm/suite/base/content/nsContextMenu.js b/comm/suite/base/content/nsContextMenu.js
new file mode 100644
index 0000000000..26514ff39d
--- /dev/null
+++ b/comm/suite/base/content/nsContextMenu.js
@@ -0,0 +1,1676 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+/*------------------------------ nsContextMenu ---------------------------------
+| This JavaScript "class" is used to implement the browser's content-area |
+| context menu. |
+| |
+| For usage, see references to this class in navigator.xul. |
+| |
+| Currently, this code is relatively useless for any other purpose. In the |
+| longer term, this code will be restructured to make it more reusable. |
+------------------------------------------------------------------------------*/
+
+var {BrowserUtils} =
+ ChromeUtils.import("resource://gre/modules/BrowserUtils.jsm");
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"
+var {LoginManagerContextMenu} =
+ ChromeUtils.import("resource://gre/modules/LoginManagerContextMenu.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "InlineSpellCheckerUI", () => {
+ let { InlineSpellChecker } = ChromeUtils.import(
+ "resource://gre/modules/InlineSpellChecker.jsm"
+ );
+ return new InlineSpellChecker();
+});
+
+XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() {
+ let tmp = {};
+ ChromeUtils.import("resource://gre/modules/PageMenu.jsm", tmp);
+ return new tmp.PageMenuParent();
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
+ findCssSelector: "resource://gre/modules/css-selector.js",
+ LoginHelper: "resource://gre/modules/LoginHelper.jsm",
+ LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
+ DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ ShellService: "resource:///modules/ShellService.jsm",
+
+});
+
+var gContextMenuContentData = null;
+
+function nsContextMenu(aXulMenu, aIsShift, aEvent) {
+ this.shouldDisplay = true;
+ this.initMenu(aXulMenu, aIsShift, aEvent);
+}
+
+// Prototype for nsContextMenu "class."
+nsContextMenu.prototype = {
+ initMenu: function(aXulMenu, aIsShift, aEvent) {
+ // Get contextual info.
+ this.setTarget(document.popupNode, document.popupRangeParent,
+ document.popupRangeOffset);
+
+ if (!this.shouldDisplay)
+ return;
+
+ this.hasPageMenu = false;
+ if (!aIsShift && this.browser.docShell.allowJavascript &&
+ Services.prefs.getBoolPref("javascript.enabled"))
+ this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
+
+ this.isTextSelected = this.isTextSelection();
+ this.isContentSelected = this.isContentSelection();
+
+ // Initialize gContextMenuContentData.
+ if (aEvent)
+ this.initContentData(aEvent);
+ // Initialize (disable/remove) menu items.
+ this.initItems();
+ },
+
+ initContentData: function(aEvent) {
+ var addonInfo = {};
+ var subject = {
+ event: aEvent,
+ addonInfo: addonInfo,
+ };
+ subject.wrappedJSObject = subject;
+ // Notifies the Addon-SDK which then populates addonInfo.
+ Services.obs.notifyObservers(subject, "content-contextmenu");
+
+ var popupNode = this.target;
+ var doc = popupNode.ownerDocument;
+
+ var contentType = null;
+ var contentDisposition = null;
+ if (this.onImage) {
+ try {
+ let imageCache = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .getImgCacheForDocument(doc);
+ let props = imageCache.findEntryProperties(popupNode.currentURI, doc);
+ if (props) {
+ let nsISupportsCString = Ci.nsISupportsCString;
+ contentType = props.get("type", nsISupportsCString).data;
+ try {
+ contentDisposition = props.get("content-disposition",
+ nsISupportsCString).data;
+ } catch (e) {}
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ gContextMenuContentData = {
+ isRemote: false,
+ event: aEvent,
+ popupNode: popupNode,
+ browser: this.browser,
+ principal: doc.nodePrincipal,
+ addonInfo: addonInfo,
+ documentURIObject: doc.documentURIObject,
+ docLocation: doc.location.href,
+ charSet: doc.characterSet,
+ referrer: doc.referrer,
+ referrerPolicy: doc.referrerPolicy,
+ contentType: contentType,
+ contentDisposition: contentDisposition,
+ frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID,
+ loginFillInfo: LoginManagerContent.getFieldContext(popupNode),
+ };
+ },
+
+ hiding: function () {
+ gContextMenuContentData = null;
+ InlineSpellCheckerUI.clearSuggestionsFromMenu();
+ InlineSpellCheckerUI.clearDictionaryListFromMenu();
+ InlineSpellCheckerUI.uninit();
+ LoginManagerContextMenu.clearLoginsFromMenu(document);
+ },
+
+ initItems: function() {
+ this.initPageMenuSeparator();
+ this.initOpenItems();
+ this.initNavigationItems();
+ this.initViewItems();
+ this.initMiscItems();
+ this.initSpellingItems();
+ this.initSaveItems();
+ this.initClipboardItems();
+ this.initMetadataItems();
+ this.initMediaPlayerItems();
+ this.initPasswordManagerItems();
+ },
+
+ initPageMenuSeparator: function() {
+ this.showItem("page-menu-separator", this.hasPageMenu);
+ },
+
+ initOpenItems: function() {
+ var showOpen = this.onSaveableLink || (this.inDirList && this.onLink);
+ this.showItem("context-openlinkintab", showOpen);
+ this.showItem("context-openlink", showOpen && !gPrivate);
+ this.showItem("context-openlinkinprivatewindow", showOpen);
+ this.showItem("context-sep-open", showOpen);
+ },
+
+ initNavigationItems: function() {
+ // Back/Forward determined by canGoBack/canGoForward broadcasters.
+ this.setItemAttrFromNode("context-back", "disabled", "canGoBack");
+ this.setItemAttrFromNode("context-forward", "disabled", "canGoForward");
+
+ var showNav = !(this.isContentSelected || this.onLink || this.onImage ||
+ this.onCanvas || this.onVideo || this.onAudio ||
+ this.onTextInput);
+
+ this.showItem("context-back", showNav);
+ this.showItem("context-forward", showNav);
+ this.showItem("context-reload", showNav);
+ this.showItem("context-stop", showNav);
+ this.showItem("context-sep-stop", showNav);
+
+ // XXX: Stop is determined in navigator.js; the canStop broadcaster is broken
+ //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
+ },
+
+ initSaveItems: function() {
+ var showSave = !(this.inDirList || this.isContentSelected ||
+ this.onTextInput || this.onStandaloneImage ||
+ this.onCanvas || this.onVideo || this.onAudio ||
+ (this.onLink && this.onImage));
+ if (showSave)
+ goSetMenuValue("context-savepage",
+ this.autoDownload ? "valueSave" : "valueSaveAs");
+ this.showItem("context-savepage", showSave);
+
+ // Save/send link depends on whether we're in a link.
+ if (this.onSaveableLink)
+ goSetMenuValue("context-savelink",
+ this.autoDownload ? "valueSave" : "valueSaveAs");
+ this.showItem("context-savelink", this.onSaveableLink);
+ this.showItem("context-sendlink", this.onSaveableLink);
+
+ // Save image depends on having loaded its content, video and audio don't.
+ showSave = (this.onLoadedImage && this.onCompletedImage) ||
+ this.onStandaloneImage || this.onCanvas;
+ if (showSave)
+ goSetMenuValue("context-saveimage",
+ this.autoDownload ? "valueSave" : "valueSaveAs");
+ this.showItem("context-saveimage", showSave);
+ this.showItem("context-savevideo", this.onVideo);
+ this.showItem("context-saveaudio", this.onAudio);
+ this.showItem("context-video-saveimage", this.onVideo);
+ if (this.onVideo)
+ this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
+ if (this.onAudio)
+ 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", showSave && !this.onCanvas);
+ this.showItem("context-sendvideo", this.onVideo);
+ this.showItem("context-sendaudio", this.onAudio);
+ if (this.onVideo)
+ this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL);
+ if (this.onAudio)
+ this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL);
+ },
+
+ initViewItems: function() {
+ // View source is always OK, unless in directory listing.
+ this.showItem("context-viewpartialsource-selection",
+ this.isContentSelected && !this.onTextInput);
+ this.showItem("context-viewpartialsource-mathml",
+ this.onMathML && !this.isContentSelected);
+
+ var showView = !(this.inDirList || this.onImage || this.isContentSelected ||
+ this.onCanvas || this.onVideo || this.onAudio ||
+ this.onLink || this.onTextInput);
+
+ this.showItem("context-viewsource", showView);
+ this.showItem("context-viewinfo", showView);
+
+ var showInspect = DevToolsShim.isEnabled() &&
+ "gDevTools" in window &&
+ Services.prefs.getBoolPref("devtools.inspector.enabled", false);
+ this.showItem("inspect-separator", showInspect);
+ this.showItem("context-inspect", showInspect);
+
+ this.showItem("context-sep-properties",
+ !(this.inDirList || this.isContentSelected || this.onTextInput ||
+ this.onCanvas || this.onVideo || this.onAudio));
+ // Set Desktop Background depends on whether an image was clicked on,
+ // and requires the shell service.
+ var canSetDesktopBackground = ShellService &&
+ ShellService.canSetDesktopBackground;
+ this.showItem("context-setDesktopBackground",
+ canSetDesktopBackground && (this.onLoadedImage || this.onStandaloneImage));
+
+ this.showItem("context-sep-image",
+ this.onLoadedImage || this.onStandaloneImage);
+
+ if (canSetDesktopBackground && this.onLoadedImage)
+ // Disable the Set Desktop Background menu item if we're still trying to load the image
+ this.setItemAttr("context-setDesktopBackground", "disabled",
+ (("complete" in this.target) && !this.target.complete) ? "true" : null);
+
+ this.showItem("context-fitimage", this.onStandaloneImage &&
+ content.document.imageResizingEnabled);
+ if (this.onStandaloneImage && content.document.imageResizingEnabled) {
+ this.setItemAttr("context-fitimage", "disabled",
+ content.document.imageIsOverflowing ? null : "true");
+ this.setItemAttr("context-fitimage", "checked",
+ content.document.imageIsResized ? "true" : null);
+ }
+
+ // 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", showView && !this.inSyntheticDoc);
+ this.showItem("context-sep-viewbgimage", showView && !this.inSyntheticDoc);
+ this.setItemAttr("context-viewbgimage", "disabled", this.hasBGImage ? null : "true");
+
+ this.showItem("context-viewimageinfo", this.onImage);
+
+ // Hide Block and Unblock menuitems.
+ this.showItem("context-blockimage", false);
+ this.showItem("context-unblockimage", false);
+ this.showItem("context-sep-blockimage", false);
+
+ // Block image depends on whether an image was clicked on.
+ if (this.onImage) {
+ var uri = Services.io.newURI(this.mediaURL);
+ if (uri instanceof Ci.nsIURL && uri.host) {
+ var serverLabel = uri.host;
+ // Limit length to max 15 characters.
+ serverLabel = serverLabel.replace(/^www\./i, "");
+ if (serverLabel.length > 15)
+ serverLabel = serverLabel.substr(0, 15) + this.ellipsis;
+
+ // Set label and accesskey for appropriate action and unhide menuitem.
+ var id = "context-blockimage";
+ var attr = "blockImage";
+ if (Services.perms.testPermission(uri, "image") == Services.perms.DENY_ACTION) {
+ id = "context-unblockimage";
+ attr = "unblockImage";
+ }
+ const bundle = document.getElementById("contentAreaCommandsBundle");
+ this.setItemAttr(id, "label",
+ bundle.getFormattedString(attr, [serverLabel]));
+ this.setItemAttr(id, "accesskey",
+ bundle.getString(attr + ".accesskey"));
+ this.showItem(id, true);
+ this.showItem("context-sep-blockimage", true);
+ }
+ }
+ },
+
+ initMiscItems: function() {
+ // Use "Bookmark This Link" if on a link.
+ this.showItem("context-bookmarkpage",
+ !(this.isContentSelected || this.onTextInput ||
+ this.onStandaloneImage || this.onVideo || this.onAudio));
+ this.showItem("context-bookmarklink", this.onLink && !this.onMailtoLink);
+ this.showItem("context-searchselect", this.isTextSelected);
+ this.showItem("context-keywordfield", this.onTextInput && this.onKeywordField);
+ this.showItem("frame", this.inFrame);
+ this.showItem("frame-sep", this.inFrame);
+ if (this.inFrame)
+ goSetMenuValue("context-saveframe",
+ this.autoDownload ? "valueSave" : "valueSaveAs");
+
+ // BiDi UI
+ this.showItem("context-sep-bidi", !this.onNumeric && gShowBiDi);
+ this.showItem("context-bidi-text-direction-toggle",
+ this.onTextInput && !this.onNumeric && gShowBiDi);
+ this.showItem("context-bidi-page-direction-toggle",
+ !this.onTextInput && gShowBiDi);
+ },
+
+ initSpellingItems: function() {
+ 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);
+ if (canSpell)
+ this.setItemAttr("spell-check-enabled", "checked", InlineSpellCheckerUI.enabled);
+ this.showItem("spell-add-to-dictionary", onMisspelling);
+ this.showItem("spell-undo-add-to-dictionary", showUndo);
+ this.showItem("spell-ignore-word", onMisspelling);
+
+ // suggestion list
+ this.showItem("spell-add-separator", onMisspelling);
+ this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
+ if (onMisspelling) {
+ var suggestionsSeparator = document.getElementById("spell-add-separator");
+ 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);
+ var dictMenu = document.getElementById("spell-dictionaries-menu");
+ if (canSpell && dictMenu) {
+ 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.onEditableArea) {
+ // 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: function() {
+ // 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-redo", 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.showItem("context-sep-selectall",
+ this.isContentSelected && !this.onTextInput);
+ // In a text area there will be nothing after select all, so we don't want a sep
+ // Otherwise, if there's text selected then there are extra menu items
+ // (search for selection and view selection source), so we do want a sep
+
+ // 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 link.
+ this.showItem("context-copylink", this.onLink);
+ this.showItem("context-sep-copylink", this.onLink);
+
+ // 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);
+ if (this.onVideo)
+ this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
+ if (this.onAudio)
+ this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
+ this.showItem("context-sep-copyimage",
+ this.onImage || this.onVideo || this.onAudio);
+ },
+
+ initMetadataItems: function() {
+ // Show if user clicked on something which has metadata.
+ this.showItem("context-metadata", this.onMetaDataItem);
+ },
+
+ initMediaPlayerItems: function() {
+ 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", onMedia && this.target.controls);
+ this.showItem("context-video-fullscreen", this.onVideo &&
+ !this.target.ownerDocument.fullscreenElement);
+
+ var statsShowing = this.onVideo && this.target.mozMediaStatisticsShowing;
+ this.showItem("context-video-showstats",
+ this.onVideo && this.target.controls && !statsShowing);
+ this.showItem("context-video-hidestats",
+ this.onVideo && this.target.controls && statsShowing);
+
+ // Disable them when there isn't a valid media source loaded.
+ if (onMedia) {
+ this.setItemAttr("context-media-playbackrate-050", "checked", this.target.playbackRate == 0.5);
+ this.setItemAttr("context-media-playbackrate-100", "checked", this.target.playbackRate == 1.0);
+ this.setItemAttr("context-media-playbackrate-125", "checked", this.target.playbackRate == 1.25);
+ this.setItemAttr("context-media-playbackrate-150", "checked", this.target.playbackRate == 1.5);
+ this.setItemAttr("context-media-playbackrate-200", "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-showcontrols", "disabled", hasError);
+ this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
+ if (this.onVideo) {
+ let canSave = this.target.readyState >= this.target.HAVE_CURRENT_DATA;
+ this.setItemAttr("context-video-saveimage", "disabled", !canSave);
+ this.setItemAttr("context-video-fullscreen", "disabled", hasError);
+ this.setItemAttr("context-video-showstats", "disabled", hasError);
+ this.setItemAttr("context-video-hidestats", "disabled", hasError);
+ }
+ }
+ this.showItem("context-media-sep-commands", onMedia);
+ },
+
+ initPasswordManagerItems: function() {
+ let fillMenu = document.getElementById("fill-login");
+ // If no fill Menu, probably mailContext so nothing to set up.
+ if (!fillMenu)
+ return;
+
+ 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
+ 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.target, 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);
+ },
+
+ openPasswordManager: function() {
+ // LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
+ toDataManager(gContextMenuContentData.documentURIObject.host + '|passwords');
+ },
+
+ /**
+ * 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).
+ */
+ getNodeSelectors: function(node) {
+ let selectors = [];
+ while (node) {
+ selectors.push(findCssSelector(node));
+ node = node.ownerGlobal.frameElement;
+ }
+
+ return selectors;
+ },
+
+ inspectNode: function() {
+ let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
+ return DevToolsShim.inspectNode(gBrowser.selectedTab,
+ this.getNodeSelectors(this.target));
+ },
+
+ // Set various context menu attributes based on the state of the world.
+ setTarget: function(aNode, aRangeParent, aRangeOffset) {
+ // Currently "isRemote" is always false.
+ //this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
+
+ const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ // Initialize contextual info.
+ this.onImage = false;
+ this.onLoadedImage = false;
+ this.onCompletedImage = false;
+ this.onStandaloneImage = false;
+ this.onCanvas = false;
+ this.onVideo = false;
+ this.onAudio = false;
+ this.onMetaDataItem = false;
+ this.onTextInput = false;
+ this.onNumeric = false;
+ this.onKeywordField = false;
+ this.mediaURL = "";
+ this.onLink = false;
+ this.onMailtoLink = false;
+ this.onSaveableLink = false;
+ this.inDirList = false;
+ this.link = null;
+ this.linkURL = "";
+ this.linkURI = null;
+ this.linkProtocol = "";
+ this.linkHasNoReferrer = false;
+ this.onMathML = false;
+ this.inFrame = false;
+ this.inSyntheticDoc = false;
+ this.hasBGImage = false;
+ this.bgImageURL = "";
+ this.autoDownload = false;
+ this.isTextSelected = false;
+ this.isContentSelected = false;
+ this.onEditableArea = false;
+ this.canSpellCheck = false;
+ this.onPassword = false;
+
+ // Remember the node that was clicked.
+ this.target = aNode;
+
+ if (aNode.nodeType == Node.DOCUMENT_NODE ||
+ // Not display on XUL element but relax for <label class="text-link">
+ (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) {
+ this.shouldDisplay = false;
+ return;
+ }
+
+ let editFlags = SpellCheckHelper.isEditable(this.target, window);
+ this.browser = this.target.ownerDocument.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ this.principal = this.target.ownerDocument.nodePrincipal;
+
+ this.autoDownload = Services.prefs.getBoolPref("browser.download.useDownloadDir");
+
+ // Check if we are in a synthetic document (stand alone image, video, etc.).
+ this.inSyntheticDoc = this.target.ownerDocument.mozSyntheticDocument;
+ // First, do checks for nodes that never have children.
+ if (this.target.nodeType == Node.ELEMENT_NODE) {
+ // See if the user clicked on an image.
+ 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;
+
+ if (this.target.ownerDocument instanceof ImageDocument)
+ this.onStandaloneImage = true;
+ }
+ else if (this.target instanceof HTMLCanvasElement) {
+ this.onCanvas = true;
+ }
+ else if (this.target instanceof HTMLVideoElement) {
+ // Gecko 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;
+
+ this.mediaURL = this.target.currentSrc || this.target.src;
+ }
+ else if (this.target instanceof HTMLAudioElement) {
+ this.onAudio = true;
+ this.mediaURL = this.target.currentSrc || this.target.src;
+ }
+ 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) {
+ InlineSpellCheckerUI.init(this.target.editor);
+ InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+ }
+ this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
+ }
+ else if ( this.target instanceof HTMLHtmlElement ) {
+ // pages with multiple <body>s are lame. we'll teach them a lesson.
+ var bodyElt = this.target.ownerDocument.body;
+ if (bodyElt) {
+ var computedURL = this.getComputedURL(bodyElt, "background-image");
+ if (computedURL) {
+ this.hasBGImage = true;
+ this.bgImageURL = makeURLAbsolute(bodyElt.baseURI, computedURL);
+ }
+ }
+ }
+ else if ("HTTPIndex" in content &&
+ content.HTTPIndex instanceof Ci.nsIHTTPIndex) {
+ this.inDirList = true;
+ // Bubble outward till we get to an element with URL attribute
+ // (which should be the href).
+ var root = this.target;
+ while (root && !this.link) {
+ if (root.tagName == "tree") {
+ // Hit root of tree; must have clicked in empty space;
+ // thus, no link.
+ break;
+ }
+
+ if (root.getAttribute("URL")) {
+ // Build pseudo link object so link-related functions work.
+ this.onLink = true;
+ this.link = {href: root.getAttribute("URL"),
+ getAttribute: function(attr) {
+ if (attr == "title") {
+ return root.firstChild.firstChild.getAttribute("label");
+ } else {
+ return "";
+ }
+ }
+ };
+ this.linkURL = this.getLinkURL();
+ this.linkURI = this.getLinkURI();
+ this.linkProtocol = this.getLinkProtocol();
+ this.onMailtoLink = (this.linkProtocol == "mailto");
+
+ // If element is a directory, then you can't save it.
+ this.onSaveableLink = root.getAttribute("container") != "true";
+ }
+ else {
+ root = root.parentNode;
+ }
+ }
+ }
+
+ this.canSpellCheck = this._isSpellCheckEnabled(this.target);
+ }
+ else 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);
+ }
+
+ // We have meta data on images.
+ this.onMetaDataItem = this.onImage;
+
+ // Bubble out, looking for items of interest
+ const NS_MathML = "http://www.w3.org/1998/Math/MathML";
+ const XMLNS = "http://www.w3.org/XML/1998/namespace";
+ var elem = this.target;
+ while (elem) {
+ if (elem.nodeType == Node.ELEMENT_NODE) {
+ // Link?
+ if (!this.onLink &&
+ (isXULTextLinkLabel(elem) ||
+ (elem instanceof HTMLAnchorElement && elem.href) ||
+ (elem instanceof HTMLAreaElement && elem.href) ||
+ elem instanceof HTMLLinkElement ||
+ (elem.namespaceURI == NS_MathML && elem.hasAttribute("href")) ||
+ elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) {
+ // Clicked on a link.
+ this.onLink = true;
+ this.onMetaDataItem = true;
+ // Remember corresponding element.
+ this.link = elem;
+ this.linkURL = this.getLinkURL();
+ this.linkURI = this.getLinkURI();
+ this.linkProtocol = this.getLinkProtocol();
+ this.onMailtoLink = (this.linkProtocol == "mailto");
+ // Remember if it is saveable.
+ this.onSaveableLink = this.isLinkSaveable();
+ this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
+ }
+
+ // Text input?
+ if (!this.onTextInput) {
+ // Clicked on a link.
+ this.onTextInput = this.isTargetATextBox(elem);
+ }
+
+ // Metadata item?
+ if (!this.onMetaDataItem) {
+ // We currently display metadata on anything which fits
+ // the below test.
+ if ((elem instanceof HTMLQuoteElement && elem.cite) ||
+ (elem instanceof HTMLTableElement && elem.summary) ||
+ (elem instanceof HTMLModElement && (elem.cite || elem.dateTime)) ||
+ (elem instanceof HTMLElement && (elem.title || elem.lang)) ||
+ elem.getAttributeNS(XMLNS, "lang")) {
+ dump("On metadata item.\n");
+ this.onMetaDataItem = true;
+ }
+ }
+
+ // 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) {
+ var bgImgUrl = this.getComputedURL(elem, "background-image");
+ if (bgImgUrl) {
+ this.hasBGImage = true;
+ this.bgImageURL = makeURLAbsolute(elem.baseURI, bgImgUrl);
+ }
+ }
+ }
+ elem = elem.parentNode;
+ }
+
+ // See if the user clicked on 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.target.ownerDocument.defaultView;
+ if (docDefaultView != docDefaultView.top)
+ this.inFrame = 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.hasBGImage = false;
+ this.onEditableArea = true;
+ var win = this.target.ownerDocument.defaultView;
+ var editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win));
+ InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
+ var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
+ this.showItem("spell-check-enabled", canSpell);
+ this.showItem("spell-separator", canSpell);
+ }
+ }
+
+ function isXULTextLinkLabel(node) {
+ return node.namespaceURI == xulNS &&
+ node.tagName == "label" &&
+ node.classList.contains('text-link') &&
+ node.href;
+ }
+ },
+
+ _isSpellCheckEnabled: function(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;
+ },
+
+ // Returns the computed style attribute for the given element.
+ getComputedStyle: function(aElem, aProp) {
+ return aElem.ownerDocument
+ .defaultView
+ .getComputedStyle(aElem, "").getPropertyValue(aProp);
+ },
+
+ // Returns a "url"-type computed style attribute value, with the url() stripped.
+ getComputedURL: function(aElem, aProp) {
+ var url = aElem.ownerDocument.defaultView
+ .getComputedStyle(aElem, "")
+ .getPropertyCSSValue(aProp);
+ if (url instanceof CSSPrimitiveValue)
+ url = [url];
+
+ for (var i = 0; i < url.length; i++)
+ if (url[i].primitiveType == CSSPrimitiveValue.CSS_URI)
+ return url[i].getStringValue();
+ return null;
+ },
+
+ // Returns true if clicked-on link targets a resource that can be saved.
+ isLinkSaveable: function() {
+ return this.linkProtocol && this.linkProtocol != "mailto" &&
+ this.linkProtocol != "javascript";
+ },
+
+ // Block/Unblock image from loading in the future.
+ toggleImageBlocking: function(aBlock) {
+ const uri = Services.io.newURI(this.mediaURL);
+ if (aBlock)
+ Services.perms.add(uri, "image", Services.perms.DENY_ACTION);
+ else
+ Services.perms.remove(uri, "image");
+ },
+
+ _openLinkInParameters : function (extra) {
+ let params = { charset: gContextMenuContentData.charSet,
+ originPrincipal: this.principal,
+ triggeringPrincipal: this.principal,
+ referrerURI: gContextMenuContentData.documentURIObject,
+ referrerPolicy: gContextMenuContentData.referrerPolicy,
+ noReferrer: this.linkHasNoReferrer || this.onPlainTextLink };
+ for (let p in extra) {
+ params[p] = extra[p];
+ }
+
+ // If we want to change userContextId, we must be sure that we don't
+ // propagate the referrer.
+ if ("userContextId" in params &&
+ params.userContextId != this.principal.originAttributes.userContextId) {
+ params.noReferrer = true;
+ }
+
+ return params;
+ },
+
+ // Open linked-to URL in a new tab.
+ openLinkInTab: function(aEvent) {
+ urlSecurityCheck(this.linkURL, this.principal);
+ let referrerURI = gContextMenuContentData.documentURIObject;
+
+ // if the mixedContentChannel is present 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 (this.browser.docShell.mixedContentChannel) {
+ const sm = Services.scriptSecurityManager;
+ try {
+ let targetURI = this.linkURI;
+ sm.checkSameOriginURI(referrerURI, targetURI, false);
+ persistAllowMixedContentInChildTab = true;
+ }
+ catch (e) { }
+ }
+
+ let params = {
+ allowMixedContent: persistAllowMixedContentInChildTab,
+ userContextId: parseInt(aEvent.target.getAttribute('usercontextid')),
+ };
+
+ openLinkIn(this.linkURL,
+ aEvent && aEvent.shiftKey ? "tabshifted" : "tab",
+ this._openLinkInParameters(params));
+ },
+
+ // Open linked-to URL in a new window.
+ openLinkInWindow: function() {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "window", this._openLinkInParameters());
+ },
+
+ // Open linked-to URL in a private window.
+ openLinkInPrivateWindow: function() {
+ urlSecurityCheck(this.linkURL, this.principal);
+ openLinkIn(this.linkURL, "window",
+ this._openLinkInParameters({ private: true }));
+ },
+
+ // Open frame in a new tab.
+ openFrameInTab: function(aEvent) {
+ let referrer = gContextMenuContentData.referrer;
+ openLinkIn(gContextMenuContentData.docLocation,
+ aEvent && aEvent.shiftKey ? "tabshifted" : "tab",
+ { charset: gContextMenuContentData.charSet,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ // Reload clicked-in frame.
+ reloadFrame: function() {
+ this.target.ownerDocument.location.reload();
+ },
+
+ // Open clicked-in frame in its own window.
+ openFrame: function() {
+ let referrer = gContextMenuContentData.referrer;
+ openLinkIn(gContextMenuContentData.docLocation, "window",
+ { charset: gContextMenuContentData.charSet,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ printFrame: function() {
+ PrintUtils.printWindow(gContextMenuContentData.frameOuterWindowID,
+ this.browser);
+ },
+
+ // Open clicked-in frame in the same window
+ showOnlyThisFrame: function() {
+ urlSecurityCheck(gContextMenuContentData.docLocation,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ let referrer = gContextMenuContentData.referrer;
+ openUILinkIn(gContextMenuContentData.docLocation, "current",
+ { disallowInheritPrincipal: true,
+ referrerURI: referrer ? makeURI(referrer) : null });
+ },
+
+ // View Partial Source
+ viewPartialSource: function(aContext) {
+ var browser = getBrowser().selectedBrowser;
+ var target = aContext == "mathml" ? this.target : null;
+ gViewSourceUtils.viewPartialSourceInBrowser(browser, target, null);
+ },
+
+ // Open new "view source" window with the frame's URL.
+ viewFrameSource: function() {
+ gViewSourceUtils.viewSource({
+ browser: this.browser,
+ URL: gContextMenuContentData.docLocation,
+ outerWindowID: gContextMenuContentData.frameOuterWindowID,
+ });
+ },
+
+ viewInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, null,
+ null, null, this.browser);
+ },
+
+ viewImageInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
+ this.target, null, this.browser);
+ },
+
+ viewFrameInfo: function() {
+ BrowserPageInfo(gContextMenuContentData.docLocation, null, null,
+ gContextMenuContentData.frameOuterWindowID, this.browser);
+ },
+
+ toggleImageSize: function() {
+ content.document.toggleImageSize();
+ },
+
+ // Reload image
+ reloadImage: function() {
+ urlSecurityCheck(this.mediaURL,
+ this.target.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ if (this.target instanceof Ci.nsIImageLoadingContent)
+ this.target.forceReload();
+ },
+
+ // Change current window to the URL of the image, video, or audio.
+ viewMedia(e) {
+ let doc = this.target.ownerDocument;
+ let where = whereToOpenLink(e);
+
+ if (this.onCanvas) {
+ let systemPrincipal = Services.scriptSecurityManager
+ .getSystemPrincipal();
+ this.target.toBlob((blob) => {
+ openUILinkIn(URL.createObjectURL(blob), where,
+ { referrerURI: doc.documentURIObject,
+ triggeringPrincipal: systemPrincipal,
+ });
+ });
+ } else {
+ urlSecurityCheck(this.mediaURL,
+ this.target.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ openUILinkIn(this.mediaURL, where,
+ { referrerURI: doc.documentURIObject,
+ triggeringPrincipal: this.target.nodePrincipal,
+ });
+ }
+ },
+
+ saveVideoFrameAsImage: function () {
+ urlSecurityCheck(this.mediaURL, this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+ var name = "snapshot.jpg";
+ try {
+ let uri = makeURI(this.mediaURL);
+ let url = uri.QueryInterface(Ci.nsIURL);
+ if (url.fileBaseName)
+ name = decodeURI(url.fileBaseName) + ".jpg";
+ } catch (e) { }
+ var video = this.target;
+ var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ var ctxDraw = canvas.getContext("2d");
+ ctxDraw.drawImage(video, 0, 0);
+ saveImageURL(canvas.toDataURL("image/jpeg", ""), name, "SaveImageTitle",
+ true, true,
+ this.target.ownerDocument.documentURIObject,
+ null, null, null, (gPrivate ? true : false),
+ this.principal);
+ },
+
+ // Full screen video playback
+ fullScreenVideo: function() {
+ var isPaused = this.target.paused && this.target.currentTime > 0;
+ this.target.pause();
+
+ openDialog("chrome://communicator/content/fullscreen-video.xhtml",
+ "", "chrome,centerscreen,dialog=no", this.target, isPaused);
+ },
+
+ // Change current window to the URL of the background image.
+ viewBGImage(e) {
+ urlSecurityCheck(this.bgImageURL,
+ this.target.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ let doc = this.target.ownerDocument;
+ let where = whereToOpenLink(e);
+ openUILinkIn(this.bgImageURL, where,
+ { referrerURI: doc.documentURIObject,
+ triggeringPrincipal: this.target.nodePrincipal,
+ });
+ },
+
+ setDesktopBackground: function() {
+ let url = (new URL(this.target.ownerDocument.location.href)).pathname;
+ let imageName = url.substr(url.lastIndexOf("/") + 1);
+ openDialog("chrome://communicator/content/setDesktopBackground.xul",
+ "_blank", "chrome,modal,titlebar,centerscreen", this.target,
+ imageName);
+ },
+
+ // Save URL of clicked-on frame.
+ saveFrame: function() {
+ saveDocument(this.target.ownerDocument, true);
+ },
+
+ // Save URL of clicked-on link.
+ saveLink: function() {
+ var doc = this.target.ownerDocument;
+ urlSecurityCheck(this.linkURL, this.principal);
+ this.saveHelper(this.linkURL, this.linkText(), null, true, doc);
+ },
+
+ // Helper function to wait for appropriate MIME-type headers and
+ // then prompt the user with a file picker
+ saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc) {
+ // 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 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;
+
+ clearTimeout(timer);
+
+ // 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");
+
+ Services.prompt.alert(doc.defaultView, title, msg);
+ } catch (ex) {}
+ return;
+ }
+
+ var extHelperAppSvc =
+ Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Ci.nsIExternalHelperAppService);
+ var channel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.extListener = extHelperAppSvc.doContent(channel.contentType, aRequest,
+ doc.defaultView, true);
+ this.extListener.onStartRequest(aRequest, aContext);
+ },
+
+ onStopRequest: function 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, true, doc.documentURIObject, doc);
+ }
+ if (this.extListener)
+ this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+ },
+
+ onDataAvailable: function onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount) {
+ this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
+ aOffset, aCount);
+ }
+ }
+
+ function Callbacks() {}
+ Callbacks.prototype = {
+ getInterface: function 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 we don't have the headers after a short time the user won't have
+ // received any feedback from the click. That's bad, so we give up
+ // waiting for the filename.
+ function timerCallback() {
+ channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+ }
+
+ // set up a channel to do the saving
+ var channel = NetUtil.newChannel({
+ uri: makeURI(linkURL),
+ loadUsingSystemPrincipal: true,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL
+ });
+
+ channel.notificationCallbacks = new Callbacks();
+
+ var 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.nsIPrivateBrowsingChannel)
+ channel.setPrivate(gPrivate);
+
+ if (channel instanceof Ci.nsIHttpChannel) {
+ channel.referrer = doc.documentURIObject;
+ 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 = setTimeout(timerCallback, timeToWait);
+
+ // kick off the channel with our proxy object as the listener
+ channel.asyncOpen2(new SaveAsListener());
+ },
+
+ // Save URL of clicked-on image, video, or audio.
+ saveMedia: function() {
+ var doc = this.target.ownerDocument;
+ let referrerURI = doc.documentURIObject;
+
+ if (this.onCanvas)
+ // Bypass cache, since it's a data: URL.
+ saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
+ true, false, referrerURI, null, null, null,
+ (gPrivate ? true : false),
+ document.nodePrincipal /* system, because blob: */);
+ else if (this.onImage) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
+ false, referrerURI, null, gContextMenuContentData.contentType,
+ gContextMenuContentData.contentDisposition,
+ (gPrivate ? true : false),
+ this.principal);
+ }
+ 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);
+ }
+ },
+
+ // Backwards-compatibility wrapper
+ saveImage: function() {
+ if (this.onCanvas || this.onImage)
+ this.saveMedia();
+ },
+
+ // Generate email address.
+ getEmail: function() {
+ // Get 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 addresses;
+ try {
+ // Let's try to unescape it using a character set
+ var characterSet = this.target.ownerDocument.characterSet;
+ const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"]
+ .getService(Ci.nsITextToSubURI);
+ addresses = this.linkURL.match(/^mailto:([^?]+)/)[1];
+ addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses);
+ }
+ catch(ex) {
+ // Do nothing.
+ }
+ return addresses;
+ },
+
+ // Copy email to clipboard
+ copyEmail: function() {
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this.getEmail());
+ },
+
+ bookmarkThisPage : function() {
+ window.top.PlacesCommandHook.bookmarkPage(this.browser,
+ true);
+ },
+
+ bookmarkLink: function CM_bookmarkLink() {
+ window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
+ this.linkURL,
+ this.linkText());
+ },
+
+ addBookmarkForFrame: function() {
+ var doc = this.target.ownerDocument;
+ var uri = doc.documentURIObject;
+
+ var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
+ if (itemId == -1) {
+ var title = doc.title;
+ var description = PlacesUIUtils.getDescriptionFromDocument(doc);
+ PlacesUIUtils.showMinimalAddBookmarkUI(uri, title, description);
+ }
+ else
+ PlacesUIUtils.showItemProperties(itemId,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ },
+
+ // Open Metadata window for node
+ showMetadata: function() {
+ window.openDialog("chrome://navigator/content/metadata.xul",
+ "_blank",
+ "scrollbars,resizable,chrome,dialog=no",
+ this.target);
+ },
+
+ ///////////////
+ // Utilities //
+ ///////////////
+
+ // Show/hide one item (specified via name or the item element itself).
+ showItem: function(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: function(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: function(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: function(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;
+ },
+
+ // Generate fully qualified URL for clicked-on link.
+ getLinkURL: function() {
+ if (this.link.href)
+ return this.link.href;
+
+ var href;
+ if (this.link.namespaceURI == "http://www.w3.org/1998/Math/MathML")
+ href = this.link.getAttribute("href");
+
+ if (!href)
+ 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: function() {
+ try {
+ return makeURI(this.linkURL);
+ }
+ catch (ex) {
+ // e.g. empty URL string
+ }
+
+ return null;
+ },
+
+ getLinkProtocol: function() {
+ if (this.linkURI)
+ return this.linkURI.scheme; // can be |undefined|
+
+ return null;
+ },
+
+ // Get text of link.
+ linkText: function() {
+ var text = gatherTextUnder(this.link);
+ if (text && text.match(/\S/))
+ return text;
+
+ text = this.link.getAttribute("title");
+ if (text && text.match(/\S/))
+ return text;
+
+ text = this.link.getAttribute("alt");
+ if (text && text.match(/\S/))
+ return text;
+
+ if (this.link.href)
+ return this.link.href;
+
+ if (elem.namespaceURI == "http://www.w3.org/1998/Math/MathML")
+ text = elem.getAttribute("href");
+ if (!text || !text.match(/\S/))
+ text = elem.getAttributeNS("http://www.w3.org/1999/xlink", "href");
+ if (text && text.match(/\S/))
+ return makeURLAbsolute(this.link.baseURI, text);
+
+ return null;
+ },
+
+ /**
+ * Determines whether the focused window has selected text, and if so
+ * formats the first 15 characters for the label of the context-searchselect
+ * element according to the searchText string.
+ * @return true if there is selected text, false if not
+ */
+ isTextSelection: function() {
+ var searchSelectText = this.searchSelected(16);
+
+ if (!searchSelectText)
+ return false;
+
+ if (searchSelectText.length > 15)
+ searchSelectText = searchSelectText.substr(0, 15) + this.ellipsis;
+
+ // Use the current engine if it's a browser window and the search bar is
+ // visible, the default engine otherwise.
+ var engineName = "";
+ if (window.BrowserSearch &&
+ (isElementVisible(BrowserSearch.searchBar) ||
+ BrowserSearch.searchSidebar))
+ engineName = Services.search.currentEngine.name;
+ else
+ engineName = Services.search.defaultEngine.name;
+
+ // format "Search <engine> for <selection>" string to show in menu
+ const bundle = document.getElementById("contentAreaCommandsBundle");
+ var menuLabel = bundle.getFormattedString("searchSelected",
+ [engineName, searchSelectText]);
+ this.setItemAttr("context-searchselect", "label", menuLabel);
+ this.setItemAttr("context-searchselect", "accesskey",
+ bundle.getString("searchSelected.accesskey"));
+
+ return true;
+ },
+
+ searchSelected: function(aCharlen) {
+ var focusedWindow = document.commandDispatcher.focusedWindow;
+ var searchStr = focusedWindow.getSelection();
+ searchStr = searchStr.toString();
+
+ if (this.onTextInput) {
+ var fElem = this.target;
+ if ((fElem instanceof HTMLInputElement &&
+ fElem.mozIsTextField(true)) ||
+ fElem instanceof HTMLTextAreaElement) {
+ searchStr = fElem.value.substring(fElem.selectionStart, fElem.selectionEnd);
+ }
+ }
+
+ // searching for more than 150 chars makes no sense
+ if (!aCharlen)
+ aCharlen = 150;
+ if (aCharlen < searchStr.length) {
+ // only use the first charlen important chars. see bug 221361
+ var pattern = new RegExp("^(?:\\s*.){0," + aCharlen + "}");
+ pattern.test(searchStr);
+ searchStr = RegExp.lastMatch;
+ }
+
+ return searchStr.trim().replace(/\s+/g, " ");
+ },
+
+ // Returns true if anything is selected.
+ isContentSelection: function() {
+ return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed;
+ },
+
+ // Returns true if the target is editable
+ isTargetEditable: function() {
+ if (this.target.ownerDocument.designMode == "on")
+ return true;
+
+ for (var node = this.target; node; node = node.parentNode)
+ if (node.nodeType == node.ELEMENT_NODE &&
+ node.namespaceURI == "http://www.w3.org/1999/xhtml")
+ return node.isContentEditable;
+ return false;
+ },
+
+ toString: function() {
+ 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";
+ },
+
+ isTextBoxEnabled: function(aNode) {
+ return !aNode.ownerDocument.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .isNodeDisabledForEvents(aNode);
+ },
+
+ isTargetATextBox: function(aNode) {
+ if (aNode instanceof HTMLInputElement)
+ return aNode.mozIsTextField(false) && this.isTextBoxEnabled(aNode);
+
+ return aNode instanceof HTMLTextAreaElement && this.isTextBoxEnabled(aNode);
+ },
+
+ /**
+ * Determine whether a separator should be shown based on whether
+ * there are any non-hidden items between it and the previous separator.
+ * @param aSeparatorID
+ * The id of the separator element
+ * @return true if the separator should be shown, false if not
+ */
+ shouldShowSeparator: function(aSeparatorID) {
+ let separator = document.getElementById(aSeparatorID);
+ if (separator) {
+ let sibling = separator.previousSibling;
+ while (sibling && sibling.localName != "menuseparator") {
+ if (sibling.getAttribute("hidden") != "true")
+ return true;
+ sibling = sibling.previousSibling;
+ }
+ }
+ return false;
+ },
+
+ mediaCommand: function(aCommand, aData) {
+ var media = this.target;
+
+ switch (aCommand) {
+ 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 = aData;
+ break;
+ case "hidecontrols":
+ media.removeAttribute("controls");
+ break;
+ case "showcontrols":
+ media.setAttribute("controls", "true");
+ break;
+ case "showstats":
+ case "hidestats":
+ var win = media.ownerDocument.defaultView;
+ var showing = aCommand == "showstats";
+ media.dispatchEvent(new win.CustomEvent("media-showStatistics",
+ { bubbles: false, cancelable: true, detail: showing }));
+ break;
+ }
+ },
+
+ copyMediaLocation: function() {
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this.mediaURL);
+ },
+
+ get imageURL() {
+ if (this.onImage)
+ return this.mediaURL;
+ return "";
+ }
+};
+
+XPCOMUtils.defineLazyGetter(nsContextMenu.prototype, "ellipsis", function() {
+ return Services.prefs.getComplexValue("intl.ellipsis",
+ Ci.nsIPrefLocalizedString).data;
+});