diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/base/content/nsContextMenu.js | |
parent | Initial commit. (diff) | |
download | thunderbird-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.js | 1676 |
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; +}); |