Bug 1914102 - Use OffscreenCanvas in TabBase.capture instead of creating a canvas...
[gecko.git] / browser / actors / ContextMenuChild.sys.mjs
blob2b98bea65e6f95ce66d9261a6ca45e1efe71a2a7
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/. */
7 const lazy = {};
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",
18 });
20 let contextMenus = new WeakMap();
22 export class ContextMenuChild extends JSWindowActorChild {
23   // PUBLIC
24   constructor() {
25     super();
27     this.target = null;
28     this.context = null;
29     this.lastMenuTarget = null;
30   }
32   static getTarget(browsingContext, message, key) {
33     let actor = contextMenus.get(browsingContext);
34     if (!actor) {
35       throw new Error(
36         "Can't find ContextMenu actor for browsing context with " +
37           "ID: " +
38           browsingContext.id
39       );
40     }
41     return actor.getTarget(message, key);
42   }
44   static getLastTarget(browsingContext) {
45     let contextMenu = contextMenus.get(browsingContext);
46     return contextMenu && contextMenu.lastMenuTarget;
47   }
49   receiveMessage(message) {
50     switch (message.name) {
51       case "ContextMenu:GetFrameTitle": {
52         let target = lazy.ContentDOMReference.resolve(
53           message.data.targetIdentifier
54         );
55         return Promise.resolve(target.ownerDocument.title);
56       }
58       case "ContextMenu:Canvas:ToBlobURL": {
59         let target = lazy.ContentDOMReference.resolve(
60           message.data.targetIdentifier
61         );
62         return new Promise(resolve => {
63           target.toBlob(blob => {
64             let blobURL = URL.createObjectURL(blob);
65             resolve(blobURL);
66           });
67         });
68       }
70       case "ContextMenu:Hiding": {
71         this.context = null;
72         this.target = null;
73         break;
74       }
76       case "ContextMenu:MediaCommand": {
77         lazy.E10SUtils.wrapHandlingUserInput(
78           this.contentWindow,
79           message.data.handlingUserInput,
80           () => {
81             let media = lazy.ContentDOMReference.resolve(
82               message.data.targetIdentifier
83             );
85             switch (message.data.command) {
86               case "play":
87                 media.play();
88                 break;
89               case "pause":
90                 media.pause();
91                 break;
92               case "loop":
93                 media.loop = !media.loop;
94                 break;
95               case "mute":
96                 media.muted = true;
97                 break;
98               case "unmute":
99                 media.muted = false;
100                 break;
101               case "playbackRate":
102                 media.playbackRate = message.data.data;
103                 break;
104               case "hidecontrols":
105                 media.removeAttribute("controls");
106                 break;
107               case "showcontrols":
108                 media.setAttribute("controls", "true");
109                 break;
110               case "fullscreen":
111                 if (this.document.fullscreenEnabled) {
112                   media.requestFullscreen();
113                 }
114                 break;
115               case "pictureinpicture":
116                 if (!media.isCloningElementVisually) {
117                   Services.telemetry.keyedScalarAdd(
118                     "pictureinpicture.opened_method",
119                     "contextmenu",
120                     1
121                   );
122                 }
123                 let event = new this.contentWindow.CustomEvent(
124                   "MozTogglePictureInPicture",
125                   {
126                     bubbles: true,
127                     detail: { reason: "contextMenu" },
128                   },
129                   this.contentWindow
130                 );
131                 media.dispatchEvent(event);
132                 break;
133             }
134           }
135         );
136         break;
137       }
139       case "ContextMenu:ReloadFrame": {
140         let target = lazy.ContentDOMReference.resolve(
141           message.data.targetIdentifier
142         );
143         target.ownerDocument.location.reload(message.data.forceReload);
144         break;
145       }
147       case "ContextMenu:GetImageText": {
148         let img = lazy.ContentDOMReference.resolve(
149           message.data.targetIdentifier
150         );
151         const { direction } = this.contentWindow.getComputedStyle(img);
153         return img.recognizeCurrentImageText().then(results => {
154           return { results, direction };
155         });
156       }
158       case "ContextMenu:ToggleRevealPassword": {
159         let target = lazy.ContentDOMReference.resolve(
160           message.data.targetIdentifier
161         );
162         target.revealPassword = !target.revealPassword;
163         break;
164       }
166       case "ContextMenu:UseRelayMask": {
167         const input = lazy.ContentDOMReference.resolve(
168           message.data.targetIdentifier
169         );
170         input.setUserInput(message.data.emailMask);
171         break;
172       }
174       case "ContextMenu:ReloadImage": {
175         let image = lazy.ContentDOMReference.resolve(
176           message.data.targetIdentifier
177         );
179         if (image instanceof Ci.nsIImageLoadingContent) {
180           image.forceReload();
181         }
182         break;
183       }
185       case "ContextMenu:SearchFieldBookmarkData": {
186         let node = lazy.ContentDOMReference.resolve(
187           message.data.targetIdentifier
188         );
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"),
193           charset,
194           formBaseURI
195         );
196         let spec = formURI.spec;
197         let isURLEncoded =
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]) {
204           if (isURLEncoded) {
205             return escape(aName + "=" + aValue);
206           }
208           return encodeURIComponent(aName) + "=" + encodeURIComponent(aValue);
209         }
210         let formData = new this.contentWindow.FormData(node.form);
211         formData.delete(node.name);
212         formData = Array.from(formData).map(escapeNameValuePair);
213         formData.push(
214           escape(node.name) + (isURLEncoded ? escape("=%s") : "=%s")
215         );
217         let postData;
219         if (isURLEncoded) {
220           postData = formData.join("&");
221         } else {
222           let separator = spec.includes("?") ? "&" : "?";
223           spec += separator + formData.join("&");
224         }
226         return Promise.resolve({ spec, title, postData, charset });
227       }
229       case "ContextMenu:SaveVideoFrameAsImage": {
230         let video = lazy.ContentDOMReference.resolve(
231           message.data.targetIdentifier
232         );
233         let canvas = this.document.createElementNS(
234           "http://www.w3.org/1999/xhtml",
235           "canvas"
236         );
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", ""));
246       }
248       case "ContextMenu:SetAsDesktopBackground": {
249         let target = lazy.ContentDOMReference.resolve(
250           message.data.targetIdentifier
251         );
253         // Paranoia: check disableSetDesktopBackground again, in case the
254         // image changed since the context menu was initiated.
255         let disable = this._disableSetDesktopBackground(target);
257         if (!disable) {
258           try {
259             Services.scriptSecurityManager.checkLoadURIWithPrincipal(
260               target.ownerDocument.nodePrincipal,
261               target.currentURI
262             );
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 });
272           } catch (e) {
273             console.error(e);
274           }
275         }
277         return Promise.resolve({
278           failed: true,
279           dataURL: null,
280           imageName: null,
281         });
282       }
283     }
285     return undefined;
286   }
288   /**
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
295    */
296   getTarget(aMessage, aKey = "target") {
297     return this.target || (aMessage.objects && aMessage.objects[aKey]);
298   }
300   // PRIVATE
301   _isXULTextLinkLabel(aNode) {
302     const XUL_NS =
303       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
304     return (
305       aNode.namespaceURI == XUL_NS &&
306       aNode.tagName == "label" &&
307       aNode.classList.contains("text-link") &&
308       aNode.href
309     );
310   }
312   // Generate fully qualified URL for clicked-on link.
313   _getLinkURL() {
314     let href = this.context.link.href;
316     if (href) {
317       // Handle SVG links:
318       if (typeof href == "object" && href.animVal) {
319         return this._makeURLAbsolute(this.context.link.baseURI, href.animVal);
320       }
322       return href;
323     }
325     href =
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");
333     }
335     return this._makeURLAbsolute(this.context.link.baseURI, href);
336   }
338   _getLinkURI() {
339     try {
340       return Services.io.newURI(this.context.linkURL);
341     } catch (ex) {
342       // e.g. empty URL string
343     }
345     return null;
346   }
348   // Get text of link.
349   _getLinkText() {
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;
358         }
359       }
360     }
362     return text;
363   }
365   _getLinkProtocol() {
366     if (this.context.linkURI) {
367       return this.context.linkURI.scheme; // can be |undefined|
368     }
370     return null;
371   }
373   // Returns true if clicked-on link targets a resource that can be saved.
374   _isLinkSaveable() {
375     // We don't do the Right Thing for news/snews yet, so turn them off
376     // until we do.
377     return (
378       this.context.linkProtocol &&
379       !(
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"
385       )
386     );
387   }
389   // Gather all descendent text under given document node.
390   _gatherTextUnder(root) {
391     let text = "";
392     let node = root.firstChild;
393     let depth = 1;
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;
404         }
405       }
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;
411         depth++;
412       } else {
413         // No children, try next sibling (or parent next sibling).
414         while (depth > 0 && !node.nextSibling) {
415           node = node.parentNode;
416           depth--;
417         }
418         if (node.nextSibling) {
419           node = node.nextSibling;
420         }
421       }
422     }
424     // Strip leading and tailing whitespace.
425     text = text.trim();
426     // Compress remaining whitespace.
427     text = text.replace(/\s+/g, " ");
428     return text;
429   }
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);
435     if (!urls.length) {
436       return null;
437     }
439     if (urls.length != 1) {
440       throw new Error("found multiple URLs");
441     }
443     return urls[0];
444   }
446   _makeURLAbsolute(aBase, aUrl) {
447     return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
448   }
450   _isProprietaryDRM() {
451     return (
452       this.context.target.isEncrypted &&
453       this.context.target.mediaKeys &&
454       this.context.target.mediaKeys.keySystem != "org.w3.clearkey"
455     );
456   }
458   _isMediaURLReusable(aURL) {
459     if (aURL.startsWith("blob:")) {
460       return URL.isValidObjectURL(aURL);
461     }
463     return true;
464   }
466   _isTargetATextBox(node) {
467     if (this.contentWindow.HTMLInputElement.isInstance(node)) {
468       return node.mozIsTextField(false);
469     }
471     return this.contentWindow.HTMLTextAreaElement.isInstance(node);
472   }
474   _isSpellCheckEnabled(aNode) {
475     // We can always force-enable spellchecking on textboxes
476     if (this._isTargetATextBox(aNode)) {
477       return true;
478     }
480     // We can never spell check something which is not content editable
481     let editable = aNode.isContentEditable;
483     if (!editable && aNode.ownerDocument) {
484       editable = aNode.ownerDocument.designMode == "on";
485     }
487     if (!editable) {
488       return false;
489     }
491     // Otherwise make sure that nothing in the parent chain disables spellchecking
492     return aNode.spellcheck;
493   }
495   _disableSetDesktopBackground(aTarget) {
496     // Disable the Set as Desktop Background menu item if we're still trying
497     // to load the image or the load failed.
498     if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
499       return true;
500     }
502     if ("complete" in aTarget && !aTarget.complete) {
503       return true;
504     }
506     if (aTarget.currentURI.schemeIs("javascript")) {
507       return true;
508     }
510     let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
512     if (!request) {
513       return true;
514     }
516     return false;
517   }
519   async handleEvent(aEvent) {
520     contextMenus.set(this.browsingContext, this);
522     let defaultPrevented = aEvent.defaultPrevented;
524     if (
525       // If the event is not from a chrome-privileged document, and if
526       // `dom.event.contextmenu.enabled` is false, force defaultPrevented=false.
527       !aEvent.composedTarget.nodePrincipal.isSystemPrincipal &&
528       !Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
529     ) {
530       defaultPrevented = false;
531     }
533     if (defaultPrevented) {
534       return;
535     }
537     let doc = aEvent.composedTarget.ownerDocument;
538     if (!doc && Cu.isInAutomation) {
539       // doc has been observed to be null for many years, causing intermittent
540       // test failures all over the place (bug 1478596). The rate of failures
541       // is too low to debug locally, but frequent enough to be a nuisance.
542       // TODO bug 1478596: use these diagnostic logs to resolve the bug.
543       dump(
544         `doc is unexpectedly null (bug 1478596), composedTarget=${aEvent.composedTarget}\n`
545       );
546       // A potential fix is to fall back to aEvent.target.ownerDocument, per
547       // https://bugzilla.mozilla.org/show_bug.cgi?id=1478596#c1
548       // Let's print potentially viable alternatives to see what we should use.
549       for (let k of ["target", "originalTarget", "explicitOriginalTarget"]) {
550         dump(
551           ` Alternative: ${k}=${aEvent[k]} and its doc=${aEvent[k]?.ownerDocument}\n`
552         );
553       }
554     }
555     let {
556       mozDocumentURIIfNotForErrorPages: docLocation,
557       characterSet: charSet,
558       baseURI,
559     } = doc;
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;
570     if (
571       aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE &&
572       aEvent.composedTarget instanceof Ci.nsIImageLoadingContent &&
573       aEvent.composedTarget.currentURI
574     ) {
575       disableSetDesktopBackground = this._disableSetDesktopBackground(
576         aEvent.composedTarget
577       );
579       try {
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,
587           doc
588         );
590         try {
591           contentType = props.get("type", Ci.nsISupportsCString).data;
592         } catch (e) {}
594         try {
595           contentDisposition = props.get(
596             "content-disposition",
597             Ci.nsISupportsCString
598           ).data;
599         } catch (e) {}
600       } catch (e) {}
601     }
603     let selectionInfo = lazy.SelectionUtils.getSelectionDetails(
604       this.contentWindow
605     );
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(
615       Ci.nsIReferrerInfo
616     );
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(
625         Ci.nsIReferrerInfo
626       );
627       linkReferrerInfo.initWithElement(context.link);
628     }
630     let target = context.target;
631     if (target) {
632       this._cleanContext();
633     }
635     editFlags = lazy.SpellCheckHelper.isEditable(
636       aEvent.composedTarget,
637       this.contentWindow
638     );
640     if (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) {
641       spellInfo = lazy.InlineSpellCheckerContent.initContextMenu(
642         aEvent,
643         editFlags,
644         this
645       );
646     }
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.docViewer
652       .QueryInterface(Ci.nsIDocumentViewerEdit)
653       .setCommandNode(aEvent.composedTarget);
654     aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu");
656     let data = {
657       context,
658       charSet,
659       baseURI,
660       referrerInfo,
661       editFlags,
662       contentType,
663       docLocation,
664       loginFillInfo,
665       selectionInfo,
666       contentDisposition,
667       disableSetDesktopBackground,
668     };
670     if (context.inFrame && !context.inSrcdocFrame) {
671       data.frameReferrerInfo = lazy.E10SUtils.serializeReferrerInfo(
672         doc.referrerInfo
673       );
674     }
676     if (linkReferrerInfo) {
677       data.linkReferrerInfo =
678         lazy.E10SUtils.serializeReferrerInfo(linkReferrerInfo);
679     }
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;
687       },
688     };
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
694     // instead.
695     aEvent.stopPropagation();
697     data.spellInfo = null;
698     if (!spellInfo) {
699       this.sendAsyncMessage("contextmenu", data);
700       return;
701     }
703     try {
704       data.spellInfo = await spellInfo;
705     } catch (ex) {}
706     this.sendAsyncMessage("contextmenu", data);
707   }
709   /**
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
713    * - link
714    * - linkURI
715    */
716   _cleanContext() {
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,
727     };
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,
736     });
738     const onMedia = context.onVideo || context.onAudio;
740     if (onMedia) {
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,
747       });
749       if (context.onVideo) {
750         Object.assign(cleanTarget, {
751           readyState: context.target.readyState,
752           HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
753         });
754       }
755     }
757     context.target = cleanTarget;
759     if (context.link) {
760       context.link = { href: context.linkURL };
761     }
763     delete context.linkURI;
764   }
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;
781       if (
782         this.contentWindow.HTMLMediaElement.isInstance(host) ||
783         this.contentWindow.HTMLEmbedElement.isInstance(host) ||
784         this.contentWindow.HTMLObjectElement.isInstance(host)
785       ) {
786         node = host;
787       }
788     }
790     const XUL_NS =
791       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
793     context.shouldDisplay = true;
795     if (
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))
799     ) {
800       context.shouldDisplay = false;
801       return;
802     }
804     const isAboutDevtoolsToolbox = this.document.documentURI.startsWith(
805       "about:devtools-toolbox"
806     );
807     const editFlags = lazy.SpellCheckHelper.isEditable(
808       node,
809       this.contentWindow
810     );
812     if (
813       isAboutDevtoolsToolbox &&
814       (editFlags & lazy.SpellCheckHelper.TEXTINPUT) === 0
815     ) {
816       // Don't display for about:devtools-toolbox page unless the source was text input.
817       context.shouldDisplay = false;
818       return;
819     }
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;
840     context.link = null;
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 ==
879       "resource://pdf.js";
880     if (context.inPDFViewer) {
881       context.pdfEditorStates = context.target.ownerDocument.editorStates;
882       context.inPDFEditor = !!context.pdfEditorStates?.isEditing;
883     }
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,
899     };
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;
909     }
910   }
912   /**
913    * Sets up the parts of the context menu for when when nodes have no children.
914    *
915    * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
916    *                            for the details.
917    */
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);
925       return;
926     }
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) {
931       return;
932     }
934     // See if the user clicked on an image. This check mirrors
935     // nsDocumentViewer::GetInImage. Make sure to update both if this is
936     // changed.
937     if (
938       context.target instanceof Ci.nsIImageLoadingContent &&
939       (context.target.currentRequestFinalURI || context.target.currentURI)
940     ) {
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
949         )
950           ? undefined
951           : context.target.title || context.target.alt,
952       };
953       const { SVGAnimatedLength } = context.target.ownerGlobal;
954       if (SVGAnimatedLength.isInstance(context.imageInfo.height)) {
955         context.imageInfo.height = context.imageInfo.height.animVal.value;
956       }
957       if (SVGAnimatedLength.isInstance(context.imageInfo.width)) {
958         context.imageInfo.width = context.imageInfo.width.animVal.value;
959       }
961       const request = context.target.getRequest(
962         Ci.nsIImageLoadingContent.CURRENT_REQUEST
963       );
965       if (request && request.imageStatus & request.STATUS_SIZE_AVAILABLE) {
966         context.onLoadedImage = true;
967       }
969       if (
970         request &&
971         request.imageStatus & request.STATUS_LOAD_COMPLETE &&
972         !(request.imageStatus & request.STATUS_ERROR)
973       ) {
974         context.onCompletedImage = true;
975       }
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)) {
982           return currentURI;
983         }
984         return "";
985       })();
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.
991       //
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)) {
997           return finalURI;
998         }
999         let currentURI = context.target.currentURI?.spec;
1000         if (currentURI && this._isMediaURLReusable(currentURI)) {
1001           return currentURI;
1002         }
1003         return "";
1004       })();
1006       const descURL = context.target.getAttribute("longdesc");
1008       if (descURL) {
1009         context.imageDescURL = this._makeURLAbsolute(
1010           context.target.ownerDocument.body.baseURI,
1011           descURL
1012         );
1013       }
1014     } else if (
1015       this.contentWindow.HTMLCanvasElement.isInstance(context.target)
1016     ) {
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;
1023       }
1025       if (this._isProprietaryDRM()) {
1026         context.onDRMMedia = true;
1027       }
1029       if (context.target.isCloningElementVisually) {
1030         context.onPiPVideo = true;
1031       }
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.
1036       if (
1037         context.target.readyState >= context.target.HAVE_METADATA &&
1038         (context.target.videoWidth == 0 || context.target.videoHeight == 0)
1039       ) {
1040         context.onAudio = true;
1041       } else {
1042         context.onVideo = true;
1043       }
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;
1050       }
1052       if (this._isProprietaryDRM()) {
1053         context.onDRMMedia = true;
1054       }
1055     } else if (
1056       editFlags &
1057       (lazy.SpellCheckHelper.INPUT | lazy.SpellCheckHelper.TEXTAREA)
1058     ) {
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;
1064       context.showRelay =
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;
1081       }
1083       context.onKeywordField = editFlags & lazy.SpellCheckHelper.KEYWORD;
1084     } else if (this.contentWindow.HTMLHtmlElement.isInstance(context.target)) {
1085       const bodyElt = context.target.ownerDocument.body;
1087       if (bodyElt) {
1088         let computedURL;
1090         try {
1091           computedURL = this._getComputedURL(bodyElt, "background-image");
1092           context.hasMultipleBGImages = false;
1093         } catch (e) {
1094           context.hasMultipleBGImages = true;
1095         }
1097         if (computedURL) {
1098           context.hasBGImage = true;
1099           context.bgImageURL = this._makeURLAbsolute(
1100             bodyElt.baseURI,
1101             computedURL
1102           );
1103         }
1104       }
1105     }
1107     context.canSpellCheck = this._isSpellCheckEnabled(context.target);
1108   }
1110   /**
1111    * Sets up the parts of the context menu for when when nodes have children.
1112    *
1113    * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
1114    *                            for the details.
1115    */
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;
1123     while (elem) {
1124       if (elem.nodeType == elem.ELEMENT_NODE) {
1125         // Link?
1126         const XLINK_NS = "http://www.w3.org/1999/xlink";
1128         if (
1129           !context.onLink &&
1130           // Be consistent with what hrefAndLinkNodeForClickEvent
1131           // does in browser.js
1132           (this._isXULTextLinkLabel(elem) ||
1133             (this.contentWindow.HTMLAnchorElement.isInstance(elem) &&
1134               elem.href) ||
1135             (this.contentWindow.SVGAElement.isInstance(elem) &&
1136               (elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
1137             (this.contentWindow.HTMLAreaElement.isInstance(elem) &&
1138               elem.href) ||
1139             this.contentWindow.HTMLLinkElement.isInstance(elem) ||
1140             elem.getAttributeNS(XLINK_NS, "type") == "simple")
1141         ) {
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";
1161           try {
1162             if (elem.download) {
1163               // Ignore download attribute on cross-origin links
1164               context.target.ownerDocument.nodePrincipal.checkMayLoad(
1165                 context.linkURI,
1166                 true
1167               );
1168               context.linkDownload = elem.download;
1169             }
1170           } catch (ex) {}
1171         }
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;
1179           try {
1180             bgImgUrl = this._getComputedURL(elem, "background-image");
1181             context.hasMultipleBGImages = false;
1182           } catch (e) {
1183             context.hasMultipleBGImages = true;
1184           }
1186           if (bgImgUrl) {
1187             context.hasBGImage = true;
1188             context.bgImageURL = this._makeURLAbsolute(elem.baseURI, bgImgUrl);
1189           }
1190         }
1191       }
1193       elem = elem.flattenedTreeParentNode;
1194     }
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;
1204       }
1205     }
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;
1224       }
1225     }
1226   }
1228   _destructionObservers = new Set();
1229   registerDestructionObserver(obj) {
1230     this._destructionObservers.add(obj);
1231   }
1233   unregisterDestructionObserver(obj) {
1234     this._destructionObservers.delete(obj);
1235   }
1237   didDestroy() {
1238     for (let obs of this._destructionObservers) {
1239       obs.actorDestroyed(this);
1240     }
1241     this._destructionObservers = null;
1242   }