browser/base/content/urlbarBindings.xml
author Johann Hofmann <jhofmann@mozilla.com>
Sun, 03 Feb 2019 19:42:19 +0000
changeset 456607 d05f776eb8408238fba4a59e24c6189c45653646
parent 456581 5b4294b589042588942714434fe614b93bb20ae1
child 457462 6ad216491c77923c525fd8a963183c11054a3f18
permissions -rw-r--r--
Bug 377496 - Improve auth dialog blocking heuristics. r=MattN The fix in bug 1312243 introduced a maximum of three consecutive cancelations (controlled by a pref) that a user could perform until Firefox would prevent the page from showing more dialogs. This, in my opinion, is a great idea. The implementation, however, has a major fallacy: It checks the inner window id in the well-meaning attempt to find user navigation or reloads and clears its internal counter when that window id changes. Unfortunately this also clears the counter on non-user-initiated navigations and reloads. I believe that the true intention of the patch was to cancel the auth dialog after 3 attempts, except if: - The user reloads the page on their own terms - The user navigates to a different site on their own Which is what I plan to implement, using the same pattern we applied to implement temporarily blocked site permissions: - Temporarily store basic auth counter state on the browser object, as a map from baseDomain (eTLD+1) to number of cancellations - Reset this state only on user initiated reload - Reset the counter for a domain if the user has entered login data into the dialog and submitted This would mitigate the DOS issue while hopefully not breaking any sites that rely on basic auth. Differential Revision: https://phabricator.services.mozilla.com/D18019

<?xml version="1.0"?>

<!--
-*- Mode: HTML -*-
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->

<!DOCTYPE bindings [
<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
%notificationDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
]>

