1 /* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 ChromeUtils.defineESModuleGetters(lazy, {
10 ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
11 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
12 InlineSpellCheckerContent:
13 "resource://gre/modules/InlineSpellCheckerContent.sys.mjs",
14 LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
15 LoginManagerChild: "resource://gre/modules/LoginManagerChild.sys.mjs",
16 SelectionUtils: "resource://gre/modules/SelectionUtils.sys.mjs",
17 SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.sys.mjs",
20 let contextMenus = new WeakMap();
22 export class ContextMenuChild extends JSWindowActorChild {
29 this.lastMenuTarget = null;
32 static getTarget(browsingContext, message, key) {
33 let actor = contextMenus.get(browsingContext);
36 "Can't find ContextMenu actor for browsing context with " +
41 return actor.getTarget(message, key);
44 static getLastTarget(browsingContext) {
45 let contextMenu = contextMenus.get(browsingContext);
46 return contextMenu && contextMenu.lastMenuTarget;
49 receiveMessage(message) {
50 switch (message.name) {
51 case "ContextMenu:GetFrameTitle": {
52 let target = lazy.ContentDOMReference.resolve(
53 message.data.targetIdentifier
55 return Promise.resolve(target.ownerDocument.title);
58 case "ContextMenu:Canvas:ToBlobURL": {
59 let target = lazy.ContentDOMReference.resolve(
60 message.data.targetIdentifier
62 return new Promise(resolve => {
63 target.toBlob(blob => {
64 let blobURL = URL.createObjectURL(blob);
70 case "ContextMenu:Hiding": {
76 case "ContextMenu:MediaCommand": {
77 lazy.E10SUtils.wrapHandlingUserInput(
79 message.data.handlingUserInput,
81 let media = lazy.ContentDOMReference.resolve(
82 message.data.targetIdentifier
85 switch (message.data.command) {
93 media.loop = !media.loop;
102 media.playbackRate = message.data.data;
105 media.removeAttribute("controls");
108 media.setAttribute("controls", "true");
111 if (this.document.fullscreenEnabled) {
112 media.requestFullscreen();
115 case "pictureinpicture":
116 if (!media.isCloningElementVisually) {
117 Services.telemetry.keyedScalarAdd(
118 "pictureinpicture.opened_method",
123 let event = new this.contentWindow.CustomEvent(
124 "MozTogglePictureInPicture",
127 detail: { reason: "contextMenu" },
131 media.dispatchEvent(event);
139 case "ContextMenu:ReloadFrame": {
140 let target = lazy.ContentDOMReference.resolve(
141 message.data.targetIdentifier
143 target.ownerDocument.location.reload(message.data.forceReload);
147 case "ContextMenu:GetImageText": {
148 let img = lazy.ContentDOMReference.resolve(
149 message.data.targetIdentifier
151 const { direction } = this.contentWindow.getComputedStyle(img);
153 return img.recognizeCurrentImageText().then(results => {
154 return { results, direction };
158 case "ContextMenu:ToggleRevealPassword": {
159 let target = lazy.ContentDOMReference.resolve(
160 message.data.targetIdentifier
162 target.revealPassword = !target.revealPassword;
166 case "ContextMenu:UseRelayMask": {
167 const input = lazy.ContentDOMReference.resolve(
168 message.data.targetIdentifier
170 input.setUserInput(message.data.emailMask);
174 case "ContextMenu:ReloadImage": {
175 let image = lazy.ContentDOMReference.resolve(
176 message.data.targetIdentifier
179 if (image instanceof Ci.nsIImageLoadingContent) {
185 case "ContextMenu:SearchFieldBookmarkData": {
186 let node = lazy.ContentDOMReference.resolve(
187 message.data.targetIdentifier
189 let charset = node.ownerDocument.characterSet;
190 let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
191 let formURI = Services.io.newURI(
192 node.form.getAttribute("action"),
196 let spec = formURI.spec;
198 node.form.method.toUpperCase() == "POST" &&
199 (node.form.enctype == "application/x-www-form-urlencoded" ||
200 node.form.enctype == "");
201 let title = node.ownerDocument.title;
203 function escapeNameValuePair([aName, aValue]) {
205 return escape(aName + "=" + aValue);
208 return encodeURIComponent(aName) + "=" + encodeURIComponent(aValue);
210 let formData = new this.contentWindow.FormData(node.form);
211 formData.delete(node.name);
212 formData = Array.from(formData).map(escapeNameValuePair);
214 escape(node.name) + (isURLEncoded ? escape("=%s") : "=%s")
220 postData = formData.join("&");
222 let separator = spec.includes("?") ? "&" : "?";
223 spec += separator + formData.join("&");
226 return Promise.resolve({ spec, title, postData, charset });
229 case "ContextMenu:SaveVideoFrameAsImage": {
230 let video = lazy.ContentDOMReference.resolve(
231 message.data.targetIdentifier
233 let canvas = this.document.createElementNS(
234 "http://www.w3.org/1999/xhtml",
237 canvas.width = video.videoWidth;
238 canvas.height = video.videoHeight;
240 let ctxDraw = canvas.getContext("2d");
241 ctxDraw.drawImage(video, 0, 0);
243 // Note: if changing the content type, don't forget to update
244 // consumers that also hardcode this content type.
245 return Promise.resolve(canvas.toDataURL("image/jpeg", ""));
248 case "ContextMenu:SetAsDesktopBackground": {
249 let target = lazy.ContentDOMReference.resolve(
250 message.data.targetIdentifier
253 // Paranoia: check disableSetDesktopBackground again, in case the
254 // image changed since the context menu was initiated.
255 let disable = this._disableSetDesktopBackground(target);
259 Services.scriptSecurityManager.checkLoadURIWithPrincipal(
260 target.ownerDocument.nodePrincipal,
263 let canvas = this.document.createElement("canvas");
264 canvas.width = target.naturalWidth;
265 canvas.height = target.naturalHeight;
266 let ctx = canvas.getContext("2d");
267 ctx.drawImage(target, 0, 0);
268 let dataURL = canvas.toDataURL();
269 let url = new URL(target.ownerDocument.location.href).pathname;
270 let imageName = url.substr(url.lastIndexOf("/") + 1);
271 return Promise.resolve({ failed: false, dataURL, imageName });
277 return Promise.resolve({
289 * Returns the event target of the context menu, using a locally stored
290 * reference if possible. If not, and aMessage.objects is defined,
291 * aMessage.objects[aKey] is returned. Otherwise null.
292 * @param {Object} aMessage Message with a objects property
293 * @param {String} aKey Key for the target on aMessage.objects
294 * @return {Object} Context menu target
296 getTarget(aMessage, aKey = "target") {
297 return this.target || (aMessage.objects && aMessage.objects[aKey]);
301 _isXULTextLinkLabel(aNode) {
303 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
305 aNode.namespaceURI == XUL_NS &&
306 aNode.tagName == "label" &&
307 aNode.classList.contains("text-link") &&
312 // Generate fully qualified URL for clicked-on link.
314 let href = this.context.link.href;
318 if (typeof href == "object" && href.animVal) {
319 return this._makeURLAbsolute(this.context.link.baseURI, href.animVal);
326 this.context.link.getAttribute("href") ||
327 this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
329 if (!href || !href.match(/\S/)) {
330 // Without this we try to save as the current doc,
331 // for example, HTML case also throws if empty
332 throw new Error("Empty href");
335 return this._makeURLAbsolute(this.context.link.baseURI, href);
340 return Services.io.newURI(this.context.linkURL);
342 // e.g. empty URL string
350 let text = this._gatherTextUnder(this.context.link);
352 if (!text || !text.match(/\S/)) {
353 text = this.context.link.getAttribute("title");
354 if (!text || !text.match(/\S/)) {
355 text = this.context.link.getAttribute("alt");
356 if (!text || !text.match(/\S/)) {
357 text = this.context.linkURL;
366 if (this.context.linkURI) {
367 return this.context.linkURI.scheme; // can be |undefined|
373 // Returns true if clicked-on link targets a resource that can be saved.
374 _isLinkSaveable(aLink) {
375 // We don't do the Right Thing for news/snews yet, so turn them off
378 this.context.linkProtocol &&
380 this.context.linkProtocol == "mailto" ||
381 this.context.linkProtocol == "tel" ||
382 this.context.linkProtocol == "javascript" ||
383 this.context.linkProtocol == "news" ||
384 this.context.linkProtocol == "snews"
389 // Gather all descendent text under given document node.
390 _gatherTextUnder(root) {
392 let node = root.firstChild;
394 while (node && depth > 0) {
395 // See if this node is text.
396 if (node.nodeType == node.TEXT_NODE) {
397 // Add this text to our collection.
398 text += " " + node.data;
399 } else if (this.contentWindow.HTMLImageElement.isInstance(node)) {
400 // If it has an "alt" attribute, add that.
401 let altText = node.getAttribute("alt");
402 if (altText && altText != "") {
403 text += " " + altText;
406 // Find next node to test.
407 // First, see if this node has children.
408 if (node.hasChildNodes()) {
409 // Go to first child.
410 node = node.firstChild;
413 // No children, try next sibling (or parent next sibling).
414 while (depth > 0 && !node.nextSibling) {
415 node = node.parentNode;
418 if (node.nextSibling) {
419 node = node.nextSibling;
424 // Strip leading and tailing whitespace.
426 // Compress remaining whitespace.
427 text = text.replace(/\s+/g, " ");
431 // Returns a "url"-type computed style attribute value, with the url() stripped.
432 _getComputedURL(aElem, aProp) {
433 let urls = aElem.ownerGlobal.getComputedStyle(aElem).getCSSImageURLs(aProp);
439 if (urls.length != 1) {
440 throw new Error("found multiple URLs");
446 _makeURLAbsolute(aBase, aUrl) {
447 return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
450 _isProprietaryDRM() {
452 this.context.target.isEncrypted &&
453 this.context.target.mediaKeys &&
454 this.context.target.mediaKeys.keySystem != "org.w3.clearkey"
458 _isMediaURLReusable(aURL) {
459 if (aURL.startsWith("blob:")) {
460 return URL.isValidObjectURL(aURL);
466 _isTargetATextBox(node) {
467 if (this.contentWindow.HTMLInputElement.isInstance(node)) {
468 return node.mozIsTextField(false);
471 return this.contentWindow.HTMLTextAreaElement.isInstance(node);
475 * Check if we are in the parent process and the current iframe is the RDM iframe.
477 _isTargetRDMFrame(node) {
479 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT &&
480 node.tagName === "iframe" &&
481 node.hasAttribute("mozbrowser")
485 _isSpellCheckEnabled(aNode) {
486 // We can always force-enable spellchecking on textboxes
487 if (this._isTargetATextBox(aNode)) {
491 // We can never spell check something which is not content editable
492 let editable = aNode.isContentEditable;
494 if (!editable && aNode.ownerDocument) {
495 editable = aNode.ownerDocument.designMode == "on";
502 // Otherwise make sure that nothing in the parent chain disables spellchecking
503 return aNode.spellcheck;
506 _disableSetDesktopBackground(aTarget) {
507 // Disable the Set as Desktop Background menu item if we're still trying
508 // to load the image or the load failed.
509 if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
513 if ("complete" in aTarget && !aTarget.complete) {
517 if (aTarget.currentURI.schemeIs("javascript")) {
521 let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
530 async handleEvent(aEvent) {
531 contextMenus.set(this.browsingContext, this);
533 let defaultPrevented = aEvent.defaultPrevented;
536 // If the event is not from a chrome-privileged document, and if
537 // `dom.event.contextmenu.enabled` is false, force defaultPrevented=false.
538 !aEvent.composedTarget.nodePrincipal.isSystemPrincipal &&
539 !Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
541 defaultPrevented = false;
544 if (defaultPrevented) {
548 if (this._isTargetRDMFrame(aEvent.composedTarget)) {
549 // The target is in the DevTools RDM iframe, a proper context menu event
550 // will be created from the RDM browser.
554 let doc = aEvent.composedTarget.ownerDocument;
556 mozDocumentURIIfNotForErrorPages: docLocation,
557 characterSet: charSet,
560 docLocation = docLocation && docLocation.spec;
561 const loginManagerChild = lazy.LoginManagerChild.forWindow(doc.defaultView);
562 const docState = loginManagerChild.stateForDocument(doc);
563 const loginFillInfo = docState.getFieldContext(aEvent.composedTarget);
565 let disableSetDesktopBackground = null;
567 // Media related cache info parent needs for saving
568 let contentType = null;
569 let contentDisposition = null;
571 aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE &&
572 aEvent.composedTarget instanceof Ci.nsIImageLoadingContent &&
573 aEvent.composedTarget.currentURI
575 disableSetDesktopBackground = this._disableSetDesktopBackground(
576 aEvent.composedTarget
580 let imageCache = Cc["@mozilla.org/image/tools;1"]
581 .getService(Ci.imgITools)
582 .getImgCacheForDocument(doc);
583 // The image cache's notion of where this image is located is
584 // the currentURI of the image loading content.
585 let props = imageCache.findEntryProperties(
586 aEvent.composedTarget.currentURI,
591 contentType = props.get("type", Ci.nsISupportsCString).data;
595 contentDisposition = props.get(
596 "content-disposition",
597 Ci.nsISupportsCString
603 let selectionInfo = lazy.SelectionUtils.getSelectionDetails(
607 this._setContext(aEvent);
608 let context = this.context;
609 this.target = context.target;
611 let spellInfo = null;
612 let editFlags = null;
614 let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
617 referrerInfo.initWithElement(aEvent.composedTarget);
618 referrerInfo = lazy.E10SUtils.serializeReferrerInfo(referrerInfo);
620 // In the case "onLink" we may have to send link referrerInfo to use in
621 // _openLinkInParameters
622 let linkReferrerInfo = null;
623 if (context.onLink) {
624 linkReferrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
627 linkReferrerInfo.initWithElement(context.link);
630 let target = context.target;
632 this._cleanContext();
635 editFlags = lazy.SpellCheckHelper.isEditable(
636 aEvent.composedTarget,
640 if (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) {
641 spellInfo = lazy.InlineSpellCheckerContent.initContextMenu(
648 // Set the event target first as the copy image command needs it to
649 // determine what was context-clicked on. Then, update the state of the
650 // commands on the context menu.
651 this.docShell.contentViewer
652 .QueryInterface(Ci.nsIContentViewerEdit)
653 .setCommandNode(aEvent.composedTarget);
654 aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu");
667 disableSetDesktopBackground,
670 if (context.inFrame && !context.inSrcdocFrame) {
671 data.frameReferrerInfo = lazy.E10SUtils.serializeReferrerInfo(
676 if (linkReferrerInfo) {
677 data.linkReferrerInfo =
678 lazy.E10SUtils.serializeReferrerInfo(linkReferrerInfo);
681 // Notify observers (currently only webextensions) of the context menu being
682 // prepared, allowing them to set webExtContextData for us.
683 let prepareContextMenu = {
684 principal: doc.nodePrincipal,
685 setWebExtContextData(webExtContextData) {
686 data.webExtContextData = webExtContextData;
689 Services.obs.notifyObservers(prepareContextMenu, "on-prepare-contextmenu");
691 // In the event that the content is running in the parent process, we don't
692 // actually want the contextmenu events to reach the parent - we'll dispatch
693 // a new contextmenu event after the async message has reached the parent
695 aEvent.stopPropagation();
697 data.spellInfo = null;
699 this.sendAsyncMessage("contextmenu", data);
704 data.spellInfo = await spellInfo;
706 this.sendAsyncMessage("contextmenu", data);
710 * Some things are not serializable, so we either have to only send
711 * their needed data or regenerate them in nsContextMenu.js
712 * - target and target.ownerDocument
716 _cleanContext(aEvent) {
717 const context = this.context;
718 const cleanTarget = Object.create(null);
720 cleanTarget.ownerDocument = {
721 // used for nsContextMenu.initLeaveDOMFullScreenItems and
722 // nsContextMenu.initMediaPlayerItems
723 fullscreen: context.target.ownerDocument.fullscreen,
725 // used for nsContextMenu.initMiscItems
726 contentType: context.target.ownerDocument.contentType,
729 // used for nsContextMenu.initMediaPlayerItems
730 Object.assign(cleanTarget, {
731 ended: context.target.ended,
732 muted: context.target.muted,
733 paused: context.target.paused,
734 controls: context.target.controls,
735 duration: context.target.duration,
738 const onMedia = context.onVideo || context.onAudio;
741 Object.assign(cleanTarget, {
742 loop: context.target.loop,
743 error: context.target.error,
744 networkState: context.target.networkState,
745 playbackRate: context.target.playbackRate,
746 NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
749 if (context.onVideo) {
750 Object.assign(cleanTarget, {
751 readyState: context.target.readyState,
752 HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
757 context.target = cleanTarget;
760 context.link = { href: context.linkURL };
763 delete context.linkURI;
766 _setContext(aEvent) {
767 this.context = Object.create(null);
768 const context = this.context;
770 context.timeStamp = aEvent.timeStamp;
771 context.screenXDevPx = aEvent.screenX * this.contentWindow.devicePixelRatio;
772 context.screenYDevPx = aEvent.screenY * this.contentWindow.devicePixelRatio;
773 context.inputSource = aEvent.inputSource;
775 let node = aEvent.composedTarget;
777 // Set the node to containing <video>/<audio>/<embed>/<object> if the node
778 // is in the videocontrols UA Widget.
779 if (node.containingShadowRoot?.isUAWidget()) {
780 const host = node.containingShadowRoot.host;
782 this.contentWindow.HTMLMediaElement.isInstance(host) ||
783 this.contentWindow.HTMLEmbedElement.isInstance(host) ||
784 this.contentWindow.HTMLObjectElement.isInstance(host)
791 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
793 context.shouldDisplay = true;
796 node.nodeType == node.DOCUMENT_NODE ||
797 // Don't display for XUL element unless <label class="text-link">
798 (node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))
800 context.shouldDisplay = false;
804 const isAboutDevtoolsToolbox = this.document.documentURI.startsWith(
805 "about:devtools-toolbox"
807 const editFlags = lazy.SpellCheckHelper.isEditable(
813 isAboutDevtoolsToolbox &&
814 (editFlags & lazy.SpellCheckHelper.TEXTINPUT) === 0
816 // Don't display for about:devtools-toolbox page unless the source was text input.
817 context.shouldDisplay = false;
821 // Initialize context to be sent to nsContextMenu
822 // Keep this consistent with the similar code in nsContextMenu's setContext
823 context.bgImageURL = "";
824 context.imageDescURL = "";
825 context.imageInfo = null;
826 context.mediaURL = "";
827 context.webExtBrowserType = "";
829 context.canSpellCheck = false;
830 context.hasBGImage = false;
831 context.hasMultipleBGImages = false;
832 context.isDesignMode = false;
833 context.inFrame = false;
834 context.inPDFViewer = false;
835 context.inSrcdocFrame = false;
836 context.inSyntheticDoc = false;
837 context.inTabBrowser = true;
838 context.inWebExtBrowser = false;
841 context.linkDownload = "";
842 context.linkProtocol = "";
843 context.linkTextStr = "";
844 context.linkURL = "";
845 context.linkURI = null;
847 context.onAudio = false;
848 context.onCanvas = false;
849 context.onCompletedImage = false;
850 context.onDRMMedia = false;
851 context.onPiPVideo = false;
852 context.onEditable = false;
853 context.onImage = false;
854 context.onKeywordField = false;
855 context.onLink = false;
856 context.onLoadedImage = false;
857 context.onMailtoLink = false;
858 context.onTelLink = false;
859 context.onMozExtLink = false;
860 context.onNumeric = false;
861 context.onPassword = false;
862 context.passwordRevealed = false;
863 context.onSaveableLink = false;
864 context.onSpellcheckable = false;
865 context.onTextInput = false;
866 context.onVideo = false;
867 context.inPDFEditor = false;
869 // Remember the node and its owner document that was clicked
870 // This may be modifed before sending to nsContextMenu
871 context.target = node;
872 context.targetIdentifier = lazy.ContentDOMReference.get(node);
874 context.csp = lazy.E10SUtils.serializeCSP(context.target.ownerDocument.csp);
876 // Check if we are in the PDF Viewer.
877 context.inPDFViewer =
878 context.target.ownerDocument.nodePrincipal.originNoSuffix ==
880 if (context.inPDFViewer) {
881 context.pdfEditorStates = context.target.ownerDocument.editorStates;
882 context.inPDFEditor = !!context.pdfEditorStates?.isEditing;
885 // Check if we are in a synthetic document (stand alone image, video, etc.).
886 context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
888 context.shouldInitInlineSpellCheckerUINoChildren = false;
889 context.shouldInitInlineSpellCheckerUIWithChildren = false;
891 this._setContextForNodesNoChildren(editFlags);
892 this._setContextForNodesWithChildren(editFlags);
894 this.lastMenuTarget = {
895 // Remember the node for extensions.
896 targetRef: Cu.getWeakReference(node),
897 // The timestamp is used to verify that the target wasn't changed since the observed menu event.
898 timeStamp: context.timeStamp,
901 if (isAboutDevtoolsToolbox) {
902 // Setup the menu items on text input in about:devtools-toolbox.
903 context.inAboutDevtoolsToolbox = true;
904 context.canSpellCheck = false;
905 context.inTabBrowser = false;
906 context.inFrame = false;
907 context.inSrcdocFrame = false;
908 context.onSpellcheckable = false;
913 * Sets up the parts of the context menu for when when nodes have no children.
915 * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
918 _setContextForNodesNoChildren(editFlags) {
919 const context = this.context;
921 if (context.target.nodeType == context.target.TEXT_NODE) {
922 // For text nodes, look at the parent node to determine the spellcheck attribute.
923 context.canSpellCheck =
924 context.target.parentNode && this._isSpellCheckEnabled(context.target);
928 // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
929 // early if we don't have one.
930 if (context.target.nodeType != context.target.ELEMENT_NODE) {
934 // See if the user clicked on an image. This check mirrors
935 // nsDocumentViewer::GetInImage. Make sure to update both if this is
938 context.target instanceof Ci.nsIImageLoadingContent &&
939 (context.target.currentRequestFinalURI || context.target.currentURI)
941 context.onImage = true;
943 context.imageInfo = {
944 currentSrc: context.target.currentSrc,
945 width: context.target.width,
946 height: context.target.height,
947 imageText: this.contentWindow.ImageDocument.isInstance(
948 context.target.ownerDocument
951 : context.target.title || context.target.alt,
953 const { SVGAnimatedLength } = context.target.ownerGlobal;
954 if (SVGAnimatedLength.isInstance(context.imageInfo.height)) {
955 context.imageInfo.height = context.imageInfo.height.animVal.value;
957 if (SVGAnimatedLength.isInstance(context.imageInfo.width)) {
958 context.imageInfo.width = context.imageInfo.width.animVal.value;
961 const request = context.target.getRequest(
962 Ci.nsIImageLoadingContent.CURRENT_REQUEST
965 if (request && request.imageStatus & request.STATUS_SIZE_AVAILABLE) {
966 context.onLoadedImage = true;
971 request.imageStatus & request.STATUS_LOAD_COMPLETE &&
972 !(request.imageStatus & request.STATUS_ERROR)
974 context.onCompletedImage = true;
977 // The URL of the image before redirects is the currentURI. This is
978 // intended to be used for "Copy Image Link".
979 context.originalMediaURL = (() => {
980 let currentURI = context.target.currentURI?.spec;
981 if (currentURI && this._isMediaURLReusable(currentURI)) {
987 // The actual URL the image was loaded from (after redirects) is the
988 // currentRequestFinalURI. We should use that as the URL for purposes of
989 // deciding on the filename, if it is present. It might not be present
990 // if images are blocked.
992 // It is important to check both the final and the current URI, as they
993 // could be different blob URIs, see bug 1625786.
994 context.mediaURL = (() => {
995 let finalURI = context.target.currentRequestFinalURI?.spec;
996 if (finalURI && this._isMediaURLReusable(finalURI)) {
999 let currentURI = context.target.currentURI?.spec;
1000 if (currentURI && this._isMediaURLReusable(currentURI)) {
1006 const descURL = context.target.getAttribute("longdesc");
1009 context.imageDescURL = this._makeURLAbsolute(
1010 context.target.ownerDocument.body.baseURI,
1015 this.contentWindow.HTMLCanvasElement.isInstance(context.target)
1017 context.onCanvas = true;
1018 } else if (this.contentWindow.HTMLVideoElement.isInstance(context.target)) {
1019 const mediaURL = context.target.currentSrc || context.target.src;
1021 if (this._isMediaURLReusable(mediaURL)) {
1022 context.mediaURL = mediaURL;
1025 if (this._isProprietaryDRM()) {
1026 context.onDRMMedia = true;
1029 if (context.target.isCloningElementVisually) {
1030 context.onPiPVideo = true;
1033 // Firefox always creates a HTMLVideoElement when loading an ogg file
1034 // directly. If the media is actually audio, be smarter and provide a
1035 // context menu with audio operations.
1037 context.target.readyState >= context.target.HAVE_METADATA &&
1038 (context.target.videoWidth == 0 || context.target.videoHeight == 0)
1040 context.onAudio = true;
1042 context.onVideo = true;
1044 } else if (this.contentWindow.HTMLAudioElement.isInstance(context.target)) {
1045 context.onAudio = true;
1046 const mediaURL = context.target.currentSrc || context.target.src;
1048 if (this._isMediaURLReusable(mediaURL)) {
1049 context.mediaURL = mediaURL;
1052 if (this._isProprietaryDRM()) {
1053 context.onDRMMedia = true;
1057 (lazy.SpellCheckHelper.INPUT | lazy.SpellCheckHelper.TEXTAREA)
1059 context.onTextInput = (editFlags & lazy.SpellCheckHelper.TEXTINPUT) !== 0;
1060 context.onNumeric = (editFlags & lazy.SpellCheckHelper.NUMERIC) !== 0;
1061 context.onEditable = (editFlags & lazy.SpellCheckHelper.EDITABLE) !== 0;
1062 context.onPassword = (editFlags & lazy.SpellCheckHelper.PASSWORD) !== 0;
1065 HTMLInputElement.isInstance(context.target) &&
1066 !context.target.disabled &&
1067 !context.target.readOnly &&
1068 (lazy.LoginHelper.isInferredEmailField(context.target) ||
1069 lazy.LoginHelper.isInferredUsernameField(context.target));
1070 context.isDesignMode =
1071 (editFlags & lazy.SpellCheckHelper.CONTENTEDITABLE) !== 0;
1072 context.passwordRevealed =
1073 context.onPassword && context.target.revealPassword;
1074 context.onSpellcheckable =
1075 (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) !== 0;
1077 // This is guaranteed to be an input or textarea because of the condition above,
1078 // so the no-children flag is always correct. We deal with contenteditable elsewhere.
1079 if (context.onSpellcheckable) {
1080 context.shouldInitInlineSpellCheckerUINoChildren = true;
1083 context.onKeywordField = editFlags & lazy.SpellCheckHelper.KEYWORD;
1084 } else if (this.contentWindow.HTMLHtmlElement.isInstance(context.target)) {
1085 const bodyElt = context.target.ownerDocument.body;
1091 computedURL = this._getComputedURL(bodyElt, "background-image");
1092 context.hasMultipleBGImages = false;
1094 context.hasMultipleBGImages = true;
1098 context.hasBGImage = true;
1099 context.bgImageURL = this._makeURLAbsolute(
1107 context.canSpellCheck = this._isSpellCheckEnabled(context.target);
1111 * Sets up the parts of the context menu for when when nodes have children.
1113 * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
1116 _setContextForNodesWithChildren(editFlags) {
1117 const context = this.context;
1119 // Second, bubble out, looking for items of interest that can have childen.
1120 // Always pick the innermost link, background image, etc.
1121 let elem = context.target;
1124 if (elem.nodeType == elem.ELEMENT_NODE) {
1126 const XLINK_NS = "http://www.w3.org/1999/xlink";
1130 // Be consistent with what hrefAndLinkNodeForClickEvent
1131 // does in browser.js
1132 (this._isXULTextLinkLabel(elem) ||
1133 (this.contentWindow.HTMLAnchorElement.isInstance(elem) &&
1135 (this.contentWindow.SVGAElement.isInstance(elem) &&
1136 (elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
1137 (this.contentWindow.HTMLAreaElement.isInstance(elem) &&
1139 this.contentWindow.HTMLLinkElement.isInstance(elem) ||
1140 elem.getAttributeNS(XLINK_NS, "type") == "simple")
1142 // Target is a link or a descendant of a link.
1143 context.onLink = true;
1145 // Remember corresponding element.
1146 context.link = elem;
1147 context.linkURL = this._getLinkURL();
1148 context.linkURI = this._getLinkURI();
1149 context.linkTextStr = this._getLinkText();
1150 context.linkProtocol = this._getLinkProtocol();
1151 context.onMailtoLink = context.linkProtocol == "mailto";
1152 context.onTelLink = context.linkProtocol == "tel";
1153 context.onMozExtLink = context.linkProtocol == "moz-extension";
1154 context.onSaveableLink = this._isLinkSaveable(context.link);
1156 context.isSponsoredLink =
1157 (elem.ownerDocument.URL === "about:newtab" ||
1158 elem.ownerDocument.URL === "about:home") &&
1159 elem.dataset.isSponsoredLink === "true";
1162 if (elem.download) {
1163 // Ignore download attribute on cross-origin links
1164 context.target.ownerDocument.nodePrincipal.checkMayLoad(
1168 context.linkDownload = elem.download;
1173 // Background image? Don't bother if we've already found a
1174 // background image further down the hierarchy. Otherwise,
1175 // we look for the computed background-image style.
1176 if (!context.hasBGImage && !context.hasMultipleBGImages) {
1177 let bgImgUrl = null;
1180 bgImgUrl = this._getComputedURL(elem, "background-image");
1181 context.hasMultipleBGImages = false;
1183 context.hasMultipleBGImages = true;
1187 context.hasBGImage = true;
1188 context.bgImageURL = this._makeURLAbsolute(elem.baseURI, bgImgUrl);
1193 elem = elem.flattenedTreeParentNode;
1196 // See if the user clicked in a frame.
1197 const docDefaultView = context.target.ownerGlobal;
1199 if (docDefaultView != docDefaultView.top) {
1200 context.inFrame = true;
1202 if (context.target.ownerDocument.isSrcdocDocument) {
1203 context.inSrcdocFrame = true;
1207 // if the document is editable, show context menu like in text inputs
1208 if (!context.onEditable) {
1209 if (editFlags & lazy.SpellCheckHelper.CONTENTEDITABLE) {
1210 // If this.onEditable is false but editFlags is CONTENTEDITABLE, then
1211 // the document itself must be editable.
1212 context.onTextInput = true;
1213 context.onKeywordField = false;
1214 context.onImage = false;
1215 context.onLoadedImage = false;
1216 context.onCompletedImage = false;
1217 context.inFrame = false;
1218 context.inSrcdocFrame = false;
1219 context.hasBGImage = false;
1220 context.isDesignMode = true;
1221 context.onEditable = true;
1222 context.onSpellcheckable = true;
1223 context.shouldInitInlineSpellCheckerUIWithChildren = true;
1228 _destructionObservers = new Set();
1229 registerDestructionObserver(obj) {
1230 this._destructionObservers.add(obj);
1233 unregisterDestructionObserver(obj) {
1234 this._destructionObservers.delete(obj);
1238 for (let obs of this._destructionObservers) {
1239 obs.actorDestroyed(this);
1241 this._destructionObservers = null;