tor-browser

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

nsContextMenu.sys.mjs (93837B)


      1 /* -*- tab-width: 2; 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  BrowserSearchTelemetry:
     11    "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     12  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     13  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     14  ContextualIdentityService:
     15    "resource://gre/modules/ContextualIdentityService.sys.mjs",
     16  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
     17  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
     18  // GenAI.sys.mjs and LinkPreview.sys.mjs are missing. tor-browser#44045.
     19  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     20  LoginManagerContextMenu:
     21    "resource://gre/modules/LoginManagerContextMenu.sys.mjs",
     22  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     23  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     24  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     25  ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
     26  SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
     27  SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
     28  ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
     29  TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
     30  WebsiteFilter: "resource:///modules/policies/WebsiteFilter.sys.mjs",
     31 });
     32 
     33 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     34 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     35 
     36 ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
     37  Components.Constructor(
     38    "@mozilla.org/referrer-info;1",
     39    "nsIReferrerInfo",
     40    "init"
     41  )
     42 );
     43 
     44 XPCOMUtils.defineLazyPreferenceGetter(
     45  lazy,
     46  "TEXT_RECOGNITION_ENABLED",
     47  "dom.text-recognition.enabled",
     48  false
     49 );
     50 
     51 XPCOMUtils.defineLazyPreferenceGetter(
     52  lazy,
     53  "STRIP_ON_SHARE_ENABLED",
     54  "privacy.query_stripping.strip_on_share.enabled",
     55  false
     56 );
     57 
     58 XPCOMUtils.defineLazyPreferenceGetter(
     59  lazy,
     60  "PDFJS_ENABLE_COMMENT",
     61  "pdfjs.enableComment",
     62  false
     63 );
     64 
     65 XPCOMUtils.defineLazyPreferenceGetter(
     66  lazy,
     67  "gPrintEnabled",
     68  "print.enabled",
     69  false
     70 );
     71 
     72 XPCOMUtils.defineLazyServiceGetter(
     73  lazy,
     74  "QueryStringStripper",
     75  "@mozilla.org/url-query-string-stripper;1",
     76  Ci.nsIURLQueryStringStripper
     77 );
     78 
     79 XPCOMUtils.defineLazyServiceGetter(
     80  lazy,
     81  "clipboard",
     82  "@mozilla.org/widget/clipboardhelper;1",
     83  Ci.nsIClipboardHelper
     84 );
     85 
     86 XPCOMUtils.defineLazyPreferenceGetter(
     87  lazy,
     88  "TEXT_FRAGMENTS_ENABLED",
     89  "dom.text_fragments.enabled",
     90  false
     91 );
     92 
     93 const PASSWORD_FIELDNAME_HINTS = ["current-password", "new-password"];
     94 const USERNAME_FIELDNAME_HINT = "username";
     95 
     96 export class nsContextMenu {
     97  /**
     98   * A promise to retrieve the translations language pair
     99   * if the context menu was opened in a context relevant to
    100   * open the SelectTranslationsPanel.
    101   *
    102   * @type {Promise<{sourceLanguage: string, targetLanguage: string}>}
    103   */
    104  #translationsLangPairPromise;
    105 
    106  /**
    107   * The value of the `main-context-menu-new-feature-badge` l10n string. Fetched
    108   * lazily.
    109   *
    110   * @type {string}
    111   */
    112  #newFeatureBadgeL10nString;
    113 
    114  constructor(aXulMenu, aIsShift) {
    115    this.window = aXulMenu.ownerGlobal;
    116    this.document = aXulMenu.ownerDocument;
    117 
    118    // Get contextual info.
    119    this.setContext();
    120 
    121    if (!this.shouldDisplay) {
    122      return;
    123    }
    124 
    125    const { gBrowser } = this.window;
    126 
    127    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
    128    if (!aIsShift) {
    129      let tab =
    130        gBrowser && gBrowser.getTabForBrowser
    131          ? gBrowser.getTabForBrowser(this.browser)
    132          : undefined;
    133 
    134      let subject = {
    135        menu: aXulMenu,
    136        tab,
    137        timeStamp: this.timeStamp,
    138        isContentSelected: this.isContentSelected,
    139        inFrame: this.inFrame,
    140        isTextSelected: this.isTextSelected,
    141        onTextInput: this.onTextInput,
    142        onLink: this.onLink,
    143        onImage: this.onImage,
    144        onVideo: this.onVideo,
    145        onAudio: this.onAudio,
    146        onCanvas: this.onCanvas,
    147        onEditable: this.onEditable,
    148        onSpellcheckable: this.onSpellcheckable,
    149        onPassword: this.onPassword,
    150        passwordRevealed: this.passwordRevealed,
    151        srcUrl: this.originalMediaURL,
    152        frameUrl: this.contentData ? this.contentData.docLocation : undefined,
    153        pageUrl: this.browser ? this.browser.currentURI.spec : undefined,
    154        linkText: this.linkTextStr,
    155        linkUrl: this.linkURL,
    156        linkURI: this.linkURI,
    157        selectionText: this.isTextSelected
    158          ? this.selectionInfo.fullText
    159          : undefined,
    160        frameId: this.frameID,
    161        webExtBrowserType: this.webExtBrowserType,
    162        webExtContextData: this.contentData
    163          ? this.contentData.webExtContextData
    164          : undefined,
    165      };
    166      subject.wrappedJSObject = subject;
    167      Services.obs.notifyObservers(subject, "on-build-contextmenu");
    168    }
    169 
    170    this.viewFrameSourceElement = this.document.getElementById(
    171      "context-viewframesource"
    172    );
    173 
    174    // Reset after "on-build-contextmenu" notification in case selection was
    175    // changed during the notification.
    176    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
    177    this.onPlainTextLink = false;
    178 
    179    // Initialize (disable/remove) menu items.
    180    this.initItems(aXulMenu);
    181  }
    182 
    183  setContext() {
    184    let context = Object.create(null);
    185 
    186    if (nsContextMenu.contentData) {
    187      this.contentData = nsContextMenu.contentData;
    188      context = this.contentData.context;
    189      nsContextMenu.contentData = null;
    190    }
    191 
    192    const { gBrowser } = this.window;
    193 
    194    this.shouldDisplay = context.shouldDisplay;
    195    this.timeStamp = context.timeStamp;
    196 
    197    // Assign what's _possibly_ needed from `context` sent by ContextMenuChild.sys.mjs
    198    // Keep this consistent with the similar code in ContextMenu's _setContext
    199    this.imageDescURL = context.imageDescURL;
    200    this.imageInfo = context.imageInfo;
    201    this.mediaURL = context.mediaURL || context.bgImageURL;
    202    this.originalMediaURL = context.originalMediaURL || this.mediaURL;
    203    this.webExtBrowserType = context.webExtBrowserType;
    204 
    205    this.canSpellCheck = context.canSpellCheck;
    206    this.hasBGImage = context.hasBGImage;
    207    this.hasMultipleBGImages = context.hasMultipleBGImages;
    208    this.isDesignMode = context.isDesignMode;
    209    this.inFrame = context.inFrame;
    210    this.inPDFViewer = context.inPDFViewer;
    211    this.inPDFEditor = context.inPDFEditor;
    212    this.inSrcdocFrame = context.inSrcdocFrame;
    213    this.inSyntheticDoc = context.inSyntheticDoc;
    214    this.inTabBrowser = context.inTabBrowser;
    215    this.inWebExtBrowser = context.inWebExtBrowser;
    216 
    217    this.link = context.link;
    218    this.linkDownload = context.linkDownload;
    219    this.linkProtocol = context.linkProtocol;
    220    this.linkTextStr = context.linkTextStr;
    221    this.linkURL = context.linkURL;
    222    this.linkURI = this.getLinkURI(); // can't send; regenerate
    223 
    224    this.onAudio = context.onAudio;
    225    this.onCanvas = context.onCanvas;
    226    this.onCompletedImage = context.onCompletedImage;
    227    this.onDRMMedia = context.onDRMMedia;
    228    this.onPiPVideo = context.onPiPVideo;
    229    this.onEditable = context.onEditable;
    230    this.onImage = context.onImage;
    231    this.onSearchField = context.onSearchField;
    232    this.onLink = context.onLink;
    233    this.onLoadedImage = context.onLoadedImage;
    234    this.onMailtoLink = context.onMailtoLink;
    235    this.onTelLink = context.onTelLink;
    236    this.onMozExtLink = context.onMozExtLink;
    237    this.onNumeric = context.onNumeric;
    238    this.onPassword = context.onPassword;
    239    this.passwordRevealed = context.passwordRevealed;
    240    this.onSaveableLink = context.onSaveableLink;
    241    this.onSpellcheckable = context.onSpellcheckable;
    242    this.onTextInput = context.onTextInput;
    243    this.onVideo = context.onVideo;
    244 
    245    this.pdfEditorStates = context.pdfEditorStates;
    246 
    247    this.target = context.target;
    248    this.targetIdentifier = context.targetIdentifier;
    249 
    250    this.principal = context.principal;
    251    this.storagePrincipal = context.storagePrincipal;
    252    this.frameID = context.frameID;
    253    this.frameOuterWindowID = context.frameOuterWindowID;
    254    this.frameBrowsingContext = BrowsingContext.get(
    255      context.frameBrowsingContextID
    256    );
    257 
    258    this.inSyntheticDoc = context.inSyntheticDoc;
    259    this.inAboutDevtoolsToolbox = context.inAboutDevtoolsToolbox;
    260 
    261    this.isSponsoredLink = context.isSponsoredLink;
    262 
    263    // Everything after this isn't sent directly from ContextMenu
    264    if (this.target) {
    265      this.ownerDoc = this.target.ownerDocument;
    266    }
    267 
    268    this.policyContainer = lazy.E10SUtils.deserializePolicyContainer(
    269      context.policyContainer
    270    );
    271 
    272    if (this.contentData) {
    273      this.browser = this.contentData.browser;
    274      this.selectionInfo = this.contentData.selectionInfo;
    275      this.actor = this.contentData.actor;
    276    } else {
    277      const { SelectionUtils } = ChromeUtils.importESModule(
    278        "resource://gre/modules/SelectionUtils.sys.mjs"
    279      );
    280 
    281      this.browser = this.ownerDoc.defaultView.docShell.chromeEventHandler;
    282      this.selectionInfo = SelectionUtils.getSelectionDetails(
    283        this.browser.ownerGlobal
    284      );
    285      this.actor =
    286        this.browser.browsingContext.currentWindowGlobal.getActor(
    287          "ContextMenu"
    288        );
    289    }
    290 
    291    this.remoteType = this.actor.manager.domProcess.remoteType;
    292 
    293    this.selectedText = this.selectionInfo.text;
    294    this.isTextSelected = !!this.selectedText.length;
    295    this.webExtBrowserType = this.browser.getAttribute(
    296      "webextension-view-type"
    297    );
    298    this.inWebExtBrowser = !!this.webExtBrowserType;
    299    this.inTabBrowser =
    300      gBrowser && gBrowser.getTabForBrowser
    301        ? !!gBrowser.getTabForBrowser(this.browser)
    302        : false;
    303 
    304    let { InlineSpellCheckerUI } = this.window;
    305    if (context.shouldInitInlineSpellCheckerUINoChildren) {
    306      InlineSpellCheckerUI.initFromRemote(
    307        this.contentData.spellInfo,
    308        this.actor.manager
    309      );
    310    }
    311 
    312    if (this.contentData.spellInfo) {
    313      this.spellSuggestions = this.contentData.spellInfo.spellSuggestions;
    314    }
    315 
    316    if (context.shouldInitInlineSpellCheckerUIWithChildren) {
    317      InlineSpellCheckerUI.initFromRemote(
    318        this.contentData.spellInfo,
    319        this.actor.manager
    320      );
    321      let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
    322      this.showItem("spell-check-enabled", canSpell);
    323    }
    324 
    325    this.hasTextFragments = context.hasTextFragments;
    326    this.textFragmentURL = null;
    327  } // setContext
    328 
    329  hiding(aXulMenu) {
    330    if (this.actor) {
    331      this.actor.hiding();
    332    }
    333 
    334    aXulMenu.showHideSeparators = null;
    335 
    336    this.contentData = null;
    337    this.window.InlineSpellCheckerUI.clearSuggestionsFromMenu();
    338    this.window.InlineSpellCheckerUI.clearDictionaryListFromMenu();
    339    this.window.InlineSpellCheckerUI.uninit();
    340    if (
    341      Cu.isESModuleLoaded(
    342        "resource://gre/modules/LoginManagerContextMenu.sys.mjs"
    343      )
    344    ) {
    345      lazy.LoginManagerContextMenu.clearLoginsFromMenu(this.document);
    346    }
    347 
    348    // This handler self-deletes, only run it if it is still there:
    349    if (this._onPopupHiding) {
    350      this._onPopupHiding();
    351    }
    352  }
    353 
    354  initItems(aXulMenu) {
    355    this.initOpenItems();
    356    this.initNavigationItems();
    357    this.initViewItems();
    358    this.initImageItems();
    359    this.initMiscItems();
    360    this.initSpellingItems();
    361    this.initSaveItems();
    362    this.initSyncItems();
    363    this.initClipboardItems();
    364    this.initMediaPlayerItems();
    365    this.initLeaveDOMFullScreenItems();
    366    this.initPasswordManagerItems();
    367    this.initViewSourceItems();
    368    this.initScreenshotItem();
    369    this.initPasswordControlItems();
    370    this.initPDFItems();
    371    this.initTextFragmentItems();
    372 
    373    this.showHideSeparators(aXulMenu);
    374    if (!aXulMenu.showHideSeparators) {
    375      // Set the showHideSeparators function on the menu itself so that
    376      // the extension code (ext-menus.js) can call it after modifying
    377      // the menus.
    378      aXulMenu.showHideSeparators = () => {
    379        this.showHideSeparators(aXulMenu);
    380      };
    381    }
    382  }
    383 
    384  initPDFItems() {
    385    for (const id of [
    386      "context-pdfjs-undo",
    387      "context-pdfjs-redo",
    388      "context-sep-pdfjs-redo",
    389      "context-pdfjs-cut",
    390      "context-pdfjs-copy",
    391      "context-pdfjs-paste",
    392      "context-pdfjs-delete",
    393      "context-pdfjs-selectall",
    394      "context-sep-pdfjs-selectall",
    395    ]) {
    396      this.showItem(id, this.inPDFEditor);
    397    }
    398 
    399    const hasSelectedText = this.pdfEditorStates?.hasSelectedText ?? false;
    400    this.showItem(
    401      "context-pdfjs-comment-selection",
    402      lazy.PDFJS_ENABLE_COMMENT && hasSelectedText
    403    );
    404    this.showItem("context-pdfjs-highlight-selection", hasSelectedText);
    405 
    406    if (!this.inPDFEditor) {
    407      return;
    408    }
    409 
    410    const {
    411      isEmpty,
    412      hasSomethingToUndo,
    413      hasSomethingToRedo,
    414      hasSelectedEditor,
    415    } = this.pdfEditorStates;
    416 
    417    const hasEmptyClipboard = !Services.clipboard.hasDataMatchingFlavors(
    418      ["application/pdfjs"],
    419      Ci.nsIClipboard.kGlobalClipboard
    420    );
    421 
    422    this.setItemAttr("context-pdfjs-undo", "disabled", !hasSomethingToUndo);
    423    this.setItemAttr("context-pdfjs-redo", "disabled", !hasSomethingToRedo);
    424    this.setItemAttr(
    425      "context-sep-pdfjs-redo",
    426      "disabled",
    427      !hasSomethingToUndo && !hasSomethingToRedo
    428    );
    429    this.setItemAttr(
    430      "context-pdfjs-cut",
    431      "disabled",
    432      isEmpty || !hasSelectedEditor
    433    );
    434    this.setItemAttr(
    435      "context-pdfjs-copy",
    436      "disabled",
    437      isEmpty || !hasSelectedEditor
    438    );
    439    this.setItemAttr("context-pdfjs-paste", "disabled", hasEmptyClipboard);
    440    this.setItemAttr(
    441      "context-pdfjs-delete",
    442      "disabled",
    443      isEmpty || !hasSelectedEditor
    444    );
    445    this.setItemAttr("context-pdfjs-selectall", "disabled", isEmpty);
    446    this.setItemAttr("context-sep-pdfjs-selectall", "disabled", isEmpty);
    447  }
    448 
    449  initTextFragmentItems() {
    450    const shouldShow =
    451      lazy.TEXT_FRAGMENTS_ENABLED &&
    452      !(
    453        this.inPDFViewer ||
    454        this.inFrame ||
    455        this.onEditable ||
    456        this.browser.currentURI.schemeIs("view-source")
    457      ) &&
    458      (this.hasTextFragments || this.isContentSelected);
    459    this.showItem("context-copy-link-to-highlight", shouldShow);
    460    this.showItem(
    461      "context-copy-clean-link-to-highlight",
    462      shouldShow && lazy.STRIP_ON_SHARE_ENABLED
    463    );
    464 
    465    // disables both options by default, while API tries to build a text fragment
    466    this.setItemAttr("context-copy-link-to-highlight", "disabled", true);
    467    this.setItemAttr("context-copy-clean-link-to-highlight", "disabled", true);
    468 
    469    // Only show remove option if there are text fragments on the page.
    470    this.showItem("context-sep-highlights", this.hasTextFragments);
    471    this.showItem("context-remove-highlight", this.hasTextFragments);
    472  }
    473 
    474  async getTextDirective() {
    475    if (!lazy.TEXT_FRAGMENTS_ENABLED) {
    476      return;
    477    }
    478    this.textFragmentURL = await this.actor.getTextDirective();
    479 
    480    // enable menu items when a text fragment can be built
    481    if (this.textFragmentURL) {
    482      this.setItemAttr("context-copy-link-to-highlight", "disabled", null);
    483      let link = this.getLinkURI(this.textFragmentURL);
    484      let disabledAttr = this.#canStripParams(link) ? null : true;
    485      this.setItemAttr(
    486        "context-copy-clean-link-to-highlight",
    487        "disabled",
    488        disabledAttr
    489      );
    490    }
    491  }
    492 
    493  async removeAllTextFragments() {
    494    await this.actor.removeAllTextFragments();
    495  }
    496 
    497  copyLinkToHighlight(stripSiteTracking = false) {
    498    if (this.textFragmentURL) {
    499      if (stripSiteTracking) {
    500        const uri = this.getLinkURI(this.textFragmentURL);
    501        this.copyStrippedLink(uri);
    502      } else {
    503        this.copyLink(this.textFragmentURL);
    504      }
    505    }
    506  }
    507 
    508  initOpenItems() {
    509    var isMailtoInternal = false;
    510    if (this.onMailtoLink) {
    511      var mailtoHandler = Cc[
    512        "@mozilla.org/uriloader/external-protocol-service;1"
    513      ]
    514        .getService(Ci.nsIExternalProtocolService)
    515        .getProtocolHandlerInfo("mailto");
    516      isMailtoInternal =
    517        !mailtoHandler.alwaysAskBeforeHandling &&
    518        mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
    519        mailtoHandler.preferredApplicationHandler instanceof
    520          Ci.nsIWebHandlerApp;
    521    }
    522 
    523    if (
    524      this.isTextSelected &&
    525      !this.onLink &&
    526      this.selectionInfo &&
    527      this.selectionInfo.linkURL
    528    ) {
    529      this.linkURL = this.selectionInfo.linkURL;
    530      this.linkURI = this.getLinkURI();
    531 
    532      this.linkTextStr = this.selectionInfo.linkText;
    533      this.onPlainTextLink = true;
    534    }
    535 
    536    let { window, document } = this;
    537    var inContainer = false;
    538    if (this.contentData.userContextId) {
    539      inContainer = true;
    540      var item = document.getElementById("context-openlinkincontainertab");
    541 
    542      item.setAttribute("data-usercontextid", this.contentData.userContextId);
    543 
    544      var label = lazy.ContextualIdentityService.getUserContextLabel(
    545        this.contentData.userContextId
    546      );
    547 
    548      document.l10n.setAttributes(
    549        item,
    550        "main-context-menu-open-link-in-container-tab",
    551        {
    552          containerName: label,
    553        }
    554      );
    555    }
    556 
    557    var shouldShow =
    558      this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
    559    var isWindowPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
    560    let showContainers =
    561      Services.prefs.getBoolPref("privacy.userContext.enabled") &&
    562      lazy.ContextualIdentityService.getPublicIdentities().length;
    563    this.showItem("context-openlink", shouldShow && !isWindowPrivate);
    564    this.showItem(
    565      "context-openlinkprivate",
    566      shouldShow && lazy.PrivateBrowsingUtils.enabled
    567    );
    568    this.showItem("context-openlinkintab", shouldShow && !inContainer);
    569    this.showItem("context-openlinkincontainertab", shouldShow && inContainer);
    570    this.showItem(
    571      "context-openlinkinusercontext-menu",
    572      shouldShow && !isWindowPrivate && showContainers
    573    );
    574    this.showItem("context-openlinkincurrent", this.onPlainTextLink);
    575    // LinkPreview.sys.mjs is missing. tor-browser#44045.
    576    this.showItem("context-previewlink", false);
    577  }
    578 
    579  initNavigationItems() {
    580    var shouldShow =
    581      !(
    582        this.isContentSelected ||
    583        this.onLink ||
    584        this.onImage ||
    585        this.onCanvas ||
    586        this.onVideo ||
    587        this.onAudio ||
    588        this.onTextInput
    589      ) && this.inTabBrowser;
    590    if (AppConstants.platform == "macosx") {
    591      for (let id of [
    592        "context-back",
    593        "context-forward",
    594        "context-reload",
    595        "context-stop",
    596        "context-sep-navigation",
    597      ]) {
    598        this.showItem(id, shouldShow);
    599      }
    600    } else {
    601      this.showItem("context-navigation", shouldShow);
    602    }
    603 
    604    let stopped =
    605      this.window.XULBrowserWindow.stopCommand.getAttribute("disabled") ==
    606      "true";
    607 
    608    let stopReloadItem = "";
    609    if (shouldShow) {
    610      stopReloadItem = stopped ? "reload" : "stop";
    611    }
    612 
    613    this.showItem("context-reload", stopReloadItem == "reload");
    614    this.showItem("context-stop", stopReloadItem == "stop");
    615 
    616    let { document } = this;
    617    let initBackForwardMenuItemTooltip = (menuItemId, l10nId, shortcutId) => {
    618      // On macOS regular menuitems are used and the shortcut isn't added
    619      if (AppConstants.platform == "macosx") {
    620        return;
    621      }
    622 
    623      let shortcut = document.getElementById(shortcutId);
    624      if (shortcut) {
    625        shortcut = lazy.ShortcutUtils.prettifyShortcut(shortcut);
    626      } else {
    627        // Sidebar doesn't have navigation buttons or shortcuts, but we still
    628        // want to format the menu item tooltip to remove "$shortcut" string.
    629        shortcut = "";
    630      }
    631 
    632      let menuItem = document.getElementById(menuItemId);
    633      document.l10n.setAttributes(menuItem, l10nId, { shortcut });
    634    };
    635 
    636    initBackForwardMenuItemTooltip(
    637      "context-back",
    638      "main-context-menu-back-2",
    639      "goBackKb"
    640    );
    641 
    642    initBackForwardMenuItemTooltip(
    643      "context-forward",
    644      "main-context-menu-forward-2",
    645      "goForwardKb"
    646    );
    647  }
    648 
    649  initLeaveDOMFullScreenItems() {
    650    // only show the option if the user is in DOM fullscreen
    651    var shouldShow = this.target.ownerDocument.fullscreen;
    652    this.showItem("context-leave-dom-fullscreen", shouldShow);
    653  }
    654 
    655  initSaveItems() {
    656    var shouldShow = !(
    657      this.onTextInput ||
    658      this.onLink ||
    659      this.isContentSelected ||
    660      this.onImage ||
    661      this.onCanvas ||
    662      this.onVideo ||
    663      this.onAudio
    664    );
    665    this.showItem("context-savepage", shouldShow);
    666 
    667    // Save link depends on whether we're in a link, or selected text matches valid URL pattern.
    668    this.showItem(
    669      "context-savelink",
    670      this.onSaveableLink || this.onPlainTextLink
    671    );
    672    if (
    673      (this.onSaveableLink || this.onPlainTextLink) &&
    674      Services.policies.status === Services.policies.ACTIVE
    675    ) {
    676      this.setItemAttr(
    677        "context-savelink",
    678        "disabled",
    679        !lazy.WebsiteFilter.isAllowed(this.linkURL)
    680      );
    681    }
    682 
    683    // Save video and audio don't rely on whether it has loaded or not.
    684    this.showItem("context-savevideo", this.onVideo);
    685    this.showItem("context-saveaudio", this.onAudio);
    686    this.showItem("context-video-saveimage", this.onVideo);
    687    this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
    688    this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
    689    this.showItem("context-sendvideo", this.onVideo);
    690    this.showItem("context-sendaudio", this.onAudio);
    691    let mediaIsBlob = this.mediaURL.startsWith("blob:");
    692    this.setItemAttr(
    693      "context-sendvideo",
    694      "disabled",
    695      !this.mediaURL || mediaIsBlob
    696    );
    697    this.setItemAttr(
    698      "context-sendaudio",
    699      "disabled",
    700      !this.mediaURL || mediaIsBlob
    701    );
    702 
    703    if (
    704      Services.policies.status === Services.policies.ACTIVE &&
    705      !Services.policies.isAllowed("filepickers")
    706    ) {
    707      // When file pickers are disallowed by enterprise policy,
    708      // these items silently fail. So to avoid confusion, we
    709      // disable them.
    710      for (let item of [
    711        "context-savepage",
    712        "context-savelink",
    713        "context-savevideo",
    714        "context-saveaudio",
    715        "context-video-saveimage",
    716        "context-saveaudio",
    717      ]) {
    718        this.setItemAttr(item, "disabled", true);
    719      }
    720    }
    721  }
    722 
    723  initImageItems() {
    724    // Reload image depends on an image that's not fully loaded
    725    this.showItem(
    726      "context-reloadimage",
    727      this.onImage && !this.onCompletedImage
    728    );
    729 
    730    // View image depends on having an image that's not standalone
    731    // (or is in a frame), or a canvas. If this isn't an image, check
    732    // if there is a background image.
    733    let showViewImage =
    734      ((this.onImage && (!this.inSyntheticDoc || this.inFrame)) ||
    735        this.onCanvas) &&
    736      !this.inPDFViewer;
    737    let showBGImage =
    738      this.hasBGImage &&
    739      !this.hasMultipleBGImages &&
    740      !this.inSyntheticDoc &&
    741      !this.inPDFViewer &&
    742      !this.isContentSelected &&
    743      !this.onImage &&
    744      !this.onCanvas &&
    745      !this.onVideo &&
    746      !this.onAudio &&
    747      !this.onLink &&
    748      !this.onTextInput;
    749    this.showItem("context-viewimage", showViewImage || showBGImage);
    750 
    751    // Save image depends on having loaded its content.
    752    this.showItem(
    753      "context-saveimage",
    754      (this.onLoadedImage || this.onCanvas) && !this.inPDFEditor
    755    );
    756 
    757    if (Services.policies.status === Services.policies.ACTIVE) {
    758      // When file pickers are disallowed by enterprise policy,
    759      // this item silently fails. So to avoid confusion, we
    760      // disable it.
    761      this.setItemAttr(
    762        "context-saveimage",
    763        "disabled",
    764        !Services.policies.isAllowed("filepickers")
    765      );
    766    }
    767 
    768    // Copy image contents depends on whether we're on an image.
    769    // Note: the element doesn't exist on all platforms, but showItem() takes
    770    // care of that by itself.
    771    this.showItem("context-copyimage-contents", this.onImage);
    772 
    773    // Copy image location depends on whether we're on an image.
    774    this.showItem("context-copyimage", this.onImage || showBGImage);
    775 
    776    // Performing text recognition only works on images, and if the feature is enabled.
    777    this.showItem(
    778      "context-imagetext",
    779      this.onImage &&
    780        Services.appinfo.isTextRecognitionSupported &&
    781        lazy.TEXT_RECOGNITION_ENABLED
    782    );
    783 
    784    // Send media URL (but not for canvas, since it's a big data: URL)
    785    this.showItem("context-sendimage", this.onImage || showBGImage);
    786 
    787    // View Image Info defaults to false, user can enable
    788    var showViewImageInfo =
    789      this.onImage &&
    790      Services.prefs.getBoolPref("browser.menu.showViewImageInfo", false);
    791 
    792    this.showItem("context-viewimageinfo", showViewImageInfo);
    793    // The image info popup is broken for WebExtension popups, since the browser
    794    // is destroyed when the popup is closed.
    795    this.setItemAttr(
    796      "context-viewimageinfo",
    797      "disabled",
    798      this.webExtBrowserType === "popup"
    799    );
    800    // Open the link to more details about the image. Does not apply to
    801    // background images.
    802    this.showItem(
    803      "context-viewimagedesc",
    804      this.onImage && this.imageDescURL !== ""
    805    );
    806 
    807    this.showAndFormatVisualSearchContextItem();
    808 
    809    // Set as Desktop background depends on whether an image was clicked on,
    810    // and only works if we have a shell service.
    811    var haveSetDesktopBackground = false;
    812 
    813    if (
    814      AppConstants.HAVE_SHELL_SERVICE &&
    815      Services.policies.isAllowed("setDesktopBackground")
    816    ) {
    817      // Only enable Set as Desktop Background if we can get the shell service.
    818      var shell = this.window.getShellService();
    819      if (shell) {
    820        haveSetDesktopBackground = shell.canSetDesktopBackground;
    821      }
    822    }
    823 
    824    this.showItem(
    825      "context-setDesktopBackground",
    826      haveSetDesktopBackground && this.onLoadedImage
    827    );
    828 
    829    if (haveSetDesktopBackground && this.onLoadedImage) {
    830      this.document.getElementById("context-setDesktopBackground").disabled =
    831        this.contentData.disableSetDesktopBackground;
    832    }
    833  }
    834 
    835  initViewItems() {
    836    // View source is always OK, unless in directory listing.
    837    this.showItem(
    838      "context-viewpartialsource-selection",
    839      !this.inAboutDevtoolsToolbox &&
    840        this.isContentSelected &&
    841        this.selectionInfo.isDocumentLevelSelection
    842    );
    843 
    844    this.showItem(
    845      "context-print-selection",
    846      !this.inAboutDevtoolsToolbox &&
    847        this.isContentSelected &&
    848        this.selectionInfo.isDocumentLevelSelection &&
    849        lazy.gPrintEnabled
    850    );
    851 
    852    var shouldShow = !(
    853      this.isContentSelected ||
    854      this.onImage ||
    855      this.onCanvas ||
    856      this.onVideo ||
    857      this.onAudio ||
    858      this.onLink ||
    859      this.onTextInput
    860    );
    861 
    862    var showInspect =
    863      this.inTabBrowser &&
    864      !this.inAboutDevtoolsToolbox &&
    865      Services.prefs.getBoolPref("devtools.inspector.enabled", true) &&
    866      !Services.prefs.getBoolPref("devtools.policy.disabled", false);
    867 
    868    var showInspectA11Y =
    869      showInspect &&
    870      Services.prefs.getBoolPref("devtools.accessibility.enabled", false) &&
    871      Services.prefs.getBoolPref("devtools.enabled", true) &&
    872      (Services.prefs.getBoolPref("devtools.everOpened", false) ||
    873        // Note: this is a legacy usecase, we will remove it in bug 1695257,
    874        // once existing users have had time to set devtools.everOpened
    875        // through normal use, and we've passed an ESR cycle (91).
    876        lazy.DevToolsShim.isDevToolsUser());
    877 
    878    this.showItem("context-viewsource", shouldShow);
    879    this.showItem("context-inspect", showInspect);
    880 
    881    this.showItem("context-inspect-a11y", showInspectA11Y);
    882 
    883    // View video depends on not having a standalone video.
    884    this.showItem(
    885      "context-viewvideo",
    886      this.onVideo && (!this.inSyntheticDoc || this.inFrame)
    887    );
    888    this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
    889  }
    890 
    891  initMiscItems() {
    892    let { window, document } = this;
    893    // Use "Bookmark Link…" if on a link.
    894    let bookmarkPage = document.getElementById("context-bookmarkpage");
    895    this.showItem(
    896      bookmarkPage,
    897      !(
    898        this.isContentSelected ||
    899        this.onTextInput ||
    900        this.onLink ||
    901        this.onImage ||
    902        this.onVideo ||
    903        this.onAudio ||
    904        this.onCanvas ||
    905        this.inWebExtBrowser
    906      )
    907    );
    908 
    909    this.showItem(
    910      "context-bookmarklink",
    911      (this.onLink &&
    912        !this.onMailtoLink &&
    913        !this.onTelLink &&
    914        !this.onMozExtLink) ||
    915        this.onPlainTextLink
    916    );
    917    this.showItem("context-add-engine", this.shouldShowAddEngine());
    918    this.showItem("frame", this.inFrame);
    919 
    920    if (this.inFrame) {
    921      // To make it easier to debug the browser running with out-of-process iframes, we
    922      // display the process PID of the iframe in the context menu for the subframe.
    923      let frameOsPid =
    924        this.actor.manager.browsingContext.currentWindowGlobal.osPid;
    925      this.setItemAttr("context-frameOsPid", "label", "PID: " + frameOsPid);
    926    }
    927 
    928    this.showAndFormatSearchContextItem();
    929    this.showTranslateSelectionItem();
    930    // GenAI.sys.mjs is missing. tor-browser#44045.
    931    // Need to remove the element from the DOM since otherwise it will cause an
    932    // error due to `.menupopup === null`.
    933    document.getElementById("context-ask-chat")?.remove();
    934 
    935    // srcdoc cannot be opened separately due to concerns about web
    936    // content with about:srcdoc in location bar masquerading as trusted
    937    // chrome/addon content.
    938    // No need to also test for this.inFrame as this is checked in the parent
    939    // submenu.
    940    this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
    941    this.showItem("context-openframeintab", !this.inSrcdocFrame);
    942    this.showItem("context-openframe", !this.inSrcdocFrame);
    943    this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
    944    this.showItem("context-printframe", lazy.gPrintEnabled);
    945    this.showItem("print-frame-sep", lazy.gPrintEnabled);
    946 
    947    // Hide menu entries for images, show otherwise
    948    if (this.inFrame) {
    949      this.viewFrameSourceElement.hidden =
    950        !lazy.BrowserUtils.mimeTypeIsTextBased(
    951          this.target.ownerDocument.contentType
    952        );
    953    }
    954 
    955    // BiDi UI
    956    this.showItem(
    957      "context-bidi-text-direction-toggle",
    958      this.onTextInput && !this.onNumeric && window.top.gBidiUI
    959    );
    960    this.showItem(
    961      "context-bidi-page-direction-toggle",
    962      !this.onTextInput && window.top.gBidiUI
    963    );
    964  }
    965 
    966  initSpellingItems() {
    967    let { document } = this;
    968    let { InlineSpellCheckerUI } = this.window;
    969    var canSpell =
    970      InlineSpellCheckerUI.canSpellCheck &&
    971      !InlineSpellCheckerUI.initialSpellCheckPending &&
    972      this.canSpellCheck;
    973    let showDictionaries = canSpell && InlineSpellCheckerUI.enabled;
    974    var onMisspelling = InlineSpellCheckerUI.overMisspelling;
    975    var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
    976    this.showItem("spell-check-enabled", canSpell);
    977    document
    978      .getElementById("spell-check-enabled")
    979      .toggleAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
    980 
    981    this.showItem("spell-add-to-dictionary", onMisspelling);
    982    this.showItem("spell-undo-add-to-dictionary", showUndo);
    983 
    984    // suggestion list
    985    if (onMisspelling) {
    986      var suggestionsSeparator = document.getElementById(
    987        "spell-add-to-dictionary"
    988      );
    989      var numsug = InlineSpellCheckerUI.addSuggestionsToMenu(
    990        suggestionsSeparator.parentNode,
    991        suggestionsSeparator,
    992        this.spellSuggestions
    993      );
    994      this.showItem("spell-no-suggestions", numsug == 0);
    995    } else {
    996      this.showItem("spell-no-suggestions", false);
    997    }
    998 
    999    // dictionary list
   1000    this.showItem("spell-dictionaries", showDictionaries);
   1001    if (canSpell) {
   1002      var dictMenu = document.getElementById("spell-dictionaries-menu");
   1003      var dictSep = document.getElementById("spell-language-separator");
   1004      InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
   1005      this.showItem("spell-add-dictionaries-main", false);
   1006    } else if (this.onSpellcheckable) {
   1007      // when there is no spellchecker but we might be able to spellcheck
   1008      // add the add to dictionaries item. This will ensure that people
   1009      // with no dictionaries will be able to download them
   1010      this.showItem("spell-add-dictionaries-main", showDictionaries);
   1011    } else {
   1012      this.showItem("spell-add-dictionaries-main", false);
   1013    }
   1014  }
   1015 
   1016  initClipboardItems() {
   1017    // Copy depends on whether there is selected text.
   1018    // Enabling this context menu item is now done through the global
   1019    // command updating system
   1020    // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
   1021    this.window.goUpdateGlobalEditMenuItems();
   1022 
   1023    this.showItem("context-undo", this.onTextInput);
   1024    this.showItem("context-redo", this.onTextInput);
   1025    this.showItem("context-cut", this.onTextInput);
   1026    this.showItem("context-copy", this.isContentSelected || this.onTextInput);
   1027    this.showItem("context-paste", this.onTextInput);
   1028    this.showItem("context-paste-no-formatting", this.isDesignMode);
   1029    this.showItem("context-delete", this.onTextInput);
   1030    this.showItem(
   1031      "context-selectall",
   1032      !(
   1033        this.onLink ||
   1034        this.onImage ||
   1035        this.onVideo ||
   1036        this.onAudio ||
   1037        this.inSyntheticDoc ||
   1038        this.inPDFEditor
   1039      ) || this.isDesignMode
   1040    );
   1041 
   1042    // XXX dr
   1043    // ------
   1044    // nsDocumentViewer.cpp has code to determine whether we're
   1045    // on a link or an image. we really ought to be using that...
   1046 
   1047    // Copy email link depends on whether we're on an email link.
   1048    this.showItem("context-copyemail", this.onMailtoLink);
   1049 
   1050    // Copy phone link depends on whether we're on a phone link.
   1051    this.showItem("context-copyphone", this.onTelLink);
   1052 
   1053    // Copy link location depends on whether we're on a non-mailto link.
   1054    this.showItem(
   1055      "context-copylink",
   1056      this.onLink && !this.onMailtoLink && !this.onTelLink
   1057    );
   1058 
   1059    // Showing "Copy Clean link" depends on whether the strip-on-share feature is enabled
   1060    // and the user is selecting a URL
   1061    this.showItem(
   1062      "context-stripOnShareLink",
   1063      lazy.STRIP_ON_SHARE_ENABLED &&
   1064        (this.onLink || this.onPlainTextLink) &&
   1065        !this.onMailtoLink &&
   1066        !this.onTelLink &&
   1067        !this.onMozExtLink &&
   1068        !this.isSecureAboutPage()
   1069    );
   1070 
   1071    let disabledAttr = this.#canStripParams() ? null : true;
   1072    this.setItemAttr("context-stripOnShareLink", "disabled", disabledAttr);
   1073 
   1074    let sendLinkSeparator = this.document.getElementById(
   1075      "context-sep-sendlinktodevice"
   1076    );
   1077    sendLinkSeparator.toggleAttribute("ensureHidden", !this.syncItemsShown);
   1078 
   1079    this.showItem("context-copyvideourl", this.onVideo);
   1080    this.showItem("context-copyaudiourl", this.onAudio);
   1081    this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
   1082    this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
   1083  }
   1084 
   1085  initMediaPlayerItems() {
   1086    var onMedia = this.onVideo || this.onAudio;
   1087    // Several mutually exclusive items... play/pause, mute/unmute, show/hide
   1088    this.showItem(
   1089      "context-media-play",
   1090      onMedia && (this.target.paused || this.target.ended)
   1091    );
   1092    this.showItem(
   1093      "context-media-pause",
   1094      onMedia && !this.target.paused && !this.target.ended
   1095    );
   1096    this.showItem("context-media-mute", onMedia && !this.target.muted);
   1097    this.showItem("context-media-unmute", onMedia && this.target.muted);
   1098    this.showItem(
   1099      "context-media-playbackrate",
   1100      onMedia && this.target.duration != Number.POSITIVE_INFINITY
   1101    );
   1102    this.showItem("context-media-loop", onMedia);
   1103    this.showItem(
   1104      "context-media-showcontrols",
   1105      onMedia && !this.target.controls
   1106    );
   1107    this.showItem(
   1108      "context-media-hidecontrols",
   1109      this.target.controls &&
   1110        (this.onVideo || (this.onAudio && !this.inSyntheticDoc))
   1111    );
   1112    this.showItem(
   1113      "context-video-fullscreen",
   1114      this.onVideo && !this.target.ownerDocument.fullscreen
   1115    );
   1116    {
   1117      let shouldDisplay =
   1118        Services.prefs.getBoolPref(
   1119          "media.videocontrols.picture-in-picture.enabled"
   1120        ) &&
   1121        this.onVideo &&
   1122        !this.target.ownerDocument.fullscreen &&
   1123        this.target.readyState > 0;
   1124      this.showItem("context-video-pictureinpicture", shouldDisplay);
   1125    }
   1126    this.showItem("context-media-eme-learnmore", this.onDRMMedia);
   1127 
   1128    // Disable them when there isn't a valid media source loaded.
   1129    if (onMedia) {
   1130      this.setItemAttr(
   1131        "context-media-playbackrate-050x",
   1132        "checked",
   1133        this.target.playbackRate == 0.5
   1134      );
   1135      this.setItemAttr(
   1136        "context-media-playbackrate-100x",
   1137        "checked",
   1138        this.target.playbackRate == 1.0
   1139      );
   1140      this.setItemAttr(
   1141        "context-media-playbackrate-125x",
   1142        "checked",
   1143        this.target.playbackRate == 1.25
   1144      );
   1145      this.setItemAttr(
   1146        "context-media-playbackrate-150x",
   1147        "checked",
   1148        this.target.playbackRate == 1.5
   1149      );
   1150      this.setItemAttr(
   1151        "context-media-playbackrate-200x",
   1152        "checked",
   1153        this.target.playbackRate == 2.0
   1154      );
   1155      this.setItemAttr("context-media-loop", "checked", this.target.loop);
   1156      var hasError =
   1157        this.target.error != null ||
   1158        this.target.networkState == this.target.NETWORK_NO_SOURCE;
   1159      this.setItemAttr("context-media-play", "disabled", hasError);
   1160      this.setItemAttr("context-media-pause", "disabled", hasError);
   1161      this.setItemAttr("context-media-mute", "disabled", hasError);
   1162      this.setItemAttr("context-media-unmute", "disabled", hasError);
   1163      this.setItemAttr("context-media-playbackrate", "disabled", hasError);
   1164      this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
   1165      this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
   1166      this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError);
   1167      this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
   1168      this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
   1169      this.setItemAttr("context-media-showcontrols", "disabled", hasError);
   1170      this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
   1171      if (this.onVideo) {
   1172        let canSaveSnapshot =
   1173          !this.onDRMMedia &&
   1174          this.target.readyState >= this.target.HAVE_CURRENT_DATA;
   1175        this.setItemAttr(
   1176          "context-video-saveimage",
   1177          "disabled",
   1178          !canSaveSnapshot
   1179        );
   1180        this.setItemAttr("context-video-fullscreen", "disabled", hasError);
   1181        this.setItemAttr(
   1182          "context-video-pictureinpicture",
   1183          "checked",
   1184          this.onPiPVideo
   1185        );
   1186        this.setItemAttr(
   1187          "context-video-pictureinpicture",
   1188          "disabled",
   1189          !this.onPiPVideo && hasError
   1190        );
   1191      }
   1192    }
   1193  }
   1194 
   1195  initPasswordManagerItems() {
   1196    let { document } = this;
   1197    let showUseSavedLogin = false;
   1198    let showGenerate = false;
   1199    let showManage = false;
   1200    let enableGeneration = Services.logins.isLoggedIn;
   1201    try {
   1202      // If we could not find a password field we don't want to
   1203      // show the form fill, manage logins and the password generation items.
   1204      if (!this.isLoginForm()) {
   1205        return;
   1206      }
   1207      showManage = true;
   1208 
   1209      // Disable the fill option if the user hasn't unlocked with their primary password
   1210      // or if the password field or target field are disabled.
   1211      // XXX: Bug 1529025 to maybe respect signon.rememberSignons.
   1212      let loginFillInfo = this.contentData?.loginFillInfo;
   1213      let disableFill =
   1214        !Services.logins.isLoggedIn ||
   1215        loginFillInfo?.passwordField.disabled ||
   1216        loginFillInfo?.activeField.disabled;
   1217      this.setItemAttr("fill-login", "disabled", disableFill);
   1218 
   1219      let onPasswordLikeField = PASSWORD_FIELDNAME_HINTS.includes(
   1220        loginFillInfo.activeField.fieldNameHint
   1221      );
   1222 
   1223      // Set the correct label for the fill menu
   1224      let fillMenu = document.getElementById("fill-login");
   1225      document.l10n.setAttributes(
   1226        fillMenu,
   1227        "main-context-menu-use-saved-password"
   1228      );
   1229 
   1230      let documentURI = this.contentData?.documentURIObject;
   1231      let formOrigin = lazy.LoginHelper.getLoginOrigin(documentURI?.spec);
   1232      let isGeneratedPasswordEnabled =
   1233        lazy.LoginHelper.generationAvailable &&
   1234        lazy.LoginHelper.generationEnabled;
   1235      showGenerate =
   1236        onPasswordLikeField &&
   1237        isGeneratedPasswordEnabled &&
   1238        Services.logins.getLoginSavingEnabled(formOrigin);
   1239 
   1240      if (disableFill) {
   1241        showUseSavedLogin = true;
   1242 
   1243        // No need to update the submenu if the fill item is disabled.
   1244        return;
   1245      }
   1246 
   1247      // Update sub-menu items.
   1248      this.updatePasswordManagerSubMenuItems(document, formOrigin);
   1249    } finally {
   1250      const documentURI = this.contentData?.documentURIObject;
   1251      const showRelay =
   1252        this.contentData?.context.showRelay &&
   1253        lazy.LoginHelper.getLoginOrigin(documentURI?.spec);
   1254 
   1255      this.showItem("fill-login", showUseSavedLogin);
   1256      this.showItem("fill-login-generated-password", showGenerate);
   1257      this.showItem("use-relay-mask", showRelay);
   1258      this.showItem("manage-saved-logins", showManage);
   1259      this.setItemAttr(
   1260        "fill-login-generated-password",
   1261        "disabled",
   1262        !enableGeneration
   1263      );
   1264      this.setItemAttr(
   1265        "passwordmgr-items-separator",
   1266        "ensureHidden",
   1267        showUseSavedLogin || showGenerate || showManage || showRelay
   1268          ? null
   1269          : true
   1270      );
   1271    }
   1272  }
   1273 
   1274  async updatePasswordManagerSubMenuItems(document, formOrigin) {
   1275    const fragment = await lazy.LoginManagerContextMenu.addLoginsToMenu(
   1276      this.targetIdentifier,
   1277      this.browser,
   1278      formOrigin
   1279    );
   1280 
   1281    if (!fragment) {
   1282      return;
   1283    }
   1284 
   1285    let popup = document.getElementById("fill-login-popup");
   1286    popup.appendChild(fragment);
   1287 
   1288    this.showItem("fill-login", true);
   1289 
   1290    this.setItemAttr("passwordmgr-items-separator", "ensureHidden", null);
   1291  }
   1292 
   1293  initSyncItems() {
   1294    this.syncItemsShown = this.window.gSync.updateContentContextMenu(this);
   1295  }
   1296 
   1297  initViewSourceItems() {
   1298    const getString = aName => {
   1299      const { bundle } = this.window.gViewSourceUtils.getPageActor(
   1300        this.browser
   1301      );
   1302      return bundle.GetStringFromName(aName);
   1303    };
   1304    const showViewSourceItem = (id, check, accesskey) => {
   1305      const fullId = `context-viewsource-${id}`;
   1306      this.showItem(fullId, onViewSource);
   1307      if (!onViewSource) {
   1308        return;
   1309      }
   1310      this.setItemAttr(fullId, "checked", check());
   1311      this.setItemAttr(fullId, "label", getString(`context_${id}_label`));
   1312      if (accesskey) {
   1313        this.setItemAttr(
   1314          fullId,
   1315          "accesskey",
   1316          getString(`context_${id}_accesskey`)
   1317        );
   1318      }
   1319    };
   1320 
   1321    const onViewSource = this.browser.currentURI.schemeIs("view-source");
   1322 
   1323    showViewSourceItem("goToLine", () => false, true);
   1324    showViewSourceItem("wrapLongLines", () =>
   1325      Services.prefs.getBoolPref("view_source.wrap_long_lines", false)
   1326    );
   1327    showViewSourceItem("highlightSyntax", () =>
   1328      Services.prefs.getBoolPref("view_source.syntax_highlight", false)
   1329    );
   1330  }
   1331 
   1332  // Iterate over the visible items on the menu and its submenus and
   1333  // hide any duplicated separators next to each other.
   1334  // The attribute "ensureHidden" will override this process and keep a particular separator hidden in special cases.
   1335  showHideSeparators(aPopup) {
   1336    let lastVisibleSeparator = null;
   1337    let count = 0;
   1338    for (let menuItem of aPopup.children) {
   1339      // Skip any items that were added by the page menu.
   1340      if (menuItem.hasAttribute("generateditemid")) {
   1341        count++;
   1342        continue;
   1343      }
   1344 
   1345      if (menuItem.localName == "menuseparator") {
   1346        // Individual separators can have the `ensureHidden` attribute added to avoid them
   1347        // becoming visible. We also set `count` to 0 below because otherwise the
   1348        // next separator would be made visible, with the same visual effect.
   1349        if (!count || menuItem.hasAttribute("ensureHidden")) {
   1350          menuItem.hidden = true;
   1351        } else {
   1352          menuItem.hidden = false;
   1353          lastVisibleSeparator = menuItem;
   1354        }
   1355 
   1356        count = 0;
   1357      } else if (!menuItem.hidden) {
   1358        if (menuItem.localName == "menu" && menuItem.menupopup) {
   1359          this.showHideSeparators(menuItem.menupopup);
   1360        } else if (menuItem.localName == "menugroup") {
   1361          this.showHideSeparators(menuItem);
   1362        }
   1363        count++;
   1364      }
   1365    }
   1366 
   1367    // If count is 0 yet lastVisibleSeparator is set, then there must be a separator
   1368    // visible at the end of the menu, so hide it. Note that there could be more than
   1369    // one but this isn't handled here.
   1370    if (!count && lastVisibleSeparator) {
   1371      lastVisibleSeparator.hidden = true;
   1372    }
   1373  }
   1374 
   1375  shouldShowTakeScreenshot() {
   1376    let shouldShow =
   1377      lazy.ScreenshotsUtils.screenshotsEnabled &&
   1378      this.inTabBrowser &&
   1379      !this.onTextInput &&
   1380      !this.onLink &&
   1381      !this.onPlainTextLink &&
   1382      !this.onAudio &&
   1383      !this.onEditable &&
   1384      !this.onPassword;
   1385 
   1386    return shouldShow;
   1387  }
   1388 
   1389  initScreenshotItem() {
   1390    let shouldShow = this.shouldShowTakeScreenshot();
   1391 
   1392    this.showItem("context-sep-screenshots", shouldShow);
   1393    this.showItem("context-take-screenshot", shouldShow);
   1394  }
   1395 
   1396  initPasswordControlItems() {
   1397    let shouldShow = this.onPassword;
   1398    if (shouldShow) {
   1399      let revealPassword = this.document.getElementById(
   1400        "context-reveal-password"
   1401      );
   1402      revealPassword.toggleAttribute("checked", this.passwordRevealed);
   1403    }
   1404    this.showItem("context-reveal-password", shouldShow);
   1405  }
   1406 
   1407  toggleRevealPassword() {
   1408    this.actor.toggleRevealPassword(this.targetIdentifier);
   1409  }
   1410 
   1411  openPasswordManager() {
   1412    lazy.LoginHelper.openPasswordManager(this.window, {
   1413      entryPoint: "Contextmenu",
   1414    });
   1415  }
   1416 
   1417  useRelayMask() {
   1418    const documentURI = this.contentData?.documentURIObject;
   1419    const aOrigin = lazy.LoginHelper.getLoginOrigin(documentURI?.spec);
   1420    this.actor.useRelayMask(this.targetIdentifier, aOrigin);
   1421  }
   1422 
   1423  useGeneratedPassword() {
   1424    lazy.LoginManagerContextMenu.useGeneratedPassword(this.targetIdentifier);
   1425  }
   1426 
   1427  isLoginForm() {
   1428    let loginFillInfo = this.contentData?.loginFillInfo;
   1429    let documentURI = this.contentData?.documentURIObject;
   1430 
   1431    // If we could not find a password field or this is not a username-only
   1432    // form, then don't treat this as a login form.
   1433    return (
   1434      (loginFillInfo?.passwordField?.found ||
   1435        loginFillInfo?.activeField.fieldNameHint == USERNAME_FIELDNAME_HINT) &&
   1436      !documentURI?.schemeIs("about") &&
   1437      this.browser.contentPrincipal.spec != "resource://pdf.js/web/viewer.html"
   1438    );
   1439  }
   1440 
   1441  inspectNode() {
   1442    return lazy.DevToolsShim.inspectNode(
   1443      this.window.gBrowser.selectedTab,
   1444      this.targetIdentifier
   1445    );
   1446  }
   1447 
   1448  inspectA11Y() {
   1449    return lazy.DevToolsShim.inspectA11Y(
   1450      this.window.gBrowser.selectedTab,
   1451      this.targetIdentifier
   1452    );
   1453  }
   1454 
   1455  _openLinkInParameters(extra) {
   1456    let params = {
   1457      charset: this.contentData.charSet,
   1458      originPrincipal: this.principal,
   1459      originStoragePrincipal: this.storagePrincipal,
   1460      triggeringPrincipal: this.principal,
   1461      triggeringRemoteType: this.remoteType,
   1462      policyContainer: this.policyContainer,
   1463      frameID: this.contentData.frameID,
   1464      hasValidUserGestureActivation: true,
   1465      textDirectiveUserActivation: true,
   1466    };
   1467    for (let p in extra) {
   1468      params[p] = extra[p];
   1469    }
   1470 
   1471    let referrerInfo = this.onLink
   1472      ? this.contentData.linkReferrerInfo
   1473      : this.contentData.referrerInfo;
   1474    // If we want to change userContextId, we must be sure that we don't
   1475    // propagate the referrer.
   1476    if (
   1477      ("userContextId" in params &&
   1478        params.userContextId != this.contentData.userContextId) ||
   1479      this.onPlainTextLink
   1480    ) {
   1481      referrerInfo = new lazy.ReferrerInfo(
   1482        referrerInfo.referrerPolicy,
   1483        false,
   1484        referrerInfo.originalReferrer
   1485      );
   1486    }
   1487 
   1488    params.referrerInfo = referrerInfo;
   1489    return params;
   1490  }
   1491 
   1492  _getGlobalHistoryOptions() {
   1493    if (this.isSponsoredLink) {
   1494      return {
   1495        globalHistoryOptions: {
   1496          triggeringSponsoredURL: this.linkURL,
   1497          triggeringSource: "newtab",
   1498        },
   1499      };
   1500    } else if (this.browser.hasAttribute("triggeringSponsoredURL")) {
   1501      return {
   1502        globalHistoryOptions: {
   1503          triggeringSponsoredURL: this.browser.getAttribute(
   1504            "triggeringSponsoredURL"
   1505          ),
   1506          triggeringSponsoredURLVisitTimeMS: this.browser.getAttribute(
   1507            "triggeringSponsoredURLVisitTimeMS"
   1508          ),
   1509          triggeringSource: this.browser.getAttribute("triggeringSource"),
   1510        },
   1511      };
   1512    }
   1513    return {};
   1514  }
   1515 
   1516  // Open linked-to URL in a new window.
   1517  openLink() {
   1518    const params = this._getGlobalHistoryOptions();
   1519 
   1520    this.window.openLinkIn(
   1521      this.linkURL,
   1522      "window",
   1523      this._openLinkInParameters(params)
   1524    );
   1525  }
   1526 
   1527  // Open linked-to URL in a new private window.
   1528  openLinkInPrivateWindow() {
   1529    this.window.openLinkIn(
   1530      this.linkURL,
   1531      "window",
   1532      this._openLinkInParameters({ private: true })
   1533    );
   1534  }
   1535 
   1536  // Open linked-to URL in a new tab.
   1537  openLinkInTab(event) {
   1538    let params = {
   1539      userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
   1540      ...this._getGlobalHistoryOptions(),
   1541    };
   1542 
   1543    this.window.openLinkIn(
   1544      this.linkURL,
   1545      "tab",
   1546      this._openLinkInParameters(params)
   1547    );
   1548  }
   1549 
   1550  // open URL in current tab
   1551  openLinkInCurrent() {
   1552    this.window.openLinkIn(
   1553      this.linkURL,
   1554      "current",
   1555      this._openLinkInParameters()
   1556    );
   1557  }
   1558 
   1559  // Open frame in a new tab.
   1560  openFrameInTab() {
   1561    this.window.openLinkIn(this.contentData.docLocation, "tab", {
   1562      charset: this.contentData.charSet,
   1563      triggeringPrincipal: this.browser.contentPrincipal,
   1564      policyContainer: this.browser.policyContainer,
   1565      referrerInfo: this.contentData.frameReferrerInfo,
   1566    });
   1567  }
   1568 
   1569  // Reload clicked-in frame.
   1570  reloadFrame(aEvent) {
   1571    let forceReload = aEvent.shiftKey;
   1572    this.actor.reloadFrame(this.targetIdentifier, forceReload);
   1573  }
   1574 
   1575  // Open clicked-in frame in its own window.
   1576  openFrame() {
   1577    this.window.openLinkIn(this.contentData.docLocation, "window", {
   1578      charset: this.contentData.charSet,
   1579      triggeringPrincipal: this.browser.contentPrincipal,
   1580      policyContainer: this.browser.policyContainer,
   1581      referrerInfo: this.contentData.frameReferrerInfo,
   1582    });
   1583  }
   1584 
   1585  // Open clicked-in frame in the same window.
   1586  showOnlyThisFrame() {
   1587    this.window.urlSecurityCheck(
   1588      this.contentData.docLocation,
   1589      this.browser.contentPrincipal,
   1590      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
   1591    );
   1592    this.window.openWebLinkIn(this.contentData.docLocation, "current", {
   1593      referrerInfo: this.contentData.frameReferrerInfo,
   1594      triggeringPrincipal: this.browser.contentPrincipal,
   1595    });
   1596  }
   1597 
   1598  takeScreenshot() {
   1599    Services.obs.notifyObservers(
   1600      this.window,
   1601      "menuitem-screenshot",
   1602      "ContextMenu"
   1603    );
   1604  }
   1605 
   1606  pdfJSCmd(aName) {
   1607    if (["cut", "copy", "paste"].includes(aName)) {
   1608      const cmd = `cmd_${aName}`;
   1609      this.document.commandDispatcher
   1610        .getControllerForCommand(cmd)
   1611        .doCommand(cmd);
   1612      if (Cu.isInAutomation) {
   1613        this.browser.sendMessageToActor(
   1614          "PDFJS:Editing",
   1615          { name: aName },
   1616          "Pdfjs"
   1617        );
   1618      }
   1619      return;
   1620    }
   1621    this.browser.sendMessageToActor("PDFJS:Editing", { name: aName }, "Pdfjs");
   1622  }
   1623 
   1624  // View Partial Source
   1625  viewPartialSource() {
   1626    let { browser } = this;
   1627    let openSelectionFn = async () => {
   1628      let tabBrowser = this.window.gBrowser;
   1629      let relatedToCurrent = tabBrowser?.selectedBrowser === browser;
   1630      const inNewWindow = !Services.prefs.getBoolPref("view_source.tab");
   1631      // In the case of popups, we need to find a non-popup browser window.
   1632      // We might also not have a tabBrowser reference (if this isn't in a
   1633      // a tabbrowser scope) or might have a fake/stub tabbrowser reference
   1634      // (in the sidebar). Deal with those cases:
   1635      if (!tabBrowser || !tabBrowser.addTab || !this.window.toolbar.visible) {
   1636        // This returns only non-popup browser windows by default.
   1637        let browserWindow =
   1638          lazy.BrowserWindowTracker.getTopWindow() ??
   1639          (await lazy.BrowserWindowTracker.promiseOpenWindow());
   1640        tabBrowser = browserWindow.gBrowser;
   1641      }
   1642 
   1643      let tab = tabBrowser.addTab("about:blank", {
   1644        relatedToCurrent,
   1645        inBackground: inNewWindow,
   1646        skipAnimation: inNewWindow,
   1647        triggeringPrincipal:
   1648          Services.scriptSecurityManager.getSystemPrincipal(),
   1649      });
   1650      const viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
   1651      if (inNewWindow) {
   1652        tabBrowser.hideTab(tab);
   1653        tabBrowser.replaceTabsWithWindow(tab);
   1654      }
   1655      return viewSourceBrowser;
   1656    };
   1657 
   1658    this.window.gViewSourceUtils.viewPartialSourceInBrowser(
   1659      this.actor.browsingContext,
   1660      openSelectionFn
   1661    );
   1662  }
   1663 
   1664  // Open new "view source" window with the frame's URL.
   1665  viewFrameSource() {
   1666    this.window.BrowserCommands.viewSourceOfDocument({
   1667      browser: this.browser,
   1668      URL: this.contentData.docLocation,
   1669      outerWindowID: this.frameOuterWindowID,
   1670    });
   1671  }
   1672 
   1673  viewInfo() {
   1674    this.window.BrowserCommands.pageInfo(
   1675      this.contentData.docLocation,
   1676      null,
   1677      null,
   1678      null,
   1679      this.browser
   1680    );
   1681  }
   1682 
   1683  viewImageInfo() {
   1684    this.window.BrowserCommands.pageInfo(
   1685      this.contentData.docLocation,
   1686      "mediaTab",
   1687      this.imageInfo,
   1688      null,
   1689      this.browser
   1690    );
   1691  }
   1692 
   1693  viewImageDesc(e) {
   1694    this.window.urlSecurityCheck(
   1695      this.imageDescURL,
   1696      this.principal,
   1697      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
   1698    );
   1699    this.window.openUILink(this.imageDescURL, e, {
   1700      referrerInfo: this.contentData.referrerInfo,
   1701      triggeringPrincipal: this.principal,
   1702      triggeringRemoteType: this.remoteType,
   1703      policyContainer: this.policyContainer,
   1704    });
   1705  }
   1706 
   1707  viewFrameInfo() {
   1708    this.window.BrowserCommands.pageInfo(
   1709      this.contentData.docLocation,
   1710      null,
   1711      null,
   1712      this.actor.browsingContext,
   1713      this.browser
   1714    );
   1715  }
   1716 
   1717  reloadImage() {
   1718    this.window.urlSecurityCheck(
   1719      this.mediaURL,
   1720      this.principal,
   1721      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
   1722    );
   1723    this.actor.reloadImage(this.targetIdentifier);
   1724  }
   1725 
   1726  _canvasToBlobURL(targetIdentifier) {
   1727    return this.actor.canvasToBlobURL(targetIdentifier);
   1728  }
   1729 
   1730  // Change current window to the URL of the image, video, or audio.
   1731  viewMedia(e) {
   1732    let where = lazy.BrowserUtils.whereToOpenLink(e, false, false);
   1733    if (where == "current") {
   1734      where = "tab";
   1735    }
   1736    let referrerInfo = this.contentData.referrerInfo;
   1737    let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
   1738    if (this.onCanvas) {
   1739      this._canvasToBlobURL(this.targetIdentifier).then(blobURL => {
   1740        this.window.openLinkIn(blobURL, where, {
   1741          referrerInfo,
   1742          triggeringPrincipal: systemPrincipal,
   1743        });
   1744      }, console.error);
   1745    } else {
   1746      this.window.urlSecurityCheck(
   1747        this.mediaURL,
   1748        this.principal,
   1749        Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
   1750      );
   1751 
   1752      // Default to opening in a new tab.
   1753      this.window.openLinkIn(this.mediaURL, where, {
   1754        referrerInfo,
   1755        forceAllowDataURI: true,
   1756        triggeringPrincipal: this.principal,
   1757        triggeringRemoteType: this.remoteType,
   1758        policyContainer: this.policyContainer,
   1759      });
   1760    }
   1761  }
   1762 
   1763  saveVideoFrameAsImage() {
   1764    let isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser);
   1765 
   1766    let aName = "";
   1767    if (this.mediaURL) {
   1768      try {
   1769        let uri = this.window.makeURI(this.mediaURL);
   1770        let url = uri.QueryInterface(Ci.nsIURL);
   1771        if (url.fileBaseName) {
   1772          aName = decodeURI(url.fileBaseName) + ".jpg";
   1773        }
   1774      } catch (e) {}
   1775    }
   1776    if (!aName) {
   1777      aName = "snapshot.jpg";
   1778    }
   1779 
   1780    // Cache this because we fetch the data async
   1781    let referrerInfo = this.contentData.referrerInfo;
   1782    let cookieJarSettings = this.contentData.cookieJarSettings;
   1783 
   1784    this.actor.saveVideoFrameAsImage(this.targetIdentifier).then(dataURL => {
   1785      // FIXME can we switch this to a blob URL?
   1786      this.window.internalSave(
   1787        dataURL,
   1788        null, // originalURL
   1789        null, // document
   1790        aName,
   1791        null, // content disposition
   1792        "image/jpeg", // content type - keep in sync with ContextMenuChild!
   1793        true, // bypass cache
   1794        "SaveImageTitle",
   1795        null, // chosen data
   1796        referrerInfo,
   1797        cookieJarSettings,
   1798        null, // initiating doc
   1799        false, // don't skip prompt for where to save
   1800        null, // cache key
   1801        isPrivate,
   1802        this.principal
   1803      );
   1804    });
   1805  }
   1806 
   1807  leaveDOMFullScreen() {
   1808    this.document.exitFullscreen();
   1809  }
   1810 
   1811  // Change current window to the URL of the background image.
   1812  viewBGImage(e) {
   1813    this.window.urlSecurityCheck(
   1814      this.bgImageURL,
   1815      this.principal,
   1816      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT
   1817    );
   1818 
   1819    this.window.openUILink(this.bgImageURL, e, {
   1820      referrerInfo: this.contentData.referrerInfo,
   1821      forceAllowDataURI: true,
   1822      triggeringPrincipal: this.principal,
   1823      triggeringRemoteType: this.remoteType,
   1824      policyContainer: this.policyContainer,
   1825    });
   1826  }
   1827 
   1828  setDesktopBackground() {
   1829    if (!Services.policies.isAllowed("setDesktopBackground")) {
   1830      return;
   1831    }
   1832 
   1833    this.actor
   1834      .setAsDesktopBackground(this.targetIdentifier)
   1835      .then(({ failed, dataURL, imageName }) => {
   1836        if (failed) {
   1837          return;
   1838        }
   1839 
   1840        let image = this.document.createElementNS(
   1841          "http://www.w3.org/1999/xhtml",
   1842          "img"
   1843        );
   1844        image.src = dataURL;
   1845 
   1846        // Confirm since it's annoying if you hit this accidentally.
   1847        const kDesktopBackgroundURL =
   1848          "chrome://browser/content/setDesktopBackground.xhtml";
   1849 
   1850        if (AppConstants.platform == "macosx") {
   1851          // On Mac, the Set Desktop Background window is not modal.
   1852          // Don't open more than one Set Desktop Background window.
   1853          let dbWin = Services.wm.getMostRecentWindow(
   1854            "Shell:SetDesktopBackground"
   1855          );
   1856          if (dbWin) {
   1857            dbWin.gSetBackground.init(image, imageName);
   1858            dbWin.focus();
   1859          } else {
   1860            this.window.openDialog(
   1861              kDesktopBackgroundURL,
   1862              "",
   1863              "centerscreen,chrome,dialog=no,dependent,resizable=no",
   1864              image,
   1865              imageName
   1866            );
   1867          }
   1868        } else {
   1869          // On non-Mac platforms, the Set Wallpaper dialog is modal.
   1870          this.window.openDialog(
   1871            kDesktopBackgroundURL,
   1872            "",
   1873            "centerscreen,chrome,dialog,modal,dependent",
   1874            image,
   1875            imageName
   1876          );
   1877        }
   1878      });
   1879  }
   1880 
   1881  // Save URL of clicked-on frame.
   1882  saveFrame() {
   1883    this.window.saveBrowser(this.browser, false, this.frameBrowsingContext);
   1884  }
   1885 
   1886  // Helper function to wait for appropriate MIME-type headers and
   1887  // then prompt the user with a file picker
   1888  saveHelper(
   1889    linkURL,
   1890    linkText,
   1891    dialogTitle,
   1892    bypassCache,
   1893    doc,
   1894    referrerInfo,
   1895    cookieJarSettings,
   1896    windowID,
   1897    linkDownload,
   1898    isContentWindowPrivate
   1899  ) {
   1900    // canonical def in nsURILoader.h
   1901    const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
   1902 
   1903    // an object to proxy the data through to
   1904    // nsIExternalHelperAppService.doContent, which will wait for the
   1905    // appropriate MIME-type headers and then prompt the user with a
   1906    // file picker
   1907    function saveAsListener(principal, aWindow) {
   1908      this._triggeringPrincipal = principal;
   1909      this._window = aWindow;
   1910    }
   1911    saveAsListener.prototype = {
   1912      extListener: null,
   1913 
   1914      onStartRequest: function saveLinkAs_onStartRequest(aRequest) {
   1915        // if the timer fired, the error status will have been caused by that,
   1916        // and we'll be restarting in onStopRequest, so no reason to notify
   1917        // the user
   1918        if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
   1919          return;
   1920        }
   1921 
   1922        timer.cancel();
   1923 
   1924        // some other error occured; notify the user...
   1925        if (!Components.isSuccessCode(aRequest.status)) {
   1926          try {
   1927            const l10n = new Localization(["browser/downloads.ftl"], true);
   1928 
   1929            let msg = null;
   1930            try {
   1931              const channel = aRequest.QueryInterface(Ci.nsIChannel);
   1932              const reason = channel.loadInfo.requestBlockingReason;
   1933              if (
   1934                reason == Ci.nsILoadInfo.BLOCKING_REASON_EXTENSION_WEBREQUEST
   1935              ) {
   1936                try {
   1937                  const properties = channel.QueryInterface(Ci.nsIPropertyBag);
   1938                  const id = properties.getProperty("cancelledByExtension");
   1939                  msg = l10n.formatValueSync("downloads-error-blocked-by", {
   1940                    extension: WebExtensionPolicy.getByID(id).name,
   1941                  });
   1942                } catch (err) {
   1943                  // "cancelledByExtension" doesn't have to be available.
   1944                  msg = l10n.formatValueSync("downloads-error-extension");
   1945                }
   1946              }
   1947            } catch (ex) {}
   1948            msg ??= l10n.formatValueSync("downloads-error-generic");
   1949 
   1950            const win = Services.wm.getOuterWindowWithId(windowID);
   1951            const title = l10n.formatValueSync("downloads-error-alert-title");
   1952            Services.prompt.alert(win, title, msg);
   1953          } catch (ex) {}
   1954          return;
   1955        }
   1956 
   1957        let extHelperAppSvc = Cc[
   1958          "@mozilla.org/uriloader/external-helper-app-service;1"
   1959        ].getService(Ci.nsIExternalHelperAppService);
   1960        let channel = aRequest.QueryInterface(Ci.nsIChannel);
   1961        this.extListener = extHelperAppSvc.doContent(
   1962          channel.contentType,
   1963          aRequest,
   1964          null,
   1965          true,
   1966          this._window
   1967        );
   1968        this.extListener.onStartRequest(aRequest);
   1969      },
   1970 
   1971      onStopRequest: function saveLinkAs_onStopRequest(aRequest, aStatusCode) {
   1972        if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
   1973          // do it the old fashioned way, which will pick the best filename
   1974          // it can without waiting.
   1975          this._window.saveURL(
   1976            linkURL,
   1977            null,
   1978            linkText,
   1979            dialogTitle,
   1980            bypassCache,
   1981            false,
   1982            referrerInfo,
   1983            cookieJarSettings,
   1984            doc,
   1985            isContentWindowPrivate,
   1986            this._triggeringPrincipal
   1987          );
   1988        }
   1989        if (this.extListener) {
   1990          this.extListener.onStopRequest(aRequest, aStatusCode);
   1991        }
   1992      },
   1993 
   1994      onDataAvailable: function saveLinkAs_onDataAvailable(
   1995        aRequest,
   1996        aInputStream,
   1997        aOffset,
   1998        aCount
   1999      ) {
   2000        this.extListener.onDataAvailable(
   2001          aRequest,
   2002          aInputStream,
   2003          aOffset,
   2004          aCount
   2005        );
   2006      },
   2007    };
   2008 
   2009    function callbacks() {}
   2010    callbacks.prototype = {
   2011      getInterface: function sLA_callbacks_getInterface(aIID) {
   2012        if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
   2013          // If the channel demands authentication prompt, we must cancel it
   2014          // because the save-as-timer would expire and cancel the channel
   2015          // before we get credentials from user.  Both authentication dialog
   2016          // and save as dialog would appear on the screen as we fall back to
   2017          // the old fashioned way after the timeout.
   2018          timer.cancel();
   2019          channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
   2020        }
   2021        throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
   2022      },
   2023    };
   2024 
   2025    // if it we don't have the headers after a short time, the user
   2026    // won't have received any feedback from their click.  that's bad.  so
   2027    // we give up waiting for the filename.
   2028    function timerCallback() {}
   2029    timerCallback.prototype = {
   2030      notify: function sLA_timer_notify() {
   2031        channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
   2032      },
   2033    };
   2034 
   2035    // setting up a new channel for 'right click - save link as ...'
   2036    var channel = lazy.NetUtil.newChannel({
   2037      uri: this.window.makeURI(linkURL),
   2038      loadingPrincipal: this.principal,
   2039      contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
   2040      securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
   2041    });
   2042 
   2043    if (linkDownload) {
   2044      channel.contentDispositionFilename = linkDownload;
   2045    }
   2046    if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
   2047      let docIsPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
   2048        this.browser
   2049      );
   2050      channel.setPrivate(docIsPrivate);
   2051    }
   2052    channel.notificationCallbacks = new callbacks();
   2053 
   2054    let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
   2055 
   2056    if (bypassCache) {
   2057      flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
   2058    }
   2059 
   2060    if (channel instanceof Ci.nsICachingChannel) {
   2061      flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
   2062    }
   2063 
   2064    channel.loadFlags |= flags;
   2065 
   2066    if (channel instanceof Ci.nsIHttpChannel) {
   2067      channel.referrerInfo = referrerInfo;
   2068      if (channel instanceof Ci.nsIHttpChannelInternal) {
   2069        channel.forceAllowThirdPartyCookie = true;
   2070      }
   2071 
   2072      channel.loadInfo.cookieJarSettings = cookieJarSettings;
   2073    }
   2074 
   2075    // fallback to the old way if we don't see the headers quickly
   2076    var timeToWait = Services.prefs.getIntPref(
   2077      "browser.download.saveLinkAsFilenameTimeout"
   2078    );
   2079    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   2080    timer.initWithCallback(
   2081      new timerCallback(),
   2082      timeToWait,
   2083      timer.TYPE_ONE_SHOT
   2084    );
   2085 
   2086    // kick off the channel with our proxy object as the listener
   2087    channel.asyncOpen(new saveAsListener(this.principal, this.window));
   2088  }
   2089 
   2090  // Save URL of clicked-on link.
   2091  saveLink() {
   2092    let referrerInfo = this.onLink
   2093      ? this.contentData.linkReferrerInfo
   2094      : this.contentData.referrerInfo;
   2095 
   2096    let isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser);
   2097    this.saveHelper(
   2098      this.linkURL,
   2099      this.linkTextStr,
   2100      null,
   2101      true,
   2102      this.ownerDoc,
   2103      referrerInfo,
   2104      this.contentData.cookieJarSettings,
   2105      this.frameOuterWindowID,
   2106      this.linkDownload,
   2107      isPrivate
   2108    );
   2109  }
   2110 
   2111  // Backwards-compatibility wrapper
   2112  saveImage() {
   2113    if (this.onCanvas || this.onImage) {
   2114      this.saveMedia();
   2115    }
   2116  }
   2117 
   2118  // Save URL of the clicked upon image, video, or audio.
   2119  saveMedia() {
   2120    let doc = this.ownerDoc;
   2121    let isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(this.browser);
   2122    let referrerInfo = this.contentData.referrerInfo;
   2123    let cookieJarSettings = this.contentData.cookieJarSettings;
   2124    if (this.onCanvas) {
   2125      // Bypass cache, since it's a data: URL.
   2126      this._canvasToBlobURL(this.targetIdentifier).then(blobURL => {
   2127        this.window.internalSave(
   2128          blobURL,
   2129          null, // originalURL
   2130          null, // document
   2131          "canvas.png",
   2132          null, // content disposition
   2133          "image/png", // _canvasToBlobURL uses image/png by default.
   2134          true, // bypass cache
   2135          "SaveImageTitle",
   2136          null, // chosen data
   2137          referrerInfo,
   2138          cookieJarSettings,
   2139          null, // initiating doc
   2140          false, // don't skip prompt for where to save
   2141          null, // cache key
   2142          isPrivate,
   2143          this.document.nodePrincipal /* system, because blob: */
   2144        );
   2145      }, console.error);
   2146    } else if (this.onImage) {
   2147      this.window.urlSecurityCheck(this.mediaURL, this.principal);
   2148      this.window.internalSave(
   2149        this.mediaURL,
   2150        null, // originalURL
   2151        null, // document
   2152        null, // file name; we'll take it from the URL
   2153        this.contentData.contentDisposition,
   2154        this.contentData.contentType,
   2155        false, // do not bypass the cache
   2156        "SaveImageTitle",
   2157        null, // chosen data
   2158        referrerInfo,
   2159        cookieJarSettings,
   2160        null, // initiating doc
   2161        false, // don't skip prompt for where to save
   2162        null, // cache key
   2163        isPrivate,
   2164        this.principal
   2165      );
   2166    } else if (this.onVideo || this.onAudio) {
   2167      let defaultFileName = "";
   2168      if (this.mediaURL.startsWith("data")) {
   2169        // Use default file name "Untitled" for data URIs
   2170        defaultFileName =
   2171          this.window.ContentAreaUtils.stringBundle.GetStringFromName(
   2172            "UntitledSaveFileName"
   2173          );
   2174      }
   2175 
   2176      var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
   2177      this.saveHelper(
   2178        this.mediaURL,
   2179        null,
   2180        dialogTitle,
   2181        false,
   2182        doc,
   2183        referrerInfo,
   2184        cookieJarSettings,
   2185        this.frameOuterWindowID,
   2186        defaultFileName,
   2187        isPrivate
   2188      );
   2189    }
   2190  }
   2191 
   2192  // Backwards-compatibility wrapper
   2193  sendImage() {
   2194    if (this.onCanvas || this.onImage) {
   2195      this.sendMedia();
   2196    }
   2197  }
   2198 
   2199  sendMedia() {
   2200    this.window.MailIntegration.sendMessage(this.mediaURL, "");
   2201  }
   2202 
   2203  // Generate email address and put it on clipboard.
   2204  copyEmail() {
   2205    // Copy the comma-separated list of email addresses only.
   2206    // There are other ways of embedding email addresses in a mailto:
   2207    // link, but such complex parsing is beyond us.
   2208    var url = this.linkURL;
   2209    var qmark = url.indexOf("?");
   2210    var addresses;
   2211 
   2212    // 7 == length of "mailto:"
   2213    addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);
   2214 
   2215    // Let's try to unescape it using a character set
   2216    // in case the address is not ASCII.
   2217    try {
   2218      addresses = Services.textToSubURI.unEscapeURIForUI(addresses);
   2219    } catch (ex) {
   2220      // Do nothing.
   2221    }
   2222 
   2223    lazy.clipboard.copyString(
   2224      addresses,
   2225      this.actor.manager.browsingContext.currentWindowGlobal
   2226    );
   2227  }
   2228 
   2229  // Extract phone and put it on clipboard
   2230  copyPhone() {
   2231    // Copies the phone number only. We won't be doing any complex parsing
   2232    var url = this.linkURL;
   2233    var phone = url.substr(4);
   2234 
   2235    // Let's try to unescape it using a character set
   2236    // in case the phone number is not ASCII.
   2237    try {
   2238      phone = Services.textToSubURI.unEscapeURIForUI(phone);
   2239    } catch (ex) {
   2240      // Do nothing.
   2241    }
   2242 
   2243    lazy.clipboard.copyString(
   2244      phone,
   2245      this.actor.manager.browsingContext.currentWindowGlobal
   2246    );
   2247  }
   2248 
   2249  copyLink(url = this.linkURL) {
   2250    // If we're in a view source tab, remove the view-source: prefix
   2251    let linkURL = url.replace(/^view-source:/, "");
   2252    lazy.clipboard.copyString(
   2253      linkURL,
   2254      this.actor.manager.browsingContext.currentWindowGlobal
   2255    );
   2256  }
   2257 
   2258  previewLink(_url = this.linkURL) {
   2259    // LinkPreview.sys.mjs is missing. Unexpected to reach here since
   2260    // #context-previewlink is hidden. tor-browser#44045.
   2261  }
   2262 
   2263  /**
   2264   * Copies a stripped version of a URI to the clipboard.
   2265   * 'Stripped' means that query parameters for tracking/ link decoration
   2266   * that are known to us will be removed from the URI.
   2267   */
   2268  copyStrippedLink(uri = this.linkURI) {
   2269    let strippedLinkURI = this.getStrippedLink(uri);
   2270    let strippedLinkURL =
   2271      Services.io.createExposableURI(strippedLinkURI)?.displaySpec;
   2272    if (strippedLinkURL) {
   2273      lazy.clipboard.copyString(
   2274        strippedLinkURL,
   2275        this.actor.manager.browsingContext.currentWindowGlobal
   2276      );
   2277    }
   2278  }
   2279 
   2280  async addSearchFieldAsEngine() {
   2281    let { url, formData, charset, method } =
   2282      await this.actor.getSearchFieldEngineData(this.targetIdentifier);
   2283 
   2284    for (let value of formData.values()) {
   2285      if (typeof value != "string") {
   2286        throw new Error("Non-string values are not supported.");
   2287      }
   2288    }
   2289 
   2290    let { engineInfo } = await this.window.gDialogBox.open(
   2291      "chrome://browser/content/search/addEngine.xhtml",
   2292      {
   2293        mode: "FORM",
   2294        title: true,
   2295        nameTemplate: Services.io.newURI(url).host,
   2296      }
   2297    );
   2298 
   2299    // If the user saved, engineInfo contains `name` and `alias`.
   2300    // Otherwise, it's undefined.
   2301    if (engineInfo) {
   2302      let searchEngine = await Services.search.addUserEngine({
   2303        name: engineInfo.name,
   2304        alias: engineInfo.alias,
   2305        url,
   2306        params: new URLSearchParams(formData),
   2307        charset,
   2308        method,
   2309      });
   2310 
   2311      this.window.gURLBar.search("", { searchEngine });
   2312    }
   2313  }
   2314 
   2315  /**
   2316   * Utilities
   2317   */
   2318 
   2319  /**
   2320   * Show/hide one item (specified via name or the item element itself).
   2321   * If the element is not found, then this function finishes silently.
   2322   *
   2323   * @param {Element | string} aItemOrId The item element or the name of the element
   2324   *                                   to show.
   2325   * @param {boolean} aShow Set to true to show the item, false to hide it.
   2326   */
   2327  showItem(aItemOrId, aShow) {
   2328    var item =
   2329      aItemOrId.constructor == String
   2330        ? this.document.getElementById(aItemOrId)
   2331        : aItemOrId;
   2332    if (item) {
   2333      item.hidden = !aShow;
   2334    }
   2335  }
   2336 
   2337  // Set given attribute of specified context-menu item.  If the
   2338  // value is null, then it removes the attribute (which works
   2339  // nicely for the disabled attribute).
   2340  setItemAttr(aID, aAttr, aVal) {
   2341    var elem = this.document.getElementById(aID);
   2342    if (!elem) {
   2343      return;
   2344    }
   2345    if (aVal == null) {
   2346      // null indicates attr should be removed.
   2347      elem.removeAttribute(aAttr);
   2348      return;
   2349    }
   2350    if (typeof aVal == "boolean") {
   2351      // TODO(emilio): Replace this with toggleAttribute, but needs test fixes.
   2352      if (aVal) {
   2353        elem.setAttribute(aAttr, aVal);
   2354      } else {
   2355        elem.removeAttribute(aAttr);
   2356      }
   2357      return;
   2358    }
   2359    // Set attr=val.
   2360    elem.setAttribute(aAttr, aVal);
   2361  }
   2362 
   2363  // Temporary workaround for DOM api not yet implemented by XUL nodes.
   2364  cloneNode(aItem) {
   2365    // Create another element like the one we're cloning.
   2366    var node = this.document.createElement(aItem.tagName);
   2367 
   2368    // Copy attributes from argument item to the new one.
   2369    var attrs = aItem.attributes;
   2370    for (var i = 0; i < attrs.length; i++) {
   2371      var attr = attrs.item(i);
   2372      node.setAttribute(attr.nodeName, attr.nodeValue);
   2373    }
   2374 
   2375    // Voila!
   2376    return node;
   2377  }
   2378 
   2379  getLinkURI(url = this.linkURL) {
   2380    try {
   2381      return this.window.makeURI(url);
   2382    } catch (ex) {
   2383      // e.g. empty URL string
   2384    }
   2385 
   2386    return null;
   2387  }
   2388 
   2389  /**
   2390   * Strips any known query params from the link URI.
   2391   *
   2392   * @returns {nsIURI|null} - the stripped version of the URI,
   2393   * or the original URI if we could not strip any query parameter.
   2394   */
   2395  getStrippedLink(uri = this.linkURI) {
   2396    if (!uri) {
   2397      return null;
   2398    }
   2399    let strippedLinkURI = null;
   2400    try {
   2401      strippedLinkURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
   2402    } catch (e) {
   2403      console.warn(`getStrippedLink: ${e.message}`);
   2404      return uri;
   2405    }
   2406 
   2407    // If nothing can be stripped, we return the original URI
   2408    // so the feature can still be used.
   2409    return strippedLinkURI ?? uri;
   2410  }
   2411 
   2412  /**
   2413   * Checks if there is a query parameter that can be stripped
   2414   *
   2415   * @returns {boolean}
   2416   */
   2417  #canStripParams(uri = this.linkURI) {
   2418    if (!uri) {
   2419      return false;
   2420    }
   2421    try {
   2422      return lazy.QueryStringStripper.canStripForShare(uri);
   2423    } catch (e) {
   2424      console.warn("canStripForShare failed!", e);
   2425      return false;
   2426    }
   2427  }
   2428 
   2429  /**
   2430   * Checks if a webpage is a secure interal webpage
   2431   *
   2432   * @returns {boolean}
   2433   */
   2434  isSecureAboutPage() {
   2435    let { currentURI } = this.browser;
   2436    if (currentURI?.schemeIs("about")) {
   2437      let module = lazy.E10SUtils.getAboutModule(currentURI);
   2438      if (module) {
   2439        let flags = module.getURIFlags(currentURI);
   2440        return !!(flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI);
   2441      }
   2442    }
   2443    return false;
   2444  }
   2445 
   2446  // Kept for addon compat
   2447  linkText() {
   2448    return this.linkTextStr;
   2449  }
   2450 
   2451  // Determines whether or not the separator with the specified ID should be
   2452  // shown or not by determining if there are any non-hidden items between it
   2453  // and the previous separator.
   2454  shouldShowSeparator(aSeparatorID) {
   2455    var separator = this.document.getElementById(aSeparatorID);
   2456    if (separator) {
   2457      var sibling = separator.previousSibling;
   2458      while (sibling && sibling.localName != "menuseparator") {
   2459        if (!sibling.hidden) {
   2460          return true;
   2461        }
   2462        sibling = sibling.previousSibling;
   2463      }
   2464    }
   2465    return false;
   2466  }
   2467 
   2468  shouldShowAddEngine() {
   2469    let uri = this.browser.currentURI;
   2470 
   2471    return (
   2472      this.onTextInput &&
   2473      this.onSearchField &&
   2474      !this.isLoginForm() &&
   2475      (uri.schemeIs("http") || uri.schemeIs("https"))
   2476    );
   2477  }
   2478 
   2479  addDictionaries() {
   2480    var uri = Services.urlFormatter.formatURLPref(
   2481      "browser.dictionaries.download.url"
   2482    );
   2483 
   2484    var locale = "-";
   2485    try {
   2486      locale = Services.locale.acceptLanguages;
   2487    } catch (e) {}
   2488 
   2489    var version = "-";
   2490    try {
   2491      version = Services.appinfo.version;
   2492    } catch (e) {}
   2493 
   2494    uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
   2495 
   2496    var newWindowPref = Services.prefs.getIntPref(
   2497      "browser.link.open_newwindow"
   2498    );
   2499    var where = newWindowPref == 3 ? "tab" : "window";
   2500 
   2501    this.window.openTrustedLinkIn(uri, where);
   2502  }
   2503 
   2504  bookmarkThisPage() {
   2505    this.window.top.PlacesCommandHook.bookmarkPage().catch(console.error);
   2506  }
   2507 
   2508  bookmarkLink() {
   2509    this.window.top.PlacesCommandHook.bookmarkLink(
   2510      this.linkURL,
   2511      this.linkTextStr
   2512    ).catch(console.error);
   2513  }
   2514 
   2515  addBookmarkForFrame() {
   2516    let uri = this.contentData.documentURIObject;
   2517 
   2518    this.actor.getFrameTitle(this.targetIdentifier).then(title => {
   2519      this.window.top.PlacesCommandHook.bookmarkLink(uri.spec, title).catch(
   2520        console.error
   2521      );
   2522    });
   2523  }
   2524 
   2525  savePageAs() {
   2526    this.window.saveBrowser(this.browser);
   2527  }
   2528 
   2529  printFrame() {
   2530    this.window.PrintUtils.startPrintWindow(this.actor.browsingContext, {
   2531      printFrameOnly: true,
   2532    });
   2533  }
   2534 
   2535  printSelection() {
   2536    this.window.PrintUtils.startPrintWindow(this.actor.browsingContext, {
   2537      printSelectionOnly: true,
   2538    });
   2539  }
   2540 
   2541  switchPageDirection() {
   2542    this.window.gBrowser.selectedBrowser.sendMessageToActor(
   2543      "SwitchDocumentDirection",
   2544      {},
   2545      "SwitchDocumentDirection",
   2546      "roots"
   2547    );
   2548  }
   2549 
   2550  mediaCommand(command, data) {
   2551    this.actor.mediaCommand(this.targetIdentifier, command, data);
   2552  }
   2553 
   2554  copyMediaLocation() {
   2555    lazy.clipboard.copyString(
   2556      this.originalMediaURL,
   2557      this.actor.manager.browsingContext.currentWindowGlobal
   2558    );
   2559  }
   2560 
   2561  getImageText() {
   2562    let dialogBox = this.window.gBrowser.getTabDialogBox(this.browser);
   2563    const imageTextResult = this.actor.getImageText(this.targetIdentifier);
   2564    let timerId = Glean.textRecognition.apiPerformance.start();
   2565    const { dialog } = dialogBox.open(
   2566      "chrome://browser/content/textrecognition/textrecognition.html",
   2567      {
   2568        features: "resizable=no",
   2569        modalType: Services.prompt.MODAL_TYPE_CONTENT,
   2570      },
   2571      imageTextResult,
   2572      () => dialog.resizeVertically(),
   2573      this.window.openLinkIn,
   2574      timerId
   2575    );
   2576  }
   2577 
   2578  drmLearnMore(aEvent) {
   2579    let drmInfoURL =
   2580      Services.urlFormatter.formatURLPref("app.support.baseURL") +
   2581      "drm-content";
   2582    let dest = lazy.BrowserUtils.whereToOpenLink(aEvent);
   2583    // Don't ever want this to open in the same tab as it'll unload the
   2584    // DRM'd video, which is going to be a bad idea in most cases.
   2585    if (dest == "current") {
   2586      dest = "tab";
   2587    }
   2588    this.window.openTrustedLinkIn(drmInfoURL, dest);
   2589  }
   2590 
   2591  /**
   2592   * Opens the SelectTranslationsPanel singleton instance.
   2593   *
   2594   * @param {Event} event - The triggering event for opening the panel.
   2595   */
   2596  openSelectTranslationsPanel(event) {
   2597    const context = this.contentData.context;
   2598    let screenX = context.screenXDevPx / this.window.devicePixelRatio;
   2599    let screenY = context.screenYDevPx / this.window.devicePixelRatio;
   2600    this.window.SelectTranslationsPanel.open(
   2601      event,
   2602      screenX,
   2603      screenY,
   2604      this.#getTextToTranslate(),
   2605      this.isTextSelected,
   2606      this.#translationsLangPairPromise
   2607    ).catch(console.error);
   2608  }
   2609 
   2610  /**
   2611   * Localizes the translate-selection menuitem.
   2612   *
   2613   * The item will either be localized with a target language's display name
   2614   * or localized in a generic way without a target language.
   2615   *
   2616   * @param {Element} translateSelectionItem
   2617   * @returns {Promise<void>}
   2618   */
   2619  async localizeTranslateSelectionItem(translateSelectionItem) {
   2620    const { targetLanguage } = await this.#translationsLangPairPromise;
   2621 
   2622    if (targetLanguage) {
   2623      // A valid to-language exists, so localize the menuitem for that language.
   2624      let displayName;
   2625 
   2626      try {
   2627        const languageDisplayNames =
   2628          lazy.TranslationsParent.createLanguageDisplayNames();
   2629        displayName = languageDisplayNames.of(targetLanguage);
   2630      } catch {
   2631        // languageDisplayNames.of threw, do nothing.
   2632      }
   2633 
   2634      if (displayName) {
   2635        translateSelectionItem.setAttribute("target-language", targetLanguage);
   2636        this.document.l10n.setAttributes(
   2637          translateSelectionItem,
   2638          this.isTextSelected
   2639            ? "main-context-menu-translate-selection-to-language"
   2640            : "main-context-menu-translate-link-text-to-language",
   2641          { language: displayName }
   2642        );
   2643        return;
   2644      }
   2645    }
   2646 
   2647    // Either no to-language exists, or an error occurred,
   2648    // so localize the menuitem without a target language.
   2649    translateSelectionItem.removeAttribute("target-language");
   2650    this.document.l10n.setAttributes(
   2651      translateSelectionItem,
   2652      this.isTextSelected
   2653        ? "main-context-menu-translate-selection"
   2654        : "main-context-menu-translate-link-text"
   2655    );
   2656  }
   2657 
   2658  /**
   2659   * Fetches text for translation, prioritizing selected text over link text.
   2660   *
   2661   * @returns {string} The text to translate.
   2662   */
   2663  #getTextToTranslate() {
   2664    if (this.isTextSelected) {
   2665      // If there is an active selection, we will always offer to translate.
   2666      return this.selectionInfo.fullText.trim();
   2667    }
   2668 
   2669    const linkText = this.linkTextStr.trim();
   2670    if (!linkText) {
   2671      // There was no underlying link text, so do not offer to translate.
   2672      return "";
   2673    }
   2674 
   2675    if (URL.canParse(linkText)) {
   2676      // The underlying link text is a URL, we should not offer to translate.
   2677      return "";
   2678    }
   2679 
   2680    // Since the underlying link text is not a URL, we should offer to translate it.
   2681    return linkText;
   2682  }
   2683 
   2684  /**
   2685   * Displays or hides the translate-selection item in the context menu.
   2686   */
   2687  showTranslateSelectionItem() {
   2688    const translateSelectionItem = this.document.getElementById(
   2689      "context-translate-selection"
   2690    );
   2691    const translationsEnabled = Services.prefs.getBoolPref(
   2692      "browser.translations.enable"
   2693    );
   2694    const selectTranslationsEnabled = Services.prefs.getBoolPref(
   2695      "browser.translations.select.enable"
   2696    );
   2697 
   2698    const textToTranslate = this.#getTextToTranslate();
   2699 
   2700    translateSelectionItem.hidden =
   2701      // Only show the item if the feature is enabled.
   2702      !(translationsEnabled && selectTranslationsEnabled) ||
   2703      // Only show the item if Translations is supported on this hardware.
   2704      !lazy.TranslationsParent.getIsTranslationsEngineSupported() ||
   2705      // If there is no text to translate, we have nothing to do.
   2706      textToTranslate.length === 0;
   2707 
   2708    if (translateSelectionItem.hidden) {
   2709      translateSelectionItem.removeAttribute("target-language");
   2710      return;
   2711    }
   2712 
   2713    this.#translationsLangPairPromise =
   2714      this.window.SelectTranslationsPanel.getLangPairPromise(textToTranslate);
   2715    this.localizeTranslateSelectionItem(translateSelectionItem);
   2716  }
   2717 
   2718  // Formats the 'Search <engine> for "<selection or link text>"' context menu.
   2719  showAndFormatSearchContextItem() {
   2720    let selectedText = this.isTextSelected
   2721      ? this.selectedText
   2722      : this.linkTextStr;
   2723 
   2724    let { document } = this.window;
   2725    let menuItem = document.getElementById("context-searchselect");
   2726    let menuItemPrivate = document.getElementById(
   2727      "context-searchselect-private"
   2728    );
   2729 
   2730    let opts = {
   2731      isContextRelevant: (this.isTextSelected || this.onLink) && !this.onImage,
   2732      searchTerms: selectedText,
   2733      searchUrlType: lazy.SearchUtils.URL_TYPE.SEARCH,
   2734    };
   2735    this.#updateSearchMenuitem({
   2736      ...opts,
   2737      menuitem: menuItem,
   2738    });
   2739    this.#updateSearchMenuitem({
   2740      ...opts,
   2741      menuitem: menuItemPrivate,
   2742      isPrivateSearchMenuitem: true,
   2743    });
   2744 
   2745    let frameSeparator = document.getElementById("frame-sep");
   2746 
   2747    // Add a divider between "Search X for Y" and "This Frame", and between
   2748    // "Search X for Y" and "Check Spelling", but no divider in other cases.
   2749    frameSeparator.toggleAttribute(
   2750      "ensureHidden",
   2751      menuItem.hidden && this.inFrame
   2752    );
   2753 
   2754    // If we're not showing the menu items, we can skip formatting the labels.
   2755    if (menuItem.hidden && menuItemPrivate.hidden) {
   2756      return;
   2757    }
   2758 
   2759    // Copied to alert.js' prefillAlertInfo().
   2760    // If the JS character after our truncation point is a trail surrogate,
   2761    // include it in the truncated string to avoid splitting a surrogate pair.
   2762    if (selectedText.length > 15) {
   2763      let truncLength = 15;
   2764      let truncChar = selectedText[15].charCodeAt(0);
   2765      if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
   2766        truncLength++;
   2767      }
   2768      selectedText =
   2769        selectedText.substr(0, truncLength) + Services.locale.ellipsis;
   2770    }
   2771 
   2772    const { gNavigatorBundle } = this.window;
   2773    // format "Search <engine> for <selection>" string to show in menu
   2774    let engineName = Services.search.defaultEngine.name;
   2775    let privateEngineName = Services.search.defaultPrivateEngine.name;
   2776    if (!menuItem.hidden) {
   2777      const docIsPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
   2778        this.browser
   2779      );
   2780 
   2781      let menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", [
   2782        docIsPrivate ? privateEngineName : engineName,
   2783        selectedText,
   2784      ]);
   2785      menuItem.label = menuLabel;
   2786      menuItem.accessKey = gNavigatorBundle.getString(
   2787        "contextMenuSearch.accesskey"
   2788      );
   2789    }
   2790 
   2791    if (!menuItemPrivate.hidden) {
   2792      let otherEngine = engineName != privateEngineName;
   2793      let accessKey = "contextMenuPrivateSearch.accesskey";
   2794      if (otherEngine) {
   2795        menuItemPrivate.label = gNavigatorBundle.getFormattedString(
   2796          "contextMenuPrivateSearchOtherEngine",
   2797          [privateEngineName]
   2798        );
   2799        accessKey = "contextMenuPrivateSearchOtherEngine.accesskey";
   2800      } else {
   2801        menuItemPrivate.label = gNavigatorBundle.getString(
   2802          "contextMenuPrivateSearch"
   2803        );
   2804      }
   2805      menuItemPrivate.accessKey = gNavigatorBundle.getString(accessKey);
   2806    }
   2807  }
   2808 
   2809  #updateSearchMenuitem({
   2810    menuitem,
   2811    isContextRelevant,
   2812    searchTerms,
   2813    searchUrlType,
   2814    isPrivateSearchMenuitem = false,
   2815  }) {
   2816    if (!menuitem) {
   2817      return;
   2818    }
   2819    if (!Services.search.hasSuccessfullyInitialized) {
   2820      menuitem.hidden = true;
   2821      return;
   2822    }
   2823 
   2824    if (isPrivateSearchMenuitem && !lazy.PrivateBrowsingUtils.enabled) {
   2825      menuitem.hidden = true;
   2826      return;
   2827    }
   2828 
   2829    let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
   2830      this.browser
   2831    );
   2832    let engine =
   2833      isBrowserPrivate || isPrivateSearchMenuitem
   2834        ? Services.search.defaultPrivateEngine
   2835        : Services.search.defaultEngine;
   2836 
   2837    menuitem.hidden =
   2838      !isContextRelevant ||
   2839      this.inAboutDevtoolsToolbox ||
   2840      !engine?.supportsResponseType(searchUrlType) ||
   2841      // Don't show the private search item when we're already in a private
   2842      // browsing window.
   2843      (isPrivateSearchMenuitem &&
   2844        (isBrowserPrivate ||
   2845          !Services.prefs.getBoolPref(
   2846            "browser.search.separatePrivateDefault.ui.enabled"
   2847          )));
   2848 
   2849    if (!menuitem.hidden) {
   2850      let url = engine.wrappedJSObject.getURLOfType(searchUrlType);
   2851      if (
   2852        url?.acceptedContentTypes &&
   2853        (!this.contentData?.contentType ||
   2854          !url.acceptedContentTypes.includes(this.contentData.contentType))
   2855      ) {
   2856        menuitem.hidden = true;
   2857      }
   2858    }
   2859 
   2860    if (!menuitem.hidden) {
   2861      menuitem.engine = engine;
   2862      menuitem.searchTerms = searchTerms;
   2863      menuitem.principal = this.principal;
   2864      menuitem.policyContainer = this.policyContainer;
   2865      menuitem.usePrivate = isPrivateSearchMenuitem || isBrowserPrivate;
   2866    }
   2867  }
   2868 
   2869  /**
   2870   * Shows or hides as appropriate the visual search context menu item:
   2871   * "Search Image with {engine}".
   2872   */
   2873  showAndFormatVisualSearchContextItem() {
   2874    let menuitem = this.window.document.getElementById("context-visual-search");
   2875    this.#updateSearchMenuitem({
   2876      menuitem,
   2877      isContextRelevant:
   2878        this.onImage &&
   2879        this.imageInfo?.currentSrc &&
   2880        // Google Lens seems not to support images encoded as data URIs on its
   2881        // GET endpoint, so we hide the visual search item for them. If we ever
   2882        // add support for its POST endpoint or another visual engine that does
   2883        // support data URIs, we should revisit this.
   2884        !this.imageInfo.currentSrc.startsWith("data:") &&
   2885        !this.contentData.contentDisposition?.startsWith("attachment"),
   2886      searchTerms: this.imageInfo?.currentSrc,
   2887      searchUrlType: lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH,
   2888    });
   2889 
   2890    if (!menuitem.hidden) {
   2891      // Record the Nimbus exposure if the menu item is shown *or would have
   2892      // been shown* if the feature were enabled.
   2893      lazy.NimbusFeatures.search.recordExposureEvent();
   2894 
   2895      // If the feature is not enabled, hide the menu item.
   2896      if (
   2897        !Services.prefs.getBoolPref("browser.search.visualSearch.featureGate")
   2898      ) {
   2899        menuitem.hidden = true;
   2900        return;
   2901      }
   2902 
   2903      let visualSearchUrl = menuitem.engine.wrappedJSObject.getURLOfType(
   2904        lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH
   2905      );
   2906      this.window.document.l10n.setAttributes(
   2907        menuitem,
   2908        "main-context-menu-visual-search-2",
   2909        {
   2910          engine: visualSearchUrl.displayName || menuitem.engine.name,
   2911        }
   2912      );
   2913      this.#setNewFeatureBadge(menuitem, visualSearchUrl.isNew());
   2914      lazy.BrowserSearchTelemetry.recordSapImpression(
   2915        this.browser,
   2916        menuitem.engine,
   2917        "contextmenu_visual"
   2918      );
   2919    }
   2920  }
   2921 
   2922  /**
   2923   * Loads a search engine SERP based on the data that this class previously
   2924   * attached to `event.target`, which is expected to be a context menu item.
   2925   *
   2926   * @param {object} options
   2927   *   Options objects.
   2928   * @param {Event} options.event
   2929   *   The event on a context menu item that triggered the search.
   2930   * @param {SearchUtils.URL_TYPE} options.searchUrlType
   2931   *   A `SearchUtils.URL_TYPE` value indicating the type of search that should
   2932   *   be performed. A falsey value is equivalent to
   2933   *   `SearchUtils.URL_TYPE.SEARCH` and will perform a usual web search.
   2934   */
   2935  loadSearch({ event, searchUrlType = null }) {
   2936    let { engine, searchTerms, usePrivate, principal, policyContainer } =
   2937      event.target;
   2938    lazy.SearchUIUtils.loadSearchFromContext({
   2939      event,
   2940      engine,
   2941      policyContainer,
   2942      searchUrlType,
   2943      usePrivateWindow: usePrivate,
   2944      window: this.window,
   2945      searchText: searchTerms,
   2946      triggeringPrincipal: principal,
   2947    });
   2948  }
   2949 
   2950  createContainerMenu(aEvent) {
   2951    let createMenuOptions = {
   2952      isContextMenu: true,
   2953      excludeUserContextId: this.contentData.userContextId,
   2954    };
   2955    return this.window.createUserContextMenu(aEvent, createMenuOptions);
   2956  }
   2957 
   2958  /**
   2959   * Sets or removes the `badge` attribute on a menuitem. If it should be set,
   2960   * it will be set to the value of the `main-context-menu-new-feature-badge`
   2961   * l10n string. If the string has already been cached, the badge is set
   2962   * synchronously, so there won't be any visual pop-in. Otherwise the string is
   2963   * first fetched and cached, and then the badge is set asynchronously.
   2964   *
   2965   * This method is async but only for ease of implementation. It doesn't need
   2966   * to be awaited unless you need to block until the badge is set.
   2967   *
   2968   * @param {Element}
   2969   *   The menuitem that should be badged.
   2970   */
   2971  async #setNewFeatureBadge(menuitem, shouldShow) {
   2972    menuitem.classList.toggle("badge-new", shouldShow);
   2973 
   2974    if (!shouldShow) {
   2975      menuitem.removeAttribute("badge");
   2976      return;
   2977    }
   2978 
   2979    if (this.#newFeatureBadgeL10nString) {
   2980      menuitem.setAttribute("badge", this.#newFeatureBadgeL10nString);
   2981      return;
   2982    }
   2983 
   2984    let value = await this.window.document.l10n.formatValue(
   2985      "main-context-menu-new-feature-badge"
   2986    );
   2987    if (value) {
   2988      this.#newFeatureBadgeL10nString = value;
   2989      this.#setNewFeatureBadge(menuitem, shouldShow);
   2990    }
   2991  }
   2992 }