<bindings id="urlbarBindings" xmlns="http://www.mozilla.org/xbl"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="urlbar" extends="chrome://global/content/bindings/textbox.xml#textbox">
    <content>
      <children includes="box"/>
      <xul:moz-input-box anonid="moz-input-box"
                         tooltip="aHTMLTooltip"
                         class="urlbar-input-box"
                         flex="1">
        <children/>
        <html:input anonid="scheme"
                    class="urlbar-scheme textbox-input"
                    required="required"
                    xbl:inherits="textoverflow,focused"/>
        <html:input anonid="input"
                    class="urlbar-input textbox-input"
                    allowevents="true"
                    inputmode="mozAwesomebar"
                    xbl:inherits="value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,focused,textoverflow"/>
      </xul:moz-input-box>
      <xul:image anonid="urlbar-go-button"
                 class="urlbar-go-button urlbar-icon"
                 onclick="gURLBar.handleCommand(event);"
                 tooltiptext="&goEndCap.tooltip;"
                 xbl:inherits="pageproxystate,parentfocused=focused,usertyping"/>
      <xul:dropmarker anonid="historydropmarker"
                      class="urlbar-history-dropmarker urlbar-icon chromeclass-toolbar-additional"
                      tooltiptext="&urlbar.openHistoryPopup.tooltip;"
                      allowevents="true"
                      xbl:inherits="open,parentfocused=focused,usertyping"/>
      <children includes="hbox"/>
    </content>
  </binding>

  <binding id="legacy-urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
    <content>
      <children includes="box"/>
      <xul:moz-input-box anonid="moz-input-box"
                         tooltip="aHTMLTooltip"
                         class="urlbar-input-box"
                         flex="1">
        <children/>
        <html:input anonid="scheme"
                    class="urlbar-scheme textbox-input"
                    required="required"
                    xbl:inherits="textoverflow,focused"/>
        <html:input anonid="input"
                    class="urlbar-input textbox-input"
                    allowevents="true"
                    inputmode="mozAwesomebar"
                    xbl:inherits="value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,focused,textoverflow"/>
      </xul:moz-input-box>
      <xul:image anonid="urlbar-go-button"
                 class="urlbar-go-button urlbar-icon"
                 onclick="gURLBar.handleCommand(event);"
                 tooltiptext="&goEndCap.tooltip;"
                 xbl:inherits="pageproxystate,parentfocused=focused,usertyping"/>
      <xul:dropmarker anonid="historydropmarker"
                      class="urlbar-history-dropmarker urlbar-icon chromeclass-toolbar-additional"
                      tooltiptext="&urlbar.openHistoryPopup.tooltip;"
                      allowevents="true"
                      xbl:inherits="open,parentfocused=focused,usertyping"/>
      <children includes="hbox"/>
    </content>

    <implementation implements="nsIObserver">
      <field name="ExtensionSearchHandler" readonly="true">
        (ChromeUtils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
      </field>

      <constructor><![CDATA[
        // UrlbarInput compatibility shims
        this.document = document;
        this.window = window;
        this.textbox = this;

        this._prefs = Cc["@mozilla.org/preferences-service;1"]
                        .getService(Ci.nsIPrefService)
                        .getBranch("browser.urlbar.");
        this._prefs.addObserver("", this);

        this._defaultPrefs = Cc["@mozilla.org/preferences-service;1"]
                               .getService(Ci.nsIPrefService)
                               .getDefaultBranch("browser.urlbar.");

        Services.prefs.addObserver("browser.search.suggest.enabled", this);
        this.browserSearchSuggestEnabled = Services.prefs.getBoolPref("browser.search.suggest.enabled");

        this.openInTab = this._prefs.getBoolPref("openintab");
        this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
        this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
        this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
        this.urlbarSearchSuggestEnabled = this._prefs.getBoolPref("suggest.searches");
        this.timeout = this._prefs.getIntPref("delay");
        this._mayTrimURLs = this._prefs.getBoolPref("trimURLs");
        this._adoptIntoActiveWindow = this._prefs.getBoolPref("switchTabs.adoptIntoActiveWindow");
        this._ctrlCanonizesURLs = this._prefs.getBoolPref("ctrlCanonizesURLs");
        this.inputField.controllers.insertControllerAt(0, this._copyCutController);
        this.inputField.addEventListener("paste", this);
        this.inputField.addEventListener("mousedown", this);
        this.inputField.addEventListener("mouseover", this);
        this.inputField.addEventListener("overflow", this);
        this.inputField.addEventListener("underflow", this);
        this.inputField.addEventListener("scrollend", this);
        window.addEventListener("resize", this);

        var textBox = document.getAnonymousElementByAttribute(this,
                                                "anonid", "moz-input-box");
        // Force the Custom Element to upgrade until Bug 1470242 handles this:
        customElements.upgrade(textBox);
        var cxmenu = textBox.menupopup;
        var pasteAndGo;
        cxmenu.addEventListener("popupshowing", function() {
          if (!pasteAndGo)
            return;
          var controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
          var enabled = controller.isCommandEnabled("cmd_paste");
          if (enabled)
            pasteAndGo.removeAttribute("disabled");
          else
            pasteAndGo.setAttribute("disabled", "true");
        });

        var insertLocation = cxmenu.firstElementChild;
        while (insertLocation.nextElementSibling &&
               insertLocation.getAttribute("cmd") != "cmd_paste")
          insertLocation = insertLocation.nextElementSibling;
        if (insertLocation) {
          pasteAndGo = document.createXULElement("menuitem");
          let label = Services.strings.createBundle("chrome://browser/locale/browser.properties").
                                   GetStringFromName("pasteAndGo.label");
          pasteAndGo.setAttribute("label", label);
          pasteAndGo.setAttribute("anonid", "paste-and-go");
          pasteAndGo.setAttribute("oncommand",
              "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();");
          cxmenu.insertBefore(pasteAndGo, insertLocation.nextElementSibling);
        }

        this.popup.addEventListener("popupshowing", () => {
          this._enableOrDisableOneOffSearches();
        }, {capture: true, once: true});

        // history dropmarker open state
        this.popup.addEventListener("popupshowing", () => {
          this.setAttribute("open", "true");
        });
        this.popup.addEventListener("popuphidden", () => {
          requestAnimationFrame(() => {
            this.removeAttribute("open");
          });
        });
      ]]></constructor>

      <destructor><![CDATA[
        // Somehow, it's possible for the XBL destructor to fire without the
        // constructor ever having fired. Fix:
        if (!this._prefs) {
          return;
        }
        this._prefs.removeObserver("", this);
        this._prefs = null;
        Services.prefs.removeObserver("browser.search.suggest.enabled", this);
        this.inputField.controllers.removeController(this._copyCutController);
        this.inputField.removeEventListener("paste", this);
        this.inputField.removeEventListener("mousedown", this);
        this.inputField.removeEventListener("mouseover", this);
        this.inputField.removeEventListener("overflow", this);
        this.inputField.removeEventListener("underflow", this);
        this.inputField.removeEventListener("scrollend", this);
        window.removeEventListener("resize", this);

        if (this._deferredKeyEventTimeout) {
          clearTimeout(this._deferredKeyEventTimeout);
          this._deferredKeyEventTimeout = null;
        }

        // Null out the one-offs' popup and textbox so that it cleans up its
        // internal state for both.  Most importantly, it removes the event
        // listeners that it added to both.
        this.popup.oneOffSearchButtons.popup = null;
        this.popup.oneOffSearchButtons.textbox = null;
      ]]></destructor>

      <field name="valueFormatter" readonly="true">
        new UrlbarValueFormatter(this);
      </field>

      <field name="goButton">
        document.getAnonymousElementByAttribute(this, "anonid", "urlbar-go-button");
      </field>

      <field name="_value">""</field>
      <field name="gotResultForCurrentQuery">false</field>

      <!--
        This is set around HandleHenter so it can be used in handleCommand.
        It is also used to track whether we must handle a delayed handleEnter,
        by checking if it has been cleared.
      -->
      <field name="handleEnterInstance">null</field>

      <!--
        Since we never want scrollbars, we always use the maxResults value.
      -->
      <property name="maxRows"
                onget="return this.popup.maxResults;"/>

      <!--
        Set by focusAndSelectUrlBar to indicate whether the next focus event was
        initiated by an explicit user action. See the "focus" handler below.
      -->
      <field name="userInitiatedFocus">false</field>

      <!--
        onBeforeValueGet is called by the base-binding's .value getter.
        It can return an object with a "value" property, to override the
        return value of the getter.
      -->
      <method name="onBeforeValueGet">
        <body><![CDATA[
          return { value: this._value };
        ]]></body>
      </method>

      <!--
        onBeforeValueSet is called by the base-binding's .value setter.
        It should return the value that the setter should use.
      -->
      <method name="onBeforeValueSet">
        <parameter name="aValue"/>
        <body><![CDATA[
          this._value = aValue;
          var returnValue = aValue;
          var action = this._parseActionUrl(aValue);

          if (action) {
            switch (action.type) {
              case "switchtab": // Fall through.
              case "remotetab": // Fall through.
              case "visiturl": {
                returnValue = action.params.displayUrl;
                break;
              }
              case "keyword": // Fall through.
              case "searchengine": {
                returnValue = action.params.input;
                break;
              }
              case "extension": {
                returnValue = action.params.content;
                break;
              }
            }
          } else {
            let originalUrl = ReaderMode.getOriginalUrlObjectForDisplay(aValue);
            if (originalUrl) {
              returnValue = originalUrl.displaySpec;
            }
          }

          // Set the actiontype only if the user is not overriding actions.
          if (action && this._pressedNoActionKeys.size == 0) {
            this.setAttribute("actiontype", action.type);
          } else {
            this.removeAttribute("actiontype");
          }
          return returnValue;
        ]]></body>
      </method>

      <method name="onKeyPress">
        <parameter name="aEvent"/>
        <parameter name="aOptions"/>
        <body><![CDATA[
          switch (aEvent.keyCode) {
            case KeyEvent.DOM_VK_LEFT:
            case KeyEvent.DOM_VK_RIGHT:
            case KeyEvent.DOM_VK_HOME:
              // Reset the selected index so that nsAutoCompleteController
              // simply closes the popup without trying to fill anything.
              this.popup.selectedIndex = -1;
              break;
            case KeyEvent.DOM_VK_TAB:
              this.userSelectionBehavior = "tab";
              // The user is explicitly making a selection, so the popup
              // should get accessibility focus.
              this.popup.richlistbox.suppressMenuItemEvent = false;
              break;
            case KeyEvent.DOM_VK_UP:
            case KeyEvent.DOM_VK_DOWN:
            case KeyEvent.DOM_VK_PAGE_UP:
            case KeyEvent.DOM_VK_PAGE_DOWN:
              if (this.userSelectionBehavior != "tab")
                this.userSelectionBehavior = "arrow";
              // The user is explicitly making a selection, so the popup
              // should get accessibility focus.
              this.popup.richlistbox.suppressMenuItemEvent = false;
              break;
          }

          if (AppConstants.platform == "macosx") {
            switch (aEvent.key) {
              case "n":
              case "p":
                if (aEvent.ctrlKey) {
                  // The user is explicitly making a selection, so the popup
                  // should get accessibility focus.
                  this.popup.richlistbox.suppressMenuItemEvent = false;
                }
                break;
            }
          }

          let noDefer = aOptions && aOptions.noDefer;
          if (!noDefer && this._shouldDeferKeyEvent(aEvent)) {
            this._deferKeyEvent(aEvent, "onKeyPress");
            return false;
          }
          if (this.popup.popupOpen && this.popup.handleKeyPress(aEvent)) {
            return true;
          }
          return this.handleKeyPress(aEvent, aOptions);
        ]]></body>
      </method>

      <!--
        Search results arrive asynchronously, which means that keypresses may
        arrive before results do and therefore not have the effect the user
        intends.  That's especially likely to happen with the down arrow and
        enter keys due to the one-off search buttons: if the user very quickly
        pastes something in the input, presses the down arrow key, and then hits
        enter, they are probably expecting to visit the first result.  But if
        there are no results, then pressing down and enter will trigger the
        first one-off button.  To prevent that undesirable behavior, certain
        keys are buffered and deferred until more results arrive, at which time
        they're replayed.

        @param  event
                The key event that should maybe be deferred.
        @return True if the event should be deferred, false if not.
       -->
      <method name="_shouldDeferKeyEvent">
        <parameter name="event"/>
        <body><![CDATA[
          // If any event has been deferred for this search, then defer all
          // subsequent events so that the user does not experience any
          // keypresses out of order.  All events will be replayed when
          // _deferredKeyEventTimeout fires.
          if (this._deferredKeyEventQueue.length) {
            return true;
          }

          // At this point, no events have been deferred for this search, and we
          // need to decide whether `event` is the first one that should be.
          if (!this._keyCodesToDefer.has(event.keyCode) &&
              !(/Mac/.test(navigator.platform) &&
                event.ctrlKey &&
                (event.key === "n" || event.key === "p") &&
                this.popupOpen)) {
            // Not a key that should trigger deferring.
            return false;
          }

          let waitedLongEnough =
            this._searchStartDate + this._deferredKeyEventTimeoutMs <= Cu.now();
          if (waitedLongEnough) {
            // This is a key that we would defer, but enough time has passed
            // since the start of the search that we don't want to block the
            // user's keypresses anymore.
            return false;
          }

          if (event.keyCode == KeyEvent.DOM_VK_TAB && !this.popupOpen) {
            // The popup is closed and the user pressed the Tab key.  The
            // focus should move out of the urlbar immediately.
            return false;
          }

          return !this._safeToPlayDeferredKeyEvent(event);
        ]]></body>
      </method>

      <!--
        Returns true if the given deferred key event can be played now without
        possibly surprising the user.  This depends on the state of the popup,
        its results, and the type of keypress.  Use this method only after
        determining that the event should be deferred, or after it's already
        been deferred and you want to know if it can be played now.

        @param  event
                The key event.
        @return True if the event can be played, false if not.
      -->
      <method name="_safeToPlayDeferredKeyEvent">
        <parameter name="event"/>
        <body><![CDATA[
          if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
            return this.popup.selectedIndex != 0 ||
                   this.gotResultForCurrentQuery;
          }

          if (!this.gotResultForCurrentQuery || !this.popupOpen) {
            // We're still waiting on the first result, or the popup hasn't
            // opened yet, so not safe.
            return false;
          }

          let maxResultsRemaining =
            this.popup.maxResults - this.popup.matchCount;
          if (maxResultsRemaining == 0) {
            // The popup can't possibly have any more results, so there's no
            // need to defer any event now.
            return true;
          }

          if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
            // Don't play the event if the last result is selected so that the
            // user doesn't accidentally arrow down into the one-off buttons
            // when they didn't mean to.
            let lastResultSelected =
              this.popup.selectedIndex + 1 == this.popup.matchCount;
            return !lastResultSelected;
          }

          return true;
        ]]></body>
      </method>

      <!--
        Adds a key event to the deferred event queue.

        @param event
               The key event to defer.
        @param methodName
               The name of the method on `this` to call.  It's expected to take
               two arguments: the event, and an optional options object:
               {
                  noDefer: If true, then the event is being replayed and it
                           should not be deferred again.
               }
      -->
      <method name="_deferKeyEvent">
        <parameter name="event"/>
        <parameter name="methodName"/>
        <body><![CDATA[
          // Somehow event.defaultPrevented ends up true for deferred events.
          // autocomplete ignores defaultPrevented events, which means it would
          // ignore replayed deferred events if we didn't tell it to bypass
          // defaultPrevented.  That's the purpose of this expando.  If we could
          // figure out what's setting defaultPrevented and prevent it, then we
          // could get rid of this.
          if (event.urlbarDeferred) {
            throw new Error("Key event already deferred!");
          }
          event.urlbarDeferred = true;

          this._deferredKeyEventQueue.push({
            methodName,
            event,
            searchString: this.mController.searchString,
          });

          if (!this._deferredKeyEventTimeout) {
            // Start the timeout that will unconditionally replay all deferred
            // events when it fires so that, after a certain point, we don't
            // keep blocking the user's keypresses when nothing else has caused
            // the events to be replayed.  Do not check whether it's safe to
            // replay the events because otherwise it may look like we ignored
            // the user's input.
            let elapsed = Cu.now() - this._searchStartDate;
            let remaining = this._deferredKeyEventTimeoutMs - elapsed;
            this._deferredKeyEventTimeout = setTimeout(() => {
              this.replayAllDeferredKeyEvents();
              this._deferredKeyEventTimeout = null;
            }, Math.max(0, remaining));
          }
        ]]></body>
      </method>

      <!-- The enter key is always deferred, so it's not included here. -->
      <field name="_keyCodesToDefer">new Set([
        KeyboardEvent.DOM_VK_RETURN,
        KeyboardEvent.DOM_VK_DOWN,
        KeyboardEvent.DOM_VK_TAB,
      ])</field>
      <field name="_deferredKeyEventQueue">[]</field>
      <field name="_deferredKeyEventTimeout">null</field>
      <field name="_deferredKeyEventTimeoutMs">200</field>
      <field name="_searchStartDate">0</field>

      <method name="replaySafeDeferredKeyEvents">
        <body><![CDATA[
          if (!this._deferredKeyEventQueue.length) {
            return;
          }
          let instance = this._deferredKeyEventQueue[0];
          if (!this._safeToPlayDeferredKeyEvent(instance.event)) {
            return;
          }
          this._deferredKeyEventQueue.shift();
          this._replayKeyEventInstance(instance);
          Services.tm.dispatchToMainThread(() => {
            this.replaySafeDeferredKeyEvents();
          });
        ]]></body>
      </method>

      <!--
        Unconditionally replays all deferred key events.  This does not check
        whether it's safe to replay the events; use replaySafeDeferredKeyEvents
        for that.  Use this method when you must replay all events so that it
        does not appear that we ignored the user's input.
      -->
      <method name="replayAllDeferredKeyEvents">
        <body><![CDATA[
          let instance = this._deferredKeyEventQueue.shift();
          if (!instance) {
            return;
          }
          this._replayKeyEventInstance(instance);
          Services.tm.dispatchToMainThread(() => {
            this.replayAllDeferredKeyEvents();
          });
        ]]></body>
      </method>

      <method name="_replayKeyEventInstance">
        <parameter name="instance"/>
        <body><![CDATA[
          // Safety check: handle only if the search string didn't change.
          if (this.mController.searchString == instance.searchString) {
            this[instance.methodName](instance.event, {noDefer: true});
          }
        ]]></body>
      </method>

      <field name="_mayTrimURLs">true</field>
      <method name="trimValue">
        <parameter name="aURL"/>
        <body><![CDATA[
          // This method must not modify the given URL such that calling
          // nsIURIFixup::createFixupURI with the result will produce a different URI.
          return this._mayTrimURLs ? trimURL(aURL) : aURL;
        ]]></body>
      </method>

      <!--
        This method tries to apply styling to the text in the input, depending
        on the text.  See the _format* methods.
      -->
      <method name="formatValue">
        <body><![CDATA[
          this.valueFormatter.update();
        ]]></body>
      </method>

      <method name="handleRevert">
        <body><![CDATA[
          var isScrolling = this.popupOpen;

          gBrowser.userTypedValue = null;

          // don't revert to last valid url unless page is NOT loading
          // and user is NOT key-scrolling through autocomplete list
          if (!XULBrowserWindow.isBusy && !isScrolling) {
            URLBarSetURI(null, true);

            // If the value isn't empty and the urlbar has focus, select the value.
            if (this.value && this.hasAttribute("focused"))
              this.select();
          }

          // tell widget to revert to last typed text only if the user
          // was scrolling when they hit escape
          return !isScrolling;
        ]]></body>
      </method>

      <method name="_whereToOpen">
        <parameter name="event"/>
        <body><![CDATA[
          let isMouseEvent = event instanceof MouseEvent;
          let reuseEmpty = !isMouseEvent;
          let where = undefined;
          if (!isMouseEvent && event && event.altKey) {
            // We support using 'alt' to open in a tab, because ctrl/shift
            // might be used for canonizing URLs:
            where = event.shiftKey ? "tabshifted" : "tab";
          } else if (!isMouseEvent && this._ctrlCanonizesURLs && event && event.ctrlKey) {
            // If we're allowing canonization, and this is a key event with ctrl
            // pressed, open in current tab to allow ctrl-enter to canonize URL.
            where = "current";
          } else {
            where = whereToOpenLink(event, false, false);
          }
          if (this.openInTab) {
            if (where == "current") {
              where = "tab";
            } else if (where == "tab") {
              where = "current";
            }
            reuseEmpty = true;
          }
          if (where == "tab" && reuseEmpty && gBrowser.selectedTab.isEmpty) {
            where = "current";
          }
          return where;
        ]]></body>
      </method>

      <!--
        This is ultimately called by the autocomplete controller as the result
        of handleEnter when the Return key is pressed in the textbox.  Since
        onPopupClick also calls handleEnter, this is also called as a result in
        that case.

        @param event
               The event that triggered the command.
        @param openUILinkWhere
               Optional.  The "where" to pass to openTrustedLinkIn.  This method
               computes the appropriate "where" given the event, but you can
               use this to override it.
        @param openUILinkParams
               Optional.  The parameters to pass to openTrustedLinkIn.  As with
               "where", this method computes the appropriate parameters, but
               any parameters you supply here will override those.
      -->
      <method name="handleCommand">
        <parameter name="event"/>
        <parameter name="openUILinkWhere"/>
        <parameter name="openUILinkParams"/>
        <parameter name="triggeringPrincipal"/>
        <body><![CDATA[
          let isMouseEvent = event instanceof MouseEvent;
          if (isMouseEvent && event.button == 2) {
            // Do nothing for right clicks.
            return;
          }

          // Determine whether to use the selected one-off search button.  In
          // one-off search buttons parlance, "selected" means that the button
          // has been navigated to via the keyboard.  So we want to use it if
          // the triggering event is not a mouse click -- i.e., it's a Return
          // key -- or if the one-off was mouse-clicked.
          let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton;
          if (selectedOneOff &&
              isMouseEvent &&
              event.originalTarget != selectedOneOff) {
            selectedOneOff = null;
          }

          // Do the command of the selected one-off if it's not an engine.
          if (selectedOneOff && !selectedOneOff.engine) {
            selectedOneOff.doCommand();
            return;
          }

          let where = openUILinkWhere || this._whereToOpen(event);

          let url = this.value;
          if (!url) {
            return;
          }

          BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
            event, this.userSelectionBehavior);

          let mayInheritPrincipal = false;
          let postData = null;
          let browser = gBrowser.selectedBrowser;
          let action = this._parseActionUrl(url);

          if (selectedOneOff && selectedOneOff.engine) {
            // If there's a selected one-off button then load a search using
            // the one-off's engine.
            [url, postData] =
              this._parseAndRecordSearchEngineLoad(selectedOneOff.engine,
                                                   this.oneOffSearchQuery,
                                                   event);
          } else if (action) {
            switch (action.type) {
              case "visiturl":
                // Unifiedcomplete uses fixupURI to tell if something is a visit
                // or a search, and passes out the fixedURI as the url param.
                // By using that uri we would end up passing a different string
                // to the docshell that may run a different not-found heuristic.
                // For example, "mozilla/run" would be fixed by unifiedcomplete
                // to "http://mozilla/run". The docshell, once it can't resolve
                // mozilla, would note the string has a scheme, and try to load
                // http://mozilla.com/run instead of searching "mozilla/run".
                // So, if we have the original input at hand, we pass it through
                // and let the docshell handle it.
                if (action.params.input) {
                  url = action.params.input;
                  break;
                }
                url = action.params.url;
                break;
              case "remotetab":
                url = action.params.url;
                break;
              case "keyword":
                if (action.params.postData) {
                  postData = UrlbarUtils.getPostDataStream(action.params.postData);
                }
                mayInheritPrincipal = true;
                url = action.params.url;
                break;
              case "switchtab":
                url = action.params.url;
                if (this.hasAttribute("actiontype")) {
                  this.handleRevert();
                  let prevTab = gBrowser.selectedTab;
                  let loadOpts = {
                    adoptIntoActiveWindow: this._adoptIntoActiveWindow,
                  };

                  if (switchToTabHavingURI(url, false, loadOpts) &&
                      prevTab.isEmpty) {
                    gBrowser.removeTab(prevTab);
                  }
                  return;
                }

                // Once we get here, we got a switchtab action but the user
                // bypassed it by pressing shift/meta/ctrl. Those modifiers
                // might otherwise affect where we open - we always want to
                // open in the current tab.
                where = "current";
                break;
              case "searchengine":
                if (selectedOneOff && selectedOneOff.engine) {
                  // Replace the engine with the selected one-off engine.
                  action.params.engineName = selectedOneOff.engine.name;
                }
                // If the selected result is an @alias offer -- an @alias with
                // an empty query string -- then instead of loading the engine's
                // empty search results page, put the @alias in the input so
                // that the user can type a search query and search directly
                // from the urlbar.
                if (action.params.alias &&
                    action.params.alias.startsWith("@") &&
                    !action.params.searchQuery) {
                  this.search(action.params.input);
                  return;
                }
                const actionDetails = {
                  isSuggestion: !!action.params.searchSuggestion,
                  alias: action.params.alias,
                };
                [url, postData] = this._parseAndRecordSearchEngineLoad(
                  action.params.engineName,
                  action.params.searchSuggestion || action.params.searchQuery,
                  event,
                  actionDetails
                );
                break;
              case "extension":
                this.handleRevert();
                // Give the extension control of handling the command.
                let searchString = action.params.content;
                let keyword = action.params.keyword;
                this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
                return;
            }
          } else {
            // This is a fallback for add-ons and old testing code that directly
            // set value and try to confirm it. UnifiedComplete should always
            // resolve to a valid url.
            try {
              url = url.trim();
              new URL(url);
            } catch (ex) {
              let lastLocationChange = browser.lastLocationChange;
              UrlbarUtils.getShortcutOrURIAndPostData(url).then(data => {
                if (where != "current" ||
                    browser.lastLocationChange == lastLocationChange) {
                  this._loadURL(data.url, browser, data.postData, where,
                                openUILinkParams, data.mayInheritPrincipal,
                                triggeringPrincipal);
                }
              });
              return;
            }
          }

          this._loadURL(url, browser, postData, where, openUILinkParams,
                        mayInheritPrincipal, triggeringPrincipal);
        ]]></body>
      </method>

      <property name="oneOffSearchQuery">
        <getter><![CDATA[
          // If the user has selected a search suggestion, chances are they
          // want to use the one off search engine to search for that suggestion,
          // not the string that they manually entered into the location bar.
          let action = this._parseActionUrl(this.value);
          if (action && action.type == "searchengine") {
            return action.params.input;
          }
          // this.textValue may be an autofilled string.  Search only with the
          // portion that the user typed, if any, by preferring the autocomplete
          // controller's searchString (including handleEnterInstance.searchString).
          return this.handleEnterSearchString ||
                 this.mController.searchString ||
                 this.textValue;
        ]]></getter>
      </property>

      <method name="_loadURL">
        <parameter name="url"/>
        <parameter name="browser"/>
        <parameter name="postData"/>
        <parameter name="openUILinkWhere"/>
        <parameter name="openUILinkParams"/>
        <parameter name="mayInheritPrincipal"/>
        <parameter name="triggeringPrincipal"/>
        <body><![CDATA[
          this.value = url;
          browser.userTypedValue = url;
          if (gInitialPages.includes(url)) {
            browser.initialPageLoadedFromUserAction = url;
          }
          try {
            UrlbarUtils.addToUrlbarHistory(url, window);
          } catch (ex) {
            // Things may go wrong when adding url to session history,
            // but don't let that interfere with the loading of the url.
            Cu.reportError(ex);
          }

          // Reset DOS mitigations for the basic auth prompt.
          // TODO: When bug 1498553 is resolved, we should be able to
          // remove the !triggeringPrincipal condition here.
          if (!triggeringPrincipal || triggeringPrincipal.isSystemPrincipal) {
            delete browser.canceledAuthenticationPromptCounter;
          }

          let params = {
            postData,
            allowThirdPartyFixup: true,
            triggeringPrincipal,
          };
          if (openUILinkWhere == "current") {
            params.targetBrowser = browser;
            params.indicateErrorPageLoad = true;
            params.allowPinnedTabHostChange = true;
            params.allowPopups = url.startsWith("javascript:");
          } else {
            params.initiatingDoc = document;
          }
          params.allowInheritPrincipal = mayInheritPrincipal;

          if (openUILinkParams) {
            for (let key in openUILinkParams) {
              params[key] = openUILinkParams[key];
            }
          }

          // Focus the content area before triggering loads, since if the load
          // occurs in a new tab, we want focus to be restored to the content
          // area when the current tab is re-selected.
          browser.focus();

          if (openUILinkWhere != "current") {
            this.handleRevert();
          }

          try {
            openTrustedLinkIn(url, openUILinkWhere, params);
          } catch (ex) {
            // This load can throw an exception in certain cases, which means
            // we'll want to replace the URL with the loaded URL:
            if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
              this.handleRevert();
            }
          }

          // Ensure the start of the URL is visible for usability reasons.
          this.selectionStart = this.selectionEnd = 0;
        ]]></body>
      </method>

      <method name="_parseAndRecordSearchEngineLoad">
        <parameter name="engineOrEngineName"/>
        <parameter name="query"/>
        <parameter name="event"/>
        <parameter name="searchActionDetails"/>
        <body><![CDATA[
          let engine =
            typeof(engineOrEngineName) == "string" ?
              Services.search.getEngineByName(engineOrEngineName) :
              engineOrEngineName;
          let isOneOff = this.popup.oneOffSearchButtons
              .maybeRecordTelemetry(event);
          // Infer the type of the event which triggered the search.
          let eventType = "unknown";
          if (event instanceof KeyboardEvent) {
            eventType = "key";
          } else if (event instanceof MouseEvent) {
            eventType = "mouse";
          }
          // Augment the search action details object.
          let details = searchActionDetails || {};
          details.isOneOff = isOneOff;
          details.type = eventType;

          BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
          let submission = engine.getSubmission(query, null, "keyword");
          return [submission.uri.spec, submission.postData];
        ]]></body>
      </method>

      <method name="maybeCanonizeURL">
        <parameter name="aTriggeringEvent"/>
        <parameter name="aUrl"/>
        <body><![CDATA[
          // Only add the suffix when the URL bar value isn't already "URL-like",
          // and only if we get a keyboard event, to match user expectations.
          if (!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl) ||
              !this._ctrlCanonizesURLs ||
              !(aTriggeringEvent instanceof KeyboardEvent) ||
              !aTriggeringEvent.ctrlKey) {
            return;
          }

          let suffix = Services.prefs.getCharPref("browser.fixup.alternate.suffix", ".com/");
          if (!suffix.endsWith("/")) {
            suffix += "/";
          }

          // trim leading/trailing spaces (bug 233205)
          let url = aUrl.trim();

          // Tack www. and suffix on.  If user has appended directories, insert
          // suffix before them (bug 279035).  Be careful not to get two slashes.
          let firstSlash = url.indexOf("/");
          if (firstSlash >= 0) {
            url = url.substring(0, firstSlash) + suffix +
                  url.substring(firstSlash + 1);
          } else {
            url = url + suffix;
          }

          this.popup.overrideValue = "http://www." + url;
        ]]></body>
      </method>

      <method name="_updateUrlTooltip">
        <body><![CDATA[
          if (this.focused || !this._inOverflow) {
            this.inputField.removeAttribute("title");
          } else {
            this.inputField.setAttribute("title", this.value);
          }
        ]]></body>
      </method>

      <!-- Returns:
           null if there's a security issue and we should do nothing.
           a URL object if there is one that we're OK with loading,
           a text value otherwise.
           -->
      <method name="_getDroppableItem">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let links;
          try {
            links = browserDragAndDrop.dropLinks(aEvent);
          } catch (ex) {
            // this is possibly a security exception, in which case we should return
            // null. Always return null because we can't *know* what exception is
            // being returned.
            return null;
          }
          // The URL bar automatically handles inputs with newline characters,
          // so we can get away with treating text/x-moz-url flavours as text/plain.
          if (links.length > 0 && links[0].url) {
            let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent);
            aEvent.preventDefault();
            let url = links[0].url;
            let strippedURL = stripUnsafeProtocolOnPaste(url);
            if (strippedURL != url) {
              aEvent.stopImmediatePropagation();
              return null;
            }
            let urlObj;
            try {
              // If this throws, urlSecurityCheck would also throw, as that's what it
              // does with things that don't pass the IO service's newURI constructor
              // without fixup. It's conceivable we may want to relax this check in
              // the future (so e.g. www.foo.com gets fixed up), but not right now.
              urlObj = new URL(url);
              // If we succeed, try to pass security checks. If this works, return the
              // URL object. If the *security checks* fail, return null.
              try {
                urlSecurityCheck(url,
                                 triggeringPrincipal,
                                 Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
                return urlObj;
              } catch (ex) {
                return null;
              }
            } catch (ex) {
              // We couldn't make a URL out of this. Continue on, and return text below.
            }
          }
          return aEvent.dataTransfer.getData("text/unicode");
        ]]></body>
      </method>

      <method name="onDragOver">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (!this._getDroppableItem(aEvent)) {
            aEvent.dataTransfer.dropEffect = "none";
          }
        ]]></body>
      </method>

      <method name="onDrop">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let droppedItem = this._getDroppableItem(aEvent);
          if (droppedItem) {
            let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent);
            this.value = droppedItem instanceof URL ? droppedItem.href : droppedItem;
            SetPageProxyState("invalid");
            this.focus();
            this.handleCommand(null, undefined, undefined, triggeringPrincipal);
            // Force not showing the dropped URI immediately.
            gBrowser.userTypedValue = null;
            URLBarSetURI(null, true);
          }
        ]]></body>
      </method>

      <method name="makeURIReadable">
        <parameter name="aURI"/>
        <body>
          <![CDATA[
            // Avoid copying 'about:reader?url=', and always provide the original URI:
            // Reader mode ensures we call createExposableURI itself.
            let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay(aURI.displaySpec);
            if (readerStrippedURI) {
              aURI = readerStrippedURI;
            } else {
              // Only copy exposable URIs
              try {
                aURI = Services.uriFixup.createExposableURI(aURI);
              } catch (ex) {}
            }
            return aURI;
          ]]>
        </body>
      </method>

      <method name="_getSelectedValueForClipboard">
        <body><![CDATA[
          // Grab the actual input field's value, not our value, which could
          // include "moz-action:".
          var inputVal = this.inputField.value;
          let selection = this.editor.selection;
          const flags = Ci.nsIDocumentEncoder.OutputPreformatted |
                        Ci.nsIDocumentEncoder.OutputRaw;
          let selectedVal = selection.toStringWithFormat("text/plain", flags, 0);

          // Handle multiple-range selection as a string for simplicity.
          if (selection.rangeCount > 1) {
             return selectedVal;
          }

          // If the selection doesn't start at the beginning or doesn't span the
          // full domain or the URL bar is modified or there is no text at all,
          // nothing else to do here.
          if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "")
            return selectedVal;
          // The selection doesn't span the full domain if it doesn't contain a slash and is
          // followed by some character other than a slash.
          if (!selectedVal.includes("/")) {
            let remainder = inputVal.replace(selectedVal, "");
            if (remainder != "" && remainder[0] != "/")
              return selectedVal;
          }

          // If the value was filled by a search suggestion, just return it.
          let action = this._parseActionUrl(this.value);
          if (action && action.type == "searchengine")
            return selectedVal;

          let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);

          let uri;
          if (this.getAttribute("pageproxystate") == "valid") {
            uri = gBrowser.currentURI;
          } else {
            // We're dealing with an autocompleted value, create a new URI from that.
            try {
              uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
            } catch (e) {}
            if (!uri)
              return selectedVal;
          }

          uri = this.makeURIReadable(uri);

          // If the entire URL is selected, just use the actual loaded URI,
          // unless we want a decoded URI, or it's a data: or javascript: URI,
          // since those are hard to read when encoded.
          if (inputVal == selectedVal &&
              !uri.schemeIs("javascript") && !uri.schemeIs("data") &&
              !Services.prefs.getBoolPref("browser.urlbar.decodeURLsOnCopy")) {
            return uri.displaySpec;
          }

          // Just the beginning of the URL is selected, or we want a decoded
          // url. First check for a trimmed value.
          let spec = uri.displaySpec;
          let trimmedSpec = this.trimValue(spec);
          if (spec != trimmedSpec) {
            // Prepend the portion that trimValue removed from the beginning.
            // This assumes trimValue will only truncate the URL at
            // the beginning or end (or both).
            let trimmedSegments = spec.split(trimmedSpec);
            selectedVal = trimmedSegments[0] + selectedVal;
          }

          return selectedVal;
        ]]></body>
      </method>

      <field name="_copyCutController"><![CDATA[
        ({
          urlbar: this,
          doCommand(aCommand) {
            var urlbar = this.urlbar;
            var val = urlbar._getSelectedValueForClipboard();
            if (!val)
              return;

            if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) {
              let start = urlbar.selectionStart;
              let end = urlbar.selectionEnd;
              urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
                                        urlbar.inputField.value.substring(end);
              urlbar.selectionStart = urlbar.selectionEnd = start;

              let event = document.createEvent("UIEvents");
              event.initUIEvent("input", true, false, window, 0);
              urlbar.inputField.dispatchEvent(event);
            }

            Cc["@mozilla.org/widget/clipboardhelper;1"]
              .getService(Ci.nsIClipboardHelper)
              .copyString(val);
          },
          supportsCommand(aCommand) {
            switch (aCommand) {
              case "cmd_copy":
              case "cmd_cut":
                return true;
            }
            return false;
          },
          isCommandEnabled(aCommand) {
            return this.supportsCommand(aCommand) &&
                   (aCommand != "cmd_cut" || !this.urlbar.readOnly) &&
                   this.urlbar.selectionStart < this.urlbar.selectionEnd;
          },
          onEvent(aEventName) {},
        })
      ]]></field>

      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body><![CDATA[
          if (aTopic == "nsPref:changed") {
            switch (aData) {
              case "clickSelectsAll":
              case "doubleClickSelectsAll":
                this[aData] = this._prefs.getBoolPref(aData);
                break;
              case "autoFill":
                this.completeDefaultIndex = this._prefs.getBoolPref(aData);
                break;
              case "delay":
                this.timeout = this._prefs.getIntPref(aData);
                break;
              case "ctrlCanonizesURLs":
                this._ctrlCanonizesURLs = this._prefs.getBoolPref(aData);
                break;
              case "openintab":
                this.openInTab = this._prefs.getBoolPref(aData);
                break;
              case "browser.search.suggest.enabled":
                this.browserSearchSuggestEnabled = Services.prefs.getBoolPref(aData);
                break;
              case "suggest.searches":
                this.urlbarSearchSuggestEnabled = this._prefs.getBoolPref(aData);
              case "userMadeSearchSuggestionsChoice":
                // Mirror the value for future use, see the comment in the
                // binding's constructor.
                this._prefs.setBoolPref("searchSuggestionsChoice",
                  this.urlbarSearchSuggestEnabled);
                // Clear the cached value to allow changing conditions in tests.
                delete this._whichSearchSuggestionsNotification;
                break;
              case "trimURLs":
                this._mayTrimURLs = this._prefs.getBoolPref(aData);
                break;
              case "oneOffSearches":
                this._enableOrDisableOneOffSearches();
                break;
              case "maxRichResults":
                this.popup.maxResults = this._prefs.getIntPref(aData);
                break;
              case "switchTabs.adoptIntoActiveWindow":
                this._adoptIntoActiveWindow =
                  this._prefs.getBoolPref("switchTabs.adoptIntoActiveWindow");
                break;
            }
          }
        ]]></body>
      </method>

      <method name="_enableOrDisableOneOffSearches">
        <body><![CDATA[
          this.popup.toggleOneOffSearches(
            this._prefs.getBoolPref("oneOffSearches"),
            "pref"
          );
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          switch (aEvent.type) {
            case "paste":
              let originalPasteData = aEvent.clipboardData.getData("text/plain");
              if (!originalPasteData) {
                return;
              }

              let oldValue = this.inputField.value;
              let oldStart = oldValue.substring(0, this.inputField.selectionStart);
              // If there is already non-whitespace content in the URL bar
              // preceding the pasted content, it's not necessary to check
              // protocols used by the pasted content:
              if (oldStart.trim()) {
                return;
              }
              let oldEnd = oldValue.substring(this.inputField.selectionEnd);

              let pasteData = stripUnsafeProtocolOnPaste(originalPasteData);
              if (originalPasteData != pasteData) {
                // Unfortunately we're not allowed to set the bits being pasted
                // so cancel this event:
                aEvent.preventDefault();
                aEvent.stopImmediatePropagation();

                this.inputField.value = oldStart + pasteData + oldEnd;
                // Fix up cursor/selection:
                let newCursorPos = oldStart.length + pasteData.length;
                this.inputField.selectionStart = newCursorPos;
                this.inputField.selectionEnd = newCursorPos;
              }
              break;
            case "mousedown":
              if (this.doubleClickSelectsAll &&
                  aEvent.button == 0 && aEvent.detail == 2) {
                this.editor.selectAll();
                aEvent.preventDefault();
              }
              break;
            case "mouseover":
              this._updateUrlTooltip();
              break;
            case "overflow": {
              const targetIsPlaceholder =
                !aEvent.originalTarget.classList.contains("anonymous-div");
              // We only care about the non-placeholder text.
              // This shouldn't be needed, see bug 1487036.
              if (targetIsPlaceholder) {
                break;
              }
              this._inOverflow = true;
              this.updateTextOverflow();
              break;
            }
            case "underflow": {
              const targetIsPlaceholder =
                !aEvent.originalTarget.classList.contains("anonymous-div");
              // We only care about the non-placeholder text.
              // This shouldn't be needed, see bug 1487036.
              if (targetIsPlaceholder) {
                break;
              }
              this._inOverflow = false;
              this.updateTextOverflow();
              this._updateUrlTooltip();
              break;
            }
            case "scrollend":
              this.updateTextOverflow();
              break;
            case "TabSelect":
              // The autocomplete controller uses heuristic on some internal caches
              // to handle cases like backspace, autofill or repeated searches.
              // Ensure to clear those internal caches when switching tabs.
              this.controller.resetInternalState();
              break;
            case "resize":
              if (aEvent.target == window) {
                // Close the popup since it would be wrongly sized, we'll
                // recalculate a proper size on reopening. For example, this may
                // happen when using special OS resize functions like Win+Arrow.
                this.closePopup();

                // Make sure the host remains visible in the input field
                // when the window is resized.  We don't want to
                // hurt resize performance though, so do this only after resize
                // events have stopped and a small timeout has elapsed.
                if (this._resizeThrottleTimeout) {
                  clearTimeout(this._resizeThrottleTimeout);
                }
                this._resizeThrottleTimeout = setTimeout(() => {
                  this._resizeThrottleTimeout = null;
                  this.valueFormatter.ensureFormattedHostVisible();
                }, 100);
              }
              break;
          }
        ]]></body>
      </method>

      <method name="updateTextOverflow">
        <body><![CDATA[
          if (this._inOverflow) {
            window.promiseDocumentFlushed(() => {
              // Check overflow again to ensure it didn't change in the meanwhile.
              let input = this.inputField;
              if (input && this._inOverflow) {
                let side = input.scrollLeft &&
                           input.scrollLeft == input.scrollLeftMax ? "start" : "end";
                window.requestAnimationFrame(() => {
                  // And check once again, since we might have stopped overflowing
                  // since the promiseDocumentFlushed callback fired.
                  if (this._inOverflow) {
                    this.setAttribute("textoverflow", side);
                  }
                });
              }
            });
          } else {
            this.removeAttribute("textoverflow");
          }
        ]]></body>
      </method>

      <!--
        onBeforeTextValueGet is called by the base-binding's .textValue getter.
        It should return the value that the getter should use.
      -->
      <method name="onBeforeTextValueGet">
        <body><![CDATA[
          return { value: this.inputField.value };
        ]]></body>
      </method>

      <!--
        onBeforeTextValueSet is called by the base-binding's .textValue setter.
        It should return the value that the setter should use.
      -->
      <method name="onBeforeTextValueSet">
        <parameter name="aValue"/>
        <body><![CDATA[
          let val = aValue;
          let uri;
          try {
            uri = makeURI(val);
          } catch (ex) {}

          if (uri) {
            // Do not touch moz-action URIs at all.  They depend on being
            // properly encoded and decoded and will break if decoded
            // unexpectedly.
            if (!this._parseActionUrl(val)) {
              val = losslessDecodeURI(uri);
            }
          }

          return val;
        ]]></body>
      </method>

      <method name="_parseActionUrl">
        <parameter name="aUrl"/>
        <body><![CDATA[
          const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
          if (!MOZ_ACTION_REGEX.test(aUrl))
            return null;

          // URL is in the format moz-action:ACTION,PARAMS
          // Where PARAMS is a JSON encoded object.
          let [, type, params] = aUrl.match(MOZ_ACTION_REGEX);

          let action = {
            type,
          };

          action.params = JSON.parse(params);
          for (let key in action.params) {
            action.params[key] = decodeURIComponent(action.params[key]);
          }

          if ("url" in action.params) {
            let uri;
            try {
              uri = makeURI(action.params.url);
              action.params.displayUrl = losslessDecodeURI(uri);
            } catch (e) {
              action.params.displayUrl = action.params.url;
            }
          }

          return action;
        ]]></body>
      </method>

      <property name="_noActionKeys" readonly="true">
        <getter><![CDATA[
          if (!this.__noActionKeys) {
            this.__noActionKeys = new Set([
              KeyEvent.DOM_VK_ALT,
              KeyEvent.DOM_VK_SHIFT,
            ]);
            let modifier = AppConstants.platform == "macosx" ?
                           KeyEvent.DOM_VK_META :
                           KeyEvent.DOM_VK_CONTROL;
            this.__noActionKeys.add(modifier);
          }
          return this.__noActionKeys;
        ]]></getter>
      </property>

      <field name="_pressedNoActionKeys"><![CDATA[
        new Set()
      ]]></field>

      <method name="_clearNoActions">
        <parameter name="aURL"/>
        <body><![CDATA[
          this._pressedNoActionKeys.clear();
          this.popup.removeAttribute("noactions");
          let action = this._parseActionUrl(this._value);
          if (action)
            this.setAttribute("actiontype", action.type);
        ]]></body>
      </method>

      <method name="onInput">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (!this.mIgnoreInput && this.mController.input == this) {
            this._value = this.inputField.value;
            gBrowser.userTypedValue = this.value;
            this.valueIsTyped = true;
            if (this.inputField.value) {
              this.setAttribute("usertyping", "true");
            } else {
              this.removeAttribute("usertyping");
            }
            // If the popup already had accessibility focus, bring it back to
            // the input, since the user is editing.
            if (!this.popup.richlistbox.suppressMenuItemEvent &&
                this.popup.richlistbox.currentItem) {
              this.popup.richlistbox._fireEvent(
                this.popup.richlistbox.currentItem, "DOMMenuItemInactive");
            }
            // The user is typing, so don't give accessibility focus to the
            // popup, even if an item gets automatically selected.
            this.popup.richlistbox.suppressMenuItemEvent = true;
            // Only wait for a result when we are sure to get one.  In some
            // cases, like when pasting the same exact text, we may not fire
            // a new search and we won't get a result.
            this._onInputHandledText = this.mController.handleText();
            if (this._onInputHandledText) {
              this.gotResultForCurrentQuery = false;
              this._searchStartDate = Cu.now();
              this._deferredKeyEventQueue = [];
              if (this._deferredKeyEventTimeout) {
                clearTimeout(this._deferredKeyEventTimeout);
                this._deferredKeyEventTimeout = null;
              }
            }
          }
          this.resetActionType();
        ]]></body>
      </method>

      <method name="handleEnter">
        <parameter name="event"/>
        <parameter name="options"/>
        <body><![CDATA[
          // We need to ensure we're using a selected autocomplete result.
          // A result should automatically be selected by default,
          // however autocomplete is async and therefore we may not
          // have a result set relating to the current input yet. If that
          // happens, we need to mark that when the first result does get added,
          // it needs to be handled as if enter was pressed with that first
          // result selected.
          // If anything other than the default (first) result is selected, then
          // it must have been manually selected by the human. We let this
          // explicit choice be used, even if it may be related to a previous
          // input.
          // However, if the default result is automatically selected, we
          // ensure that it corresponds to the current input.

          // Store the current search string so it can be used in handleCommand,
          // which will be called as a result of mController.handleEnter().
          this.handleEnterSearchString = this.mController.searchString;

          let noDefer = options && options.noDefer;
          if (!noDefer && this._shouldDeferKeyEvent(event)) {
            // Defer the event until the first non-heuristic result comes in.
            this._deferKeyEvent(event, "handleEnter");
            return false;
          }

          let canonizeValue = this.value;
          if (event.ctrlKey) {
            let action = this._parseActionUrl(canonizeValue);
            if (action && "searchSuggestion" in action.params) {
              canonizeValue = action.params.searchSuggestion;
            } else if (this.popup.selectedIndex === 0 &&
                       this.mController.getStyleAt(0).includes("autofill")) {
              canonizeValue = this.handleEnterSearchString;
            }
          }
          this.maybeCanonizeURL(event, canonizeValue);
          let handled = this.mController.handleEnter(false, event);
          this.handleEnterSearchString = null;
          this.popup.overrideValue = null;
          return handled;
        ]]></body>
      </method>

      <method name="handleDelete">
        <body><![CDATA[
          // If the heuristic result is selected, then the autocomplete
          // controller's handleDelete implementation will remove it, which is
          // not what we want.  So in that case, call handleText so it acts as
          // a backspace on the text value instead of removing the result.
          if (this.popup.selectedIndex == 0 &&
              this.popup._isFirstResultHeuristic) {
            this.mController.handleText();
            return false;
          }
          return this.mController.handleDelete();
        ]]></body>
      </method>

      <property name="_userMadeSearchSuggestionsChoice" readonly="true">
        <getter><![CDATA[
          return this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
                 this._defaultPrefs.getBoolPref("suggest.searches") != this._prefs.getBoolPref("suggest.searches");
        ]]></getter>
      </property>

      <property name="whichSearchSuggestionsNotification" readonly="true">
        <getter><![CDATA[
          // Once we return "none" once, we'll always return "none".
          // If available, use the cached value, rather than running all of the
          // checks again at every locationbar focus.
          if (this._whichSearchSuggestionsNotification) {
            return this._whichSearchSuggestionsNotification;
          }

          if (this.browserSearchSuggestEnabled && !this.inPrivateContext &&
              // In any case, if the user made a choice we should not nag him.
              !this._userMadeSearchSuggestionsChoice) {
            if (this._defaultPrefs.getBoolPref("suggest.searches") &&
                this.urlbarSearchSuggestEnabled && // Has not been switched off.
                this._prefs.getIntPref("timesBeforeHidingSuggestionsHint")) {
              return "opt-out";
            }
          }
          return this._whichSearchSuggestionsNotification = "none";
        ]]></getter>
      </property>

      <method name="updateSearchSuggestionsNotificationImpressions">
        <parameter name="whichNotification"/>
        <body><![CDATA[
          if (whichNotification == "none") {
            throw new Error("Unexpected notification type");
          }

          let remaining = this._prefs.getIntPref("timesBeforeHidingSuggestionsHint");
          if (remaining > 0) {
            this._prefs.setIntPref("timesBeforeHidingSuggestionsHint", remaining - 1);
          }
        ]]></body>
      </method>

      <method name="maybeShowSearchSuggestionsNotificationOnFocus">
        <parameter name="mouseFocused"/>
        <body><![CDATA[
          let whichNotification = this.whichSearchSuggestionsNotification;
          if (this._showSearchSuggestionNotificationOnMouseFocus &&
              mouseFocused) {
            // Force showing the opt-out notification.
            this._whichSearchSuggestionsNotification = whichNotification = "opt-out";
          }
          if (whichNotification == "opt-out") {
            try {
              this.popup.openAutocompletePopup(this, this);
            } finally {
              if (mouseFocused) {
                delete this._whichSearchSuggestionsNotification;
                this._showSearchSuggestionNotificationOnMouseFocus = false;
              }
            }
          }
        ]]></body>
      </method>

      <!--
        Sets the input's value, starts a search, and opens the popup.

        @param  value
                The input's value will be set to this value, and the search will
                use it as its query.
      -->
      <method name="search">
        <parameter name="value"/>
        <body><![CDATA[
          // Hide the suggestions notification if the search uses an "@engine"
          // search engine alias.
          if (value.trim()[0] == "@") {
            let which = this.whichSearchSuggestionsNotification;
            this._whichSearchSuggestionsNotification = "none";
            this.popup.addEventListener("popuphidden", () => {
              this._whichSearchSuggestionsNotification = which;
            }, {once: true});
          }

          // We want the value to be treated as text that the user typed.  It
          // should go through the controller.handleText() path in onInput() so
          // that gBrowser.userTypedValue, this.valueIsTyped, etc. are set and
          // nsAutoCompleteController::HandleText() is called.  Set this.value
          // and fire an input event to do that.  (If we set this.textValue we'd
          // get an input event for free, but it would also set mIgnoreInput,
          // skipping all of the above requirements.)
          focusAndSelectUrlBar();

          // If the value is a restricted token, append a space.
          if (Object.values(UrlbarTokenizer.RESTRICT).includes(value)) {
            this.inputField.value = value + " ";
          } else {
            this.inputField.value = value;
          }

          // Avoid selecting the text if this method is called twice in a row.
          this.selectionStart = -1;

          let event = document.createEvent("Events");
          event.initEvent("input", true, true);
          this.inputField.dispatchEvent(event);

          // handleText() ignores the value if it's the same as the previous
          // value, but we want consecutive searches with the same value to be
          // possible.  If handleText() returned false, then manually start a
          // new search here.
          if (!this._onInputHandledText) {
            this.gotResultForCurrentQuery = false;
            this.controller.startSearch(value);
          }
        ]]></body>
      </method>

      <method name="removeHiddenFocus">
        <body><![CDATA[
          this.classList.remove("hidden-focus");
        ]]></body>
      </method>

      <method name="setHiddenFocus">
        <body><![CDATA[
          this.classList.add("hidden-focus");
          this.focus();
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="keydown"><![CDATA[
        if (this._noActionKeys.has(event.keyCode) &&
            this.popup.selectedIndex >= 0 &&
            !this._pressedNoActionKeys.has(event.keyCode)) {
          if (this._pressedNoActionKeys.size == 0) {
            this.popup.setAttribute("noactions", "true");
            this.removeAttribute("actiontype");
          }
          this._pressedNoActionKeys.add(event.keyCode);
        }
      ]]></handler>

      <handler event="keyup"><![CDATA[
        if (this._noActionKeys.has(event.keyCode) &&
            this._pressedNoActionKeys.has(event.keyCode)) {
          this._pressedNoActionKeys.delete(event.keyCode);
          if (this._pressedNoActionKeys.size == 0)
            this._clearNoActions();
        }
      ]]></handler>

      <handler event="mousedown"><![CDATA[
        if (event.button == 0) {
          if (event.originalTarget.getAttribute("anonid") == "historydropmarker") {
            this.toggleHistoryPopup();
          }

          // Eventually show the opt-out notification even if the location bar is
          // empty, focused, and the user clicks on it.
          if (this.focused && this.textValue == "") {
            this.maybeShowSearchSuggestionsNotificationOnFocus(true);
          }
        }
      ]]></handler>

      <handler event="focus"><![CDATA[
        if (event.originalTarget == this.inputField) {
          this._updateUrlTooltip();
          this.formatValue();
          if (this.getAttribute("pageproxystate") != "valid") {
            UpdatePopupNotificationsVisibility();
          }

          // We show the opt-out notification when the mouse/keyboard focus the
          // urlbar, but in any case we want to enforce at least one
          // notification when the user focuses it with the mouse.
          let whichNotification = this.whichSearchSuggestionsNotification;
          if (whichNotification == "opt-out" &&
              this._showSearchSuggestionNotificationOnMouseFocus === undefined) {
            this._showSearchSuggestionNotificationOnMouseFocus = true;
          }

          // Check whether the focus change came from a keyboard/mouse action.
          let focusMethod = Services.focus.getLastFocusMethod(window);
          // If it's a focus started by code and the primary user intention was
          // not to go to the location bar, don't show a notification.
          if (!focusMethod && !this.userInitiatedFocus) {
            return;
          }

          let mouseFocused = !!(focusMethod & Services.focus.FLAG_BYMOUSE);
          this.maybeShowSearchSuggestionsNotificationOnFocus(mouseFocused);
        }
      ]]></handler>

      <handler event="blur"><![CDATA[
        if (event.originalTarget == this.inputField) {
          this._clearNoActions();
          this.formatValue();
          if (this.getAttribute("pageproxystate") != "valid") {
            UpdatePopupNotificationsVisibility();
          }
        }
        if (this.ExtensionSearchHandler.hasActiveInputSession()) {
          this.ExtensionSearchHandler.handleInputCancelled();
        }
        if (this._deferredKeyEventTimeout) {
          clearTimeout(this._deferredKeyEventTimeout);
          this._deferredKeyEventTimeout = null;
        }
        this._deferredKeyEventQueue = [];
      ]]></handler>

      <handler event="dragstart" phase="capturing"><![CDATA[
        // Drag only if the gesture starts from the input field.
        if (this.inputField != event.originalTarget &&
            !(this.inputField.compareDocumentPosition(event.originalTarget) &
              Node.DOCUMENT_POSITION_CONTAINED_BY))
          return;

        // Drag only if the entire value is selected and it's a valid URI.
        var isFullSelection = this.selectionStart == 0 &&
                              this.selectionEnd == this.textLength;
        if (!isFullSelection ||
            this.getAttribute("pageproxystate") != "valid")
          return;

        var urlString = gBrowser.selectedBrowser.currentURI.displaySpec;
        var title = gBrowser.selectedBrowser.contentTitle || urlString;
        var htmlString = "<a href=\"" + urlString + "\">" + urlString + "</a>";

        var dt = event.dataTransfer;
        dt.setData("text/x-moz-url", urlString + "\n" + title);
        dt.setData("text/unicode", urlString);
        dt.setData("text/html", htmlString);

        dt.effectAllowed = "copyLink";
        event.stopPropagation();
      ]]></handler>

      <handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/>
      <handler event="drop" phase="capturing" action="this.onDrop(event, this);"/>
      <handler event="select"><![CDATA[
        if (!Cc["@mozilla.org/widget/clipboard;1"]
               .getService(Ci.nsIClipboard)
               .supportsSelectionClipboard())
          return;

        if (!window.windowUtils.isHandlingUserInput)
          return;

        var val = this._getSelectedValueForClipboard();
        if (!val)
          return;

        Cc["@mozilla.org/widget/clipboardhelper;1"]
          .getService(Ci.nsIClipboardHelper)
          .copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard);
      ]]></handler>
    </handlers>

  </binding>

  <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">

    <content ignorekeys="true" level="top" consumeoutsideclicks="never"
             aria-owns="richlistbox">
      <xul:deck anonid="search-suggestions-notification"
                align="center"
                role="alert"
                selectedIndex="0">
        <!-- OPT-OUT -->
        <xul:hbox flex="1" align="center" anonid="search-suggestions-opt-out">
          <xul:image class="ac-site-icon" type="searchengine"/>
          <xul:hbox anonid="search-suggestions-hint-typing">
            <xul:description class="ac-title-text">&brandShortName;</xul:description>
          </xul:hbox>
          <xul:hbox anonid="search-suggestions-hint-box" flex="1">
            <xul:description id="search-suggestions-hint">
              <html:span class="prefix">&#x1f4a1; &urlbar.searchSuggestionsNotification.hintPrefix;</html:span>
              <html:span>&urlbar.searchSuggestionsNotification.hint;</html:span>
            </xul:description>
          </xul:hbox>
          <xul:label id="search-suggestions-change-settings"
                     class="text-link"
                     role="link"
#ifdef XP_WIN
                     value="&urlbar.searchSuggestionsNotification.changeSettingsWin;"
                     accesskey="&urlbar.searchSuggestionsNotification.changeSettingsWin.accesskey;"
#else
                     value="&urlbar.searchSuggestionsNotification.changeSettingsUnix;"
                     accesskey="&urlbar.searchSuggestionsNotification.changeSettingsUnix.accesskey;"
#endif
                     onclick="openPreferences('paneSearch', {origin: 'searchChangeSettings'});"
                     control="search-suggestions-change-settings"/>
        </xul:hbox>
      </xul:deck>
      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
                       flex="1"/>
      <xul:hbox anonid="footer">
        <children/>
        <xul:hbox anonid="one-off-search-buttons"
                  class="search-one-offs"
                  compact="true"
                  includecurrentengine="true"
                  disabletab="true"
                  flex="1"/>
      </xul:hbox>
    </content>

    <implementation>
      <!--
        For performance reasons we want to limit the size of the text runs we
        build and show to the user.
      -->
      <field name="textRunsMaxLen">255</field>

      <field name="_maxResults">0</field>

      <field name="_bundle" readonly="true">
        Cc["@mozilla.org/intl/stringbundle;1"].
          getService(Ci.nsIStringBundleService).
          createBundle("chrome://browser/locale/places/places.properties");
      </field>

      <field name="searchSuggestionsNotification" readonly="true">
        document.getAnonymousElementByAttribute(
          this, "anonid", "search-suggestions-notification"
        );
      </field>

      <field name="footer" readonly="true">
        document.getAnonymousElementByAttribute(this, "anonid", "footer");
      </field>

      <field name="shrinkDelay" readonly="true">
        250
      </field>

      <field name="oneOffSearchButtons" readonly="true">
        new SearchOneOffs(
          document.getAnonymousElementByAttribute(this, "anonid",
                                                  "one-off-search-buttons"));
      </field>

      <field name="_overrideValue">null</field>
      <property name="overrideValue"
                onget="return this._overrideValue;"
                onset="this._overrideValue = val; return val;"/>

      <method name="onPopupClick">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.button == 2) {
            // Ignore right-clicks.
            return;
          }
          // Otherwise "call super" -- do what autocomplete-base-popup does.
          this.input.controller.handleEnter(true, aEvent);
        ]]></body>
      </method>

      <field name="_oneOffSearchesEnabledByReason">new Map()</field>

      <method name="toggleOneOffSearches">
        <parameter name="enable"/>
        <parameter name="reason"/>
        <body><![CDATA[
          this._oneOffSearchesEnabledByReason.set(reason || "runtime", enable);
          this._syncOneOffSearchesEnabled();
        ]]></body>
      </method>

      <method name="_syncOneOffSearchesEnabled">
        <body><![CDATA[
          // If the popup hasn't ever been opened yet, then don't actually do
          // anything.  (The popup will still be hidden in that case.)  The
          // input adds a popupshowing listener that will call this method back
          // and lazily initialize the one-off buttons the first time the popup
          // opens.  There are performance tests that fail if we don't do this.
          if (this.hidden) {
            return;
          }

          let enable = Array.from(this._oneOffSearchesEnabledByReason.values())
                            .every(v => v);
          if (enable) {
            this.oneOffSearchButtons.telemetryOrigin = "urlbar";
            this.oneOffSearchButtons.style.display = "-moz-box";
            // Set .textbox first, since the popup setter will cause
            // a _rebuild call that uses it.
            this.oneOffSearchButtons.textbox = this.input;
            this.oneOffSearchButtons.popup = this;
          } else {
            this.oneOffSearchButtons.telemetryOrigin = null;
            this.oneOffSearchButtons.style.display = "none";
            this.oneOffSearchButtons.textbox = null;
            this.oneOffSearchButtons.popup = null;
          }
        ]]></body>
      </method>

      <property name="oneOffSearchesEnabled" readonly="true">
        <getter><![CDATA[
          return this.oneOffSearchButtons.style.display != "none";
        ]]></getter>
      </property>

      <!-- Override this so that navigating between items results in an item
           always being selected. -->
      <method name="getNextIndex">
        <parameter name="reverse"/>
        <parameter name="amount"/>
        <parameter name="index"/>
        <parameter name="maxRow"/>
        <body><![CDATA[
          if (maxRow < 0)
            return -1;

          let newIndex = index + (reverse ? -1 : 1) * amount;

          // We only want to wrap if navigation is in any direction by one item,
          // otherwise we clamp to one end of the list.
          // ie, hitting page-down will only cause is to wrap if we're already
          // at one end of the list.

          // Allow the selection to be removed if the first result is not a
          // heuristic result.
          if (!this._isFirstResultHeuristic) {
            if (reverse && index == -1 || newIndex > maxRow && index != maxRow)
              newIndex = maxRow;
            else if (!reverse && index == -1 || newIndex < 0 && index != 0)
              newIndex = 0;

            if (newIndex < 0 && index == 0 || newIndex > maxRow && index == maxRow)
              newIndex = -1;

            return newIndex;
          }

          // Otherwise do not allow the selection to be removed.
          if (newIndex < 0) {
            newIndex = index > 0 ? 0 : maxRow;
          } else if (newIndex > maxRow) {
            newIndex = index < maxRow ? maxRow : 0;
          }
          return newIndex;
        ]]></body>
      </method>

      <property name="_isFirstResultHeuristic" readonly="true">
        <getter>
          <![CDATA[
            // The popup usually has a special "heuristic" first result (added
            // by UnifiedComplete.js) that is automatically selected when the
            // popup opens.
            return this.input.mController.matchCount > 0 &&
                   this.input.mController
                             .getStyleAt(0)
                             .split(/\s+/).indexOf("heuristic") > 0;
          ]]>
        </getter>
      </property>

      <property name="maxResults">
        <getter>
          <![CDATA[
            if (!this._maxResults) {
              this._maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
            }
            return this._maxResults;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            return this._maxResults = parseInt(val);
          ]]>
        </setter>
      </property>

      <!-- This is set either to undefined or to a new object containing
           { start, end } margin values in pixels. These are used to align the
           results to the input field. -->
      <property name="margins"
                onget="return this._margins;">
        <setter>
          <![CDATA[
          this._margins = val;

          if (val) {
            /* eslint-disable no-multi-spaces */
            let paddingInCSS =
                3   // .autocomplete-richlistbox padding-left/right
              + 6   // .ac-site-icon margin-inline-start
              + 16  // .ac-site-icon width
              + 6;  // .ac-site-icon margin-inline-end
            /* eslint-enable no-multi-spaces */
            let actualVal = Math.round(val.start) - paddingInCSS;
            let actualValEnd = Math.round(val.end);
            this.style.setProperty("--item-padding-start", actualVal + "px");
            this.style.setProperty("--item-padding-end", actualValEnd + "px");
          } else {
            this.style.removeProperty("--item-padding-start");
            this.style.removeProperty("--item-padding-end");
          }

          return val;
          ]]>
        </setter>
      </property>

      <method name="openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body>
          <![CDATA[
          // initially the panel is hidden
          // to avoid impacting startup / new window performance
          aInput.popup.hidden = false;

          this._openAutocompletePopup(aInput, aElement);
          ]]>
        </body>
      </method>

      <method name="_openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body><![CDATA[
          if (this.mPopupOpen) {
            return;
          }

          // Explicitly set the direction of the popup because automplete.xml
          // expects this.
          this.style.direction = (RTL_UI ? "rtl" : "ltr");

          // Make the popup span the width of the window.  First, set its width.
          let documentRect =
            window.windowUtils
                .getBoundsWithoutFlushing(window.document.documentElement);
          let width = documentRect.right - documentRect.left;
          this.setAttribute("width", width);

          // Now make its starting margin negative so that its leading edge
          // aligns with the window border.
          let elementRect =
            window.windowUtils.getBoundsWithoutFlushing(aElement);
          if (RTL_UI) {
            let offset = elementRect.right - documentRect.right;
            this.style.marginRight = offset + "px";
          } else {
            let offset = documentRect.left - elementRect.left;
            this.style.marginLeft = offset + "px";
          }

          // Keep the popup items' site icons aligned with the urlbar's identity
          // icon if it's not too far from the edge of the window.  We define
          // "too far" as "more than 30% of the window's width AND more than
          // 250px".  Do this *before* adding any items because when the new
          // value of the margins are different from the previous value, over-
          // and underflow must be handled for each item already in the popup.
          let needsHandleOverUnderflow = false;
          let boundToCheck = RTL_UI ? "right" : "left";
          let inputRect = window.windowUtils.getBoundsWithoutFlushing(aInput);
          let startOffset = Math.abs(inputRect[boundToCheck] - documentRect[boundToCheck]);
          let alignSiteIcons = startOffset / width <= 0.3 || startOffset <= 250;
          if (alignSiteIcons) {
            // Calculate the end margin if we have a start margin.
            let boundToCheckEnd = RTL_UI ? "left" : "right";
            let endOffset = Math.abs(inputRect[boundToCheckEnd] -
                                     documentRect[boundToCheckEnd]);
            if (endOffset > startOffset * 2) {
              // Provide more space when aligning would result in an unbalanced
              // margin. This allows the location bar to be moved to the start
              // of the navigation toolbar to reclaim space for results.
              endOffset = startOffset;
            }
            let identityIcon = document.getElementById("identity-icon");
            let identityRect =
              window.windowUtils.getBoundsWithoutFlushing(identityIcon);
            let start = RTL_UI ?
                        documentRect.right - identityRect.right :
                        identityRect.left;
            if (!this.margins || start != this.margins.start ||
                                 endOffset != this.margins.end ||
                                 width != this.margins.width) {
              this.margins = { start, end: endOffset, width };
              needsHandleOverUnderflow = true;
            }
          } else if (this.margins) {
            // Reset the alignment so that the site icons are positioned
            // according to whatever's in the CSS.
            this.margins = undefined;
            needsHandleOverUnderflow = true;
          }

          // Now that the margins have been set, start adding items (via
          // _invalidate).
          this.mInput = aInput;
          this.input.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1);
          this.input.userSelectionBehavior = "none";
          this._invalidate();

          try {
            let whichNotification = aInput.whichSearchSuggestionsNotification;
            if (whichNotification != "none") {
              // Update the impressions count on real popupshown, since there's
              // no guarantee openPopup will be respected by the platform.
              // Though, we must ensure the handled event is the expected one.
              let impressionId = this._searchSuggestionsImpressionId = {};
              this.addEventListener("popupshown", () => {
                if (this._searchSuggestionsImpressionId == impressionId)
                  aInput.updateSearchSuggestionsNotificationImpressions(whichNotification);
              }, {once: true});
              this._showSearchSuggestionsNotification(whichNotification);
            } else if (this.classList.contains("showSearchSuggestionsNotification")) {
              this._hideSearchSuggestionsNotification();
            }
          } catch (ex) {
            // Not critical for the urlbar functionality, just report the error.
            Cu.reportError(ex);
          }

          // Position the popup below the navbar.  To get the y-coordinate,
          // which is an offset from the bottom of the input, subtract the
          // bottom of the navbar from the buttom of the input.
          let yOffset = Math.round(
            window.windowUtils.getBoundsWithoutFlushing(document.getElementById("nav-bar")).bottom -
            window.windowUtils.getBoundsWithoutFlushing(aInput).bottom);

          if (!this.richlistbox.suppressMenuItemEvent && this.richlistbox.currentItem) {
            // The richlistbox fired a DOMMenuItemActive for accessibility,
            // but because the popup isn't open yet, accessibility will ignore
            // it. Therefore, fire it again once the popup opens.
            this.addEventListener("popupshown", () => {
              this.richlistbox._fireEvent(this.richlistbox.currentItem,
                "DOMMenuItemActive");
            }, {once: true});
          }

          this.openPopup(aElement, "after_start", 0, yOffset, false, false);

          // Do this immediately after we've requested the popup to open. This
          // will cause sync reflows but prevents flickering.
          if (needsHandleOverUnderflow) {
            for (let item of this.richlistbox.children) {
              item.handleOverUnderflow();
            }
          }
        ]]></body>
      </method>

      <method name="adjustHeight">
        <body>
          <![CDATA[
          // If we were going to shrink later, cancel that for now:
          if (this._shrinkTimeout) {
            clearTimeout(this._shrinkTimeout);
            this._shrinkTimeout = null;
          }
          let lastRowCount = this._lastRowCount;
          // Figure out how many rows to show
          let rows = this.richlistbox.children;
          this._lastRowCount = rows.length;
          let numRows = Math.min(this.matchCount, this.maxRows, rows.length);

          // If we're going from 0 to non-0 rows, we might need to remove
          // the height attribute to allow the popup to size. The attribute
          // is set from XUL popup management code.
          if (!lastRowCount && rows.length) {
            this.removeAttribute("height");
          }

          // Default the height to 0 if we have no rows to show
          let height = 0;
          if (numRows) {
            if (!this._rowHeight) {
              window.promiseDocumentFlushed(() => {
                if (window.closed) {
                  return;
                }
                this._rowHeight = rows[0].getBoundingClientRect().height;
                let style = window.getComputedStyle(this.richlistbox);

                let paddingTop = parseInt(style.paddingTop) || 0;
                let paddingBottom = parseInt(style.paddingBottom) || 0;
                this._rlbPadding = paddingTop + paddingBottom;
                // Then re-run - but don't dirty layout from inside this callback.
                window.requestAnimationFrame(() => this.adjustHeight());
              });
              return;
            }

            // Calculate the height to have the first row to last row shown
            height = (this._rowHeight * numRows) + this._rlbPadding;
          }

          let animate = this.getAttribute("dontanimate") != "true";
          let currentHeight =
            parseFloat(this.richlistbox.getAttribute("height"), 10) ||
            parseFloat(this.richlistbox.style.height, 10) ||
            0; // It's possible we get here when we haven't set height on the richlistbox
               // yet, which means parseFloat will return NaN. It should return 0 instead.
          if (height > currentHeight) {
            // Grow immediately.
            if (animate) {
              this.richlistbox.removeAttribute("height");
              this.richlistbox.style.height = height + "px";
            } else {
              this.richlistbox.style.removeProperty("height");
              this.richlistbox.height = height;
            }
          } else if (height < currentHeight) { // Don't shrink if height matches exactly
            // Delay shrinking to avoid flicker.
            this._shrinkTimeout = setTimeout(() => {
              this._collapseUnusedItems();
              if (animate) {
                this.richlistbox.removeAttribute("height");
                this.richlistbox.style.height = height + "px";
              } else {
                this.richlistbox.style.removeProperty("height");
                this.richlistbox.height = height;
              }
            }, this.shrinkDelay);
          }
          ]]>
        </body>
      </method>

      <method name="_showSearchSuggestionsNotification">
        <parameter name="whichNotification"/>
        <body>
          <![CDATA[
          if (whichNotification == "opt-out") {
            if (this.margins) {
              this.searchSuggestionsNotification.style.paddingInlineStart =
                this.margins.start + "px";
            } else {
              this.searchSuggestionsNotification.style.removeProperty("padding-inline-start");
            }

            // We want to animate the opt-out hint only once.
            if (!this._firstSearchSuggestionsNotification) {
              this._firstSearchSuggestionsNotification = true;
              this.searchSuggestionsNotification.setAttribute("animate", "true");
            }
          }

          this.searchSuggestionsNotification.setAttribute("aria-describedby",
                                                          "search-suggestions-hint");

          // With the notification shown, the listbox's height can sometimes be
          // too small when it's flexed, as it normally is.  Also, it can start
          // out slightly scrolled down.  Both problems appear together, most
          // often when the popup is very narrow and the notification's text
          // must wrap.  Work around them by removing the flex.
          //
          // But without flexing the listbox, the listbox's height animation
          // sometimes fails to complete, leaving the popup too tall.  Work
          // around that problem by disabling the listbox animation.
          this.richlistbox.flex = 0;
          this.setAttribute("dontanimate", "true");

          this.classList.add("showSearchSuggestionsNotification");
          // Don't show the one-off buttons if we are showing onboarding and
          // there's no result, since it would be ugly and pointless.
          this.footer.collapsed = this.matchCount == 0;
          this.input.tabScrolling = this.matchCount != 0;

          // This event allows accessibility APIs to see the notification.
          if (!this.popupOpen) {
            let event = document.createEvent("Events");
            event.initEvent("AlertActive", true, true);
            this.searchSuggestionsNotification.dispatchEvent(event);
          }
          ]]>
        </body>
      </method>

      <method name="_hideSearchSuggestionsNotification">
        <body>
          <![CDATA[
          this.classList.remove("showSearchSuggestionsNotification");
          this.richlistbox.flex = 1;
          this.removeAttribute("dontanimate");
          this.searchSuggestionsNotification.removeAttribute("animate");
          if (this.matchCount) {
            // Update popup height.
            this._invalidate();
          } else {
            this.closePopup();
          }
          ]]>
        </body>
      </method>

      <!-- This handles keypress changes to the selection among the one-off
           search buttons and between the one-offs and the listbox.  It returns
           true if the keypress was consumed and false if not. -->
      <method name="handleKeyPress">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this.oneOffSearchButtons.handleKeyPress(aEvent, this.matchCount,
                                                  !this._isFirstResultHeuristic,
                                                  gBrowser.userTypedValue);
          return aEvent.defaultPrevented && !aEvent.urlbarDeferred;
        ]]></body>
      </method>

      <!-- This is called when a one-off is clicked and when "search in new tab"
           is selected from a one-off context menu. -->
      <method name="handleOneOffSearch">
        <parameter name="event"/>
        <parameter name="engine"/>
        <parameter name="where"/>
        <parameter name="params"/>
        <body><![CDATA[
          this.input.handleCommand(event, where, params);
        ]]></body>
      </method>

      <!-- Result listitems call this to determine which search engine they
           should show in their labels and include in their url attributes. -->
      <property name="overrideSearchEngineName" readonly="true">
        <getter><![CDATA[
          let button = this.oneOffSearchButtons.selectedButton;
          return button && button.engine && button.engine.name;
        ]]></getter>
      </property>

      <method name="createResultLabel">
        <parameter name="item"/>
        <parameter name="proposedLabel"/>
        <body>
          <![CDATA[
            let parts = [proposedLabel];

            let action = this.input._parseActionUrl(item.getAttribute("url"));
            if (action) {
              switch (action.type) {
              case "searchengine":
                parts = [
                  action.params.searchSuggestion || action.params.searchQuery,
                  action.params.engineName,
                ];
                break;
              case "switchtab":
              case "remotetab":
                parts = [
                  item.getAttribute("title"),
                  item.getAttribute("displayurl"),
                ];
                break;
              }
            }

            let types = item.getAttribute("type").split(/\s+/);
            let type = types.find(t => t != "action" && t != "heuristic");
            try {
              // Some types intentionally do not map to strings, which is not
              // an error.
              parts.push(this._bundle.GetStringFromName(type + "ResultLabel"));
            } catch (e) {}

            return parts.filter(str => str).join(" ");
          ]]>
        </body>
      </method>

      <method name="onResultsAdded">
        <body>
          <![CDATA[
            // If nothing is selected yet, select the first result if it is a
            // pre-selected "heuristic" result.  (See UnifiedComplete.js.)
            let selectHeuristic =
              this.selectedIndex == -1 && this._isFirstResultHeuristic;
            if (selectHeuristic) {
              this.input.controller.setInitiallySelectedIndex(0);
            }

            // If this is the heuristic result of a new search, format its
            // search alias in the input or remove the formatting of the
            // previous alias, as necessary.  We need to check selectHeuristic
            // because the result may have already been added but only now is
            // being selected, and we need to check gotResultForCurrentQuery
            // because the result may be from the previous search and already
            // selected and is now being reused.
            if (selectHeuristic || !this.input.gotResultForCurrentQuery) {
              this.input.formatValue();

              // Also, hide the one-off search buttons if the user is using, or
              // starting to use, an "@engine" search engine alias, or typed
              // only the search restriction character.
              let trimmedValue = this.input.value.trim();
              this.toggleOneOffSearches(
                trimmedValue[0] != "@" &&
                (trimmedValue[0] != UrlbarTokenizer.RESTRICT.SEARCH ||
                 trimmedValue.length != 1)
              );
            }

            if (this.matchCount > 0) {
              // If this is the first time we get the result from the current
              // search and we are not in the private context, we can speculatively
              // connect to the intended site as a performance optimization.
              if (!this.input.gotResultForCurrentQuery &&
                  !this.input.inPrivateContext) {
                let firstStyle = this.input.mController.getStyleAt(0);
                if (firstStyle.includes("autofill")) {
                  let uri = this.input.mController.getFinalCompleteValueAt(0);
                  UrlbarUtils.setupSpeculativeConnection(uri, window);
                } else if (firstStyle.includes("searchengine") &&
                          this.input.browserSearchSuggestEnabled &&
                          this.input.urlbarSearchSuggestEnabled) {
                  // Preconnect to the current search engine only if the search
                  // suggestions are enabled.
                  let engine = Services.search.defaultEngine;
                  UrlbarUtils.setupSpeculativeConnection(engine, window);
                }
              }

              // When a result is present the footer should always be visible.
              this.footer.collapsed = false;
            }

            this.input.tabScrolling = true;
            this.input.gotResultForCurrentQuery = true;
            this.input.replaySafeDeferredKeyEvents();
          ]]>
        </body>
      </method>

      <method name="_onSearchBegin">
        <body><![CDATA[
          // Set the selected index to 0 (heuristic) until a result comes back
          // and we can evaluate it better.
          //
          // This is required to properly manage delayed handleEnter:
          // 1. if a search starts we set selectedIndex to 0 here, and it will
          //    be updated by onResultsAdded. Since selectedIndex is 0,
          //    handleEnter will delay the action if a result didn't arrive yet.
          // 2. if a search doesn't start (for example if autocomplete is
          //    disabled), this won't be called, and the selectedIndex will be
          //    the default -1 value. Then handleEnter will know it should not
          //    delay the action, cause a result wont't ever arrive.
          this.input.controller.setInitiallySelectedIndex(0);

          // Since we are starting a new search, reset the currently selected
          // one-off button, to cover those cases where the oneoff buttons
          // binding won't receive an actual DOM event. For example, a search
          // could be started without an actual input event, and the popup may
          // not have been closed from the previous search.
          this.oneOffSearchButtons.selectedButton = null;
        ]]></body>
      </method>

      <field name="_addonIframe">null</field>
      <field name="_addonIframeOwner">null</field>
      <field name="_addonIframeOverriddenFunctionsByName">{}</field>

      <!-- These methods must be overridden and properly handled by the API
           runtime so that it doesn't break the popup.  If any of these methods
           is not overridden, then initAddonIframe should throw. -->
      <field name="_addonIframeOverrideFunctionNames">[
        "_invalidate",
      ]</field>

      <field name="_addonIframeHiddenAnonids">[
        "search-suggestions-notification",
        "richlistbox",
        "one-off-search-buttons",
      ]</field>
      <field name="_addonIframeHiddenDisplaysByAnonid">{}</field>

      <method name="initAddonIframe">
        <parameter name="owner"/>
        <parameter name="overrides"/>
        <body><![CDATA[
          if (this._addonIframeOwner) {
            // Another add-on has already requested the iframe.  Return null to
            // signal to the calling add-on that it should not take over the
            // popup.  First add-on wins for now.
            return null;
          }
          // Make sure all overrides are provided before doing anything.
          for (let name of this._addonIframeOverrideFunctionNames) {
            if (typeof(overrides[name]) != "function") {
              throw new Error(
                "Override for method '" + name + "' must be given"
              );
            }
          }
          // OK, insert the iframe.
          this._addonIframeOwner = owner;
          this._addonIframe = this._makeAddonIframe();
          this._addonIframeOverriddenFunctionsByName = {};
          for (let name of this._addonIframeOverrideFunctionNames) {
            this._addonIframeOverriddenFunctionsByName[name] = this[name];
            this[name] = overrides[name];
          }
          return this._addonIframe;
        ]]></body>
      </method>

      <method name="destroyAddonIframe">
        <parameter name="owner"/>
        <body><![CDATA[
          if (this._addonIframeOwner != owner) {
            throw new Error("You're not the iframe owner");
          }
          this._addonIframeOwner = null;
          this._addonIframe.remove();
          this._addonIframe = null;
          for (let anonid of this._addonIframeHiddenAnonids) {
            let child = document.getAnonymousElementByAttribute(
              this, "anonid", anonid
            );
            child.style.display =
              this._addonIframeHiddenDisplaysByAnonid[anonid];
          }
          for (let name in this._addonIframeOverriddenFunctionsByName) {
            this[name] = this._addonIframeOverriddenFunctionsByName[name];
          }
          this._addonIframeOverriddenFunctionsByName = {};
        ]]></body>
      </method>

      <method name="_makeAddonIframe">
        <body><![CDATA[
          this._addonIframeHiddenDisplaysByAnonid = {};
          for (let anonid of this._addonIframeHiddenAnonids) {
            let child = document.getAnonymousElementByAttribute(
              this, "anonid", anonid
            );
            this._addonIframeHiddenDisplaysByAnonid[anonid] =
              child.style.display;
            child.style.display = "none";
          }
          let iframe = document.createXULElement("iframe");
          iframe.setAttribute("type", "content");
          iframe.setAttribute("flex", "1");
          iframe.style.transition = "height 100ms";
          this.appendChild(iframe);
          return iframe;
        ]]></body>
      </method>

    </implementation>
    <handlers>

      <handler event="SelectedOneOffButtonChanged"><![CDATA[
        // Update all searchengine result items to use the newly selected
        // engine.
        for (let item of this.richlistbox.children) {
          if (item.collapsed) {
            break;
          }
          let url = item.getAttribute("url");
          if (url) {
            let action = item._parseActionUrl(url);
            if (action && action.type == "searchengine") {
              item._adjustAcItem();
            }
          }
        }

        // If the selection moved from the results to the one-off settings
        // button, then call formatValue to remove the formatting of the search
        // alias in the input, if any.  In all other cases the alias formatting
        // is removed when the input's value setter calls formatValue, but in
        // this specific case, at the time that formatValue is called,
        // oneOffSearchButtons.selectedButton is still null, so the formatting
        // is not removed.  The settings button is selected right after that.
        if (this.oneOffSearchButtons.selectedButton ==
              this.oneOffSearchButtons.settingsButtonCompact &&
            (!event.detail || !event.detail.previousSelectedButton)) {
          this.input.formatValue();
        }
      ]]></handler>

      <handler event="mousedown"><![CDATA[
        // Required to make the xul:label.text-link elements in the search
        // suggestions notification work correctly when clicked on Linux.
        // This is copied from the mousedown handler in
        // browser-search-autocomplete-result-popup, which apparently had a
        // similar problem.
        event.preventDefault();

        if (event.button == 2) {
          // Right mouse button currently allows to select.
          this.input.userSelectionBehavior = "rightClick";
          // Ignore right-clicks.
          return;
        }

        if (!UrlbarPrefs.get("speculativeConnect.enabled")) {
          return;
        }

        // Ensure the user is clicking on an url instead of other buttons
        // on the popup.
        let elt = event.originalTarget;
        while (elt && elt.localName != "richlistitem" && elt != this) {
          elt = elt.parentNode;
        }
        if (!elt || elt.localName != "richlistitem") {
          return;
        }
        // The user might click on a ghost entry which was removed because of
        // the coming new results.
        if (this.input.controller.matchCount <= this.selectedIndex) {
          return;
        }

        // Whitelist the cases that we want to speculative connect, and ignore
        // other moz-action uris or fancy protocols.
        // Note that it's likely we've speculatively connected to the first
        // url because it is a heuristic "autofill" result (see bug 1348275).
        // "moz-action:searchengine" is also the same case. (see bug 1355443)
        // So we won't duplicate the effort here.
        let url = this.input.controller.getFinalCompleteValueAt(this.selectedIndex);
        if (url.startsWith("http") && this.selectedIndex > 0) {
          UrlbarUtils.setupSpeculativeConnection(url, window);
        } else if (url.startsWith("moz-action:remotetab")) {
          let action = PlacesUtils.parseActionUrl(url);
          if (action && action.params.url) {
            UrlbarUtils.setupSpeculativeConnection(action.params.url, window);
          }
        }
      ]]></handler>

    </handlers>
  </binding>

  <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
    <implementation>
      <constructor><![CDATA[
        if (!this.notification)
          return;

        this.notification.options.installs.forEach(function(aInstall) {
          aInstall.addListener(this);
        }, this);

        // Calling updateProgress can sometimes cause this notification to be
        // removed in the middle of refreshing the notification panel which
        // makes the panel get refreshed again. Just initialise to the
        // undetermined state and then schedule a proper check at the next
        // opportunity
        this.setProgress(0, -1);
        this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0);
      ]]></constructor>

      <destructor><![CDATA[
        this.destroy();
      ]]></destructor>

      <field name="progressmeter" readonly="true">
        document.getElementById("addon-progress-notification-progressmeter");
      </field>
      <field name="progresstext" readonly="true">
        document.getElementById("addon-progress-notification-progresstext");
      </field>
      <property name="DownloadUtils" readonly="true">
        <getter><![CDATA[
          let module = {};
          ChromeUtils.import("resource://gre/modules/DownloadUtils.jsm", module);
          Object.defineProperty(this, "DownloadUtils", {
            configurable: true,
            enumerable: true,
            writable: true,
            value: module.DownloadUtils,
          });
          return module.DownloadUtils;
        ]]></getter>
      </property>

      <method name="destroy">
        <body><![CDATA[
          if (!this.notification)
            return;

          this.notification.options.installs.forEach(function(aInstall) {
            aInstall.removeListener(this);
          }, this);
          clearTimeout(this._updateProgressTimeout);
        ]]></body>
      </method>

      <method name="setProgress">
        <parameter name="aProgress"/>
        <parameter name="aMaxProgress"/>
        <body><![CDATA[
          if (aMaxProgress == -1) {
            this.progressmeter.removeAttribute("value");
          } else {
            this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress);
          }

          let now = Date.now();

          if (!this.notification.lastUpdate) {
            this.notification.lastUpdate = now;
            this.notification.lastProgress = aProgress;
            return;
          }

          let delta = now - this.notification.lastUpdate;
          if ((delta < 400) && (aProgress < aMaxProgress))
            return;

          delta /= 1000;

          // This algorithm is the same used by the downloads code.
          let speed = (aProgress - this.notification.lastProgress) / delta;
          if (this.notification.speed)
            speed = speed * 0.9 + this.notification.speed * 0.1;

          this.notification.lastUpdate = now;
          this.notification.lastProgress = aProgress;
          this.notification.speed = speed;

          let status = null;
          [status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last);
          this.progresstext.setAttribute("value", status);
          this.progresstext.setAttribute("tooltiptext", status);
        ]]></body>
      </method>

      <method name="cancel">
        <body><![CDATA[
          let installs = this.notification.options.installs;
          installs.forEach(function(aInstall) {
            try {
              aInstall.cancel();
            } catch (e) {
              // Cancel will throw if the download has already failed
            }
          }, this);

          PopupNotifications.remove(this.notification);
        ]]></body>
      </method>

      <method name="updateProgress">
        <body><![CDATA[
          if (!this.notification)
            return;

          let downloadingCount = 0;
          let progress = 0;
          let maxProgress = 0;

          this.notification.options.installs.forEach(function(aInstall) {
            if (aInstall.maxProgress == -1)
              maxProgress = -1;
            progress += aInstall.progress;
            if (maxProgress >= 0)
              maxProgress += aInstall.maxProgress;
            if (aInstall.state < AddonManager.STATE_DOWNLOADED)
              downloadingCount++;
          });

          if (downloadingCount == 0) {
            this.destroy();
            this.progressmeter.removeAttribute("value");
            let status = gNavigatorBundle.getString("addonDownloadVerifying");
            this.progresstext.setAttribute("value", status);
            this.progresstext.setAttribute("tooltiptext", status);
          } else {
            this.setProgress(progress, maxProgress);
          }
        ]]></body>
      </method>

      <method name="onDownloadProgress">
        <body><![CDATA[
          this.updateProgress();
        ]]></body>
      </method>

      <method name="onDownloadFailed">
        <body><![CDATA[
          this.updateProgress();
        ]]></body>
      </method>

      <method name="onDownloadCancelled">
        <body><![CDATA[
          this.updateProgress();
        ]]></body>
      </method>

      <method name="onDownloadEnded">
        <body><![CDATA[
          this.updateProgress();
        ]]></body>
      </method>
    </implementation>
  </binding>
</bindings>