tor-browser

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

UrlbarInput.mjs (205352B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const { XPCOMUtils } = ChromeUtils.importESModule(
      6  "resource://gre/modules/XPCOMUtils.sys.mjs"
      7 );
      8 
      9 const { AppConstants } = ChromeUtils.importESModule(
     10  "resource://gre/modules/AppConstants.sys.mjs"
     11 );
     12 
     13 /**
     14 * @import {UrlbarSearchOneOffs} from "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs"
     15 */
     16 
     17 const lazy = XPCOMUtils.declareLazy({
     18  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     19  BrowserSearchTelemetry:
     20    "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
     21  BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
     22  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     23  ExtensionSearchHandler:
     24    "resource://gre/modules/ExtensionSearchHandler.sys.mjs",
     25  ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
     26  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
     27  PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
     28  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     29  ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
     30  SearchModeSwitcher:
     31    "moz-src:///browser/components/urlbar/SearchModeSwitcher.sys.mjs",
     32  SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
     33  SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
     34  UrlbarController:
     35    "moz-src:///browser/components/urlbar/UrlbarController.sys.mjs",
     36  UrlbarEventBufferer:
     37    "moz-src:///browser/components/urlbar/UrlbarEventBufferer.sys.mjs",
     38  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     39  UrlbarQueryContext:
     40    "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     41  UrlbarProviderGlobalActions:
     42    "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs",
     43  UrlbarProviderOpenTabs:
     44    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     45  UrlbarSearchUtils:
     46    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     47  UrlbarTokenizer:
     48    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     49  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     50  UrlbarValueFormatter:
     51    "moz-src:///browser/components/urlbar/UrlbarValueFormatter.sys.mjs",
     52  UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs",
     53  UrlbarSearchTermsPersistence:
     54    "moz-src:///browser/components/urlbar/UrlbarSearchTermsPersistence.sys.mjs",
     55  UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
     56  ClipboardHelper: {
     57    service: "@mozilla.org/widget/clipboardhelper;1",
     58    iid: Ci.nsIClipboardHelper,
     59  },
     60  QueryStringStripper: {
     61    service: "@mozilla.org/url-query-string-stripper;1",
     62    iid: Ci.nsIURLQueryStringStripper,
     63  },
     64  QUERY_STRIPPING_STRIP_ON_SHARE: {
     65    pref: "privacy.query_stripping.strip_on_share.enabled",
     66    default: false,
     67  },
     68  logger: () => lazy.UrlbarUtils.getLogger({ prefix: "Input" }),
     69 });
     70 
     71 const UNLIMITED_MAX_RESULTS = 99;
     72 
     73 let getBoundsWithoutFlushing = element =>
     74  element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
     75 let px = number => number.toFixed(2) + "px";
     76 
     77 /**
     78 * Implements the text input part of the address bar UI.
     79 */
     80 export class UrlbarInput extends HTMLElement {
     81  static get #markup() {
     82    return `
     83      <hbox class="urlbar-background"/>
     84      <hbox class="urlbar-input-container"
     85            flex="1"
     86            pageproxystate="invalid">
     87        <moz-urlbar-slot name="remote-control-box"> </moz-urlbar-slot>
     88        <toolbarbutton id="urlbar-searchmode-switcher"
     89                       class="searchmode-switcher chromeclass-toolbar-additional"
     90                       align="center"
     91                       aria-expanded="false"
     92                       aria-haspopup="menu"
     93                       tooltip="dynamic-shortcut-tooltip"
     94                       data-l10n-id="urlbar-searchmode-default"
     95                       type="menu">
     96          <image class="searchmode-switcher-icon toolbarbutton-icon"/>
     97          <image class="searchmode-switcher-dropmarker toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker"
     98                 data-l10n-id="urlbar-searchmode-dropmarker" />
     99          <menupopup class="searchmode-switcher-popup toolbar-menupopup"
    100                     consumeoutsideclicks="false">
    101            <label class="searchmode-switcher-popup-description"
    102                   role="heading" />
    103            <menuseparator/>
    104            <menuseparator class="searchmode-switcher-popup-footer-separator"/>
    105            <menuitem class="searchmode-switcher-popup-search-settings-button menuitem-iconic"
    106                      data-action="openpreferences"
    107                      image="chrome://global/skin/icons/settings.svg"
    108                      data-l10n-id="urlbar-searchmode-popup-search-settings-menuitem"/>
    109          </menupopup>
    110        </toolbarbutton>
    111        <box class="searchmode-switcher-chicklet">
    112          <label class="searchmode-switcher-title" />
    113          <toolbarbutton class="searchmode-switcher-close toolbarbutton-icon close-button"
    114                         data-action="exitsearchmode"
    115                         role="button"
    116                         data-l10n-id="urlbar-searchmode-exit-button" />
    117        </box>
    118        <moz-urlbar-slot name="site-info"> </moz-urlbar-slot>
    119        <moz-input-box tooltip="aHTMLTooltip"
    120                       class="urlbar-input-box"
    121                       flex="1"
    122                       role="combobox"
    123                       aria-owns="urlbar-results">
    124          <html:input id="urlbar-scheme"
    125                      required="required"/>
    126          <html:input id="urlbar-input"
    127                      class="urlbar-input textbox-input"
    128                      aria-controls="urlbar-results"
    129                      aria-autocomplete="both"
    130                      inputmode="mozAwesomebar"
    131                      data-l10n-id="urlbar-placeholder"/>
    132        </moz-input-box>
    133        <moz-urlbar-slot name="revert-button"> </moz-urlbar-slot>
    134        <image class="urlbar-icon urlbar-go-button"
    135               role="button"
    136               data-l10n-id="urlbar-go-button"/>
    137        <moz-urlbar-slot name="page-actions" hidden=""> </moz-urlbar-slot>
    138      </hbox>
    139      <vbox class="urlbarView"
    140            context=""
    141            role="group"
    142            tooltip="aHTMLTooltip">
    143        <html:div class="urlbarView-body-outer">
    144          <html:div class="urlbarView-body-inner">
    145            <html:div id="urlbar-results"
    146                      class="urlbarView-results"
    147                      role="listbox"/>
    148          </html:div>
    149        </html:div>
    150        <menupopup class="urlbarView-result-menu"
    151                   consumeoutsideclicks="false"/>
    152        <hbox class="search-one-offs"
    153              includecurrentengine="true"
    154              disabletab="true"/>
    155      </vbox>`;
    156  }
    157 
    158  /** @type {DocumentFragment} */
    159  static get fragment() {
    160    if (!UrlbarInput.#fragment) {
    161      UrlbarInput.#fragment = window.MozXULElement.parseXULToFragment(
    162        UrlbarInput.#markup
    163      );
    164    }
    165    // @ts-ignore
    166    return document.importNode(UrlbarInput.#fragment, true);
    167  }
    168 
    169  /**
    170   * @type {DocumentFragment=}
    171   *
    172   * The cached fragment.
    173   */
    174  static #fragment;
    175 
    176  static #inputFieldEvents = [
    177    "compositionstart",
    178    "compositionend",
    179    "contextmenu",
    180    "dragover",
    181    "dragstart",
    182    "drop",
    183    "focus",
    184    "blur",
    185    "input",
    186    "beforeinput",
    187    "keydown",
    188    "keyup",
    189    "mouseover",
    190    "overflow",
    191    "underflow",
    192    "paste",
    193    "scrollend",
    194    "select",
    195    "selectionchange",
    196  ];
    197 
    198  #allowBreakout = false;
    199  #gBrowserListenersAdded = false;
    200  #breakoutBlockerCount = 0;
    201  #isAddressbar = false;
    202  #sapName = "";
    203  _userTypedValue = "";
    204  _actionOverrideKeyCount = 0;
    205  _lastValidURLStr = "";
    206  _valueOnLastSearch = "";
    207  _suppressStartQuery = false;
    208  _suppressPrimaryAdjustment = false;
    209  _lastSearchString = "";
    210  // Tracks IME composition.
    211  #compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
    212  #compositionClosedPopup = false;
    213 
    214  valueIsTyped = false;
    215 
    216  // Properties accessed in tests.
    217  lastQueryContextPromise = Promise.resolve();
    218  _autofillPlaceholder = null;
    219  _resultForCurrentValue = null;
    220  _untrimmedValue = "";
    221  _enableAutofillPlaceholder = true;
    222 
    223  constructor() {
    224    super();
    225 
    226    this.window = this.ownerGlobal;
    227    this.document = this.window.document;
    228    this.isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(this.window);
    229 
    230    lazy.UrlbarPrefs.addObserver(this);
    231    window.addEventListener("unload", () => {
    232      // Stop listening to pref changes to make sure we don't init the new
    233      // searchbar in closed windows that have not been gc'd yet.
    234      lazy.UrlbarPrefs.removeObserver(this);
    235    });
    236  }
    237 
    238  /**
    239   * Populates moz-urlbar-slots by moving all children with a urlbar-slot
    240   * attribute into their moz-urlbar-slots and removing the slots.
    241   *
    242   * Should only be called once all children have been parsed.
    243   */
    244  #populateSlots() {
    245    let urlbarSlots = this.querySelectorAll("moz-urlbar-slot[name]");
    246    for (let slot of urlbarSlots) {
    247      let slotName = slot.getAttribute("name");
    248      let nodes = this.querySelectorAll(`:scope > [urlbar-slot="${slotName}"]`);
    249 
    250      for (let node of nodes) {
    251        slot.parentNode.insertBefore(node, slot);
    252      }
    253 
    254      slot.remove();
    255    }
    256 
    257    // Slotted elements only used by the addressbar.
    258    // Will be null for searchbar and others.
    259    this._identityBox = this.querySelector(".identity-box");
    260    this._revertButton = this.querySelector(".urlbar-revert-button");
    261    // Pre scotch bonnet search mode indicator (addressbar only).
    262    this._searchModeIndicator = this.querySelector(
    263      "#urlbar-search-mode-indicator"
    264    );
    265    this._searchModeIndicatorTitle = this._searchModeIndicator?.querySelector(
    266      "#urlbar-search-mode-indicator-title"
    267    );
    268    this._searchModeIndicatorClose = this._searchModeIndicator?.querySelector(
    269      "#urlbar-search-mode-indicator-close"
    270    );
    271  }
    272 
    273  /**
    274   * Initialization that happens once on the first connect.
    275   */
    276  #initOnce() {
    277    this.#sapName = this.getAttribute("sap-name");
    278    this.#isAddressbar = this.#sapName == "urlbar";
    279 
    280    // This listener must be added before connecting the fragment
    281    // because the event could fire while or after connecting it.
    282    this.addEventListener(
    283      "moz-input-box-rebuilt",
    284      this.#onContextMenuRebuilt.bind(this)
    285    );
    286 
    287    this.appendChild(UrlbarInput.fragment);
    288 
    289    // Make sure all children have been parsed before calling #populateSlots.
    290    if (document.readyState === "loading") {
    291      document.addEventListener(
    292        "DOMContentLoaded",
    293        () => this.#populateSlots(),
    294        { once: true }
    295      );
    296    } else {
    297      this.#populateSlots();
    298    }
    299 
    300    this.panel = this.querySelector(".urlbarView");
    301    this.inputField = /** @type {HTMLInputElement} */ (
    302      this.querySelector(".urlbar-input")
    303    );
    304    if (this.#sapName == "searchbar") {
    305      // This adds a native clear button.
    306      this.inputField.setAttribute("type", "search");
    307    }
    308    this._inputContainer = this.querySelector(".urlbar-input-container");
    309 
    310    this.controller = new lazy.UrlbarController({ input: this });
    311    this.view = new lazy.UrlbarView(this);
    312    this.searchModeSwitcher = new lazy.SearchModeSwitcher(this);
    313 
    314    let searchModeSwitcherDescription = this.querySelector(
    315      ".searchmode-switcher-popup-description"
    316    );
    317    searchModeSwitcherDescription.setAttribute(
    318      "data-l10n-id",
    319      this.#isAddressbar
    320        ? "urlbar-searchmode-popup-description"
    321        : "urlbar-searchmode-popup-sticky-description"
    322    );
    323 
    324    // The event bufferer can be used to defer events that may affect users
    325    // muscle memory; for example quickly pressing DOWN+ENTER should end up
    326    // on a predictable result, regardless of the search status. The event
    327    // bufferer will invoke the handling code at the right time.
    328    this.eventBufferer = new lazy.UrlbarEventBufferer(this);
    329 
    330    // Forward certain properties.
    331    // Note if you are extending these, you'll also need to extend the inline
    332    // type definitions.
    333    const READ_WRITE_PROPERTIES = [
    334      "placeholder",
    335      "readOnly",
    336      "selectionStart",
    337      "selectionEnd",
    338    ];
    339 
    340    for (let property of READ_WRITE_PROPERTIES) {
    341      Object.defineProperty(this, property, {
    342        enumerable: true,
    343        get() {
    344          return this.inputField[property];
    345        },
    346        set(val) {
    347          this.inputField[property] = val;
    348        },
    349      });
    350    }
    351 
    352    // The engine name is not known yet, but update placeholder anyway to
    353    // reflect value of keyword.enabled or set the searchbar placeholder.
    354    this._setPlaceholder(null);
    355  }
    356 
    357  connectedCallback() {
    358    if (
    359      this.getAttribute("sap-name") == "searchbar" &&
    360      !lazy.UrlbarPrefs.get("browser.search.widget.new")
    361    ) {
    362      return;
    363    }
    364 
    365    this.#init();
    366  }
    367 
    368  #init() {
    369    if (!this.controller) {
    370      this.#initOnce();
    371    }
    372 
    373    if (this.sapName == "searchbar") {
    374      this.parentNode.setAttribute("overflows", "false");
    375    }
    376 
    377    // Don't attach event listeners if the toolbar is not visible
    378    // in this window or the urlbar is readonly.
    379    if (
    380      !this.window.toolbar.visible ||
    381      this.window.document.documentElement.hasAttribute("taskbartab") ||
    382      this.readOnly
    383    ) {
    384      return;
    385    }
    386 
    387    this._initCopyCutController();
    388 
    389    for (let event of UrlbarInput.#inputFieldEvents) {
    390      this.inputField.addEventListener(event, this);
    391    }
    392 
    393    // These are on the window to detect focusing shortcuts like F6.
    394    this.window.addEventListener("keydown", this);
    395    this.window.addEventListener("keyup", this);
    396 
    397    this.window.addEventListener("mousedown", this);
    398    if (AppConstants.platform == "win") {
    399      this.window.addEventListener("draggableregionleftmousedown", this);
    400    }
    401    this.addEventListener("mousedown", this);
    402 
    403    // This listener handles clicks from our children too, included the search mode
    404    // indicator close button.
    405    this._inputContainer.addEventListener("click", this);
    406 
    407    // This is used to detect commands launched from the panel, to avoid
    408    // recording abandonment events when the command causes a blur event.
    409    this.view.panel.addEventListener("command", this, true);
    410 
    411    this.window.addEventListener("customizationstarting", this);
    412    this.window.addEventListener("aftercustomization", this);
    413    this.window.addEventListener("toolbarvisibilitychange", this);
    414    let menuToolbar = this.window.document.getElementById("toolbar-menubar");
    415    if (menuToolbar) {
    416      menuToolbar.addEventListener("DOMMenuBarInactive", this);
    417      menuToolbar.addEventListener("DOMMenuBarActive", this);
    418    }
    419 
    420    if (this.window.gBrowser) {
    421      // On startup, this will be called again by browser-init.js
    422      // once gBrowser has been initialized.
    423      this.addGBrowserListeners();
    424    }
    425 
    426    // If the search service is not initialized yet, the placeholder
    427    // and icon will be updated in delayedStartupInit.
    428    if (
    429      Cu.isESModuleLoaded("resource://gre/modules/SearchService.sys.mjs") &&
    430      Services.search.isInitialized
    431    ) {
    432      this.searchModeSwitcher.updateSearchIcon();
    433      this._updatePlaceholderFromDefaultEngine();
    434    }
    435 
    436    // Expanding requires a parent toolbar, and us not being read-only.
    437    this.#allowBreakout = !!this.closest("toolbar");
    438    if (this.#allowBreakout) {
    439      // TODO(emilio): This could use CSS anchor positioning rather than this
    440      // ResizeObserver, eventually.
    441      this._resizeObserver = new this.window.ResizeObserver(([entry]) => {
    442        this.style.setProperty(
    443          "--urlbar-width",
    444          px(entry.borderBoxSize[0].inlineSize)
    445        );
    446      });
    447      this._resizeObserver.observe(this.parentNode);
    448    }
    449 
    450    this.#updateLayoutBreakout();
    451 
    452    this._addObservers();
    453  }
    454 
    455  disconnectedCallback() {
    456    if (
    457      this.getAttribute("sap-name") == "searchbar" &&
    458      !lazy.UrlbarPrefs.get("browser.search.widget.new")
    459    ) {
    460      return;
    461    }
    462 
    463    this.#uninit();
    464  }
    465 
    466  #uninit() {
    467    if (this.sapName == "searchbar") {
    468      this.parentNode.removeAttribute("overflows");
    469 
    470      // Exit search mode to make sure it doesn't become stale while the
    471      // searchbar is invisible. Otherwise, the engine might get deleted
    472      // but we don't notice because the search service observer is inactive.
    473      this.searchMode = null;
    474    }
    475 
    476    if (this._copyCutController) {
    477      this.inputField.controllers.removeController(this._copyCutController);
    478      delete this._copyCutController;
    479    }
    480 
    481    for (let event of UrlbarInput.#inputFieldEvents) {
    482      this.inputField.removeEventListener(event, this);
    483    }
    484 
    485    // These are on the window to detect focusing shortcuts like F6.
    486    this.window.removeEventListener("keydown", this);
    487    this.window.removeEventListener("keyup", this);
    488 
    489    this.window.removeEventListener("mousedown", this);
    490    if (AppConstants.platform == "win") {
    491      this.window.removeEventListener("draggableregionleftmousedown", this);
    492    }
    493    this.removeEventListener("mousedown", this);
    494 
    495    // This listener handles clicks from our children too, included the search mode
    496    // indicator close button.
    497    this._inputContainer.removeEventListener("click", this);
    498 
    499    // This is used to detect commands launched from the panel, to avoid
    500    // recording abandonment events when the command causes a blur event.
    501    this.view.panel.removeEventListener("command", this, true);
    502 
    503    this.window.removeEventListener("customizationstarting", this);
    504    this.window.removeEventListener("aftercustomization", this);
    505    this.window.removeEventListener("toolbarvisibilitychange", this);
    506    let menuToolbar = this.window.document.getElementById("toolbar-menubar");
    507    if (menuToolbar) {
    508      menuToolbar.removeEventListener("DOMMenuBarInactive", this);
    509      menuToolbar.removeEventListener("DOMMenuBarActive", this);
    510    }
    511    if (this.#gBrowserListenersAdded) {
    512      this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this);
    513      this.window.gBrowser.tabContainer.removeEventListener("TabClose", this);
    514      this.window.gBrowser.removeTabsProgressListener(this);
    515      this.#gBrowserListenersAdded = false;
    516    }
    517 
    518    this._resizeObserver?.disconnect();
    519 
    520    this._removeObservers();
    521  }
    522 
    523  /**
    524   * This method is used to attach new context menu options to the urlbar
    525   * context menu, i.e. the context menu of the moz-input-box.
    526   * It is called when the moz-input-box rebuilds its context menu.
    527   *
    528   * Note that it might be called before #init has finished.
    529   */
    530  #onContextMenuRebuilt() {
    531    this._initStripOnShare();
    532    this._initPasteAndGo();
    533  }
    534 
    535  addGBrowserListeners() {
    536    if (this.window.gBrowser && !this.#gBrowserListenersAdded) {
    537      this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
    538      this.window.gBrowser.tabContainer.addEventListener("TabClose", this);
    539      this.window.gBrowser.addTabsProgressListener(this);
    540      this.#gBrowserListenersAdded = true;
    541    }
    542  }
    543 
    544  #lazy = XPCOMUtils.declareLazy({
    545    valueFormatter: () => new lazy.UrlbarValueFormatter(this),
    546    addSearchEngineHelper: () => new AddSearchEngineHelper(this),
    547  });
    548 
    549  /**
    550   * Manages the Add Search Engine contextual menu entries.
    551   */
    552  get addSearchEngineHelper() {
    553    return this.#lazy.addSearchEngineHelper;
    554  }
    555 
    556  /**
    557   * The search access point name of the UrlbarInput for use with telemetry or
    558   * logging, e.g. `urlbar`, `searchbar`.
    559   */
    560  get sapName() {
    561    return this.#sapName;
    562  }
    563 
    564  blur() {
    565    this.inputField.blur();
    566  }
    567 
    568  /**
    569   * @type {typeof HTMLInputElement.prototype.placeholder}
    570   */
    571  placeholder;
    572 
    573  /**
    574   * @type {typeof HTMLInputElement.prototype.readOnly}
    575   */
    576  readOnly;
    577 
    578  /**
    579   * @type {typeof HTMLInputElement.prototype.selectionStart}
    580   */
    581  selectionStart;
    582 
    583  /**
    584   * @type {typeof HTMLInputElement.prototype.selectionEnd}
    585   */
    586  selectionEnd;
    587 
    588  /**
    589   * Called when a urlbar or urlbar related pref changes.
    590   *
    591   * @param {string} pref
    592   *   The name of the pref. Relative to `browser.urlbar` for urlbar prefs.
    593   */
    594  onPrefChanged(pref) {
    595    switch (pref) {
    596      case "keyword.enabled":
    597        this._updatePlaceholderFromDefaultEngine().catch(e =>
    598          // This can happen if the search service failed.
    599          console.warn("Falied to update urlbar placeholder:", e)
    600        );
    601        break;
    602      case "browser.search.widget.new": {
    603        if (this.getAttribute("sap-name") == "searchbar" && this.isConnected) {
    604          if (lazy.UrlbarPrefs.get("browser.search.widget.new")) {
    605            // The connectedCallback was skipped. Init now.
    606            this.#init();
    607          } else {
    608            // Uninit now, the disconnectedCallback will be skipped.
    609            this.#uninit();
    610          }
    611        }
    612      }
    613    }
    614  }
    615 
    616  /**
    617   * Applies styling to the text in the urlbar input, depending on the text.
    618   */
    619  formatValue() {
    620    // The editor may not exist if the toolbar is not visible.
    621    if (this.#isAddressbar && this.editor) {
    622      this.#lazy.valueFormatter.update();
    623    }
    624  }
    625 
    626  focus() {
    627    let beforeFocus = new CustomEvent("beforefocus", {
    628      bubbles: true,
    629      cancelable: true,
    630    });
    631    this.inputField.dispatchEvent(beforeFocus);
    632    if (beforeFocus.defaultPrevented) {
    633      return;
    634    }
    635 
    636    this.inputField.focus();
    637  }
    638 
    639  select() {
    640    let beforeSelect = new CustomEvent("beforeselect", {
    641      bubbles: true,
    642      cancelable: true,
    643    });
    644    this.inputField.dispatchEvent(beforeSelect);
    645    if (beforeSelect.defaultPrevented) {
    646      return;
    647    }
    648 
    649    // See _on_select().  HTMLInputElement.select() dispatches a "select"
    650    // event but does not set the primary selection.
    651    this._suppressPrimaryAdjustment = true;
    652    this.inputField.select();
    653    this._suppressPrimaryAdjustment = false;
    654  }
    655 
    656  setSelectionRange(selectionStart, selectionEnd) {
    657    let beforeSelect = new CustomEvent("beforeselect", {
    658      bubbles: true,
    659      cancelable: true,
    660    });
    661    this.inputField.dispatchEvent(beforeSelect);
    662    if (beforeSelect.defaultPrevented) {
    663      return;
    664    }
    665 
    666    // See _on_select().  HTMLInputElement.select() dispatches a "select"
    667    // event but does not set the primary selection.
    668    this._suppressPrimaryAdjustment = true;
    669    this.inputField.setSelectionRange(selectionStart, selectionEnd);
    670    this._suppressPrimaryAdjustment = false;
    671  }
    672 
    673  saveSelectionStateForBrowser(browser) {
    674    let state = this.getBrowserState(browser);
    675    state.selection = {
    676      // When the value is empty, we're either on a blank page, or the whole
    677      // text has been edited away. In the latter case we'll restore value to
    678      // the current URI, and we want to fully select it.
    679      start: this.value ? this.selectionStart : 0,
    680      end: this.value ? this.selectionEnd : Number.MAX_SAFE_INTEGER,
    681      // When restoring a URI from an empty value, we don't want to untrim it.
    682      shouldUntrim: this.value && !this._protocolIsTrimmed,
    683    };
    684  }
    685 
    686  restoreSelectionStateForBrowser(browser) {
    687    // Address bar must be focused to untrim and for selection to make sense.
    688    this.focus();
    689    let state = this.getBrowserState(browser);
    690    if (state.selection) {
    691      if (state.selection.shouldUntrim) {
    692        this.#maybeUntrimUrl();
    693      }
    694      this.setSelectionRange(
    695        state.selection.start,
    696        // When selecting all the end value may be larger than the actual value.
    697        Math.min(state.selection.end, this.value.length)
    698      );
    699    }
    700  }
    701 
    702  /**
    703   * Sets the URI to display in the location bar.
    704   *
    705   * @param {object} [options]
    706   * @param {?nsIURI} [options.uri]
    707   *        If this is unspecified, the current URI will be used.
    708   * @param {boolean} [options.dueToTabSwitch=false]
    709   *        Whether this is being called due to switching tabs.
    710   * @param {boolean} [options.dueToSessionRestore=false]
    711   *        Whether this is being called due to session restore.
    712   * @param {boolean} [options.hideSearchTerms=false]
    713   *        True if userTypedValue should not be overidden by search terms
    714   *        and false otherwise.
    715   * @param {boolean} [options.isSameDocument=false]
    716   *        Whether the caller loaded a new document or not (e.g. location
    717   *        change from an anchor scroll or a pushState event).
    718   */
    719  setURI({
    720    uri = null,
    721    dueToTabSwitch = false,
    722    dueToSessionRestore = false,
    723    hideSearchTerms = false,
    724    isSameDocument = false,
    725  } = {}) {
    726    if (!this.#isAddressbar) {
    727      throw new Error(
    728        "Cannot set URI for UrlbarInput that is not an address bar"
    729      );
    730    }
    731    if (
    732      this.window.browsingContext.isDocumentPiP &&
    733      uri.spec.startsWith("about:blank")
    734    ) {
    735      // If this is a Document PiP, its url will be about:blank while
    736      // the opener will be a secure context, i.e. no about:blank
    737      throw new Error("Document PiP should show its opener URL");
    738    }
    739    // We only need to update the searchModeUI on tab switch conditionally
    740    // as we only persist searchMode with ScotchBonnet enabled.
    741    if (
    742      dueToTabSwitch &&
    743      lazy.UrlbarPrefs.getScotchBonnetPref("scotchBonnet.persistSearchMode")
    744    ) {
    745      this._updateSearchModeUI(this.searchMode);
    746    }
    747 
    748    let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
    749    this.#handlePersistedSearchTerms({
    750      state,
    751      uri,
    752      dueToTabSwitch,
    753      hideSearchTerms,
    754      isSameDocument,
    755    });
    756 
    757    let value = this.userTypedValue;
    758    let valid = false;
    759    let isReverting = !uri;
    760 
    761    // If `value` is null or if it's an empty string and we're switching tabs
    762    // set value to the browser's current URI. When a user empties the input,
    763    // switches tabs, and switches back, we want the URI to become visible again
    764    // so the user knows what URI they're viewing.
    765    // An exception to this is made in case of an auth request from a different
    766    // base domain. To avoid auth prompt spoofing we already display the url of
    767    // the cross domain resource, although the page is not loaded yet.
    768    // This url will be set/unset by PromptParent. See bug 791594 for reference.
    769    if (value === null || (!value && dueToTabSwitch)) {
    770      uri =
    771        this.window.gBrowser.selectedBrowser.currentAuthPromptURI ||
    772        uri ||
    773        this.#isOpenedPageInBlankTargetLoading ||
    774        this.window.gBrowser.currentURI;
    775      // Strip off usernames and passwords for the location bar
    776      try {
    777        uri = Services.io.createExposableURI(uri);
    778      } catch (e) {}
    779 
    780      let isInitialPageControlledByWebContent = false;
    781 
    782      // Replace initial page URIs with an empty string
    783      // only if there's no opener (bug 370555).
    784      if (
    785        this.window.isInitialPage(uri) &&
    786        lazy.BrowserUIUtils.checkEmptyPageOrigin(
    787          this.window.gBrowser.selectedBrowser,
    788          uri
    789        )
    790      ) {
    791        value = "";
    792      } else {
    793        isInitialPageControlledByWebContent = true;
    794 
    795        // We should deal with losslessDecodeURI throwing for exotic URIs
    796        try {
    797          value = losslessDecodeURI(uri);
    798        } catch (ex) {
    799          value = "about:blank";
    800        }
    801      }
    802      // If we update the URI while restoring a session, set the proxyState to
    803      // invalid, because we don't have a valid security state to show via site
    804      // identity yet. See Bug 1746383.
    805      valid =
    806        !dueToSessionRestore &&
    807        (!this.#canHandleAsBlankPage(uri.spec) ||
    808          lazy.ExtensionUtils.isExtensionUrl(uri) ||
    809          isInitialPageControlledByWebContent);
    810    } else if (
    811      this.window.isInitialPage(value) &&
    812      lazy.BrowserUIUtils.checkEmptyPageOrigin(
    813        this.window.gBrowser.selectedBrowser
    814      )
    815    ) {
    816      value = "";
    817      valid = true;
    818    }
    819 
    820    const previousUntrimmedValue = this.untrimmedValue;
    821    // When calculating the selection indices we must take into account a
    822    // trimmed protocol.
    823    let offset = this._protocolIsTrimmed
    824      ? lazy.BrowserUIUtils.trimURLProtocol.length
    825      : 0;
    826    const previousSelectionStart = this.selectionStart + offset;
    827    const previousSelectionEnd = this.selectionEnd + offset;
    828 
    829    this._setValue(value, { allowTrim: true, valueIsTyped: !valid });
    830    this.toggleAttribute("usertyping", !valid && value);
    831 
    832    if (this.focused && value != previousUntrimmedValue) {
    833      if (
    834        previousSelectionStart != previousSelectionEnd &&
    835        value.substring(previousSelectionStart, previousSelectionEnd) ===
    836          previousUntrimmedValue.substring(
    837            previousSelectionStart,
    838            previousSelectionEnd
    839          )
    840      ) {
    841        // If the same text is in the same place as the previously selected text,
    842        // the selection is kept.
    843        this.inputField.setSelectionRange(
    844          previousSelectionStart - offset,
    845          previousSelectionEnd - offset
    846        );
    847      } else if (
    848        previousSelectionEnd &&
    849        (previousUntrimmedValue.length === previousSelectionEnd ||
    850          value.length <= previousSelectionEnd)
    851      ) {
    852        // If the previous end caret is not 0 and the caret is at the end of the
    853        // input or its position is beyond the end of the new value, keep the
    854        // position at the end.
    855        this.inputField.setSelectionRange(value.length, value.length);
    856      } else {
    857        // Otherwise clear selection and set the caret position to the previous
    858        // caret end position.
    859        this.inputField.setSelectionRange(
    860          previousSelectionEnd - offset,
    861          previousSelectionEnd - offset
    862        );
    863      }
    864    }
    865 
    866    // The proxystate must be set before setting search mode below because
    867    // search mode depends on it.
    868    this.setPageProxyState(
    869      valid ? "valid" : "invalid",
    870      dueToTabSwitch,
    871      !isReverting &&
    872        dueToTabSwitch &&
    873        this.getBrowserState(this.window.gBrowser.selectedBrowser)
    874          .isUnifiedSearchButtonAvailable
    875    );
    876 
    877    if (
    878      state.persist?.shouldPersist &&
    879      !lazy.UrlbarSearchTermsPersistence.searchModeMatchesState(
    880        this.searchMode,
    881        state
    882      )
    883    ) {
    884      // When search terms persist, on non-default engine search result pages
    885      // the address bar should show the same search mode. For default engines,
    886      // search mode should not persist.
    887      if (state.persist.isDefaultEngine) {
    888        this.searchMode = null;
    889      } else {
    890        this.searchMode = {
    891          engineName: state.persist.originalEngineName,
    892          source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
    893          isPreview: false,
    894        };
    895      }
    896    } else if (dueToTabSwitch && !valid) {
    897      // If we're switching tabs, restore the tab's search mode.
    898      this.restoreSearchModeState();
    899    } else if (valid) {
    900      // If the URI is valid, exit search mode.  This must happen
    901      // after setting proxystate above because search mode depends on it.
    902      this.searchMode = null;
    903    }
    904 
    905    // Dispatch URIUpdate event to synchronize the tab status when switching.
    906    let event = new CustomEvent("SetURI", { bubbles: true });
    907    this.inputField.dispatchEvent(event);
    908  }
    909 
    910  /**
    911   * Converts an internal URI (e.g. a URI with a username or password) into one
    912   * which we can expose to the user.
    913   *
    914   * @param {nsIURI} uri
    915   *   The URI to be converted
    916   * @returns {nsIURI}
    917   *   The converted, exposable URI
    918   */
    919  makeURIReadable(uri) {
    920    // Avoid copying 'about:reader?url=', and always provide the original URI:
    921    // Reader mode ensures we call createExposableURI itself.
    922    let readerStrippedURI = lazy.ReaderMode.getOriginalUrlObjectForDisplay(
    923      uri.displaySpec
    924    );
    925    if (readerStrippedURI) {
    926      return readerStrippedURI;
    927    }
    928 
    929    try {
    930      return Services.io.createExposableURI(uri);
    931    } catch (ex) {}
    932 
    933    return uri;
    934  }
    935 
    936  /**
    937   * Function for tabs progress listener.
    938   *
    939   * @param {nsIBrowser} browser
    940   * @param {nsIWebProgress} webProgress
    941   *   The nsIWebProgress instance that fired the notification.
    942   * @param {nsIRequest} request
    943   *   The associated nsIRequest.  This may be null in some cases.
    944   * @param {nsIURI} locationURI
    945   *   The URI of the location that is being loaded.
    946   */
    947  onLocationChange(browser, webProgress, request, locationURI) {
    948    if (!webProgress.isTopLevel) {
    949      return;
    950    }
    951 
    952    if (
    953      browser != this.window.gBrowser.selectedBrowser &&
    954      !this.#canHandleAsBlankPage(locationURI.spec)
    955    ) {
    956      // If the page is loaded on background tab, make Unified Search Button
    957      // unavailable when back to the tab.
    958      this.getBrowserState(browser).isUnifiedSearchButtonAvailable = false;
    959    }
    960 
    961    // Using browser navigation buttons should potentially trigger a bounce
    962    // telemetry event.
    963    if (webProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
    964      this.controller.engagementEvent.handleBounceEventTrigger(browser);
    965    }
    966  }
    967 
    968  /**
    969   * Passes DOM events to the _on_<event type> methods.
    970   *
    971   * @param {Event} event The event to handle.
    972   */
    973  handleEvent(event) {
    974    let methodName = "_on_" + event.type;
    975    if (methodName in this) {
    976      try {
    977        this[methodName](event);
    978      } catch (e) {
    979        console.error(`Error calling UrlbarInput::${methodName}:`, e);
    980      }
    981    } else {
    982      throw new Error("Unrecognized UrlbarInput event: " + event.type);
    983    }
    984  }
    985 
    986  /**
    987   * Handles an event which might open text or a URL. If the event requires
    988   * doing so, handleCommand forwards it to handleNavigation.
    989   *
    990   * @param {Event} [event] The event triggering the open.
    991   */
    992  handleCommand(event = null) {
    993    let isMouseEvent = MouseEvent.isInstance(event);
    994    if (isMouseEvent && event.button == 2) {
    995      // Do nothing for right clicks.
    996      return;
    997    }
    998 
    999    // Determine whether to use the selected one-off search button.  In
   1000    // one-off search buttons parlance, "selected" means that the button
   1001    // has been navigated to via the keyboard.  So we want to use it if
   1002    // the triggering event is not a mouse click -- i.e., it's a Return
   1003    // key -- or if the one-off was mouse-clicked.
   1004    if (this.view.isOpen) {
   1005      let selectedOneOff = this.view.oneOffSearchButtons?.selectedButton;
   1006      if (selectedOneOff && (!isMouseEvent || event.target == selectedOneOff)) {
   1007        this.view.oneOffSearchButtons.handleSearchCommand(event, {
   1008          engineName: selectedOneOff.engine?.name,
   1009          source: selectedOneOff.source,
   1010          entry: "oneoff",
   1011        });
   1012        return;
   1013      }
   1014    }
   1015 
   1016    this.handleNavigation({ event });
   1017  }
   1018 
   1019  /**
   1020   * @typedef {object} HandleNavigationOneOffParams
   1021   *
   1022   * @property {string} openWhere
   1023   *   Where we expect the result to be opened.
   1024   * @property {object} openParams
   1025   *   The parameters related to where the result will be opened.
   1026   * @property {nsISearchEngine} engine
   1027   *   The selected one-off's engine.
   1028   */
   1029 
   1030  /**
   1031   * Handles an event which would cause a URL or text to be opened.
   1032   *
   1033   * @param {object} options
   1034   *   Options for the navigation.
   1035   * @param {Event} [options.event]
   1036   *   The event triggering the open.
   1037   * @param {HandleNavigationOneOffParams} [options.oneOffParams]
   1038   *   Optional. Pass if this navigation was triggered by a one-off. Practically
   1039   *   speaking, UrlbarSearchOneOffs passes this when the user holds certain key
   1040   *   modifiers while picking a one-off. In those cases, we do an immediate
   1041   *   search using the one-off's engine instead of entering search mode.
   1042   * @param {object} [options.triggeringPrincipal]
   1043   *   The principal that the action was triggered from.
   1044   */
   1045  handleNavigation({ event, oneOffParams, triggeringPrincipal }) {
   1046    let element = this.view.selectedElement;
   1047    let result = this.view.getResultFromElement(element);
   1048    let openParams = oneOffParams?.openParams || { triggeringPrincipal };
   1049 
   1050    // If the value was submitted during composition, the result may not have
   1051    // been updated yet, because the input event happens after composition end.
   1052    // We can't trust element nor _resultForCurrentValue targets in that case,
   1053    // so we always generate a new heuristic to load.
   1054    let isComposing = this.editor.composing;
   1055 
   1056    // Use the selected element if we have one; this is usually the case
   1057    // when the view is open.
   1058    let selectedPrivateResult =
   1059      result &&
   1060      result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   1061      result.payload.inPrivateWindow;
   1062    let selectedPrivateEngineResult =
   1063      selectedPrivateResult && result.payload.isPrivateEngine;
   1064    // Whether the user has been editing the value in the URL bar after selecting
   1065    // the result. However, if the result type is tip, pick as it is. The result
   1066    // heuristic is also kept the behavior as is for safety.
   1067    let safeToPickResult =
   1068      result &&
   1069      (result.heuristic ||
   1070        !this.valueIsTyped ||
   1071        result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP ||
   1072        this.value == this.#getValueFromResult(result));
   1073    if (
   1074      !isComposing &&
   1075      element &&
   1076      (!oneOffParams?.engine || selectedPrivateEngineResult) &&
   1077      safeToPickResult
   1078    ) {
   1079      this.pickElement(element, event);
   1080      return;
   1081    }
   1082 
   1083    // Use the hidden heuristic if it exists and there's no selection.
   1084    if (
   1085      lazy.UrlbarPrefs.get("experimental.hideHeuristic") &&
   1086      !element &&
   1087      !isComposing &&
   1088      !oneOffParams?.engine &&
   1089      this._resultForCurrentValue?.heuristic
   1090    ) {
   1091      this.pickResult(this._resultForCurrentValue, event);
   1092      return;
   1093    }
   1094 
   1095    // We don't select a heuristic result when we're autofilling a token alias,
   1096    // but we want pressing Enter to behave like the first result was selected.
   1097    if (!result && this.value.startsWith("@")) {
   1098      let tokenAliasResult = this.view.getResultAtIndex(0);
   1099      if (tokenAliasResult?.autofill && tokenAliasResult?.payload.keyword) {
   1100        this.pickResult(tokenAliasResult, event);
   1101        return;
   1102      }
   1103    }
   1104 
   1105    let url;
   1106    let selType = this.controller.engagementEvent.typeFromElement(
   1107      result,
   1108      element
   1109    );
   1110    let typedValue = this.value;
   1111    if (oneOffParams?.engine) {
   1112      selType = "oneoff";
   1113      typedValue = this._lastSearchString;
   1114      // If there's a selected one-off button then load a search using
   1115      // the button's engine.
   1116      result = this._resultForCurrentValue;
   1117 
   1118      let searchString =
   1119        (result && (result.payload.suggestion || result.payload.query)) ||
   1120        this._lastSearchString;
   1121      [url, openParams.postData] = lazy.UrlbarUtils.getSearchQueryUrl(
   1122        oneOffParams.engine,
   1123        searchString
   1124      );
   1125      if (oneOffParams.openWhere == "tab") {
   1126        this.window.gBrowser.tabContainer.addEventListener(
   1127          "TabOpen",
   1128          tabEvent =>
   1129            this._recordSearch(
   1130              oneOffParams.engine,
   1131              event,
   1132              {},
   1133              tabEvent.target.linkedBrowser
   1134            ),
   1135          { once: true }
   1136        );
   1137      } else {
   1138        this._recordSearch(oneOffParams.engine, event);
   1139      }
   1140 
   1141      lazy.UrlbarUtils.addToFormHistory(
   1142        this,
   1143        searchString,
   1144        oneOffParams.engine.name
   1145      ).catch(console.error);
   1146    } else {
   1147      // Use the current value if we don't have a UrlbarResult e.g. because the
   1148      // view is closed.
   1149      url = this.untrimmedValue;
   1150      openParams.postData = null;
   1151    }
   1152 
   1153    if (!url) {
   1154      return;
   1155    }
   1156 
   1157    // When the user hits enter in a local search mode and there's no selected
   1158    // result or one-off, don't do anything.
   1159    if (
   1160      this.searchMode &&
   1161      !this.searchMode.engineName &&
   1162      !result &&
   1163      !oneOffParams
   1164    ) {
   1165      return;
   1166    }
   1167 
   1168    let where = oneOffParams?.openWhere || this._whereToOpen(event);
   1169    if (selectedPrivateResult) {
   1170      where = "window";
   1171      openParams.private = true;
   1172    }
   1173    openParams.allowInheritPrincipal = false;
   1174    url = this._maybeCanonizeURL(event, url) || url.trim();
   1175 
   1176    let selectedResult = result || this.view.selectedResult;
   1177    this.controller.engagementEvent.record(event, {
   1178      element,
   1179      selType,
   1180      searchString: typedValue,
   1181      result: selectedResult || this._resultForCurrentValue || null,
   1182    });
   1183 
   1184    if (URL.canParse(url)) {
   1185      // Annotate if the untrimmed value contained a scheme, to later potentially
   1186      // be upgraded by schemeless HTTPS-First.
   1187      openParams.schemelessInput = this.#getSchemelessInput(
   1188        this.untrimmedValue
   1189      );
   1190      this._loadURL(url, event, where, openParams);
   1191      return;
   1192    }
   1193 
   1194    // This is not a URL and there's no selected element, because likely the
   1195    // view is closed, or paste&go was used.
   1196    // We must act consistently here, having or not an open view should not
   1197    // make a difference if the search string is the same.
   1198 
   1199    // If we have a result for the current value, we can just use it.
   1200    if (!isComposing && this._resultForCurrentValue) {
   1201      this.pickResult(this._resultForCurrentValue, event);
   1202      return;
   1203    }
   1204 
   1205    // Otherwise, we must fetch the heuristic result for the current value.
   1206    // TODO (Bug 1604927): If the urlbar results are restricted to a specific
   1207    // engine, here we must search with that specific engine; indeed the
   1208    // docshell wouldn't know about our engine restriction.
   1209    // Also remember to invoke this._recordSearch, after replacing url with
   1210    // the appropriate engine submission url.
   1211    let browser = this.window.gBrowser.selectedBrowser;
   1212    let lastLocationChange = browser.lastLocationChange;
   1213 
   1214    // Increment rate denominator measuring how often Address Bar handleCommand fallback path is hit.
   1215    Glean.urlbar.heuristicResultMissing.addToDenominator(1);
   1216 
   1217    lazy.UrlbarUtils.getHeuristicResultFor(url, this)
   1218      .then(newResult => {
   1219        // Because this happens asynchronously, we must verify that the browser
   1220        // location did not change in the meanwhile.
   1221        if (
   1222          where != "current" ||
   1223          browser.lastLocationChange == lastLocationChange
   1224        ) {
   1225          this.pickResult(newResult, event, null, browser);
   1226        }
   1227      })
   1228      .catch(() => {
   1229        if (url) {
   1230          // Something went wrong, we should always have a heuristic result,
   1231          // otherwise it means we're not able to search at all, maybe because
   1232          // some parts of the profile are corrupt.
   1233          // The urlbar should still allow to search or visit the typed string,
   1234          // so that the user can look for help to resolve the problem.
   1235 
   1236          // Increment rate numerator measuring how often Address Bar handleCommand fallback path is hit.
   1237          Glean.urlbar.heuristicResultMissing.addToNumerator(1);
   1238 
   1239          let flags =
   1240            Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   1241            Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   1242          if (this.isPrivate) {
   1243            flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
   1244          }
   1245          let {
   1246            preferredURI: uri,
   1247            postData,
   1248            keywordAsSent,
   1249          } = Services.uriFixup.getFixupURIInfo(url, flags);
   1250          if (
   1251            where != "current" ||
   1252            browser.lastLocationChange == lastLocationChange
   1253          ) {
   1254            openParams.postData = postData;
   1255            if (!keywordAsSent) {
   1256              // `uri` is not a search engine url, so we annotate if the untrimmed
   1257              // value contained a scheme, to potentially be later upgraded by
   1258              // schemeless HTTPS-First.
   1259              openParams.schemelessInput = this.#getSchemelessInput(
   1260                this.untrimmedValue
   1261              );
   1262            }
   1263            this._loadURL(uri.spec, event, where, openParams, null, browser);
   1264          }
   1265        }
   1266      });
   1267    // Don't add further handling here, the catch above is our last resort.
   1268  }
   1269 
   1270  handleRevert() {
   1271    this.userTypedValue = null;
   1272    // Nullify search mode before setURI so it won't try to restore it.
   1273    this.searchMode = null;
   1274    if (this.#isAddressbar) {
   1275      this.setURI({
   1276        dueToTabSwitch: true,
   1277        hideSearchTerms: true,
   1278      });
   1279    } else {
   1280      this.value = "";
   1281    }
   1282    if (this.value && this.focused) {
   1283      this.select();
   1284    }
   1285  }
   1286 
   1287  maybeHandleRevertFromPopup(anchorElement) {
   1288    let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   1289    if (anchorElement?.closest("#urlbar") && state.persist?.shouldPersist) {
   1290      this.handleRevert();
   1291      Glean.urlbarPersistedsearchterms.revertByPopupCount.add(1);
   1292    }
   1293  }
   1294 
   1295  /**
   1296   * Called by inputs that resemble search boxes, but actually hand input off
   1297   * to the Urlbar. We use these fake inputs on the new tab page and
   1298   * about:privatebrowsing.
   1299   *
   1300   * @param {string} searchString
   1301   *   The search string to use.
   1302   * @param {nsISearchEngine} [searchEngine]
   1303   *   Optional. If included and the right prefs are set, we will enter search
   1304   *   mode when handing `searchString` from the fake input to the Urlbar.
   1305   * @param {string} [newtabSessionId]
   1306   *   Optional. The id of the newtab session that handed off this search.
   1307   */
   1308  handoff(searchString, searchEngine, newtabSessionId) {
   1309    this._isHandoffSession = true;
   1310    this._handoffSession = newtabSessionId;
   1311    if (lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") && searchEngine) {
   1312      this.search(searchString, {
   1313        searchEngine,
   1314        searchModeEntry: "handoff",
   1315      });
   1316    } else {
   1317      this.search(searchString);
   1318    }
   1319  }
   1320 
   1321  /**
   1322   * Called when an element of the view is picked.
   1323   *
   1324   * @param {HTMLElement} element The element that was picked.
   1325   * @param {Event} event The event that picked the element.
   1326   */
   1327  pickElement(element, event) {
   1328    let result = this.view.getResultFromElement(element);
   1329    lazy.logger.debug(
   1330      `pickElement ${element} with event ${event?.type}, result: ${result}`
   1331    );
   1332    if (!result) {
   1333      return;
   1334    }
   1335    this.pickResult(result, event, element);
   1336  }
   1337 
   1338  /**
   1339   * Called when a result is picked.
   1340   *
   1341   * @param {UrlbarResult} result The result that was picked.
   1342   * @param {Event} event The event that picked the result.
   1343   * @param {HTMLElement} element the picked view element, if available.
   1344   * @param {object} browser The browser to use for the load.
   1345   */
   1346  // eslint-disable-next-line complexity
   1347  pickResult(
   1348    result,
   1349    event,
   1350    element = null,
   1351    browser = this.window.gBrowser.selectedBrowser
   1352  ) {
   1353    if (element?.classList.contains("urlbarView-button-menu")) {
   1354      this.view.openResultMenu(result, element);
   1355      return;
   1356    }
   1357 
   1358    if (element?.dataset.command) {
   1359      this.#pickMenuResult(result, event, element, browser);
   1360      return;
   1361    }
   1362 
   1363    if (
   1364      result.providerName == lazy.UrlbarProviderGlobalActions.name &&
   1365      this.#providesSearchMode(result)
   1366    ) {
   1367      this.maybeConfirmSearchModeFromResult({
   1368        result,
   1369        checkValue: false,
   1370      });
   1371      return;
   1372    }
   1373 
   1374    // When a one-off is selected, we restyle heuristic results to look like
   1375    // search results. In the unlikely event that they are clicked, instead of
   1376    // picking the results as usual, we confirm search mode, same as if the user
   1377    // had selected them and pressed the enter key. Restyling results in this
   1378    // manner was agreed on as a compromise between consistent UX and
   1379    // engineering effort. See review discussion at bug 1667766.
   1380    if (
   1381      (this.searchMode?.isPreview &&
   1382        result.providerName == lazy.UrlbarProviderGlobalActions.name) ||
   1383      (result.heuristic &&
   1384        this.searchMode?.isPreview &&
   1385        this.view.oneOffSearchButtons?.selectedButton)
   1386    ) {
   1387      this.confirmSearchMode();
   1388      this.search(this.value);
   1389      return;
   1390    }
   1391 
   1392    if (
   1393      result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP &&
   1394      result.payload.type == "dismissalAcknowledgment"
   1395    ) {
   1396      // The user clicked the "Got it" button inside the dismissal
   1397      // acknowledgment tip. Dismiss the tip.
   1398      this.controller.engagementEvent.record(event, {
   1399        result,
   1400        element,
   1401        searchString: this._lastSearchString,
   1402        selType: "dismiss",
   1403      });
   1404      this.view.onQueryResultRemoved(result.rowIndex);
   1405      return;
   1406    }
   1407 
   1408    let resultUrl = element?.dataset.url;
   1409    let originalUntrimmedValue = this.untrimmedValue;
   1410    let isCanonized = this.setValueFromResult({
   1411      result,
   1412      event,
   1413      element,
   1414      urlOverride: resultUrl,
   1415    });
   1416    let where = this._whereToOpen(event);
   1417    let openParams = {
   1418      allowInheritPrincipal: false,
   1419      globalHistoryOptions: {
   1420        triggeringSource: this.#sapName,
   1421        triggeringSearchEngine: result.payload?.engine,
   1422        triggeringSponsoredURL: result.payload?.isSponsored
   1423          ? result.payload.url
   1424          : undefined,
   1425      },
   1426      private: this.isPrivate,
   1427    };
   1428 
   1429    if (resultUrl && where == "current") {
   1430      // Open help links in a new tab.
   1431      where = "tab";
   1432    }
   1433 
   1434    if (!this.#providesSearchMode(result)) {
   1435      this.view.close({ elementPicked: true });
   1436    }
   1437 
   1438    if (isCanonized) {
   1439      this.controller.engagementEvent.record(event, {
   1440        result,
   1441        element,
   1442        selType: "canonized",
   1443        searchString: this._lastSearchString,
   1444      });
   1445      this._loadURL(this._untrimmedValue, event, where, openParams, browser);
   1446      return;
   1447    }
   1448 
   1449    let { url, postData } = resultUrl
   1450      ? { url: resultUrl, postData: null }
   1451      : lazy.UrlbarUtils.getUrlFromResult(result, { element });
   1452    openParams.postData = postData;
   1453 
   1454    switch (result.type) {
   1455      case lazy.UrlbarUtils.RESULT_TYPE.URL: {
   1456        if (result.heuristic) {
   1457          // Bug 1578856: both the provider and the docshell run heuristics to
   1458          // decide how to handle a non-url string, either fixing it to a url, or
   1459          // searching for it.
   1460          // Some preferences can control the docshell behavior, for example
   1461          // if dns_first_for_single_words is true, the docshell looks up the word
   1462          // against the dns server, and either loads it as an url or searches for
   1463          // it, depending on the lookup result. The provider instead will always
   1464          // return a fixed url in this case, because URIFixup is synchronous and
   1465          // can't do a synchronous dns lookup. A possible long term solution
   1466          // would involve sharing the docshell logic with the provider, along
   1467          // with the dns lookup.
   1468          // For now, in this specific case, we'll override the result's url
   1469          // with the input value, and let it pass through to _loadURL(), and
   1470          // finally to the docshell.
   1471          // This also means that in some cases the heuristic result will show a
   1472          // Visit entry, but the docshell will instead execute a search. It's a
   1473          // rare case anyway, most likely to happen for enterprises customizing
   1474          // the urifixup prefs.
   1475          if (
   1476            lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
   1477            lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
   1478          ) {
   1479            url = originalUntrimmedValue;
   1480          }
   1481          // Annotate if the untrimmed value contained a scheme, to later potentially
   1482          // be upgraded by schemeless HTTPS-First.
   1483          openParams.schemelessInput = this.#getSchemelessInput(
   1484            originalUntrimmedValue
   1485          );
   1486        }
   1487        break;
   1488      }
   1489      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD: {
   1490        // If this result comes from a bookmark keyword, let it inherit the
   1491        // current document's principal, otherwise bookmarklets would break.
   1492        openParams.allowInheritPrincipal = true;
   1493        break;
   1494      }
   1495      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
   1496        // Behaviour is reversed with SecondaryActions, default behaviour is to navigate
   1497        // and button is provided to switch to tab.
   1498        if (
   1499          this.hasAttribute("action-override") ||
   1500          (lazy.UrlbarPrefs.get("secondaryActions.switchToTab") &&
   1501            element?.dataset.action !== "tabswitch")
   1502        ) {
   1503          where = "current";
   1504          break;
   1505        }
   1506 
   1507        // Keep the searchMode for telemetry since handleRevert sets it to null.
   1508        const searchMode = this.searchMode;
   1509        this.handleRevert();
   1510        let prevTab = this.window.gBrowser.selectedTab;
   1511        let loadOpts = {
   1512          adoptIntoActiveWindow: lazy.UrlbarPrefs.get(
   1513            "switchTabs.adoptIntoActiveWindow"
   1514          ),
   1515        };
   1516 
   1517        // We cache the search string because switching tab may clear it.
   1518        let searchString = this._lastSearchString;
   1519        this.controller.engagementEvent.record(event, {
   1520          result,
   1521          element,
   1522          searchString,
   1523          searchMode,
   1524          selType: this.controller.engagementEvent.typeFromElement(
   1525            result,
   1526            element
   1527          ),
   1528        });
   1529 
   1530        let switched = this.window.switchToTabHavingURI(
   1531          Services.io.newURI(url),
   1532          true,
   1533          loadOpts,
   1534          lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
   1535            lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId(
   1536              result.payload.userContextId
   1537            )
   1538            ? result.payload.userContextId
   1539            : null
   1540        );
   1541        if (switched && prevTab.isEmpty) {
   1542          this.window.gBrowser.removeTab(prevTab);
   1543        }
   1544 
   1545        if (switched && !this.isPrivate && !result.heuristic) {
   1546          // We don't await for this, because a rejection should not interrupt
   1547          // the load. Just reportError it.
   1548          lazy.UrlbarUtils.addToInputHistory(url, searchString).catch(
   1549            console.error
   1550          );
   1551        }
   1552 
   1553        // TODO (Bug 1865757): We should not show a "switchtotab" result for
   1554        // tabs that are not currently open. Find out why tabs are not being
   1555        // properly unregistered when they are being closed.
   1556        if (!switched) {
   1557          console.error(`Tried to switch to non-existent tab: ${url}`);
   1558          lazy.UrlbarProviderOpenTabs.unregisterOpenTab(
   1559            url,
   1560            result.payload.userContextId,
   1561            result.payload.tabGroup,
   1562            this.isPrivate
   1563          );
   1564        }
   1565 
   1566        return;
   1567      }
   1568      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
   1569        if (result.payload.providesSearchMode) {
   1570          this.controller.engagementEvent.record(event, {
   1571            result,
   1572            element,
   1573            searchString: this._lastSearchString,
   1574            selType: this.controller.engagementEvent.typeFromElement(
   1575              result,
   1576              element
   1577            ),
   1578          });
   1579          this.maybeConfirmSearchModeFromResult({
   1580            result,
   1581            checkValue: false,
   1582          });
   1583          return;
   1584        }
   1585 
   1586        if (
   1587          !this.searchMode &&
   1588          result.heuristic &&
   1589          // If we asked the DNS earlier, avoid the post-facto check.
   1590          !lazy.UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") &&
   1591          // TODO (bug 1642623): for now there is no smart heuristic to skip the
   1592          // DNS lookup, so any value above 0 will run it.
   1593          lazy.UrlbarPrefs.get("dnsResolveSingleWordsAfterSearch") > 0 &&
   1594          this.window.gKeywordURIFixup &&
   1595          lazy.UrlbarUtils.looksLikeSingleWordHost(originalUntrimmedValue)
   1596        ) {
   1597          // When fixing a single word to a search, the docShell would also
   1598          // query the DNS and if resolved ask the user whether they would
   1599          // rather visit that as a host. On a positive answer, it adds the host
   1600          // to the list that we use to make decisions.
   1601          // Because we are directly asking for a search here, bypassing the
   1602          // docShell, we need to do the same ourselves.
   1603          // See also URIFixupChild.sys.mjs and keyword-uri-fixup.
   1604          let fixupInfo = this._getURIFixupInfo(originalUntrimmedValue.trim());
   1605          if (fixupInfo) {
   1606            this.window.gKeywordURIFixup.check(
   1607              this.window.gBrowser.selectedBrowser,
   1608              fixupInfo
   1609            );
   1610          }
   1611        }
   1612 
   1613        if (result.payload.inPrivateWindow) {
   1614          where = "window";
   1615          openParams.private = true;
   1616        }
   1617 
   1618        const actionDetails = {
   1619          isSuggestion: !!result.payload.suggestion,
   1620          isFormHistory:
   1621            result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY,
   1622          alias: result.payload.keyword,
   1623        };
   1624        const engine = Services.search.getEngineByName(result.payload.engine);
   1625 
   1626        if (where == "tab") {
   1627          // The TabOpen event is fired synchronously so tabEvent.target
   1628          // is guaranteed to be our new search tab.
   1629          this.window.gBrowser.tabContainer.addEventListener(
   1630            "TabOpen",
   1631            tabEvent =>
   1632              this._recordSearch(
   1633                engine,
   1634                event,
   1635                actionDetails,
   1636                tabEvent.target.linkedBrowser
   1637              ),
   1638            { once: true }
   1639          );
   1640        } else {
   1641          this._recordSearch(engine, event, actionDetails);
   1642        }
   1643 
   1644        if (!result.payload.inPrivateWindow) {
   1645          lazy.UrlbarUtils.addToFormHistory(
   1646            this,
   1647            result.payload.suggestion || result.payload.query,
   1648            engine.name
   1649          ).catch(console.error);
   1650        }
   1651        break;
   1652      }
   1653      case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
   1654        if (url) {
   1655          break;
   1656        }
   1657        this.handleRevert();
   1658        this.controller.engagementEvent.record(event, {
   1659          result,
   1660          element,
   1661          selType: "tip",
   1662          searchString: this._lastSearchString,
   1663        });
   1664        return;
   1665      }
   1666      case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: {
   1667        if (!url) {
   1668          // If we're not loading a URL, the engagement is done. First revert
   1669          // and then record the engagement since providers expect the urlbar to
   1670          // be reverted when they're notified of the engagement, but before
   1671          // reverting, copy the search mode since it's nulled on revert.
   1672          const { searchMode } = this;
   1673          this.handleRevert();
   1674          this.controller.engagementEvent.record(event, {
   1675            result,
   1676            element,
   1677            searchMode,
   1678            searchString: this._lastSearchString,
   1679            selType: this.controller.engagementEvent.typeFromElement(
   1680              result,
   1681              element
   1682            ),
   1683          });
   1684          return;
   1685        }
   1686        break;
   1687      }
   1688      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX: {
   1689        this.controller.engagementEvent.record(event, {
   1690          result,
   1691          element,
   1692          selType: "extension",
   1693          searchString: this._lastSearchString,
   1694        });
   1695 
   1696        // The urlbar needs to revert to the loaded url when a command is
   1697        // handled by the extension.
   1698        this.handleRevert();
   1699        // We don't directly handle a load when an Omnibox API result is picked,
   1700        // instead we forward the request to the WebExtension itself, because
   1701        // the value may not even be a url.
   1702        // We pass the keyword and content, that actually is the retrieved value
   1703        // prefixed by the keyword. ExtensionSearchHandler uses this keyword
   1704        // redundancy as a sanity check.
   1705        lazy.ExtensionSearchHandler.handleInputEntered(
   1706          result.payload.keyword,
   1707          result.payload.content,
   1708          where
   1709        );
   1710        return;
   1711      }
   1712      case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT: {
   1713        this.handleRevert();
   1714        this.controller.engagementEvent.record(event, {
   1715          result,
   1716          element,
   1717          searchString: this._lastSearchString,
   1718          selType: this.controller.engagementEvent.typeFromElement(
   1719            result,
   1720            element
   1721          ),
   1722        });
   1723        this.maybeConfirmSearchModeFromResult({
   1724          result,
   1725          checkValue: false,
   1726        });
   1727 
   1728        return;
   1729      }
   1730    }
   1731 
   1732    if (!url) {
   1733      throw new Error(`Invalid url for result ${JSON.stringify(result)}`);
   1734    }
   1735 
   1736    // Record input history but only in non-private windows.
   1737    if (!this.isPrivate) {
   1738      let input;
   1739      if (!result.heuristic) {
   1740        input = this._lastSearchString;
   1741      } else if (result.autofill?.type == "adaptive") {
   1742        input = result.autofill.adaptiveHistoryInput;
   1743      }
   1744      // `input` may be an empty string, so do a strict comparison here.
   1745      if (input !== undefined) {
   1746        // We don't await for this, because a rejection should not interrupt
   1747        // the load. Just reportError it.
   1748        lazy.UrlbarUtils.addToInputHistory(url, input).catch(console.error);
   1749      }
   1750    }
   1751 
   1752    this.controller.engagementEvent.startTrackingBounceEvent(browser, event, {
   1753      result,
   1754      element,
   1755      searchString: this._lastSearchString,
   1756      selType: this.controller.engagementEvent.typeFromElement(result, element),
   1757      searchSource: this.getSearchSource(event),
   1758    });
   1759 
   1760    this.controller.engagementEvent.record(event, {
   1761      result,
   1762      element,
   1763      searchString: this._lastSearchString,
   1764      selType: this.controller.engagementEvent.typeFromElement(result, element),
   1765      searchSource: this.getSearchSource(event),
   1766    });
   1767 
   1768    if (result.payload.sendAttributionRequest) {
   1769      lazy.PartnerLinkAttribution.makeRequest({
   1770        targetURL: result.payload.url,
   1771        source: this.#sapName,
   1772        campaignID: Services.prefs.getStringPref(
   1773          "browser.partnerlink.campaign.topsites"
   1774        ),
   1775      });
   1776      if (!this.isPrivate && result.providerName === "UrlbarProviderTopSites") {
   1777        // The position is 1-based for telemetry
   1778        const position = result.rowIndex + 1;
   1779        Glean.contextualServicesTopsites.click[`urlbar_${position}`].add(1);
   1780      }
   1781    }
   1782 
   1783    this._loadURL(
   1784      url,
   1785      event,
   1786      where,
   1787      openParams,
   1788      {
   1789        source: result.source,
   1790        type: result.type,
   1791        searchTerm: result.payload.suggestion ?? result.payload.query,
   1792      },
   1793      browser
   1794    );
   1795  }
   1796 
   1797  /**
   1798   * Called by the view when moving through results with the keyboard, and when
   1799   * picking a result.  This sets the input value to the value of the result and
   1800   * invalidates the pageproxystate.  It also sets the result that is associated
   1801   * with the current input value.  If you need to set this result but don't
   1802   * want to also set the input value, then use setResultForCurrentValue.
   1803   *
   1804   * @param {object} options
   1805   *   Options.
   1806   * @param {UrlbarResult} [options.result]
   1807   *   The result that was selected or picked, null if no result was selected.
   1808   * @param {Event} [options.event]
   1809   *   The event that picked the result.
   1810   * @param {string} [options.urlOverride]
   1811   *   Normally the URL is taken from `result.payload.url`, but if `urlOverride`
   1812   *   is specified, it's used instead. See `#getValueFromResult()`.
   1813   * @param {Element} [options.element]
   1814   *   The element that was selected or picked, if available. For results that
   1815   *   have multiple selectable children, the value may be taken from a child
   1816   *   element rather than the result. See `#getValueFromResult()`.
   1817   * @returns {boolean}
   1818   *   Whether the value has been canonized
   1819   */
   1820  setValueFromResult({
   1821    result = null,
   1822    event = null,
   1823    urlOverride = null,
   1824    element = null,
   1825  } = {}) {
   1826    // Usually this is set by a previous input event, but in certain cases, like
   1827    // when opening Top Sites on a loaded page, it wouldn't happen. To avoid
   1828    // confusing the user, we always enforce it when a result changes our value.
   1829    this.setPageProxyState("invalid", true);
   1830 
   1831    // A previous result may have previewed search mode. If we don't expect that
   1832    // we might stay in a search mode of some kind, exit it now.
   1833    if (
   1834      this.searchMode?.isPreview &&
   1835      !this.#providesSearchMode(result) &&
   1836      !this.view.oneOffSearchButtons?.selectedButton
   1837    ) {
   1838      this.searchMode = null;
   1839    }
   1840 
   1841    if (!result) {
   1842      // This happens when there's no selection, for example when moving to the
   1843      // one-offs search settings button, or to the input field when Top Sites
   1844      // are shown; then we must reset the input value.
   1845      // Note that for Top Sites the last search string would be empty, thus we
   1846      // must restore the last text value.
   1847      // Note that unselected autofill results will still arrive in this
   1848      // function with a non-null `result`. They are handled below.
   1849      this.value = this._lastSearchString || this._valueOnLastSearch;
   1850      this.setResultForCurrentValue(result);
   1851      return false;
   1852    }
   1853 
   1854    // We won't allow trimming when calling _setValue, since it makes too easy
   1855    // for the user to wrongly transform `https` into `http`, for example by
   1856    // picking a https://site/path_1 result and editing the path to path_2,
   1857    // then we'd end up visiting http://site/path_2.
   1858    // Trimming `http` would be ok, but there's other cases where it's unsafe,
   1859    // like transforming a url into a search.
   1860    // This choice also makes it easier to copy the full url of a result.
   1861 
   1862    // We are supporting canonization of any result, in particular this allows
   1863    // for single word search suggestions to be converted to a .com URL.
   1864    // For autofilled results, the value to canonize is the user typed string,
   1865    // not the autofilled value.
   1866    let canonizedUrl = this._maybeCanonizeURL(
   1867      event,
   1868      result.autofill ? this._lastSearchString : this.value
   1869    );
   1870    if (canonizedUrl) {
   1871      this._setValue(canonizedUrl);
   1872 
   1873      this.setResultForCurrentValue(result);
   1874      return true;
   1875    }
   1876 
   1877    if (result.autofill) {
   1878      this._autofillValue(result.autofill);
   1879    }
   1880 
   1881    if (this.#providesSearchMode(result)) {
   1882      let enteredSearchMode;
   1883      // Only preview search mode if the result is selected.
   1884      if (this.view.resultIsSelected(result)) {
   1885        // For ScotchBonnet, As Tab and Arrow Down/Up, Page Down/Up key are used
   1886        // for selection of the urlbar results, keep the search mode as preview
   1887        // mode if there are multiple results.
   1888        // If ScotchBonnet is disabled, not starting a query means we will only
   1889        // preview search mode.
   1890        enteredSearchMode = this.maybeConfirmSearchModeFromResult({
   1891          result,
   1892          checkValue: false,
   1893          startQuery:
   1894            lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") &&
   1895            this.view.visibleResults.length == 1,
   1896        });
   1897      }
   1898      if (!enteredSearchMode) {
   1899        this._setValue(this.#getValueFromResult(result), {
   1900          actionType: this.#getActionTypeFromResult(result),
   1901        });
   1902        this.searchMode = null;
   1903      }
   1904      this.setResultForCurrentValue(result);
   1905      return false;
   1906    }
   1907 
   1908    if (!result.autofill) {
   1909      let value = this.#getValueFromResult(result, { urlOverride, element });
   1910      this._setValue(value, {
   1911        actionType: this.#getActionTypeFromResult(result),
   1912      });
   1913    }
   1914 
   1915    this.setResultForCurrentValue(result);
   1916 
   1917    // Update placeholder selection and value to the current selected result to
   1918    // prevent the on_selectionchange event to detect a "accent-character"
   1919    // insertion.
   1920    if (!result.autofill && this._autofillPlaceholder) {
   1921      this._autofillPlaceholder.value = this.value;
   1922      this._autofillPlaceholder.selectionStart = this.value.length;
   1923      this._autofillPlaceholder.selectionEnd = this.value.length;
   1924    }
   1925    return false;
   1926  }
   1927 
   1928  /**
   1929   * The input keeps track of the result associated with the current input
   1930   * value.  This result can be set by calling either setValueFromResult or this
   1931   * method.  Use this method when you need to set the result without also
   1932   * setting the input value.  This can be the case when either the selection is
   1933   * cleared and no other result becomes selected, or when the result is the
   1934   * heuristic and we don't want to modify the value the user is typing.
   1935   *
   1936   * @param {UrlbarResult} result
   1937   *   The result to associate with the current input value.
   1938   */
   1939  setResultForCurrentValue(result) {
   1940    this._resultForCurrentValue = result;
   1941  }
   1942 
   1943  /**
   1944   * Called by the controller when the first result of a new search is received.
   1945   * If it's an autofill result, then it may need to be autofilled, subject to a
   1946   * few restrictions.
   1947   *
   1948   * @param {UrlbarResult} result
   1949   *   The first result.
   1950   */
   1951  _autofillFirstResult(result) {
   1952    if (!result.autofill) {
   1953      return;
   1954    }
   1955 
   1956    let isPlaceholderSelected =
   1957      this._autofillPlaceholder &&
   1958      this.selectionEnd == this._autofillPlaceholder.value.length &&
   1959      this.selectionStart == this._lastSearchString.length &&
   1960      this._autofillPlaceholder.value
   1961        .toLocaleLowerCase()
   1962        .startsWith(this._lastSearchString.toLocaleLowerCase());
   1963 
   1964    // Don't autofill if there's already a selection (with one caveat described
   1965    // next) or the cursor isn't at the end of the input.  But if there is a
   1966    // selection and it's the autofill placeholder value, then do autofill.
   1967    if (
   1968      !isPlaceholderSelected &&
   1969      !this._autofillIgnoresSelection &&
   1970      (this.selectionStart != this.selectionEnd ||
   1971        this.selectionEnd != this._lastSearchString.length)
   1972    ) {
   1973      return;
   1974    }
   1975 
   1976    this.setValueFromResult({ result });
   1977  }
   1978  /**
   1979   * Clears displayed autofill values and unsets the autofill placeholder.
   1980   */
   1981  #clearAutofill() {
   1982    if (!this._autofillPlaceholder) {
   1983      return;
   1984    }
   1985    let currentSelectionStart = this.selectionStart;
   1986    let currentSelectionEnd = this.selectionEnd;
   1987 
   1988    // Overriding this value clears the selection.
   1989    this.inputField.value = this.value.substring(
   1990      0,
   1991      this._autofillPlaceholder.selectionStart
   1992    );
   1993    this._autofillPlaceholder = null;
   1994    // Restore selection
   1995    this.setSelectionRange(currentSelectionStart, currentSelectionEnd);
   1996  }
   1997 
   1998  /**
   1999   * Invoked by the controller when the first result is received.
   2000   *
   2001   * @param {UrlbarResult} firstResult
   2002   *   The first result received.
   2003   * @returns {boolean}
   2004   *   True if this method canceled the query and started a new one.  False
   2005   *   otherwise.
   2006   */
   2007  onFirstResult(firstResult) {
   2008    // If the heuristic result has a keyword but isn't a keyword offer, we may
   2009    // need to enter search mode.
   2010    if (
   2011      firstResult.heuristic &&
   2012      firstResult.payload.keyword &&
   2013      !this.#providesSearchMode(firstResult) &&
   2014      this.maybeConfirmSearchModeFromResult({
   2015        result: firstResult,
   2016        entry: "typed",
   2017        checkValue: false,
   2018      })
   2019    ) {
   2020      return true;
   2021    }
   2022 
   2023    // To prevent selection flickering, we apply autofill on input through a
   2024    // placeholder, without waiting for results. But, if the first result is
   2025    // not an autofill one, the autofill prediction was wrong and we should
   2026    // restore the original user typed string.
   2027    if (firstResult.autofill) {
   2028      this._autofillFirstResult(firstResult);
   2029    } else if (
   2030      this._autofillPlaceholder &&
   2031      // Avoid clobbering added spaces (for token aliases, for example).
   2032      !this.value.endsWith(" ")
   2033    ) {
   2034      this._autofillPlaceholder = null;
   2035      this._setValue(this.userTypedValue);
   2036    }
   2037 
   2038    return false;
   2039  }
   2040 
   2041  /**
   2042   * Starts a query based on the current input value.
   2043   *
   2044   * @param {object} [options]
   2045   *   Object options
   2046   * @param {boolean} [options.allowAutofill]
   2047   *   Whether or not to allow providers to include autofill results.
   2048   * @param {boolean} [options.autofillIgnoresSelection]
   2049   *   Normally we autofill only if the cursor is at the end of the string,
   2050   *   if this is set we'll autofill regardless of selection.
   2051   * @param {string} [options.searchString]
   2052   *   The search string.  If not given, the current input value is used.
   2053   *   Otherwise, the current input value must start with this value.
   2054   * @param {boolean} [options.resetSearchState]
   2055   *   If this is the first search of a user interaction with the input, set
   2056   *   this to true (the default) so that search-related state from the previous
   2057   *   interaction doesn't interfere with the new interaction.  Otherwise set it
   2058   *   to false so that state is maintained during a single interaction.  The
   2059   *   intended use for this parameter is that it should be set to false when
   2060   *   this method is called due to input events.
   2061   * @param {event} [options.event]
   2062   *   The user-generated event that triggered the query, if any.  If given, we
   2063   *   will record engagement event telemetry for the query.
   2064   */
   2065  startQuery({
   2066    allowAutofill,
   2067    autofillIgnoresSelection = false,
   2068    searchString,
   2069    resetSearchState = true,
   2070    event,
   2071  } = {}) {
   2072    if (!searchString) {
   2073      searchString =
   2074        this.getAttribute("pageproxystate") == "valid" ? "" : this.value;
   2075    } else if (!this.value.startsWith(searchString)) {
   2076      throw new Error("The current value doesn't start with the search string");
   2077    }
   2078 
   2079    let queryContext = this.#makeQueryContext({
   2080      allowAutofill,
   2081      event,
   2082      searchString,
   2083    });
   2084 
   2085    if (event) {
   2086      this.controller.engagementEvent.start(event, queryContext, searchString);
   2087    }
   2088 
   2089    if (this._suppressStartQuery) {
   2090      return;
   2091    }
   2092 
   2093    this._autofillIgnoresSelection = autofillIgnoresSelection;
   2094    if (resetSearchState) {
   2095      this._resetSearchState();
   2096    }
   2097 
   2098    if (this.searchMode) {
   2099      this.confirmSearchMode();
   2100    }
   2101 
   2102    this._lastSearchString = searchString;
   2103    this._valueOnLastSearch = this.value;
   2104 
   2105    // TODO (Bug 1522902): This promise is necessary for tests, because some
   2106    // tests are not listening for completion when starting a query through
   2107    // other methods than startQuery (input events for example).
   2108    this.lastQueryContextPromise = this.controller.startQuery(queryContext);
   2109  }
   2110 
   2111  /**
   2112   * Sets the input's value, starts a search, and opens the view.
   2113   *
   2114   * @param {string} value
   2115   *   The input's value will be set to this value, and the search will
   2116   *   use it as its query.
   2117   * @param {object} [options]
   2118   *   Object options
   2119   * @param {nsISearchEngine} [options.searchEngine]
   2120   *   Search engine to use when the search is using a known alias.
   2121   * @param {UrlbarUtils.SEARCH_MODE_ENTRY} [options.searchModeEntry]
   2122   *   If provided, we will record this parameter as the search mode entry point
   2123   *   in Telemetry. Consumers should provide this if they expect their call
   2124   *   to enter search mode.
   2125   * @param {boolean} [options.focus]
   2126   *   If true, the urlbar will be focused.  If false, the focus will remain
   2127   *   unchanged.
   2128   * @param {boolean} [options.startQuery]
   2129   *   If true, start query to show urlbar result by fireing input event. If
   2130   *   false, not fire the event.
   2131   */
   2132  search(value, options = {}) {
   2133    let { searchEngine, searchModeEntry, startQuery = true } = options;
   2134    if (options.focus ?? true) {
   2135      this.focus();
   2136    }
   2137    let trimmedValue = value.trim();
   2138    let end = trimmedValue.search(lazy.UrlUtils.REGEXP_SPACES);
   2139    let firstToken = end == -1 ? trimmedValue : trimmedValue.substring(0, end);
   2140    // Enter search mode if the string starts with a restriction token.
   2141    let searchMode = this.searchModeForToken(firstToken);
   2142    let firstTokenIsRestriction = !!searchMode;
   2143    if (!searchMode && searchEngine) {
   2144      searchMode = { engineName: searchEngine.name };
   2145      firstTokenIsRestriction = searchEngine.aliases.includes(firstToken);
   2146    }
   2147 
   2148    if (searchMode) {
   2149      searchMode.entry = searchModeEntry;
   2150      this.searchMode = searchMode;
   2151      if (firstTokenIsRestriction) {
   2152        // Remove the restriction token/alias from the string to be searched for
   2153        // in search mode.
   2154        value = value.replace(firstToken, "");
   2155      }
   2156      if (lazy.UrlUtils.REGEXP_SPACES.test(value[0])) {
   2157        // If there was a trailing space after the restriction token/alias,
   2158        // remove it.
   2159        value = value.slice(1);
   2160      }
   2161    } else if (
   2162      Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(firstToken)
   2163    ) {
   2164      this.searchMode = null;
   2165      // If the entire value is a restricted token, append a space.
   2166      if (Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(value)) {
   2167        value += " ";
   2168      }
   2169    }
   2170    this.inputField.value = value;
   2171    // Avoid selecting the text if this method is called twice in a row.
   2172    this.selectionStart = -1;
   2173 
   2174    if (startQuery) {
   2175      // Note: proper IME Composition handling depends on the fact this generates
   2176      // an input event, rather than directly invoking the controller; everything
   2177      // goes through _on_input, that will properly skip the search until the
   2178      // composition is committed. _on_input also skips the search when it's the
   2179      // same as the previous search, but we want to allow consecutive searches
   2180      // with the same string. So clear _lastSearchString first.
   2181      this._lastSearchString = "";
   2182      let event = new UIEvent("input", {
   2183        bubbles: true,
   2184        cancelable: false,
   2185        view: this.window,
   2186        detail: 0,
   2187      });
   2188      this.inputField.dispatchEvent(event);
   2189    }
   2190  }
   2191 
   2192  /**
   2193   * Returns a search mode object if a token should enter search mode when
   2194   * typed. This does not handle engine aliases.
   2195   *
   2196   * @param {Values<typeof lazy.UrlbarTokenizer.RESTRICT>} token
   2197   *   A restriction token to convert to search mode.
   2198   * @returns {?object}
   2199   *   A search mode object. Null if search mode should not be entered. See
   2200   *   setSearchMode documentation for details.
   2201   */
   2202  searchModeForToken(token) {
   2203    if (token == lazy.UrlbarTokenizer.RESTRICT.SEARCH) {
   2204      return {
   2205        engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)
   2206          ?.name,
   2207      };
   2208    }
   2209 
   2210    let mode =
   2211      this.#isAddressbar &&
   2212      lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(m => m.restrict == token);
   2213    if (mode) {
   2214      // Return a copy so callers don't modify the object in LOCAL_SEARCH_MODES.
   2215      return { ...mode };
   2216    }
   2217 
   2218    return null;
   2219  }
   2220 
   2221  /**
   2222   * Opens a search page if the value is non-empty, otherwise opens the
   2223   * search engine homepage (searchform).
   2224   *
   2225   * @param {string} value
   2226   * @param {object} options
   2227   * @param {nsISearchEngine} options.searchEngine
   2228   */
   2229  openEngineHomePage(value, { searchEngine }) {
   2230    if (!searchEngine) {
   2231      console.warn("No searchEngine parameter");
   2232      return;
   2233    }
   2234 
   2235    let trimmedValue = value.trim();
   2236    let url;
   2237    if (trimmedValue) {
   2238      url = searchEngine.getSubmission(trimmedValue, null).uri.spec;
   2239      // TODO: record SAP telemetry, see Bug 1961789.
   2240    } else {
   2241      url = searchEngine.searchForm;
   2242      lazy.BrowserSearchTelemetry.recordSearchForm(searchEngine, this.#sapName);
   2243    }
   2244 
   2245    this._lastSearchString = "";
   2246    if (this.#isAddressbar) {
   2247      this.inputField.value = url;
   2248    }
   2249    this.selectionStart = -1;
   2250 
   2251    this.window.openTrustedLinkIn(url, "current");
   2252  }
   2253 
   2254  /**
   2255   * Focus without the focus styles.
   2256   * This is used by Activity Stream and about:privatebrowsing for search hand-off.
   2257   */
   2258  setHiddenFocus() {
   2259    this._hideFocus = true;
   2260    if (this.focused) {
   2261      this.removeAttribute("focused");
   2262    } else {
   2263      this.focus();
   2264    }
   2265  }
   2266 
   2267  /**
   2268   * Restore focus styles.
   2269   * This is used by Activity Stream and about:privatebrowsing for search hand-off.
   2270   *
   2271   * @param {boolean} forceSuppressFocusBorder
   2272   *   Set true to suppress-focus-border attribute if this flag is true.
   2273   */
   2274  removeHiddenFocus(forceSuppressFocusBorder = false) {
   2275    this._hideFocus = false;
   2276    if (this.focused) {
   2277      this.toggleAttribute("focused", true);
   2278 
   2279      if (forceSuppressFocusBorder) {
   2280        this.toggleAttribute("suppress-focus-border", true);
   2281      }
   2282    }
   2283  }
   2284 
   2285  /**
   2286   * Addressbar: Gets the search mode for a specific browser instance.
   2287   * Searchbar: Gets the window-global search mode.
   2288   *
   2289   * @param {MozBrowser} browser
   2290   *   The search mode for this browser will be returned.
   2291   *   Pass the selected browser for the searchbar.
   2292   * @param {boolean} [confirmedOnly]
   2293   *   Normally, if the browser has both preview and confirmed modes, preview
   2294   *   mode will be returned since it takes precedence.  If this argument is
   2295   *   true, then only confirmed search mode will be returned, or null if
   2296   *   search mode hasn't been confirmed.
   2297   * @returns {?object}
   2298   *   A search mode object or null if the browser/window is not in search mode.
   2299   *   See setSearchMode documentation.
   2300   */
   2301  getSearchMode(browser, confirmedOnly = false) {
   2302    let modes = this.#getSearchModesObject(browser);
   2303 
   2304    // Return copies so that callers don't modify the stored values.
   2305    if (!confirmedOnly && modes.preview) {
   2306      return { ...modes.preview };
   2307    }
   2308    if (modes.confirmed) {
   2309      return { ...modes.confirmed };
   2310    }
   2311    return null;
   2312  }
   2313 
   2314  /**
   2315   * Addressbar: Sets the search mode for a specific browser instance.
   2316   * Searchbar: Sets the window-global search mode.
   2317   * If the given browser is selected, then this will also enter search mode.
   2318   *
   2319   * @param {object} searchMode
   2320   *   A search mode object.
   2321   * @param {string} searchMode.engineName
   2322   *   The name of the search engine to restrict to.
   2323   * @param {UrlbarUtils.RESULT_SOURCE} searchMode.source
   2324   *   A result source to restrict to.
   2325   * @param {string} searchMode.entry
   2326   *   How search mode was entered. This is recorded in event telemetry. One of
   2327   *   the values in UrlbarUtils.SEARCH_MODE_ENTRY.
   2328   * @param {boolean} [searchMode.isPreview]
   2329   *   If true, we will preview search mode. Search mode preview does not record
   2330   *   telemetry and has slighly different UI behavior. The preview is exited in
   2331   *   favor of full search mode when a query is executed. False should be
   2332   *   passed if the caller needs to enter search mode but expects it will not
   2333   *   be interacted with right away. Defaults to true.
   2334   * @param {MozBrowser} browser
   2335   *   The browser for which to set search mode.
   2336   *   Pass the selected browser for the searchbar.
   2337   */
   2338  async setSearchMode(searchMode, browser) {
   2339    let currentSearchMode = this.getSearchMode(browser);
   2340    let areSearchModesSame =
   2341      (!currentSearchMode && !searchMode) ||
   2342      lazy.ObjectUtils.deepEqual(currentSearchMode, searchMode);
   2343 
   2344    // Exit search mode if the passed-in engine is invalid or hidden.
   2345    let engine;
   2346    if (searchMode?.engineName) {
   2347      if (!Services.search.isInitialized) {
   2348        await Services.search.init();
   2349      }
   2350      engine = Services.search.getEngineByName(searchMode.engineName);
   2351      if (!engine || engine.hidden) {
   2352        searchMode = null;
   2353      }
   2354    }
   2355 
   2356    let {
   2357      engineName,
   2358      source,
   2359      entry,
   2360      restrictType,
   2361      isPreview = true,
   2362    } = searchMode || {};
   2363 
   2364    searchMode = null;
   2365 
   2366    if (engineName) {
   2367      searchMode = {
   2368        engineName,
   2369        isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
   2370      };
   2371      if (source) {
   2372        searchMode.source = source;
   2373      } else if (searchMode.isGeneralPurposeEngine) {
   2374        // History results for general-purpose search engines are often not
   2375        // useful, so we hide them in search mode. See bug 1658646 for
   2376        // discussion.
   2377        searchMode.source = lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
   2378      }
   2379    } else if (source) {
   2380      let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
   2381      if (sourceName) {
   2382        searchMode = { source };
   2383      } else {
   2384        console.error(`Unrecognized source: ${source}`);
   2385      }
   2386    }
   2387 
   2388    let modes = this.#getSearchModesObject(browser);
   2389 
   2390    if (searchMode) {
   2391      searchMode.isPreview = isPreview;
   2392      if (lazy.UrlbarUtils.SEARCH_MODE_ENTRY.has(entry)) {
   2393        searchMode.entry = entry;
   2394      } else {
   2395        // If we see this value showing up in telemetry, we should review
   2396        // search mode's entry points.
   2397        searchMode.entry = "other";
   2398      }
   2399 
   2400      if (!searchMode.isPreview) {
   2401        modes.confirmed = searchMode;
   2402        delete modes.preview;
   2403      } else {
   2404        modes.preview = searchMode;
   2405      }
   2406    } else {
   2407      delete modes.preview;
   2408      delete modes.confirmed;
   2409    }
   2410 
   2411    if (restrictType) {
   2412      searchMode.restrictType = restrictType;
   2413    }
   2414 
   2415    // Enter search mode if the browser is selected.
   2416    if (browser == this.window.gBrowser.selectedBrowser) {
   2417      this._updateSearchModeUI(searchMode);
   2418      if (searchMode) {
   2419        // Set userTypedValue to the query string so that it's properly restored
   2420        // when switching back to the current tab and across sessions.
   2421        this.userTypedValue = this.untrimmedValue;
   2422        this.valueIsTyped = true;
   2423        if (!searchMode.isPreview && !areSearchModesSame) {
   2424          try {
   2425            lazy.BrowserSearchTelemetry.recordSearchMode(searchMode);
   2426          } catch (ex) {
   2427            console.error(ex);
   2428          }
   2429        }
   2430      }
   2431    }
   2432  }
   2433 
   2434  /**
   2435   * @typedef {object} SearchModesObject
   2436   *
   2437   * @property {object} [preview] preview search mode
   2438   * @property {object} [confirmed] confirmed search mode
   2439   */
   2440 
   2441  /**
   2442   * @type {SearchModesObject|undefined}
   2443   *
   2444   * The (lazily initialized) search mode object for the searchbar.
   2445   * This is needed because the searchbar has one search mode per window that
   2446   * shouldn't change when switching tabs. For the address bar, the search mode
   2447   * is stored per browser in #browserStates and this is always undefined.
   2448   */
   2449  #searchbarSearchModes;
   2450 
   2451  /**
   2452   * Addressbar: Gets the search modes object for a specific browser instance.
   2453   * Searchbar: Gets the window-global search modes object.
   2454   *
   2455   * @param {MozBrowser} browser
   2456   *   The browser to get the search modes object for.
   2457   *   Pass the selected browser for the searchbar.
   2458   * @returns {SearchModesObject}
   2459   */
   2460  #getSearchModesObject(browser) {
   2461    if (!this.#isAddressbar) {
   2462      // The passed browser doesn't matter here, but it does in setSearchMode.
   2463      this.#searchbarSearchModes ??= {};
   2464      return this.#searchbarSearchModes;
   2465    }
   2466 
   2467    let state = this.getBrowserState(browser);
   2468    state.searchModes ??= {};
   2469    return state.searchModes;
   2470  }
   2471 
   2472  /**
   2473   * Restores the current browser search mode from a previously stored state.
   2474   */
   2475  restoreSearchModeState() {
   2476    this.searchMode = this.#getSearchModesObject(
   2477      this.window.gBrowser.selectedBrowser
   2478    ).confirmed;
   2479  }
   2480 
   2481  /**
   2482   * Enters search mode with the default engine.
   2483   */
   2484  searchModeShortcut() {
   2485    // We restrict to search results when entering search mode from this
   2486    // shortcut to honor historical behaviour.
   2487    this.searchMode = {
   2488      source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
   2489      engineName: lazy.UrlbarSearchUtils.getDefaultEngine(this.isPrivate)?.name,
   2490      entry: "shortcut",
   2491    };
   2492    // The searchMode setter clears the input if pageproxystate is valid, so
   2493    // we know at this point this.value will either be blank or the user's
   2494    // typed string.
   2495    this.search(this.value);
   2496    this.select();
   2497  }
   2498 
   2499  /**
   2500   * Confirms the current search mode.
   2501   */
   2502  confirmSearchMode() {
   2503    let searchMode = this.searchMode;
   2504    if (searchMode?.isPreview) {
   2505      searchMode.isPreview = false;
   2506      this.searchMode = searchMode;
   2507 
   2508      // Unselect the one-off search button to ensure UI consistency.
   2509      if (this.view.oneOffSearchButtons) {
   2510        this.view.oneOffSearchButtons.selectedButton = null;
   2511      }
   2512    }
   2513  }
   2514 
   2515  // Getters and Setters below.
   2516 
   2517  get editor() {
   2518    return this.inputField.editor;
   2519  }
   2520 
   2521  get focused() {
   2522    return this.document.activeElement == this.inputField;
   2523  }
   2524 
   2525  get goButton() {
   2526    return this.querySelector(".urlbar-go-button");
   2527  }
   2528 
   2529  get value() {
   2530    return this.inputField.value;
   2531  }
   2532 
   2533  set value(val) {
   2534    this._setValue(val, { allowTrim: true });
   2535  }
   2536 
   2537  get untrimmedValue() {
   2538    return this._untrimmedValue;
   2539  }
   2540 
   2541  get userTypedValue() {
   2542    return this.#isAddressbar
   2543      ? this.window.gBrowser.userTypedValue
   2544      : this._userTypedValue;
   2545  }
   2546 
   2547  set userTypedValue(val) {
   2548    if (this.#isAddressbar) {
   2549      this.window.gBrowser.userTypedValue = val;
   2550    } else {
   2551      this._userTypedValue = val;
   2552    }
   2553  }
   2554 
   2555  get lastSearchString() {
   2556    return this._lastSearchString;
   2557  }
   2558 
   2559  get searchMode() {
   2560    if (!this.window.gBrowser) {
   2561      // This only happens before DOMContentLoaded.
   2562      return null;
   2563    }
   2564    return this.getSearchMode(this.window.gBrowser.selectedBrowser);
   2565  }
   2566 
   2567  set searchMode(searchMode) {
   2568    this.setSearchMode(searchMode, this.window.gBrowser.selectedBrowser);
   2569    this.searchModeSwitcher?.onSearchModeChanged();
   2570    lazy.UrlbarSearchTermsPersistence.onSearchModeChanged(this.window);
   2571  }
   2572 
   2573  getBrowserState(browser) {
   2574    let state = this.#browserStates.get(browser);
   2575    if (!state) {
   2576      state = {};
   2577      this.#browserStates.set(browser, state);
   2578    }
   2579    return state;
   2580  }
   2581 
   2582  async #updateLayoutBreakout() {
   2583    if (!this.#allowBreakout) {
   2584      return;
   2585    }
   2586    if (this.document.fullscreenElement) {
   2587      // Toolbars are hidden in DOM fullscreen mode, so we can't get proper
   2588      // layout information and need to retry after leaving that mode.
   2589      this.window.addEventListener(
   2590        "fullscreen",
   2591        () => {
   2592          this.#updateLayoutBreakout();
   2593        },
   2594        { once: true }
   2595      );
   2596      return;
   2597    }
   2598    await this.#updateLayoutBreakoutDimensions();
   2599  }
   2600 
   2601  startLayoutExtend() {
   2602    if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
   2603      // Do not expand if the Urlbar does not support being expanded or it is
   2604      // already expanded.
   2605      return;
   2606    }
   2607    if (!this.view.isOpen) {
   2608      return;
   2609    }
   2610 
   2611    this.#updateTextboxPosition();
   2612 
   2613    this.setAttribute("breakout-extend", "true");
   2614 
   2615    // Enable the animation only after the first extend call to ensure it
   2616    // doesn't run when opening a new window.
   2617    if (!this.hasAttribute("breakout-extend-animate")) {
   2618      this.window.promiseDocumentFlushed(() => {
   2619        this.window.requestAnimationFrame(() => {
   2620          this.setAttribute("breakout-extend-animate", "true");
   2621        });
   2622      });
   2623    }
   2624  }
   2625 
   2626  endLayoutExtend() {
   2627    // If reduce motion is enabled, we want to collapse the Urlbar here so the
   2628    // user sees only sees two states: not expanded, and expanded with the view
   2629    // open.
   2630    if (!this.hasAttribute("breakout-extend") || this.view.isOpen) {
   2631      return;
   2632    }
   2633 
   2634    this.removeAttribute("breakout-extend");
   2635    this.#updateTextboxPosition();
   2636  }
   2637 
   2638  /**
   2639   * Updates the user interface to indicate whether the URI in the address bar
   2640   * is different than the loaded page, because it's being edited or because a
   2641   * search result is currently selected and is displayed in the location bar.
   2642   *
   2643   * @param {string} state
   2644   *        The string "valid" indicates that the security indicators and other
   2645   *        related user interface elments should be shown because the URI in
   2646   *        the location bar matches the loaded page. The string "invalid"
   2647   *        indicates that the URI in the location bar is different than the
   2648   *        loaded page.
   2649   * @param {boolean} [updatePopupNotifications]
   2650   *        Indicates whether we should update the PopupNotifications
   2651   *        visibility due to this change, otherwise avoid doing so as it is
   2652   *        being handled somewhere else.
   2653   * @param {boolean} [forceUnifiedSearchButtonAvailable]
   2654   *        If this parameter is true, force to make Unified Search Button available.
   2655   *        Otherwise, the availability will be depedent on the proxy state.
   2656   *        Default value is false.
   2657   */
   2658  setPageProxyState(
   2659    state,
   2660    updatePopupNotifications,
   2661    forceUnifiedSearchButtonAvailable = false
   2662  ) {
   2663    let prevState = this.getAttribute("pageproxystate");
   2664 
   2665    this.setAttribute("pageproxystate", state);
   2666    this._inputContainer.setAttribute("pageproxystate", state);
   2667    this._identityBox?.setAttribute("pageproxystate", state);
   2668    this.setUnifiedSearchButtonAvailability(
   2669      forceUnifiedSearchButtonAvailable || state == "invalid"
   2670    );
   2671 
   2672    if (state == "valid") {
   2673      this._lastValidURLStr = this.value;
   2674    }
   2675 
   2676    if (
   2677      updatePopupNotifications &&
   2678      prevState != state &&
   2679      this.window.UpdatePopupNotificationsVisibility
   2680    ) {
   2681      this.window.UpdatePopupNotificationsVisibility();
   2682    }
   2683  }
   2684 
   2685  /**
   2686   * When switching tabs quickly, TabSelect sometimes happens before
   2687   * _adjustFocusAfterTabSwitch and due to the focus still being on the old
   2688   * tab, we end up flickering the results pane briefly.
   2689   */
   2690  afterTabSwitchFocusChange() {
   2691    this._gotFocusChange = true;
   2692    this._afterTabSelectAndFocusChange();
   2693  }
   2694 
   2695  /**
   2696   * Confirms search mode and starts a new search if appropriate for the given
   2697   * result.  See also _searchModeForResult.
   2698   *
   2699   * @param {object} options
   2700   *   Options object.
   2701   * @param {string} [options.entry]
   2702   *   If provided, this will be recorded as the entry point into search mode.
   2703   *   See setSearchMode documentation for details.
   2704   * @param {UrlbarResult} [options.result]
   2705   *   The result to confirm. Defaults to the currently selected result.
   2706   * @param {boolean} [options.checkValue]
   2707   *   If true, the trimmed input value must equal the result's keyword in order
   2708   *   to enter search mode.
   2709   * @param {boolean} [options.startQuery]
   2710   *   If true, start a query after entering search mode. Defaults to true.
   2711   * @returns {boolean}
   2712   *   True if we entered search mode and false if not.
   2713   */
   2714  maybeConfirmSearchModeFromResult({
   2715    entry,
   2716    result = this._resultForCurrentValue,
   2717    checkValue = true,
   2718    startQuery = true,
   2719  }) {
   2720    if (
   2721      !result ||
   2722      (checkValue &&
   2723        this.value.trim() != result.payload.keyword?.trim() &&
   2724        this.value.trim() != result.payload.autofillKeyword?.trim())
   2725    ) {
   2726      return false;
   2727    }
   2728 
   2729    let searchMode = this._searchModeForResult(result, entry);
   2730    if (!searchMode) {
   2731      return false;
   2732    }
   2733 
   2734    this.searchMode = searchMode;
   2735 
   2736    let value = result.payload.query?.trimStart() || "";
   2737    this._setValue(value);
   2738 
   2739    if (startQuery) {
   2740      this.startQuery({ allowAutofill: false });
   2741    }
   2742 
   2743    return true;
   2744  }
   2745 
   2746  observe(subject, topic, data) {
   2747    switch (topic) {
   2748      case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED: {
   2749        let engine = subject.QueryInterface(Ci.nsISearchEngine);
   2750        switch (data) {
   2751          case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
   2752          case lazy.SearchUtils.MODIFIED_TYPE.REMOVED: {
   2753            let searchMode = this.searchMode;
   2754            if (searchMode?.engineName == engine.name) {
   2755              // Exit search mode if the current search mode engine was removed.
   2756              this.searchMode = searchMode;
   2757            }
   2758            break;
   2759          }
   2760          case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT:
   2761            if (!this.isPrivate) {
   2762              this._updatePlaceholder(engine.name);
   2763            }
   2764            break;
   2765          case lazy.SearchUtils.MODIFIED_TYPE.DEFAULT_PRIVATE:
   2766            if (this.isPrivate) {
   2767              this._updatePlaceholder(engine.name);
   2768            }
   2769            break;
   2770        }
   2771        break;
   2772      }
   2773    }
   2774  }
   2775 
   2776  /**
   2777   * Get search source.
   2778   *
   2779   * @param {Event} event
   2780   *   The event that triggered this query.
   2781   * @returns {string}
   2782   *   The source name.
   2783   */
   2784  getSearchSource(event) {
   2785    if (this.#isAddressbar) {
   2786      if (this._isHandoffSession) {
   2787        return "urlbar-handoff";
   2788      }
   2789 
   2790      const isOneOff =
   2791        this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event);
   2792      if (this.searchMode && !isOneOff) {
   2793        // Without checking !isOneOff, we might record the string
   2794        // oneoff_urlbar-searchmode in the SEARCH_COUNTS probe (in addition to
   2795        // oneoff_urlbar and oneoff_searchbar). The extra information is not
   2796        // necessary; the intent is the same regardless of whether the user is
   2797        // in search mode when they do a key-modified click/enter on a one-off.
   2798        return "urlbar-searchmode";
   2799      }
   2800 
   2801      let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   2802      if (state.persist?.searchTerms && !isOneOff) {
   2803        // Normally, we use state.persist.shouldPersist to check if search terms
   2804        // persisted. However when the user modifies the search term, the boolean
   2805        // will become false. Thus, we check the presence of the search terms to
   2806        // know whether or not search terms ever persisted in the address bar.
   2807        return "urlbar-persisted";
   2808      }
   2809    }
   2810    return this.#sapName;
   2811  }
   2812 
   2813  // Private methods below.
   2814 
   2815  /*
   2816   * Actions can have several buttons in the same result where not all
   2817   * will provide a searchMode so check the currently selected button
   2818   * in that case.
   2819   */
   2820  #providesSearchMode(result) {
   2821    if (!result) {
   2822      return false;
   2823    }
   2824    if (
   2825      this.view.selectedElement &&
   2826      result.providerName == lazy.UrlbarProviderGlobalActions.name
   2827    ) {
   2828      return this.view.selectedElement.dataset.providesSearchmode == "true";
   2829    }
   2830    return result.payload.providesSearchMode;
   2831  }
   2832 
   2833  _addObservers() {
   2834    this._observer ??= {
   2835      observe: this.observe.bind(this),
   2836      QueryInterface: ChromeUtils.generateQI([
   2837        "nsIObserver",
   2838        "nsISupportsWeakReference",
   2839      ]),
   2840    };
   2841    Services.obs.addObserver(
   2842      this._observer,
   2843      lazy.SearchUtils.TOPIC_ENGINE_MODIFIED,
   2844      true
   2845    );
   2846  }
   2847 
   2848  _removeObservers() {
   2849    if (this._observer) {
   2850      Services.obs.removeObserver(
   2851        this._observer,
   2852        lazy.SearchUtils.TOPIC_ENGINE_MODIFIED
   2853      );
   2854      this._observer = null;
   2855    }
   2856  }
   2857 
   2858  _getURIFixupInfo(searchString) {
   2859    let flags =
   2860      Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   2861      Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   2862    if (this.isPrivate) {
   2863      flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
   2864    }
   2865    try {
   2866      return Services.uriFixup.getFixupURIInfo(searchString, flags);
   2867    } catch (ex) {
   2868      console.error(
   2869        `An error occured while trying to fixup "${searchString}"`,
   2870        ex
   2871      );
   2872    }
   2873    return null;
   2874  }
   2875 
   2876  _afterTabSelectAndFocusChange() {
   2877    // We must have seen both events to proceed safely.
   2878    if (!this._gotFocusChange || !this._gotTabSelect) {
   2879      return;
   2880    }
   2881    this._gotFocusChange = this._gotTabSelect = false;
   2882 
   2883    this.formatValue();
   2884    this._resetSearchState();
   2885 
   2886    // We don't use the original TabSelect event because caching it causes
   2887    // leaks on MacOS.
   2888    const event = new CustomEvent("tabswitch");
   2889    // If the urlbar is focused after a tab switch, record a potential
   2890    // engagement event. When switching from a focused to a non-focused urlbar,
   2891    // the blur event would record the abandonment. When switching from an
   2892    // unfocused to a focused urlbar, there should be no search session ongoing,
   2893    // so this will be a no-op.
   2894    if (this.focused) {
   2895      this.controller.engagementEvent.record(event, {
   2896        searchString: this._lastSearchString,
   2897        searchSource: this.getSearchSource(event),
   2898      });
   2899    }
   2900 
   2901    // Switching tabs doesn't always change urlbar focus, so we must try to
   2902    // reopen here too, not just on focus.
   2903    if (this.view.autoOpen({ event })) {
   2904      return;
   2905    }
   2906    // The input may retain focus when switching tabs in which case we
   2907    // need to close the view and search mode switcher popup explicitly.
   2908    this.searchModeSwitcher.closePanel();
   2909    this.view.close();
   2910  }
   2911 
   2912  #updateTextboxPosition() {
   2913    if (!this.view.isOpen) {
   2914      this.style.top = "";
   2915      return;
   2916    }
   2917    this.style.top = px(
   2918      this.parentNode.getBoxQuads({
   2919        ignoreTransforms: true,
   2920        flush: false,
   2921      })[0].p1.y
   2922    );
   2923  }
   2924 
   2925  #updateTextboxPositionNextFrame() {
   2926    if (!this.hasAttribute("breakout")) {
   2927      return;
   2928    }
   2929    // Allow for any layout changes to take place (e.g. when the menubar becomes
   2930    // inactive) before re-measuring to position the textbox
   2931    this.window.requestAnimationFrame(() => {
   2932      this.window.requestAnimationFrame(() => {
   2933        this.#updateTextboxPosition();
   2934      });
   2935    });
   2936  }
   2937 
   2938  #stopBreakout() {
   2939    this.removeAttribute("breakout");
   2940    this.parentNode.removeAttribute("breakout");
   2941    this.style.top = "";
   2942    try {
   2943      this.hidePopover();
   2944    } catch (ex) {
   2945      // No big deal if not a popover already.
   2946    }
   2947    this._layoutBreakoutUpdateKey = {};
   2948  }
   2949 
   2950  incrementBreakoutBlockerCount() {
   2951    this.#breakoutBlockerCount++;
   2952    if (this.#breakoutBlockerCount == 1) {
   2953      this.#stopBreakout();
   2954    }
   2955  }
   2956 
   2957  decrementBreakoutBlockerCount() {
   2958    if (this.#breakoutBlockerCount > 0) {
   2959      this.#breakoutBlockerCount--;
   2960    }
   2961    if (this.#breakoutBlockerCount === 0) {
   2962      this.#updateLayoutBreakout();
   2963    }
   2964  }
   2965 
   2966  async #updateLayoutBreakoutDimensions() {
   2967    this.#stopBreakout();
   2968 
   2969    // When this method gets called a second time before the first call
   2970    // finishes, we need to disregard the first one.
   2971    let updateKey = {};
   2972    this._layoutBreakoutUpdateKey = updateKey;
   2973    await this.window.promiseDocumentFlushed(() => {});
   2974    await new Promise(resolve => {
   2975      this.window.requestAnimationFrame(() => {
   2976        if (this._layoutBreakoutUpdateKey != updateKey || !this.isConnected) {
   2977          return;
   2978        }
   2979 
   2980        this.parentNode.style.setProperty(
   2981          "--urlbar-container-height",
   2982          px(getBoundsWithoutFlushing(this.parentNode).height)
   2983        );
   2984        this.style.setProperty(
   2985          "--urlbar-height",
   2986          px(getBoundsWithoutFlushing(this).height)
   2987        );
   2988 
   2989        if (this.#breakoutBlockerCount) {
   2990          return;
   2991        }
   2992 
   2993        this.setAttribute("breakout", "true");
   2994        this.parentNode.setAttribute("breakout", "true");
   2995        this.showPopover();
   2996        this.#updateTextboxPosition();
   2997 
   2998        resolve();
   2999      });
   3000    });
   3001  }
   3002 
   3003  /**
   3004   * Sets the input field value.
   3005   *
   3006   * @param {string} val The new value to set.
   3007   * @param {object} [options] Options for setting.
   3008   * @param {boolean} [options.allowTrim] Whether the value can be trimmed.
   3009   * @param {string} [options.untrimmedValue] Override for this._untrimmedValue.
   3010   * @param {boolean} [options.valueIsTyped] Override for this.valueIsTyped.
   3011   * @param {string} [options.actionType] Value for the `actiontype` attribute.
   3012   *
   3013   * @returns {string} The set value.
   3014   */
   3015  _setValue(
   3016    val,
   3017    {
   3018      allowTrim = false,
   3019      untrimmedValue = null,
   3020      valueIsTyped = false,
   3021      actionType = undefined,
   3022    } = {}
   3023  ) {
   3024    // Don't expose internal about:reader URLs to the user.
   3025    let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
   3026    if (originalUrl) {
   3027      val = originalUrl.displaySpec;
   3028    }
   3029    this._untrimmedValue = untrimmedValue ?? val;
   3030    this._protocolIsTrimmed = false;
   3031    if (allowTrim) {
   3032      let oldVal = val;
   3033      val = this._trimValue(val);
   3034      this._protocolIsTrimmed =
   3035        oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
   3036        !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol);
   3037    }
   3038 
   3039    this.valueIsTyped = valueIsTyped;
   3040    this._resultForCurrentValue = null;
   3041    this.inputField.value = val;
   3042    this.formatValue();
   3043 
   3044    if (actionType !== undefined) {
   3045      this.setAttribute("actiontype", actionType);
   3046    } else {
   3047      this.removeAttribute("actiontype");
   3048    }
   3049 
   3050    // Dispatch ValueChange event for accessibility.
   3051    let event = this.document.createEvent("Events");
   3052    event.initEvent("ValueChange", true, true);
   3053    this.inputField.dispatchEvent(event);
   3054 
   3055    return val;
   3056  }
   3057 
   3058  /**
   3059   * Extracts a input value from a UrlbarResult, used when filling the input
   3060   * field on selecting a result.
   3061   *
   3062   * Some examples:
   3063   *  - If the result is a bookmark keyword or dynamic, the value will be
   3064   *    its `input` property.
   3065   *  - If the result is search, the value may be `keyword` combined with
   3066   *    `suggestion` or `query`.
   3067   *  - If the result is WebExtension Omnibox, the value will be extracted
   3068   *    from `content`.
   3069   *  - For results returning URLs the value may be `urlOverride` or `url`.
   3070   *
   3071   * @param {UrlbarResult} result
   3072   *   The result to extract the value from.
   3073   * @param {object} options
   3074   *   Options object.
   3075   * @param {string} [options.urlOverride]
   3076   *   For results normally returning a url string, this allows to override
   3077   *   it. A blank string may passed-in to clear the input.
   3078   * @param {HTMLElement} [options.element]
   3079   *   The element that was selected or picked, if available. For results that
   3080   *   have multiple selectable children, the value may be taken from a child
   3081   *   element rather than the result.
   3082   * @returns {string} The value.
   3083   */
   3084  #getValueFromResult(result, { urlOverride = null, element = null } = {}) {
   3085    switch (result.type) {
   3086      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
   3087        return result.payload.input;
   3088      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH: {
   3089        let value = "";
   3090        if (result.payload.keyword) {
   3091          value += result.payload.keyword + " ";
   3092        }
   3093        value += result.payload.suggestion || result.payload.query;
   3094        return value;
   3095      }
   3096      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
   3097        return result.payload.content;
   3098      case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC:
   3099        return (
   3100          element?.dataset.query ||
   3101          element?.dataset.url ||
   3102          result.payload.input ||
   3103          result.payload.query ||
   3104          ""
   3105        );
   3106      case lazy.UrlbarUtils.RESULT_TYPE.RESTRICT:
   3107        return result.payload.autofillKeyword + " ";
   3108      case lazy.UrlbarUtils.RESULT_TYPE.TIP: {
   3109        let value = element?.dataset.url || element?.dataset.input;
   3110        if (value) {
   3111          return value;
   3112        }
   3113        break;
   3114      }
   3115    }
   3116 
   3117    // Always respect a set urlOverride property.
   3118    if (urlOverride !== null) {
   3119      // This returns null for the empty string, allowing callers to clear the
   3120      // input by passing an empty string as urlOverride.
   3121      let url = URL.parse(urlOverride);
   3122      return url ? losslessDecodeURI(url.URI) : "";
   3123    }
   3124 
   3125    let parsedUrl = URL.parse(result.payload.url);
   3126    // If the url is not parsable, just return an empty string;
   3127    if (!parsedUrl) {
   3128      return "";
   3129    }
   3130 
   3131    let url = losslessDecodeURI(parsedUrl.URI);
   3132    // If the user didn't originally type a protocol, and we generated one,
   3133    // trim the http protocol from the input value, as https-first may upgrade
   3134    // it to https, breaking user expectations.
   3135    let stripHttp =
   3136      result.heuristic &&
   3137      result.payload.url.startsWith("http://") &&
   3138      this.userTypedValue &&
   3139      this.#getSchemelessInput(this.userTypedValue) ==
   3140        Ci.nsILoadInfo.SchemelessInputTypeSchemeless;
   3141    if (!stripHttp) {
   3142      return url;
   3143    }
   3144    // Attempt to trim the url. If doing so results in a string that is
   3145    // interpreted as search (e.g. unknown single word host, or domain suffix),
   3146    // use the unmodified url instead. Otherwise, if the user edits the url
   3147    // and confirms the new value, we may transform the url into a search.
   3148    let trimmedUrl = lazy.UrlbarUtils.stripPrefixAndTrim(url, { stripHttp })[0];
   3149    let isSearch = !!this._getURIFixupInfo(trimmedUrl)?.keywordAsSent;
   3150    if (isSearch) {
   3151      // Although https-first might not respect the shown protocol, converting
   3152      // the result to a search would be more disruptive.
   3153      return url;
   3154    }
   3155    return trimmedUrl;
   3156  }
   3157 
   3158  /**
   3159   * Extracts from a result the value to use for the `actiontype` attribute.
   3160   *
   3161   * @param {UrlbarResult} result The UrlbarResult to consider.
   3162   *
   3163   * @returns {string} The `actiontype` value, or undefined.
   3164   */
   3165  #getActionTypeFromResult(result) {
   3166    switch (result.type) {
   3167      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
   3168        return "switchtab";
   3169      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
   3170        return "extension";
   3171      default:
   3172        return undefined;
   3173    }
   3174  }
   3175 
   3176  /**
   3177   * Resets some state so that searches from the user's previous interaction
   3178   * with the input don't interfere with searches from a new interaction.
   3179   */
   3180  _resetSearchState() {
   3181    this._lastSearchString = this.value;
   3182    this._autofillPlaceholder = null;
   3183  }
   3184 
   3185  /**
   3186   * Autofills the autofill placeholder string if appropriate, and determines
   3187   * whether autofill should be allowed for the new search started by an input
   3188   * event.
   3189   *
   3190   * @param {string} value
   3191   *   The new search string.
   3192   * @returns {boolean}
   3193   *   Whether autofill should be allowed in the new search.
   3194   */
   3195  _maybeAutofillPlaceholder(value) {
   3196    // We allow autofill in local but not remote search modes.
   3197    let allowAutofill =
   3198      this.selectionEnd == value.length &&
   3199      !this.searchMode?.engineName &&
   3200      this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.SEARCH;
   3201 
   3202    if (!allowAutofill) {
   3203      this.#clearAutofill();
   3204      return false;
   3205    }
   3206 
   3207    // Determine whether we can autofill the placeholder.  The placeholder is a
   3208    // value that we autofill now, when the search starts and before we wait on
   3209    // its first result, in order to prevent a flicker in the input caused by
   3210    // the previous autofilled substring disappearing and reappearing when the
   3211    // first result arrives.  Of course we can only autofill the placeholder if
   3212    // it starts with the new search string, and we shouldn't autofill anything
   3213    // if the caret isn't at the end of the input.
   3214    let canAutofillPlaceholder = false;
   3215    if (this._autofillPlaceholder) {
   3216      if (this._autofillPlaceholder.type == "adaptive") {
   3217        canAutofillPlaceholder =
   3218          value.length >=
   3219            this._autofillPlaceholder.adaptiveHistoryInput.length &&
   3220          this._autofillPlaceholder.value
   3221            .toLocaleLowerCase()
   3222            .startsWith(value.toLocaleLowerCase());
   3223      } else {
   3224        canAutofillPlaceholder = lazy.UrlbarUtils.canAutofillURL(
   3225          this._autofillPlaceholder.value,
   3226          value
   3227        );
   3228      }
   3229    }
   3230 
   3231    if (!canAutofillPlaceholder) {
   3232      this._autofillPlaceholder = null;
   3233    } else if (
   3234      this._autofillPlaceholder &&
   3235      this.selectionEnd == this.value.length &&
   3236      this._enableAutofillPlaceholder
   3237    ) {
   3238      let autofillValue =
   3239        value + this._autofillPlaceholder.value.substring(value.length);
   3240      this._autofillValue({
   3241        value: autofillValue,
   3242        selectionStart: value.length,
   3243        selectionEnd: autofillValue.length,
   3244        type: this._autofillPlaceholder.type,
   3245        adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput,
   3246        untrimmedValue: this._autofillPlaceholder.untrimmedValue,
   3247      });
   3248    }
   3249 
   3250    return true;
   3251  }
   3252 
   3253  /**
   3254   * Invoked on overflow/underflow/scrollend events to update attributes
   3255   * related to the input text directionality. Overflow fade masks use these
   3256   * attributes to appear at the proper side of the urlbar.
   3257   */
   3258  updateTextOverflow() {
   3259    if (!this._overflowing) {
   3260      this.removeAttribute("textoverflow");
   3261      return;
   3262    }
   3263 
   3264    let isRTL =
   3265      this.getAttribute("domaindir") === "rtl" &&
   3266      lazy.UrlbarUtils.isTextDirectionRTL(this.value, this.window);
   3267 
   3268    this.window.promiseDocumentFlushed(() => {
   3269      // Check overflow again to ensure it didn't change in the meanwhile.
   3270      let input = this.inputField;
   3271      if (input && this._overflowing) {
   3272        // Normally we overflow at the end side of the text direction, though
   3273        // RTL domains may cause us to overflow at the opposite side.
   3274        // The outcome differs depending on the input field contents and applied
   3275        // formatting, and reports the final state of all the scrolling into an
   3276        // attribute available to css rules.
   3277        // Note it's also possible to scroll an unfocused input field using
   3278        // SHIFT + mousewheel on Windows, or with just the mousewheel / touchpad
   3279        // scroll (without modifiers) on Mac.
   3280        let side = "both";
   3281        if (isRTL) {
   3282          if (input.scrollLeft == 0) {
   3283            side = "left";
   3284          } else if (input.scrollLeft == input.scrollLeftMin) {
   3285            side = "right";
   3286          }
   3287        } else if (input.scrollLeft == 0) {
   3288          side = "right";
   3289        } else if (input.scrollLeft == input.scrollLeftMax) {
   3290          side = "left";
   3291        }
   3292 
   3293        this.window.requestAnimationFrame(() => {
   3294          // And check once again, since we might have stopped overflowing
   3295          // since the promiseDocumentFlushed callback fired.
   3296          if (this._overflowing) {
   3297            this.setAttribute("textoverflow", side);
   3298          }
   3299        });
   3300      }
   3301    });
   3302  }
   3303 
   3304  _updateUrlTooltip() {
   3305    if (this.focused || !this._overflowing) {
   3306      this.inputField.removeAttribute("title");
   3307    } else {
   3308      this.inputField.setAttribute("title", this.untrimmedValue);
   3309    }
   3310  }
   3311 
   3312  _getSelectedValueForClipboard() {
   3313    let selectedVal = this.#selectedText;
   3314 
   3315    // Handle multiple-range selection as a string for simplicity.
   3316    if (this.editor.selection.rangeCount > 1) {
   3317      return selectedVal;
   3318    }
   3319 
   3320    // If the selection doesn't start at the beginning or doesn't span the
   3321    // full domain or the URL bar is modified or there is no text at all,
   3322    // nothing else to do here.
   3323    // TODO (Bug 1908360): the valueIsTyped usage here is confusing, as often
   3324    // it doesn't really indicate a user typed a value, it's rather used as
   3325    // a way to tell if the value was modified.
   3326    if (
   3327      this.selectionStart > 0 ||
   3328      selectedVal == "" ||
   3329      (this.valueIsTyped && !this._protocolIsTrimmed)
   3330    ) {
   3331      return selectedVal;
   3332    }
   3333 
   3334    // The selection doesn't span the full domain if it doesn't contain a slash and is
   3335    // followed by some character other than a slash.
   3336    if (!selectedVal.includes("/")) {
   3337      let remainder = this.value.replace(selectedVal, "");
   3338      if (remainder != "" && remainder[0] != "/") {
   3339        return selectedVal;
   3340      }
   3341    }
   3342 
   3343    let uri;
   3344    if (this.getAttribute("pageproxystate") == "valid") {
   3345      uri = this.#isOpenedPageInBlankTargetLoading
   3346        ? this.window.gBrowser.selectedBrowser.browsingContext
   3347            .nonWebControlledBlankURI
   3348        : this.window.gBrowser.currentURI;
   3349    } else {
   3350      // The value could be:
   3351      // 1. a trimmed url, set by selecting a result
   3352      // 2. a search string set by selecting a result
   3353      // 3. a url that was confirmed but didn't finish loading yet
   3354      // If it's an url the untrimmedValue should resolve to a valid URI,
   3355      // otherwise it's a search string that should be copied as-is.
   3356 
   3357      // If the copied text is that autofilled value, return the url including
   3358      // the protocol from its suggestion.
   3359      let result = this._resultForCurrentValue;
   3360 
   3361      if (result?.autofill?.value == selectedVal) {
   3362        return result.payload.url;
   3363      }
   3364 
   3365      uri = URL.parse(this._untrimmedValue)?.URI;
   3366      if (!uri) {
   3367        return selectedVal;
   3368      }
   3369    }
   3370    uri = this.makeURIReadable(uri);
   3371    let displaySpec = uri.displaySpec;
   3372 
   3373    // If the entire URL is selected, just use the actual loaded URI,
   3374    // unless we want a decoded URI, or it's a data: or javascript: URI,
   3375    // since those are hard to read when encoded.
   3376    if (
   3377      this.value == selectedVal &&
   3378      !uri.schemeIs("javascript") &&
   3379      !uri.schemeIs("data") &&
   3380      !lazy.UrlbarPrefs.get("decodeURLsOnCopy")
   3381    ) {
   3382      return displaySpec;
   3383    }
   3384 
   3385    // Just the beginning of the URL is selected, or we want a decoded
   3386    // url. First check for a trimmed value.
   3387 
   3388    if (
   3389      !selectedVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
   3390      // Note _trimValue may also trim a trailing slash, thus we can't just do
   3391      // a straight string compare to tell if the protocol was trimmed.
   3392      !displaySpec.startsWith(this._trimValue(displaySpec))
   3393    ) {
   3394      selectedVal = lazy.BrowserUIUtils.trimURLProtocol + selectedVal;
   3395    }
   3396 
   3397    // If selection starts from the beginning and part or all of the URL
   3398    // is selected, we check for decoded characters and encode them.
   3399    // Unless decodeURLsOnCopy is set. Do not encode data: URIs.
   3400    if (!lazy.UrlbarPrefs.get("decodeURLsOnCopy") && !uri.schemeIs("data")) {
   3401      try {
   3402        if (URL.canParse(selectedVal)) {
   3403          // Use encodeURI instead of URL.href because we don't want
   3404          // trailing slash.
   3405          selectedVal = encodeURI(selectedVal);
   3406        }
   3407      } catch (ex) {
   3408        // URL is invalid. Return original selected value.
   3409      }
   3410    }
   3411 
   3412    return selectedVal;
   3413  }
   3414 
   3415  _toggleActionOverride(event) {
   3416    if (
   3417      event.keyCode == KeyEvent.DOM_VK_SHIFT ||
   3418      event.keyCode == KeyEvent.DOM_VK_ALT ||
   3419      event.keyCode ==
   3420        (AppConstants.platform == "macosx"
   3421          ? KeyEvent.DOM_VK_META
   3422          : KeyEvent.DOM_VK_CONTROL)
   3423    ) {
   3424      if (event.type == "keydown") {
   3425        this._actionOverrideKeyCount++;
   3426        this.toggleAttribute("action-override", true);
   3427        this.view.panel.setAttribute("action-override", true);
   3428      } else if (
   3429        this._actionOverrideKeyCount &&
   3430        --this._actionOverrideKeyCount == 0
   3431      ) {
   3432        this._clearActionOverride();
   3433      }
   3434    }
   3435  }
   3436 
   3437  _clearActionOverride() {
   3438    this._actionOverrideKeyCount = 0;
   3439    this.removeAttribute("action-override");
   3440    this.view.panel.removeAttribute("action-override");
   3441  }
   3442 
   3443  /**
   3444   * Records in telemetry that a search is being loaded,
   3445   * updates an incremental total number of searches in a pref,
   3446   * and informs ASRouter that a search has occurred via a trigger send
   3447   *
   3448   * @param {nsISearchEngine} engine
   3449   *   The engine to generate the query for.
   3450   * @param {Event} event
   3451   *   The event that triggered this query.
   3452   * @param {object} [searchActionDetails]
   3453   *   The details associated with this search query.
   3454   * @param {boolean} [searchActionDetails.isSuggestion]
   3455   *   True if this query was initiated from a suggestion from the search engine.
   3456   * @param {boolean} [searchActionDetails.alias]
   3457   *   True if this query was initiated via a search alias.
   3458   * @param {boolean} [searchActionDetails.isFormHistory]
   3459   *   True if this query was initiated from a form history result.
   3460   * @param {string} [searchActionDetails.url]
   3461   *   The url this query was triggered with.
   3462   * @param {MozBrowser} [browser]
   3463   *   The browser where the search is being opened.
   3464   *   Defaults to the window's selected browser.
   3465   */
   3466  _recordSearch(
   3467    engine,
   3468    event,
   3469    searchActionDetails = {},
   3470    browser = this.window.gBrowser.selectedBrowser
   3471  ) {
   3472    const isOneOff = this.view.oneOffSearchButtons?.eventTargetIsAOneOff(event);
   3473    const searchSource = this.getSearchSource(event);
   3474 
   3475    // Record when the user uses the search bar to be
   3476    // used for message targeting. This is arbitrarily capped
   3477    // at 100, only to prevent the number from growing ifinitely.
   3478    const totalSearches = Services.prefs.getIntPref(
   3479      "browser.search.totalSearches"
   3480    );
   3481    const totalSearchesCap = 100;
   3482    if (totalSearches < totalSearchesCap) {
   3483      Services.prefs.setIntPref(
   3484        "browser.search.totalSearches",
   3485        totalSearches + 1
   3486      );
   3487    }
   3488 
   3489    // Sending a trigger to ASRouter when a search happens
   3490    lazy.ASRouter.sendTriggerMessage({
   3491      browser,
   3492      id: "onSearch",
   3493      context: {
   3494        isSuggestion: searchActionDetails.isSuggestion || false,
   3495        searchSource,
   3496        isOneOff,
   3497      },
   3498    });
   3499 
   3500    lazy.BrowserSearchTelemetry.recordSearch(browser, engine, searchSource, {
   3501      ...searchActionDetails,
   3502      isOneOff,
   3503      newtabSessionId: this._handoffSession,
   3504    });
   3505  }
   3506 
   3507  /**
   3508   * Shortens the given value, usually by removing http:// and trailing slashes.
   3509   *
   3510   * @param {string} val
   3511   *   The string to be trimmed if it appears to be URI
   3512   * @returns {string}
   3513   *   The trimmed string
   3514   */
   3515  _trimValue(val) {
   3516    if (!this.#isAddressbar) {
   3517      return val;
   3518    }
   3519    let trimmedValue = lazy.UrlbarPrefs.get("trimURLs")
   3520      ? lazy.BrowserUIUtils.trimURL(val)
   3521      : val;
   3522    // Only trim value if the directionality doesn't change to RTL and we're not
   3523    // showing a strikeout https protocol.
   3524    return lazy.UrlbarUtils.isTextDirectionRTL(trimmedValue, this.window) ||
   3525      this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val)
   3526      ? val
   3527      : trimmedValue;
   3528  }
   3529 
   3530  /**
   3531   * Returns whether the passed-in event may represents a canonization request.
   3532   *
   3533   * @param {Event} event
   3534   *   An Event to examine.
   3535   * @returns {boolean}
   3536   *   Whether the event is a KeyboardEvent that triggers canonization.
   3537   */
   3538  #isCanonizeKeyboardEvent(event) {
   3539    return (
   3540      KeyboardEvent.isInstance(event) &&
   3541      event.keyCode == KeyEvent.DOM_VK_RETURN &&
   3542      (AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) &&
   3543      !event._disableCanonization &&
   3544      lazy.UrlbarPrefs.get("ctrlCanonizesURLs")
   3545    );
   3546  }
   3547 
   3548  /**
   3549   * If appropriate, this prefixes a search string with 'www.' and suffixes it
   3550   * with browser.fixup.alternate.suffix prior to navigating.
   3551   *
   3552   * @param {Event} event
   3553   *   The event that triggered this query.
   3554   * @param {string} value
   3555   *   The search string that should be canonized.
   3556   * @returns {string}
   3557   *   Returns the canonized URL if available and null otherwise.
   3558   */
   3559  _maybeCanonizeURL(event, value) {
   3560    // Only add the suffix when the URL bar value isn't already "URL-like",
   3561    // and only if we get a keyboard event, to match user expectations.
   3562    if (
   3563      this.sapName == "searchbar" ||
   3564      !this.#isCanonizeKeyboardEvent(event) ||
   3565      !/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(value)
   3566    ) {
   3567      return null;
   3568    }
   3569 
   3570    let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix");
   3571    if (!suffix.endsWith("/")) {
   3572      suffix += "/";
   3573    }
   3574 
   3575    // trim leading/trailing spaces (bug 233205)
   3576    value = value.trim();
   3577 
   3578    // Tack www. and suffix on.  If user has appended directories, insert
   3579    // suffix before them (bug 279035).  Be careful not to get two slashes.
   3580    let firstSlash = value.indexOf("/");
   3581    if (firstSlash >= 0) {
   3582      value =
   3583        value.substring(0, firstSlash) +
   3584        suffix +
   3585        value.substring(firstSlash + 1);
   3586    } else {
   3587      value = value + suffix;
   3588    }
   3589 
   3590    try {
   3591      const info = Services.uriFixup.getFixupURIInfo(
   3592        value,
   3593        Ci.nsIURIFixup.FIXUP_FLAGS_MAKE_ALTERNATE_URI
   3594      );
   3595      value = info.fixedURI.spec;
   3596    } catch (ex) {
   3597      console.error(`An error occured while trying to fixup "${value}"`, ex);
   3598    }
   3599 
   3600    this.value = value;
   3601    return value;
   3602  }
   3603 
   3604  /**
   3605   * Autofills a value into the input.  The value will be autofilled regardless
   3606   * of the input's current value.
   3607   *
   3608   * @param {object} options
   3609   *   The options object.
   3610   * @param {string} options.value
   3611   *   The value to autofill.
   3612   * @param {number} options.selectionStart
   3613   *   The new selectionStart.
   3614   * @param {number} options.selectionEnd
   3615   *   The new selectionEnd.
   3616   * @param {"origin" | "url" | "adaptive"} options.type
   3617   *   The autofill type, one of: "origin", "url", "adaptive"
   3618   * @param {string} options.adaptiveHistoryInput
   3619   *   If the autofill type is "adaptive", this is the matching `input` value
   3620   *   from adaptive history.
   3621   * @param {string} [options.untrimmedValue]
   3622   *   Untrimmed value including a protocol.
   3623   */
   3624  _autofillValue({
   3625    value,
   3626    selectionStart,
   3627    selectionEnd,
   3628    type,
   3629    adaptiveHistoryInput,
   3630    untrimmedValue,
   3631  }) {
   3632    // The autofilled value may be a URL that includes a scheme at the
   3633    // beginning.  Do not allow it to be trimmed.
   3634    this._setValue(value, { untrimmedValue });
   3635    this.inputField.setSelectionRange(selectionStart, selectionEnd);
   3636    this._autofillPlaceholder = {
   3637      value,
   3638      type,
   3639      adaptiveHistoryInput,
   3640      selectionStart,
   3641      selectionEnd,
   3642      untrimmedValue,
   3643    };
   3644  }
   3645 
   3646  /**
   3647   * Called when a menu item from results menu is picked.
   3648   *
   3649   * @param {UrlbarResult} result The result that was picked.
   3650   * @param {Event} event The event that picked the result.
   3651   * @param {HTMLElement} element the picked view element, if available.
   3652   * @param {object} browser The browser to use for the load.
   3653   */
   3654  #pickMenuResult(result, event, element, browser) {
   3655    this.controller.engagementEvent.record(event, {
   3656      result,
   3657      element,
   3658      searchString: this._lastSearchString,
   3659      selType: element.dataset.command,
   3660    });
   3661 
   3662    if (element.dataset.command == "manage") {
   3663      this.window.openPreferences("search-locationBar");
   3664      return;
   3665    }
   3666 
   3667    let url;
   3668    if (element.dataset.command == "help") {
   3669      url = result.payload.helpUrl;
   3670    }
   3671    url ||= element.dataset.url;
   3672 
   3673    if (!url) {
   3674      return;
   3675    }
   3676 
   3677    let where = this._whereToOpen(event);
   3678    if (element.dataset.command == "help" && where == "current") {
   3679      // Open help links in a new tab.
   3680      where = "tab";
   3681    }
   3682 
   3683    this.view.close({ elementPicked: true });
   3684 
   3685    this._loadURL(
   3686      url,
   3687      event,
   3688      where,
   3689      {
   3690        allowInheritPrincipal: false,
   3691        private: this.isPrivate,
   3692      },
   3693      {
   3694        source: result.source,
   3695        type: result.type,
   3696      },
   3697      browser
   3698    );
   3699  }
   3700 
   3701  /**
   3702   * Loads the url in the appropriate place.
   3703   *
   3704   * @param {string} url
   3705   *   The URL to open.
   3706   * @param {string} openUILinkWhere
   3707   *   Where we expect the result to be opened.
   3708   * @param {object} params
   3709   *   The parameters related to how and where the result will be opened.
   3710   *   Further supported paramters are listed in _loadURL.
   3711   * @param {object} [params.triggeringPrincipal]
   3712   *   The principal that the action was triggered from.
   3713   * @param {object} [resultDetails]
   3714   *   Details of the selected result, if any.
   3715   *   Further supported details are listed in _loadURL.
   3716   * @param {string} [resultDetails.searchTerm]
   3717   *   Search term of the result source, if any.
   3718   * @param {object} browser the browser to use for the load.
   3719   */
   3720  #prepareAddressbarLoad(
   3721    url,
   3722    openUILinkWhere,
   3723    params,
   3724    resultDetails = null,
   3725    browser
   3726  ) {
   3727    if (!this.#isAddressbar) {
   3728      throw new Error(
   3729        "Can't prepare addressbar load when this isn't an addressbar input"
   3730      );
   3731    }
   3732 
   3733    // No point in setting these because we'll handleRevert() a few rows below.
   3734    if (openUILinkWhere == "current") {
   3735      // Make sure URL is formatted properly (don't show punycode).
   3736      let formattedURL = url;
   3737      try {
   3738        formattedURL = losslessDecodeURI(new URL(url).URI);
   3739      } catch {}
   3740 
   3741      this.value =
   3742        lazy.UrlbarPrefs.isPersistedSearchTermsEnabled() &&
   3743        resultDetails?.searchTerm
   3744          ? resultDetails.searchTerm
   3745          : formattedURL;
   3746      browser.userTypedValue = this.value;
   3747    }
   3748 
   3749    // No point in setting this if we are loading in a new window.
   3750    if (
   3751      openUILinkWhere != "window" &&
   3752      this.window.gInitialPages.includes(url)
   3753    ) {
   3754      browser.initialPageLoadedFromUserAction = url;
   3755    }
   3756 
   3757    try {
   3758      lazy.UrlbarUtils.addToUrlbarHistory(url, this.window);
   3759    } catch (ex) {
   3760      // Things may go wrong when adding url to session history,
   3761      // but don't let that interfere with the loading of the url.
   3762      console.error(ex);
   3763    }
   3764 
   3765    // TODO: When bug 1498553 is resolved, we should be able to
   3766    // remove the !triggeringPrincipal condition here.
   3767    if (
   3768      !params.triggeringPrincipal ||
   3769      params.triggeringPrincipal.isSystemPrincipal
   3770    ) {
   3771      // Reset DOS mitigations for the basic auth prompt.
   3772      delete browser.authPromptAbuseCounter;
   3773 
   3774      // Reset temporary permissions on the current tab if the user reloads
   3775      // the tab via the urlbar.
   3776      if (
   3777        openUILinkWhere == "current" &&
   3778        browser.currentURI &&
   3779        url === browser.currentURI.spec
   3780      ) {
   3781        this.window.SitePermissions.clearTemporaryBlockPermissions(browser);
   3782      }
   3783    }
   3784 
   3785    // Specifies that the URL load was initiated by the URL bar.
   3786    params.initiatedByURLBar = true;
   3787  }
   3788 
   3789  /**
   3790   * Loads the url in the appropriate place.
   3791   *
   3792   * @param {string} url
   3793   *   The URL to open.
   3794   * @param {Event} event
   3795   *   The event that triggered to load the url.
   3796   * @param {string} openUILinkWhere
   3797   *   Where we expect the result to be opened.
   3798   * @param {object} params
   3799   *   The parameters related to how and where the result will be opened.
   3800   *   Further supported parameters are listed in utilityOverlay.js#openUILinkIn.
   3801   * @param {object} [params.triggeringPrincipal]
   3802   *   The principal that the action was triggered from.
   3803   * @param {nsIInputStream} [params.postData]
   3804   *   The POST data associated with a search submission.
   3805   * @param {boolean} [params.allowInheritPrincipal]
   3806   *   Whether the principal can be inherited.
   3807   * @param {nsILoadInfo.SchemelessInputType} [params.schemelessInput]
   3808   *   Whether the search/URL term was without an explicit scheme.
   3809   * @param {object} [resultDetails]
   3810   *   Details of the selected result, if any.
   3811   * @param {Values<typeof lazy.UrlbarUtils.RESULT_TYPE>} [resultDetails.type]
   3812   *   Details of the result type, if any.
   3813   * @param {string} [resultDetails.searchTerm]
   3814   *   Search term of the result source, if any.
   3815   * @param {Values<typeof lazy.UrlbarUtils.RESULT_SOURCE>} [resultDetails.source]
   3816   *   Details of the result source, if any.
   3817   * @param {object} browser [optional] the browser to use for the load.
   3818   */
   3819  _loadURL(
   3820    url,
   3821    event,
   3822    openUILinkWhere,
   3823    params,
   3824    resultDetails = null,
   3825    browser = this.window.gBrowser.selectedBrowser
   3826  ) {
   3827    if (this.#isAddressbar) {
   3828      this.#prepareAddressbarLoad(
   3829        url,
   3830        openUILinkWhere,
   3831        params,
   3832        resultDetails,
   3833        browser
   3834      );
   3835    }
   3836 
   3837    params.allowThirdPartyFixup = true;
   3838 
   3839    if (openUILinkWhere == "current") {
   3840      params.targetBrowser = browser;
   3841      params.indicateErrorPageLoad = true;
   3842      params.allowPinnedTabHostChange = true;
   3843      params.allowPopups = url.startsWith("javascript:");
   3844    } else {
   3845      params.initiatingDoc = this.window.document;
   3846    }
   3847 
   3848    if (
   3849      this._keyDownEnterDeferred &&
   3850      event?.keyCode === KeyEvent.DOM_VK_RETURN &&
   3851      openUILinkWhere === "current"
   3852    ) {
   3853      // In this case, we move the focus to the browser that loads the content
   3854      // upon key up the enter key.
   3855      // To do it, send avoidBrowserFocus flag to openTrustedLinkIn() to avoid
   3856      // focusing on the browser in the function. And also, set loadedContent
   3857      // flag that whether the content is loaded in the current tab by this enter
   3858      // key. _keyDownEnterDeferred promise is processed at key up the enter,
   3859      // focus on the browser passed by _keyDownEnterDeferred.resolve().
   3860      params.avoidBrowserFocus = true;
   3861      this._keyDownEnterDeferred.loadedContent = true;
   3862      this._keyDownEnterDeferred.resolve(browser);
   3863    }
   3864 
   3865    // Ensure the window gets the `private` feature if the current window
   3866    // is private, unless the caller explicitly requested not to.
   3867    if (this.isPrivate && !("private" in params)) {
   3868      params.private = true;
   3869    }
   3870 
   3871    // Focus the content area before triggering loads, since if the load
   3872    // occurs in a new tab, we want focus to be restored to the content
   3873    // area when the current tab is re-selected.
   3874    if (!params.avoidBrowserFocus) {
   3875      browser.focus();
   3876      // Make sure the domain name stays visible for spoof protection and usability.
   3877      this.inputField.setSelectionRange(0, 0);
   3878    }
   3879 
   3880    if (openUILinkWhere != "current" && this.sapName != "searchbar") {
   3881      this.handleRevert();
   3882    }
   3883 
   3884    // Notify about the start of navigation.
   3885    this.#notifyStartNavigation(resultDetails);
   3886 
   3887    try {
   3888      this.window.openTrustedLinkIn(url, openUILinkWhere, params);
   3889    } catch (ex) {
   3890      // This load can throw an exception in certain cases, which means
   3891      // we'll want to replace the URL with the loaded URL:
   3892      if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
   3893        this.handleRevert();
   3894      }
   3895    }
   3896 
   3897    // If we show the focus border after closing the view, it would appear to
   3898    // flash since this._on_blur would remove it immediately after.
   3899    this.view.close({ showFocusBorder: false });
   3900  }
   3901 
   3902  /**
   3903   * Determines where a URL/page should be opened.
   3904   *
   3905   * @param {Event} event the event triggering the opening.
   3906   * @returns {"current" | "tabshifted" | "tab" | "save" | "window"}
   3907   */
   3908  _whereToOpen(event) {
   3909    let isKeyboardEvent = KeyboardEvent.isInstance(event);
   3910    let reuseEmpty = isKeyboardEvent;
   3911    let where = undefined;
   3912    if (
   3913      isKeyboardEvent &&
   3914      (event.altKey || event.getModifierState("AltGraph"))
   3915    ) {
   3916      // We support using 'alt' to open in a tab, because ctrl/shift
   3917      // might be used for canonizing URLs:
   3918      where = event.shiftKey ? "tabshifted" : "tab";
   3919    } else if (this.#isCanonizeKeyboardEvent(event)) {
   3920      // If we're allowing canonization, and this is a canonization key event,
   3921      // open in current tab to avoid handling as new tab modifier.
   3922      where = "current";
   3923    } else {
   3924      where = lazy.BrowserUtils.whereToOpenLink(event, false, false);
   3925    }
   3926    let openInTabPref =
   3927      this.#sapName == "searchbar"
   3928        ? lazy.UrlbarPrefs.get("browser.search.openintab")
   3929        : lazy.UrlbarPrefs.get("openintab");
   3930    if (openInTabPref) {
   3931      if (where == "current") {
   3932        where = "tab";
   3933      } else if (where == "tab") {
   3934        where = "current";
   3935      }
   3936      reuseEmpty = true;
   3937    }
   3938    if (
   3939      where == "tab" &&
   3940      reuseEmpty &&
   3941      this.window.gBrowser.selectedTab.isEmpty
   3942    ) {
   3943      where = "current";
   3944    }
   3945    return where;
   3946  }
   3947 
   3948  _initCopyCutController() {
   3949    if (this._copyCutController) {
   3950      return;
   3951    }
   3952    this._copyCutController = new CopyCutController(this);
   3953    this.inputField.controllers.insertControllerAt(0, this._copyCutController);
   3954  }
   3955 
   3956  /**
   3957   * Searches the context menu for the location of a specific command.
   3958   *
   3959   * @param {string} menuItemCommand
   3960   *    The command to search for.
   3961   * @returns {HTMLElement}
   3962   *    Html element that matches the command or
   3963   *    the last element if we could not find the command.
   3964   */
   3965  #findMenuItemLocation(menuItemCommand) {
   3966    let inputBox = this.querySelector("moz-input-box");
   3967    let contextMenu = inputBox.menupopup;
   3968    let insertLocation = contextMenu.firstElementChild;
   3969    // find the location of the command
   3970    while (
   3971      insertLocation.nextElementSibling &&
   3972      insertLocation.getAttribute("cmd") != menuItemCommand
   3973    ) {
   3974      insertLocation = insertLocation.nextElementSibling;
   3975    }
   3976 
   3977    return insertLocation;
   3978  }
   3979 
   3980  /**
   3981   * Strips known tracking query parameters/ link decorators.
   3982   *
   3983   * @returns {nsIURI}
   3984   *   The stripped URI or original URI, if nothing can be
   3985   *   stripped
   3986   */
   3987  #stripURI() {
   3988    let copyString = this._getSelectedValueForClipboard();
   3989    if (!copyString) {
   3990      return null;
   3991    }
   3992    let strippedURI = null;
   3993 
   3994    // Error check occurs during isClipboardURIValid
   3995    let uri = Services.io.newURI(copyString);
   3996    try {
   3997      strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
   3998    } catch (e) {
   3999      console.warn(`stripForCopyOrShare: ${e.message}`);
   4000      return uri;
   4001    }
   4002 
   4003    if (strippedURI) {
   4004      return this.makeURIReadable(strippedURI);
   4005    }
   4006    return uri;
   4007  }
   4008 
   4009  /**
   4010   * Checks if the clipboard contains a valid URI
   4011   *
   4012   * @returns {true|false}
   4013   */
   4014  #isClipboardURIValid() {
   4015    let copyString = this._getSelectedValueForClipboard();
   4016    if (!copyString) {
   4017      return false;
   4018    }
   4019 
   4020    return URL.canParse(copyString);
   4021  }
   4022 
   4023  /**
   4024   * Checks if there is a query parameter that can be stripped
   4025   *
   4026   * @returns {true|false}
   4027   */
   4028  #canStrip() {
   4029    let copyString = this._getSelectedValueForClipboard();
   4030    if (!copyString) {
   4031      return false;
   4032    }
   4033    // throws if the selected string is not a valid URI
   4034    try {
   4035      let uri = Services.io.newURI(copyString);
   4036      return lazy.QueryStringStripper.canStripForShare(uri);
   4037    } catch (e) {
   4038      console.warn("canStrip failed!", e);
   4039      return false;
   4040    }
   4041  }
   4042 
   4043  /**
   4044   * Restores the untrimmed value in the urlbar.
   4045   *
   4046   * @param {object} [options]
   4047   *  Options for untrimming.
   4048   * @param {boolean} [options.moveCursorToStart]
   4049   *  Whether the cursor should be moved at position 0 after untrimming.
   4050   * @param {boolean} [options.ignoreSelection]
   4051   *  Whether this should untrim, regardless of the current selection state.
   4052   */
   4053  #maybeUntrimUrl({ moveCursorToStart = false, ignoreSelection = false } = {}) {
   4054    // Check if we can untrim the current value.
   4055    if (
   4056      !lazy.UrlbarPrefs.getScotchBonnetPref(
   4057        "untrimOnUserInteraction.featureGate"
   4058      ) ||
   4059      !this._protocolIsTrimmed ||
   4060      !this.focused ||
   4061      (!ignoreSelection && this.#allTextSelected)
   4062    ) {
   4063      return;
   4064    }
   4065 
   4066    let selectionStart = this.selectionStart;
   4067    let selectionEnd = this.selectionEnd;
   4068 
   4069    // Correct the selection taking the trimmed protocol into account.
   4070    let offset = lazy.BrowserUIUtils.trimURLProtocol.length;
   4071 
   4072    // In case of autofill, we may have to adjust its boundaries.
   4073    if (this._autofillPlaceholder) {
   4074      this._autofillPlaceholder.selectionStart += offset;
   4075      this._autofillPlaceholder.selectionEnd += offset;
   4076    }
   4077 
   4078    if (moveCursorToStart) {
   4079      this._setValue(this._untrimmedValue, {
   4080        valueIsTyped: this.valueIsTyped,
   4081      });
   4082      this.setSelectionRange(0, 0);
   4083      return;
   4084    }
   4085 
   4086    if (selectionStart == selectionEnd) {
   4087      // When cursor is at the end of the string, untrimming may
   4088      // reintroduced a trailing slash and we want to move past it.
   4089      if (selectionEnd == this.value.length) {
   4090        offset += 1;
   4091      }
   4092      selectionStart = selectionEnd += offset;
   4093    } else {
   4094      // There's a selection, so we must calculate both the initial
   4095      // protocol and the eventual trailing slash.
   4096      if (selectionStart != 0) {
   4097        selectionStart += offset;
   4098      } else {
   4099        // When selection starts at the beginning, the adjusted selection will
   4100        // include the protocol only if the selected text includes the host.
   4101        // The port is left out, as one may want to exclude it from the copy.
   4102        let prePathMinusPort;
   4103        try {
   4104          let uri = Services.io.newURI(this._untrimmedValue);
   4105          prePathMinusPort = [uri.userPass, uri.displayHost]
   4106            .filter(Boolean)
   4107            .join("@");
   4108        } catch (ex) {
   4109          lazy.logger.error("Should only try to untrim valid URLs");
   4110        }
   4111        if (!this.#selectedText.startsWith(prePathMinusPort)) {
   4112          selectionStart += offset;
   4113        }
   4114      }
   4115      if (selectionEnd == this.value.length) {
   4116        offset += 1;
   4117      }
   4118      selectionEnd += offset;
   4119    }
   4120 
   4121    this._setValue(this._untrimmedValue, {
   4122      valueIsTyped: this.valueIsTyped,
   4123    });
   4124 
   4125    this.setSelectionRange(selectionStart, selectionEnd);
   4126  }
   4127 
   4128  // The strip-on-share feature will strip known tracking/decorational
   4129  // query params from the URI and copy the stripped version to the clipboard.
   4130  _initStripOnShare() {
   4131    let contextMenu = this.querySelector("moz-input-box").menupopup;
   4132    let insertLocation = this.#findMenuItemLocation("cmd_copy");
   4133    // set up the menu item
   4134    let stripOnShare = this.document.createXULElement("menuitem");
   4135    this.document.l10n.setAttributes(
   4136      stripOnShare,
   4137      "text-action-copy-clean-link"
   4138    );
   4139    stripOnShare.setAttribute("anonid", "strip-on-share");
   4140    stripOnShare.id = "strip-on-share";
   4141 
   4142    insertLocation.insertAdjacentElement("afterend", stripOnShare);
   4143 
   4144    // Register listener that returns the stripped url or falls back
   4145    // to the original url if nothing can be stripped.
   4146    stripOnShare.addEventListener("command", () => {
   4147      let strippedURI = this.#stripURI();
   4148      lazy.ClipboardHelper.copyString(strippedURI.displaySpec);
   4149    });
   4150 
   4151    // Register a listener that hides the menu item if there is nothing to copy.
   4152    contextMenu.addEventListener("popupshowing", () => {
   4153      // feature is not enabled
   4154      if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) {
   4155        stripOnShare.setAttribute("hidden", true);
   4156        return;
   4157      }
   4158      let controller =
   4159        this.document.commandDispatcher.getControllerForCommand("cmd_copy");
   4160      if (
   4161        !controller.isCommandEnabled("cmd_copy") ||
   4162        !this.#isClipboardURIValid()
   4163      ) {
   4164        stripOnShare.setAttribute("hidden", true);
   4165        return;
   4166      }
   4167      stripOnShare.removeAttribute("hidden");
   4168      if (!this.#canStrip()) {
   4169        stripOnShare.setAttribute("disabled", true);
   4170        return;
   4171      }
   4172      stripOnShare.removeAttribute("disabled");
   4173    });
   4174  }
   4175 
   4176  _initPasteAndGo() {
   4177    let inputBox = this.querySelector("moz-input-box");
   4178    let contextMenu = inputBox.menupopup;
   4179    let insertLocation = this.#findMenuItemLocation("cmd_paste");
   4180    if (!insertLocation) {
   4181      return;
   4182    }
   4183 
   4184    let pasteAndGo = this.document.createXULElement("menuitem");
   4185    pasteAndGo.id = "paste-and-go";
   4186    let label = Services.strings
   4187      .createBundle("chrome://browser/locale/browser.properties")
   4188      .GetStringFromName("pasteAndGo.label");
   4189    pasteAndGo.setAttribute("label", label);
   4190    pasteAndGo.setAttribute("anonid", "paste-and-go");
   4191    pasteAndGo.addEventListener("command", () => {
   4192      this._suppressStartQuery = true;
   4193 
   4194      this.select();
   4195      this.window.goDoCommand("cmd_paste");
   4196      this.setResultForCurrentValue(null);
   4197      this.handleCommand();
   4198      this.controller.clearLastQueryContextCache();
   4199 
   4200      this._suppressStartQuery = false;
   4201    });
   4202 
   4203    contextMenu.addEventListener("popupshowing", () => {
   4204      // Close the results pane when the input field contextual menu is open,
   4205      // because paste and go doesn't want a result selection.
   4206      this.view.close();
   4207 
   4208      let controller =
   4209        this.document.commandDispatcher.getControllerForCommand("cmd_paste");
   4210      let enabled = controller.isCommandEnabled("cmd_paste");
   4211      if (enabled) {
   4212        pasteAndGo.removeAttribute("disabled");
   4213      } else {
   4214        pasteAndGo.setAttribute("disabled", "true");
   4215      }
   4216    });
   4217 
   4218    insertLocation.insertAdjacentElement("afterend", pasteAndGo);
   4219  }
   4220 
   4221  /**
   4222   * This notifies observers that the user has entered or selected something in
   4223   * the URL bar which will cause navigation.
   4224   *
   4225   * We use the observer service, so that we don't need to load extra facilities
   4226   * if they aren't being used, e.g. WebNavigation.
   4227   *
   4228   * @param {UrlbarResult} result
   4229   *   Details of the result that was selected, if any.
   4230   */
   4231  #notifyStartNavigation(result) {
   4232    if (this.#isAddressbar) {
   4233      Services.obs.notifyObservers({ result }, "urlbar-user-start-navigation");
   4234    }
   4235  }
   4236 
   4237  /**
   4238   * Returns a search mode object if a result should enter search mode when
   4239   * selected.
   4240   *
   4241   * @param {UrlbarResult} result
   4242   *   The result to check.
   4243   * @param {string} [entry]
   4244   *   If provided, this will be recorded as the entry point into search mode.
   4245   *   See setSearchMode() documentation for details.
   4246   * @returns {object} A search mode object. Null if search mode should not be
   4247   *   entered. See setSearchMode documentation for details.
   4248   */
   4249  _searchModeForResult(result, entry = null) {
   4250    // Search mode is determined by the result's keyword or engine.
   4251    if (
   4252      !result.payload.keyword &&
   4253      !result.payload.engine &&
   4254      !this.view.selectedElement.dataset?.engine
   4255    ) {
   4256      return null;
   4257    }
   4258 
   4259    let searchMode = this.searchModeForToken(result.payload.keyword);
   4260    // If result.originalEngine is set, then the user is Alt+Tabbing
   4261    // through the one-offs, so the keyword doesn't match the engine.
   4262    if (
   4263      !searchMode &&
   4264      result.payload.engine &&
   4265      (!result.payload.originalEngine ||
   4266        result.payload.engine == result.payload.originalEngine)
   4267    ) {
   4268      searchMode = { engineName: result.payload.engine };
   4269    } else if (this.view.selectedElement?.dataset.engine) {
   4270      searchMode = { engineName: this.view.selectedElement.dataset.engine };
   4271    }
   4272 
   4273    if (searchMode) {
   4274      if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) {
   4275        searchMode.restrictType = "keyword";
   4276      } else if (
   4277        lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(result.payload.keyword)
   4278      ) {
   4279        searchMode.restrictType = "symbol";
   4280      }
   4281      if (entry) {
   4282        searchMode.entry = entry;
   4283      } else {
   4284        switch (result.providerName) {
   4285          case "UrlbarProviderTopSites":
   4286            searchMode.entry = "topsites_urlbar";
   4287            break;
   4288          case "UrlbarProviderTabToSearch":
   4289            if (result.payload.dynamicType) {
   4290              searchMode.entry = "tabtosearch_onboard";
   4291            } else {
   4292              searchMode.entry = "tabtosearch";
   4293            }
   4294            break;
   4295          default:
   4296            searchMode.entry = "keywordoffer";
   4297            break;
   4298        }
   4299      }
   4300    }
   4301 
   4302    return searchMode;
   4303  }
   4304 
   4305  /**
   4306   * Updates the UI so that search mode is either entered or exited.
   4307   *
   4308   * @param {object} searchMode
   4309   *   See setSearchMode documentation.  If null, then search mode is exited.
   4310   */
   4311  _updateSearchModeUI(searchMode) {
   4312    let { engineName, source, isGeneralPurposeEngine } = searchMode || {};
   4313 
   4314    // As an optimization, bail if the given search mode is null but search mode
   4315    // is already inactive. Otherwise, browser_preferences_usage.js fails due to
   4316    // accessing the browser.urlbar.placeholderName pref (via the call to
   4317    // initPlaceHolder below) too many times. That test does not enter search mode,
   4318    // but it triggers many calls to this method with a null search mode, via setURI.
   4319    if (!engineName && !source && !this.hasAttribute("searchmode")) {
   4320      return;
   4321    }
   4322 
   4323    if (this._searchModeIndicatorTitle) {
   4324      this._searchModeIndicatorTitle.textContent = "";
   4325      this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
   4326    }
   4327 
   4328    if (!engineName && !source) {
   4329      this.removeAttribute("searchmode");
   4330      this.initPlaceHolder(true);
   4331      return;
   4332    }
   4333 
   4334    if (this.#isAddressbar) {
   4335      if (engineName) {
   4336        // Set text content for the search mode indicator.
   4337        this._searchModeIndicatorTitle.textContent = engineName;
   4338        this.document.l10n.setAttributes(
   4339          this.inputField,
   4340          isGeneralPurposeEngine
   4341            ? "urlbar-placeholder-search-mode-web-2"
   4342            : "urlbar-placeholder-search-mode-other-engine",
   4343          { name: engineName }
   4344        );
   4345      } else if (source) {
   4346        const messageIDs = {
   4347          actions: "urlbar-placeholder-search-mode-other-actions",
   4348          bookmarks: "urlbar-placeholder-search-mode-other-bookmarks",
   4349          engine: "urlbar-placeholder-search-mode-other-engine",
   4350          history: "urlbar-placeholder-search-mode-other-history",
   4351          tabs: "urlbar-placeholder-search-mode-other-tabs",
   4352        };
   4353        let sourceName = lazy.UrlbarUtils.getResultSourceName(source);
   4354        let l10nID = `urlbar-search-mode-${sourceName}`;
   4355        this.document.l10n.setAttributes(
   4356          this._searchModeIndicatorTitle,
   4357          l10nID
   4358        );
   4359        this.document.l10n.setAttributes(
   4360          this.inputField,
   4361          messageIDs[sourceName]
   4362        );
   4363      }
   4364    }
   4365 
   4366    this.toggleAttribute("searchmode", true);
   4367    // Clear autofill.
   4368    if (this._autofillPlaceholder && this.userTypedValue) {
   4369      this.value = this.userTypedValue;
   4370    }
   4371    // Search mode should only be active when pageproxystate is invalid.
   4372    if (this.getAttribute("pageproxystate") == "valid") {
   4373      this.value = "";
   4374      this.setPageProxyState("invalid", true);
   4375    }
   4376 
   4377    this.searchModeSwitcher?.onSearchModeChanged();
   4378  }
   4379 
   4380  /**
   4381   * Handles persisted search terms logic for the current browser. This manages
   4382   * state and updates the UI accordingly.
   4383   *
   4384   * @param {object} options
   4385   * @param {object} options.state
   4386   *   The state object for the currently viewed browser.
   4387   * @param {boolean} options.hideSearchTerms
   4388   *   True if we must hide the search terms and instead show the page URL.
   4389   * @param {boolean} options.dueToTabSwitch
   4390   *   True if the browser was revealed again due to a tab switch.
   4391   * @param {boolean} options.isSameDocument
   4392   *   True if the page load was same document.
   4393   * @param {nsIURI} [options.uri]
   4394   *   The latest URI of the page.
   4395   * @returns {boolean}
   4396   *   Whether search terms should persist.
   4397   */
   4398  #handlePersistedSearchTerms({
   4399    state,
   4400    hideSearchTerms,
   4401    dueToTabSwitch,
   4402    isSameDocument,
   4403    uri,
   4404  }) {
   4405    if (!lazy.UrlbarPrefs.isPersistedSearchTermsEnabled()) {
   4406      if (state.persist) {
   4407        this.removeAttribute("persistsearchterms");
   4408        delete state.persist;
   4409      }
   4410      return false;
   4411    }
   4412 
   4413    // The first time the browser URI has been loaded to the input. If
   4414    // persist is not defined, it is likely due to the tab being created in
   4415    // the background or an existing tab moved to a new window and we have to
   4416    // do the work for the first time.
   4417    let firstView = (!isSameDocument && !dueToTabSwitch) || !state.persist;
   4418 
   4419    let cachedUriDidChange =
   4420      state.persist?.originalURI &&
   4421      (!this.window.gBrowser.selectedBrowser.originalURI ||
   4422        !state.persist.originalURI.equals(
   4423          this.window.gBrowser.selectedBrowser.originalURI
   4424        ));
   4425 
   4426    // Capture the shouldPersist property if it exists before
   4427    // setPersistenceState potentially modifies it.
   4428    let wasPersisting = state.persist?.shouldPersist ?? false;
   4429 
   4430    if (firstView || cachedUriDidChange) {
   4431      lazy.UrlbarSearchTermsPersistence.setPersistenceState(
   4432        state,
   4433        this.window.gBrowser.selectedBrowser.originalURI
   4434      );
   4435    }
   4436    let shouldPersist =
   4437      !hideSearchTerms &&
   4438      lazy.UrlbarSearchTermsPersistence.shouldPersist(state, {
   4439        dueToTabSwitch,
   4440        isSameDocument,
   4441        uri: uri ?? this.window.gBrowser.currentURI,
   4442        userTypedValue: this.userTypedValue,
   4443        firstView,
   4444      });
   4445    // When persisting, userTypedValue should have a value consistent with the
   4446    // search terms to mimic a user typing the search terms.
   4447    // When turning off persist, check if the userTypedValue needs to be
   4448    // removed in order for the URL to return to the address bar. Single page
   4449    // application SERPs will load secondary search pages (e.g. Maps, Images)
   4450    // with the same document, which won't unset userTypedValue.
   4451    if (shouldPersist) {
   4452      this.userTypedValue = state.persist.searchTerms;
   4453    } else if (wasPersisting && !shouldPersist) {
   4454      this.userTypedValue = null;
   4455    }
   4456 
   4457    state.persist.shouldPersist = shouldPersist;
   4458    this.toggleAttribute("persistsearchterms", state.persist.shouldPersist);
   4459 
   4460    if (state.persist.shouldPersist && !isSameDocument) {
   4461      Glean.urlbarPersistedsearchterms.viewCount.add(1);
   4462    }
   4463 
   4464    return shouldPersist;
   4465  }
   4466 
   4467  /**
   4468   * Initializes the urlbar placeholder to the pre-saved engine name. We do this
   4469   * via a preference, to avoid needing to synchronously init the search service.
   4470   *
   4471   * This should be called around the time of DOMContentLoaded, so that it is
   4472   * initialized quickly before the user sees anything.
   4473   *
   4474   * Note: If the preference doesn't exist, we don't do anything as the default
   4475   * placeholder is a string which doesn't have the engine name; however, this
   4476   * can be overridden using the `force` parameter.
   4477   *
   4478   * @param {boolean} force If true and the preference doesn't exist, the
   4479   *                        placeholder will be set to the default version
   4480   *                        without an engine name ("Search or enter address").
   4481   */
   4482  initPlaceHolder(force = false) {
   4483    if (!this.#isAddressbar) {
   4484      return;
   4485    }
   4486 
   4487    let prefName =
   4488      "browser.urlbar.placeholderName" + (this.isPrivate ? ".private" : "");
   4489    let engineName = Services.prefs.getStringPref(prefName, "");
   4490    if (engineName || force) {
   4491      // We can do this directly, since we know we're at DOMContentLoaded.
   4492      this._setPlaceholder(engineName || null);
   4493    }
   4494  }
   4495 
   4496  /**
   4497   * Asynchronously changes the urlbar placeholder to the name of the default
   4498   * engine according to the search service when it is initialized.
   4499   *
   4500   * This should be called around the time of MozAfterPaint. Since the
   4501   * placeholder was already initialized to the pre-saved engine name by
   4502   * initPlaceHolder when this is called, the update is delayed to avoid
   4503   * confusing the user.
   4504   */
   4505  async delayedStartupInit() {
   4506    // Only delay if requested, and we're not displaying text in the URL bar
   4507    // currently.
   4508    if (!this.value) {
   4509      // Delays changing the URL Bar placeholder and Unified Search Button icon
   4510      // until the user is not going to be seeing it, e.g. when there is a value
   4511      // entered in the bar, or if there is a tab switch to a tab which has a url
   4512      // loaded. We delay the update until the user is out of search mode since
   4513      // an alternative placeholder is used in search mode.
   4514      let updateListener = () => {
   4515        if (this.value && !this.searchMode) {
   4516          // By the time the user has switched, they may have changed the engine
   4517          // again, so we need to call this function again but with the
   4518          // new engine name.
   4519          // No need to await for this to finish, we're in a listener here anyway.
   4520          this.searchModeSwitcher.updateSearchIcon();
   4521          this._updatePlaceholderFromDefaultEngine();
   4522          this.inputField.removeEventListener("input", updateListener);
   4523          this.window.gBrowser.tabContainer.removeEventListener(
   4524            "TabSelect",
   4525            updateListener
   4526          );
   4527        }
   4528      };
   4529 
   4530      this.inputField.addEventListener("input", updateListener);
   4531      this.window.gBrowser.tabContainer.addEventListener(
   4532        "TabSelect",
   4533        updateListener
   4534      );
   4535    } else {
   4536      await this._updatePlaceholderFromDefaultEngine();
   4537    }
   4538 
   4539    // If we haven't finished initializing, ensure the placeholder
   4540    // preference is set for the next startup.
   4541    if (this.#isAddressbar) {
   4542      lazy.SearchUIUtils.updatePlaceholderNamePreference(
   4543        await this._getDefaultSearchEngine(),
   4544        this.isPrivate
   4545      );
   4546    }
   4547  }
   4548 
   4549  /**
   4550   * Set Unified Search Button availability.
   4551   *
   4552   * @param {boolean} available If true Unified Search Button will be available.
   4553   */
   4554  setUnifiedSearchButtonAvailability(available) {
   4555    this.toggleAttribute("unifiedsearchbutton-available", available);
   4556    this.getBrowserState(
   4557      this.window.gBrowser.selectedBrowser
   4558    ).isUnifiedSearchButtonAvailable = available;
   4559  }
   4560 
   4561  /**
   4562   * Returns a Promise that resolves with default search engine.
   4563   *
   4564   * @returns {Promise<nsISearchEngine>}
   4565   */
   4566  _getDefaultSearchEngine() {
   4567    return this.isPrivate
   4568      ? Services.search.getDefaultPrivate()
   4569      : Services.search.getDefault();
   4570  }
   4571 
   4572  /**
   4573   * This is a wrapper around '_updatePlaceholder' that uses the appropriate
   4574   * default engine to get the engine name.
   4575   */
   4576  async _updatePlaceholderFromDefaultEngine() {
   4577    const defaultEngine = await this._getDefaultSearchEngine();
   4578    this._updatePlaceholder(defaultEngine.name);
   4579  }
   4580 
   4581  /**
   4582   * Updates the URLBar placeholder for the specified engine, delaying the
   4583   * update if required.
   4584   *
   4585   * Note: The engine name will only be displayed for application-provided
   4586   * engines, as we know they should have short names.
   4587   *
   4588   * @param {string}  engineName     The search engine name to use for the update.
   4589   */
   4590  _updatePlaceholder(engineName) {
   4591    if (!engineName) {
   4592      throw new Error("Expected an engineName to be specified");
   4593    }
   4594 
   4595    if (this.searchMode || !this.#isAddressbar) {
   4596      return;
   4597    }
   4598 
   4599    let engine = Services.search.getEngineByName(engineName);
   4600    if (engine.isConfigEngine) {
   4601      this._setPlaceholder(engineName);
   4602    } else {
   4603      // Display the default placeholder string.
   4604      this._setPlaceholder(null);
   4605    }
   4606  }
   4607 
   4608  /**
   4609   * Sets the URLBar placeholder to either something based on the engine name,
   4610   * or the default placeholder.
   4611   *
   4612   * @param {?string} engineName
   4613   * The name of the engine or null to use the default placeholder.
   4614   */
   4615  _setPlaceholder(engineName) {
   4616    if (!this.#isAddressbar) {
   4617      this.document.l10n.setAttributes(this.inputField, "searchbar-input");
   4618      return;
   4619    }
   4620 
   4621    let l10nId;
   4622    if (lazy.UrlbarPrefs.get("keyword.enabled")) {
   4623      l10nId = engineName
   4624        ? "urlbar-placeholder-with-name"
   4625        : "urlbar-placeholder";
   4626    } else {
   4627      l10nId = "urlbar-placeholder-keyword-disabled";
   4628    }
   4629 
   4630    this.document.l10n.setAttributes(
   4631      this.inputField,
   4632      l10nId,
   4633      l10nId == "urlbar-placeholder-with-name"
   4634        ? { name: engineName }
   4635        : undefined
   4636    );
   4637  }
   4638 
   4639  /**
   4640   * Determines if we should select all the text in the Urlbar based on the
   4641   *  Urlbar state, and whether the selection is empty.
   4642   */
   4643  #maybeSelectAll() {
   4644    if (
   4645      !this._preventClickSelectsAll &&
   4646      this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING &&
   4647      this.focused &&
   4648      this.inputField.selectionStart == this.inputField.selectionEnd
   4649    ) {
   4650      this.select();
   4651    }
   4652  }
   4653 
   4654  // Event handlers below.
   4655 
   4656  _on_command(event) {
   4657    // Something is executing a command, likely causing a focus change. This
   4658    // should not be recorded as an abandonment. If the user is selecting a
   4659    // result menu item or entering search mode from a one-off, then they are
   4660    // in the same engagement and we should not discard.
   4661    if (
   4662      !event.target.classList.contains("urlbarView-result-menuitem") &&
   4663      (!event.target.classList.contains("searchbar-engine-one-off-item") ||
   4664        this.searchMode?.entry != "oneoff")
   4665    ) {
   4666      this.controller.engagementEvent.discard();
   4667    }
   4668  }
   4669 
   4670  _on_blur(event) {
   4671    lazy.logger.debug("Blur Event");
   4672    // We cannot count every blur events after a missed engagement as abandoment
   4673    // because the user may have clicked on some view element that executes
   4674    // a command causing a focus change. For example opening preferences from
   4675    // the oneoff settings button.
   4676    // For now we detect that case by discarding the event on command, but we
   4677    // may want to figure out a more robust way to detect abandonment.
   4678    this.controller.engagementEvent.record(event, {
   4679      searchString: this._lastSearchString,
   4680      searchSource: this.getSearchSource(event),
   4681    });
   4682 
   4683    this.focusedViaMousedown = false;
   4684    this._handoffSession = undefined;
   4685    this._isHandoffSession = false;
   4686    this.removeAttribute("focused");
   4687 
   4688    if (this._autofillPlaceholder && this.userTypedValue) {
   4689      // If we were autofilling, remove the autofilled portion, by restoring
   4690      // the value to the last typed one.
   4691      this.value = this.userTypedValue;
   4692    } else if (
   4693      this.value == this._untrimmedValue &&
   4694      !this.userTypedValue &&
   4695      !this.focused
   4696    ) {
   4697      // If the value was untrimmed by _on_focus and didn't change, trim it.
   4698      this.value = this._untrimmedValue;
   4699    } else {
   4700      // We're not updating the value, so just format it.
   4701      this.formatValue();
   4702    }
   4703 
   4704    this._resetSearchState();
   4705 
   4706    // In certain cases, like holding an override key and confirming an entry,
   4707    // we don't key a keyup event for the override key, thus we make this
   4708    // additional cleanup on blur.
   4709    this._clearActionOverride();
   4710 
   4711    // The extension input sessions depends more on blur than on the fact we
   4712    // actually cancel a running query, so we do it here.
   4713    if (lazy.ExtensionSearchHandler.hasActiveInputSession()) {
   4714      lazy.ExtensionSearchHandler.handleInputCancelled();
   4715    }
   4716 
   4717    // Respect the autohide preference for easier inspecting/debugging via
   4718    // the browser toolbox.
   4719    if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   4720      this.view.close();
   4721    }
   4722 
   4723    // We may have hidden popup notifications, show them again if necessary.
   4724    if (
   4725      this.getAttribute("pageproxystate") != "valid" &&
   4726      this.window.UpdatePopupNotificationsVisibility
   4727    ) {
   4728      this.window.UpdatePopupNotificationsVisibility();
   4729    }
   4730 
   4731    // If user move the focus to another component while pressing Enter key,
   4732    // then keyup at that component, as we can't get the event, clear the promise.
   4733    if (this._keyDownEnterDeferred) {
   4734      this._keyDownEnterDeferred.resolve();
   4735      this._keyDownEnterDeferred = null;
   4736    }
   4737    this._isKeyDownWithCtrl = false;
   4738    this._isKeyDownWithMeta = false;
   4739    this._isKeyDownWithMetaAndLeft = false;
   4740 
   4741    Services.obs.notifyObservers(null, "urlbar-blur");
   4742  }
   4743 
   4744  _on_click(event) {
   4745    switch (event.target) {
   4746      case this.inputField:
   4747      case this._inputContainer:
   4748        this.#maybeSelectAll();
   4749        this.#maybeUntrimUrl();
   4750        break;
   4751 
   4752      case this._searchModeIndicatorClose:
   4753        if (event.button != 2) {
   4754          this.searchMode = null;
   4755          if (this.view.oneOffSearchButtons) {
   4756            this.view.oneOffSearchButtons.selectedButton = null;
   4757          }
   4758          if (this.view.isOpen) {
   4759            this.startQuery({
   4760              event,
   4761            });
   4762          }
   4763        }
   4764        break;
   4765 
   4766      case this._revertButton:
   4767        this.handleRevert();
   4768        this.select();
   4769        break;
   4770 
   4771      case this.goButton:
   4772        this.handleCommand(event);
   4773        break;
   4774    }
   4775  }
   4776 
   4777  _on_contextmenu(event) {
   4778    this.#lazy.addSearchEngineHelper.refreshContextMenu(event);
   4779 
   4780    // Context menu opened via keyboard shortcut.
   4781    if (!event.button) {
   4782      return;
   4783    }
   4784 
   4785    this.#maybeSelectAll();
   4786  }
   4787 
   4788  _on_focus(event) {
   4789    lazy.logger.debug("Focus Event");
   4790    if (!this._hideFocus) {
   4791      this.toggleAttribute("focused", true);
   4792    }
   4793 
   4794    // If the value was trimmed, check whether we should untrim it.
   4795    // This is necessary when a protocol was typed, but the whole url has
   4796    // invalid parts, like the origin, then editing and confirming the trimmed
   4797    // value would execute a search instead of visiting the typed url.
   4798    if (this._protocolIsTrimmed) {
   4799      let untrim = false;
   4800      let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
   4801      if (fixedURI) {
   4802        try {
   4803          let expectedURI = Services.io.newURI(this._untrimmedValue);
   4804          if (
   4805            lazy.UrlbarPrefs.getScotchBonnetPref("trimHttps") &&
   4806            this._untrimmedValue.startsWith("https://")
   4807          ) {
   4808            untrim =
   4809              fixedURI.displaySpec.replace("http://", "https://") !=
   4810              expectedURI.displaySpec; // FIXME bug 1847723: Figure out a way to do this without manually messing with the fixed up URI.
   4811          } else {
   4812            untrim = fixedURI.displaySpec != expectedURI.displaySpec;
   4813          }
   4814        } catch (ex) {
   4815          untrim = true;
   4816        }
   4817      }
   4818      if (untrim) {
   4819        this._setValue(this._untrimmedValue);
   4820      }
   4821    }
   4822 
   4823    if (this.focusedViaMousedown) {
   4824      this.view.autoOpen({ event });
   4825    } else {
   4826      if (this._untrimOnFocusAfterKeydown) {
   4827        // While the mousedown focus has more complex implications due to drag
   4828        // and double-click select, we can untrim immediately when the urlbar is
   4829        // focused by a keyboard shortcut.
   4830        this.#maybeUntrimUrl({ ignoreSelection: true });
   4831      }
   4832 
   4833      if (this.inputField.hasAttribute("refocused-by-panel")) {
   4834        this.#maybeSelectAll();
   4835      }
   4836    }
   4837 
   4838    this._updateUrlTooltip();
   4839    this.formatValue();
   4840 
   4841    // Hide popup notifications, to reduce visual noise.
   4842    if (
   4843      this.getAttribute("pageproxystate") != "valid" &&
   4844      this.window.UpdatePopupNotificationsVisibility
   4845    ) {
   4846      this.window.UpdatePopupNotificationsVisibility();
   4847    }
   4848 
   4849    Services.obs.notifyObservers(null, "urlbar-focus");
   4850  }
   4851 
   4852  _on_mouseover() {
   4853    this._updateUrlTooltip();
   4854  }
   4855 
   4856  _on_draggableregionleftmousedown() {
   4857    if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   4858      this.view.close();
   4859    }
   4860  }
   4861 
   4862  _on_mousedown(event) {
   4863    switch (event.currentTarget) {
   4864      case this: {
   4865        this._mousedownOnUrlbarDescendant = true;
   4866        if (
   4867          event.composedTarget != this.inputField &&
   4868          event.composedTarget != this._inputContainer
   4869        ) {
   4870          break;
   4871        }
   4872 
   4873        this.focusedViaMousedown = !this.focused;
   4874        this._preventClickSelectsAll = this.focused;
   4875 
   4876        // Keep the focus status, since the attribute may be changed
   4877        // upon calling this.focus().
   4878        const hasFocus = this.hasAttribute("focused");
   4879        if (event.composedTarget != this.inputField) {
   4880          this.focus();
   4881        }
   4882 
   4883        // The rest of this case only cares about left clicks.
   4884        if (event.button != 0) {
   4885          break;
   4886        }
   4887 
   4888        // Clear any previous selection unless we are focused, to ensure it
   4889        // doesn't affect drag selection.
   4890        if (this.focusedViaMousedown) {
   4891          this.inputField.setSelectionRange(0, 0);
   4892        }
   4893 
   4894        // Do not suppress the focus border if we are already focused. If we
   4895        // did, we'd hide the focus border briefly then show it again if the
   4896        // user has Top Sites disabled, creating a flashing effect.
   4897        this.view.autoOpen({
   4898          event,
   4899          suppressFocusBorder: !hasFocus,
   4900        });
   4901        break;
   4902      }
   4903      case this.window:
   4904        if (this._mousedownOnUrlbarDescendant) {
   4905          this._mousedownOnUrlbarDescendant = false;
   4906          break;
   4907        }
   4908        // Don't close the view when clicking on a tab; we may want to keep the
   4909        // view open on tab switch, and the TabSelect event arrived earlier.
   4910        if (event.target.closest("tab")) {
   4911          break;
   4912        }
   4913 
   4914        // Close the view when clicking on toolbars and other UI pieces that
   4915        // might not automatically remove focus from the input.
   4916        // Respect the autohide preference for easier inspecting/debugging via
   4917        // the browser toolbox.
   4918        if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   4919          if (this.view.isOpen && !this.hasAttribute("focused")) {
   4920            // In this case, as blur event never happen from the inputField, we
   4921            // record abandonment event explicitly.
   4922            let blurEvent = new FocusEvent("blur", {
   4923              relatedTarget: this.inputField,
   4924            });
   4925            this.controller.engagementEvent.record(blurEvent, {
   4926              searchString: this._lastSearchString,
   4927              searchSource: this.getSearchSource(blurEvent),
   4928            });
   4929          }
   4930 
   4931          this.view.close();
   4932        }
   4933        break;
   4934    }
   4935  }
   4936 
   4937  _on_input(event) {
   4938    if (
   4939      this._autofillPlaceholder &&
   4940      this.value === this.userTypedValue &&
   4941      (event.inputType === "deleteContentBackward" ||
   4942        event.inputType === "deleteContentForward")
   4943    ) {
   4944      // Take a telemetry if user deleted whole autofilled value.
   4945      Glean.urlbar.autofillDeletion.add(1);
   4946    }
   4947 
   4948    let value = this.value;
   4949    this.valueIsTyped = true;
   4950    this._untrimmedValue = value;
   4951    this._protocolIsTrimmed = false;
   4952    this._resultForCurrentValue = null;
   4953 
   4954    this.userTypedValue = value;
   4955    // Unset userSelectionBehavior because the user is modifying the search
   4956    // string, thus there's no valid selection. This is also used by the view
   4957    // to set "aria-activedescendant", thus it should never get stale.
   4958    this.controller.userSelectionBehavior = "none";
   4959 
   4960    let compositionState = this.#compositionState;
   4961    let compositionClosedPopup = this.#compositionClosedPopup;
   4962 
   4963    // Clear composition values if we're no more composing.
   4964    if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
   4965      this.#compositionState = lazy.UrlbarUtils.COMPOSITION.NONE;
   4966      this.#compositionClosedPopup = false;
   4967    }
   4968 
   4969    this.toggleAttribute("usertyping", value);
   4970    this.removeAttribute("actiontype");
   4971 
   4972    if (
   4973      this.getAttribute("pageproxystate") == "valid" &&
   4974      this.value != this._lastValidURLStr
   4975    ) {
   4976      this.setPageProxyState("invalid", true);
   4977    }
   4978 
   4979    let state = this.getBrowserState(this.window.gBrowser.selectedBrowser);
   4980    if (
   4981      state.persist?.shouldPersist &&
   4982      this.value !== state.persist.searchTerms
   4983    ) {
   4984      state.persist.shouldPersist = false;
   4985      this.removeAttribute("persistsearchterms");
   4986    }
   4987 
   4988    if (this.view.isOpen) {
   4989      if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) {
   4990        // UrlbarView rolls up all popups when it opens, but we should
   4991        // do the same for UrlbarInput when it's already open in case
   4992        // a tab preview was opened
   4993        this.window.docShell.treeOwner
   4994          .QueryInterface(Ci.nsIInterfaceRequestor)
   4995          .getInterface(Ci.nsIAppWindow)
   4996          .rollupAllPopups();
   4997      }
   4998      if (!value && !lazy.UrlbarPrefs.get("suggest.topsites")) {
   4999        this.view.clear();
   5000        if (!this.searchMode || !this.view.oneOffSearchButtons?.hasView) {
   5001          this.view.close();
   5002          return;
   5003        }
   5004      }
   5005    } else {
   5006      this.view.clear();
   5007    }
   5008 
   5009    this.view.removeAccessibleFocus();
   5010 
   5011    // During composition with an IME, the following events happen in order:
   5012    // 1. a compositionstart event
   5013    // 2. some input events
   5014    // 3. a compositionend event
   5015    // 4. an input event
   5016 
   5017    // We should do nothing during composition or if composition was canceled
   5018    // and we didn't close the popup on composition start.
   5019    if (
   5020      !lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") &&
   5021      (compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING ||
   5022        (compositionState == lazy.UrlbarUtils.COMPOSITION.CANCELED &&
   5023          !compositionClosedPopup))
   5024    ) {
   5025      return;
   5026    }
   5027 
   5028    // Autofill only when text is inserted (i.e., event.data is not empty) and
   5029    // it's not due to pasting.
   5030    const allowAutofill =
   5031      (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition") ||
   5032        compositionState !== lazy.UrlbarUtils.COMPOSITION.COMPOSING) &&
   5033      !!event.data &&
   5034      !lazy.UrlbarUtils.isPasteEvent(event) &&
   5035      this._maybeAutofillPlaceholder(value);
   5036 
   5037    this.startQuery({
   5038      searchString: value,
   5039      allowAutofill,
   5040      resetSearchState: false,
   5041      event,
   5042    });
   5043  }
   5044 
   5045  _on_selectionchange() {
   5046    // Confirm placeholder as user text if it gets explicitly deselected. This
   5047    // happens when the user wants to modify the autofilled text by either
   5048    // clicking on it, or pressing HOME, END, RIGHT, …
   5049    if (
   5050      this._autofillPlaceholder &&
   5051      this._autofillPlaceholder.value == this.value &&
   5052      (this._autofillPlaceholder.selectionStart != this.selectionStart ||
   5053        this._autofillPlaceholder.selectionEnd != this.selectionEnd)
   5054    ) {
   5055      this._autofillPlaceholder = null;
   5056      this.userTypedValue = this.value;
   5057    }
   5058  }
   5059 
   5060  _on_select() {
   5061    // On certain user input, AutoCopyListener::OnSelectionChange() updates
   5062    // the primary selection with user-selected text (when supported).
   5063    // Selection::NotifySelectionListeners() then dispatches a "select" event
   5064    // under similar conditions via TextInputListener::OnSelectionChange().
   5065    // This event is received here in order to replace the primary selection
   5066    // from the editor with text having the adjustments of
   5067    // _getSelectedValueForClipboard(), such as adding the scheme for the url.
   5068    //
   5069    // Other "select" events are also received, however, and must be excluded.
   5070    if (
   5071      // _suppressPrimaryAdjustment is set during select().  Don't update
   5072      // the primary selection because that is not the intent of user input,
   5073      // which may be new tab or urlbar focus.
   5074      this._suppressPrimaryAdjustment ||
   5075      // The check on isHandlingUserInput filters out async "select" events
   5076      // from setSelectionRange(), which occur when autofill text is selected.
   5077      !this.window.windowUtils.isHandlingUserInput ||
   5078      !Services.clipboard.isClipboardTypeSupported(
   5079        Services.clipboard.kSelectionClipboard
   5080      )
   5081    ) {
   5082      return;
   5083    }
   5084 
   5085    let val = this._getSelectedValueForClipboard();
   5086    if (!val) {
   5087      return;
   5088    }
   5089 
   5090    lazy.ClipboardHelper.copyStringToClipboard(
   5091      val,
   5092      Services.clipboard.kSelectionClipboard
   5093    );
   5094  }
   5095 
   5096  _on_overflow(event) {
   5097    const targetIsPlaceholder =
   5098      event.originalTarget.implementedPseudoElement == "::placeholder";
   5099    // We only care about the non-placeholder text.
   5100    // This shouldn't be needed, see bug 1487036.
   5101    if (targetIsPlaceholder) {
   5102      return;
   5103    }
   5104    this._overflowing = true;
   5105    this.updateTextOverflow();
   5106  }
   5107 
   5108  _on_underflow(event) {
   5109    const targetIsPlaceholder =
   5110      event.originalTarget.implementedPseudoElement == "::placeholder";
   5111    // We only care about the non-placeholder text.
   5112    // This shouldn't be needed, see bug 1487036.
   5113    if (targetIsPlaceholder) {
   5114      return;
   5115    }
   5116    this._overflowing = false;
   5117 
   5118    this.updateTextOverflow();
   5119 
   5120    this._updateUrlTooltip();
   5121  }
   5122 
   5123  _on_paste(event) {
   5124    let originalPasteData = event.clipboardData.getData("text/plain");
   5125    if (!originalPasteData) {
   5126      return;
   5127    }
   5128 
   5129    let oldValue = this.value;
   5130    let oldStart = oldValue.substring(0, this.selectionStart);
   5131    // If there is already non-whitespace content in the URL bar
   5132    // preceding the pasted content, it's not necessary to check
   5133    // protocols used by the pasted content:
   5134    if (oldStart.trim()) {
   5135      return;
   5136    }
   5137    let oldEnd = oldValue.substring(this.selectionEnd);
   5138 
   5139    const pasteData = this.sanitizeTextFromClipboard(originalPasteData);
   5140 
   5141    if (originalPasteData != pasteData) {
   5142      // Unfortunately we're not allowed to set the bits being pasted
   5143      // so cancel this event:
   5144      event.preventDefault();
   5145      event.stopImmediatePropagation();
   5146 
   5147      const value = oldStart + pasteData + oldEnd;
   5148      this._setValue(value, { valueIsTyped: true });
   5149      this.userTypedValue = value;
   5150 
   5151      // Since we prevent the default paste event, we have to ensure the
   5152      // pageproxystate is updated. The paste event replaces the actual current
   5153      // page's URL with user-typed content, so we should set pageproxystate to
   5154      // invalid.
   5155      if (this.getAttribute("pageproxystate") == "valid") {
   5156        this.setPageProxyState("invalid");
   5157      }
   5158      this.toggleAttribute("usertyping", this._untrimmedValue);
   5159 
   5160      // Fix up cursor/selection:
   5161      let newCursorPos = oldStart.length + pasteData.length;
   5162      this.inputField.setSelectionRange(newCursorPos, newCursorPos);
   5163 
   5164      this.startQuery({
   5165        searchString: this.value,
   5166        allowAutofill: false,
   5167        resetSearchState: false,
   5168        event,
   5169      });
   5170    }
   5171  }
   5172 
   5173  /**
   5174   * Sanitize and process data retrieved from the clipboard
   5175   *
   5176   * @param {string} clipboardData
   5177   *   The original data retrieved from the clipboard.
   5178   * @returns {string}
   5179   *   The sanitized paste data, ready to use.
   5180   */
   5181  sanitizeTextFromClipboard(clipboardData) {
   5182    let fixedURI, keywordAsSent;
   5183    try {
   5184      ({ fixedURI, keywordAsSent } = Services.uriFixup.getFixupURIInfo(
   5185        clipboardData,
   5186        Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   5187          Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
   5188      ));
   5189    } catch (e) {}
   5190 
   5191    let pasteData;
   5192    if (keywordAsSent) {
   5193      // For performance reasons, we don't want to beautify a long string.
   5194      if (clipboardData.length < 500) {
   5195        // For only keywords, replace any white spaces including line break
   5196        // with white space.
   5197        pasteData = clipboardData.replace(/\s/g, " ");
   5198      } else {
   5199        pasteData = clipboardData;
   5200      }
   5201    } else if (
   5202      fixedURI?.scheme == "data" &&
   5203      !fixedURI.spec.match(/^data:.+;base64,/)
   5204    ) {
   5205      // For data url without base64, replace line break with white space.
   5206      pasteData = clipboardData.replace(/[\r\n]/g, " ");
   5207    } else {
   5208      // For normal url or data url having basic64, or if fixup failed, just
   5209      // remove line breaks.
   5210      pasteData = clipboardData.replace(/[\r\n]/g, "");
   5211    }
   5212 
   5213    return lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(pasteData);
   5214  }
   5215 
   5216  /**
   5217   * Generate a UrlbarQueryContext from the current context.
   5218   *
   5219   * @param {object} [options]
   5220   *   Optional params
   5221   * @param {boolean} [options.allowAutofill]
   5222   *   Whether autofill is enabled.
   5223   * @param {string} [options.searchString]
   5224   *   The string being searched.
   5225   * @param {object} [options.event]
   5226   *   The event triggering the query.
   5227   * @returns {UrlbarQueryContext}
   5228   *   The queryContext object.
   5229   */
   5230  #makeQueryContext({
   5231    allowAutofill = true,
   5232    searchString = null,
   5233    event = null,
   5234  } = {}) {
   5235    // When we are in actions search mode we can show more results so
   5236    // increase the limit.
   5237    let maxResults =
   5238      this.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS
   5239        ? lazy.UrlbarPrefs.get("maxRichResults")
   5240        : UNLIMITED_MAX_RESULTS;
   5241    let options = {
   5242      allowAutofill,
   5243      isPrivate: this.isPrivate,
   5244      sapName: this.sapName,
   5245      maxResults,
   5246      searchString,
   5247      userContextId: parseInt(
   5248        this.window.gBrowser.selectedBrowser.getAttribute("usercontextid") || 0
   5249      ),
   5250      tabGroup: this.window.gBrowser.selectedTab.group?.id ?? null,
   5251      currentPage: this.window.gBrowser.currentURI.spec,
   5252      prohibitRemoteResults:
   5253        event &&
   5254        lazy.UrlbarUtils.isPasteEvent(event) &&
   5255        lazy.UrlbarPrefs.get("maxCharsForSearchSuggestions") <
   5256          event.data?.length,
   5257    };
   5258 
   5259    if (this.searchMode) {
   5260      options.searchMode = this.searchMode;
   5261      if (this.searchMode.source) {
   5262        options.sources = [this.searchMode.source];
   5263      }
   5264    }
   5265 
   5266    return new lazy.UrlbarQueryContext(options);
   5267  }
   5268 
   5269  _on_scrollend() {
   5270    this.updateTextOverflow();
   5271  }
   5272 
   5273  _on_TabSelect() {
   5274    // TabSelect may be activated by a keyboard shortcut and cause the urlbar
   5275    // to take focus, in this case we should not untrim.
   5276    this._untrimOnFocusAfterKeydown = false;
   5277    this._gotTabSelect = true;
   5278    this._afterTabSelectAndFocusChange();
   5279  }
   5280 
   5281  _on_TabClose(event) {
   5282    this.controller.engagementEvent.handleBounceEventTrigger(
   5283      event.target.linkedBrowser
   5284    );
   5285 
   5286    if (this.view.isOpen) {
   5287      // Refresh results when a tab is closed while the results view is open.
   5288      // This prevents switch-to-tab results from remaining in the results
   5289      // list after their tab is closed.
   5290      this.startQuery();
   5291    }
   5292  }
   5293 
   5294  _on_beforeinput(event) {
   5295    if (event.data && this._keyDownEnterDeferred) {
   5296      // Ignore char key input while processing enter key.
   5297      event.preventDefault();
   5298    }
   5299  }
   5300 
   5301  _on_keydown(event) {
   5302    if (event.currentTarget == this.window) {
   5303      // It would be great if we could more easily detect the user focusing the
   5304      // address bar through a keyboard shortcut, but F6 and TAB bypass are
   5305      // not going through commands handling.
   5306      // Also note we'll unset this on TabSelect, as it can focus the address
   5307      // bar but we should not untrim in that case.
   5308      this._untrimOnFocusAfterKeydown = !this.focused;
   5309      return;
   5310    }
   5311 
   5312    // Repeated KeyboardEvents can easily cause subtle bugs in this logic, if
   5313    // not properly handled, so let's first handle things that should not be
   5314    // evaluated repeatedly.
   5315    if (!event.repeat) {
   5316      this.#allTextSelectedOnKeyDown = this.#allTextSelected;
   5317 
   5318      if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
   5319        if (this._keyDownEnterDeferred) {
   5320          this._keyDownEnterDeferred.reject();
   5321        }
   5322        this._keyDownEnterDeferred = Promise.withResolvers();
   5323        event._disableCanonization =
   5324          AppConstants.platform == "macosx"
   5325            ? this._isKeyDownWithMeta
   5326            : this._isKeyDownWithCtrl;
   5327      }
   5328 
   5329      // Now set the keydown trackers for the current event, anything that wants
   5330      // to check the previous events should have happened before this point.
   5331      // The previously value is persisted until keyup, as we check if the
   5332      // modifiers were down, even if other keys are pressed in the meanwhile.
   5333      if (event.ctrlKey && event.keyCode != KeyEvent.DOM_VK_CONTROL) {
   5334        this._isKeyDownWithCtrl = true;
   5335      }
   5336      if (event.metaKey && event.keyCode != KeyEvent.DOM_VK_META) {
   5337        this._isKeyDownWithMeta = true;
   5338      }
   5339      // This is used in keyup, so it can be set every time.
   5340      this._isKeyDownWithMetaAndLeft =
   5341        this._isKeyDownWithMeta &&
   5342        !event.shiftKey &&
   5343        event.keyCode == KeyEvent.DOM_VK_LEFT;
   5344 
   5345      this._toggleActionOverride(event);
   5346    }
   5347 
   5348    // Due to event deferring, it's possible preventDefault() won't be invoked
   5349    // soon enough to actually prevent some of the default behaviors, thus we
   5350    // have to handle the event "twice". This first immediate call passes false
   5351    // as second argument so that handleKeyNavigation will only simulate the
   5352    // event handling, without actually executing actions.
   5353    // TODO (Bug 1541806): improve this handling, maybe by delaying actions
   5354    // instead of events.
   5355    if (this.eventBufferer.shouldDeferEvent(event)) {
   5356      this.controller.handleKeyNavigation(event, false);
   5357    }
   5358    this.eventBufferer.maybeDeferEvent(event, () => {
   5359      this.controller.handleKeyNavigation(event);
   5360    });
   5361  }
   5362 
   5363  async _on_keyup(event) {
   5364    if (event.currentTarget == this.window) {
   5365      this._untrimOnFocusAfterKeydown = false;
   5366      return;
   5367    }
   5368 
   5369    if (this.#allTextSelectedOnKeyDown) {
   5370      let moveCursorToStart = this.#isHomeKeyUpEvent(event);
   5371      // We must set the selection immediately because:
   5372      //  - on Mac Fn + Left is not handled properly as Home
   5373      //  - untrim depends on text not being fully selected.
   5374      if (moveCursorToStart) {
   5375        this.selectionStart = this.selectionEnd = 0;
   5376      }
   5377      this.#maybeUntrimUrl({ moveCursorToStart });
   5378    }
   5379    if (event.keyCode === KeyEvent.DOM_VK_META) {
   5380      this._isKeyDownWithMeta = false;
   5381      this._isKeyDownWithMetaAndLeft = false;
   5382    }
   5383    if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
   5384      this._isKeyDownWithCtrl = false;
   5385    }
   5386 
   5387    this._toggleActionOverride(event);
   5388 
   5389    // Pressing Enter key while pressing Meta key, and next, even when releasing
   5390    // Enter key before releasing Meta key, the keyup event is not fired.
   5391    // Therefore, if Enter keydown is detecting, continue the post processing
   5392    // for Enter key when any keyup event is detected.
   5393    if (this._keyDownEnterDeferred) {
   5394      if (this._keyDownEnterDeferred.loadedContent) {
   5395        try {
   5396          const loadingBrowser = await this._keyDownEnterDeferred.promise;
   5397          // Ensure the selected browser didn't change in the meanwhile.
   5398          if (this.window.gBrowser.selectedBrowser === loadingBrowser) {
   5399            loadingBrowser.focus();
   5400            // Make sure the domain name stays visible for spoof protection and usability.
   5401            this.inputField.setSelectionRange(0, 0);
   5402          }
   5403        } catch (ex) {
   5404          // Not all the Enter actions in the urlbar will cause a navigation, then it
   5405          // is normal for this to be rejected.
   5406          // If _keyDownEnterDeferred was rejected on keydown, we don't nullify it here
   5407          // to ensure not overwriting the new value created by keydown.
   5408        }
   5409      } else {
   5410        // Discard the _keyDownEnterDeferred promise to receive any key inputs immediately.
   5411        this._keyDownEnterDeferred.resolve();
   5412      }
   5413 
   5414      this._keyDownEnterDeferred = null;
   5415    }
   5416  }
   5417 
   5418  _on_compositionstart() {
   5419    if (this.#compositionState == lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
   5420      throw new Error("Trying to start a nested composition?");
   5421    }
   5422    this.#compositionState = lazy.UrlbarUtils.COMPOSITION.COMPOSING;
   5423 
   5424    if (lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
   5425      return;
   5426    }
   5427 
   5428    // Close the view. This will also stop searching.
   5429    if (this.view.isOpen) {
   5430      // We're closing the view, but we want to retain search mode if the
   5431      // selected result was previewing it.
   5432      if (this.searchMode) {
   5433        // If we entered search mode with an empty string, clear userTypedValue,
   5434        // otherwise confirmSearchMode may try to set it as value.
   5435        // This can happen for example if we entered search mode typing a
   5436        // a partial engine domain and selecting a tab-to-search result.
   5437        if (!this.value) {
   5438          this.userTypedValue = null;
   5439        }
   5440        this.confirmSearchMode();
   5441      }
   5442      this.#compositionClosedPopup = true;
   5443      this.view.close();
   5444    } else {
   5445      this.#compositionClosedPopup = false;
   5446    }
   5447  }
   5448 
   5449  _on_compositionend(event) {
   5450    if (this.#compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING) {
   5451      throw new Error("Trying to stop a non existing composition?");
   5452    }
   5453 
   5454    if (!lazy.UrlbarPrefs.get("keepPanelOpenDuringImeComposition")) {
   5455      // Clear the selection and the cached result, since they refer to the
   5456      // state before this composition. A new input even will be generated
   5457      // after this.
   5458      this.view.clearSelection();
   5459      this._resultForCurrentValue = null;
   5460    }
   5461 
   5462    // We can't yet retrieve the committed value from the editor, since it isn't
   5463    // completely committed yet. We'll handle it at the next input event.
   5464    this.#compositionState = event.data
   5465      ? lazy.UrlbarUtils.COMPOSITION.COMMIT
   5466      : lazy.UrlbarUtils.COMPOSITION.CANCELED;
   5467  }
   5468 
   5469  _on_dragstart(event) {
   5470    // Drag only if the gesture starts from the input field.
   5471    let nodePosition = this.inputField.compareDocumentPosition(
   5472      event.originalTarget
   5473    );
   5474    if (
   5475      event.target != this.inputField &&
   5476      !(nodePosition & Node.DOCUMENT_POSITION_CONTAINED_BY)
   5477    ) {
   5478      return;
   5479    }
   5480 
   5481    // Don't cover potential drop targets on the toolbars or in content.
   5482    this.view.close();
   5483 
   5484    // Only customize the drag data if the entire value is selected and it's a
   5485    // loaded URI. Use default behavior otherwise.
   5486    if (
   5487      !this.#allTextSelected ||
   5488      this.getAttribute("pageproxystate") != "valid"
   5489    ) {
   5490      return;
   5491    }
   5492 
   5493    let uri = this.makeURIReadable(this.window.gBrowser.currentURI);
   5494    let href = uri.displaySpec;
   5495    let title = this.window.gBrowser.contentTitle || href;
   5496 
   5497    event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
   5498    event.dataTransfer.setData("text/plain", href);
   5499    event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
   5500    event.dataTransfer.effectAllowed = "copyLink";
   5501    event.stopPropagation();
   5502  }
   5503 
   5504  /**
   5505   * Handles dragover events for the input.
   5506   *
   5507   * @param {DragEvent} event
   5508   */
   5509  _on_dragover(event) {
   5510    if (!getDroppableData(event)) {
   5511      event.dataTransfer.dropEffect = "none";
   5512    }
   5513  }
   5514 
   5515  /**
   5516   * Handles dropping of data on the input.
   5517   *
   5518   * @param {DragEvent} event
   5519   */
   5520  _on_drop(event) {
   5521    let droppedData = getDroppableData(event);
   5522    let droppedString = URL.isInstance(droppedData)
   5523      ? droppedData.href
   5524      : droppedData;
   5525    if (
   5526      droppedString &&
   5527      droppedString !== this.window.gBrowser.currentURI.spec
   5528    ) {
   5529      this.value = droppedString;
   5530      this.setPageProxyState("invalid");
   5531      this.focus();
   5532      if (this.#isAddressbar) {
   5533        // If we're an address bar, we automatically open the dropped address or
   5534        // submit the dropped string to the search engine.
   5535        let principal =
   5536          Services.droppedLinkHandler.getTriggeringPrincipal(event);
   5537        // To simplify tracking of events, register an initial event for event
   5538        // telemetry, to replace the missing input event.
   5539        let queryContext = this.#makeQueryContext({
   5540          searchString: droppedString,
   5541        });
   5542        this.controller.setLastQueryContextCache(queryContext);
   5543        this.controller.engagementEvent.start(event, queryContext);
   5544        this.handleNavigation({ triggeringPrincipal: principal });
   5545        // For safety reasons, in the drop case we don't want to immediately show
   5546        // the dropped value, instead we want to keep showing the current page
   5547        // url until an onLocationChange happens.
   5548        // See the handling in `setURI` for further details.
   5549        this.userTypedValue = null;
   5550        this.setURI({ dueToTabSwitch: true });
   5551      } else {
   5552        // If we're a search bar, allow for getting search suggestions, changing
   5553        // the search engine, or modifying the search term before submitting.
   5554        this.startQuery({
   5555          searchString: droppedString,
   5556          event,
   5557        });
   5558      }
   5559    }
   5560  }
   5561 
   5562  _on_customizationstarting() {
   5563    this.incrementBreakoutBlockerCount();
   5564    this.blur();
   5565  }
   5566 
   5567  _on_aftercustomization() {
   5568    this.decrementBreakoutBlockerCount();
   5569    this.#updateLayoutBreakout();
   5570  }
   5571 
   5572  uiDensityChanged() {
   5573    if (this.#breakoutBlockerCount) {
   5574      return;
   5575    }
   5576    this.#updateLayoutBreakout();
   5577  }
   5578 
   5579  _on_toolbarvisibilitychange() {
   5580    this.#updateTextboxPositionNextFrame();
   5581  }
   5582 
   5583  _on_DOMMenuBarActive() {
   5584    this.#updateTextboxPositionNextFrame();
   5585  }
   5586 
   5587  _on_DOMMenuBarInactive() {
   5588    this.#updateTextboxPositionNextFrame();
   5589  }
   5590 
   5591  #allTextSelectedOnKeyDown = false;
   5592  get #allTextSelected() {
   5593    return this.selectionStart == 0 && this.selectionEnd == this.value.length;
   5594  }
   5595 
   5596  /**
   5597   * @param {string} value
   5598   *   A untrimmed address bar input.
   5599   * @returns {nsILoadInfo.SchemelessInputType}
   5600   *   Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeless` if the input
   5601   *   doesn't start with a scheme relevant for schemeless HTTPS-First
   5602   *   (http://, https:// and file://).
   5603   *   Returns `Ci.nsILoadInfo.SchemelessInputTypeSchemeful` if it does have a scheme.
   5604   */
   5605  #getSchemelessInput(value) {
   5606    return ["http://", "https://", "file://"].every(
   5607      scheme => !value.trim().startsWith(scheme)
   5608    )
   5609      ? Ci.nsILoadInfo.SchemelessInputTypeSchemeless
   5610      : Ci.nsILoadInfo.SchemelessInputTypeSchemeful;
   5611  }
   5612 
   5613  get #isOpenedPageInBlankTargetLoading() {
   5614    return (
   5615      this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory
   5616        ?.count === 0 &&
   5617      this.window.gBrowser.selectedBrowser.browsingContext
   5618        .nonWebControlledBlankURI
   5619    );
   5620  }
   5621 
   5622  // Search modes are per browser and are stored in the `searchModes` property of this map.
   5623  // For a browser, search mode can be in preview mode, confirmed, or both.
   5624  // Typically, search mode is entered in preview mode with a particular
   5625  // source and is confirmed with the same source once a query starts.  It's
   5626  // also possible for a confirmed search mode to be replaced with a preview
   5627  // mode with a different source, and in those cases, we need to re-confirm
   5628  // search mode when preview mode is exited. In addition, only confirmed
   5629  // search modes should be restored across sessions. We therefore need to
   5630  // keep track of both the current confirmed and preview modes, per browser.
   5631  //
   5632  // For each browser with a search mode, this maps the browser to an object
   5633  // like this: { preview, confirmed }.  Both `preview` and `confirmed` are
   5634  // search mode objects; see the setSearchMode documentation.  Either one may
   5635  // be undefined if that particular mode is not active for the browser.
   5636 
   5637  /**
   5638   * Tracks a state object per browser.
   5639   */
   5640  #browserStates = new WeakMap();
   5641 
   5642  get #selectedText() {
   5643    return this.editor.selection.toStringWithFormat(
   5644      "text/plain",
   5645      Ci.nsIDocumentEncoder.OutputPreformatted |
   5646        Ci.nsIDocumentEncoder.OutputRaw,
   5647      0
   5648    );
   5649  }
   5650 
   5651  /**
   5652   * Check whether a key event has a similar effect as the Home key.
   5653   *
   5654   * @param {KeyboardEvent} event A Keyboard event
   5655   * @returns {boolean} Whether the even will act like the Home key.
   5656   */
   5657  #isHomeKeyUpEvent(event) {
   5658    let isMac = AppConstants.platform === "macosx";
   5659    return (
   5660      // On MacOS this can be generated with Fn + Left.
   5661      event.keyCode == KeyEvent.DOM_VK_HOME ||
   5662      // Windows and Linux also support Ctrl + Left.
   5663      (!isMac &&
   5664        event.keyCode == KeyboardEvent.DOM_VK_LEFT &&
   5665        event.ctrlKey &&
   5666        !event.shiftKey) ||
   5667      // MacOS supports other combos to move cursor at the start of the line.
   5668      // For example Ctrl + A.
   5669      (isMac &&
   5670        event.keyCode == KeyboardEvent.DOM_VK_A &&
   5671        event.ctrlKey &&
   5672        !event.shiftKey) ||
   5673      // And also Cmd (Meta) + Left.
   5674      // Unfortunately on MacOS it's not possible to detect combos with the meta
   5675      // key during the keyup event, due to how the OS handles events. Thus we
   5676      // record the combo on keydown, and check for it here.
   5677      (isMac &&
   5678        event.keyCode == KeyEvent.DOM_VK_META &&
   5679        this._isKeyDownWithMetaAndLeft)
   5680    );
   5681  }
   5682 
   5683  #canHandleAsBlankPage(spec) {
   5684    return this.window.isBlankPageURL(spec) || spec == "about:privatebrowsing";
   5685  }
   5686 }
   5687 
   5688 /**
   5689 * Tries to extract droppable data from a DND event.
   5690 *
   5691 * @param {DragEvent} event The DND event to examine.
   5692 * @returns {URL|string|null}
   5693 *          null if there's a security reason for which we should do nothing.
   5694 *          A URL object if it's a value we can load.
   5695 *          A string value otherwise.
   5696 */
   5697 function getDroppableData(event) {
   5698  let links;
   5699  try {
   5700    links = Services.droppedLinkHandler.dropLinks(event);
   5701  } catch (ex) {
   5702    // This is either an unexpected failure or a security exception; in either
   5703    // case we should always return null.
   5704    return null;
   5705  }
   5706  // The URL bar automatically handles inputs with newline characters,
   5707  // so we can get away with treating text/x-moz-url flavours as text/plain.
   5708  if (links[0]?.url) {
   5709    event.preventDefault();
   5710    let href = links[0].url;
   5711    if (lazy.UrlbarUtils.stripUnsafeProtocolOnPaste(href) != href) {
   5712      // We may have stripped an unsafe protocol like javascript: and if so
   5713      // there's no point in handling a partial drop.
   5714      event.stopImmediatePropagation();
   5715      return null;
   5716    }
   5717 
   5718    // If this fails, checkLoadURIStrWithPrincipal would also fail,
   5719    // as that's what it does with things that don't pass the IO
   5720    // service's newURI constructor without fixup. It's conceivable we
   5721    // may want to relax this check in the future (so e.g. www.foo.com
   5722    // gets fixed up), but not right now.
   5723    let url = URL.parse(href);
   5724    if (url) {
   5725      // If we succeed, try to pass security checks. If this works, return the
   5726      // URL object. If the *security checks* fail, return null.
   5727      try {
   5728        let principal =
   5729          Services.droppedLinkHandler.getTriggeringPrincipal(event);
   5730        Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
   5731          principal,
   5732          url.href,
   5733          Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL
   5734        );
   5735        return url;
   5736      } catch (ex) {
   5737        return null;
   5738      }
   5739    }
   5740    // We couldn't make a URL out of this. Continue on, and return text below.
   5741  }
   5742  // Handle as text.
   5743  return event.dataTransfer.getData("text/plain");
   5744 }
   5745 
   5746 /**
   5747 * Decodes the given URI for displaying it in the address bar without losing
   5748 * information, such that hitting Enter again will load the same URI.
   5749 *
   5750 * @param {nsIURI} aURI
   5751 *   The URI to decode
   5752 * @returns {string}
   5753 *   The decoded URI
   5754 */
   5755 function losslessDecodeURI(aURI) {
   5756  let scheme = aURI.scheme;
   5757  let value = aURI.displaySpec;
   5758 
   5759  // Try to decode as UTF-8 if there's no encoding sequence that we would break.
   5760  if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
   5761    let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
   5762    if (decodeASCIIOnly) {
   5763      // This only decodes ascii characters (hex) 20-7e, except 25 (%).
   5764      // This avoids both cases stipulated below (%-related issues, and \r, \n
   5765      // and \t, which would be %0d, %0a and %09, respectively) as well as any
   5766      // non-US-ascii characters.
   5767      value = value.replace(
   5768        /%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g,
   5769        decodeURI
   5770      );
   5771    } else {
   5772      try {
   5773        value = decodeURI(value)
   5774          // decodeURI decodes %25 to %, which creates unintended encoding
   5775          // sequences. Re-encode it, unless it's part of a sequence that
   5776          // survived decodeURI, i.e. one for:
   5777          // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
   5778          // (RFC 3987 section 3.2)
   5779          .replace(
   5780            /%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/gi,
   5781            encodeURIComponent
   5782          );
   5783      } catch (e) {}
   5784    }
   5785  }
   5786 
   5787  // IMPORTANT: The following regular expressions are Unicode-aware due to /v.
   5788  // Avoid matching high or low surrogate pairs directly, always work with
   5789  // full Unicode scalar values.
   5790 
   5791  // Encode potentially invisible characters:
   5792  //   U+0000-001F: C0/C1 control characters
   5793  //   U+007F-009F: commands
   5794  //   U+00A0, U+1680, U+2000-200A, U+202F, U+205F, U+3000: other spaces
   5795  //   U+2028-2029: line and paragraph separators
   5796  //   U+2800: braille empty pattern
   5797  //   U+FFFC: object replacement character
   5798  // Encode any trailing whitespace that may be part of a pasted URL, so that it
   5799  // doesn't get eaten away by the location bar (bug 410726).
   5800  // Encode all adjacent space chars (U+0020), to prevent spoofing attempts
   5801  // where they would push part of the URL to overflow the location bar
   5802  // (bug 1395508). A single space, or the last space if the are many, is
   5803  // preserved to maintain readability of certain urls if it's not followed by a
   5804  // control or separator character. We only do this for the common space,
   5805  // because others may be eaten when copied to the clipboard,so it's safer to
   5806  // preserve them encoded.
   5807  value = value.replace(
   5808    // eslint-disable-next-line no-control-regex
   5809    /[[\p{Separator}--\u{0020}]\p{Control}\u{2800}\u{FFFC}]|\u{0020}(?=[\p{Other}\p{Separator}])|\s$/gv,
   5810    encodeURIComponent
   5811  );
   5812 
   5813  // Encode characters that are ignorable, can't be rendered usefully, or may
   5814  // confuse users.
   5815  //
   5816  // Default ignorable characters; ZWNJ (U+200C) and ZWJ (U+200D) are excluded
   5817  // per bug 582186:
   5818  //   U+00AD, U+034F, U+06DD, U+070F, U+115F-1160, U+17B4, U+17B5, U+180B-180E,
   5819  //   U+2060, U+FEFF, U+200B, U+2060-206F, U+3164, U+FE00-FE0F, U+FFA0,
   5820  //   U+FFF0-FFFB, U+1D173-1D17A, U+E0000-E0FFF
   5821  // Bidi control characters (RFC 3987 sections 3.2 and 4.1 paragraph 6):
   5822  //   U+061C, U+200E, U+200F, U+202A-202E, U+2066-2069
   5823  // Other format characters in the Cf category that are unlikely to be rendered
   5824  // usefully:
   5825  //   U+0600-0605, U+08E2, U+110BD, U+110CD, U+13430-13438, U+1BCA0-1BCA3
   5826  // Mimicking UI parts:
   5827  //   U+1F50F-1F513, U+1F6E1
   5828  // Unassigned codepoints, sometimes shown as empty glyphs.
   5829  value = value.replace(
   5830    // eslint-disable-next-line no-misleading-character-class
   5831    /[[\p{Format}--[\u{200C}\u{200D}]]\u{034F}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180B}-\u{180D}\u{3164}\u{FE00}-\u{FE0F}\u{FFA0}\u{FFF0}-\u{FFFB}\p{Unassigned}\p{Private_Use}\u{E0000}-\u{E0FFF}\u{1F50F}-\u{1F513}\u{1F6E1}]/gv,
   5832    encodeURIComponent
   5833  );
   5834  return value;
   5835 }
   5836 
   5837 /**
   5838 * Handles copy and cut commands for the urlbar.
   5839 */
   5840 class CopyCutController {
   5841  /**
   5842   * @param {UrlbarInput} urlbar
   5843   *   The UrlbarInput instance to use this controller for.
   5844   */
   5845  constructor(urlbar) {
   5846    this.urlbar = urlbar;
   5847  }
   5848 
   5849  /**
   5850   * @param {string} command
   5851   *   The name of the command to handle.
   5852   */
   5853  doCommand(command) {
   5854    let urlbar = this.urlbar;
   5855    let val = urlbar._getSelectedValueForClipboard();
   5856    if (!val) {
   5857      return;
   5858    }
   5859 
   5860    if (command == "cmd_cut" && this.isCommandEnabled(command)) {
   5861      let start = urlbar.selectionStart;
   5862      let end = urlbar.selectionEnd;
   5863      urlbar.inputField.value =
   5864        urlbar.inputField.value.substring(0, start) +
   5865        urlbar.inputField.value.substring(end);
   5866      urlbar.inputField.setSelectionRange(start, start);
   5867 
   5868      let event = new UIEvent("input", {
   5869        bubbles: true,
   5870        cancelable: false,
   5871        view: urlbar.window,
   5872        detail: 0,
   5873      });
   5874      urlbar.inputField.dispatchEvent(event);
   5875    }
   5876 
   5877    lazy.ClipboardHelper.copyString(val);
   5878  }
   5879 
   5880  /**
   5881   * @param {string} command
   5882   *   The name of the command to check.
   5883   * @returns {boolean}
   5884   *   Whether the command is handled by this controller.
   5885   */
   5886  supportsCommand(command) {
   5887    switch (command) {
   5888      case "cmd_copy":
   5889      case "cmd_cut":
   5890        return true;
   5891    }
   5892    return false;
   5893  }
   5894 
   5895  /**
   5896   * @param {string} command
   5897   *   The name of the command to check.
   5898   * @returns {boolean}
   5899   *   Whether the command should be enabled.
   5900   */
   5901  isCommandEnabled(command) {
   5902    return (
   5903      this.supportsCommand(command) &&
   5904      (command != "cmd_cut" || !this.urlbar.readOnly) &&
   5905      this.urlbar.selectionStart < this.urlbar.selectionEnd
   5906    );
   5907  }
   5908 
   5909  onEvent() {}
   5910 }
   5911 
   5912 /**
   5913 * Manages the Add Search Engine contextual menu entries.
   5914 *
   5915 * Note: setEnginesFromBrowser must be invoked from the outside when the
   5916 *       page provided engines list changes.
   5917 *       refreshContextMenu must be invoked when the context menu is opened.
   5918 */
   5919 class AddSearchEngineHelper {
   5920  /**
   5921   * @type {UrlbarSearchOneOffs}
   5922   */
   5923  shortcutButtons;
   5924 
   5925  /**
   5926   * @param {UrlbarInput} input The parent UrlbarInput.
   5927   */
   5928  constructor(input) {
   5929    this.input = input;
   5930    this.shortcutButtons = input.view.oneOffSearchButtons;
   5931  }
   5932 
   5933  /**
   5934   * If there's more than this number of engines, the context menu offers
   5935   * them in a submenu.
   5936   *
   5937   * @returns {number}
   5938   */
   5939  get maxInlineEngines() {
   5940    return this.shortcutButtons._maxInlineAddEngines;
   5941  }
   5942 
   5943  /**
   5944   * Invoked by OpenSearchManager when the list of available engines changes.
   5945   *
   5946   * @param {object} browser The current browser.
   5947   * @param {object} engines The updated list of available engines.
   5948   */
   5949  setEnginesFromBrowser(browser, engines) {
   5950    this.browsingContext = browser.browsingContext;
   5951    // Make a copy of the array for state comparison.
   5952    engines = engines.slice();
   5953    if (!this._sameEngines(this.engines, engines)) {
   5954      this.engines = engines;
   5955      this.shortcutButtons?.updateWebEngines();
   5956    }
   5957  }
   5958 
   5959  _sameEngines(engines1, engines2) {
   5960    if (engines1?.length != engines2?.length) {
   5961      return false;
   5962    }
   5963    return lazy.ObjectUtils.deepEqual(
   5964      engines1.map(e => e.title),
   5965      engines2.map(e => e.title)
   5966    );
   5967  }
   5968 
   5969  _createMenuitem(engine, index) {
   5970    let elt = this.input.document.createXULElement("menuitem");
   5971    elt.setAttribute("anonid", `add-engine-${index}`);
   5972    elt.classList.add("menuitem-iconic");
   5973    elt.classList.add("context-menu-add-engine");
   5974    this.input.document.l10n.setAttributes(elt, "search-one-offs-add-engine", {
   5975      engineName: engine.title,
   5976    });
   5977    elt.setAttribute("uri", engine.uri);
   5978    if (engine.icon) {
   5979      elt.setAttribute("image", engine.icon);
   5980    } else {
   5981      elt.removeAttribute("image");
   5982    }
   5983    elt.addEventListener("command", this._onCommand.bind(this));
   5984    return elt;
   5985  }
   5986 
   5987  _createMenu(engine) {
   5988    let elt = this.input.document.createXULElement("menu");
   5989    elt.setAttribute("anonid", "add-engine-menu");
   5990    elt.classList.add("menu-iconic");
   5991    elt.classList.add("context-menu-add-engine");
   5992    this.input.document.l10n.setAttributes(
   5993      elt,
   5994      "search-one-offs-add-engine-menu"
   5995    );
   5996    if (engine.icon) {
   5997      elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(engine.icon));
   5998    }
   5999    let popup = this.input.document.createXULElement("menupopup");
   6000    elt.appendChild(popup);
   6001    return elt;
   6002  }
   6003 
   6004  refreshContextMenu() {
   6005    let engines = this.engines;
   6006    let contextMenu = this.input.querySelector("moz-input-box").menupopup;
   6007 
   6008    // Certain operations, like customization, destroy and recreate widgets,
   6009    // so we cannot rely on cached elements.
   6010    if (!contextMenu.querySelector(".menuseparator-add-engine")) {
   6011      this.contextSeparator =
   6012        this.input.document.createXULElement("menuseparator");
   6013      this.contextSeparator.setAttribute("anonid", "add-engine-separator");
   6014      this.contextSeparator.classList.add("menuseparator-add-engine");
   6015      this.contextSeparator.collapsed = true;
   6016      contextMenu.appendChild(this.contextSeparator);
   6017    }
   6018 
   6019    this.contextSeparator.collapsed = !engines.length;
   6020    let curElt = this.contextSeparator;
   6021    // Remove the previous items, if any.
   6022    for (let elt = curElt.nextElementSibling; elt; ) {
   6023      let nextElementSibling = elt.nextElementSibling;
   6024      elt.remove();
   6025      elt = nextElementSibling;
   6026    }
   6027 
   6028    // If the page provides too many engines, we only show a single menu entry
   6029    // with engines in a submenu.
   6030    if (engines.length > this.maxInlineEngines) {
   6031      // Set the menu button's image to the image of the first engine.  The
   6032      // offered engines may have differing images, so there's no perfect
   6033      // choice here.
   6034      let elt = this._createMenu(engines[0]);
   6035      this.contextSeparator.insertAdjacentElement("afterend", elt);
   6036      curElt = elt.lastElementChild;
   6037    }
   6038 
   6039    // Insert the engines, either in the contextual menu or the sub menu.
   6040    for (let i = 0; i < engines.length; ++i) {
   6041      let elt = this._createMenuitem(engines[i], i);
   6042      if (curElt.localName == "menupopup") {
   6043        curElt.appendChild(elt);
   6044      } else {
   6045        curElt.insertAdjacentElement("afterend", elt);
   6046      }
   6047      curElt = elt;
   6048    }
   6049  }
   6050 
   6051  async _onCommand(event) {
   6052    let added = await lazy.SearchUIUtils.addOpenSearchEngine(
   6053      event.target.getAttribute("uri"),
   6054      event.target.getAttribute("image"),
   6055      this.browsingContext
   6056    ).catch(console.error);
   6057    if (added) {
   6058      // Remove the offered engine from the list. The browser updated the
   6059      // engines list at this point, so we just have to refresh the menu.)
   6060      this.refreshContextMenu();
   6061    }
   6062  }
   6063 }
   6064 
   6065 customElements.define("moz-urlbar", UrlbarInput);