mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-13 11:38:16 +00:00
5dec0e0beb
This patch was autogenerated by my decomponents.py It covers almost every file with the extension js, jsm, html, py, xhtml, or xul. It removes blank lines after removed lines, when the removed lines are preceded by either blank lines or the start of a new block. The "start of a new block" is defined fairly hackily: either the line starts with //, ends with */, ends with {, <![CDATA[, """ or '''. The first two cover comments, the third one covers JS, the fourth covers JS embedded in XUL, and the final two cover JS embedded in Python. This also applies if the removed line was the first line of the file. It covers the pattern matching cases like "var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;". It'll remove the entire thing if they are all either Ci, Cr, Cc or Cu, or it will remove the appropriate ones and leave the residue behind. If there's only one behind, then it will turn it into a normal, non-pattern matching variable definition. (For instance, "const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components" becomes "const CC = Components.Constructor".) MozReview-Commit-ID: DeSHcClQ7cG --HG-- extra : rebase_source : d9c41878036c1ef7766ef5e91a7005025bc1d72b
1033 lines
34 KiB
JavaScript
1033 lines
34 KiB
JavaScript
/* -*- mode: js; 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/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["ContextMenu"];
|
|
|
|
Cu.importGlobalProperties(["URL"]);
|
|
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
|
|
CastingApps: "resource:///modules/CastingApps.jsm",
|
|
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
|
|
PlacesUIUtils: "resource:///modules/PlacesUIUtils.jsm",
|
|
findCssSelector: "resource://gre/modules/css-selector.js",
|
|
SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.jsm",
|
|
LoginManagerContent: "resource://gre/modules/LoginManagerContent.jsm",
|
|
WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
|
|
InlineSpellCheckerContent: "resource://gre/modules/InlineSpellCheckerContent.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "PageMenuChild", () => {
|
|
let tmp = {};
|
|
ChromeUtils.import("resource://gre/modules/PageMenu.jsm", tmp);
|
|
return new tmp.PageMenuChild();
|
|
});
|
|
|
|
const messageListeners = {
|
|
"ContextMenu:BookmarkFrame": function(aMessage) {
|
|
let frame = this.getTarget(aMessage).ownerDocument;
|
|
|
|
this.global.sendAsyncMessage("ContextMenu:BookmarkFrame:Result",
|
|
{ title: frame.title,
|
|
description: PlacesUIUtils.getDescriptionFromDocument(frame) });
|
|
},
|
|
|
|
"ContextMenu:Canvas:ToBlobURL": function(aMessage) {
|
|
this.getTarget(aMessage).toBlob((blob) => {
|
|
let blobURL = URL.createObjectURL(blob);
|
|
this.global.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL });
|
|
});
|
|
},
|
|
|
|
"ContextMenu:DoCustomCommand": function(aMessage) {
|
|
E10SUtils.wrapHandlingUserInput(
|
|
this.content,
|
|
aMessage.data.handlingUserInput,
|
|
() => PageMenuChild.executeMenu(aMessage.data.generatedItemId)
|
|
);
|
|
},
|
|
|
|
"ContextMenu:Hiding": function() {
|
|
this.context = null;
|
|
this.target = null;
|
|
},
|
|
|
|
"ContextMenu:MediaCommand": function(aMessage) {
|
|
E10SUtils.wrapHandlingUserInput(
|
|
this.content, aMessage.data.handlingUserInput, () => {
|
|
let media = this.getTarget(aMessage, "element");
|
|
|
|
switch (aMessage.data.command) {
|
|
case "play":
|
|
media.play();
|
|
break;
|
|
case "pause":
|
|
media.pause();
|
|
break;
|
|
case "loop":
|
|
media.loop = !media.loop;
|
|
break;
|
|
case "mute":
|
|
media.muted = true;
|
|
break;
|
|
case "unmute":
|
|
media.muted = false;
|
|
break;
|
|
case "playbackRate":
|
|
media.playbackRate = aMessage.data.data;
|
|
break;
|
|
case "hidecontrols":
|
|
media.removeAttribute("controls");
|
|
break;
|
|
case "showcontrols":
|
|
media.setAttribute("controls", "true");
|
|
break;
|
|
case "fullscreen":
|
|
if (this.content.document.fullscreenEnabled) {
|
|
media.requestFullscreen();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
);
|
|
},
|
|
|
|
"ContextMenu:ReloadFrame": function(aMessage) {
|
|
let forceReload = aMessage.objects && aMessage.objects.forceReload;
|
|
this.getTarget(aMessage).ownerDocument.location.reload(forceReload);
|
|
},
|
|
|
|
"ContextMenu:ReloadImage": function(aMessage) {
|
|
let image = this.getTarget(aMessage);
|
|
|
|
if (image instanceof Ci.nsIImageLoadingContent) {
|
|
image.forceReload();
|
|
}
|
|
},
|
|
|
|
"ContextMenu:SearchFieldBookmarkData": function(aMessage) {
|
|
let node = this.getTarget(aMessage);
|
|
let charset = node.ownerDocument.characterSet;
|
|
let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
|
|
let formURI = Services.io.newURI(node.form.getAttribute("action"),
|
|
charset, formBaseURI);
|
|
let spec = formURI.spec;
|
|
let isURLEncoded = (node.form.method.toUpperCase() == "POST" &&
|
|
(node.form.enctype == "application/x-www-form-urlencoded" ||
|
|
node.form.enctype == ""));
|
|
let title = node.ownerDocument.title;
|
|
let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument);
|
|
let formData = [];
|
|
|
|
function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) {
|
|
if (aIsFormUrlEncoded) {
|
|
return escape(aName + "=" + aValue);
|
|
}
|
|
|
|
return escape(aName) + "=" + escape(aValue);
|
|
}
|
|
|
|
for (let el of node.form.elements) {
|
|
if (!el.type) // happens with fieldsets
|
|
continue;
|
|
|
|
if (el == node) {
|
|
formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) :
|
|
// Don't escape "%s", just append
|
|
escapeNameValuePair(el.name, "", false) + "%s");
|
|
continue;
|
|
}
|
|
|
|
let type = el.type.toLowerCase();
|
|
|
|
if (((el instanceof this.content.HTMLInputElement && el.mozIsTextField(true)) ||
|
|
type == "hidden" || type == "textarea") ||
|
|
((type == "checkbox" || type == "radio") && el.checked)) {
|
|
formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded));
|
|
} else if (el instanceof this.content.HTMLSelectElement && el.selectedIndex >= 0) {
|
|
for (let j = 0; j < el.options.length; j++) {
|
|
if (el.options[j].selected)
|
|
formData.push(escapeNameValuePair(el.name, el.options[j].value,
|
|
isURLEncoded));
|
|
}
|
|
}
|
|
}
|
|
|
|
let postData;
|
|
|
|
if (isURLEncoded) {
|
|
postData = formData.join("&");
|
|
} else {
|
|
let separator = spec.includes("?") ? "&" : "?";
|
|
spec += separator + formData.join("&");
|
|
}
|
|
|
|
this.global.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
|
|
{ spec, title, description, postData, charset });
|
|
},
|
|
|
|
"ContextMenu:SaveVideoFrameAsImage": function(aMessage) {
|
|
let video = this.getTarget(aMessage);
|
|
let canvas = this.content.document.createElementNS("http://www.w3.org/1999/xhtml",
|
|
"canvas");
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
|
|
let ctxDraw = canvas.getContext("2d");
|
|
ctxDraw.drawImage(video, 0, 0);
|
|
|
|
this.global.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", {
|
|
dataURL: canvas.toDataURL("image/jpeg", ""),
|
|
});
|
|
},
|
|
|
|
"ContextMenu:SetAsDesktopBackground": function(aMessage) {
|
|
let target = this.getTarget(aMessage);
|
|
|
|
// Paranoia: check disableSetDesktopBackground again, in case the
|
|
// image changed since the context menu was initiated.
|
|
let disable = this._disableSetDesktopBackground(target);
|
|
|
|
if (!disable) {
|
|
try {
|
|
BrowserUtils.urlSecurityCheck(target.currentURI.spec,
|
|
target.ownerDocument.nodePrincipal);
|
|
let canvas = this.content.document.createElement("canvas");
|
|
canvas.width = target.naturalWidth;
|
|
canvas.height = target.naturalHeight;
|
|
let ctx = canvas.getContext("2d");
|
|
ctx.drawImage(target, 0, 0);
|
|
let dataUrl = canvas.toDataURL();
|
|
let url = (new URL(target.ownerDocument.location.href)).pathname;
|
|
let imageName = url.substr(url.lastIndexOf("/") + 1);
|
|
this.global.sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
|
|
{ dataUrl, imageName });
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
disable = true;
|
|
}
|
|
}
|
|
|
|
if (disable) {
|
|
this.global.sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
|
|
{ disable });
|
|
}
|
|
},
|
|
};
|
|
|
|
class ContextMenu {
|
|
// PUBLIC
|
|
constructor(global) {
|
|
this.target = null;
|
|
this.context = null;
|
|
this.global = global;
|
|
this.content = global.content;
|
|
|
|
Services.els.addSystemEventListener(global, "contextmenu",
|
|
this._handleContentContextMenu.bind(this), false);
|
|
|
|
Object.keys(messageListeners).forEach(key =>
|
|
global.addMessageListener(key, messageListeners[key].bind(this))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the event target of the context menu, using a locally stored
|
|
* reference if possible. If not, and aMessage.objects is defined,
|
|
* aMessage.objects[aKey] is returned. Otherwise null.
|
|
* @param {Object} aMessage Message with a objects property
|
|
* @param {String} aKey Key for the target on aMessage.objects
|
|
* @return {Object} Context menu target
|
|
*/
|
|
getTarget(aMessage, aKey = "target") {
|
|
return this.target || (aMessage.objects && aMessage.objects[aKey]);
|
|
}
|
|
|
|
// PRIVATE
|
|
_isXULTextLinkLabel(aNode) {
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
return aNode.namespaceURI == XUL_NS &&
|
|
aNode.tagName == "label" &&
|
|
aNode.classList.contains("text-link") &&
|
|
aNode.href;
|
|
}
|
|
|
|
// Generate fully qualified URL for clicked-on link.
|
|
_getLinkURL() {
|
|
let href = this.context.link.href;
|
|
|
|
if (href) {
|
|
// Handle SVG links:
|
|
if (typeof href == "object" && href.animVal) {
|
|
return this._makeURLAbsolute(this.context.link.baseURI, href.animVal);
|
|
}
|
|
|
|
return href;
|
|
}
|
|
|
|
href = this.context.link.getAttribute("href") ||
|
|
this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
|
|
|
|
if (!href || !href.match(/\S/)) {
|
|
// Without this we try to save as the current doc,
|
|
// for example, HTML case also throws if empty
|
|
throw "Empty href";
|
|
}
|
|
|
|
return this._makeURLAbsolute(this.context.link.baseURI, href);
|
|
}
|
|
|
|
_getLinkURI() {
|
|
try {
|
|
return Services.io.newURI(this.context.linkURL);
|
|
} catch (ex) {
|
|
// e.g. empty URL string
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Get text of link.
|
|
_getLinkText() {
|
|
let text = this._gatherTextUnder(this.context.link);
|
|
|
|
if (!text || !text.match(/\S/)) {
|
|
text = this.context.link.getAttribute("title");
|
|
if (!text || !text.match(/\S/)) {
|
|
text = this.context.link.getAttribute("alt");
|
|
if (!text || !text.match(/\S/)) {
|
|
text = this.context.linkURL;
|
|
}
|
|
}
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
_getLinkProtocol() {
|
|
if (this.context.linkURI) {
|
|
return this.context.linkURI.scheme; // can be |undefined|
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Returns true if clicked-on link targets a resource that can be saved.
|
|
_isLinkSaveable(aLink) {
|
|
// We don't do the Right Thing for news/snews yet, so turn them off
|
|
// until we do.
|
|
return this.context.linkProtocol && !(
|
|
this.context.linkProtocol == "mailto" ||
|
|
this.context.linkProtocol == "javascript" ||
|
|
this.context.linkProtocol == "news" ||
|
|
this.context.linkProtocol == "snews");
|
|
}
|
|
|
|
// Gather all descendent text under given document node.
|
|
_gatherTextUnder(root) {
|
|
let text = "";
|
|
let node = root.firstChild;
|
|
let depth = 1;
|
|
while (node && depth > 0) {
|
|
// See if this node is text.
|
|
if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
|
|
// Add this text to our collection.
|
|
text += " " + node.data;
|
|
} else if (node instanceof this.content.HTMLImageElement) {
|
|
// If it has an "alt" attribute, add that.
|
|
let altText = node.getAttribute( "alt" );
|
|
if ( altText && altText != "" ) {
|
|
text += " " + altText;
|
|
}
|
|
}
|
|
// Find next node to test.
|
|
// First, see if this node has children.
|
|
if (node.hasChildNodes()) {
|
|
// Go to first child.
|
|
node = node.firstChild;
|
|
depth++;
|
|
} else {
|
|
// No children, try next sibling (or parent next sibling).
|
|
while (depth > 0 && !node.nextSibling) {
|
|
node = node.parentNode;
|
|
depth--;
|
|
}
|
|
if (node.nextSibling) {
|
|
node = node.nextSibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strip leading and tailing whitespace.
|
|
text = text.trim();
|
|
// Compress remaining whitespace.
|
|
text = text.replace(/\s+/g, " ");
|
|
return text;
|
|
}
|
|
|
|
// Returns a "url"-type computed style attribute value, with the url() stripped.
|
|
_getComputedURL(aElem, aProp) {
|
|
let url = aElem.ownerGlobal.getComputedStyle(aElem).getPropertyCSSValue(aProp);
|
|
|
|
if (url instanceof this.content.CSSValueList) {
|
|
if (url.length != 1) {
|
|
throw "found multiple URLs";
|
|
}
|
|
|
|
url = url[0];
|
|
}
|
|
|
|
return url.primitiveType == this.content.CSSPrimitiveValue.CSS_URI ?
|
|
url.getStringValue() : null;
|
|
}
|
|
|
|
_makeURLAbsolute(aBase, aUrl) {
|
|
return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
|
|
}
|
|
|
|
_isProprietaryDRM() {
|
|
return this.context.target.isEncrypted && this.context.target.mediaKeys &&
|
|
this.context.target.mediaKeys.keySystem != "org.w3.clearkey";
|
|
}
|
|
|
|
_isMediaURLReusable(aURL) {
|
|
if (aURL.startsWith("blob:")) {
|
|
return URL.isValidURL(aURL);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_isTargetATextBox(node) {
|
|
if (node instanceof this.content.HTMLInputElement) {
|
|
return node.mozIsTextField(false);
|
|
}
|
|
|
|
return (node instanceof this.content.HTMLTextAreaElement);
|
|
}
|
|
|
|
_isSpellCheckEnabled(aNode) {
|
|
// We can always force-enable spellchecking on textboxes
|
|
if (this._isTargetATextBox(aNode)) {
|
|
return true;
|
|
}
|
|
|
|
// We can never spell check something which is not content editable
|
|
let editable = aNode.isContentEditable;
|
|
|
|
if (!editable && aNode.ownerDocument) {
|
|
editable = aNode.ownerDocument.designMode == "on";
|
|
}
|
|
|
|
if (!editable) {
|
|
return false;
|
|
}
|
|
|
|
// Otherwise make sure that nothing in the parent chain disables spellchecking
|
|
return aNode.spellcheck;
|
|
}
|
|
|
|
_disableSetDesktopBackground(aTarget) {
|
|
// Disable the Set as Desktop Background menu item if we're still trying
|
|
// to load the image or the load failed.
|
|
if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
|
|
return true;
|
|
}
|
|
|
|
if (("complete" in aTarget) && !aTarget.complete) {
|
|
return true;
|
|
}
|
|
|
|
if (aTarget.currentURI.schemeIs("javascript")) {
|
|
return true;
|
|
}
|
|
|
|
let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
|
|
|
|
if (!request) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the array of CSS selectors corresponding to the provided node. The first item
|
|
* of the array is the selector of the node in its owner document. Additional items are
|
|
* used if the node is inside a frame, each representing the CSS selector for finding the
|
|
* frame element in its parent document.
|
|
*
|
|
* This format is expected by DevTools in order to handle the Inspect Node context menu
|
|
* item.
|
|
*
|
|
* @param {aNode}
|
|
* The node for which the CSS selectors should be computed
|
|
* @return {Array} array of css selectors (strings).
|
|
*/
|
|
_getNodeSelectors(aNode) {
|
|
let selectors = [];
|
|
while (aNode) {
|
|
selectors.push(findCssSelector(aNode));
|
|
aNode = aNode.ownerGlobal.frameElement;
|
|
}
|
|
|
|
return selectors;
|
|
}
|
|
|
|
_handleContentContextMenu(aEvent) {
|
|
let defaultPrevented = aEvent.defaultPrevented;
|
|
|
|
if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
|
|
let plugin = null;
|
|
|
|
try {
|
|
plugin = aEvent.target.QueryInterface(Ci.nsIObjectLoadingContent);
|
|
} catch (e) {}
|
|
|
|
if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
|
|
// Don't open a context menu for plugins.
|
|
return;
|
|
}
|
|
|
|
defaultPrevented = false;
|
|
}
|
|
|
|
if (defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
let doc = aEvent.target.ownerDocument;
|
|
let {
|
|
mozDocumentURIIfNotForErrorPages: docLocation,
|
|
characterSet: charSet,
|
|
baseURI,
|
|
referrer,
|
|
referrerPolicy
|
|
} = doc;
|
|
docLocation = docLocation && docLocation.spec;
|
|
let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
|
|
let loginFillInfo = LoginManagerContent.getFieldContext(aEvent.target);
|
|
|
|
// The same-origin check will be done in nsContextMenu.openLinkInTab.
|
|
let parentAllowsMixedContent = !!this.global.docShell.mixedContentChannel;
|
|
|
|
// Get referrer attribute from clicked link and parse it
|
|
let referrerAttrValue = Services.netUtils.parseAttributePolicyString(aEvent.target.
|
|
getAttribute("referrerpolicy"));
|
|
|
|
if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) {
|
|
referrerPolicy = referrerAttrValue;
|
|
}
|
|
|
|
let disableSetDesktopBg = null;
|
|
|
|
// Media related cache info parent needs for saving
|
|
let contentType = null;
|
|
let contentDisposition = null;
|
|
if (aEvent.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
|
|
aEvent.target instanceof Ci.nsIImageLoadingContent &&
|
|
aEvent.target.currentRequestFinalURI) {
|
|
disableSetDesktopBg = this._disableSetDesktopBackground(aEvent.target);
|
|
|
|
try {
|
|
let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
|
|
.getImgCacheForDocument(doc);
|
|
let props = imageCache.findEntryProperties(aEvent.target.currentRequestFinalURI, doc);
|
|
|
|
try {
|
|
contentType = props.get("type", Ci.nsISupportsCString).data;
|
|
} catch (e) {}
|
|
|
|
try {
|
|
contentDisposition = props.get("content-disposition", Ci.nsISupportsCString).data;
|
|
} catch (e) {}
|
|
} catch (e) {}
|
|
}
|
|
|
|
let selectionInfo = BrowserUtils.getSelectionDetails(this.content);
|
|
let loadContext = this.global.docShell.QueryInterface(Ci.nsILoadContext);
|
|
let userContextId = loadContext.originAttributes.userContextId;
|
|
let popupNodeSelectors = this._getNodeSelectors(aEvent.target);
|
|
|
|
this._setContext(aEvent);
|
|
let context = this.context;
|
|
this.target = context.target;
|
|
|
|
let spellInfo = null;
|
|
let editFlags = null;
|
|
let principal = null;
|
|
let customMenuItems = null;
|
|
|
|
let targetAsCPOW = context.target;
|
|
if (targetAsCPOW) {
|
|
this._cleanContext();
|
|
}
|
|
|
|
let isRemote = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
|
|
|
|
if (isRemote) {
|
|
editFlags = SpellCheckHelper.isEditable(aEvent.target, this.content);
|
|
|
|
if (editFlags & SpellCheckHelper.SPELLCHECKABLE) {
|
|
spellInfo = InlineSpellCheckerContent.initContextMenu(aEvent, editFlags, this.global);
|
|
}
|
|
|
|
// Set the event target first as the copy image command needs it to
|
|
// determine what was context-clicked on. Then, update the state of the
|
|
// commands on the context menu.
|
|
this.global.docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit)
|
|
.setCommandNode(aEvent.target);
|
|
aEvent.target.ownerGlobal.updateCommands("contentcontextmenu");
|
|
|
|
customMenuItems = PageMenuChild.build(aEvent.target);
|
|
principal = doc.nodePrincipal;
|
|
}
|
|
|
|
let data = {
|
|
context,
|
|
charSet,
|
|
baseURI,
|
|
isRemote,
|
|
referrer,
|
|
editFlags,
|
|
principal,
|
|
spellInfo,
|
|
contentType,
|
|
docLocation,
|
|
loginFillInfo,
|
|
selectionInfo,
|
|
userContextId,
|
|
referrerPolicy,
|
|
customMenuItems,
|
|
contentDisposition,
|
|
frameOuterWindowID,
|
|
popupNodeSelectors,
|
|
disableSetDesktopBg,
|
|
parentAllowsMixedContent,
|
|
};
|
|
|
|
if (isRemote) {
|
|
this.global.sendAsyncMessage("contextmenu", data, {
|
|
targetAsCPOW,
|
|
});
|
|
} else {
|
|
let browser = this.global.docShell.chromeEventHandler;
|
|
let mainWin = browser.ownerGlobal;
|
|
|
|
data.documentURIObject = doc.documentURIObject;
|
|
data.disableSetDesktopBackground = data.disableSetDesktopBg;
|
|
delete data.disableSetDesktopBg;
|
|
|
|
data.context.targetAsCPOW = targetAsCPOW;
|
|
|
|
mainWin.setContextMenuContentData(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Some things are not serializable, so we either have to only send
|
|
* their needed data or regenerate them in nsContextMenu.js
|
|
* - target and target.ownerDocument
|
|
* - link
|
|
* - linkURI
|
|
*/
|
|
_cleanContext(aEvent) {
|
|
const context = this.context;
|
|
const cleanTarget = Object.create(null);
|
|
|
|
cleanTarget.ownerDocument = {
|
|
// used for nsContextMenu.initLeaveDOMFullScreenItems and
|
|
// nsContextMenu.initMediaPlayerItems
|
|
fullscreen: context.target.ownerDocument.fullscreen,
|
|
|
|
// used for nsContextMenu.initMiscItems
|
|
contentType: context.target.ownerDocument.contentType,
|
|
|
|
// used for nsContextMenu.saveLink
|
|
isPrivate: PrivateBrowsingUtils.isContentWindowPrivate(context.target.ownerGlobal),
|
|
};
|
|
|
|
// used for nsContextMenu.initMediaPlayerItems
|
|
Object.assign(cleanTarget, {
|
|
ended: context.target.ended,
|
|
muted: context.target.muted,
|
|
paused: context.target.paused,
|
|
controls: context.target.controls,
|
|
duration: context.target.duration,
|
|
});
|
|
|
|
const onMedia = context.onVideo || context.onAudio;
|
|
|
|
if (onMedia) {
|
|
Object.assign(cleanTarget, {
|
|
loop: context.target.loop,
|
|
error: context.target.error,
|
|
networkState: context.target.networkState,
|
|
playbackRate: context.target.playbackRate,
|
|
NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
|
|
});
|
|
|
|
if (context.onVideo) {
|
|
Object.assign(cleanTarget, {
|
|
readyState: context.target.readyState,
|
|
HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
|
|
});
|
|
}
|
|
}
|
|
|
|
context.target = cleanTarget;
|
|
|
|
if (context.link) {
|
|
context.link = { href: context.linkURL };
|
|
}
|
|
|
|
delete context.linkURI;
|
|
}
|
|
|
|
_setContext(aEvent) {
|
|
this.context = Object.create(null);
|
|
const context = this.context;
|
|
|
|
context.screenX = aEvent.screenX;
|
|
context.screenY = aEvent.screenY;
|
|
context.mozInputSource = aEvent.mozInputSource;
|
|
|
|
const node = aEvent.target;
|
|
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
context.shouldDisplay = true;
|
|
|
|
if (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ||
|
|
// Don't display for XUL element unless <label class="text-link">
|
|
(node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))) {
|
|
context.shouldDisplay = false;
|
|
return;
|
|
}
|
|
|
|
// Initialize context to be sent to nsContextMenu
|
|
// Keep this consistent with the similar code in nsContextMenu's setContext
|
|
context.bgImageURL = "";
|
|
context.imageDescURL = "";
|
|
context.imageInfo = null;
|
|
context.mediaURL = "";
|
|
context.webExtBrowserType = "";
|
|
|
|
context.canSpellCheck = false;
|
|
context.hasBGImage = false;
|
|
context.hasMultipleBGImages = false;
|
|
context.isDesignMode = false;
|
|
context.inFrame = false;
|
|
context.inSrcdocFrame = false;
|
|
context.inSyntheticDoc = false;
|
|
context.inTabBrowser = true;
|
|
context.inWebExtBrowser = false;
|
|
|
|
context.link = null;
|
|
context.linkDownload = "";
|
|
context.linkHasNoReferrer = false;
|
|
context.linkProtocol = "";
|
|
context.linkTextStr = "";
|
|
context.linkURL = "";
|
|
context.linkURI = null;
|
|
|
|
context.onAudio = false;
|
|
context.onCanvas = false;
|
|
context.onCompletedImage = false;
|
|
context.onCTPPlugin = false;
|
|
context.onDRMMedia = false;
|
|
context.onEditable = false;
|
|
context.onImage = false;
|
|
context.onKeywordField = false;
|
|
context.onLink = false;
|
|
context.onLoadedImage = false;
|
|
context.onMailtoLink = false;
|
|
context.onMathML = false;
|
|
context.onMozExtLink = false;
|
|
context.onNumeric = false;
|
|
context.onPassword = false;
|
|
context.onSaveableLink = false;
|
|
context.onSpellcheckable = false;
|
|
context.onTextInput = false;
|
|
context.onVideo = false;
|
|
|
|
// Remember the node and its owner document that was clicked
|
|
// This may be modifed before sending to nsContextMenu
|
|
context.target = node;
|
|
|
|
context.principal = context.target.ownerDocument.nodePrincipal;
|
|
context.frameOuterWindowID = WebNavigationFrames.getFrameId(context.target.ownerGlobal);
|
|
|
|
// Check if we are in a synthetic document (stand alone image, video, etc.).
|
|
context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
|
|
|
|
context.shouldInitInlineSpellCheckerUINoChildren = false;
|
|
context.shouldInitInlineSpellCheckerUIWithChildren = false;
|
|
|
|
let editFlags = SpellCheckHelper.isEditable(context.target, this.content);
|
|
this._setContextForNodesNoChildren(editFlags);
|
|
this._setContextForNodesWithChildren(editFlags);
|
|
}
|
|
|
|
/**
|
|
* Sets up the parts of the context menu for when when nodes have no children.
|
|
*
|
|
* @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
|
|
* for the details.
|
|
*/
|
|
_setContextForNodesNoChildren(editFlags) {
|
|
const context = this.context;
|
|
|
|
if (context.target.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
|
|
// For text nodes, look at the parent node to determine the spellcheck attribute.
|
|
context.canSpellCheck = context.target.parentNode &&
|
|
this._isSpellCheckEnabled(context.target);
|
|
return;
|
|
}
|
|
|
|
// We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
|
|
// early if we don't have one.
|
|
if (context.target.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
|
|
return;
|
|
}
|
|
|
|
// See if the user clicked on an image. This check mirrors
|
|
// nsDocumentViewer::GetInImage. Make sure to update both if this is
|
|
// changed.
|
|
if (context.target instanceof Ci.nsIImageLoadingContent &&
|
|
context.target.currentRequestFinalURI) {
|
|
context.onImage = true;
|
|
|
|
context.imageInfo = {
|
|
currentSrc: context.target.currentSrc,
|
|
width: context.target.width,
|
|
height: context.target.height,
|
|
imageText: context.target.title || context.target.alt
|
|
};
|
|
|
|
const request = context.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
|
|
|
|
if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) {
|
|
context.onLoadedImage = true;
|
|
}
|
|
|
|
if (request &&
|
|
(request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
|
|
!(request.imageStatus & request.STATUS_ERROR)) {
|
|
context.onCompletedImage = true;
|
|
}
|
|
|
|
context.mediaURL = context.target.currentRequestFinalURI.spec;
|
|
|
|
const descURL = context.target.getAttribute("longdesc");
|
|
|
|
if (descURL) {
|
|
context.imageDescURL = this._makeURLAbsolute(context.target.ownerDocument.body.baseURI,
|
|
descURL);
|
|
}
|
|
} else if (context.target instanceof this.content.HTMLCanvasElement) {
|
|
context.onCanvas = true;
|
|
} else if (context.target instanceof this.content.HTMLVideoElement) {
|
|
const mediaURL = context.target.currentSrc || context.target.src;
|
|
|
|
if (this._isMediaURLReusable(mediaURL)) {
|
|
context.mediaURL = mediaURL;
|
|
}
|
|
|
|
if (this._isProprietaryDRM()) {
|
|
context.onDRMMedia = true;
|
|
}
|
|
|
|
// Firefox always creates a HTMLVideoElement when loading an ogg file
|
|
// directly. If the media is actually audio, be smarter and provide a
|
|
// context menu with audio operations.
|
|
if (context.target.readyState >= context.target.HAVE_METADATA &&
|
|
(context.target.videoWidth == 0 || context.target.videoHeight == 0)) {
|
|
context.onAudio = true;
|
|
} else {
|
|
context.onVideo = true;
|
|
}
|
|
} else if (context.target instanceof this.content.HTMLAudioElement) {
|
|
context.onAudio = true;
|
|
const mediaURL = context.target.currentSrc || context.target.src;
|
|
|
|
if (this._isMediaURLReusable(mediaURL)) {
|
|
context.mediaURL = mediaURL;
|
|
}
|
|
|
|
if (this._isProprietaryDRM()) {
|
|
context.onDRMMedia = true;
|
|
}
|
|
} else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
|
|
context.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
|
|
context.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
|
|
context.onEditable = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
|
|
context.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
|
|
context.onSpellcheckable = (editFlags & SpellCheckHelper.SPELLCHECKABLE) !== 0;
|
|
|
|
// This is guaranteed to be an input or textarea because of the condition above,
|
|
// so the no-children flag is always correct. We deal with contenteditable elsewhere.
|
|
if (context.onSpellcheckable) {
|
|
context.shouldInitInlineSpellCheckerUINoChildren = true;
|
|
}
|
|
|
|
context.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
|
|
} else if (context.target instanceof this.content.HTMLHtmlElement) {
|
|
const bodyElt = context.target.ownerDocument.body;
|
|
|
|
if (bodyElt) {
|
|
let computedURL;
|
|
|
|
try {
|
|
computedURL = this._getComputedURL(bodyElt, "background-image");
|
|
context.hasMultipleBGImages = false;
|
|
} catch (e) {
|
|
context.hasMultipleBGImages = true;
|
|
}
|
|
|
|
if (computedURL) {
|
|
context.hasBGImage = true;
|
|
context.bgImageURL = this._makeURLAbsolute(bodyElt.baseURI,
|
|
computedURL);
|
|
}
|
|
}
|
|
} else if ((context.target instanceof this.content.HTMLEmbedElement ||
|
|
context.target instanceof this.content.HTMLObjectElement) &&
|
|
context.target.displayedType == this.content.HTMLObjectElement.TYPE_NULL &&
|
|
context.target.pluginFallbackType == this.content.HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) {
|
|
context.onCTPPlugin = true;
|
|
}
|
|
|
|
context.canSpellCheck = this._isSpellCheckEnabled(context.target);
|
|
}
|
|
|
|
/**
|
|
* Sets up the parts of the context menu for when when nodes have children.
|
|
*
|
|
* @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
|
|
* for the details.
|
|
*/
|
|
_setContextForNodesWithChildren(editFlags) {
|
|
const context = this.context;
|
|
|
|
// Second, bubble out, looking for items of interest that can have childen.
|
|
// Always pick the innermost link, background image, etc.
|
|
let elem = context.target;
|
|
|
|
while (elem) {
|
|
if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
|
|
// Link?
|
|
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
|
|
if (!context.onLink &&
|
|
// Be consistent with what hrefAndLinkNodeForClickEvent
|
|
// does in browser.js
|
|
(this._isXULTextLinkLabel(elem) ||
|
|
(elem instanceof this.content.HTMLAnchorElement && elem.href) ||
|
|
(elem instanceof this.content.SVGAElement &&
|
|
(elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
|
|
(elem instanceof this.content.HTMLAreaElement && elem.href) ||
|
|
elem instanceof this.content.HTMLLinkElement ||
|
|
elem.getAttributeNS(XLINK_NS, "type") == "simple")) {
|
|
|
|
// Target is a link or a descendant of a link.
|
|
context.onLink = true;
|
|
|
|
// Remember corresponding element.
|
|
context.link = elem;
|
|
context.linkURL = this._getLinkURL();
|
|
context.linkURI = this._getLinkURI();
|
|
context.linkTextStr = this._getLinkText();
|
|
context.linkProtocol = this._getLinkProtocol();
|
|
context.onMailtoLink = (context.linkProtocol == "mailto");
|
|
context.onMozExtLink = (context.linkProtocol == "moz-extension");
|
|
context.onSaveableLink = this._isLinkSaveable(context.link);
|
|
context.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem);
|
|
|
|
try {
|
|
if (elem.download) {
|
|
// Ignore download attribute on cross-origin links
|
|
context.principal.checkMayLoad(context.linkURI, false, true);
|
|
context.linkDownload = elem.download;
|
|
}
|
|
} catch (ex) {}
|
|
}
|
|
|
|
// Background image? Don't bother if we've already found a
|
|
// background image further down the hierarchy. Otherwise,
|
|
// we look for the computed background-image style.
|
|
if (!context.hasBGImage &&
|
|
!context.hasMultipleBGImages) {
|
|
let bgImgUrl = null;
|
|
|
|
try {
|
|
bgImgUrl = this._getComputedURL(elem, "background-image");
|
|
context.hasMultipleBGImages = false;
|
|
} catch (e) {
|
|
context.hasMultipleBGImages = true;
|
|
}
|
|
|
|
if (bgImgUrl) {
|
|
context.hasBGImage = true;
|
|
context.bgImageURL = this._makeURLAbsolute(elem.baseURI,
|
|
bgImgUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
elem = elem.parentNode;
|
|
}
|
|
|
|
// See if the user clicked on MathML
|
|
const MathML_NS = "http://www.w3.org/1998/Math/MathML";
|
|
|
|
if ((context.target.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
|
|
context.target.parentNode.namespaceURI == MathML_NS) ||
|
|
(context.target.namespaceURI == MathML_NS)) {
|
|
context.onMathML = true;
|
|
}
|
|
|
|
// See if the user clicked in a frame.
|
|
const docDefaultView = context.target.ownerGlobal;
|
|
|
|
if (docDefaultView != docDefaultView.top) {
|
|
context.inFrame = true;
|
|
|
|
if (context.target.ownerDocument.isSrcdocDocument) {
|
|
context.inSrcdocFrame = true;
|
|
}
|
|
}
|
|
|
|
// if the document is editable, show context menu like in text inputs
|
|
if (!context.onEditable) {
|
|
if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
|
|
// If this.onEditable is false but editFlags is CONTENTEDITABLE, then
|
|
// the document itself must be editable.
|
|
context.onTextInput = true;
|
|
context.onKeywordField = false;
|
|
context.onImage = false;
|
|
context.onLoadedImage = false;
|
|
context.onCompletedImage = false;
|
|
context.onMathML = false;
|
|
context.inFrame = false;
|
|
context.inSrcdocFrame = false;
|
|
context.hasBGImage = false;
|
|
context.isDesignMode = true;
|
|
context.onEditable = true;
|
|
context.onSpellcheckable = true;
|
|
context.shouldInitInlineSpellCheckerUIWithChildren = true;
|
|
}
|
|
}
|
|
}
|
|
}
|