forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1243 lines
		
	
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1243 lines
		
	
	
	
		
			38 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/. */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
 | |
|   E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
 | |
|   InlineSpellCheckerContent:
 | |
|     "resource://gre/modules/InlineSpellCheckerContent.sys.mjs",
 | |
|   LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
 | |
|   LoginManagerChild: "resource://gre/modules/LoginManagerChild.sys.mjs",
 | |
|   SelectionUtils: "resource://gre/modules/SelectionUtils.sys.mjs",
 | |
|   SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.sys.mjs",
 | |
| });
 | |
| 
 | |
| let contextMenus = new WeakMap();
 | |
| 
 | |
| export class ContextMenuChild extends JSWindowActorChild {
 | |
|   // PUBLIC
 | |
|   constructor() {
 | |
|     super();
 | |
| 
 | |
|     this.target = null;
 | |
|     this.context = null;
 | |
|     this.lastMenuTarget = null;
 | |
|   }
 | |
| 
 | |
|   static getTarget(browsingContext, message, key) {
 | |
|     let actor = contextMenus.get(browsingContext);
 | |
|     if (!actor) {
 | |
|       throw new Error(
 | |
|         "Can't find ContextMenu actor for browsing context with " +
 | |
|           "ID: " +
 | |
|           browsingContext.id
 | |
|       );
 | |
|     }
 | |
|     return actor.getTarget(message, key);
 | |
|   }
 | |
| 
 | |
|   static getLastTarget(browsingContext) {
 | |
|     let contextMenu = contextMenus.get(browsingContext);
 | |
|     return contextMenu && contextMenu.lastMenuTarget;
 | |
|   }
 | |
| 
 | |
|   receiveMessage(message) {
 | |
|     switch (message.name) {
 | |
|       case "ContextMenu:GetFrameTitle": {
 | |
|         let target = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         return Promise.resolve(target.ownerDocument.title);
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:Canvas:ToBlobURL": {
 | |
|         let target = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         return new Promise(resolve => {
 | |
|           target.toBlob(blob => {
 | |
|             let blobURL = URL.createObjectURL(blob);
 | |
|             resolve(blobURL);
 | |
|           });
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:Hiding": {
 | |
|         this.context = null;
 | |
|         this.target = null;
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:MediaCommand": {
 | |
|         lazy.E10SUtils.wrapHandlingUserInput(
 | |
|           this.contentWindow,
 | |
|           message.data.handlingUserInput,
 | |
|           () => {
 | |
|             let media = lazy.ContentDOMReference.resolve(
 | |
|               message.data.targetIdentifier
 | |
|             );
 | |
| 
 | |
|             switch (message.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 = message.data.data;
 | |
|                 break;
 | |
|               case "hidecontrols":
 | |
|                 media.removeAttribute("controls");
 | |
|                 break;
 | |
|               case "showcontrols":
 | |
|                 media.setAttribute("controls", "true");
 | |
|                 break;
 | |
|               case "fullscreen":
 | |
|                 if (this.document.fullscreenEnabled) {
 | |
|                   media.requestFullscreen();
 | |
|                 }
 | |
|                 break;
 | |
|               case "pictureinpicture":
 | |
|                 if (!media.isCloningElementVisually) {
 | |
|                   Services.telemetry.keyedScalarAdd(
 | |
|                     "pictureinpicture.opened_method",
 | |
|                     "contextmenu",
 | |
|                     1
 | |
|                   );
 | |
|                 }
 | |
|                 let event = new this.contentWindow.CustomEvent(
 | |
|                   "MozTogglePictureInPicture",
 | |
|                   {
 | |
|                     bubbles: true,
 | |
|                     detail: { reason: "contextMenu" },
 | |
|                   },
 | |
|                   this.contentWindow
 | |
|                 );
 | |
|                 media.dispatchEvent(event);
 | |
|                 break;
 | |
|             }
 | |
|           }
 | |
|         );
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:ReloadFrame": {
 | |
|         let target = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         target.ownerDocument.location.reload(message.data.forceReload);
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:GetImageText": {
 | |
|         let img = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         const { direction } = this.contentWindow.getComputedStyle(img);
 | |
| 
 | |
|         return img.recognizeCurrentImageText().then(results => {
 | |
|           return { results, direction };
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:ToggleRevealPassword": {
 | |
|         let target = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         target.revealPassword = !target.revealPassword;
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:UseRelayMask": {
 | |
|         const input = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         input.setUserInput(message.data.emailMask);
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:ReloadImage": {
 | |
|         let image = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
| 
 | |
|         if (image instanceof Ci.nsIImageLoadingContent) {
 | |
|           image.forceReload();
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:SearchFieldBookmarkData": {
 | |
|         let node = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         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;
 | |
| 
 | |
|         function escapeNameValuePair([aName, aValue]) {
 | |
|           if (isURLEncoded) {
 | |
|             return escape(aName + "=" + aValue);
 | |
|           }
 | |
| 
 | |
|           return encodeURIComponent(aName) + "=" + encodeURIComponent(aValue);
 | |
|         }
 | |
|         let formData = new this.contentWindow.FormData(node.form);
 | |
|         formData.delete(node.name);
 | |
|         formData = Array.from(formData).map(escapeNameValuePair);
 | |
|         formData.push(
 | |
|           escape(node.name) + (isURLEncoded ? escape("=%s") : "=%s")
 | |
|         );
 | |
| 
 | |
|         let postData;
 | |
| 
 | |
|         if (isURLEncoded) {
 | |
|           postData = formData.join("&");
 | |
|         } else {
 | |
|           let separator = spec.includes("?") ? "&" : "?";
 | |
|           spec += separator + formData.join("&");
 | |
|         }
 | |
| 
 | |
|         return Promise.resolve({ spec, title, postData, charset });
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:SaveVideoFrameAsImage": {
 | |
|         let video = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
|         let canvas = this.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);
 | |
| 
 | |
|         // Note: if changing the content type, don't forget to update
 | |
|         // consumers that also hardcode this content type.
 | |
|         return Promise.resolve(canvas.toDataURL("image/jpeg", ""));
 | |
|       }
 | |
| 
 | |
|       case "ContextMenu:SetAsDesktopBackground": {
 | |
|         let target = lazy.ContentDOMReference.resolve(
 | |
|           message.data.targetIdentifier
 | |
|         );
 | |
| 
 | |
|         // Paranoia: check disableSetDesktopBackground again, in case the
 | |
|         // image changed since the context menu was initiated.
 | |
|         let disable = this._disableSetDesktopBackground(target);
 | |
| 
 | |
|         if (!disable) {
 | |
|           try {
 | |
|             Services.scriptSecurityManager.checkLoadURIWithPrincipal(
 | |
|               target.ownerDocument.nodePrincipal,
 | |
|               target.currentURI
 | |
|             );
 | |
|             let canvas = this.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);
 | |
|             return Promise.resolve({ failed: false, dataURL, imageName });
 | |
|           } catch (e) {
 | |
|             console.error(e);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         return Promise.resolve({
 | |
|           failed: true,
 | |
|           dataURL: null,
 | |
|           imageName: null,
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return undefined;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 new Error("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 == "tel" ||
 | |
|         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 == node.TEXT_NODE) {
 | |
|         // Add this text to our collection.
 | |
|         text += " " + node.data;
 | |
|       } else if (this.contentWindow.HTMLImageElement.isInstance(node)) {
 | |
|         // 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 urls = aElem.ownerGlobal.getComputedStyle(aElem).getCSSImageURLs(aProp);
 | |
| 
 | |
|     if (!urls.length) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     if (urls.length != 1) {
 | |
|       throw new Error("found multiple URLs");
 | |
|     }
 | |
| 
 | |
|     return urls[0];
 | |
|   }
 | |
| 
 | |
|   _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.isValidObjectURL(aURL);
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   _isTargetATextBox(node) {
 | |
|     if (this.contentWindow.HTMLInputElement.isInstance(node)) {
 | |
|       return node.mozIsTextField(false);
 | |
|     }
 | |
| 
 | |
|     return this.contentWindow.HTMLTextAreaElement.isInstance(node);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if we are in the parent process and the current iframe is the RDM iframe.
 | |
|    */
 | |
|   _isTargetRDMFrame(node) {
 | |
|     return (
 | |
|       Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT &&
 | |
|       node.tagName === "iframe" &&
 | |
|       node.hasAttribute("mozbrowser")
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   _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;
 | |
|   }
 | |
| 
 | |
|   async handleEvent(aEvent) {
 | |
|     contextMenus.set(this.browsingContext, this);
 | |
| 
 | |
|     let defaultPrevented = aEvent.defaultPrevented;
 | |
| 
 | |
|     if (
 | |
|       // If the event is not from a chrome-privileged document, and if
 | |
|       // `dom.event.contextmenu.enabled` is false, force defaultPrevented=false.
 | |
|       !aEvent.composedTarget.nodePrincipal.isSystemPrincipal &&
 | |
|       !Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
 | |
|     ) {
 | |
|       defaultPrevented = false;
 | |
|     }
 | |
| 
 | |
|     if (defaultPrevented) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this._isTargetRDMFrame(aEvent.composedTarget)) {
 | |
|       // The target is in the DevTools RDM iframe, a proper context menu event
 | |
|       // will be created from the RDM browser.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let doc = aEvent.composedTarget.ownerDocument;
 | |
|     let {
 | |
|       mozDocumentURIIfNotForErrorPages: docLocation,
 | |
|       characterSet: charSet,
 | |
|       baseURI,
 | |
|     } = doc;
 | |
|     docLocation = docLocation && docLocation.spec;
 | |
|     const loginManagerChild = lazy.LoginManagerChild.forWindow(doc.defaultView);
 | |
|     const docState = loginManagerChild.stateForDocument(doc);
 | |
|     const loginFillInfo = docState.getFieldContext(aEvent.composedTarget);
 | |
| 
 | |
|     let disableSetDesktopBackground = null;
 | |
| 
 | |
|     // Media related cache info parent needs for saving
 | |
|     let contentType = null;
 | |
|     let contentDisposition = null;
 | |
|     if (
 | |
|       aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE &&
 | |
|       aEvent.composedTarget instanceof Ci.nsIImageLoadingContent &&
 | |
|       aEvent.composedTarget.currentURI
 | |
|     ) {
 | |
|       disableSetDesktopBackground = this._disableSetDesktopBackground(
 | |
|         aEvent.composedTarget
 | |
|       );
 | |
| 
 | |
|       try {
 | |
|         let imageCache = Cc["@mozilla.org/image/tools;1"]
 | |
|           .getService(Ci.imgITools)
 | |
|           .getImgCacheForDocument(doc);
 | |
|         // The image cache's notion of where this image is located is
 | |
|         // the currentURI of the image loading content.
 | |
|         let props = imageCache.findEntryProperties(
 | |
|           aEvent.composedTarget.currentURI,
 | |
|           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 = lazy.SelectionUtils.getSelectionDetails(
 | |
|       this.contentWindow
 | |
|     );
 | |
| 
 | |
|     this._setContext(aEvent);
 | |
|     let context = this.context;
 | |
|     this.target = context.target;
 | |
| 
 | |
|     let spellInfo = null;
 | |
|     let editFlags = null;
 | |
| 
 | |
|     let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
 | |
|       Ci.nsIReferrerInfo
 | |
|     );
 | |
|     referrerInfo.initWithElement(aEvent.composedTarget);
 | |
|     referrerInfo = lazy.E10SUtils.serializeReferrerInfo(referrerInfo);
 | |
| 
 | |
|     // In the case "onLink" we may have to send link referrerInfo to use in
 | |
|     // _openLinkInParameters
 | |
|     let linkReferrerInfo = null;
 | |
|     if (context.onLink) {
 | |
|       linkReferrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
 | |
|         Ci.nsIReferrerInfo
 | |
|       );
 | |
|       linkReferrerInfo.initWithElement(context.link);
 | |
|     }
 | |
| 
 | |
|     let target = context.target;
 | |
|     if (target) {
 | |
|       this._cleanContext();
 | |
|     }
 | |
| 
 | |
|     editFlags = lazy.SpellCheckHelper.isEditable(
 | |
|       aEvent.composedTarget,
 | |
|       this.contentWindow
 | |
|     );
 | |
| 
 | |
|     if (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) {
 | |
|       spellInfo = lazy.InlineSpellCheckerContent.initContextMenu(
 | |
|         aEvent,
 | |
|         editFlags,
 | |
|         this
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // 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.docShell.contentViewer
 | |
|       .QueryInterface(Ci.nsIContentViewerEdit)
 | |
|       .setCommandNode(aEvent.composedTarget);
 | |
|     aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu");
 | |
| 
 | |
|     let data = {
 | |
|       context,
 | |
|       charSet,
 | |
|       baseURI,
 | |
|       referrerInfo,
 | |
|       editFlags,
 | |
|       contentType,
 | |
|       docLocation,
 | |
|       loginFillInfo,
 | |
|       selectionInfo,
 | |
|       contentDisposition,
 | |
|       disableSetDesktopBackground,
 | |
|     };
 | |
| 
 | |
|     if (context.inFrame && !context.inSrcdocFrame) {
 | |
|       data.frameReferrerInfo = lazy.E10SUtils.serializeReferrerInfo(
 | |
|         doc.referrerInfo
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (linkReferrerInfo) {
 | |
|       data.linkReferrerInfo =
 | |
|         lazy.E10SUtils.serializeReferrerInfo(linkReferrerInfo);
 | |
|     }
 | |
| 
 | |
|     // Notify observers (currently only webextensions) of the context menu being
 | |
|     // prepared, allowing them to set webExtContextData for us.
 | |
|     let prepareContextMenu = {
 | |
|       principal: doc.nodePrincipal,
 | |
|       setWebExtContextData(webExtContextData) {
 | |
|         data.webExtContextData = webExtContextData;
 | |
|       },
 | |
|     };
 | |
|     Services.obs.notifyObservers(prepareContextMenu, "on-prepare-contextmenu");
 | |
| 
 | |
|     // In the event that the content is running in the parent process, we don't
 | |
|     // actually want the contextmenu events to reach the parent - we'll dispatch
 | |
|     // a new contextmenu event after the async message has reached the parent
 | |
|     // instead.
 | |
|     aEvent.stopPropagation();
 | |
| 
 | |
|     data.spellInfo = null;
 | |
|     if (!spellInfo) {
 | |
|       this.sendAsyncMessage("contextmenu", data);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       data.spellInfo = await spellInfo;
 | |
|     } catch (ex) {}
 | |
|     this.sendAsyncMessage("contextmenu", 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.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.timeStamp = aEvent.timeStamp;
 | |
|     context.screenXDevPx = aEvent.screenX * this.contentWindow.devicePixelRatio;
 | |
|     context.screenYDevPx = aEvent.screenY * this.contentWindow.devicePixelRatio;
 | |
|     context.inputSource = aEvent.inputSource;
 | |
| 
 | |
|     let node = aEvent.composedTarget;
 | |
| 
 | |
|     // Set the node to containing <video>/<audio>/<embed>/<object> if the node
 | |
|     // is in the videocontrols UA Widget.
 | |
|     if (node.containingShadowRoot?.isUAWidget()) {
 | |
|       const host = node.containingShadowRoot.host;
 | |
|       if (
 | |
|         this.contentWindow.HTMLMediaElement.isInstance(host) ||
 | |
|         this.contentWindow.HTMLEmbedElement.isInstance(host) ||
 | |
|         this.contentWindow.HTMLObjectElement.isInstance(host)
 | |
|       ) {
 | |
|         node = host;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const XUL_NS =
 | |
|       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 | |
| 
 | |
|     context.shouldDisplay = true;
 | |
| 
 | |
|     if (
 | |
|       node.nodeType == node.DOCUMENT_NODE ||
 | |
|       // Don't display for XUL element unless <label class="text-link">
 | |
|       (node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))
 | |
|     ) {
 | |
|       context.shouldDisplay = false;
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const isAboutDevtoolsToolbox = this.document.documentURI.startsWith(
 | |
|       "about:devtools-toolbox"
 | |
|     );
 | |
|     const editFlags = lazy.SpellCheckHelper.isEditable(
 | |
|       node,
 | |
|       this.contentWindow
 | |
|     );
 | |
| 
 | |
|     if (
 | |
|       isAboutDevtoolsToolbox &&
 | |
|       (editFlags & lazy.SpellCheckHelper.TEXTINPUT) === 0
 | |
|     ) {
 | |
|       // Don't display for about:devtools-toolbox page unless the source was text input.
 | |
|       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.inPDFViewer = false;
 | |
|     context.inSrcdocFrame = false;
 | |
|     context.inSyntheticDoc = false;
 | |
|     context.inTabBrowser = true;
 | |
|     context.inWebExtBrowser = false;
 | |
| 
 | |
|     context.link = null;
 | |
|     context.linkDownload = "";
 | |
|     context.linkProtocol = "";
 | |
|     context.linkTextStr = "";
 | |
|     context.linkURL = "";
 | |
|     context.linkURI = null;
 | |
| 
 | |
|     context.onAudio = false;
 | |
|     context.onCanvas = false;
 | |
|     context.onCompletedImage = false;
 | |
|     context.onDRMMedia = false;
 | |
|     context.onPiPVideo = false;
 | |
|     context.onEditable = false;
 | |
|     context.onImage = false;
 | |
|     context.onKeywordField = false;
 | |
|     context.onLink = false;
 | |
|     context.onLoadedImage = false;
 | |
|     context.onMailtoLink = false;
 | |
|     context.onTelLink = false;
 | |
|     context.onMozExtLink = false;
 | |
|     context.onNumeric = false;
 | |
|     context.onPassword = false;
 | |
|     context.passwordRevealed = false;
 | |
|     context.onSaveableLink = false;
 | |
|     context.onSpellcheckable = false;
 | |
|     context.onTextInput = false;
 | |
|     context.onVideo = false;
 | |
|     context.inPDFEditor = false;
 | |
| 
 | |
|     // Remember the node and its owner document that was clicked
 | |
|     // This may be modifed before sending to nsContextMenu
 | |
|     context.target = node;
 | |
|     context.targetIdentifier = lazy.ContentDOMReference.get(node);
 | |
| 
 | |
|     context.csp = lazy.E10SUtils.serializeCSP(context.target.ownerDocument.csp);
 | |
| 
 | |
|     // Check if we are in the PDF Viewer.
 | |
|     context.inPDFViewer =
 | |
|       context.target.ownerDocument.nodePrincipal.originNoSuffix ==
 | |
|       "resource://pdf.js";
 | |
|     if (context.inPDFViewer) {
 | |
|       context.pdfEditorStates = context.target.ownerDocument.editorStates;
 | |
|       context.inPDFEditor = !!context.pdfEditorStates?.isEditing;
 | |
|     }
 | |
| 
 | |
|     // 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;
 | |
| 
 | |
|     this._setContextForNodesNoChildren(editFlags);
 | |
|     this._setContextForNodesWithChildren(editFlags);
 | |
| 
 | |
|     this.lastMenuTarget = {
 | |
|       // Remember the node for extensions.
 | |
|       targetRef: Cu.getWeakReference(node),
 | |
|       // The timestamp is used to verify that the target wasn't changed since the observed menu event.
 | |
|       timeStamp: context.timeStamp,
 | |
|     };
 | |
| 
 | |
|     if (isAboutDevtoolsToolbox) {
 | |
|       // Setup the menu items on text input in about:devtools-toolbox.
 | |
|       context.inAboutDevtoolsToolbox = true;
 | |
|       context.canSpellCheck = false;
 | |
|       context.inTabBrowser = false;
 | |
|       context.inFrame = false;
 | |
|       context.inSrcdocFrame = false;
 | |
|       context.onSpellcheckable = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 == context.target.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 != context.target.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.target.currentURI)
 | |
|     ) {
 | |
|       context.onImage = true;
 | |
| 
 | |
|       context.imageInfo = {
 | |
|         currentSrc: context.target.currentSrc,
 | |
|         width: context.target.width,
 | |
|         height: context.target.height,
 | |
|         imageText: this.contentWindow.ImageDocument.isInstance(
 | |
|           context.target.ownerDocument
 | |
|         )
 | |
|           ? undefined
 | |
|           : context.target.title || context.target.alt,
 | |
|       };
 | |
|       const { SVGAnimatedLength } = context.target.ownerGlobal;
 | |
|       if (SVGAnimatedLength.isInstance(context.imageInfo.height)) {
 | |
|         context.imageInfo.height = context.imageInfo.height.animVal.value;
 | |
|       }
 | |
|       if (SVGAnimatedLength.isInstance(context.imageInfo.width)) {
 | |
|         context.imageInfo.width = context.imageInfo.width.animVal.value;
 | |
|       }
 | |
| 
 | |
|       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;
 | |
|       }
 | |
| 
 | |
|       // The URL of the image before redirects is the currentURI.  This is
 | |
|       // intended to be used for "Copy Image Link".
 | |
|       context.originalMediaURL = (() => {
 | |
|         let currentURI = context.target.currentURI?.spec;
 | |
|         if (currentURI && this._isMediaURLReusable(currentURI)) {
 | |
|           return currentURI;
 | |
|         }
 | |
|         return "";
 | |
|       })();
 | |
| 
 | |
|       // The actual URL the image was loaded from (after redirects) is the
 | |
|       // currentRequestFinalURI.  We should use that as the URL for purposes of
 | |
|       // deciding on the filename, if it is present. It might not be present
 | |
|       // if images are blocked.
 | |
|       //
 | |
|       // It is important to check both the final and the current URI, as they
 | |
|       // could be different blob URIs, see bug 1625786.
 | |
|       context.mediaURL = (() => {
 | |
|         let finalURI = context.target.currentRequestFinalURI?.spec;
 | |
|         if (finalURI && this._isMediaURLReusable(finalURI)) {
 | |
|           return finalURI;
 | |
|         }
 | |
|         let currentURI = context.target.currentURI?.spec;
 | |
|         if (currentURI && this._isMediaURLReusable(currentURI)) {
 | |
|           return currentURI;
 | |
|         }
 | |
|         return "";
 | |
|       })();
 | |
| 
 | |
|       const descURL = context.target.getAttribute("longdesc");
 | |
| 
 | |
|       if (descURL) {
 | |
|         context.imageDescURL = this._makeURLAbsolute(
 | |
|           context.target.ownerDocument.body.baseURI,
 | |
|           descURL
 | |
|         );
 | |
|       }
 | |
|     } else if (
 | |
|       this.contentWindow.HTMLCanvasElement.isInstance(context.target)
 | |
|     ) {
 | |
|       context.onCanvas = true;
 | |
|     } else if (this.contentWindow.HTMLVideoElement.isInstance(context.target)) {
 | |
|       const mediaURL = context.target.currentSrc || context.target.src;
 | |
| 
 | |
|       if (this._isMediaURLReusable(mediaURL)) {
 | |
|         context.mediaURL = mediaURL;
 | |
|       }
 | |
| 
 | |
|       if (this._isProprietaryDRM()) {
 | |
|         context.onDRMMedia = true;
 | |
|       }
 | |
| 
 | |
|       if (context.target.isCloningElementVisually) {
 | |
|         context.onPiPVideo = 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 (this.contentWindow.HTMLAudioElement.isInstance(context.target)) {
 | |
|       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 &
 | |
|       (lazy.SpellCheckHelper.INPUT | lazy.SpellCheckHelper.TEXTAREA)
 | |
|     ) {
 | |
|       context.onTextInput = (editFlags & lazy.SpellCheckHelper.TEXTINPUT) !== 0;
 | |
|       context.onNumeric = (editFlags & lazy.SpellCheckHelper.NUMERIC) !== 0;
 | |
|       context.onEditable = (editFlags & lazy.SpellCheckHelper.EDITABLE) !== 0;
 | |
|       context.onPassword = (editFlags & lazy.SpellCheckHelper.PASSWORD) !== 0;
 | |
| 
 | |
|       context.showRelay =
 | |
|         HTMLInputElement.isInstance(context.target) &&
 | |
|         !context.target.disabled &&
 | |
|         !context.target.readOnly &&
 | |
|         (lazy.LoginHelper.isInferredEmailField(context.target) ||
 | |
|           lazy.LoginHelper.isInferredUsernameField(context.target));
 | |
|       context.isDesignMode =
 | |
|         (editFlags & lazy.SpellCheckHelper.CONTENTEDITABLE) !== 0;
 | |
|       context.passwordRevealed =
 | |
|         context.onPassword && context.target.revealPassword;
 | |
|       context.onSpellcheckable =
 | |
|         (editFlags & lazy.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 & lazy.SpellCheckHelper.KEYWORD;
 | |
|     } else if (this.contentWindow.HTMLHtmlElement.isInstance(context.target)) {
 | |
|       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
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     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 == elem.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) ||
 | |
|             (this.contentWindow.HTMLAnchorElement.isInstance(elem) &&
 | |
|               elem.href) ||
 | |
|             (this.contentWindow.SVGAElement.isInstance(elem) &&
 | |
|               (elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
 | |
|             (this.contentWindow.HTMLAreaElement.isInstance(elem) &&
 | |
|               elem.href) ||
 | |
|             this.contentWindow.HTMLLinkElement.isInstance(elem) ||
 | |
|             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.onTelLink = context.linkProtocol == "tel";
 | |
|           context.onMozExtLink = context.linkProtocol == "moz-extension";
 | |
|           context.onSaveableLink = this._isLinkSaveable(context.link);
 | |
| 
 | |
|           context.isSponsoredLink =
 | |
|             (elem.ownerDocument.URL === "about:newtab" ||
 | |
|               elem.ownerDocument.URL === "about:home") &&
 | |
|             elem.dataset.isSponsoredLink === "true";
 | |
| 
 | |
|           try {
 | |
|             if (elem.download) {
 | |
|               // Ignore download attribute on cross-origin links
 | |
|               context.target.ownerDocument.nodePrincipal.checkMayLoad(
 | |
|                 context.linkURI,
 | |
|                 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.flattenedTreeParentNode;
 | |
|     }
 | |
| 
 | |
|     // 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 & lazy.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.inFrame = false;
 | |
|         context.inSrcdocFrame = false;
 | |
|         context.hasBGImage = false;
 | |
|         context.isDesignMode = true;
 | |
|         context.onEditable = true;
 | |
|         context.onSpellcheckable = true;
 | |
|         context.shouldInitInlineSpellCheckerUIWithChildren = true;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _destructionObservers = new Set();
 | |
|   registerDestructionObserver(obj) {
 | |
|     this._destructionObservers.add(obj);
 | |
|   }
 | |
| 
 | |
|   unregisterDestructionObserver(obj) {
 | |
|     this._destructionObservers.delete(obj);
 | |
|   }
 | |
| 
 | |
|   didDestroy() {
 | |
|     for (let obs of this._destructionObservers) {
 | |
|       obs.actorDestroyed(this);
 | |
|     }
 | |
|     this._destructionObservers = null;
 | |
|   }
 | |
| }
 | 
