/* -*- 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/. Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm"); Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm"); Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", "resource://gre/modules/LoginHelper.jsm"); var gContextMenuContentData = null; function nsContextMenu(aXulMenu, aIsShift) { this.shouldDisplay = true; this.initMenu(aXulMenu, aIsShift); } // Prototype for nsContextMenu "class." nsContextMenu.prototype = { initMenu: function CM_initMenu(aXulMenu, aIsShift) { // Get contextual info. this.setTarget(document.popupNode, document.popupRangeParent, document.popupRangeOffset); 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, onEditableArea: this.onEditableArea, srcUrl: this.mediaURL, frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined, pageUrl: this.browser ? this.browser.currentURI.spec : undefined, linkUrl: this.linkURL, selectionText: this.isTextSelected ? this.selectionInfo.text : undefined, }; subject.wrappedJSObject = subject; Services.obs.notifyObservers(subject, "on-build-contextmenu", null); } this.isFrameImage = document.getElementById("isFrameImage"); this.ellipsis = "\u2026"; try { this.ellipsis = gPrefService.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(); }, hiding: function CM_hiding() { gContextMenuContentData = null; InlineSpellCheckerUI.clearSuggestionsFromMenu(); InlineSpellCheckerUI.clearDictionaryListFromMenu(); InlineSpellCheckerUI.uninit(); 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(); }, 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 shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink; var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); this.showItem("context-openlink", shouldShow && !isWindowPrivate); this.showItem("context-openlinkprivate", shouldShow); this.showItem("context-openlinkintab", shouldShow); 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.showItem("context-navigation", shouldShow); this.showItem("context-sep-navigation", shouldShow); let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true"; let stopReloadItem = ""; if (shouldShow) { stopReloadItem = (stopped) ? "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.fullscreenElement != null); 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 = gPrefService.getBoolPref("devtools.inspector.enabled"); this.showItem("context-viewsource", shouldShow); this.showItem("context-viewinfo", shouldShow); this.showItem("inspect-separator", showInspect); this.showItem("context-inspect", showInspect); 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; #ifdef HAVE_SHELL_SERVICE // Only enable Set as Desktop Background if we can get the shell service. var shell = getShellService(); if (shell) haveSetDesktopBackground = shell.canSetDesktopBackground; #endif 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); 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)); bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext")); this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink) || 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: 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); 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.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-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)); #ifdef CONTEXT_COPY_IMAGE_CONTENTS // Copy image contents depends on whether we're on an image. this.showItem("context-copyimage-contents", this.onImage); #endif // 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: 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", this.target.controls && (this.onVideo || (this.onAudio && !this.inSyntheticDoc))); this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.fullscreenElement == null); 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: function() { this.showItem("context-ctp-play", this.onCTPPlugin); this.showItem("context-ctp-hide", this.onCTPPlugin); this.showItem("context-sep-ctp", this.onCTPPlugin); }, initPasswordManagerItems: function() { 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.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); }, inspectNode: function() { let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {}); let gBrowser = this.browser.ownerGlobal.gBrowser; let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); return gDevTools.showToolbox(target, "inspector").then(toolbox => { let inspector = toolbox.getCurrentPanel(); // new-node-front tells us when the node has been selected, whether the // browser is remote or not. let onNewNode = inspector.selection.once("new-node-front"); this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, {node: this.target}); inspector.walker.findInspectingNode().then(nodeFront => { inspector.selection.setNodeFront(nodeFront, "browser-context-menu"); }); return onNewNode.then(() => { // Now that the node has been selected, wait until the inspector is // fully updated. return inspector.once("inspector-updated"); }); }); }, // Set various context menu attributes based on the state of the world. setTarget: function (aNode, aRangeParent, aRangeOffset) { // gContextMenuContentData.isRemote tells us if the event came from a remote // process. gContextMenuContentData can be null if something (like tests) // opens the context menu directly. let editFlags; this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote; if (this.isRemote) { aNode = gContextMenuContentData.event.target; aRangeParent = gContextMenuContentData.event.rangeParent; aRangeOffset = gContextMenuContentData.event.rangeOffset; editFlags = gContextMenuContentData.editFlags; } const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; if (aNode.nodeType == Node.DOCUMENT_NODE || // Not display on XUL element but relax for