tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ContextMenuChild.sys.mjs (40036B)


      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/. */
      6 
      7 const lazy = {};
      8 
      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 });
     19 
     20 let contextMenus = new WeakMap();
     21 
     22 export class ContextMenuChild extends JSWindowActorChild {
     23  // PUBLIC
     24  constructor() {
     25    super();
     26 
     27    this.target = null;
     28    this.context = null;
     29    this.lastMenuTarget = null;
     30  }
     31 
     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  }
     43 
     44  static getLastTarget(browsingContext) {
     45    let contextMenu = contextMenus.get(browsingContext);
     46    return contextMenu && contextMenu.lastMenuTarget;
     47  }
     48 
     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      }
     57 
     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      }
     69 
     70      case "ContextMenu:Hiding": {
     71        this.context = null;
     72        this.target = null;
     73        break;
     74      }
     75 
     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            );
     84 
     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                let event = new this.contentWindow.CustomEvent(
    117                  "MozTogglePictureInPicture",
    118                  {
    119                    bubbles: true,
    120                    detail: { reason: "ContextMenu" },
    121                  },
    122                  this.contentWindow
    123                );
    124                this.contentWindow.windowUtils.dispatchEventToChromeOnly(
    125                  media,
    126                  event
    127                );
    128                break;
    129              }
    130            }
    131          }
    132        );
    133        break;
    134      }
    135 
    136      case "ContextMenu:ReloadFrame": {
    137        let target = lazy.ContentDOMReference.resolve(
    138          message.data.targetIdentifier
    139        );
    140        target.ownerDocument.location.reload(message.data.forceReload);
    141        break;
    142      }
    143 
    144      case "ContextMenu:GetImageText": {
    145        let img = lazy.ContentDOMReference.resolve(
    146          message.data.targetIdentifier
    147        );
    148        const { direction } = this.contentWindow.getComputedStyle(img);
    149 
    150        return img.recognizeCurrentImageText().then(results => {
    151          return { results, direction };
    152        });
    153      }
    154 
    155      case "ContextMenu:ToggleRevealPassword": {
    156        let target = lazy.ContentDOMReference.resolve(
    157          message.data.targetIdentifier
    158        );
    159        target.revealPassword = !target.revealPassword;
    160        break;
    161      }
    162 
    163      case "ContextMenu:UseRelayMask": {
    164        const input = lazy.ContentDOMReference.resolve(
    165          message.data.targetIdentifier
    166        );
    167        input.setUserInput(message.data.emailMask);
    168        break;
    169      }
    170 
    171      case "ContextMenu:ReloadImage": {
    172        let image = lazy.ContentDOMReference.resolve(
    173          message.data.targetIdentifier
    174        );
    175 
    176        if (image instanceof Ci.nsIImageLoadingContent) {
    177          image.forceReload();
    178        }
    179        break;
    180      }
    181 
    182      case "ContextMenu:SearchFieldEngineData": {
    183        let node = lazy.ContentDOMReference.resolve(
    184          message.data.targetIdentifier
    185        );
    186        let charset = node.ownerDocument.characterSet;
    187        let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
    188        let method = node.form.method.toUpperCase();
    189 
    190        let formData = new FormData(node.form);
    191        formData.set(node.name, "{searchTerms}");
    192 
    193        let url = Services.io.newURI(
    194          node.form.getAttribute("action"),
    195          charset,
    196          formBaseURI
    197        ).spec;
    198 
    199        if (
    200          !node.name ||
    201          (method != "POST" && method != "GET") ||
    202          node.form.enctype != "application/x-www-form-urlencoded" ||
    203          formData.values().some(v => typeof v != "string")
    204        ) {
    205          // This should never happen since these conditions are checked in
    206          // `isTargetASearchEngineField`.
    207          return Promise.reject("Cannot create search engine from this form.");
    208        }
    209 
    210        return Promise.resolve({ url, formData, charset, method });
    211      }
    212 
    213      case "ContextMenu:SaveVideoFrameAsImage": {
    214        let video = lazy.ContentDOMReference.resolve(
    215          message.data.targetIdentifier
    216        );
    217        let canvas = this.document.createElementNS(
    218          "http://www.w3.org/1999/xhtml",
    219          "canvas"
    220        );
    221        canvas.width = video.videoWidth;
    222        canvas.height = video.videoHeight;
    223 
    224        let ctxDraw = canvas.getContext("2d");
    225        ctxDraw.drawImage(video, 0, 0);
    226 
    227        // Note: if changing the content type, don't forget to update
    228        // consumers that also hardcode this content type.
    229        return Promise.resolve(canvas.toDataURL("image/jpeg", ""));
    230      }
    231 
    232      case "ContextMenu:SetAsDesktopBackground": {
    233        let target = lazy.ContentDOMReference.resolve(
    234          message.data.targetIdentifier
    235        );
    236 
    237        // Paranoia: check disableSetDesktopBackground again, in case the
    238        // image changed since the context menu was initiated.
    239        let disable = this._disableSetDesktopBackground(target);
    240 
    241        if (!disable) {
    242          try {
    243            Services.scriptSecurityManager.checkLoadURIWithPrincipal(
    244              target.ownerDocument.nodePrincipal,
    245              target.currentURI
    246            );
    247            let canvas = this.document.createElement("canvas");
    248            canvas.width = target.naturalWidth;
    249            canvas.height = target.naturalHeight;
    250            let ctx = canvas.getContext("2d");
    251            ctx.drawImage(target, 0, 0);
    252            let dataURL = canvas.toDataURL();
    253            let url = target.ownerDocument.location;
    254            let imageName = url.pathname.substr(
    255              url.pathname.lastIndexOf("/") + 1
    256            );
    257            return Promise.resolve({ failed: false, dataURL, imageName });
    258          } catch (e) {
    259            console.error(e);
    260          }
    261        }
    262 
    263        return Promise.resolve({
    264          failed: true,
    265          dataURL: null,
    266          imageName: null,
    267        });
    268      }
    269 
    270      case "ContextMenu:GetTextDirective": {
    271        const sel = this.contentWindow.getSelection();
    272        const ranges = !sel.isCollapsed
    273          ? Array.from({ length: sel.rangeCount }, (_, i) => sel.getRangeAt(i))
    274          : this.document.fragmentDirective.getTextDirectiveRanges();
    275        return ranges
    276          ? this.document.fragmentDirective
    277              .createTextDirectiveForRanges(ranges)
    278              .then(textFragment => {
    279                if (textFragment) {
    280                  let url = URL.fromURI(this.document?.documentURIObject);
    281                  url.hash += `:~:${textFragment}`;
    282                  return url.href;
    283                }
    284                return null;
    285              })
    286          : null;
    287      }
    288      case "ContextMenu:RemoveAllTextFragments": {
    289        this.document.fragmentDirective.removeAllTextDirectives();
    290        this.contentWindow.history.replaceState(
    291          this.contentWindow.history.state,
    292          "",
    293          this.contentWindow.location.href
    294        );
    295      }
    296    }
    297 
    298    return undefined;
    299  }
    300 
    301  /**
    302   * Returns the event target of the context menu, using a locally stored
    303   * reference if possible. If not, and aMessage.objects is defined,
    304   * aMessage.objects[aKey] is returned. Otherwise null.
    305   *
    306   * @param  {object} aMessage Message with a objects property
    307   * @param  {string} aKey     Key for the target on aMessage.objects
    308   * @return {object}          Context menu target
    309   */
    310  getTarget(aMessage, aKey = "target") {
    311    return this.target || (aMessage.objects && aMessage.objects[aKey]);
    312  }
    313 
    314  // PRIVATE
    315  _isXULTextLinkLabel(aNode) {
    316    const XUL_NS =
    317      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    318    return (
    319      aNode.namespaceURI == XUL_NS &&
    320      aNode.tagName == "label" &&
    321      aNode.classList.contains("text-link") &&
    322      aNode.href
    323    );
    324  }
    325 
    326  // Generate fully qualified URL for clicked-on link.
    327  _getLinkURL() {
    328    let href = this.context.link.href;
    329 
    330    if (href) {
    331      // Handle SVG links:
    332      if (typeof href == "object" && href.animVal) {
    333        return new URL(href.animVal, this.context.link.baseURI).href;
    334      }
    335 
    336      return href;
    337    }
    338 
    339    href =
    340      this.context.link.getAttribute("href") ||
    341      this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
    342 
    343    if (!href || !href.match(/\S/)) {
    344      // Without this we try to save as the current doc,
    345      // for example, HTML case also throws if empty
    346      throw new Error("Empty href");
    347    }
    348 
    349    return new URL(href, this.context.link.baseURI).href;
    350  }
    351 
    352  _getLinkURI() {
    353    try {
    354      return Services.io.newURI(this.context.linkURL);
    355    } catch (ex) {
    356      // e.g. empty URL string
    357    }
    358 
    359    return null;
    360  }
    361 
    362  // Get text of link.
    363  _getLinkText() {
    364    let text = this._gatherTextUnder(this.context.link);
    365 
    366    if (!text || !text.match(/\S/)) {
    367      text = this.context.link.getAttribute("title");
    368      if (!text || !text.match(/\S/)) {
    369        text = this.context.link.getAttribute("alt");
    370        if (!text || !text.match(/\S/)) {
    371          text = this.context.linkURL;
    372        }
    373      }
    374    }
    375 
    376    return text;
    377  }
    378 
    379  _getLinkProtocol() {
    380    if (this.context.linkURI) {
    381      return this.context.linkURI.scheme; // can be |undefined|
    382    }
    383 
    384    return null;
    385  }
    386 
    387  // Returns true if clicked-on link targets a resource that can be saved.
    388  _isLinkSaveable() {
    389    // We don't do the Right Thing for news/snews yet, so turn them off
    390    // until we do.
    391    return (
    392      this.context.linkProtocol &&
    393      !(
    394        this.context.linkProtocol == "mailto" ||
    395        this.context.linkProtocol == "tel" ||
    396        this.context.linkProtocol == "javascript" ||
    397        this.context.linkProtocol == "news" ||
    398        this.context.linkProtocol == "snews"
    399      )
    400    );
    401  }
    402 
    403  // Gather all descendent text under given document node.
    404  _gatherTextUnder(root) {
    405    let text = "";
    406    let node = root.firstChild;
    407    let depth = 1;
    408    while (node && depth > 0) {
    409      // See if this node is text.
    410      if (node.nodeType == node.TEXT_NODE) {
    411        // Add this text to our collection.
    412        text += " " + node.data;
    413      } else if (this.contentWindow.HTMLImageElement.isInstance(node)) {
    414        // If it has an "alt" attribute, add that.
    415        let altText = node.getAttribute("alt");
    416        if (altText && altText != "") {
    417          text += " " + altText;
    418        }
    419      }
    420      // Find next node to test.
    421      // First, see if this node has children.
    422      if (node.hasChildNodes()) {
    423        // Go to first child.
    424        node = node.firstChild;
    425        depth++;
    426      } else {
    427        // No children, try next sibling (or parent next sibling).
    428        while (depth > 0 && !node.nextSibling) {
    429          node = node.parentNode;
    430          depth--;
    431        }
    432        if (node.nextSibling) {
    433          node = node.nextSibling;
    434        }
    435      }
    436    }
    437 
    438    // Strip leading and tailing whitespace.
    439    text = text.trim();
    440    // Compress remaining whitespace.
    441    text = text.replace(/\s+/g, " ");
    442    return text;
    443  }
    444 
    445  // Returns a "url"-type computed style attribute value, with the url() stripped.
    446  _getComputedURL(aElem, aProp) {
    447    let urls = aElem.ownerGlobal.getComputedStyle(aElem).getCSSImageURLs(aProp);
    448 
    449    if (!urls.length) {
    450      return null;
    451    }
    452 
    453    if (urls.length != 1) {
    454      throw new Error("found multiple URLs");
    455    }
    456 
    457    return urls[0];
    458  }
    459 
    460  _isProprietaryDRM() {
    461    return (
    462      this.context.target.isEncrypted &&
    463      this.context.target.mediaKeys &&
    464      this.context.target.mediaKeys.keySystem != "org.w3.clearkey"
    465    );
    466  }
    467 
    468  _isMediaURLReusable(aURL) {
    469    if (aURL.startsWith("blob:")) {
    470      return URL.isBoundToBlob(aURL);
    471    }
    472 
    473    return true;
    474  }
    475 
    476  _isTargetATextBox(node) {
    477    if (this.contentWindow.HTMLInputElement.isInstance(node)) {
    478      return node.mozIsTextField(false);
    479    }
    480 
    481    return this.contentWindow.HTMLTextAreaElement.isInstance(node);
    482  }
    483 
    484  _isSpellCheckEnabled(aNode) {
    485    // We can always force-enable spellchecking on textboxes
    486    if (this._isTargetATextBox(aNode)) {
    487      return true;
    488    }
    489 
    490    // We can never spell check something which is not content editable
    491    let editable = aNode.isContentEditable;
    492 
    493    if (!editable && aNode.ownerDocument) {
    494      editable = aNode.ownerDocument.designMode == "on";
    495    }
    496 
    497    if (!editable) {
    498      return false;
    499    }
    500 
    501    // Otherwise make sure that nothing in the parent chain disables spellchecking
    502    return aNode.spellcheck;
    503  }
    504 
    505  _disableSetDesktopBackground(aTarget) {
    506    // Disable the Set as Desktop Background menu item if we're still trying
    507    // to load the image or the load failed.
    508    if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
    509      return true;
    510    }
    511 
    512    if ("complete" in aTarget && !aTarget.complete) {
    513      return true;
    514    }
    515 
    516    if (aTarget.currentURI.schemeIs("javascript")) {
    517      return true;
    518    }
    519 
    520    let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
    521 
    522    if (!request) {
    523      return true;
    524    }
    525 
    526    return false;
    527  }
    528 
    529  async handleEvent(aEvent) {
    530    contextMenus.set(this.browsingContext, this);
    531 
    532    let defaultPrevented = aEvent.defaultPrevented;
    533 
    534    if (
    535      // If the event is not from a chrome-privileged document, and if
    536      // `dom.event.contextmenu.enabled` is false, force defaultPrevented=false.
    537      !aEvent.composedTarget.nodePrincipal.isSystemPrincipal &&
    538      !Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
    539    ) {
    540      defaultPrevented = false;
    541    }
    542 
    543    if (defaultPrevented) {
    544      return;
    545    }
    546 
    547    let doc = aEvent.composedTarget.ownerDocument;
    548    if (!doc && Cu.isInAutomation) {
    549      // doc has been observed to be null for many years, causing intermittent
    550      // test failures all over the place (bug 1478596). The rate of failures
    551      // is too low to debug locally, but frequent enough to be a nuisance.
    552      // TODO bug 1478596: use these diagnostic logs to resolve the bug.
    553      dump(
    554        `doc is unexpectedly null (bug 1478596), composedTarget=${aEvent.composedTarget}\n`
    555      );
    556      // A potential fix is to fall back to aEvent.target.ownerDocument, per
    557      // https://bugzilla.mozilla.org/show_bug.cgi?id=1478596#c1
    558      // Let's print potentially viable alternatives to see what we should use.
    559      for (let k of ["target", "originalTarget", "explicitOriginalTarget"]) {
    560        dump(
    561          ` Alternative: ${k}=${aEvent[k]} and its doc=${aEvent[k]?.ownerDocument}\n`
    562        );
    563      }
    564    }
    565    let {
    566      mozDocumentURIIfNotForErrorPages: docLocation,
    567      characterSet: charSet,
    568      baseURI,
    569    } = doc;
    570    docLocation = docLocation && docLocation.spec;
    571    const loginManagerChild = lazy.LoginManagerChild.forWindow(doc.defaultView);
    572    const docState = loginManagerChild.stateForDocument(doc);
    573    const loginFillInfo = docState.getFieldContext(aEvent.composedTarget);
    574 
    575    let disableSetDesktopBackground = null;
    576 
    577    // Media related cache info parent needs for saving
    578    let contentType = null;
    579    let contentDisposition = null;
    580    if (
    581      aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE &&
    582      aEvent.composedTarget instanceof Ci.nsIImageLoadingContent &&
    583      aEvent.composedTarget.currentURI
    584    ) {
    585      disableSetDesktopBackground = this._disableSetDesktopBackground(
    586        aEvent.composedTarget
    587      );
    588 
    589      try {
    590        let imageCache = Cc["@mozilla.org/image/tools;1"]
    591          .getService(Ci.imgITools)
    592          .getImgCacheForDocument(doc);
    593        // The image cache's notion of where this image is located is
    594        // the currentURI of the image loading content.
    595        let props = imageCache.findEntryProperties(
    596          aEvent.composedTarget.currentURI,
    597          doc
    598        );
    599 
    600        try {
    601          contentType = props.get("type", Ci.nsISupportsCString).data;
    602        } catch (e) {}
    603 
    604        try {
    605          contentDisposition = props.get(
    606            "content-disposition",
    607            Ci.nsISupportsCString
    608          ).data;
    609        } catch (e) {}
    610      } catch (e) {}
    611    }
    612 
    613    let selectionInfo = lazy.SelectionUtils.getSelectionDetails(
    614      this.contentWindow
    615    );
    616 
    617    this._setContext(aEvent);
    618    let context = this.context;
    619    this.target = context.target;
    620 
    621    let spellInfo = null;
    622    let editFlags = null;
    623 
    624    let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
    625      Ci.nsIReferrerInfo
    626    );
    627    referrerInfo.initWithElement(aEvent.composedTarget);
    628    referrerInfo = lazy.E10SUtils.serializeReferrerInfo(referrerInfo);
    629 
    630    // In the case "onLink" we may have to send link referrerInfo to use in
    631    // _openLinkInParameters
    632    let linkReferrerInfo = null;
    633    if (context.onLink) {
    634      linkReferrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
    635        Ci.nsIReferrerInfo
    636      );
    637      linkReferrerInfo.initWithElement(context.link);
    638    }
    639 
    640    let target = context.target;
    641    if (target) {
    642      this._cleanContext();
    643    }
    644 
    645    editFlags = lazy.SpellCheckHelper.isEditable(
    646      aEvent.composedTarget,
    647      this.contentWindow
    648    );
    649 
    650    if (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) {
    651      spellInfo = lazy.InlineSpellCheckerContent.initContextMenu(
    652        aEvent,
    653        editFlags,
    654        this
    655      );
    656    }
    657 
    658    // Set the event target first as the copy image command needs it to
    659    // determine what was context-clicked on. Then, update the state of the
    660    // commands on the context menu.
    661    this.docShell.docViewer
    662      .QueryInterface(Ci.nsIDocumentViewerEdit)
    663      .setCommandNode(aEvent.composedTarget);
    664    aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu");
    665 
    666    let data = {
    667      context,
    668      charSet,
    669      baseURI,
    670      referrerInfo,
    671      editFlags,
    672      contentType,
    673      docLocation,
    674      loginFillInfo,
    675      selectionInfo,
    676      contentDisposition,
    677      disableSetDesktopBackground,
    678    };
    679 
    680    if (context.inFrame && !context.inSrcdocFrame) {
    681      data.frameReferrerInfo = lazy.E10SUtils.serializeReferrerInfo(
    682        doc.referrerInfo
    683      );
    684    }
    685 
    686    if (linkReferrerInfo) {
    687      data.linkReferrerInfo =
    688        lazy.E10SUtils.serializeReferrerInfo(linkReferrerInfo);
    689    }
    690 
    691    // Notify observers (currently only webextensions) of the context menu being
    692    // prepared, allowing them to set webExtContextData for us.
    693    let prepareContextMenu = {
    694      principal: doc.nodePrincipal,
    695      setWebExtContextData(webExtContextData) {
    696        data.webExtContextData = webExtContextData;
    697      },
    698    };
    699    Services.obs.notifyObservers(prepareContextMenu, "on-prepare-contextmenu");
    700 
    701    // In the event that the content is running in the parent process, we don't
    702    // actually want the contextmenu events to reach the parent - we'll dispatch
    703    // a new contextmenu event after the async message has reached the parent
    704    // instead.
    705    aEvent.stopPropagation();
    706 
    707    data.spellInfo = null;
    708    if (!spellInfo) {
    709      this.sendAsyncMessage("contextmenu", data);
    710      return;
    711    }
    712 
    713    try {
    714      data.spellInfo = await spellInfo;
    715    } catch (ex) {}
    716    this.sendAsyncMessage("contextmenu", data);
    717  }
    718 
    719  /**
    720   * Some things are not serializable, so we either have to only send
    721   * their needed data or regenerate them in nsContextMenu.js
    722   * - target and target.ownerDocument
    723   * - link
    724   * - linkURI
    725   */
    726  _cleanContext() {
    727    const context = this.context;
    728    const cleanTarget = Object.create(null);
    729 
    730    cleanTarget.ownerDocument = {
    731      // used for nsContextMenu.initLeaveDOMFullScreenItems and
    732      // nsContextMenu.initMediaPlayerItems
    733      fullscreen: context.target.ownerDocument.fullscreen,
    734 
    735      // used for nsContextMenu.initMiscItems
    736      contentType: context.target.ownerDocument.contentType,
    737    };
    738 
    739    // used for nsContextMenu.initMediaPlayerItems
    740    Object.assign(cleanTarget, {
    741      ended: context.target.ended,
    742      muted: context.target.muted,
    743      paused: context.target.paused,
    744      controls: context.target.controls,
    745      duration: context.target.duration,
    746    });
    747 
    748    const onMedia = context.onVideo || context.onAudio;
    749 
    750    if (onMedia) {
    751      Object.assign(cleanTarget, {
    752        loop: context.target.loop,
    753        error: context.target.error,
    754        networkState: context.target.networkState,
    755        playbackRate: context.target.playbackRate,
    756        NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
    757      });
    758 
    759      if (context.onVideo) {
    760        Object.assign(cleanTarget, {
    761          readyState: context.target.readyState,
    762          HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
    763        });
    764      }
    765    }
    766 
    767    context.target = cleanTarget;
    768 
    769    if (context.link) {
    770      context.link = { href: context.linkURL };
    771    }
    772 
    773    delete context.linkURI;
    774  }
    775 
    776  _setContext(aEvent) {
    777    this.context = Object.create(null);
    778    const context = this.context;
    779 
    780    context.timeStamp = aEvent.timeStamp;
    781    context.screenXDevPx = aEvent.screenX * this.contentWindow.devicePixelRatio;
    782    context.screenYDevPx = aEvent.screenY * this.contentWindow.devicePixelRatio;
    783    context.inputSource = aEvent.inputSource;
    784 
    785    let node = aEvent.composedTarget;
    786 
    787    // Set the node to containing <video>/<audio>/<embed>/<object> if the node
    788    // is in the videocontrols UA Widget.
    789    if (node.containingShadowRoot?.isUAWidget()) {
    790      const host = node.containingShadowRoot.host;
    791      if (
    792        this.contentWindow.HTMLMediaElement.isInstance(host) ||
    793        this.contentWindow.HTMLEmbedElement.isInstance(host) ||
    794        this.contentWindow.HTMLObjectElement.isInstance(host)
    795      ) {
    796        node = host;
    797      }
    798    }
    799 
    800    const XUL_NS =
    801      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    802 
    803    context.shouldDisplay = true;
    804 
    805    if (
    806      node.nodeType == node.DOCUMENT_NODE ||
    807      // Don't display for XUL element unless <label class="text-link">
    808      (node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))
    809    ) {
    810      context.shouldDisplay = false;
    811      return;
    812    }
    813 
    814    const isAboutDevtoolsToolbox = this.document.documentURI.startsWith(
    815      "about:devtools-toolbox"
    816    );
    817    const editFlags = lazy.SpellCheckHelper.isEditable(
    818      node,
    819      this.contentWindow
    820    );
    821 
    822    if (
    823      isAboutDevtoolsToolbox &&
    824      (editFlags & lazy.SpellCheckHelper.TEXTINPUT) === 0
    825    ) {
    826      // Don't display for about:devtools-toolbox page unless the source was text input.
    827      context.shouldDisplay = false;
    828      return;
    829    }
    830 
    831    // Initialize context to be sent to nsContextMenu
    832    // Keep this consistent with the similar code in nsContextMenu's setContext
    833    context.bgImageURL = "";
    834    context.imageDescURL = "";
    835    context.imageInfo = null;
    836    context.mediaURL = "";
    837    context.webExtBrowserType = "";
    838 
    839    context.canSpellCheck = false;
    840    context.hasBGImage = false;
    841    context.hasMultipleBGImages = false;
    842    context.isDesignMode = false;
    843    context.inFrame = false;
    844    context.inPDFViewer = false;
    845    context.inSrcdocFrame = false;
    846    context.inSyntheticDoc = false;
    847    context.inTabBrowser = true;
    848    context.inWebExtBrowser = false;
    849 
    850    context.link = null;
    851    context.linkDownload = "";
    852    context.linkProtocol = "";
    853    context.linkTextStr = "";
    854    context.linkURL = "";
    855    context.linkURI = null;
    856 
    857    context.onAudio = false;
    858    context.onCanvas = false;
    859    context.onCompletedImage = false;
    860    context.onDRMMedia = false;
    861    context.onPiPVideo = false;
    862    context.onEditable = false;
    863    context.onImage = false;
    864    context.onLink = false;
    865    context.onLoadedImage = false;
    866    context.onMailtoLink = false;
    867    context.onTelLink = false;
    868    context.onMozExtLink = false;
    869    context.onNumeric = false;
    870    context.onPassword = false;
    871    context.passwordRevealed = false;
    872    context.onSaveableLink = false;
    873    context.onSpellcheckable = false;
    874    context.onTextInput = false;
    875    context.onVideo = false;
    876    context.inPDFEditor = false;
    877 
    878    const textDirectiveRanges =
    879      this.document.fragmentDirective?.getTextDirectiveRanges?.() || [];
    880    // .hasTextFragments indicates whether the page will show highlights.
    881    context.hasTextFragments = !!textDirectiveRanges.length;
    882 
    883    // Remember the node and its owner document that was clicked
    884    // This may be modifed before sending to nsContextMenu
    885    context.target = node;
    886    context.targetIdentifier = lazy.ContentDOMReference.get(node);
    887 
    888    context.policyContainer = lazy.E10SUtils.serializePolicyContainer(
    889      context.target.ownerDocument.policyContainer
    890    );
    891 
    892    // Check if we are in the PDF Viewer.
    893    context.inPDFViewer =
    894      context.target.ownerDocument.nodePrincipal.originNoSuffix ==
    895      "resource://pdf.js";
    896    if (context.inPDFViewer) {
    897      context.pdfEditorStates = context.target.ownerDocument.editorStates;
    898      context.inPDFEditor = !!context.pdfEditorStates?.isEditing;
    899    }
    900 
    901    // Check if we are in a synthetic document (stand alone image, video, etc.).
    902    context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
    903 
    904    context.shouldInitInlineSpellCheckerUINoChildren = false;
    905    context.shouldInitInlineSpellCheckerUIWithChildren = false;
    906 
    907    this._setContextForNodesNoChildren(editFlags);
    908    this._setContextForNodesWithChildren(editFlags);
    909 
    910    this.lastMenuTarget = {
    911      // Remember the node for extensions.
    912      targetRef: Cu.getWeakReference(node),
    913      // The timestamp is used to verify that the target wasn't changed since the observed menu event.
    914      timeStamp: context.timeStamp,
    915    };
    916 
    917    if (isAboutDevtoolsToolbox) {
    918      // Setup the menu items on text input in about:devtools-toolbox.
    919      context.inAboutDevtoolsToolbox = true;
    920      context.canSpellCheck = false;
    921      context.inTabBrowser = false;
    922      context.inFrame = false;
    923      context.inSrcdocFrame = false;
    924      context.onSpellcheckable = false;
    925    }
    926  }
    927 
    928  /**
    929   * Sets up the parts of the context menu for when when nodes have no children.
    930   *
    931   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
    932   *                            for the details.
    933   */
    934  _setContextForNodesNoChildren(editFlags) {
    935    const context = this.context;
    936 
    937    if (context.target.nodeType == context.target.TEXT_NODE) {
    938      // For text nodes, look at the parent node to determine the spellcheck attribute.
    939      context.canSpellCheck =
    940        context.target.parentNode && this._isSpellCheckEnabled(context.target);
    941      return;
    942    }
    943 
    944    // We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
    945    // early if we don't have one.
    946    if (context.target.nodeType != context.target.ELEMENT_NODE) {
    947      return;
    948    }
    949 
    950    // See if the user clicked on an image. This check mirrors
    951    // nsDocumentViewer::GetInImage. Make sure to update both if this is
    952    // changed.
    953    if (
    954      context.target instanceof Ci.nsIImageLoadingContent &&
    955      (context.target.currentRequestFinalURI || context.target.currentURI)
    956    ) {
    957      context.onImage = true;
    958 
    959      context.imageInfo = {
    960        currentSrc: context.target.currentSrc,
    961        width: context.target.width,
    962        height: context.target.height,
    963        imageText: this.contentWindow.ImageDocument.isInstance(
    964          context.target.ownerDocument
    965        )
    966          ? undefined
    967          : context.target.title || context.target.alt,
    968      };
    969      if (SVGAnimatedLength.isInstance(context.imageInfo.height)) {
    970        context.imageInfo.height = context.imageInfo.height.animVal.value;
    971      }
    972      if (SVGAnimatedLength.isInstance(context.imageInfo.width)) {
    973        context.imageInfo.width = context.imageInfo.width.animVal.value;
    974      }
    975 
    976      const request = context.target.getRequest(
    977        Ci.nsIImageLoadingContent.CURRENT_REQUEST
    978      );
    979 
    980      if (request && request.imageStatus & request.STATUS_SIZE_AVAILABLE) {
    981        context.onLoadedImage = true;
    982      }
    983 
    984      if (
    985        request &&
    986        request.imageStatus & request.STATUS_LOAD_COMPLETE &&
    987        !(request.imageStatus & request.STATUS_ERROR)
    988      ) {
    989        context.onCompletedImage = true;
    990      }
    991 
    992      // The URL of the image before redirects is the currentURI.  This is
    993      // intended to be used for "Copy Image Link".
    994      context.originalMediaURL = (() => {
    995        let currentURI = context.target.currentURI?.spec;
    996        if (currentURI && this._isMediaURLReusable(currentURI)) {
    997          return currentURI;
    998        }
    999        return "";
   1000      })();
   1001 
   1002      // The actual URL the image was loaded from (after redirects) is the
   1003      // currentRequestFinalURI.  We should use that as the URL for purposes of
   1004      // deciding on the filename, if it is present. It might not be present
   1005      // if images are blocked.
   1006      //
   1007      // It is important to check both the final and the current URI, as they
   1008      // could be different blob URIs, see bug 1625786.
   1009      context.mediaURL = (() => {
   1010        let finalURI = context.target.currentRequestFinalURI?.spec;
   1011        if (finalURI && this._isMediaURLReusable(finalURI)) {
   1012          return finalURI;
   1013        }
   1014        let currentURI = context.target.currentURI?.spec;
   1015        if (currentURI && this._isMediaURLReusable(currentURI)) {
   1016          return currentURI;
   1017        }
   1018        return "";
   1019      })();
   1020 
   1021      const descURL = context.target.getAttribute("longdesc");
   1022 
   1023      if (descURL) {
   1024        context.imageDescURL = new URL(
   1025          descURL,
   1026          context.target.ownerDocument.body.baseURI
   1027        ).href;
   1028      }
   1029    } else if (
   1030      this.contentWindow.HTMLCanvasElement.isInstance(context.target)
   1031    ) {
   1032      context.onCanvas = true;
   1033    } else if (this.contentWindow.HTMLVideoElement.isInstance(context.target)) {
   1034      const mediaURL = context.target.currentSrc || context.target.src;
   1035 
   1036      if (this._isMediaURLReusable(mediaURL)) {
   1037        context.mediaURL = mediaURL;
   1038      }
   1039 
   1040      if (this._isProprietaryDRM()) {
   1041        context.onDRMMedia = true;
   1042      }
   1043 
   1044      if (context.target.isCloningElementVisually) {
   1045        context.onPiPVideo = true;
   1046      }
   1047 
   1048      // Firefox always creates a HTMLVideoElement when loading an ogg file
   1049      // directly. If the media is actually audio, be smarter and provide a
   1050      // context menu with audio operations.
   1051      if (
   1052        context.target.readyState >= context.target.HAVE_METADATA &&
   1053        (context.target.videoWidth == 0 || context.target.videoHeight == 0)
   1054      ) {
   1055        context.onAudio = true;
   1056      } else {
   1057        context.onVideo = true;
   1058      }
   1059    } else if (this.contentWindow.HTMLAudioElement.isInstance(context.target)) {
   1060      context.onAudio = true;
   1061      const mediaURL = context.target.currentSrc || context.target.src;
   1062 
   1063      if (this._isMediaURLReusable(mediaURL)) {
   1064        context.mediaURL = mediaURL;
   1065      }
   1066 
   1067      if (this._isProprietaryDRM()) {
   1068        context.onDRMMedia = true;
   1069      }
   1070    } else if (
   1071      editFlags &
   1072      (lazy.SpellCheckHelper.INPUT | lazy.SpellCheckHelper.TEXTAREA)
   1073    ) {
   1074      context.onTextInput = (editFlags & lazy.SpellCheckHelper.TEXTINPUT) !== 0;
   1075      context.onNumeric = (editFlags & lazy.SpellCheckHelper.NUMERIC) !== 0;
   1076      context.onEditable = (editFlags & lazy.SpellCheckHelper.EDITABLE) !== 0;
   1077      context.onPassword = (editFlags & lazy.SpellCheckHelper.PASSWORD) !== 0;
   1078 
   1079      context.showRelay =
   1080        HTMLInputElement.isInstance(context.target) &&
   1081        !context.target.disabled &&
   1082        !context.target.readOnly &&
   1083        (lazy.LoginHelper.isInferredEmailField(context.target) ||
   1084          lazy.LoginHelper.isInferredUsernameField(context.target));
   1085      context.isDesignMode =
   1086        (editFlags & lazy.SpellCheckHelper.CONTENTEDITABLE) !== 0;
   1087      context.passwordRevealed =
   1088        context.onPassword && context.target.revealPassword;
   1089      context.onSpellcheckable =
   1090        (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) !== 0;
   1091 
   1092      // This is guaranteed to be an input or textarea because of the condition above,
   1093      // so the no-children flag is always correct. We deal with contenteditable elsewhere.
   1094      if (context.onSpellcheckable) {
   1095        context.shouldInitInlineSpellCheckerUINoChildren = true;
   1096      }
   1097 
   1098      context.onSearchField = editFlags & lazy.SpellCheckHelper.SEARCHENGINE;
   1099    } else if (this.contentWindow.HTMLHtmlElement.isInstance(context.target)) {
   1100      const bodyElt = context.target.ownerDocument.body;
   1101 
   1102      if (bodyElt) {
   1103        let computedURL;
   1104 
   1105        try {
   1106          computedURL = this._getComputedURL(bodyElt, "background-image");
   1107          context.hasMultipleBGImages = false;
   1108        } catch (e) {
   1109          context.hasMultipleBGImages = true;
   1110        }
   1111 
   1112        if (computedURL) {
   1113          context.hasBGImage = true;
   1114          context.bgImageURL = new URL(computedURL, bodyElt.baseURI).href;
   1115        }
   1116      }
   1117    }
   1118 
   1119    context.canSpellCheck = this._isSpellCheckEnabled(context.target);
   1120  }
   1121 
   1122  /**
   1123   * Sets up the parts of the context menu for when when nodes have children.
   1124   *
   1125   * @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
   1126   *                            for the details.
   1127   */
   1128  _setContextForNodesWithChildren(editFlags) {
   1129    const context = this.context;
   1130 
   1131    // Second, bubble out, looking for items of interest that can have childen.
   1132    // Always pick the innermost link, background image, etc.
   1133    let elem = context.target;
   1134 
   1135    while (elem) {
   1136      if (elem.nodeType == elem.ELEMENT_NODE) {
   1137        // Link?
   1138        const XLINK_NS = "http://www.w3.org/1999/xlink";
   1139 
   1140        if (
   1141          !context.onLink &&
   1142          // Be consistent with what hrefAndLinkNodeForClickEvent
   1143          // does in browser.js
   1144          (this._isXULTextLinkLabel(elem) ||
   1145            (this.contentWindow.HTMLAnchorElement.isInstance(elem) &&
   1146              elem.href) ||
   1147            (this.contentWindow.SVGAElement.isInstance(elem) &&
   1148              (elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
   1149            (this.contentWindow.HTMLAreaElement.isInstance(elem) &&
   1150              elem.href) ||
   1151            this.contentWindow.HTMLLinkElement.isInstance(elem) ||
   1152            elem.getAttributeNS(XLINK_NS, "type") == "simple")
   1153        ) {
   1154          // Target is a link or a descendant of a link.
   1155          context.onLink = true;
   1156 
   1157          // Remember corresponding element.
   1158          context.link = elem;
   1159          context.linkURL = this._getLinkURL();
   1160          context.linkURI = this._getLinkURI();
   1161          context.linkTextStr = this._getLinkText();
   1162          context.linkProtocol = this._getLinkProtocol();
   1163          context.onMailtoLink = context.linkProtocol == "mailto";
   1164          context.onTelLink = context.linkProtocol == "tel";
   1165          context.onMozExtLink = context.linkProtocol == "moz-extension";
   1166          context.onSaveableLink = this._isLinkSaveable(context.link);
   1167 
   1168          context.isSponsoredLink =
   1169            (elem.ownerDocument.URL === "about:newtab" ||
   1170              elem.ownerDocument.URL === "about:home") &&
   1171            elem.dataset.isSponsoredLink === "true";
   1172 
   1173          try {
   1174            if (elem.download) {
   1175              // Ignore download attribute on cross-origin links
   1176              context.target.ownerDocument.nodePrincipal.checkMayLoad(
   1177                context.linkURI,
   1178                true
   1179              );
   1180              context.linkDownload = elem.download;
   1181            }
   1182          } catch (ex) {}
   1183        }
   1184 
   1185        // Background image?  Don't bother if we've already found a
   1186        // background image further down the hierarchy.  Otherwise,
   1187        // we look for the computed background-image style.
   1188        if (!context.hasBGImage && !context.hasMultipleBGImages) {
   1189          let bgImgUrl = null;
   1190 
   1191          try {
   1192            bgImgUrl = this._getComputedURL(elem, "background-image");
   1193            context.hasMultipleBGImages = false;
   1194          } catch (e) {
   1195            context.hasMultipleBGImages = true;
   1196          }
   1197 
   1198          if (bgImgUrl) {
   1199            context.hasBGImage = true;
   1200            context.bgImageURL = new URL(bgImgUrl, elem.baseURI).href;
   1201          }
   1202        }
   1203      }
   1204 
   1205      elem = elem.flattenedTreeParentNode;
   1206    }
   1207 
   1208    // See if the user clicked in a frame.
   1209    const docDefaultView = context.target.ownerGlobal;
   1210 
   1211    if (docDefaultView != docDefaultView.top) {
   1212      context.inFrame = true;
   1213 
   1214      if (context.target.ownerDocument.isSrcdocDocument) {
   1215        context.inSrcdocFrame = true;
   1216      }
   1217    }
   1218 
   1219    // if the document is editable, show context menu like in text inputs
   1220    if (!context.onEditable) {
   1221      if (editFlags & lazy.SpellCheckHelper.CONTENTEDITABLE) {
   1222        // If this.onEditable is false but editFlags is CONTENTEDITABLE, then
   1223        // the document itself must be editable.
   1224        context.onTextInput = true;
   1225        context.onImage = false;
   1226        context.onLoadedImage = false;
   1227        context.onCompletedImage = false;
   1228        context.inFrame = false;
   1229        context.inSrcdocFrame = false;
   1230        context.hasBGImage = false;
   1231        context.isDesignMode = true;
   1232        context.onEditable = true;
   1233        context.onSpellcheckable = true;
   1234        context.shouldInitInlineSpellCheckerUIWithChildren = true;
   1235      }
   1236    }
   1237  }
   1238 
   1239  _destructionObservers = new Set();
   1240  registerDestructionObserver(obj) {
   1241    this._destructionObservers.add(obj);
   1242  }
   1243 
   1244  unregisterDestructionObserver(obj) {
   1245    this._destructionObservers.delete(obj);
   1246  }
   1247 
   1248  didDestroy() {
   1249    for (let obs of this._destructionObservers) {
   1250      obs.actorDestroyed(this);
   1251    }
   1252    this._destructionObservers = null;
   1253  }
   1254 }