/** * 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/. */ /* import-globals-from folderDisplay.js */ /* import-globals-from mailTabs.js */ /* import-globals-from mailWindow.js */ /* import-globals-from messageDisplay.js */ /* import-globals-from utilityOverlay.js */ /* global EnigmailURIs: false, gEncryptedURIService: true */ var { InlineSpellChecker, SpellCheckHelper } = ChromeUtils.importESModule( "resource://gre/modules/InlineSpellChecker.sys.mjs" ); var { PlacesUtils } = ChromeUtils.importESModule( "resource://gre/modules/PlacesUtils.sys.mjs" ); var { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); ChromeUtils.defineModuleGetter( this, "MailUtils", "resource:///modules/MailUtils.jsm" ); var { E10SUtils } = ChromeUtils.importESModule( "resource://gre/modules/E10SUtils.sys.mjs" ); var gSpellChecker = new InlineSpellChecker(); /** Called by ContextMenuParent.sys.mjs */ function openContextMenu({ data }, browser, actor) { if (!browser.hasAttribute("context")) { return; } let spellInfo = data.spellInfo; let frameReferrerInfo = data.frameReferrerInfo; let linkReferrerInfo = data.linkReferrerInfo; let principal = data.principal; let storagePrincipal = data.storagePrincipal; let documentURIObject = makeURI( data.docLocation, data.charSet, makeURI(data.baseURI) ); if (frameReferrerInfo) { frameReferrerInfo = E10SUtils.deserializeReferrerInfo(frameReferrerInfo); } if (linkReferrerInfo) { linkReferrerInfo = E10SUtils.deserializeReferrerInfo(linkReferrerInfo); } nsContextMenu.contentData = { context: data.context, browser, actor, editFlags: data.editFlags, spellInfo, principal, storagePrincipal, documentURIObject, docLocation: data.docLocation, charSet: data.charSet, referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo), frameReferrerInfo, linkReferrerInfo, contentType: data.contentType, contentDisposition: data.contentDisposition, frameID: data.frameID, frameOuterWindowID: data.frameID, frameBrowsingContext: BrowsingContext.get(data.frameBrowsingContextID), selectionInfo: data.selectionInfo, disableSetDesktopBackground: data.disableSetDesktopBackground, loginFillInfo: data.loginFillInfo, parentAllowsMixedContent: data.parentAllowsMixedContent, userContextId: data.userContextId, webExtContextData: data.webExtContextData, }; let popup = browser.ownerDocument.getElementById( browser.getAttribute("context") ); let context = nsContextMenu.contentData.context; // We don't have access to the original event here, as that happened in // another process. Therefore we synthesize a new MouseEvent to propagate the // inputSource to the subsequently triggered popupshowing event. let newEvent = document.createEvent("MouseEvent"); let screenX = context.screenXDevPx / window.devicePixelRatio; let screenY = context.screenYDevPx / window.devicePixelRatio; newEvent.initNSMouseEvent( "contextmenu", true, true, null, 0, screenX, screenY, 0, 0, false, false, false, false, 0, null, 0, context.mozInputSource ); popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent); } /** Called by a popupshowing event via fillMailContextMenu x3. */ class nsContextMenu { constructor(aXulMenu, aIsShift) { this.xulMenu = aXulMenu; // Get contextual info. this.setContext(); if (!this.shouldDisplay) { return; } // Message Related Items this.inAMessage = false; this.inThreadPane = false; this.inStandaloneWindow = false; this.numSelectedMessages = 0; this.isNewsgroup = false; this.hideMailItems = false; this.isContentSelected = !this.selectionInfo || !this.selectionInfo.docSelectionIsCollapsed; this.setMessageTargets(); if (!aIsShift) { // The rest of this block sends menu information to WebExtensions. let subject = { menu: aXulMenu, tab: document.getElementById("tabmail") ? document.getElementById("tabmail").currentTabInfo : window, timeStamp: this.timeStamp, isContentSelected: this.isContentSelected, inFrame: this.inFrame, isTextSelected: this.isTextSelected, onTextInput: this.onTextInput, onLink: this.onLink, onImage: this.onImage, onVideo: this.onVideo, onAudio: this.onAudio, onCanvas: this.onCanvas, onEditable: this.onEditable, onSpellcheckable: this.onSpellcheckable, onPassword: this.onPassword, srcUrl: this.mediaURL, frameUrl: this.contentData ? this.contentData.docLocation : undefined, pageUrl: this.browser ? this.browser.currentURI.spec : undefined, linkText: this.linkTextStr, linkUrl: this.linkURL, selectionText: this.isTextSelected ? this.selectionInfo.fullText : undefined, frameId: this.frameID, webExtBrowserType: this.webExtBrowserType, webExtContextData: this.contentData ? this.contentData.webExtContextData : undefined, }; if (this.inThreadPane) { subject.displayedFolder = gFolderDisplay.view.displayedFolder; subject.selectedMessages = gFolderDisplay.selectedMessages; } subject.wrappedJSObject = subject; Services.obs.notifyObservers(subject, "on-build-contextmenu"); } // Reset after "on-build-contextmenu" notification in case selection was // changed during the notification. this.isContentSelected = !this.selectionInfo || !this.selectionInfo.docSelectionIsCollapsed; this.initItems(); // If all items in the menu are hidden, set this.shouldDisplay to false // so that the callers know to not even display the empty menu. let contextPopup = document.getElementById("mailContext"); for (let item of contextPopup.children) { if (!item.hidden) { return; } } // All items must have been hidden. this.shouldDisplay = false; } setContext() { let context = Object.create(null); if (nsContextMenu.contentData) { this.contentData = nsContextMenu.contentData; context = this.contentData.context; nsContextMenu.contentData = null; } this.shouldDisplay = !this.contentData || context.shouldDisplay; this.timeStamp = context.timeStamp; // Assign what's _possibly_ needed from `context` sent by ContextMenuChild.sys.mjs // Keep this consistent with the similar code in ContextMenu's _setContext this.bgImageURL = context.bgImageURL; this.imageDescURL = context.imageDescURL; this.imageInfo = context.imageInfo; this.mediaURL = context.mediaURL; this.canSpellCheck = context.canSpellCheck; this.hasBGImage = context.hasBGImage; this.hasMultipleBGImages = context.hasMultipleBGImages; this.isDesignMode = context.isDesignMode; this.inFrame = context.inFrame; this.inPDFViewer = context.inPDFViewer; this.inSrcdocFrame = context.inSrcdocFrame; this.inSyntheticDoc = context.inSyntheticDoc; this.link = context.link; this.linkDownload = context.linkDownload; this.linkProtocol = context.linkProtocol; this.linkTextStr = context.linkTextStr; this.linkURL = context.linkURL; this.linkURI = this.getLinkURI(); // can't send; regenerate this.onAudio = context.onAudio; this.onCanvas = context.onCanvas; this.onCompletedImage = context.onCompletedImage; this.onDRMMedia = context.onDRMMedia; this.onPiPVideo = context.onPiPVideo; this.onEditable = context.onEditable; this.onImage = context.onImage; this.onKeywordField = context.onKeywordField; this.onLink = context.onLink; this.onLoadedImage = context.onLoadedImage; this.onMailtoLink = context.onMailtoLink; this.onMozExtLink = context.onMozExtLink; this.onNumeric = context.onNumeric; this.onPassword = context.onPassword; this.onSaveableLink = context.onSaveableLink; this.onSpellcheckable = context.onSpellcheckable; this.onTextInput = context.onTextInput; this.onVideo = context.onVideo; this.target = context.target; this.targetIdentifier = context.targetIdentifier; this.principal = context.principal; this.storagePrincipal = context.storagePrincipal; this.frameID = context.frameID; this.frameOuterWindowID = context.frameOuterWindowID; this.frameBrowsingContext = BrowsingContext.get( context.frameBrowsingContextID ); this.inSyntheticDoc = context.inSyntheticDoc; this.inAboutDevtoolsToolbox = context.inAboutDevtoolsToolbox; // Everything after this isn't sent directly from ContextMenu if (this.target) { this.ownerDoc = this.target.ownerDocument; } this.csp = E10SUtils.deserializeCSP(context.csp); if (!this.contentData) { return; } this.browser = this.contentData.browser; if (this.browser && this.browser.currentURI.spec == "about:blank") { this.shouldDisplay = false; return; } this.selectionInfo = this.contentData.selectionInfo; this.actor = this.contentData.actor; this.textSelected = this.selectionInfo?.text; this.isTextSelected = !!this.textSelected?.length; this.webExtBrowserType = this.browser.getAttribute( "webextension-view-type" ); if (context.shouldInitInlineSpellCheckerUINoChildren) { gSpellChecker.initFromRemote( this.contentData.spellInfo, this.actor.manager ); } if (this.contentData.spellInfo) { this.spellSuggestions = this.contentData.spellInfo.spellSuggestions; } if (context.shouldInitInlineSpellCheckerUIWithChildren) { gSpellChecker.initFromRemote( this.contentData.spellInfo, this.actor.manager ); let canSpell = gSpellChecker.canSpellCheck && this.canSpellCheck; this.showItem("mailContext-spell-check-enabled", canSpell); this.showItem("mailContext-spell-separator", canSpell); } } hiding() { if (this.actor) { this.actor.hiding(); } this.contentData = null; gSpellChecker.clearSuggestionsFromMenu(); gSpellChecker.clearDictionaryListFromMenu(); gSpellChecker.uninit(); } initItems() { this.initSaveItems(); this.initClipboardItems(); this.initMediaPlayerItems(); this.initBrowserItems(); this.initMessageItems(); this.initSpellingItems(); this.initSeparators(); } addDictionaries() { openDictionaryList(); } initSpellingItems() { let canSpell = gSpellChecker.canSpellCheck && !gSpellChecker.initialSpellCheckPending && this.canSpellCheck; let showDictionaries = canSpell && gSpellChecker.enabled; let onMisspelling = gSpellChecker.overMisspelling; let showUndo = canSpell && gSpellChecker.canUndo(); this.showItem("mailContext-spell-check-enabled", canSpell); this.showItem("mailContext-spell-separator", canSpell); document .getElementById("mailContext-spell-check-enabled") .setAttribute("checked", canSpell && gSpellChecker.enabled); this.showItem("mailContext-spell-add-to-dictionary", onMisspelling); this.showItem("mailContext-spell-undo-add-to-dictionary", showUndo); // suggestion list this.showItem( "mailContext-spell-suggestions-separator", onMisspelling || showUndo ); if (onMisspelling) { let addMenuItem = document.getElementById( "mailContext-spell-add-to-dictionary" ); let suggestionCount = gSpellChecker.addSuggestionsToMenu( addMenuItem.parentNode, addMenuItem, this.spellSuggestions ); this.showItem("mailContext-spell-no-suggestions", suggestionCount == 0); } else { this.showItem("mailContext-spell-no-suggestions", false); } // dictionary list this.showItem("mailContext-spell-dictionaries", showDictionaries); if (canSpell) { let dictMenu = document.getElementById( "mailContext-spell-dictionaries-menu" ); let dictSep = document.getElementById( "mailContext-spell-language-separator" ); let count = gSpellChecker.addDictionaryListToMenu(dictMenu, dictSep); this.showItem(dictSep, count > 0); this.showItem("mailContext-spell-add-dictionaries-main", false); } else if (this.onSpellcheckable) { // when there is no spellchecker but we might be able to spellcheck // add the add to dictionaries item. This will ensure that people // with no dictionaries will be able to download them this.showItem("mailContext-spell-language-separator", showDictionaries); this.showItem( "mailContext-spell-add-dictionaries-main", showDictionaries ); } else { this.showItem("mailContext-spell-add-dictionaries-main", false); } } initSaveItems() { this.showItem("mailContext-savelink", this.onSaveableLink); this.showItem("mailContext-saveimage", this.onLoadedImage); } initClipboardItems() { // Copy depends on whether there is selected text. // Enabling this context menu item is now done through the global // command updating system. goUpdateGlobalEditMenuItems(); this.showItem("mailContext-cut", !this.inAMessage && this.onTextInput); this.showItem( "mailContext-copy", !this.inThreadPane && !this.onPlayableMedia && (this.isContentSelected || this.onTextInput) ); this.showItem("mailContext-paste", !this.inAMessage && this.onTextInput); this.showItem("mailContext-undo", !this.inAMessage && this.onTextInput); // Select all not available in the thread pane or on playable media. this.showItem( "mailContext-selectall", !this.inThreadPane && !this.onPlayableMedia ); this.showItem("mailContext-copyemail", this.onMailtoLink); this.showItem("mailContext-copylink", this.onLink && !this.onMailtoLink); this.showItem("mailContext-copyimage", this.onImage); this.showItem( "mailContext-composeemailto", this.onMailtoLink && !this.inThreadPane ); this.showItem( "mailContext-addemail", this.onMailtoLink && !this.inThreadPane ); let searchTheWeb = document.getElementById("mailContext-searchTheWeb"); this.showItem( searchTheWeb, !this.inThreadPane && !this.onPlayableMedia && this.isContentSelected ); if (!searchTheWeb.hidden) { let selection = document.commandDispatcher.focusedWindow .getSelection() .toString(); let bundle = document.getElementById("bundle_messenger"); let key = "openSearch.label"; let abbrSelection; if (selection.length > 15) { key += ".truncated"; abbrSelection = selection.slice(0, 15); } else { abbrSelection = selection; } searchTheWeb.label = bundle.getFormattedString(key, [ Services.search.defaultEngine.name, abbrSelection, ]); searchTheWeb.value = selection; } } initMediaPlayerItems() { let onMedia = this.onVideo || this.onAudio; // Several mutually exclusive items.... play/pause, mute/unmute, show/hide this.showItem("mailContext-media-play", onMedia && this.target.paused); this.showItem("mailContext-media-pause", onMedia && !this.target.paused); this.showItem("mailContext-media-mute", onMedia && !this.target.muted); this.showItem("mailContext-media-unmute", onMedia && this.target.muted); if (onMedia) { let hasError = this.target.error != null || this.target.networkState == this.target.NETWORK_NO_SOURCE; this.setItemAttr("mailContext-media-play", "disabled", hasError); this.setItemAttr("mailContext-media-pause", "disabled", hasError); this.setItemAttr("mailContext-media-mute", "disabled", hasError); this.setItemAttr("mailContext-media-unmute", "disabled", hasError); } } initBrowserItems() { // Work out if we are a context menu on a special item e.g. an image, link // etc. let notOnSpecialItem = !( this.inAMessage || this.isContentSelected || this.onCanvas || this.onLink || this.onImage || this.onAudio || this.onVideo || this.onTextInput ); // Ensure these commands are updated with their current status. if (notOnSpecialItem) { goUpdateCommand("cmd_stop"); goUpdateCommand("cmd_reload"); } // These only needs showing if we're not on something special. this.showItem("mailContext-stop", notOnSpecialItem); this.showItem("mailContext-reload", notOnSpecialItem); let loadedProtocol = ""; if (this.target && this.target.ownerGlobal?.top.location) { loadedProtocol = this.target.ownerGlobal?.top.location.protocol; } // Only show open in browser if we're not on a special item and we're not // on an about: or chrome: protocol - for these protocols the browser is // unlikely to show the same thing as we do (if at all), so therefore don't // offer the option. this.showItem( "mailContext-openInBrowser", notOnSpecialItem && ["http:", "https:"].includes(loadedProtocol) ); // Only show mailContext-openLinkInBrowser if we're on a link and it isn't // a mailto link. this.showItem( "mailContext-openLinkInBrowser", this.onLink && ["http", "https"].includes(this.linkProtocol) ); } /* eslint-disable complexity */ initMessageItems() { // If we're not in a message related tab, we're just going to bulk hide most // items as this simplifies the logic below. if (!this.inAMessage) { const messageTabSpecificItems = [ "mailContext-openNewWindow", "threadPaneContext-openNewTab", "mailContext-openConversation", "mailContext-openContainingFolder", "mailContext-archive", "mailContext-replySender", "mailContext-replyNewsgroup", "mailContext-replyAll", "mailContext-replyList", "mailContext-forward", "mailContext-forwardAsMenu", "mailContext-multiForwardAsAttachment", "mailContext-redirect", "mailContext-editAsNew", "mailContext-editDraftMsg", "mailContext-newMsgFromTemplate", "mailContext-editTemplateMsg", "mailContext-copyMessageUrl", "mailContext-moveMenu", "mailContext-copyMenu", "mailContext-moveToFolderAgain", "mailContext-decryptToFolder", "mailContext-ignoreThread", "mailContext-ignoreSubthread", "mailContext-watchThread", "mailContext-tags", "mailContext-mark", "mailContext-saveAs", "mailContext-print", "mailContext-delete", "downloadSelected", "mailContext-reportPhishingURL", "mailContext-calendar-convert-menu", ]; for (let i = 0; i < messageTabSpecificItems.length; ++i) { this.showItem(messageTabSpecificItems[i], false); } return; } let canMove = gFolderDisplay.canDeleteSelectedMessages; // Show the Open in New Window and New Tab options if there is exactly one // message selected. this.showItem( "mailContext-openNewWindow", this.numSelectedMessages == 1 && this.inThreadPane ); this.showItem( "threadPaneContext-openNewTab", this.numSelectedMessages == 1 && this.inThreadPane ); this.showItem( "mailContext-openConversation", this.numSelectedMessages == 1 && this.inThreadPane && ConversationOpener.isMessageIndexed(gFolderDisplay.selectedMessage) ); this.showItem( "mailContext-openContainingFolder", !gFolderDisplay.folderPaneVisible && this.numSelectedMessages == 1 && !gMessageDisplay.isDummy ); this.setSingleSelection("mailContext-replySender"); this.setSingleSelection("mailContext-replyNewsgroup", this.isNewsgroup); this.setSingleSelection("mailContext-replyAll"); this.setSingleSelection("mailContext-replyList"); this.setSingleSelection("mailContext-forward"); this.setSingleSelection("mailContext-forwardAsMenu"); this.setSingleSelection("mailContext-redirect"); this.setSingleSelection("mailContext-editAsNew"); this.setSingleSelection( "mailContext-editDraftMsg", !document.getElementById("cmd_editDraftMsg").hidden ); this.setSingleSelection( "mailContext-newMsgFromTemplate", !document.getElementById("cmd_newMsgFromTemplate").hidden ); this.setSingleSelection( "mailContext-editTemplateMsg", !document.getElementById("cmd_editTemplateMsg").hidden ); this.showItem( "mailContext-multiForwardAsAttachment", this.numSelectedMessages > 1 && this.inThreadPane && !this.hideMailItems ); this.setSingleSelection("mailContext-copyMessageUrl", this.isNewsgroup); let msgModifyItems = this.numSelectedMessages > 0 && !this.hideMailItems && !this.onPlayableMedia && !(this.numSelectedMessages == 1 && gMessageDisplay.isDummy); let canArchive = gFolderDisplay.canArchiveSelectedMessages; this.showItem( "mailContext-archive", canMove && msgModifyItems && canArchive ); // Set up the move menu. We can't move from newsgroups. this.showItem("mailContext-moveMenu", msgModifyItems && !this.isNewsgroup); // disable move if we can't delete message(s) from this folder this.enableItem("mailContext-moveMenu", canMove && !this.onPlayableMedia); // Copy is available as long as something is selected. let canCopy = msgModifyItems || (gMessageDisplay.isDummy && window.arguments[0].scheme == "file"); this.showItem("mailContext-copyMenu", canCopy); this.showItem("mailContext-moveToFolderAgain", msgModifyItems); if (msgModifyItems) { initMoveToFolderAgainMenu( document.getElementById("mailContext-moveToFolderAgain") ); goUpdateCommand("cmd_moveToFolderAgain"); } let showDecrypt = this.numSelectedMessages > 1; if (this.numSelectedMessages == 1) { let msgURI = gFolderDisplay.selectedMessageUris[0]; showDecrypt = EnigmailURIs.isEncryptedUri(msgURI) || gEncryptedURIService.isEncrypted(msgURI); } this.showItem("mailContext-decryptToFolder", showDecrypt); this.showItem("mailContext-tags", msgModifyItems); this.showItem("mailContext-mark", msgModifyItems); this.showItem( "mailContext-ignoreThread", !this.inStandaloneWindow && this.numSelectedMessages >= 1 && !this.hideMailItems && !this.onPlayableMedia ); this.showItem( "mailContext-ignoreSubthread", !this.inStandaloneWindow && this.numSelectedMessages >= 1 && !this.hideMailItems && !this.onPlayableMedia ); this.showItem( "mailContext-watchThread", !this.inStandaloneWindow && this.numSelectedMessages > 0 && !this.hideMailItems && !this.onPlayableMedia ); this.showItem("mailContext-afterWatchThread", !this.inStandaloneWindow); this.showItem( "mailContext-saveAs", this.numSelectedMessages > 0 && !this.hideMailItems && !gMessageDisplay.isDummy && !this.onPlayableMedia ); // XXX Not quite modifying the message, but the same rules apply at the // moment as we can't print non-message content from the message pane yet. this.showItem("mailContext-print", msgModifyItems); this.showItem( "mailContext-delete", msgModifyItems && (this.isNewsgroup || canMove) ); // This function is needed for the case where a folder is just loaded (while // there isn't a message loaded in the message pane), a right-click is done // in the thread pane. This function will disable enable the 'Delete // Message' menu item. goUpdateCommand("cmd_delete"); this.showItem( "downloadSelected", this.numSelectedMessages > 1 && !this.hideMailItems ); this.showItem( "mailContext-reportPhishingURL", !this.inThreadPane && this.onLink && !this.onMailtoLink ); this.setSingleSelection("mailContext-calendar-convert-menu"); } initSeparators() { let separators = Array.from( this.xulMenu.querySelectorAll(":scope > menuseparator") ); let lastShownSeparator = null; for (let separator of separators) { let shouldShow = this.shouldShowSeparator(separator); if ( !shouldShow && lastShownSeparator && separator.classList.contains("webextension-group-separator") ) { // The separator for the WebExtension elements group must be shown, hide // the last shown menu separator instead. lastShownSeparator.hidden = true; shouldShow = true; } if (shouldShow) { lastShownSeparator = separator; } separator.hidden = !shouldShow; } this.checkLastSeparator(this.xulMenu); } setMessageTargets() { if (this.browser) { this.inAMessage = ["imap", "mailbox", "news", "snews"].includes( this.browser.currentURI.scheme ); this.inThreadPane = false; if (!this.inAMessage) { this.inStandaloneWindow = true; this.numSelectedMessages = 0; this.isNewsgroup = false; this.hideMailItems = true; return; } } else { this.inThreadPane = true; } this.inAMessage = true; this.inStandaloneWindow = false; this.numSelectedMessages = gFolderDisplay.selectedCount; this.isNewsgroup = gFolderDisplay.selectedMessageIsNews; // Don't show mail items for links/images, just show related items. this.hideMailItems = !this.inThreadPane && (this.onImage || this.onLink); } /** * Get a computed style property for an element. * * @param aElem * A DOM node * @param aProp * The desired CSS property * @returns the value of the property */ getComputedStyle(aElem, aProp) { return aElem.ownerGlobal.getComputedStyle(aElem).getPropertyValue(aProp); } /** * Determine whether the clicked-on link can be saved, and whether it * may be saved according to the ScriptSecurityManager. * * @returns true if the protocol can be persisted and if the target has * permission to link to the URL, false if not */ isLinkSaveable() { try { Services.scriptSecurityManager.checkLoadURIWithPrincipal( this.target.nodePrincipal, this.linkURI, Ci.nsIScriptSecurityManager.STANDARD ); } catch (e) { // Don't save things we can't link to. return false; } // We don't do the Right Thing for news/snews yet, so turn them off // until we do. return ( this.linkProtocol && !( this.linkProtocol == "mailto" || this.linkProtocol == "javascript" || this.linkProtocol == "news" || this.linkProtocol == "snews" ) ); } /** * Save URL of clicked-on link. */ saveLink() { saveURL( this.linkURL, null, this.linkTextStr, null, true, null, null, null, document ); } /** * Save a clicked-on image. */ saveImage() { saveURL( this.imageInfo.currentSrc, null, null, "SaveImageTitle", false, null, null, null, document ); } /** * Extract email addresses from a mailto: link and put them on the * clipboard. */ copyEmail() { // Copy the comma-separated list of email addresses only. // There are other ways of embedding email addresses in a mailto: // link, but such complex parsing is beyond us. const kMailToLength = 7; // length of "mailto:" var url = this.linkURL; var qmark = url.indexOf("?"); var addresses; if (qmark > kMailToLength) { addresses = url.substring(kMailToLength, qmark); } else { addresses = url.substr(kMailToLength); } // Let's try to unescape it using a character set. try { addresses = Services.textToSubURI.unEscapeURIForUI(addresses); } catch (ex) { // Do nothing. } var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( Ci.nsIClipboardHelper ); clipboard.copyString(addresses); } // --------- // Utilities /** * Set a DOM node's hidden property by passing in the node's id or the * element itself. * * @param aItemOrId * a DOM node or the id of a DOM node * @param aShow * true to show, false to hide */ showItem(aItemOrId, aShow) { var item = aItemOrId.constructor == String ? document.getElementById(aItemOrId) : aItemOrId; if (item) { item.hidden = !aShow; } } /** * Set a DOM node's disabled property by passing in the node's id or the * element itself. * * @param aItemOrId A DOM node or the id of a DOM node * @param aEnabled True to enable the element, false to disable. */ enableItem(aItemOrId, aEnabled) { var item = aItemOrId.constructor == String ? document.getElementById(aItemOrId) : aItemOrId; item.disabled = !aEnabled; } /** * Most menu items are visible if there's 1 or 0 messages selected, and * enabled if there's exactly one selected. Handle those here. * Exception: playable media is selected, in which case, don't show them. * * @param aID the id of the element to display/enable * @param aShow (optional) - an additional criteria to evaluate when we * decide whether to display the element. If false, we'll hide * the item no matter what messages are selected. */ setSingleSelection(aID, aShow) { let show = aShow != undefined ? aShow : true; this.showItem( aID, this.numSelectedMessages == 1 && !this.hideMailItems && show && !this.onPlayableMedia ); this.enableItem(aID, this.numSelectedMessages == 1); } /** * 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). * * @param aId * The id of an element * @param aAttr * The attribute name * @param aVal * The value to set the attribute to, or null to remove the attribute */ setItemAttr(aId, aAttr, aVal) { var elem = document.getElementById(aId); if (elem) { if (aVal == null) { // null indicates attr should be removed. elem.removeAttribute(aAttr); } else { // Set attr=val. elem.setAttribute(aAttr, aVal); } } } /** * Get an absolute URL for clicked-on link, from the href property or by * resolving an XLink URL by hand. * * @returns the string absolute URL for the clicked-on link */ getLinkURL() { if (this.link.href) { return this.link.href; } var href = this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href"); if (!href || href.trim() == "") { // Without this we try to save as the current doc, // for example, HTML case also throws if empty. throw new Error("Empty href"); } href = this.makeURLAbsolute(this.link.baseURI, href); return href; } /** * Generate a URI object from the linkURL spec * * @returns an nsIURI if possible, or null if not */ getLinkURI() { try { return Services.io.newURI(this.linkURL); } catch (ex) { // e.g. empty URL string } return null; } /** * Get the scheme for the clicked-on linkURI, if present. * * @returns a scheme, possibly undefined, or null if there's no linkURI */ getLinkProtocol() { if (this.linkURI) { return this.linkURI.scheme; // Can be |undefined|. } return null; } /** * Get the text of the clicked-on link. * * @returns {string} */ linkText() { return this.linkTextStr; } /** * Determines whether the focused window has something selected. * * @returns true if there is a selection, false if not */ isContentSelection() { return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed; } /** * Convert relative URL to absolute, using a provided . * * @param aBase * The URL string to use as the base * @param aUrl * The possibly-relative URL string * @returns The string absolute URL */ makeURLAbsolute(aBase, aUrl) { // Construct nsIURL. var baseURI = Services.io.newURI(aBase); return Services.io.newURI(baseURI.resolve(aUrl)).spec; } /** * Determine whether a DOM node is a text or password input, or a textarea. * * @param aNode * The DOM node to check * @returns true for textboxes, false for other elements */ isTargetATextBox(aNode) { if (HTMLInputElement.isInstance(aNode)) { return aNode.type == "text" || aNode.type == "password"; } return HTMLTextAreaElement.isInstance(aNode); } /** * Determine whether a separator should be shown based on whether * there are any non-hidden items between it and the previous separator. * * @param {DomElement} element - The separator element. * @returns {boolean} True if the separator should be shown, false if not. */ shouldShowSeparator(element) { if (element) { let sibling = element.previousElementSibling; while (sibling && sibling.localName != "menuseparator") { if (!sibling.hidden) { return true; } sibling = sibling.previousElementSibling; } } return false; } /** * Ensures that there isn't a separator shown at the bottom of the menu. * * @param aPopup The menu to check. */ checkLastSeparator(aPopup) { let sibling = aPopup.lastElementChild; while (sibling) { if (!sibling.hidden) { if (sibling.localName == "menuseparator") { // If we got here then the item is a menuseparator and everything // below it hidden. sibling.setAttribute("hidden", true); return; } return; } sibling = sibling.previousElementSibling; } } openInBrowser() { let url = this.target.ownerGlobal?.top.location.href; PlacesUtils.history .insert({ url, visits: [ { date: new Date(), }, ], }) .catch(console.error); Cc["@mozilla.org/uriloader/external-protocol-service;1"] .getService(Ci.nsIExternalProtocolService) .loadURI(Services.io.newURI(url)); } openLinkInBrowser() { PlacesUtils.history .insert({ url: this.linkURL, visits: [ { date: new Date(), }, ], }) .catch(console.error); Cc["@mozilla.org/uriloader/external-protocol-service;1"] .getService(Ci.nsIExternalProtocolService) .loadURI(this.linkURI); } mediaCommand(command) { var media = this.target; switch (command) { case "play": media.play(); break; case "pause": media.pause(); break; case "mute": media.muted = true; break; case "unmute": media.muted = false; break; // XXX hide controls & show controls don't work in emails as Javascript is // disabled. May want to consider later for RSS feeds. } } }