Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
GenAI: "resource:///modules/GenAI.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
LoginManagerContextMenu:
"resource://gre/modules/LoginManagerContextMenu.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
WebsiteFilter: "resource:///modules/policies/WebsiteFilter.sys.mjs",
});
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
Components.Constructor(
"@mozilla.org/referrer-info;1",
"nsIReferrerInfo",
"init"
)
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"SCREENSHOT_BROWSER_COMPONENT",
"screenshots.browser.component.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"TEXT_RECOGNITION_ENABLED",
"dom.text-recognition.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"STRIP_ON_SHARE_ENABLED",
"privacy.query_stripping.strip_on_share.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"STRIP_ON_SHARE_CAN_DISABLE",
"privacy.query_stripping.strip_on_share.canDisable",
false
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"QueryStringStripper",
"@mozilla.org/url-query-string-stripper;1",
"nsIURLQueryStringStripper"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"clipboard",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper"
);
const PASSWORD_FIELDNAME_HINTS = ["current-password", "new-password"];
const USERNAME_FIELDNAME_HINT = "username";
export class nsContextMenu {
/**
* A promise to retrieve the translations language pair
* if the context menu was opened in a context relevant to
* open the SelectTranslationsPanel.
* @type {Promise<{sourceLanguage: string, targetLanguage: string}>}
*/
#translationsLangPairPromise;
constructor(aXulMenu, aIsShift) {
this.window = aXulMenu.ownerGlobal;
this.document = aXulMenu.ownerDocument;
// Get contextual info.
this.setContext();
if (!this.shouldDisplay) {
return;
}
const { gBrowser } = this.window;
this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
if (!aIsShift) {
let tab =
gBrowser && gBrowser.getTabForBrowser
? gBrowser.getTabForBrowser(this.browser)
: undefined;
let subject = {
menu: aXulMenu,
tab,
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,
passwordRevealed: this.passwordRevealed,
srcUrl: this.originalMediaURL,
frameUrl: this.contentData ? this.contentData.docLocation : undefined,
pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
linkText: this.linkTextStr,
linkUrl: this.linkURL,
linkURI: this.linkURI,
selectionText: this.isTextSelected
? this.selectionInfo.fullText
: undefined,
frameId: this.frameID,
webExtBrowserType: this.webExtBrowserType,
webExtContextData: this.contentData
? this.contentData.webExtContextData
: undefined,
};
subject.wrappedJSObject = subject;
Services.obs.notifyObservers(subject, "on-build-contextmenu");
}
this.viewFrameSourceElement = this.document.getElementById(
"context-viewframesource"
);
this.ellipsis = "\u2026";
try {
this.ellipsis = Services.prefs.getComplexValue(
"intl.ellipsis",
Ci.nsIPrefLocalizedString
).data;
} catch (e) {}
// Reset after "on-build-contextmenu" notification in case selection was
// changed during the notification.
this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
this.onPlainTextLink = false;
// Initialize (disable/remove) menu items.
this.initItems(aXulMenu);
}
setContext() {
let context = Object.create(null);
if (nsContextMenu.contentData) {
this.contentData = nsContextMenu.contentData;
context = this.contentData.context;
nsContextMenu.contentData = null;
}
this.remoteType = this.actor?.domProcess?.remoteType;
const { gBrowser } = this.window;
this.shouldDisplay = 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.imageDescURL = context.imageDescURL;
this.imageInfo = context.imageInfo;
this.mediaURL = context.mediaURL || context.bgImageURL;
this.originalMediaURL = context.originalMediaURL || this.mediaURL;
this.webExtBrowserType = context.webExtBrowserType;
this.canSpellCheck = context.canSpellCheck;
this.hasBGImage = context.hasBGImage;
this.hasMultipleBGImages = context.hasMultipleBGImages;
this.isDesignMode = context.isDesignMode;
this.inFrame = context.inFrame;
this.inPDFViewer = context.inPDFViewer;
this.inPDFEditor = context.inPDFEditor;
this.inSrcdocFrame = context.inSrcdocFrame;
this.inSyntheticDoc = context.inSyntheticDoc;
this.inTabBrowser = context.inTabBrowser;
this.inWebExtBrowser = context.inWebExtBrowser;
this.link = context.link;
this.linkDownload = context.linkDownload;
this.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.onTelLink = context.onTelLink;
this.onMozExtLink = context.onMozExtLink;
this.onNumeric = context.onNumeric;
this.onPassword = context.onPassword;
this.passwordRevealed = context.passwordRevealed;
this.onSaveableLink = context.onSaveableLink;
this.onSpellcheckable = context.onSpellcheckable;
this.onTextInput = context.onTextInput;
this.onVideo = context.onVideo;
this.pdfEditorStates = context.pdfEditorStates;
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;
this.isSponsoredLink = context.isSponsoredLink;
// Everything after this isn't sent directly from ContextMenu
if (this.target) {
this.ownerDoc = this.target.ownerDocument;
}
this.csp = lazy.E10SUtils.deserializeCSP(context.csp);
if (this.contentData) {
this.browser = this.contentData.browser;
this.selectionInfo = this.contentData.selectionInfo;
this.actor = this.contentData.actor;
} else {
const { SelectionUtils } = ChromeUtils.importESModule(
"resource://gre/modules/SelectionUtils.sys.mjs"
);
this.browser = this.ownerDoc.defaultView.docShell.chromeEventHandler;
this.selectionInfo = SelectionUtils.getSelectionDetails(
this.browser.ownerGlobal
);
this.actor =
this.browser.browsingContext.currentWindowGlobal.getActor(
"ContextMenu"
);
}
this.selectedText = this.selectionInfo.text;
this.isTextSelected = !!this.selectedText.length;
this.webExtBrowserType = this.browser.getAttribute(
"webextension-view-type"
);
this.inWebExtBrowser = !!this.webExtBrowserType;
this.inTabBrowser =
gBrowser && gBrowser.getTabForBrowser
? !!gBrowser.getTabForBrowser(this.browser)
: false;
let { InlineSpellCheckerUI } = this.window;
if (context.shouldInitInlineSpellCheckerUINoChildren) {
InlineSpellCheckerUI.initFromRemote(
this.contentData.spellInfo,
this.actor.manager
);
}
if (this.contentData.spellInfo) {
this.spellSuggestions = this.contentData.spellInfo.spellSuggestions;
}
if (context.shouldInitInlineSpellCheckerUIWithChildren) {
InlineSpellCheckerUI.initFromRemote(
this.contentData.spellInfo,
this.actor.manager
);
let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
this.showItem("spell-check-enabled", canSpell);
}
} // setContext
hiding(aXulMenu) {
if (this.actor) {
this.actor.hiding();
}
aXulMenu.showHideSeparators = null;
this.contentData = null;
this.window.InlineSpellCheckerUI.clearSuggestionsFromMenu();
this.window.InlineSpellCheckerUI.clearDictionaryListFromMenu();
this.window.InlineSpellCheckerUI.uninit();
if (
Cu.isESModuleLoaded(
"resource://gre/modules/LoginManagerContextMenu.sys.mjs"
)
) {
lazy.LoginManagerContextMenu.clearLoginsFromMenu(this.document);
}
// This handler self-deletes, only run it if it is still there:
if (this._onPopupHiding) {
this._onPopupHiding();
}
}
initItems(aXulMenu) {
this.initOpenItems();
this.initNavigationItems();
this.initViewItems();
this.initImageItems();
this.initMiscItems();
this.initPocketItems();
this.initSpellingItems();
this.initSaveItems();
this.initSyncItems();
this.initClipboardItems();
this.initMediaPlayerItems();
this.initLeaveDOMFullScreenItems();
this.initPasswordManagerItems();
this.initViewSourceItems();
this.initScreenshotItem();
this.initPasswordControlItems();
this.initPDFItems();
this.showHideSeparators(aXulMenu);
if (!aXulMenu.showHideSeparators) {
// Set the showHideSeparators function on the menu itself so that
// the extension code (ext-menus.js) can call it after modifying
// the menus.
aXulMenu.showHideSeparators = () => {
this.showHideSeparators(aXulMenu);
};
}
}
initPDFItems() {
for (const id of [
"context-pdfjs-undo",
"context-pdfjs-redo",
"context-sep-pdfjs-redo",
"context-pdfjs-cut",
"context-pdfjs-copy",
"context-pdfjs-paste",
"context-pdfjs-delete",
"context-pdfjs-selectall",
"context-sep-pdfjs-selectall",
]) {
this.showItem(id, this.inPDFEditor);
}
this.showItem(
"context-pdfjs-highlight-selection",
this.pdfEditorStates?.hasSelectedText
);
if (!this.inPDFEditor) {
return;
}
const {
isEmpty,
hasSomethingToUndo,
hasSomethingToRedo,
hasSelectedEditor,
} = this.pdfEditorStates;
const hasEmptyClipboard = !Services.clipboard.hasDataMatchingFlavors(
["application/pdfjs"],
Ci.nsIClipboard.kGlobalClipboard
);
this.setItemAttr("context-pdfjs-undo", "disabled", !hasSomethingToUndo);
this.setItemAttr("context-pdfjs-redo", "disabled", !hasSomethingToRedo);
this.setItemAttr(
"context-sep-pdfjs-redo",
"disabled",
!hasSomethingToUndo && !hasSomethingToRedo
);
this.setItemAttr(
"context-pdfjs-cut",
"disabled",
isEmpty || !hasSelectedEditor
);
this.setItemAttr(
"context-pdfjs-copy",
"disabled",
isEmpty || !hasSelectedEditor
);
this.setItemAttr("context-pdfjs-paste", "disabled", hasEmptyClipboard);
this.setItemAttr(
"context-pdfjs-delete",
"disabled",
isEmpty || !hasSelectedEditor
);
this.setItemAttr("context-pdfjs-selectall", "disabled", isEmpty);
this.setItemAttr("context-sep-pdfjs-selectall", "disabled", isEmpty);
}
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;
this.linkURI = this.getLinkURI();
this.linkTextStr = this.selectionInfo.linkText;
this.onPlainTextLink = true;
}
let { window, document } = this;
var inContainer = false;
if (this.contentData.userContextId) {
inContainer = true;
var item = document.getElementById("context-openlinkincontainertab");
item.setAttribute("data-usercontextid", this.contentData.userContextId);
var label = lazy.ContextualIdentityService.getUserContextLabel(
this.contentData.userContextId
);
document.l10n.setAttributes(
item,
"main-context-menu-open-link-in-container-tab",
{
containerName: label,
}
);
}
var shouldShow =
this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
var isWindowPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
let showContainers =
Services.prefs.getBoolPref("privacy.userContext.enabled") &&
lazy.ContextualIdentityService.getPublicIdentities().length;
this.showItem("context-openlink", shouldShow && !isWindowPrivate);
this.showItem(
"context-openlinkprivate",
shouldShow && lazy.PrivateBrowsingUtils.enabled
);
this.showItem("context-openlinkintab", shouldShow && !inContainer);
this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
this.showItem(
"context-openlinkinusercontext-menu",
shouldShow && !isWindowPrivate && showContainers
);
this.showItem("context-openlinkincurrent", this.onPlainTextLink);
}
initNavigationItems() {
var shouldShow =
!(
this.isContentSelected ||
this.onLink ||
this.onImage ||
this.onCanvas ||
this.onVideo ||
this.onAudio ||
this.onTextInput
) && this.inTabBrowser;
if (AppConstants.platform == "macosx") {
for (let id of [
"context-back",
"context-forward",
"context-reload",
"context-stop",
"context-sep-navigation",
]) {
this.showItem(id, shouldShow);
}
} else {
this.showItem("context-navigation", shouldShow);
}
let stopped =
this.window.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");
let { document } = this;
let initBackForwardMenuItemTooltip = (menuItemId, l10nId, shortcutId) => {
// On macOS regular menuitems are used and the shortcut isn't added
if (AppConstants.platform == "macosx") {
return;
}
let shortcut = document.getElementById(shortcutId);
if (shortcut) {
shortcut = lazy.ShortcutUtils.prettifyShortcut(shortcut);
} else {
// Sidebar doesn't have navigation buttons or shortcuts, but we still
// want to format the menu item tooltip to remove "$shortcut" string.
shortcut = "";
}
let menuItem = document.getElementById(menuItemId);
document.l10n.setAttributes(menuItem, l10nId, { shortcut });
};
initBackForwardMenuItemTooltip(
"context-back",
"main-context-menu-back-2",
"goBackKb"
);
initBackForwardMenuItemTooltip(
"context-forward",
"main-context-menu-forward-2",
"goForwardKb"
);
}
initLeaveDOMFullScreenItems() {
// only show the option if the user is in DOM fullscreen
var shouldShow = this.target.ownerDocument.fullscreen;
this.showItem("context-leave-dom-fullscreen", shouldShow);
}
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
);
if (
(this.onSaveableLink || this.onPlainTextLink) &&
Services.policies.status === Services.policies.ACTIVE
) {
this.setItemAttr(
"context-savelink",
"disabled",
!lazy.WebsiteFilter.isAllowed(this.linkURL)
);
}
// Save video and audio don't rely on whether it has loaded or not.
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);
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
);
if (
Services.policies.status === Services.policies.ACTIVE &&
!Services.policies.isAllowed("filepickers")
) {
// When file pickers are disallowed by enterprise policy,
// these items silently fail. So to avoid confusion, we
// disable them.
for (let item of [
"context-savepage",
"context-savelink",
"context-savevideo",
"context-saveaudio",
"context-video-saveimage",
"context-saveaudio",
]) {
this.setItemAttr(item, "disabled", true);
}
}
}
initImageItems() {
// 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. If this isn't an image, check
// if there is a background image.
let showViewImage =
((this.onImage && (!this.inSyntheticDoc || this.inFrame)) ||
this.onCanvas) &&
!this.inPDFViewer;
let showBGImage =
this.hasBGImage &&
!this.hasMultipleBGImages &&
!this.inSyntheticDoc &&
!this.inPDFViewer &&
!this.isContentSelected &&
!this.onImage &&
!this.onCanvas &&
!this.onVideo &&
!this.onAudio &&
!this.onLink &&
!this.onTextInput;
this.showItem("context-viewimage", showViewImage || showBGImage);
// Save image depends on having loaded its content.
this.showItem(
"context-saveimage",
(this.onLoadedImage || this.onCanvas) && !this.inPDFEditor
);
if (Services.policies.status === Services.policies.ACTIVE) {
// When file pickers are disallowed by enterprise policy,
// this item silently fails. So to avoid confusion, we
// disable it.
this.setItemAttr(
"context-saveimage",
"disabled",
!Services.policies.isAllowed("filepickers")
);
}
// Copy image contents depends on whether we're on an image.
// Note: the element doesn't exist on all platforms, but showItem() takes
// care of that by itself.
this.showItem("context-copyimage-contents", this.onImage);
// Copy image location depends on whether we're on an image.
this.showItem("context-copyimage", this.onImage || showBGImage);
// Performing text recognition only works on images, and if the feature is enabled.
this.showItem(
"context-imagetext",
this.onImage &&
Services.appinfo.isTextRecognitionSupported &&
lazy.TEXT_RECOGNITION_ENABLED
);
// Send media URL (but not for canvas, since it's a big data: URL)
this.showItem("context-sendimage", this.onImage || showBGImage);
// View Image Info defaults to false, user can enable
var showViewImageInfo =
this.onImage &&
Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false);
this.showItem("context-viewimageinfo", showViewImageInfo);
// The image info popup is broken for WebExtension popups, since the browser
// is destroyed when the popup is closed.
this.setItemAttr(
"context-viewimageinfo",
"disabled",
this.webExtBrowserType === "popup"
);
// Open the link to more details about the image. Does not apply to
// background images.
this.showItem(
"context-viewimagedesc",
this.onImage && this.imageDescURL !== ""
);
// Set as Desktop background depends on whether an image was clicked on,
// and only works if we have a shell service.
var haveSetDesktopBackground = false;
if (
AppConstants.HAVE_SHELL_SERVICE &&
Services.policies.isAllowed("setDesktopBackground")
) {
// Only enable Set as Desktop Background if we can get the shell service.
var shell = this.window.getShellService();
if (shell) {
haveSetDesktopBackground = shell.canSetDesktopBackground;
}
}
this.showItem(
"context-setDesktopBackground",
haveSetDesktopBackground && this.onLoadedImage
);
if (haveSetDesktopBackground && this.onLoadedImage) {
this.document.getElementById("context-setDesktopBackground").disabled =
this.contentData.disableSetDesktopBackground;
}
}
initViewItems() {
// View source is always OK, unless in directory listing.
this.showItem(
"context-viewpartialsource-selection",
!this.inAboutDevtoolsToolbox &&
this.isContentSelected &&
this.selectionInfo.isDocumentLevelSelection
);
this.showItem(
"context-print-selection",
!this.inAboutDevtoolsToolbox &&
this.isContentSelected &&
this.selectionInfo.isDocumentLevelSelection
);
var shouldShow = !(
this.isContentSelected ||
this.onImage ||
this.onCanvas ||
this.onVideo ||
this.onAudio ||
this.onLink ||
this.onTextInput
);
var showInspect =
this.inTabBrowser &&
!this.inAboutDevtoolsToolbox &&
Services.prefs.getBoolPref("devtools.inspector.enabled", true) &&
!Services.prefs.getBoolPref("devtools.policy.disabled", false);
var showInspectA11Y =
showInspect &&
Services.prefs.getBoolPref("devtools.accessibility.enabled", false) &&
Services.prefs.getBoolPref("devtools.enabled", true) &&
(Services.prefs.getBoolPref("devtools.everOpened", false) ||
// once existing users have had time to set devtools.everOpened
// through normal use, and we've passed an ESR cycle (91).
lazy.DevToolsShim.isDevToolsUser());
this.showItem("context-viewsource", shouldShow);
this.showItem("context-inspect", showInspect);
this.showItem("context-inspect-a11y", showInspectA11Y);
// 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);
}
initMiscItems() {
let { window, document } = this;
// Use "Bookmark Link…" if on a link.
let bookmarkPage = document.getElementById("context-bookmarkpage");
this.showItem(
bookmarkPage,
!(
this.isContentSelected ||
this.onTextInput ||
this.onLink ||
this.onImage ||
this.onVideo ||
this.onAudio ||
this.onCanvas ||
this.inWebExtBrowser
)
);
this.showItem(
"context-bookmarklink",
(this.onLink &&
!this.onMailtoLink &&
!this.onTelLink &&
!this.onMozExtLink) ||
this.onPlainTextLink
);
this.showItem("context-keywordfield", this.shouldShowAddKeyword());
this.showItem("frame", this.inFrame);
if (this.inFrame) {
// To make it easier to debug the browser running with out-of-process iframes, we
// display the process PID of the iframe in the context menu for the subframe.
let frameOsPid =
this.actor.manager.browsingContext.currentWindowGlobal.osPid;
this.setItemAttr("context-frameOsPid", "label", "PID: " + frameOsPid);
// We need to check if "Take Screenshot" should be displayed in the "This Frame"
// context menu
let shouldShowTakeScreenshotFrame = this.shouldShowTakeScreenshot();
this.showItem(
"context-take-frame-screenshot",
shouldShowTakeScreenshotFrame
);
this.showItem(
"context-sep-frame-screenshot",
shouldShowTakeScreenshotFrame
);
}
this.showAndFormatSearchContextItem();
this.showTranslateSelectionItem();
lazy.GenAI.buildAskChatMenu(
document.getElementById("context-ask-chat"),
this
);
// 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);
// Hide menu entries for images, show otherwise
if (this.inFrame) {
this.viewFrameSourceElement.hidden =
!lazy.BrowserUtils.mimeTypeIsTextBased(
this.target.ownerDocument.contentType
);
}
// BiDi UI
this.showItem(
"context-bidi-text-direction-toggle",
this.onTextInput && !this.onNumeric && window.top.gBidiUI
);
this.showItem(
"context-bidi-page-direction-toggle",
!this.onTextInput && window.top.gBidiUI
);
}
initPocketItems() {
const pocketEnabled = Services.prefs.getBoolPref(
"extensions.pocket.enabled"
);
let showSaveCurrentPageToPocket = false;
let showSaveLinkToPocket = false;
// We can skip all this is Pocket is not enabled.
if (pocketEnabled) {
let targetURL, targetURI;
// If the context menu is opened over a link, we target the link,
// if not, we target the page.
if (this.onLink) {
targetURL = this.linkURL;
// linkURI may be null if the URL is invalid.
targetURI = this.linkURI;
} else {
targetURL = this.browser?.currentURI?.spec;
targetURI = Services.io.newURI(targetURL);
}
const canPocket =
targetURI?.schemeIs("http") ||
targetURI?.schemeIs("https") ||
(targetURI?.schemeIs("about") &&
lazy.ReaderMode?.getOriginalUrl(targetURL));
// If the target is valid, decide which menu item to enable.
if (canPocket) {
showSaveLinkToPocket = this.onLink;
showSaveCurrentPageToPocket = !(
this.onTextInput ||
this.onLink ||
this.isContentSelected ||
this.onImage ||
this.onCanvas ||
this.onVideo ||
this.onAudio
);
}
}
this.showItem("context-pocket", showSaveCurrentPageToPocket);
this.showItem("context-savelinktopocket", showSaveLinkToPocket);
}
initSpellingItems() {
let { document } = this;
let { InlineSpellCheckerUI } = this.window;
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);
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
if (onMisspelling) {
var suggestionsSeparator = document.getElementById(
"spell-add-to-dictionary"
);
var numsug = InlineSpellCheckerUI.addSuggestionsToMenu(
suggestionsSeparator.parentNode,
suggestionsSeparator,
this.spellSuggestions
);
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");
InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
this.showItem("spell-add-dictionaries-main", false);
} else if (this.onSpellcheckable) {
// when there is no spellchecker but we might be able to spellcheck
// add the add to dictionaries item. This will ensure that people
// with no dictionaries will be able to download them
this.showItem("spell-add-dictionaries-main", showDictionaries);
} else {
this.showItem("spell-add-dictionaries-main", false);
}
}
initClipboardItems() {
// Copy depends on whether there is selected text.
// Enabling this context menu item is now done through the global
// command updating system
// this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
this.window.goUpdateGlobalEditMenuItems();
this.showItem("context-undo", this.onTextInput);
this.showItem("context-redo", 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-paste-no-formatting", this.isDesignMode);
this.showItem("context-delete", this.onTextInput);
this.showItem(
"context-selectall",
!(
this.onLink ||
this.onImage ||
this.onVideo ||
this.onAudio ||
this.inSyntheticDoc ||
this.inPDFEditor
) || this.isDesignMode
);
// 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 phone link depends on whether we're on a phone link.
this.showItem("context-copyphone", this.onTelLink);
// Copy link location depends on whether we're on a non-mailto link.
this.showItem(
"context-copylink",
this.onLink && !this.onMailtoLink && !this.onTelLink
);
// Showing "Copy Clean link" depends on whether the strip-on-share feature is enabled
// and the user is selecting a URL
this.showItem(
"context-stripOnShareLink",
lazy.STRIP_ON_SHARE_ENABLED &&
(this.onLink || this.onPlainTextLink) &&
!this.onMailtoLink &&
!this.onTelLink &&
!this.onMozExtLink &&
!this.isSecureAboutPage()
);
let canNotStrip =
lazy.STRIP_ON_SHARE_CAN_DISABLE && !this.#canStripParams();
this.setItemAttr("context-stripOnShareLink", "disabled", canNotStrip);
let copyLinkSeparator = this.document.getElementById(
"context-sep-copylink"
);
// Show "Copy Link", "Copy" and "Copy Clean Link" with no divider, and "copy link" and "Send link to Device" with no divider between.
// Other cases will show a divider.
copyLinkSeparator.toggleAttribute(
"ensureHidden",
this.onLink &&
!this.onMailtoLink &&
!this.onTelLink &&
!this.onImage &&
this.syncItemsShown
);
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);
}
initMediaPlayerItems() {
var onMedia = this.onVideo || this.onAudio;
// Several mutually exclusive items... play/pause, mute/unmute, show/hide
this.showItem(
"context-media-play",
onMedia && (this.target.paused || this.target.ended)
);
this.showItem(
"context-media-pause",
onMedia && !this.target.paused && !this.target.ended
);
this.showItem("context-media-mute", onMedia && !this.target.muted);
this.showItem("context-media-unmute", onMedia && this.target.muted);
this.showItem(
"context-media-playbackrate",
onMedia && this.target.duration != Number.POSITIVE_INFINITY
);
this.showItem("context-media-loop", onMedia);
this.showItem(
"context-media-showcontrols",
onMedia && !this.target.controls
);
this.showItem(
"context-media-hidecontrols",
this.target.controls &&
(this.onVideo || (this.onAudio && !this.inSyntheticDoc))
);
this.showItem(
"context-video-fullscreen",
this.onVideo && !this.target.ownerDocument.fullscreen
);
{
let shouldDisplay =
Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.enabled"
) &&
this.onVideo &&
!this.target.ownerDocument.fullscreen &&
this.target.readyState > 0;
this.showItem("context-video-pictureinpicture", shouldDisplay);
}
this.showItem("context-media-eme-learnmore", 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.setItemAttr(
"context-video-pictureinpicture",
"checked",
this.onPiPVideo
);
this.setItemAttr(
"context-video-pictureinpicture",
"disabled",
!this.onPiPVideo && hasError
);
}
}
}
initPasswordManagerItems() {
let { document } = this;
let showUseSavedLogin = false;
let showGenerate = false;
let showManage = false;
let enableGeneration = Services.logins.isLoggedIn;
try {
// If we could not find a password field we don't want to
// show the form fill, manage logins and the password generation items.
if (!this.isLoginForm()) {
return;
}
showManage = true;
// Disable the fill option if the user hasn't unlocked with their primary password
// or if the password field or target field are disabled.
let loginFillInfo = this.contentData?.loginFillInfo;
let disableFill =
!Services.logins.isLoggedIn ||
loginFillInfo?.passwordField.disabled ||
loginFillInfo?.activeField.disabled;
this.setItemAttr("fill-login", "disabled", disableFill);
let onPasswordLikeField = PASSWORD_FIELDNAME_HINTS.includes(
loginFillInfo.activeField.fieldNameHint
);
// Set the correct label for the fill menu
let fillMenu = document.getElementById("fill-login");
document.l10n.setAttributes(
fillMenu,
"main-context-menu-use-saved-password"
);
let documentURI = this.contentData?.documentURIObject;
let formOrigin = lazy.LoginHelper.getLoginOrigin(documentURI?.spec);
let isGeneratedPasswordEnabled =
lazy.LoginHelper.generationAvailable &&
lazy.LoginHelper.generationEnabled;
showGenerate =
onPasswordLikeField &&
isGeneratedPasswordEnabled &&
Services.logins.getLoginSavingEnabled(formOrigin);
if (disableFill) {
showUseSavedLogin = true;
// No need to update the submenu if the fill item is disabled.
return;
}
// Update sub-menu items.
let fragment = lazy.LoginManagerContextMenu.addLoginsToMenu(
this.targetIdentifier,
this.browser,
formOrigin
);
if (!fragment) {
return;
}
showUseSavedLogin = true;
let popup = document.getElementById("fill-login-popup");
popup.appendChild(fragment);
} finally {
const documentURI = this.contentData?.documentURIObject;
const showRelay =
this.contentData?.context.showRelay &&
lazy.LoginHelper.getLoginOrigin(documentURI?.spec);
this.showItem("fill-login", showUseSavedLogin);
this.showItem("fill-login-generated-password", showGenerate);
this.showItem("use-relay-mask", showRelay);
this.showItem("manage-saved-logins", showManage);
this.setItemAttr(
"fill-login-generated-password",
"disabled",
!enableGeneration
);
this.setItemAttr(
"passwordmgr-items-separator",
"ensureHidden",
showUseSavedLogin || showGenerate || showManage || showRelay
? null
: true
);
}
}
initSyncItems() {
this.syncItemsShown = this.window.gSync.updateContentContextMenu(this);
}
initViewSourceItems() {
const getString = aName => {
const { bundle } = this.window.gViewSourceUtils.getPageActor(
this.browser
);
return bundle.GetStringFromName(aName);
};
const showViewSourceItem = (id, check, accesskey) => {
const fullId = `context-viewsource-${id}`;
this.showItem(fullId, onViewSource);
if (!onViewSource) {
return;
}
this.setItemAttr(fullId, "checked", check());
this.setItemAttr(fullId, "label", getString(`context_${id}_label`));
if (accesskey) {
this.setItemAttr(
fullId,
"accesskey",
getString(`context_${id}_accesskey`)
);
}
};
const onViewSource = this.browser.currentURI.schemeIs("view-source");
showViewSourceItem("goToLine", () => false, true);
showViewSourceItem("wrapLongLines", () =>
Services.prefs.getBoolPref("view_source.wrap_long_lines", false)
);
showViewSourceItem("highlightSyntax", () =>
Services.prefs.getBoolPref("view_source.syntax_highlight", false)
);
}
// Iterate over the visible items on the menu and its submenus and
// hide any duplicated separators next to each other.
// The attribute "ensureHidden" will override this process and keep a particular separator hidden in special cases.
showHideSeparators(aPopup) {
let lastVisibleSeparator = null;
let count = 0;
for (let menuItem of aPopup.children) {
// Skip any items that were added by the page menu.
if (menuItem.hasAttribute("generateditemid")) {
count++;
continue;
}
if (menuItem.localName == "menuseparator") {
// Individual separators can have the `ensureHidden` attribute added to avoid them
// becoming visible. We also set `count` to 0 below because otherwise the
// next separator would be made visible, with the same visual effect.
if (!count || menuItem.hasAttribute("ensureHidden")) {
menuItem.hidden = true;
} else {
menuItem.hidden = false;
lastVisibleSeparator = menuItem;
}
count = 0;
} else if (!menuItem.hidden) {
if (menuItem.localName == "menu") {
this.showHideSeparators(menuItem.menupopup);
} else if (menuItem.localName == "menugroup") {
this.showHideSeparators(menuItem);
}
count++;
}
}
// If count is 0 yet lastVisibleSeparator is set, then there must be a separator
// visible at the end of the menu, so hide it. Note that there could be more than
// one but this isn't handled here.
if (!count && lastVisibleSeparator) {
lastVisibleSeparator.hidden = true;
}
}
shouldShowTakeScreenshot() {
let shouldShow =
!this.window.gScreenshots.shouldScreenshotsButtonBeDisabled() &&
this.inTabBrowser &&
!this.onTextInput &&
!this.onLink &&
!this.onPlainTextLink &&
!this.onAudio &&
!this.onEditable &&
!this.onPassword;
return shouldShow;
}
initScreenshotItem() {
let shouldShow = this.shouldShowTakeScreenshot() && !this.inFrame;
this.showItem("context-sep-screenshots", shouldShow);
this.showItem("context-take-screenshot", shouldShow);
}
initPasswordControlItems() {
let shouldShow = this.onPassword;
if (shouldShow) {
let revealPassword = this.document.getElementById(
"context-reveal-password"
);
if (this.passwordRevealed) {
revealPassword.setAttribute("checked", "true");
} else {
revealPassword.removeAttribute("checked");
}
}
this.showItem("context-reveal-password", shouldShow);
}
toggleRevealPassword() {
this.actor.toggleRevealPassword(this.targetIdentifier);
}
openPasswordManager() {
lazy.LoginHelper.openPasswordManager(this.window, {
entryPoint: "Contextmenu",
});
}
useRelayMask() {
const documentURI = this.contentData?.documentURIObject;
const aOrigin = lazy.LoginHelper.getLoginOrigin(documentURI?.spec);
this.actor.useRelayMask(this.targetIdentifier, aOrigin);
}
useGeneratedPassword() {
lazy.LoginManagerContextMenu.useGeneratedPassword(this.targetIdentifier);
}
isLoginForm() {
let loginFillInfo = this.contentData?.loginFillInfo;
let documentURI = this.contentData?.documentURIObject;
// If we could not find a password field or this is not a username-only
// form, then don't treat this as a login form.
return (
(loginFillInfo?.passwordField?.found ||
loginFillInfo?.activeField.fieldNameHint == USERNAME_FIELDNAME_HINT) &&
!documentURI?.schemeIs("about") &&
this.browser.contentPrincipal.spec != "resource://pdf.js/web/viewer.html"
);
}
inspectNode() {
return lazy.DevToolsShim.inspectNode(
this.window.gBrowser.selectedTab,
this.targetIdentifier
);
}
inspectA11Y() {
return lazy.DevToolsShim.inspectA11Y(
this.window.gBrowser.selectedTab,
this.targetIdentifier
);
}
_openLinkInParameters(extra) {
let params = {
charset: this.contentData.charSet,
originPrincipal: this.principal,
originStoragePrincipal: this.storagePrincipal,
triggeringPrincipal: this.principal,
triggeringRemoteType: this.remoteType,
csp: this.csp,
frameID: this.contentData.frameID,
hasValidUserGestureActivation: true,
};
for (let p in extra) {
params[p] = extra[p];
}
let referrerInfo = this.onLink
? this.contentData.linkReferrerInfo
: this.contentData.referrerInfo;
// If we want to change userContextId, we must be sure that we don't
// propagate the referrer.
if (
("userContextId" in params &&
params.userContextId != this.contentData.userContextId) ||
this.onPlainTextLink
) {
referrerInfo = new lazy.ReferrerInfo(
referrerInfo.referrerPolicy,
false,
referrerInfo.originalReferrer
);
}
params.referrerInfo = referrerInfo;
return params;
}
_getGlobalHistoryOptions() {
if (this.isSponsoredLink) {
return {
globalHistoryOptions: { triggeringSponsoredURL: this.linkURL },
};
} else if (this.browser.hasAttribute("triggeringSponsoredURL")) {
return {
globalHistoryOptions: {
triggeringSponsoredURL: this.browser.getAttribute(
"triggeringSponsoredURL"
),
triggeringSponsoredURLVisitTimeMS: this.browser.getAttribute(
"triggeringSponsoredURLVisitTimeMS"
),
},
};
}
return {};
}
// Open linked-to URL in a new window.
openLink() {
const params = this._getGlobalHistoryOptions();
this.window.openLinkIn(
this.linkURL,
"window",
this._openLinkInParameters(params)
);
}
// Open linked-to URL in a new private window.
openLinkInPrivateWindow() {
this.window.openLinkIn(
this.linkURL,
"window",
this._openLinkInParameters({ private: true })
);
}
// Open linked-to URL in a new tab.
openLinkInTab(event) {
let params = {
userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
...this._getGlobalHistoryOptions(),
};
this.window.openLinkIn(
this.linkURL,
"tab",
this._openLinkInParameters(params)
);
}
// open URL in current tab
openLinkInCurrent() {
this.window.openLinkIn(
this.linkURL,
"current",
this._openLinkInParameters()
);
}
// Open frame in a new tab.
openFrameInTab() {
this.window.openLinkIn(this.contentData.docLocation, "tab", {
charset: this.contentData.charSet,
triggeringPrincipal: this.browser.contentPrincipal,
csp: this.browser.csp,
referrerInfo: this.contentData.frameReferrerInfo,
});
}
// Reload clicked-in frame.
reloadFrame(aEvent) {
let forceReload = aEvent.shiftKey;
this.actor.reloadFrame(this.targetIdentifier, forceReload);
}
// Open clicked-in frame in its own window.
openFrame() {
this.window.openLinkIn(this.contentData.docLocation, "window", {
charset: this.contentData.charSet,
triggeringPrincipal: this.browser.contentPrincipal,
csp: this.browser.csp,
referrerInfo: this.contentData.frameReferrerInfo,
});
}
// Open clicked-in frame in the same window.
showOnlyThisFrame() {
this.window.urlSecurityCheck(
this.contentData.docLocation,
this.browser.contentPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
);
this.window.openWebLinkIn(this.contentData.docLocation, "current", {
referrerInfo: this.contentData.frameReferrerInfo,
triggeringPrincipal: this.browser.contentPrincipal,
});
}
takeScreenshot() {
if (lazy.SCREENSHOT_BROWSER_COMPONENT) {
Services.obs.notifyObservers(
this.window,
"menuitem-screenshot",
"ContextMenu"
);
} else {
Services.obs.notifyObservers(
null,
"menuitem-screenshot-extension",
"contextMenu"
);
}
}
pdfJSCmd(aName) {
if (["cut", "copy", "paste"].includes(aName)) {
const cmd = `cmd_${aName}`;
this.document.commandDispatcher
.getControllerForCommand(cmd)
.doCommand(cmd);
if (Cu.isInAutomation) {
this.browser.sendMessageToActor(
"PDFJS:Editing",
{ name: aName },
"Pdfjs"
);
}
return;
}
this.browser.sendMessageToActor("PDFJS:Editing", { name: aName }, "Pdfjs");
}
// View Partial Source
viewPartialSource() {
let { browser } = this;
let openSelectionFn = () => {
let tabBrowser = this.window.gBrowser;
let relatedToCurrent = tabBrowser?.selectedBrowser === browser;
const inNewWindow = !Services.prefs.getBoolPref("view_source.tab");
// In the case of popups, we need to find a non-popup browser window.
// We might also not have a tabBrowser reference (if this isn't in a
// a tabbrowser scope) or might have a fake/stub tabbrowser reference
// (in the sidebar). Deal with those cases:
if (!tabBrowser || !tabBrowser.addTab || !this.window.toolbar.visible) {
// This returns only non-popup browser windows by default.
let browserWindow = lazy.BrowserWindowTracker.getTopWindow();
tabBrowser = browserWindow.gBrowser;
}
let tab = tabBrowser.addTab("about:blank", {
relatedToCurrent,
inBackground: inNewWindow,
skipAnimation: inNewWindow,
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),