browser/base/content/urlbarBindings.xml
author Valentin Gosu <valentin.gosu@gmail.com>
Wed, 07 Dec 2016 10:18:46 -1000
changeset 454216 ff3d77e96732e415c5a6af942d0572de596a63bf
parent 453632 7c26572bc257084897fd019f1ccc6e6c45d2a421
child 454644 2c48e6cf77896bab121ec4f8fff8de1f5270bcb0
permissions -rw-r--r--
Bug 1320061 - Add pref that allows users to copy unescaped URL from the URL bar r=mak77 MozReview-Commit-ID: CDnMnkqj8gW * * * Bug 1320061 - Add test for copying non-ascii URL from the urlbar MozReview-Commit-ID: 72jymxn6DJv

<?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/autocomplete.xml#autocomplete">

    <content sizetopopup="pref">
      <xul:hbox anonid="textbox-container"
                class="autocomplete-textbox-container urlbar-textbox-container"
                flex="1" xbl:inherits="focused">
        <children includes="image|deck|stack|box">
          <xul:image class="autocomplete-icon" allowevents="true"/>
        </children>
        <xul:hbox anonid="textbox-input-box"
                  class="textbox-input-box urlbar-input-box"
                  flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
          <children/>
          <html:input anonid="input"
                      class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align"
                      allowevents="true"
                      inputmode="url"
                      xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/>
        </xul:hbox>
        <xul:dropmarker anonid="historydropmarker"
                        class="autocomplete-history-dropmarker urlbar-history-dropmarker"
                        tooltiptext="&urlbar.openHistoryPopup.tooltip;"
                        allowevents="true"
                        xbl:inherits="open,enablehistory,parentfocused=focused"/>
        <children includes="hbox"/>
      </xul:hbox>
      <xul:popupset anonid="popupset"
                    class="autocomplete-result-popupset"/>
      <children includes="toolbarbutton"/>
    </content>

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

      <field name="ExtensionSearchHandler" readonly="true">
        (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
      </field>

      <constructor><![CDATA[
        this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                                .getService(Components.interfaces.nsIPrefService)
                                .getBranch("browser.urlbar.");

        this._prefs.addObserver("", this, false);
        this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
        this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
        this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
        this.timeout = this._prefs.getIntPref("delay");
        this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled");
        this._mayTrimURLs = this._prefs.getBoolPref("trimURLs");
        this._cacheUserMadeSearchSuggestionsChoice();
        this.inputField.controllers.insertControllerAt(0, this._copyCutController);
        this.inputField.addEventListener("paste", this, false);
        this.inputField.addEventListener("mousedown", this, false);
        this.inputField.addEventListener("mousemove", this, false);
        this.inputField.addEventListener("mouseout", this, false);
        this.inputField.addEventListener("overflow", this, false);
        this.inputField.addEventListener("underflow", this, false);

        var textBox = document.getAnonymousElementByAttribute(this,
                                                "anonid", "textbox-input-box");
        var cxmenu = document.getAnonymousElementByAttribute(textBox,
                                            "anonid", "input-box-contextmenu");
        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");
        }, false);

        var insertLocation = cxmenu.firstChild;
        while (insertLocation.nextSibling &&
               insertLocation.getAttribute("cmd") != "cmd_paste")
          insertLocation = insertLocation.nextSibling;
        if (insertLocation) {
          pasteAndGo = document.createElement("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.nextSibling);
        }

        this._enableOrDisableOneOffSearches();
      ]]></constructor>

      <destructor><![CDATA[
        this._prefs.removeObserver("", this);
        this._prefs = null;
        this.inputField.controllers.removeController(this._copyCutController);
        this.inputField.removeEventListener("paste", this, false);
        this.inputField.removeEventListener("mousedown", this, false);
        this.inputField.removeEventListener("mousemove", this, false);
        this.inputField.removeEventListener("mouseout", this, false);
        this.inputField.removeEventListener("overflow", this, false);
        this.inputField.removeEventListener("underflow", this, false);
      ]]></destructor>

      <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>

      <!--
        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>

      <!--
        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.getOriginalUrl(aValue);
            if (originalUrl) {
              returnValue = originalUrl;
            }
          }

          // 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"/>
        <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;
          }
          if (this.popup.popupOpen &&
              !this.popup.disableKeyNavigation &&
              this.popup.handleKeyPress(aEvent)) {
            return true;
          }
          return this.handleKeyPress(aEvent);
        ]]></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>

      <field name="_formattingEnabled">true</field>
      <method name="formatValue">
        <body><![CDATA[
          if (!this._formattingEnabled || !this.editor)
            return;

          let controller = this.editor.selectionController;
          let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT);
          strikeOut.removeAllRanges();

          let selection = controller.getSelection(controller.SELECTION_URLSECONDARY);
          selection.removeAllRanges();

          if (this.focused)
            return;

          let textNode = this.editor.rootElement.firstChild;
          let value = textNode.textContent;
          if (!value)
            return;

          // Get the URL from the fixup service:
          let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
                      Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
          let uriInfo;
          try {
            uriInfo = Services.uriFixup.getFixupURIInfo(value, flags);
          } catch (ex) {}
          // Ignore if we couldn't make a URI out of this, the URI resulted in a search,
          // or the URI has a non-http(s)/ftp protocol.
          if (!uriInfo ||
              !uriInfo.fixedURI ||
              uriInfo.keywordProviderName ||
              ["http", "https", "ftp"].indexOf(uriInfo.fixedURI.scheme) == -1) {
            return;
          }

          // If we trimmed off the http scheme, ensure we stick it back on before
          // trying to figure out what domain we're accessing, so we don't get
          // confused by user:pass@host http URLs. We later use
          // trimmedLength to ensure we don't count the length of a trimmed protocol
          // when determining which parts of the URL to highlight as "preDomain".
          let trimmedLength = 0;
          if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) {
            value = "http://" + value;
            trimmedLength = "http://".length;
          }

          let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/);
          if (!matchedURL)
            return;

          // Strike out the "https" part if mixed active content is loaded.
          if (this.getAttribute("pageproxystate") == "valid" &&
              value.startsWith("https:") &&
              gBrowser.securityUI.state &
                Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
            let range = document.createRange();
            range.setStart(textNode, 0);
            range.setEnd(textNode, 5);
            strikeOut.addRange(range);
          }

          let [, preDomain, domain] = matchedURL;
          let baseDomain = domain;
          let subDomain = "";
          try {
            baseDomain = Services.eTLD.getBaseDomainFromHost(uriInfo.fixedURI.host);
            if (!domain.endsWith(baseDomain)) {
              // getBaseDomainFromHost converts its resultant to ACE.
              let IDNService = Cc["@mozilla.org/network/idn-service;1"]
                               .getService(Ci.nsIIDNService);
              baseDomain = IDNService.convertACEtoUTF8(baseDomain);
            }
          } catch (e) {}
          if (baseDomain != domain) {
            subDomain = domain.slice(0, -baseDomain.length);
          }

          let rangeLength = preDomain.length + subDomain.length - trimmedLength;
          if (rangeLength) {
            let range = document.createRange();
            range.setStart(textNode, 0);
            range.setEnd(textNode, rangeLength);
            selection.addRange(range);
          }

          let startRest = preDomain.length + domain.length - trimmedLength;
          if (startRest < value.length - trimmedLength) {
            let range = document.createRange();
            range.setStart(textNode, startRest);
            range.setEnd(textNode, value.length - trimmedLength);
            selection.addRange(range);
          }
        ]]></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();

            // 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>

      <!--
        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 openUILinkIn.  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 openUILinkIn.  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"/>
        <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;
          if (!where) {
            if (isMouseEvent) {
              where = whereToOpenLink(event, false, false);
            } else {
              // If the current tab is empty, ignore Alt+Enter (reuse this tab)
              let altEnter = !isMouseEvent &&
                             event &&
                             event.altKey &&
                             !isTabEmpty(gBrowser.selectedTab);
              where = altEnter ? "tab" : "current";
            }
          }

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

          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, where,
                                                   openUILinkParams);
          } 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 = 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;
                  if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
                    gBrowser.removeTab(prevTab);
                  }
                  return;
                }
                break;
              case "searchengine":
                if (selectedOneOff && selectedOneOff.engine) {
                  // Replace the engine with the selected one-off engine.
                  action.params.engineName = selectedOneOff.engine.name;
                }
                const actionDetails = {
                  isSuggestion: !!action.params.searchSuggestion,
                  isAlias: !!action.params.alias
                };
                [url, postData] = this._parseAndRecordSearchEngineLoad(
                  action.params.engineName,
                  action.params.searchSuggestion || action.params.searchQuery,
                  event,
                  where,
                  openUILinkParams,
                  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 {
              new URL(url);
            } catch (ex) {
              let lastLocationChange = browser.lastLocationChange;
              getShortcutOrURIAndPostData(url).then(data => {
                if (where != "current" ||
                    browser.lastLocationChange == lastLocationChange) {
                  this._loadURL(data.url, browser, data.postData, where,
                                openUILinkParams, data.mayInheritPrincipal);
                }
              });
              return;
            }
          }

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

      <property name="oneOffSearchQuery">
        <getter><![CDATA[
          // 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.handleEnterInstance && this.handleEnterInstance.searchString) ||
                 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"/>
        <body><![CDATA[
          this.value = url;
          browser.userTypedValue = url;
          if (gInitialPages.includes(url)) {
            browser.initialPageLoadedFromURLBar = url;
          }
          try {
            addToUrlbarHistory(url);
          } 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);
          }

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

          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 {
            openUILinkIn(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();
            }
          }

          if (openUILinkWhere == "current") {
            // 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="openUILinkWhere"/>
        <parameter name="openUILinkParams"/>
        <parameter name="searchActionDetails"/>
        <body><![CDATA[
          let engine =
            typeof(engineOrEngineName) == "string" ?
              Services.search.getEngineByName(engineOrEngineName) :
              engineOrEngineName;
          let isOneOff = this.popup.oneOffSearchButtons
              .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
          // 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) ||
              !(aTriggeringEvent instanceof KeyEvent)) {
            return;
          }

          let url = aUrl;
          let accel = this.AppConstants.platform == "macosx" ?
                      aTriggeringEvent.metaKey :
                      aTriggeringEvent.ctrlKey;
          let shift = aTriggeringEvent.shiftKey;
          let suffix = "";

          switch (true) {
            case (accel && shift):
              suffix = ".org/";
              break;
            case (shift):
              suffix = ".net/";
              break;
            case (accel):
              try {
                suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix");
                if (suffix.charAt(suffix.length - 1) != "/")
                  suffix += "/";
              } catch (e) {
                suffix = ".com/";
              }
              break;
          }

          if (!suffix)
            return;

          // trim leading/trailing spaces (bug 233205)
          url = url.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>

      <field name="_contentIsCropped">false</field>

      <method name="_initURLTooltip">
        <body><![CDATA[
          if (this.focused || !this._contentIsCropped)
            return;
          this.inputField.setAttribute("tooltiptext", this.value);
        ]]></body>
      </method>

      <method name="_hideURLTooltip">
        <body><![CDATA[
          this.inputField.removeAttribute("tooltiptext");
        ]]></body>
      </method>

      <method name="_getDroppableLink">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let links = browserDragAndDrop.dropLinks(aEvent);
          // 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) {
            aEvent.preventDefault();
            let url = links[0].url;
            let strippedURL = stripUnsafeProtocolOnPaste(url);
            if (strippedURL != url) {
              aEvent.stopImmediatePropagation();
              return null;
            }
            try {
              urlSecurityCheck(url,
                               gBrowser.contentPrincipal,
                               Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
            } catch (ex) {
              return null;
            }
            return url;
          }
          return null;
        ]]></body>
      </method>

      <method name="onDragOver">
        <parameter name="aEvent"/>
        <body><![CDATA[
          // We don't need the link here, so we ignore the return value.
          if (!this._getDroppableLink(aEvent)) {
            aEvent.dataTransfer.dropEffect = "none";
          }
        ]]></body>
      </method>

      <method name="onDrop">
        <parameter name="aEvent"/>
        <body><![CDATA[
          let url = this._getDroppableLink(aEvent);
          if (url) {
            this.value = url;
            SetPageProxyState("invalid");
            this.focus();
            this.handleCommand();
            // Force not showing the dropped URI immediately.
            gBrowser.userTypedValue = null;
            URLBarSetURI();
          }
        ]]></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;
          var selectedVal = inputVal.substring(this.selectionStart, this.selectionEnd);

          // If the selection doesn't start at the beginning or doesn't span the full domain or
          // the URL bar is modified, nothing else to do here.
          if (this.selectionStart > 0 || this.valueIsTyped)
            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;
          }

          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;
          }

          // Avoid copying 'about:reader?url=', and always provide the original URI:
          let readerOriginalURL = ReaderMode.getOriginalUrl(uri.spec);
          if (readerOriginalURL) {
            uri = uriFixup.createFixupURI(readerOriginalURL, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
          }

          // Only copy exposable URIs
          try {
            uri = uriFixup.createExposableURI(uri);
          } catch (ex) {}

          // 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.spec;
          }

          // Just the beginning of the URL is selected, or we want a decoded
          // url. First check for a trimmed value.
          let spec = uri.spec;
          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: function(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.dispatchEvent(event);

              SetPageProxyState("invalid");
            }

            Cc["@mozilla.org/widget/clipboardhelper;1"]
              .getService(Ci.nsIClipboardHelper)
              .copyString(val);
          },
          supportsCommand: function(aCommand) {
            switch (aCommand) {
              case "cmd_copy":
              case "cmd_cut":
                return true;
            }
            return false;
          },
          isCommandEnabled: function(aCommand) {
            return this.supportsCommand(aCommand) &&
                   (aCommand != "cmd_cut" || !this.urlbar.readOnly) &&
                   this.urlbar.selectionStart < this.urlbar.selectionEnd;
          },
          onEvent: function(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 "formatting.enabled":
                this._formattingEnabled = this._prefs.getBoolPref(aData);
                break;
              case "userMadeSearchSuggestionsChoice":
              case "suggest.searches":
                this._cacheUserMadeSearchSuggestionsChoice();
                if (this._userMadeSearchSuggestionsChoice) {
                  this.popup.searchSuggestionsNotificationWasDismissed(
                    this._prefs.getBoolPref("suggest.searches")
                  );
                }
                break;
              case "trimURLs":
                this._mayTrimURLs = this._prefs.getBoolPref(aData);
                break;
              case "oneOffSearches":
                this._enableOrDisableOneOffSearches();
                break;
            }
          }
        ]]></body>
      </method>

      <method name="_enableOrDisableOneOffSearches">
        <body><![CDATA[
          let enable = this._prefs.getBoolPref("oneOffSearches");
          this.popup.enableOneOffSearches(enable);
        ]]></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 "mousemove":
              this._initURLTooltip();
              break;
            case "mouseout":
              this._hideURLTooltip();
              break;
            case "overflow":
              this._contentIsCropped = true;
              break;
            case "underflow":
              this._contentIsCropped = false;
              this._hideURLTooltip();
              break;
          }
        ]]></body>
      </method>

      <!--
        onBeforeTextValueSet 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: 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 = this.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;
            // 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.
            if (this.mController.handleText()) {
              this.gotResultForCurrentQuery = false;
            }
          }
          this.resetActionType();
        ]]></body>
      </method>

      <method name="handleEnter">
        <parameter name="event"/>
        <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().
          // Note this is also used to detect if we should perform a delayed
          // handleEnter, in such a case it won't have been cleared.
          this.handleEnterInstance = {
            searchString: this.mController.searchString,
            event: event
          };

          if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) {
            this.maybeCanonizeURL(event, this.value);
            let rv = this.mController.handleEnter(false, event);
            this.handleEnterInstance = null;
            return rv;
          }

          return true;
        ]]></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>

      <field name="_userMadeSearchSuggestionsChoice"><![CDATA[
        false
      ]]></field>

      <method name="_cacheUserMadeSearchSuggestionsChoice">
        <body><![CDATA[
          this._userMadeSearchSuggestionsChoice =
            this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
            this._prefs.getBoolPref("suggest.searches");
        ]]></body>
      </method>

      <property name="shouldShowSearchSuggestionsNotification" readonly="true">
        <getter><![CDATA[
          return !this._userMadeSearchSuggestionsChoice &&
                 !this.inPrivateContext &&
                 // When _urlbarFocused is true, tabbrowser would close the
                 // popup if it's opened here, so don't show the notification.
                 !gBrowser.selectedBrowser._urlbarFocused &&
                 Services.prefs.getBoolPref("browser.search.suggest.enabled") &&
                 this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt");
        ]]></getter>
      </property>

    </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="focus"><![CDATA[
        if (event.originalTarget == this.inputField) {
          this._hideURLTooltip();
          this.formatValue();
        }
      ]]></handler>

      <handler event="blur"><![CDATA[
        if (event.originalTarget == this.inputField) {
          this._clearNoActions();
          this.formatValue();
        }
        if (ExtensionSearchHandler.hasActiveInputSession()) {
          ExtensionSearchHandler.handleInputCancelled();
        }
      ]]></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.spec;
        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.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils)
                   .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">

    <resources>
      <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
      <stylesheet src="chrome://browser/skin/searchbar.css"/>
    </resources>

    <content ignorekeys="true" level="top" consumeoutsideclicks="never"
             aria-owns="richlistbox">
      <xul:hbox anonid="search-suggestions-notification"
                align="center"
                role="alert"
                aria-describedby="search-suggestions-notification-text">
        <xul:description flex="1">
          &urlbar.searchSuggestionsNotification.question;
          <!-- Several things here are to make the label accessibile via an
               accesskey so that a11y doesn't suck: the accesskey, using an
               onclick handler instead of an href attribute, the control
               attribute, and having the control attribute refer to a valid ID
               that is the label itself. -->
          <xul:label id="search-suggestions-notification-learn-more"
                     class="text-link"
                     role="link"
                     value="&urlbar.searchSuggestionsNotification.learnMore;"
                     accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;"
                     onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();"
                     control="search-suggestions-notification-learn-more"/>
        </xul:description>
        <xul:button anonid="search-suggestions-notification-disable"
                    label="&urlbar.searchSuggestionsNotification.disable;"
                    accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;"
                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
        <xul:button anonid="search-suggestions-notification-enable"
                    label="&urlbar.searchSuggestionsNotification.enable;"
                    accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
      </xul:hbox>
      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
                       flex="1"/>
      <xul:hbox anonid="footer">
        <children/>
        <xul:vbox anonid="one-off-search-buttons"
                  class="search-one-offs"
                  compact="true"
                  includecurrentengine="true"
                  disabletab="true"
                  flex="1"/>
      </xul:hbox>
    </content>

    <implementation>
      <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="oneOffSearchButtons" readonly="true">
        document.getAnonymousElementByAttribute(this, "anonid",
                                                "one-off-search-buttons");
      </field>

      <field name="_oneOffSearchesEnabled">false</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.
          let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
          controller.handleEnter(true, aEvent);
        ]]></body>
      </method>

      <method name="enableOneOffSearches">
        <parameter name="enable"/>
        <body><![CDATA[
          this._oneOffSearchesEnabled = enable;
          if (enable) {
            this.oneOffSearchButtons.telemetryOrigin = "urlbar";
            this.oneOffSearchButtons.style.display = "-moz-box";
            this.oneOffSearchButtons.popup = this;
            this.oneOffSearchButtons.textbox = this.input;
          } else {
            this.oneOffSearchButtons.telemetryOrigin = null;
            this.oneOffSearchButtons.style.display = "none";
            this.oneOffSearchButtons.popup = null;
            this.oneOffSearchButtons.textbox = null;
          }
        ]]></body>
      </method>

      <method name="openSearchSuggestionsNotificationLearnMoreURL">
        <body><![CDATA[
        let url = Services.urlFormatter.formatURL(
          Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
        );
        openUILinkIn(url, "tab");
        ]]></body>
      </method>

      <method name="dismissSearchSuggestionsNotification">
        <parameter name="enableSuggestions"/>
        <body><![CDATA[
          // Make sure the urlbar is focused.  It won't be, for example, if the
          // user used an accesskey to make an opt-in choice.  mIgnoreFocus
          // prevents the text from being selected.
          this.input.mIgnoreFocus = true;
          this.input.focus();
          this.input.mIgnoreFocus = false;

          Services.prefs.setBoolPref(
            "browser.urlbar.suggest.searches", enableSuggestions
          );
          Services.prefs.setBoolPref(
            "browser.urlbar.userMadeSearchSuggestionsChoice", true
          );
          // The input's pref observer will now hide the notification.
        ]]></body>
      </method>

      <!-- 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) {
              var prefService =
                Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Components.interfaces.nsIPrefBranch);
              this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults");
            }
            return this._maxResults;
          ]]>
        </getter>
        <setter>
          <![CDATA[
            return this._maxResults = parseInt(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;

          let showNotification = aInput.shouldShowSearchSuggestionsNotification;
          if (showNotification) {
            let prefs = aInput._prefs;
            let now = new Date();
            let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
            let previousDate = prefs.getIntPref("lastSuggestionsPromptDate");
            if (previousDate < date) {
              let remainingDays =
                prefs.getIntPref("daysBeforeHidingSuggestionsPrompt") - 1;
              prefs.setIntPref("daysBeforeHidingSuggestionsPrompt",
                               remainingDays);
              prefs.setIntPref("lastSuggestionsPromptDate", date);
              if (!remainingDays)
                showNotification = false;
            }
          }

          if (showNotification) {
            this._showSearchSuggestionsNotification();
          } else if (this.classList.contains("showSearchSuggestionsNotification")) {
            this._hideSearchSuggestionsNotification();
          }

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

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

          this.mInput = aInput;
          aInput.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1);
          this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
          this._invalidate();

          var rect = window.document.documentElement.getBoundingClientRect();
          var width = rect.right - rect.left;
          this.setAttribute("width", width);

          // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
          var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
          this.style.direction = popupDirection;

          // Make the popup's starting margin negative so that the leading edge
          // of the popup aligns with the window border.
          let elementRect = aElement.getBoundingClientRect();
          if (popupDirection == "rtl") {
            let offset = elementRect.right - rect.right
            this.style.marginRight = offset + "px";
          } else {
            let offset = rect.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.  If there are
          // at most two toolbar buttons between the window edge and the urlbar,
          // then consider that as "not too far."  The forward button's
          // visibility may have changed since the last time the popup was
          // opened, so this needs to happen now.  Do it *before* the popup
          // opens because otherwise the items will visibly shift.
          let nodes = [...document.getElementById("nav-bar-customization-target").childNodes];
          let urlbarPosition = nodes.findIndex(n => n.id == "urlbar-container");
          let alignSiteIcons = urlbarPosition <= 2 &&
                               nodes.slice(0, urlbarPosition)
                                    .every(n => n.localName == "toolbarbutton");
          if (alignSiteIcons) {
            let identityRect =
              document.getElementById("identity-icon").getBoundingClientRect();
            this.siteIconStart = popupDirection == "rtl" ? identityRect.right
                                                         : identityRect.left;
          }
          else {
            // Reset the alignment so that the site icons are positioned
            // according to whatever's in the CSS.
            this.siteIconStart = undefined;
          }

          // 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 =
            document.getElementById("nav-bar").getBoundingClientRect().bottom -
            aInput.getBoundingClientRect().bottom;
          this.openPopup(aElement, "after_start", 0, yOffset, false, false);
        ]]></body>
      </method>

      <method name="_updateFooterVisibility">
        <body>
          <![CDATA[
          this.footer.collapsed = this._matchCount == 0;
          ]]>
        </body>
      </method>

      <method name="_showSearchSuggestionsNotification">
        <body>
          <![CDATA[
          // 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");
          this._updateFooterVisibility();

          // 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="searchSuggestionsNotificationWasDismissed">
        <parameter name="enableSuggestions"/>
        <body>
          <![CDATA[
          if (!this.popupOpen) {
            this._hideSearchSuggestionsNotification();
            return;
          }
          this._hideSearchSuggestionsNotificationWithAnimation().then(() => {
            if (enableSuggestions && this.input.textValue) {
              // Start a new search so that suggestions appear immediately.
              this.input.controller.startSearch(this.input.textValue);
            }
          });
          ]]>
        </body>
      </method>

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

      <method name="_hideSearchSuggestionsNotificationWithAnimation">
        <body>
          <![CDATA[
          return new Promise(resolve => {
            let notificationHeight = this.searchSuggestionsNotification
                                         .getBoundingClientRect()
                                         .height;
            this.searchSuggestionsNotification.style.marginTop =
              "-" + notificationHeight + "px";

            let popupHeightPx =
              (this.getBoundingClientRect().height - notificationHeight) + "px";
            this.style.height = popupHeightPx;

            let onTransitionEnd = () => {
              this.removeEventListener("transitionend", onTransitionEnd, true);
              this.searchSuggestionsNotification.style.marginTop = "0px";
              this.style.removeProperty("height");
              this._hideSearchSuggestionsNotification();
              resolve();
            };
            this.addEventListener("transitionend", onTransitionEnd, true);
          });
          ]]>
        </body>
      </method>

      <method name="_selectedOneOffChanged">
        <body><![CDATA[
          // Update all searchengine result items to use the newly selected
          // engine.
          for (let item of this.richlistbox.childNodes) {
            if (item.collapsed) {
              break;
            }
            let url = item.getAttribute("url");
            if (url) {
              let action = item._parseActionUrl(url);
              if (action && action.type == "searchengine") {
                item._adjustAcItem();
              }
            }
          }
        ]]></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;
        ]]></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.mInput._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.)
            if (this.selectedIndex == -1 && this._isFirstResultHeuristic) {
              // Don't fire DOMMenuItemActive so that screen readers still see
              // the input as being focused.
              this.richlistbox.suppressMenuItemEvent = true;
              this.input.controller.setInitiallySelectedIndex(0);
              this.richlistbox.suppressMenuItemEvent = false;
            }

            this.input.gotResultForCurrentQuery = true;

            // Check if we should perform a delayed handleEnter.
            if (this.input.handleEnterInstance) {
              let instance = this.input.handleEnterInstance;
              this.input.handleEnterInstance = null;
              // Don't handle this immediately or we could cause a recursive
              // loop where the controller sets popupOpen and re-enters here.
              setTimeout(() => {
                // Safety check: handle only if the search string didn't change.
                let { event, searchString } = instance;
                if (this.input.mController.searchString == searchString) {
                  this.input.maybeCanonizeURL(event, searchString);
                  this.input.mController.handleEnter(false, event);
                }
              }, 0);
            }
          ]]>
        </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);
          this.overrideValue = 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 XUL_NS =
            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          let iframe = document.createElementNS(XUL_NS, "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[
        this._selectedOneOffChanged();
      ]]></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();
      ]]></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 = {};
          Components.utils.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.setAttribute("mode", "undetermined");
          }
          else {
            this.progressmeter.setAttribute("mode", "determined");
            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 code is taken from nsDownloadManager.cpp
          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();
            if (Preferences.get("xpinstall.customConfirmationUI", false)) {
              this.progressmeter.setAttribute("mode", "undetermined");
              let status = gNavigatorBundle.getString("addonDownloadVerifying");
              this.progresstext.setAttribute("value", status);
              this.progresstext.setAttribute("tooltiptext", status);
            } else {
              PopupNotifications.remove(this.notification);
            }
          }
          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>

  <binding id="plugin-popupnotification-center-item">
    <content align="center">
      <xul:vbox pack="center" anonid="itemBox" class="itemBox">
        <xul:description anonid="center-item-label" class="center-item-label" />
        <xul:hbox flex="1" pack="start" align="center" anonid="center-item-warning">
          <xul:image anonid="center-item-warning-icon" class="center-item-warning-icon"/>
          <xul:label anonid="center-item-warning-label"/>
          <xul:label anonid="center-item-link" value="&checkForUpdates;" class="text-link"/>
        </xul:hbox>
      </xul:vbox>
      <xul:vbox pack="center">
        <xul:menulist class="center-item-menulist"
                      anonid="center-item-menulist">
          <xul:menupopup>
            <xul:menuitem anonid="allownow" value="allownow"
                          label="&pluginActivateNow.label;" />
            <xul:menuitem anonid="allowalways" value="allowalways"
                          label="&pluginActivateAlways.label;" />
            <xul:menuitem anonid="block" value="block"
                          label="&pluginBlockNow.label;" />
          </xul:menupopup>
        </xul:menulist>
      </xul:vbox>
    </content>
    <resources>
      <stylesheet src="chrome://global/skin/notification.css"/>
    </resources>
    <implementation>
      <constructor><![CDATA[
        document.getAnonymousElementByAttribute(this, "anonid", "center-item-label").value = this.action.pluginName;

        let curState = "block";
        if (this.action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
          if (this.action.pluginPermissionType == Ci.nsIPermissionManager.EXPIRE_SESSION) {
            curState = "allownow";
          }
          else {
            curState = "allowalways";
          }
        }
        document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").value = curState;

        let warningString = "";
        let linkString = "";

        let link = document.getAnonymousElementByAttribute(this, "anonid", "center-item-link");

        let url;
        let linkHandler;

        if (this.action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) {
          document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true;
          warningString = gNavigatorBundle.getString("pluginActivateDisabled.label");
          linkString = gNavigatorBundle.getString("pluginActivateDisabled.manage");
          linkHandler = function(event) {
            event.preventDefault();
            gPluginHandler.managePlugins();
          };
          document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-icon").hidden = true;
        }
        else {
          url = this.action.detailsLink;

          switch (this.action.blocklistState) {
          case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
            document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning").hidden = true;
            break;
          case Ci.nsIBlocklistService.STATE_BLOCKED:
            document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true;
            warningString = gNavigatorBundle.getString("pluginActivateBlocked.label");
            linkString = gNavigatorBundle.getString("pluginActivate.learnMore");
            break;
          case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
            warningString = gNavigatorBundle.getString("pluginActivateOutdated.label");
            linkString = gNavigatorBundle.getString("pluginActivate.updateLabel");
            break;
          case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
            warningString = gNavigatorBundle.getString("pluginActivateVulnerable.label");
            linkString = gNavigatorBundle.getString("pluginActivate.riskLabel");
            break;
          }
        }
        document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-label").value = warningString;

        let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
        let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);

        if (isWindowPrivate) {
          // TODO: temporary compromise of hiding some privacy leaks, remove once bug 892487 is fixed
          let allowalways = document.getAnonymousElementByAttribute(this, "anonid", "allowalways");
          let block = document.getAnonymousElementByAttribute(this, "anonid", "block");
          let allownow = document.getAnonymousElementByAttribute(this, "anonid", "allownow");

          allowalways.hidden = curState !== "allowalways";
          block.hidden       = curState !== "block";
          allownow.hidden    = curState === "allowalways";
        }

        if (url || linkHandler) {
          link.value = linkString;
          if (url) {
            link.href = url;
          }
          if (linkHandler) {
            link.addEventListener("click", linkHandler, false);
          }
        }
        else {
          link.hidden = true;
        }
      ]]></constructor>
      <property name="value">
        <getter>
          return document.getAnonymousElementByAttribute(this, "anonid",
                   "center-item-menulist").value;
        </getter>
        <setter><!-- This should be used only in automated tests -->
          document.getAnonymousElementByAttribute(this, "anonid",
                    "center-item-menulist").value = val;
        </setter>
      </property>
    </implementation>
  </binding>

  <binding id="click-to-play-plugins-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
    <content align="start" style="width: &pluginNotification.width;;">
      <xul:vbox flex="1" align="stretch" class="popup-notification-main-box"
                xbl:inherits="popupid">
        <xul:hbox class="click-to-play-plugins-notification-description-box" flex="1" align="start">
          <xul:description class="click-to-play-plugins-outer-description" flex="1">
            <html:span anonid="click-to-play-plugins-notification-description" />
            <xul:label class="text-link click-to-play-plugins-notification-link" anonid="click-to-play-plugins-notification-link" />
          </xul:description>
          <xul:toolbarbutton anonid="closebutton"
                             class="messageCloseButton popup-notification-closebutton tabbable close-icon"
                             xbl:inherits="oncommand=closebuttoncommand"
                             tooltiptext="&closeNotification.tooltip;"/>
        </xul:hbox>
        <xul:grid anonid="click-to-play-plugins-notification-center-box"
                  class="click-to-play-plugins-notification-center-box">
          <xul:columns>
            <xul:column flex="1"/>
            <xul:column/>
          </xul:columns>
          <xul:rows>
            <children includes="row"/>
            <xul:hbox pack="start" anonid="plugin-notification-showbox">
              <xul:button label="&pluginNotification.showAll.label;"
                          accesskey="&pluginNotification.showAll.accesskey;"
                          class="plugin-notification-showbutton"
                          oncommand="document.getBindingParent(this)._setState(2)"/>
            </xul:hbox>
          </xul:rows>
        </xul:grid>
        <xul:hbox anonid="button-container"
                  class="click-to-play-plugins-notification-button-container"
                  pack="center" align="center">
          <xul:button anonid="primarybutton"
                      class="click-to-play-popup-button"
                      oncommand="document.getBindingParent(this)._onButton(this)"
                      flex="1"/>
          <xul:button anonid="secondarybutton"
                      class="click-to-play-popup-button"
                      oncommand="document.getBindingParent(this)._onButton(this);"
                      flex="1"/>
        </xul:hbox>
        <xul:box hidden="true">
          <children/>
        </xul:box>
      </xul:vbox>
    </content>
    <resources>
      <stylesheet src="chrome://global/skin/notification.css"/>
    </resources>
    <implementation>
      <field name="_states">
        ({SINGLE: 0, MULTI_COLLAPSED: 1, MULTI_EXPANDED: 2})
      </field>
      <field name="_primaryButton">
        document.getAnonymousElementByAttribute(this, "anonid", "primarybutton");
      </field>
      <field name="_secondaryButton">
        document.getAnonymousElementByAttribute(this, "anonid", "secondarybutton")
      </field>
      <field name="_buttonContainer">
        document.getAnonymousElementByAttribute(this, "anonid", "button-container")
      </field>
      <field name="_brandShortName">
        document.getElementById("bundle_brand").getString("brandShortName")
      </field>
      <field name="_items">[]</field>
      <constructor><![CDATA[
        const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
        let sortedActions = [];
        for (let action of this.notification.options.pluginData.values()) {
          sortedActions.push(action);
        }
        sortedActions.sort((a, b) => a.pluginName.localeCompare(b.pluginName));

        for (let action of sortedActions) {
          let item = document.createElementNS(XUL_NS, "row");
          item.setAttribute("class", "plugin-popupnotification-centeritem");
          item.action = action;
          this.appendChild(item);
          this._items.push(item);
        }
        switch (this._items.length) {
          case 0:
            PopupNotifications._dismiss();
            break;
          case 1:
            this._setState(this._states.SINGLE);
            break;
          default:
            if (this.notification.options.primaryPlugin) {
              this._setState(this._states.MULTI_COLLAPSED);
            } else {
              this._setState(this._states.MULTI_EXPANDED);
            }
        }
      ]]></constructor>
      <method name="_setState">
        <parameter name="state" />
        <body><![CDATA[
          var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box");

          if (this._states.SINGLE == state) {
            grid.hidden = true;
            this._setupSingleState();
            return;
          }

          let prePath = this.notification.options.principal.URI.prePath;
          this._setupDescription("pluginActivateMultiple.message", null, prePath);

          var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox");

          var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties");
          this._primaryButton.label = dialogStrings.GetStringFromName("button-accept");
          this._primaryButton.setAttribute("default", "true");

          this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel");
          this._primaryButton.setAttribute("action", "_multiAccept");
          this._secondaryButton.setAttribute("action", "_cancel");

          grid.hidden = false;

          if (this._states.MULTI_COLLAPSED == state) {
            for (let child of this.childNodes) {
              if (child.tagName != "row") {
                continue;
              }
              child.hidden = this.notification.options.primaryPlugin !=
                             child.action.permissionString;
            }
            showBox.hidden = false;
          }
          else {
            for (let child of this.childNodes) {
              if (child.tagName != "row") {
                continue;
              }
              child.hidden = false;
            }
            showBox.hidden = true;
          }
          this._setupLink(null);
        ]]></body>
      </method>
      <method name="_setupSingleState">
        <body><![CDATA[
          var action = this._items[0].action;
          var prePath = action.pluginPermissionPrePath;
          let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
          let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);

          let label, linkLabel, button1, button2;

          if (action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
            button1 = {
              label: "pluginBlockNow.label",
              accesskey: "pluginBlockNow.accesskey",
              action: "_singleBlock"
            };
            button2 = {
              label: "pluginContinue.label",
              accesskey: "pluginContinue.accesskey",
              action: "_singleContinue",
              default: true
            };
            switch (action.blocklistState) {
            case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
              label = "pluginEnabled.message";
              linkLabel = "pluginActivate.learnMore";
              break;

            case Ci.nsIBlocklistService.STATE_BLOCKED:
              Cu.reportError(Error("Cannot happen!"));
              break;

            case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
              label = "pluginEnabledOutdated.message";
              linkLabel = "pluginActivate.updateLabel";
              break;

            case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
              label = "pluginEnabledVulnerable.message";
              linkLabel = "pluginActivate.riskLabel"
              break;

            default:
              Cu.reportError(Error("Unexpected blocklist state"));
            }

            // TODO: temporary compromise, remove this once bug 892487 is fixed
            if (isWindowPrivate) {
              this._buttonContainer.hidden = true;
            }
          }
          else if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) {
            let linkElement =
              document.getAnonymousElementByAttribute(
                         this, "anonid", "click-to-play-plugins-notification-link");
            linkElement.textContent = gNavigatorBundle.getString("pluginActivateDisabled.manage");
            linkElement.setAttribute("onclick", "gPluginHandler.managePlugins()");

            let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
            descElement.textContent = gNavigatorBundle.getFormattedString(
              "pluginActivateDisabled.message", [action.pluginName, this._brandShortName]) + " ";
            this._buttonContainer.hidden = true;
            return;
          }
          else if (action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
            let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
            descElement.textContent = gNavigatorBundle.getFormattedString(
              "pluginActivateBlocked.message", [action.pluginName, this._brandShortName]) + " ";
            this._setupLink("pluginActivate.learnMore", action.detailsLink);
            this._buttonContainer.hidden = true;
            return;
          }
          else {
            button1 = {
              label: "pluginActivateNow.label",
              accesskey: "pluginActivateNow.accesskey",
              action: "_singleActivateNow"
            };
            button2 = {
              label: "pluginActivateAlways.label",
              accesskey: "pluginActivateAlways.accesskey",
              action: "_singleActivateAlways"
            };
            switch (action.blocklistState) {
            case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
              label = "pluginActivateNew.message";
              linkLabel = "pluginActivate.learnMore";
              button2.default = true;
              break;

            case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
              label = "pluginActivateOutdated.message";
              linkLabel = "pluginActivate.updateLabel";
              button1.default = true;
              break;

            case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
              label = "pluginActivateVulnerable.message";
              linkLabel = "pluginActivate.riskLabel"
              button1.default = true;
              break;

            default:
              Cu.reportError(Error("Unexpected blocklist state"));
            }

            // TODO: temporary compromise, remove this once bug 892487 is fixed
            if (isWindowPrivate) {
              button1.default = true;
              this._secondaryButton.hidden = true;
            }
          }
          this._setupDescription(label, action.pluginName, prePath);
          this._setupLink(linkLabel, action.detailsLink);

          this._primaryButton.label = gNavigatorBundle.getString(button1.label);
          this._primaryButton.accessKey = gNavigatorBundle.getString(button1.accesskey);
          this._primaryButton.setAttribute("action", button1.action);

          this._secondaryButton.label = gNavigatorBundle.getString(button2.label);
          this._secondaryButton.accessKey = gNavigatorBundle.getString(button2.accesskey);
          this._secondaryButton.setAttribute("action", button2.action);
          if (button1.default) {
            this._primaryButton.setAttribute("default", "true");
          }
          else if (button2.default) {
            this._secondaryButton.setAttribute("default", "true");
          }
        ]]></body>
      </method>
      <method name="_setupDescription">
        <parameter name="baseString" />
        <parameter name="pluginName" /> <!-- null for the multiple-plugin case -->
        <parameter name="prePath" />
        <body><![CDATA[
          var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description");
          while (span.lastChild) {
            span.removeChild(span.lastChild);
          }

          var args = ["__prepath__", this._brandShortName];
          if (pluginName) {
            args.unshift(pluginName);
          }
          var bases = gNavigatorBundle.getFormattedString(baseString, args).
            split("__prepath__", 2);

          span.appendChild(document.createTextNode(bases[0]));
          var prePathSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "em");
          prePathSpan.appendChild(document.createTextNode(prePath));
          span.appendChild(prePathSpan);
          span.appendChild(document.createTextNode(bases[1] + " "));
        ]]></body>
      </method>
      <method name="_setupLink">
        <parameter name="linkString"/>
        <parameter name="linkUrl" />
        <body><![CDATA[
          var link = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-link");
          if (!linkString || !linkUrl) {
            link.hidden = true;
            return;
          }

          link.hidden = false;
          link.textContent = gNavigatorBundle.getString(linkString);
          link.href = linkUrl;
        ]]></body>
      </method>
      <method name="_onButton">
        <parameter name="aButton" />
        <body><![CDATA[
          let methodName = aButton.getAttribute("action");
          this[methodName]();
        ]]></body>
      </method>
      <method name="_singleActivateNow">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "allownow");
          this._cancel();
        ]]></body>
      </method>
      <method name="_singleBlock">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "block");
            this._cancel();
        ]]></body>
      </method>
      <method name="_singleActivateAlways">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "allowalways");
          this._cancel();
        ]]></body>
      </method>
      <method name="_singleContinue">
        <body><![CDATA[
          gPluginHandler._updatePluginPermission(this.notification,
            this._items[0].action,
            "continue");
          this._cancel();
        ]]></body>
      </method>
      <method name="_multiAccept">
        <body><![CDATA[
          for (let item of this._items) {
            let action = item.action;
            if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED ||
                action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
              continue;
            }
            gPluginHandler._updatePluginPermission(this.notification,
              item.action, item.value);
          }
          this._cancel();
        ]]></body>
      </method>
      <method name="_cancel">
        <body><![CDATA[
          PopupNotifications._dismiss();
        ]]></body>
      </method>
      <method name="_accept">
        <parameter name="aEvent" />
        <body><![CDATA[
          if (aEvent.defaultPrevented)
            return;
          aEvent.preventDefault();
          if (this._primaryButton.getAttribute("default") == "true") {
            this._primaryButton.click();
          }
          else if (this._secondaryButton.getAttribute("default") == "true") {
            this._secondaryButton.click();
          }
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <!-- The _accept method checks for .defaultPrevented so that if focus is in a button,
           enter activates the button and not this default action -->
      <handler event="keypress" keycode="VK_RETURN" group="system" action="this._accept(event);"/>
    </handlers>
  </binding>

  <!-- This binding is only retained for add-ons compatibility -->
  <binding id="menuitem-iconic-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem-iconic">
    <implementation>
      <constructor><![CDATA[
        this.setAttribute("tooltiptext", this.getAttribute("acceltext"));
        // TODO: Simplify this to this.setAttribute("acceltext", "") once bug
        // 592424 is fixed
        document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", "");
      ]]></constructor>
    </implementation>
  </binding>
</bindings>