gecko-dev/browser/base/content/nsContextMenu.js

1582 строки
63 KiB
JavaScript

/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
LoginManagerContextMenu: "resource://gre/modules/LoginManagerContextMenu.jsm",
WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
DevToolsShim: "chrome://devtools-shim/content/DevToolsShim.jsm",
});
var gContextMenuContentData = null;
function setContextMenuContentData(data) {
gContextMenuContentData = data;
}
function openContextMenu(aMessage) {
let data = aMessage.data;
let browser = aMessage.target;
let spellInfo = data.spellInfo;
// ContextMenu.jsm sends us the target as a CPOW only so that
// we can send that CPOW back down to the content process and
// have it resolve to a DOM node. The parent should not attempt
// to access any properties on this CPOW (in fact, doing so
// will throw an exception).
data.context.targetAsCPOW = aMessage.objects.targetAsCPOW;
if (spellInfo) {
spellInfo.target = aMessage.target.messageManager;
}
let documentURIObject = makeURI(data.docLocation,
data.charSet,
makeURI(data.baseURI));
gContextMenuContentData = { context: data.context,
isRemote: data.isRemote,
popupNodeSelectors: data.popupNodeSelectors,
browser,
editFlags: data.editFlags,
spellInfo,
principal: data.principal,
customMenuItems: data.customMenuItems,
documentURIObject,
docLocation: data.docLocation,
charSet: data.charSet,
referrer: data.referrer,
referrerPolicy: data.referrerPolicy,
contentType: data.contentType,
contentDisposition: data.contentDisposition,
frameOuterWindowID: data.frameOuterWindowID,
selectionInfo: data.selectionInfo,
disableSetDesktopBackground: data.disableSetDesktopBg,
loginFillInfo: data.loginFillInfo,
parentAllowsMixedContent: data.parentAllowsMixedContent,
userContextId: data.userContextId,
};
let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
let context = gContextMenuContentData.context;
// The event is a CPOW that can't be passed into the native openPopupAtScreen
// function. Therefore we synthesize a new MouseEvent to propagate the
// inputSource to the subsequently triggered popupshowing event.
var newEvent = document.createEvent("MouseEvent");
newEvent.initNSMouseEvent("contextmenu", true, true, null, 0, context.screenX, context.screenY,
0, 0, false, false, false, false, 0, null, 0, context.mozInputSource);
popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
}
function nsContextMenu(aXulMenu, aIsShift) {
this.shouldDisplay = true;
this.initMenu(aXulMenu, aIsShift);
}
// Prototype for nsContextMenu "class."
nsContextMenu.prototype = {
initMenu: function CM_initMenu(aXulMenu, aIsShift) {
// Get contextual info.
this.setContext();
if (!this.shouldDisplay)
return;
this.hasPageMenu = false;
this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
if (!aIsShift) {
if (this.isRemote) {
this.hasPageMenu =
PageMenuParent.addToPopup(gContextMenuContentData.customMenuItems,
this.browser, aXulMenu);
} else {
this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
}
let subject = {
menu: aXulMenu,
tab: gBrowser ? gBrowser.getTabForBrowser(this.browser) : undefined,
isContentSelected: this.isContentSelected,
inFrame: this.inFrame,
isTextSelected: this.isTextSelected,
onTextInput: this.onTextInput,
onLink: this.onLink,
onImage: this.onImage,
onVideo: this.onVideo,
onAudio: this.onAudio,
onCanvas: this.onCanvas,
onEditableArea: this.onEditableArea,
onPassword: this.onPassword,
srcUrl: this.mediaURL,
frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined,
pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
linkText: this.linkTextStr,
linkUrl: this.linkURL,
selectionText: this.isTextSelected ? this.selectionInfo.fullText : undefined,
frameId: this.frameOuterWindowID,
};
subject.wrappedJSObject = subject;
Services.obs.notifyObservers(subject, "on-build-contextmenu");
}
this.isFrameImage = document.getElementById("isFrameImage");
this.ellipsis = "\u2026";
try {
this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
Ci.nsIPrefLocalizedString).data;
} catch (e) { }
// Reset after "on-build-contextmenu" notification in case selection was
// changed during the notification.
this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
this.onPlainTextLink = false;
let bookmarkPage = document.getElementById("context-bookmarkpage");
if (bookmarkPage)
BookmarkingUI.onCurrentPageContextPopupShowing();
// Initialize (disable/remove) menu items.
this.initItems();
// Register this opening of the menu with telemetry:
this._checkTelemetryForMenu(aXulMenu);
},
setContext() {
let context = Object.create(null);
this.isRemote = false;
if (gContextMenuContentData) {
context = gContextMenuContentData.context;
gContextMenuContentData.context = null;
this.isRemote = gContextMenuContentData.isRemote;
}
this.shouldDisplay = context.shouldDisplay;
// Assign what's _possibly_ needed from `context` sent by ContextMenu.jsm
// Keep this consistent with the similar code in ContextMenu's _setContext
this.bgImageURL = context.bgImageURL;
this.imageDescURL = context.imageDescURL;
this.imageInfo = context.imageInfo;
this.mediaURL = context.mediaURL;
this.webExtBrowserType = context.webExtBrowserType;
this.canSpellCheck = context.canSpellCheck;
this.hasBGImage = context.hasBGImage;
this.hasMultipleBGImages = context.hasMultipleBGImages;
this.isDesignMode = context.isDesignMode;
this.inFrame = context.inFrame;
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.linkHasNoReferrer = context.linkHasNoReferrer;
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.onCTPPlugin = context.onCTPPlugin;
this.onDRMMedia = context.onDRMMedia;
this.onEditableArea = context.onEditableArea;
this.onImage = context.onImage;
this.onKeywordField = context.onKeywordField;
this.onLink = context.onLink;
this.onLoadedImage = context.onLoadedImage;
this.onMailtoLink = context.onMailtoLink;
this.onMathML = context.onMathML;
this.onMozExtLink = context.onMozExtLink;
this.onNumeric = context.onNumeric;
this.onPassword = context.onPassword;
this.onSaveableLink = context.onSaveableLink;
this.onTextInput = context.onTextInput;
this.onVideo = context.onVideo;
this.target = this.isRemote ? context.target : document.popupNode;
this.targetAsCPOW = context.targetAsCPOW;
this.principal = context.principal;
this.frameOuterWindowID = context.frameOuterWindowID;
this.inSyntheticDoc = context.inSyntheticDoc;
// Everything after this isn't sent directly from ContextMenu
this.ownerDoc = this.target.ownerDocument;
// Remember the CSS selectors corresponding to clicked node. gContextMenuContentData
// can be null if the menu was triggered by tests in which case use an empty array.
this.targetSelectors = gContextMenuContentData
? gContextMenuContentData.popupNodeSelectors
: [];
if (this.isRemote) {
this.browser = gContextMenuContentData.browser;
this.selectionInfo = gContextMenuContentData.selectionInfo;
} else {
this.browser = this.ownerDoc.defaultView
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
this.selectionInfo = BrowserUtils.getSelectionDetails(window);
}
this.textSelected = this.selectionInfo.text;
this.isTextSelected = this.textSelected.length != 0;
this.webExtBrowserType = this.browser.getAttribute("webextension-view-type");
this.inWebExtBrowser = !!this.webExtBrowserType;
this.inTabBrowser = this.browser.ownerGlobal.gBrowser ?
!!this.browser.ownerGlobal.gBrowser.getTabForBrowser(this.browser) : false;
if (context.shouldInitInlineSpellCheckerUINoChildren) {
if (this.isRemote) {
InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
} else {
InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
document.popupRangeOffset);
}
}
if (context.shouldInitInlineSpellCheckerUIWithChildren) {
if (this.isRemote) {
InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
} else {
var targetWin = this.ownerDoc.defaultView;
var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIEditingSession);
InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
document.popupRangeOffset);
}
let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
this.showItem("spell-check-enabled", canSpell);
this.showItem("spell-separator", canSpell);
}
}, // setContext
hiding: function CM_hiding() {
if (this.browser) {
this.browser.messageManager.sendAsyncMessage("ContextMenu:Hiding");
}
gContextMenuContentData = null;
InlineSpellCheckerUI.clearSuggestionsFromMenu();
InlineSpellCheckerUI.clearDictionaryListFromMenu();
InlineSpellCheckerUI.uninit();
if (Cu.isModuleLoaded("resource://gre/modules/LoginManagerContextMenu.jsm")) {
LoginManagerContextMenu.clearLoginsFromMenu(document);
}
// This handler self-deletes, only run it if it is still there:
if (this._onPopupHiding) {
this._onPopupHiding();
}
},
initItems: function CM_initItems() {
this.initPageMenuSeparator();
this.initOpenItems();
this.initNavigationItems();
this.initViewItems();
this.initMiscItems();
this.initSpellingItems();
this.initSaveItems();
this.initClipboardItems();
this.initMediaPlayerItems();
this.initLeaveDOMFullScreenItems();
this.initClickToPlayItems();
this.initPasswordManagerItems();
this.initSyncItems();
},
initPageMenuSeparator: function CM_initPageMenuSeparator() {
this.showItem("page-menu-separator", this.hasPageMenu);
},
initOpenItems: function CM_initOpenItems() {
var isMailtoInternal = false;
if (this.onMailtoLink) {
var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
getService(Ci.nsIExternalProtocolService).
getProtocolHandlerInfo("mailto");
isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling &&
mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
(mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp));
}
if (this.isTextSelected && !this.onLink &&
this.selectionInfo && this.selectionInfo.linkURL) {
this.linkURL = this.selectionInfo.linkURL;
try {
this.linkURI = makeURI(this.linkURL);
} catch (ex) {}
this.linkTextStr = this.selectionInfo.linkText;
this.onPlainTextLink = true;
}
var inContainer = false;
if (gContextMenuContentData.userContextId) {
inContainer = true;
var item = document.getElementById("context-openlinkincontainertab");
item.setAttribute("data-usercontextid", gContextMenuContentData.userContextId);
var label =
ContextualIdentityService.getUserContextLabel(gContextMenuContentData.userContextId);
item.setAttribute("label",
gBrowserBundle.formatStringFromName("userContextOpenLink.label",
[label], 1));
}
var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled");
this.showItem("context-openlink", shouldShow && !isWindowPrivate);
this.showItem("context-openlinkprivate", shouldShow);
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);
this.showItem("context-sep-open", shouldShow);
},
initNavigationItems: function CM_initNavigationItems() {
var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
this.onCanvas || this.onVideo || this.onAudio ||
this.onTextInput) && this.inTabBrowser;
this.showItem("context-navigation", shouldShow);
this.showItem("context-sep-navigation", shouldShow);
let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
let stopReloadItem = "";
if (shouldShow || !this.inTabBrowser) {
stopReloadItem = (stopped || !this.inTabBrowser) ? "reload" : "stop";
}
this.showItem("context-reload", stopReloadItem == "reload");
this.showItem("context-stop", stopReloadItem == "stop");
// XXX: Stop is determined in browser.js; the canStop broadcaster is broken
// this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
},
initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() {
// only show the option if the user is in DOM fullscreen
var shouldShow = this.target.ownerDocument.fullscreen;
this.showItem("context-leave-dom-fullscreen", shouldShow);
// Explicitly show if in DOM fullscreen, but do not hide it has already been shown
if (shouldShow)
this.showItem("context-media-sep-commands", true);
},
initSaveItems: function CM_initSaveItems() {
var shouldShow = !(this.onTextInput || this.onLink ||
this.isContentSelected || this.onImage ||
this.onCanvas || this.onVideo || this.onAudio);
this.showItem("context-savepage", shouldShow);
// Save link depends on whether we're in a link, or selected text matches valid URL pattern.
this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink);
// Save image depends on having loaded its content, video and audio don't.
this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
this.showItem("context-savevideo", this.onVideo);
this.showItem("context-saveaudio", this.onAudio);
this.showItem("context-video-saveimage", this.onVideo);
this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
// Send media URL (but not for canvas, since it's a big data: URL)
this.showItem("context-sendimage", this.onImage);
this.showItem("context-sendvideo", this.onVideo);
this.showItem("context-sendaudio", this.onAudio);
let mediaIsBlob = this.mediaURL.startsWith("blob:");
this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL || mediaIsBlob);
this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL || mediaIsBlob);
},
initViewItems: function CM_initViewItems() {
// View source is always OK, unless in directory listing.
this.showItem("context-viewpartialsource-selection",
this.isContentSelected);
this.showItem("context-viewpartialsource-mathml",
this.onMathML && !this.isContentSelected);
var shouldShow = !(this.isContentSelected ||
this.onImage || this.onCanvas ||
this.onVideo || this.onAudio ||
this.onLink || this.onTextInput);
var showInspect = this.inTabBrowser &&
gPrefService.getBoolPref("devtools.inspector.enabled", true);
this.showItem("context-viewsource", shouldShow);
this.showItem("context-viewinfo", shouldShow);
// The page info is broken for WebExtension popups, as the browser is
// destroyed when the popup is closed.
this.setItemAttr("context-viewinfo", "disabled", this.webExtBrowserType === "popup");
this.showItem("inspect-separator", showInspect);
this.showItem("context-inspect", showInspect);
this.showItem("context-sep-viewsource", shouldShow);
// Set as Desktop background depends on whether an image was clicked on,
// and only works if we have a shell service.
var haveSetDesktopBackground = false;
if (AppConstants.HAVE_SHELL_SERVICE) {
// 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 = gContextMenuContentData.disableSetDesktopBackground;
}
// Reload image depends on an image that's not fully loaded
this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage));
// View image depends on having an image that's not standalone
// (or is in a frame), or a canvas.
this.showItem("context-viewimage", (this.onImage &&
(!this.inSyntheticDoc || this.inFrame)) || this.onCanvas);
// View video depends on not having a standalone video.
this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame));
this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
// View background image depends on whether there is one, but don't make
// background images of a stand-alone media document available.
this.showItem("context-viewbgimage", shouldShow &&
!this.hasMultipleBGImages &&
!this.inSyntheticDoc);
this.showItem("context-sep-viewbgimage", shouldShow &&
!this.hasMultipleBGImages &&
!this.inSyntheticDoc);
document.getElementById("context-viewbgimage")
.disabled = !this.hasBGImage;
this.showItem("context-viewimageinfo", this.onImage);
// 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");
this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== "");
},
initMiscItems: function CM_initMiscItems() {
// Use "Bookmark This Link" if on a link.
let bookmarkPage = document.getElementById("context-bookmarkpage");
this.showItem(bookmarkPage,
!(this.isContentSelected || this.onTextInput || this.onLink ||
this.onImage || this.onVideo || this.onAudio ||
this.onCanvas || this.inWebExtBrowser));
bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext"));
this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
!this.onMozExtLink) || this.onPlainTextLink);
this.showItem("context-keywordfield",
this.onTextInput && this.onKeywordField);
this.showItem("frame", this.inFrame);
let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage;
this.showItem("context-searchselect", showSearchSelect);
if (showSearchSelect) {
this.formatSearchContextItem();
}
// srcdoc cannot be opened separately due to concerns about web
// content with about:srcdoc in location bar masquerading as trusted
// chrome/addon content.
// No need to also test for this.inFrame as this is checked in the parent
// submenu.
this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
this.showItem("context-openframeintab", !this.inSrcdocFrame);
this.showItem("context-openframe", !this.inSrcdocFrame);
this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
this.showItem("open-frame-sep", !this.inSrcdocFrame);
this.showItem("frame-sep", this.inFrame && this.isTextSelected);
// Hide menu entries for images, show otherwise
if (this.inFrame) {
if (BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType))
this.isFrameImage.removeAttribute("hidden");
else
this.isFrameImage.setAttribute("hidden", "true");
}
// BiDi UI
this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI);
this.showItem("context-bidi-text-direction-toggle",
this.onTextInput && !this.onNumeric && top.gBidiUI);
this.showItem("context-bidi-page-direction-toggle",
!this.onTextInput && top.gBidiUI);
},
initSpellingItems() {
var canSpell = InlineSpellCheckerUI.canSpellCheck &&
!InlineSpellCheckerUI.initialSpellCheckPending &&
this.canSpellCheck;
let showDictionaries = canSpell && InlineSpellCheckerUI.enabled;
var onMisspelling = InlineSpellCheckerUI.overMisspelling;
var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
this.showItem("spell-check-enabled", canSpell);
this.showItem("spell-separator", canSpell);
document.getElementById("spell-check-enabled")
.setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
this.showItem("spell-add-to-dictionary", onMisspelling);
this.showItem("spell-undo-add-to-dictionary", showUndo);
// suggestion list
this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
if (onMisspelling) {
var suggestionsSeparator =
document.getElementById("spell-add-to-dictionary");
var numsug =
InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode,
suggestionsSeparator, 5);
this.showItem("spell-no-suggestions", numsug == 0);
} else {
this.showItem("spell-no-suggestions", false);
}
// dictionary list
this.showItem("spell-dictionaries", showDictionaries);
if (canSpell) {
var dictMenu = document.getElementById("spell-dictionaries-menu");
var dictSep = document.getElementById("spell-language-separator");
let count = InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
this.showItem(dictSep, count > 0);
this.showItem("spell-add-dictionaries-main", false);
} else if (this.onEditableArea) {
// when there is no spellchecker but we might be able to spellcheck
// add the add to dictionaries item. This will ensure that people
// with no dictionaries will be able to download them
this.showItem("spell-language-separator", showDictionaries);
this.showItem("spell-add-dictionaries-main", showDictionaries);
} else {
this.showItem("spell-add-dictionaries-main", false);
}
},
initClipboardItems() {
// Copy depends on whether there is selected text.
// Enabling this context menu item is now done through the global
// command updating system
// this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
goUpdateGlobalEditMenuItems();
this.showItem("context-undo", this.onTextInput);
this.showItem("context-sep-undo", this.onTextInput);
this.showItem("context-cut", this.onTextInput);
this.showItem("context-copy",
this.isContentSelected || this.onTextInput);
this.showItem("context-paste", this.onTextInput);
this.showItem("context-delete", this.onTextInput);
this.showItem("context-sep-paste", this.onTextInput);
this.showItem("context-selectall", !(this.onLink || this.onImage ||
this.onVideo || this.onAudio ||
this.inSyntheticDoc) ||
this.isDesignMode);
this.showItem("context-sep-selectall", this.isContentSelected );
// XXX dr
// ------
// nsDocumentViewer.cpp has code to determine whether we're
// on a link or an image. we really ought to be using that...
// Copy email link depends on whether we're on an email link.
this.showItem("context-copyemail", this.onMailtoLink);
// Copy link location depends on whether we're on a non-mailto link.
this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
this.showItem("context-sep-copylink", this.onLink &&
(this.onImage || this.onVideo || this.onAudio));
// 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);
this.showItem("context-copyvideourl", this.onVideo);
this.showItem("context-copyaudiourl", this.onAudio);
this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
this.showItem("context-sep-copyimage", this.onImage ||
this.onVideo || this.onAudio);
},
initMediaPlayerItems() {
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);
this.showItem("context-media-eme-learnmore", this.onDRMMedia);
this.showItem("context-media-eme-separator", this.onDRMMedia);
// Disable them when there isn't a valid media source loaded.
if (onMedia) {
this.setItemAttr("context-media-playbackrate-050x", "checked", this.target.playbackRate == 0.5);
this.setItemAttr("context-media-playbackrate-100x", "checked", this.target.playbackRate == 1.0);
this.setItemAttr("context-media-playbackrate-125x", "checked", this.target.playbackRate == 1.25);
this.setItemAttr("context-media-playbackrate-150x", "checked", this.target.playbackRate == 1.5);
this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0);
this.setItemAttr("context-media-loop", "checked", this.target.loop);
var hasError = this.target.error != null ||
this.target.networkState == this.target.NETWORK_NO_SOURCE;
this.setItemAttr("context-media-play", "disabled", hasError);
this.setItemAttr("context-media-pause", "disabled", hasError);
this.setItemAttr("context-media-mute", "disabled", hasError);
this.setItemAttr("context-media-unmute", "disabled", hasError);
this.setItemAttr("context-media-playbackrate", "disabled", hasError);
this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError);
this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
this.setItemAttr("context-media-showcontrols", "disabled", hasError);
this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
if (this.onVideo) {
let canSaveSnapshot = !this.onDRMMedia && this.target.readyState >= this.target.HAVE_CURRENT_DATA;
this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot);
this.setItemAttr("context-video-fullscreen", "disabled", hasError);
}
}
this.showItem("context-media-sep-commands", onMedia);
},
initClickToPlayItems() {
this.showItem("context-ctp-play", this.onCTPPlugin);
this.showItem("context-ctp-hide", this.onCTPPlugin);
this.showItem("context-sep-ctp", this.onCTPPlugin);
},
initPasswordManagerItems() {
let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo;
// If we could not find a password field we
// don't want to show the form fill option.
let showFill = loginFillInfo && loginFillInfo.passwordField.found;
// Disable the fill option if the user has set a master password
// or if the password field or target field are disabled.
let disableFill = !loginFillInfo ||
!Services.logins ||
!Services.logins.isLoggedIn ||
loginFillInfo.passwordField.disabled ||
(!this.onPassword && loginFillInfo.usernameField.disabled);
this.showItem("fill-login-separator", showFill);
this.showItem("fill-login", showFill);
this.setItemAttr("fill-login", "disabled", disableFill);
// Set the correct label for the fill menu
let fillMenu = document.getElementById("fill-login");
if (this.onPassword) {
fillMenu.setAttribute("label", fillMenu.getAttribute("label-password"));
fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-password"));
} else {
fillMenu.setAttribute("label", fillMenu.getAttribute("label-login"));
fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-login"));
}
if (!showFill || disableFill) {
return;
}
let documentURI = gContextMenuContentData.documentURIObject;
let fragment = LoginManagerContextMenu.addLoginsToMenu(this.targetAsCPOW, this.browser, documentURI);
this.showItem("fill-login-no-logins", !fragment);
if (!fragment) {
return;
}
let popup = document.getElementById("fill-login-popup");
let insertBeforeElement = document.getElementById("fill-login-no-logins");
popup.insertBefore(fragment, insertBeforeElement);
},
initSyncItems() {
gSync.updateContentContextMenu(this);
},
openPasswordManager() {
LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
},
inspectNode() {
return DevToolsShim.inspectNode(gBrowser.selectedTab, this.targetSelectors);
},
_openLinkInParameters(extra) {
let params = { charset: gContextMenuContentData.charSet,
originPrincipal: this.principal,
triggeringPrincipal: this.principal,
referrerURI: gContextMenuContentData.documentURIObject,
referrerPolicy: gContextMenuContentData.referrerPolicy,
frameOuterWindowID: gContextMenuContentData.frameOuterWindowID,
noReferrer: this.linkHasNoReferrer };
for (let p in extra) {
params[p] = extra[p];
}
if (!this.isRemote) {
// Propagate the frameOuterWindowID value saved when
// the context menu has been opened.
params.frameOuterWindowID = this.frameOuterWindowID;
}
// If we want to change userContextId, we must be sure that we don't
// propagate the referrer.
if ("userContextId" in params &&
params.userContextId != gContextMenuContentData.userContextId) {
params.noReferrer = true;
}
return params;
},
// Open linked-to URL in a new window.
openLink() {
urlSecurityCheck(this.linkURL, this.principal);
openLinkIn(this.linkURL, "window", this._openLinkInParameters());
},
// Open linked-to URL in a new private window.
openLinkInPrivateWindow() {
urlSecurityCheck(this.linkURL, this.principal);
openLinkIn(this.linkURL, "window",
this._openLinkInParameters({ private: true }));
},
// Open linked-to URL in a new tab.
openLinkInTab(event) {
urlSecurityCheck(this.linkURL, this.principal);
let referrerURI = gContextMenuContentData.documentURIObject;
// if its parent allows mixed content and the referring URI passes
// a same origin check with the target URI, we can preserve the users
// decision of disabling MCB on a page for it's child tabs.
let persistAllowMixedContentInChildTab = false;
if (gContextMenuContentData.parentAllowsMixedContent) {
const sm = Services.scriptSecurityManager;
try {
let targetURI = this.linkURI;
sm.checkSameOriginURI(referrerURI, targetURI, false);
persistAllowMixedContentInChildTab = true;
} catch (e) { }
}
let params = {
allowMixedContent: persistAllowMixedContentInChildTab,
userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
};
openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
},
// open URL in current tab
openLinkInCurrent() {
urlSecurityCheck(this.linkURL, this.principal);
openLinkIn(this.linkURL, "current", this._openLinkInParameters());
},
// Open frame in a new tab.
openFrameInTab() {
let referrer = gContextMenuContentData.referrer;
openLinkIn(gContextMenuContentData.docLocation, "tab",
{ charset: gContextMenuContentData.charSet,
referrerURI: referrer ? makeURI(referrer) : null });
},
// Reload clicked-in frame.
reloadFrame(aEvent) {
let forceReload = aEvent.shiftKey;
this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadFrame",
null, { target: this.target, forceReload });
},
// Open clicked-in frame in its own window.
openFrame() {
let referrer = gContextMenuContentData.referrer;
openLinkIn(gContextMenuContentData.docLocation, "window",
{ charset: gContextMenuContentData.charSet,
referrerURI: referrer ? makeURI(referrer) : null });
},
// Open clicked-in frame in the same window.
showOnlyThisFrame() {
urlSecurityCheck(gContextMenuContentData.docLocation,
this.browser.contentPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
let referrer = gContextMenuContentData.referrer;
openUILinkIn(gContextMenuContentData.docLocation, "current",
{ disallowInheritPrincipal: true,
referrerURI: referrer ? makeURI(referrer) : null });
},
reload(event) {
BrowserReloadOrDuplicate(event);
},
// View Partial Source
viewPartialSource(aContext) {
let inWindow = !Services.prefs.getBoolPref("view_source.tab");
let openSelectionFn = inWindow ? null : function() {
let tabBrowser = gBrowser;
// In the case of popups, we need to find a non-popup browser window.
if (!tabBrowser || !window.toolbar.visible) {
// This returns only non-popup browser windows by default.
let browserWindow = RecentWindow.getMostRecentBrowserWindow();
tabBrowser = browserWindow.gBrowser;
}
let tab = tabBrowser.loadOneTab("about:blank", {
relatedToCurrent: true,
inBackground: false,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
return tabBrowser.getBrowserForTab(tab);
};
let target = aContext == "mathml" ? this.target : null;
top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target, openSelectionFn);
},
// Open new "view source" window with the frame's URL.
viewFrameSource() {
BrowserViewSourceOfDocument({
browser: this.browser,
URL: gContextMenuContentData.docLocation,
outerWindowID: this.frameOuterWindowID,
});
},
viewInfo() {
BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser);
},
viewImageInfo() {
BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
this.imageInfo, null, this.browser);
},
viewImageDesc(e) {
urlSecurityCheck(this.imageDescURL,
this.browser.contentPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
referrerURI: gContextMenuContentData.documentURIObject });
},
viewFrameInfo() {
BrowserPageInfo(gContextMenuContentData.docLocation, null, null,
this.frameOuterWindowID, this.browser);
},
reloadImage() {
urlSecurityCheck(this.mediaURL,
this.browser.contentPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
null, { target: this.target });
},
_canvasToBlobURL(target) {
let mm = this.browser.messageManager;
return new Promise(function(resolve) {
mm.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL", {}, { target });
let onMessage = (message) => {
mm.removeMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
resolve(message.data.blobURL);
};
mm.addMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage);
});
},
// Change current window to the URL of the image, video, or audio.
viewMedia(e) {
let referrerURI = gContextMenuContentData.documentURIObject;
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
if (this.onCanvas) {
this._canvasToBlobURL(this.target).then(function(blobURL) {
openUILink(blobURL, e, { disallowInheritPrincipal: true,
referrerURI,
triggeringPrincipal: systemPrincipal});
}, Cu.reportError);
} else {
urlSecurityCheck(this.mediaURL,
this.browser.contentPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
openUILink(this.mediaURL, e, { disallowInheritPrincipal: true,
referrerURI,
forceAllowDataURI: true });
}
},
saveVideoFrameAsImage() {
let mm = this.browser.messageManager;
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";
mm.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage", {}, {
target: this.target,
});
let onMessage = (message) => {
mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
let dataURL = message.data.dataURL;
saveImageURL(dataURL, name, "SaveImageTitle", true, false,
document.documentURIObject, null, null, null,
isPrivate);
};
mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
},
leaveDOMFullScreen() {
document.exitFullscreen();
},
// Change current window to the URL of the background image.
viewBGImage(e) {
urlSecurityCheck(this.bgImageURL,
this.browser.contentPrincipal,
Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
referrerURI: gContextMenuContentData.documentURIObject });
},
setDesktopBackground() {
let mm = this.browser.messageManager;
mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null,
{ target: this.target });
let onMessage = (message) => {
mm.removeMessageListener("ContextMenu:SetAsDesktopBackground:Result",
onMessage);
if (message.data.disable)
return;
let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
image.src = message.data.dataUrl;
let imageName = message.data.imageName;
// Confirm since it's annoying if you hit this accidentally.
const kDesktopBackgroundURL =
"chrome://browser/content/setDesktopBackground.xul";
if (AppConstants.platform == "macosx") {
// On Mac, the Set Desktop Background window is not modal.
// Don't open more than one Set Desktop Background window.
const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
let dbWin = 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);
}
};
mm.addMessageListener("ContextMenu:SetAsDesktopBackground:Result", onMessage);
},
// Save URL of clicked-on frame.
saveFrame() {
saveBrowser(this.browser, false, this.frameOuterWindowID);
},
// Helper function to wait for appropriate MIME-type headers and
// then prompt the user with a file picker
saveHelper(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
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() {}
saveAsListener.prototype = {
extListener: null,
onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {
// if the timer fired, the error status will have been caused by that,
// and we'll be restarting in onStopRequest, so no reason to notify
// the user
if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
return;
timer.cancel();
// some other error occured; notify the user...
if (!Components.isSuccessCode(aRequest.status)) {
try {
const sbs = Cc["@mozilla.org/intl/stringbundle;1"].
getService(Ci.nsIStringBundleService);
const bundle = sbs.createBundle(
"chrome://mozapps/locale/downloads/downloads.properties");
const title = bundle.GetStringFromName("downloadErrorAlertTitle");
const msg = bundle.GetStringFromName("downloadErrorGeneric");
const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
getService(Ci.nsIPromptService);
const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
let window = wm.getOuterWindowWithId(windowID);
promptSvc.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, aContext);
},
onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
aStatusCode) {
if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
// do it the old fashioned way, which will pick the best filename
// it can without waiting.
saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI,
doc, isContentWindowPrivate);
}
if (this.extListener)
this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
},
onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
aInputStream,
aOffset, aCount) {
this.extListener.onDataAvailable(aRequest, aContext, 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 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_DATA_INHERITS,
});
if (linkDownload)
channel.contentDispositionFilename = linkDownload;
if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser);
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.referrer = docURI;
if (channel instanceof Ci.nsIHttpChannelInternal)
channel.forceAllowThirdPartyCookie = true;
}
// fallback to the old way if we don't see the headers quickly
var timeToWait =
gPrefService.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.asyncOpen2(new saveAsListener());
},
// Save URL of clicked-on link.
saveLink() {
urlSecurityCheck(this.linkURL, this.principal);
let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
gContextMenuContentData.documentURIObject,
this.frameOuterWindowID,
this.linkDownload,
isContentWindowPrivate);
},
// 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 isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
let referrerURI = gContextMenuContentData.documentURIObject;
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
if (this.onCanvas) {
// Bypass cache, since it's a data: URL.
this._canvasToBlobURL(this.target).then(function(blobURL) {
saveImageURL(blobURL, "canvas.png", "SaveImageTitle",
true, false, referrerURI, null, null, null,
isPrivate);
}, Cu.reportError);
} else if (this.onImage) {
urlSecurityCheck(this.mediaURL, this.principal);
saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
false, referrerURI, null, gContextMenuContentData.contentType,
gContextMenuContentData.contentDisposition, isPrivate);
} else if (this.onVideo || this.onAudio) {
urlSecurityCheck(this.mediaURL, this.principal);
var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
this.frameOuterWindowID, "", isContentWindowPrivate);
}
},
// Backwards-compatibility wrapper
sendImage() {
if (this.onCanvas || this.onImage)
this.sendMedia();
},
sendMedia() {
MailIntegration.sendMessage(this.mediaURL, "");
},
playPlugin() {
gPluginHandler.contextMenuCommand(this.browser, this.target, "play");
},
hidePlugin() {
gPluginHandler.contextMenuCommand(this.browser, this.target, "hide");
},
// 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 {
const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
getService(Ci.nsITextToSubURI);
addresses = textToSubURI.unEscapeURIForUI(gContextMenuContentData.charSet,
addresses);
} catch (ex) {
// Do nothing.
}
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
clipboard.copyString(addresses);
},
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);
},
/**
* 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);
}
}
},
// Set context menu attribute according to like attribute of another node
// (such as a broadcaster).
setItemAttrFromNode(aItem_id, aAttr, aOther_id) {
var elem = document.getElementById(aOther_id);
if (elem && elem.getAttribute(aAttr) == "true")
this.setItemAttr(aItem_id, aAttr, "true");
else
this.setItemAttr(aItem_id, aAttr, null);
},
// Temporary workaround for DOM api not yet implemented by XUL nodes.
cloneNode(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;
},
// 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;
},
addDictionaries() {
var uri = formatURL("browser.dictionaries.download.url", true);
var locale = "-";
try {
locale = gPrefService.getComplexValue("intl.accept_languages",
Ci.nsIPrefLocalizedString).data;
} catch (e) { }
var version = "-";
try {
version = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULAppInfo).version;
} catch (e) { }
uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow");
var where = newWindowPref == 3 ? "tab" : "window";
openUILinkIn(uri, where);
},
bookmarkThisPage: function CM_bookmarkThisPage() {
window.top.PlacesCommandHook
.bookmarkPage(this.browser, true)
.catch(Components.utils.reportError);
},
bookmarkLink: function CM_bookmarkLink() {
window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
this.linkURL, this.linkTextStr);
},
addBookmarkForFrame: function CM_addBookmarkForFrame() {
let uri = gContextMenuContentData.documentURIObject;
let mm = this.browser.messageManager;
let onMessage = (message) => {
mm.removeMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId,
uri.spec,
message.data.title,
message.data.description)
.catch(Components.utils.reportError);
};
mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage);
mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target });
},
savePageAs: function CM_savePageAs() {
saveBrowser(this.browser);
},
printFrame: function CM_printFrame() {
PrintUtils.printWindow(this.frameOuterWindowID, this.browser);
},
switchPageDirection: function CM_switchPageDirection() {
this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
},
mediaCommand: function CM_mediaCommand(command, data) {
let mm = this.browser.messageManager;
let win = this.browser.ownerGlobal;
let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
mm.sendAsyncMessage("ContextMenu:MediaCommand",
{command,
data,
handlingUserInput: windowUtils.isHandlingUserInput},
{element: this.target});
},
copyMediaLocation() {
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
clipboard.copyString(this.mediaURL);
},
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";
}
openUILinkIn(drmInfoURL, dest);
},
get imageURL() {
if (this.onImage)
return this.mediaURL;
return "";
},
// Formats the 'Search <engine> for "<selection or link text>"' context menu.
formatSearchContextItem() {
var menuItem = document.getElementById("context-searchselect");
let selectedText = this.isTextSelected ? this.textSelected : this.linkTextStr;
// Store searchTerms in context menu item so we know what to search onclick
menuItem.searchTerms = selectedText;
// 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.currentEngine.name;
var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
[engineName,
selectedText]);
menuItem.label = menuLabel;
menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
},
_getTelemetryClickInfo(aXulMenu) {
this._onPopupHiding = () => {
aXulMenu.ownerDocument.removeEventListener("command", activationHandler, true);
aXulMenu.removeEventListener("popuphiding", this._onPopupHiding, true);
delete this._onPopupHiding;
let eventKey = [
this._telemetryPageContext,
this._telemetryHadCustomItems ? "withcustom" : "withoutcustom"
];
let target = this._telemetryClickID || "close-without-interaction";
BrowserUITelemetry.registerContextMenuInteraction(eventKey, target);
};
let activationHandler = (e) => {
// Deal with command events being routed to command elements; figure out
// what triggered the event (which will have the right e.target)
if (e.sourceEvent) {
e = e.sourceEvent;
}
// Target should be in the menu (this catches using shortcuts for items
// not in the menu while the menu is up)
if (!aXulMenu.contains(e.target)) {
return;
}
// Check if this is a page menu item:
if (e.target.hasAttribute(PageMenuParent.GENERATEDITEMID_ATTR)) {
this._telemetryClickID = "custom-page-item";
} else {
this._telemetryClickID = (e.target.id || "unknown").replace(/^context-/i, "");
}
};
aXulMenu.ownerDocument.addEventListener("command", activationHandler, true);
aXulMenu.addEventListener("popuphiding", this._onPopupHiding, true);
},
_getTelemetryPageContextInfo() {
let rv = [];
for (let k of ["isContentSelected", "onLink", "onImage", "onCanvas", "onVideo", "onAudio",
"onTextInput", "inWebExtBrowser", "inTabBrowser"]) {
if (this[k]) {
rv.push(k.replace(/^(?:is|on)(.)/, (match, firstLetter) => firstLetter.toLowerCase()));
}
}
if (!rv.length) {
rv.push("other");
}
return JSON.stringify(rv);
},
_checkTelemetryForMenu(aXulMenu) {
this._telemetryClickID = null;
this._telemetryPageContext = this._getTelemetryPageContextInfo();
this._telemetryHadCustomItems = this.hasPageMenu;
this._getTelemetryClickInfo(aXulMenu);
},
createContainerMenu(aEvent) {
let createMenuOptions = {
isContextMenu: true,
excludeUserContextId: gContextMenuContentData.userContextId,
};
return createUserContextMenu(aEvent, createMenuOptions);
},
};