summaryrefslogtreecommitdiffstats
path: root/browser/base/content/nsContextMenu.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/nsContextMenu.js')
-rw-r--r--browser/base/content/nsContextMenu.js2732
1 files changed, 2732 insertions, 0 deletions
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
new file mode 100644
index 0000000000..4e313e7f01
--- /dev/null
+++ b/browser/base/content/nsContextMenu.js
@@ -0,0 +1,2732 @@
+/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PASSWORD_FIELDNAME_HINTS = ["current-password", "new-password"];
+const USERNAME_FIELDNAME_HINT = "username";
+
+function openContextMenu(aMessage, aBrowser, aActor) {
+ if (BrowserHandler.kiosk) {
+ // Don't display context menus in kiosk mode
+ return;
+ }
+ let data = aMessage.data;
+ let browser = aBrowser;
+ let actor = aActor;
+ let wgp = actor.manager;
+
+ if (!wgp.isCurrentGlobal) {
+ // Don't display context menus for unloaded documents
+ return;
+ }
+
+ // NOTE: We don't use `wgp.documentURI` here as we want to use the failed
+ // channel URI in the case we have loaded an error page.
+ let documentURIObject = wgp.browsingContext.currentURI;
+
+ let frameReferrerInfo = data.frameReferrerInfo;
+ if (frameReferrerInfo) {
+ frameReferrerInfo = E10SUtils.deserializeReferrerInfo(frameReferrerInfo);
+ }
+
+ let linkReferrerInfo = data.linkReferrerInfo;
+ if (linkReferrerInfo) {
+ linkReferrerInfo = E10SUtils.deserializeReferrerInfo(linkReferrerInfo);
+ }
+
+ let frameID = nsContextMenu.WebNavigationFrames.getFrameId(
+ wgp.browsingContext
+ );
+
+ nsContextMenu.contentData = {
+ context: data.context,
+ browser,
+ actor,
+ editFlags: data.editFlags,
+ spellInfo: data.spellInfo,
+ principal: wgp.documentPrincipal,
+ storagePrincipal: wgp.documentStoragePrincipal,
+ documentURIObject,
+ docLocation: documentURIObject.spec,
+ charSet: data.charSet,
+ referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
+ frameReferrerInfo,
+ linkReferrerInfo,
+ contentType: data.contentType,
+ contentDisposition: data.contentDisposition,
+ frameID,
+ frameOuterWindowID: frameID,
+ frameBrowsingContext: wgp.browsingContext,
+ selectionInfo: data.selectionInfo,
+ disableSetDesktopBackground: data.disableSetDesktopBackground,
+ showRelay: data.showRelay,
+ loginFillInfo: data.loginFillInfo,
+ userContextId: wgp.browsingContext.originAttributes.userContextId,
+ webExtContextData: data.webExtContextData,
+ cookieJarSettings: wgp.cookieJarSettings,
+ };
+
+ let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
+ let context = nsContextMenu.contentData.context;
+
+ // Fill in some values in the context from the WindowGlobalParent actor.
+ context.principal = wgp.documentPrincipal;
+ context.storagePrincipal = wgp.documentStoragePrincipal;
+ context.frameID = frameID;
+ context.frameOuterWindowID = wgp.outerWindowId;
+ context.frameBrowsingContextID = wgp.browsingContext.id;
+
+ // 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,
+ 2,
+ null,
+ 0,
+ context.inputSource
+ );
+ popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
+}
+
+class nsContextMenu {
+ constructor(aXulMenu, aIsShift) {
+ // Get contextual info.
+ this.setContext();
+
+ if (!this.shouldDisplay) {
+ return;
+ }
+
+ 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 = 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.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 = 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(window);
+ this.actor =
+ this.browser.browsingContext.currentWindowGlobal.getActor(
+ "ContextMenu"
+ );
+ }
+
+ this.remoteType = this.actor?.domProcess?.remoteType;
+
+ const { gBrowser } = this.browser.ownerGlobal;
+
+ 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;
+
+ 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;
+ InlineSpellCheckerUI.clearSuggestionsFromMenu();
+ InlineSpellCheckerUI.clearDictionaryListFromMenu();
+ InlineSpellCheckerUI.uninit();
+ if (
+ Cu.isModuleLoaded("resource://gre/modules/LoginManagerContextMenu.jsm")
+ ) {
+ nsContextMenu.LoginManagerContextMenu.clearLoginsFromMenu(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);
+ }
+
+ 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;
+ }
+
+ var inContainer = false;
+ if (this.contentData.userContextId) {
+ inContainer = true;
+ var item = document.getElementById("context-openlinkincontainertab");
+
+ item.setAttribute("data-usercontextid", this.contentData.userContextId);
+
+ var label = 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 = PrivateBrowsingUtils.isWindowPrivate(window);
+ let showContainers =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ ContextualIdentityService.getPublicIdentities().length;
+ this.showItem("context-openlink", shouldShow && !isWindowPrivate);
+ this.showItem(
+ "context-openlinkprivate",
+ shouldShow && 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 =
+ 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");
+
+ function 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 = 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",
+ !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 &&
+ 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 = getShellService();
+ if (shell) {
+ haveSetDesktopBackground = shell.canSetDesktopBackground;
+ }
+ }
+
+ this.showItem(
+ "context-setDesktopBackground",
+ haveSetDesktopBackground && this.onLoadedImage
+ );
+
+ if (haveSetDesktopBackground && this.onLoadedImage) {
+ 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) ||
+ // Note: this is a legacy usecase, we will remove it in bug 1695257,
+ // once existing users have had time to set devtools.everOpened
+ // through normal use, and we've passed an ESR cycle (91).
+ nsContextMenu.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() {
+ // 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();
+
+ // 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 = !BrowserUtils.mimeTypeIsTextBased(
+ this.target.ownerDocument.contentType
+ );
+ }
+
+ // BiDi UI
+ this.showItem(
+ "context-bidi-text-direction-toggle",
+ this.onTextInput && !this.onNumeric && top.gBidiUI
+ );
+ this.showItem(
+ "context-bidi-page-direction-toggle",
+ !this.onTextInput && 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") && 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() {
+ 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() );
+ 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",
+ STRIP_ON_SHARE_ENABLED &&
+ this.onLink &&
+ !this.onMailtoLink &&
+ !this.onTelLink &&
+ !this.onMozExtLink &&
+ !this.isSecureAboutPage()
+ );
+
+ let copyLinkSeparator = 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 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.
+ // XXX: Bug 1529025 to maybe respect signon.rememberSignons.
+ 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 = LoginHelper.getLoginOrigin(documentURI?.spec);
+ let isGeneratedPasswordEnabled =
+ LoginHelper.generationAvailable && 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 = nsContextMenu.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 &&
+ 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 = gSync.updateContentContextMenu(this);
+ }
+
+ initViewSourceItems() {
+ const getString = name => {
+ const { bundle } = gViewSourceUtils.getPageActor(this.browser);
+ return bundle.GetStringFromName(name);
+ };
+ const showViewSourceItem = (id, check, accesskey) => {
+ const fullId = `context-viewsource-${id}`;
+ this.showItem(fullId, onViewSource);
+ if (!onViewSource) {
+ return;
+ }
+ check().then(checked => this.setItemAttr(fullId, "checked", checked));
+ 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", async () => false, true);
+ showViewSourceItem("wrapLongLines", () =>
+ gViewSourceUtils.getPageActor(this.browser).queryIsWrapping()
+ );
+ showViewSourceItem("highlightSyntax", () =>
+ gViewSourceUtils.getPageActor(this.browser).queryIsSyntaxHighlighting()
+ );
+ }
+
+ // 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 =
+ !gScreenshots.shouldScreenshotsButtonBeDisabled() &&
+ this.inTabBrowser &&
+ !this.onTextInput &&
+ !this.onLink &&
+ !this.onPlainTextLink &&
+ !this.onImage &&
+ !this.onVideo &&
+ !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 && REVEAL_PASSWORD_ENABLED;
+ if (shouldShow) {
+ let revealPassword = 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() {
+ LoginHelper.openPasswordManager(window, {
+ entryPoint: "contextmenu",
+ });
+ }
+
+ useRelayMask() {
+ const documentURI = this.contentData?.documentURIObject;
+ const origin = LoginHelper.getLoginOrigin(documentURI?.spec);
+ this.actor.useRelayMask(this.targetIdentifier, origin);
+ }
+
+ useGeneratedPassword() {
+ nsContextMenu.LoginManagerContextMenu.useGeneratedPassword(
+ this.targetIdentifier,
+ this.contentData.documentURIObject,
+ this.browser
+ );
+ }
+
+ 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 nsContextMenu.DevToolsShim.inspectNode(
+ gBrowser.selectedTab,
+ this.targetIdentifier
+ );
+ }
+
+ inspectA11Y() {
+ return nsContextMenu.DevToolsShim.inspectA11Y(
+ 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 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();
+
+ openLinkIn(this.linkURL, "window", this._openLinkInParameters(params));
+ }
+
+ // Open linked-to URL in a new private window.
+ openLinkInPrivateWindow() {
+ 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(),
+ };
+
+ openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
+ }
+
+ // open URL in current tab
+ openLinkInCurrent() {
+ openLinkIn(this.linkURL, "current", this._openLinkInParameters());
+ }
+
+ // Open frame in a new tab.
+ openFrameInTab() {
+ 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() {
+ 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() {
+ urlSecurityCheck(
+ this.contentData.docLocation,
+ this.browser.contentPrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ openWebLinkIn(this.contentData.docLocation, "current", {
+ referrerInfo: this.contentData.frameReferrerInfo,
+ triggeringPrincipal: this.browser.contentPrincipal,
+ });
+ }
+
+ takeScreenshot() {
+ if (SCREENSHOT_BROWSER_COMPONENT) {
+ Services.obs.notifyObservers(
+ window,
+ "menuitem-screenshot",
+ "context_menu"
+ );
+ } else {
+ Services.obs.notifyObservers(
+ null,
+ "menuitem-screenshot-extension",
+ "contextMenu"
+ );
+ }
+ }
+
+ pdfJSCmd(name) {
+ if (["cut", "copy", "paste"].includes(name)) {
+ const cmd = `cmd_${name}`;
+ document.commandDispatcher.getControllerForCommand(cmd).doCommand(cmd);
+ if (Cu.isInAutomation) {
+ this.browser.sendMessageToActor("PDFJS:Editing", { name }, "Pdfjs");
+ }
+ return;
+ }
+ this.browser.sendMessageToActor("PDFJS:Editing", { name }, "Pdfjs");
+ }
+
+ // View Partial Source
+ viewPartialSource() {
+ let { browser } = this;
+ let openSelectionFn = function () {
+ let tabBrowser = gBrowser;
+ 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 || !window.toolbar.visible) {
+ // This returns only non-popup browser windows by default.
+ let browserWindow = BrowserWindowTracker.getTopWindow();
+ tabBrowser = browserWindow.gBrowser;
+ }
+ let relatedToCurrent = gBrowser && gBrowser.selectedBrowser == browser;
+ let tab = tabBrowser.addTab("about:blank", {
+ relatedToCurrent,
+ inBackground: inNewWindow,
+ skipAnimation: inNewWindow,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ const viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
+ if (inNewWindow) {
+ tabBrowser.hideTab(tab);
+ tabBrowser.replaceTabsWithWindow(tab);
+ }
+ return viewSourceBrowser;
+ };
+
+ top.gViewSourceUtils.viewPartialSourceInBrowser(
+ this.actor.browsingContext,
+ openSelectionFn
+ );
+ }
+
+ // Open new "view source" window with the frame's URL.
+ viewFrameSource() {
+ BrowserViewSourceOfDocument({
+ browser: this.browser,
+ URL: this.contentData.docLocation,
+ outerWindowID: this.frameOuterWindowID,
+ });
+ }
+
+ viewInfo() {
+ BrowserPageInfo(
+ this.contentData.docLocation,
+ null,
+ null,
+ null,
+ this.browser
+ );
+ }
+
+ viewImageInfo() {
+ BrowserPageInfo(
+ this.contentData.docLocation,
+ "mediaTab",
+ this.imageInfo,
+ null,
+ this.browser
+ );
+ }
+
+ viewImageDesc(e) {
+ urlSecurityCheck(
+ this.imageDescURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ openUILink(this.imageDescURL, e, {
+ referrerInfo: this.contentData.referrerInfo,
+ triggeringPrincipal: this.principal,
+ triggeringRemoteType: this.remoteType,
+ csp: this.csp,
+ });
+ }
+
+ viewFrameInfo() {
+ BrowserPageInfo(
+ this.contentData.docLocation,
+ null,
+ null,
+ this.actor.browsingContext,
+ this.browser
+ );
+ }
+
+ reloadImage() {
+ urlSecurityCheck(
+ this.mediaURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+ this.actor.reloadImage(this.targetIdentifier);
+ }
+
+ _canvasToBlobURL(targetIdentifier) {
+ return this.actor.canvasToBlobURL(targetIdentifier);
+ }
+
+ // Change current window to the URL of the image, video, or audio.
+ viewMedia(e) {
+ let where = whereToOpenLink(e, false, false);
+ if (where == "current") {
+ where = "tab";
+ }
+ let referrerInfo = this.contentData.referrerInfo;
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ if (this.onCanvas) {
+ this._canvasToBlobURL(this.targetIdentifier).then(function (blobURL) {
+ openLinkIn(blobURL, where, {
+ referrerInfo,
+ triggeringPrincipal: systemPrincipal,
+ });
+ }, console.error);
+ } else {
+ urlSecurityCheck(
+ this.mediaURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+
+ // Default to opening in a new tab.
+ openLinkIn(this.mediaURL, where, {
+ referrerInfo,
+ forceAllowDataURI: true,
+ triggeringPrincipal: this.principal,
+ triggeringRemoteType: this.remoteType,
+ csp: this.csp,
+ });
+ }
+ }
+
+ saveVideoFrameAsImage() {
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+
+ let name = "";
+ if (this.mediaURL) {
+ try {
+ let uri = makeURI(this.mediaURL);
+ let url = uri.QueryInterface(Ci.nsIURL);
+ if (url.fileBaseName) {
+ name = decodeURI(url.fileBaseName) + ".jpg";
+ }
+ } catch (e) {}
+ }
+ if (!name) {
+ name = "snapshot.jpg";
+ }
+
+ // Cache this because we fetch the data async
+ let referrerInfo = this.contentData.referrerInfo;
+ let cookieJarSettings = this.contentData.cookieJarSettings;
+
+ this.actor.saveVideoFrameAsImage(this.targetIdentifier).then(dataURL => {
+ // FIXME can we switch this to a blob URL?
+ internalSave(
+ dataURL,
+ null, // originalURL
+ null, // document
+ name,
+ null, // content disposition
+ "image/jpeg", // content type - keep in sync with ContextMenuChild!
+ true, // bypass cache
+ "SaveImageTitle",
+ null, // chosen data
+ referrerInfo,
+ cookieJarSettings,
+ null, // initiating doc
+ false, // don't skip prompt for where to save
+ null, // cache key
+ isPrivate,
+ this.principal
+ );
+ });
+ }
+
+ leaveDOMFullScreen() {
+ document.exitFullscreen();
+ }
+
+ // Change current window to the URL of the background image.
+ viewBGImage(e) {
+ urlSecurityCheck(
+ this.bgImageURL,
+ this.principal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
+ );
+
+ openUILink(this.bgImageURL, e, {
+ referrerInfo: this.contentData.referrerInfo,
+ forceAllowDataURI: true,
+ triggeringPrincipal: this.principal,
+ triggeringRemoteType: this.remoteType,
+ csp: this.csp,
+ });
+ }
+
+ setDesktopBackground() {
+ if (!Services.policies.isAllowed("setDesktopBackground")) {
+ return;
+ }
+
+ this.actor
+ .setAsDesktopBackground(this.targetIdentifier)
+ .then(({ failed, dataURL, imageName }) => {
+ if (failed) {
+ return;
+ }
+
+ let image = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "img"
+ );
+ image.src = dataURL;
+
+ // Confirm since it's annoying if you hit this accidentally.
+ const kDesktopBackgroundURL =
+ "chrome://browser/content/setDesktopBackground.xhtml";
+
+ if (AppConstants.platform == "macosx") {
+ // On Mac, the Set Desktop Background window is not modal.
+ // Don't open more than one Set Desktop Background window.
+ let dbWin = Services.wm.getMostRecentWindow(
+ "Shell:SetDesktopBackground"
+ );
+ if (dbWin) {
+ dbWin.gSetBackground.init(image, imageName);
+ dbWin.focus();
+ } else {
+ openDialog(
+ kDesktopBackgroundURL,
+ "",
+ "centerscreen,chrome,dialog=no,dependent,resizable=no",
+ image,
+ imageName
+ );
+ }
+ } else {
+ // On non-Mac platforms, the Set Wallpaper dialog is modal.
+ openDialog(
+ kDesktopBackgroundURL,
+ "",
+ "centerscreen,chrome,dialog,modal,dependent",
+ image,
+ imageName
+ );
+ }
+ });
+ }
+
+ // Save URL of clicked-on frame.
+ saveFrame() {
+ saveBrowser(this.browser, false, this.frameBrowsingContext);
+ }
+
+ // Helper function to wait for appropriate MIME-type headers and
+ // then prompt the user with a file picker
+ saveHelper(
+ linkURL,
+ linkText,
+ dialogTitle,
+ bypassCache,
+ doc,
+ referrerInfo,
+ cookieJarSettings,
+ windowID,
+ linkDownload,
+ isContentWindowPrivate
+ ) {
+ // 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(principal) {
+ this._triggeringPrincipal = principal;
+ }
+ saveAsListener.prototype = {
+ extListener: null,
+
+ onStartRequest: function saveLinkAs_onStartRequest(aRequest) {
+ // 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;
+ }
+
+ timer.cancel();
+
+ // some other error occured; notify the user...
+ if (!Components.isSuccessCode(aRequest.status)) {
+ try {
+ const l10n = new Localization(["browser/downloads.ftl"], true);
+
+ let msg = null;
+ try {
+ const channel = aRequest.QueryInterface(Ci.nsIChannel);
+ const reason = channel.loadInfo.requestBlockingReason;
+ if (
+ reason == Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST
+ ) {
+ try {
+ const properties = channel.QueryInterface(Ci.nsIPropertyBag);
+ const id = properties.getProperty("cancelledByExtension");
+ msg = l10n.formatValueSync("downloads-error-blocked-by", {
+ extension: WebExtensionPolicy.getByID(id).name,
+ });
+ } catch (err) {
+ // "cancelledByExtension" doesn't have to be available.
+ msg = l10n.formatValueSync("downloads-error-extension");
+ }
+ }
+ } catch (ex) {}
+ msg ??= l10n.formatValueSync("downloads-error-generic");
+
+ const window = Services.wm.getOuterWindowWithId(windowID);
+ const title = l10n.formatValueSync("downloads-error-alert-title");
+ Services.prompt.alert(window, title, msg);
+ } catch (ex) {}
+ return;
+ }
+
+ let extHelperAppSvc = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsIExternalHelperAppService);
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ this.extListener = extHelperAppSvc.doContent(
+ channel.contentType,
+ aRequest,
+ null,
+ true,
+ window
+ );
+ this.extListener.onStartRequest(aRequest);
+ },
+
+ onStopRequest: function saveLinkAs_onStopRequest(aRequest, 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,
+ null,
+ linkText,
+ dialogTitle,
+ bypassCache,
+ false,
+ referrerInfo,
+ cookieJarSettings,
+ doc,
+ isContentWindowPrivate,
+ this._triggeringPrincipal
+ );
+ }
+ if (this.extListener) {
+ this.extListener.onStopRequest(aRequest, aStatusCode);
+ }
+ },
+
+ onDataAvailable: function saveLinkAs_onDataAvailable(
+ aRequest,
+ aInputStream,
+ aOffset,
+ aCount
+ ) {
+ this.extListener.onDataAvailable(
+ aRequest,
+ aInputStream,
+ aOffset,
+ aCount
+ );
+ },
+ };
+
+ function callbacks() {}
+ callbacks.prototype = {
+ getInterface: function sLA_callbacks_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 Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+ };
+
+ // if it we don't have the headers after a short time, the user
+ // won't have received any feedback from their click. that's bad. so
+ // we give up waiting for the filename.
+ function timerCallback() {}
+ timerCallback.prototype = {
+ notify: function sLA_timer_notify(aTimer) {
+ channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
+ },
+ };
+
+ // setting up a new channel for 'right click - save link as ...'
+ var channel = NetUtil.newChannel({
+ uri: makeURI(linkURL),
+ loadingPrincipal: this.principal,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ });
+
+ if (linkDownload) {
+ channel.contentDispositionFilename = linkDownload;
+ }
+ if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
+ let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ channel.setPrivate(docIsPrivate);
+ }
+ channel.notificationCallbacks = new callbacks();
+
+ let 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.nsIHttpChannel) {
+ channel.referrerInfo = referrerInfo;
+ if (channel instanceof Ci.nsIHttpChannelInternal) {
+ channel.forceAllowThirdPartyCookie = true;
+ }
+
+ channel.loadInfo.cookieJarSettings = cookieJarSettings;
+ }
+
+ // fallback to the old way if we don't see the headers quickly
+ var timeToWait = Services.prefs.getIntPref(
+ "browser.download.saveLinkAsFilenameTimeout"
+ );
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(
+ new timerCallback(),
+ timeToWait,
+ timer.TYPE_ONE_SHOT
+ );
+
+ // kick off the channel with our proxy object as the listener
+ channel.asyncOpen(new saveAsListener(this.principal));
+ }
+
+ // Save URL of clicked-on link.
+ saveLink() {
+ let referrerInfo = this.onLink
+ ? this.contentData.linkReferrerInfo
+ : this.contentData.referrerInfo;
+
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ this.saveHelper(
+ this.linkURL,
+ this.linkTextStr,
+ null,
+ true,
+ this.ownerDoc,
+ referrerInfo,
+ this.contentData.cookieJarSettings,
+ this.frameOuterWindowID,
+ this.linkDownload,
+ isPrivate
+ );
+ }
+
+ // Backwards-compatibility wrapper
+ saveImage() {
+ if (this.onCanvas || this.onImage) {
+ this.saveMedia();
+ }
+ }
+
+ // Save URL of the clicked upon image, video, or audio.
+ saveMedia() {
+ let doc = this.ownerDoc;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ let referrerInfo = this.contentData.referrerInfo;
+ let cookieJarSettings = this.contentData.cookieJarSettings;
+ if (this.onCanvas) {
+ // Bypass cache, since it's a data: URL.
+ this._canvasToBlobURL(this.targetIdentifier).then(function (blobURL) {
+ internalSave(
+ blobURL,
+ null, // originalURL
+ null, // document
+ "canvas.png",
+ null, // content disposition
+ "image/png", // _canvasToBlobURL uses image/png by default.
+ true, // bypass cache
+ "SaveImageTitle",
+ null, // chosen data
+ referrerInfo,
+ cookieJarSettings,
+ null, // initiating doc
+ false, // don't skip prompt for where to save
+ null, // cache key
+ isPrivate,
+ document.nodePrincipal /* system, because blob: */
+ );
+ }, console.error);
+ } else if (this.onImage) {
+ urlSecurityCheck(this.mediaURL, this.principal);
+ internalSave(
+ this.mediaURL,
+ null, // originalURL
+ null, // document
+ null, // file name; we'll take it from the URL
+ this.contentData.contentDisposition,
+ this.contentData.contentType,
+ false, // do not bypass the cache
+ "SaveImageTitle",
+ null, // chosen data
+ referrerInfo,
+ cookieJarSettings,
+ null, // initiating doc
+ false, // don't skip prompt for where to save
+ null, // cache key
+ isPrivate,
+ this.principal
+ );
+ } else if (this.onVideo || this.onAudio) {
+ let defaultFileName = "";
+ if (this.mediaURL.startsWith("data")) {
+ // Use default file name "Untitled" for data URIs
+ defaultFileName = ContentAreaUtils.stringBundle.GetStringFromName(
+ "UntitledSaveFileName"
+ );
+ }
+
+ var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
+ this.saveHelper(
+ this.mediaURL,
+ null,
+ dialogTitle,
+ false,
+ doc,
+ referrerInfo,
+ cookieJarSettings,
+ this.frameOuterWindowID,
+ defaultFileName,
+ isPrivate
+ );
+ }
+ }
+
+ // Backwards-compatibility wrapper
+ sendImage() {
+ if (this.onCanvas || this.onImage) {
+ this.sendMedia();
+ }
+ }
+
+ sendMedia() {
+ MailIntegration.sendMessage(this.mediaURL, "");
+ }
+
+ // Generate email address and put it on 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.
+ var url = this.linkURL;
+ var qmark = url.indexOf("?");
+ var addresses;
+
+ // 7 == length of "mailto:"
+ addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);
+
+ // Let's try to unescape it using a character set
+ // in case the address is not ASCII.
+ try {
+ addresses = Services.textToSubURI.unEscapeURIForUI(addresses);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(addresses);
+ }
+
+ // Extract phone and put it on clipboard
+ copyPhone() {
+ // Copies the phone number only. We won't be doing any complex parsing
+ var url = this.linkURL;
+ var phone = url.substr(4);
+
+ // Let's try to unescape it using a character set
+ // in case the phone number is not ASCII.
+ try {
+ phone = Services.textToSubURI.unEscapeURIForUI(phone);
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(phone);
+ }
+
+ copyLink() {
+ // If we're in a view source tab, remove the view-source: prefix
+ let linkURL = this.linkURL.replace(/^view-source:/, "");
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(linkURL);
+ }
+
+ /**
+ * Copies a stripped version of this.linkURI to the clipboard.
+ * 'Stripped' means that query parameters for tracking/ link decoration
+ * that are known to us will be removed from the URI.
+ */
+ copyStrippedLink() {
+ let strippedLinkURI = this.getStrippedLink();
+ let strippedLinkURL =
+ Services.io.createExposableURI(strippedLinkURI)?.displaySpec;
+ if (strippedLinkURL) {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(strippedLinkURL);
+ }
+ }
+
+ addKeywordForSearchField() {
+ this.actor.getSearchFieldBookmarkData(this.targetIdentifier).then(data => {
+ let title = gNavigatorBundle.getFormattedString(
+ "addKeywordTitleAutoFill",
+ [data.title]
+ );
+ PlacesUIUtils.showBookmarkDialog(
+ {
+ action: "add",
+ type: "bookmark",
+ uri: makeURI(data.spec),
+ title,
+ keyword: "",
+ postData: data.postData,
+ charSet: data.charset,
+ hiddenRows: ["location", "tags"],
+ },
+ window
+ );
+ });
+ }
+
+ /**
+ * Utilities
+ */
+
+ /**
+ * Show/hide one item (specified via name or the item element itself).
+ * If the element is not found, then this function finishes silently.
+ *
+ * @param {Element|String} aItemOrId The item element or the name of the element
+ * to show.
+ * @param {Boolean} aShow Set to true to show the item, false to hide it.
+ */
+ showItem(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(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);
+ }
+ }
+ }
+
+ // Temporary workaround for DOM api not yet implemented by XUL nodes.
+ cloneNode(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;
+ }
+
+ getLinkURI() {
+ try {
+ return makeURI(this.linkURL);
+ } catch (ex) {
+ // e.g. empty URL string
+ }
+
+ return null;
+ }
+
+ /**
+ * Strips any known query params from the link URI.
+ * @returns {nsIURI|null} - the stripped version of the URI,
+ * or the original URI if we could not strip any query parameter.
+ *
+ */
+ getStrippedLink() {
+ if (!this.linkURI) {
+ return null;
+ }
+ let strippedLinkURI = null;
+ try {
+ strippedLinkURI = QueryStringStripper.stripForCopyOrShare(this.linkURI);
+ } catch (e) {
+ console.warn(`isLinkURIStrippable: ${e.message}`);
+ return null;
+ }
+
+ // If nothing can be stripped, we return the original URI
+ // so the feature can still be used.
+ return strippedLinkURI ?? this.linkURI;
+ }
+
+ /**
+ * Checks if a webpage is a secure interal webpage
+ * @returns {Boolean}
+ *
+ */
+ isSecureAboutPage() {
+ let { currentURI } = this.browser;
+ if (currentURI?.schemeIs("about")) {
+ let module = E10SUtils.getAboutModule(currentURI);
+ if (module) {
+ let flags = module.getURIFlags(currentURI);
+ return !!(flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI);
+ }
+ }
+ return false;
+ }
+
+ // Kept for addon compat
+ linkText() {
+ return this.linkTextStr;
+ }
+
+ // Determines whether or not the separator with the specified ID should be
+ // shown or not by determining if there are any non-hidden items between it
+ // and the previous separator.
+ shouldShowSeparator(aSeparatorID) {
+ var separator = document.getElementById(aSeparatorID);
+ if (separator) {
+ var sibling = separator.previousSibling;
+ while (sibling && sibling.localName != "menuseparator") {
+ if (!sibling.hidden) {
+ return true;
+ }
+ sibling = sibling.previousSibling;
+ }
+ }
+ return false;
+ }
+
+ shouldShowAddKeyword() {
+ return this.onTextInput && this.onKeywordField && !this.isLoginForm();
+ }
+
+ addDictionaries() {
+ var uri = Services.urlFormatter.formatURLPref(
+ "browser.dictionaries.download.url"
+ );
+
+ var locale = "-";
+ try {
+ locale = Services.prefs.getComplexValue(
+ "intl.accept_languages",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {}
+
+ var version = "-";
+ try {
+ version = Services.appinfo.version;
+ } catch (e) {}
+
+ uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
+
+ var newWindowPref = Services.prefs.getIntPref(
+ "browser.link.open_newwindow"
+ );
+ var where = newWindowPref == 3 ? "tab" : "window";
+
+ openTrustedLinkIn(uri, where);
+ }
+
+ bookmarkThisPage() {
+ window.top.PlacesCommandHook.bookmarkPage().catch(console.error);
+ }
+
+ bookmarkLink() {
+ window.top.PlacesCommandHook.bookmarkLink(
+ this.linkURL,
+ this.linkTextStr
+ ).catch(console.error);
+ }
+
+ addBookmarkForFrame() {
+ let uri = this.contentData.documentURIObject;
+
+ this.actor.getFrameTitle(this.targetIdentifier).then(title => {
+ window.top.PlacesCommandHook.bookmarkLink(uri.spec, title).catch(
+ console.error
+ );
+ });
+ }
+
+ savePageAs() {
+ saveBrowser(this.browser);
+ }
+
+ printFrame() {
+ PrintUtils.startPrintWindow(this.actor.browsingContext, {
+ printFrameOnly: true,
+ });
+ }
+
+ printSelection() {
+ PrintUtils.startPrintWindow(this.actor.browsingContext, {
+ printSelectionOnly: true,
+ });
+ }
+
+ switchPageDirection() {
+ gBrowser.selectedBrowser.sendMessageToActor(
+ "SwitchDocumentDirection",
+ {},
+ "SwitchDocumentDirection",
+ "roots"
+ );
+ }
+
+ mediaCommand(command, data) {
+ this.actor.mediaCommand(this.targetIdentifier, command, data);
+ }
+
+ copyMediaLocation() {
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ clipboard.copyString(this.originalMediaURL);
+ }
+
+ getImageText() {
+ let dialogBox = gBrowser.getTabDialogBox(this.browser);
+ const imageTextResult = this.actor.getImageText(this.targetIdentifier);
+ TelemetryStopwatch.start(
+ "TEXT_RECOGNITION_API_PERFORMANCE",
+ imageTextResult
+ );
+ const { dialog } = dialogBox.open(
+ "chrome://browser/content/textrecognition/textrecognition.html",
+ {
+ features: "resizable=no",
+ modalType: Services.prompt.MODAL_TYPE_CONTENT,
+ },
+ imageTextResult,
+ () => dialog.resizeVertically(),
+ openLinkIn
+ );
+ }
+
+ drmLearnMore(aEvent) {
+ let drmInfoURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "drm-content";
+ let dest = whereToOpenLink(aEvent);
+ // Don't ever want this to open in the same tab as it'll unload the
+ // DRM'd video, which is going to be a bad idea in most cases.
+ if (dest == "current") {
+ dest = "tab";
+ }
+ openTrustedLinkIn(drmInfoURL, dest);
+ }
+
+ /**
+ * Retrieves an instance of the TranslationsParent actor.
+ * @returns {TranslationsParent} - The TranslationsParent actor.
+ * @throws Throws if an instance of the actor cannot be retrieved.
+ */
+ static #getTranslationsActor() {
+ const actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
+ "Translations"
+ );
+
+ if (!actor) {
+ throw new Error("Unable to get the TranslationsParent");
+ }
+ return actor;
+ }
+
+ /**
+ * Determines if Full Page Translations is currently active on this page.
+ *
+ * @returns {boolean}
+ */
+ static #isFullPageTranslationsActive() {
+ try {
+ const { requestedTranslationPair } =
+ this.#getTranslationsActor().languageState;
+ return requestedTranslationPair !== null;
+ } catch {
+ // Failed to retrieve the Full Page Translations actor, do nothing.
+ }
+ return false;
+ }
+
+ /**
+ * Displays or hides as well as localizes the translate-selection item in the context menu.
+ */
+ async showTranslateSelectionItem() {
+ const translateSelectionItem = document.getElementById(
+ "context-translate-selection"
+ );
+ const translationsEnabled = Services.prefs.getBoolPref(
+ "browser.translations.enable"
+ );
+ const selectTranslationsEnabled = Services.prefs.getBoolPref(
+ "browser.translations.select.enable"
+ );
+
+ // Selected text takes precedence over link text.
+ const translatableText = this.isTextSelected
+ ? this.selectedText.trim()
+ : this.linkTextStr.trim();
+
+ translateSelectionItem.hidden =
+ // Only show the item if the feature is enabled.
+ !(translationsEnabled && selectTranslationsEnabled) ||
+ // If there is no text to translate, we have nothing to do.
+ translatableText.length === 0 ||
+ // We do not allow translating selections on top of Full Page Translations.
+ nsContextMenu.#isFullPageTranslationsActive();
+
+ if (translateSelectionItem.hidden) {
+ return;
+ }
+
+ const preferredLanguages =
+ nsContextMenu.TranslationsParent.getPreferredLanguages();
+ const topPreferredLanguage = preferredLanguages[0];
+
+ if (topPreferredLanguage) {
+ const { language } = await nsContextMenu.LanguageDetector.detectLanguage(
+ translatableText
+ );
+ if (topPreferredLanguage !== language) {
+ try {
+ const dn = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ });
+ document.l10n.setAttributes(
+ translateSelectionItem,
+ this.isTextSelected
+ ? "main-context-menu-translate-selection-to-language"
+ : "main-context-menu-translate-link-text-to-language",
+ { language: dn.of(topPreferredLanguage) }
+ );
+ return;
+ } catch {
+ // Services.intl.DisplayNames.of threw, do nothing.
+ }
+ }
+ }
+
+ document.l10n.setAttributes(
+ translateSelectionItem,
+ this.isTextSelected
+ ? "main-context-menu-translate-selection"
+ : "main-context-menu-translate-link-text"
+ );
+ }
+
+ // Formats the 'Search <engine> for "<selection or link text>"' context menu.
+ showAndFormatSearchContextItem() {
+ let menuItem = document.getElementById("context-searchselect");
+ let menuItemPrivate = document.getElementById(
+ "context-searchselect-private"
+ );
+ if (!Services.search.isInitialized) {
+ menuItem.hidden = true;
+ menuItemPrivate.hidden = true;
+ return;
+ }
+ const docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ const privatePref = "browser.search.separatePrivateDefault.ui.enabled";
+ let showSearchSelect =
+ !this.inAboutDevtoolsToolbox &&
+ (this.isTextSelected || this.onLink) &&
+ !this.onImage;
+ // Don't show the private search item when we're already in a private
+ // browsing window.
+ let showPrivateSearchSelect =
+ showSearchSelect &&
+ !docIsPrivate &&
+ Services.prefs.getBoolPref(privatePref);
+
+ menuItem.hidden = !showSearchSelect;
+ menuItemPrivate.hidden = !showPrivateSearchSelect;
+ let frameSeparator = document.getElementById("frame-sep");
+
+ // Add a divider between "Search X for Y" and "This Frame", and between "Search X for Y" and "Check Spelling",
+ // but no divider in other cases.
+ frameSeparator.toggleAttribute(
+ "ensureHidden",
+ !showSearchSelect && this.inFrame
+ );
+ // If we're not showing the menu items, we can skip formatting the labels.
+ if (!showSearchSelect) {
+ return;
+ }
+
+ let selectedText = this.isTextSelected
+ ? this.selectedText
+ : this.linkTextStr;
+
+ // Store searchTerms in context menu item so we know what to search onclick
+ menuItem.searchTerms = menuItemPrivate.searchTerms = selectedText;
+ menuItem.principal = menuItemPrivate.principal = this.principal;
+ menuItem.csp = menuItemPrivate.csp = this.csp;
+
+ // Copied to alert.js' prefillAlertInfo().
+ // If the JS character after our truncation point is a trail surrogate,
+ // include it in the truncated string to avoid splitting a surrogate pair.
+ if (selectedText.length > 15) {
+ let truncLength = 15;
+ let truncChar = selectedText[15].charCodeAt(0);
+ if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
+ truncLength++;
+ }
+ selectedText = selectedText.substr(0, truncLength) + this.ellipsis;
+ }
+
+ // format "Search <engine> for <selection>" string to show in menu
+ let engineName = Services.search.defaultEngine.name;
+ let privateEngineName = Services.search.defaultPrivateEngine.name;
+ menuItem.usePrivate = docIsPrivate;
+ let menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", [
+ docIsPrivate ? privateEngineName : engineName,
+ selectedText,
+ ]);
+ menuItem.label = menuLabel;
+ menuItem.accessKey = gNavigatorBundle.getString(
+ "contextMenuSearch.accesskey"
+ );
+
+ if (showPrivateSearchSelect) {
+ let otherEngine = engineName != privateEngineName;
+ let accessKey = "contextMenuPrivateSearch.accesskey";
+ if (otherEngine) {
+ menuItemPrivate.label = gNavigatorBundle.getFormattedString(
+ "contextMenuPrivateSearchOtherEngine",
+ [privateEngineName]
+ );
+ accessKey = "contextMenuPrivateSearchOtherEngine.accesskey";
+ } else {
+ menuItemPrivate.label = gNavigatorBundle.getString(
+ "contextMenuPrivateSearch"
+ );
+ }
+ menuItemPrivate.accessKey = gNavigatorBundle.getString(accessKey);
+ }
+ }
+
+ createContainerMenu(aEvent) {
+ let createMenuOptions = {
+ isContextMenu: true,
+ excludeUserContextId: this.contentData.userContextId,
+ };
+ return createUserContextMenu(aEvent, createMenuOptions);
+ }
+}
+
+ChromeUtils.defineESModuleGetters(nsContextMenu, {
+ DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
+ LanguageDetector:
+ "resource://gre/modules/translation/LanguageDetector.sys.mjs",
+ LoginManagerContextMenu:
+ "resource://gre/modules/LoginManagerContextMenu.sys.mjs",
+ TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
+ WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "screenshotsDisabled",
+ "extensions.screenshots.disabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "SCREENSHOT_BROWSER_COMPONENT",
+ "screenshots.browser.component.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "REVEAL_PASSWORD_ENABLED",
+ "layout.forms.reveal-password-context-menu.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "TEXT_RECOGNITION_ENABLED",
+ "dom.text-recognition.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "STRIP_ON_SHARE_ENABLED",
+ "privacy.query_stripping.strip_on_share.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "QueryStringStripper",
+ "@mozilla.org/url-query-string-stripper;1",
+ "nsIURLQueryStringStripper"
+);