tor-browser

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

UrlbarUtils.sys.mjs (118311B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This module exports the UrlbarUtils singleton, which contains constants and
      7 * helper functions that are useful to all components of the urlbar.
      8 */
      9 
     10 /**
     11 * @import {Query} from "UrlbarProvidersManager.sys.mjs"
     12 * @import {UrlbarSearchStringTokenData} from "UrlbarTokenizer.sys.mjs"
     13 */
     14 
     15 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     16 
     17 const lazy = {};
     18 
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  ContextualIdentityService:
     21    "resource://gre/modules/ContextualIdentityService.sys.mjs",
     22  DEFAULT_FORM_HISTORY_PARAM:
     23    "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs",
     24  FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
     25  KeywordUtils: "resource://gre/modules/KeywordUtils.sys.mjs",
     26  PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
     27  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     28  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     29  SearchSuggestionController:
     30    "moz-src:///toolkit/components/search/SearchSuggestionController.sys.mjs",
     31  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     32  UrlbarProviderInterventions:
     33    "moz-src:///browser/components/urlbar/UrlbarProviderInterventions.sys.mjs",
     34  UrlbarProviderOpenTabs:
     35    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     36  UrlbarProviderSearchTips:
     37    "moz-src:///browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs",
     38  UrlbarSearchUtils:
     39    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     40  UrlbarTokenizer:
     41    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     42  BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
     43  UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
     44 });
     45 
     46 XPCOMUtils.defineLazyServiceGetter(
     47  lazy,
     48  "parserUtils",
     49  "@mozilla.org/parserutils;1",
     50  Ci.nsIParserUtils
     51 );
     52 
     53 export var UrlbarUtils = {
     54  // Results are categorized into groups to help the muxer compose them.  See
     55  // UrlbarUtils.getResultGroup.  Since result groups are stored in result
     56  // groups and result groups are stored in prefs, additions and changes to
     57  // result groups may require adding UI migrations to BrowserGlue.  Be careful
     58  // about making trivial changes to existing groups, like renaming them,
     59  // because we don't want to make downgrades unnecessarily hard.
     60  RESULT_GROUP: Object.freeze({
     61    ABOUT_PAGES: "aboutPages",
     62    GENERAL: "general",
     63    GENERAL_PARENT: "generalParent",
     64    FORM_HISTORY: "formHistory",
     65    HEURISTIC_AUTOFILL: "heuristicAutofill",
     66    HEURISTIC_ENGINE_ALIAS: "heuristicEngineAlias",
     67    HEURISTIC_EXTENSION: "heuristicExtension",
     68    HEURISTIC_FALLBACK: "heuristicFallback",
     69    HEURISTIC_BOOKMARK_KEYWORD: "heuristicBookmarkKeyword",
     70    HEURISTIC_HISTORY_URL: "heuristicHistoryUrl",
     71    HEURISTIC_OMNIBOX: "heuristicOmnibox",
     72    HEURISTIC_RESTRICT_KEYWORD_AUTOFILL: "heuristicRestrictKeywordAutofill",
     73    HEURISTIC_SEARCH_TIP: "heuristicSearchTip",
     74    HEURISTIC_TEST: "heuristicTest",
     75    HEURISTIC_TOKEN_ALIAS_ENGINE: "heuristicTokenAliasEngine",
     76    INPUT_HISTORY: "inputHistory",
     77    OMNIBOX: "extension",
     78    RECENT_SEARCH: "recentSearch",
     79    REMOTE_SUGGESTION: "remoteSuggestion",
     80    REMOTE_TAB: "remoteTab",
     81    RESTRICT_SEARCH_KEYWORD: "restrictSearchKeyword",
     82    SUGGESTED_INDEX: "suggestedIndex",
     83    TAIL_SUGGESTION: "tailSuggestion",
     84  }),
     85 
     86  // Defines provider types.
     87  PROVIDER_TYPE: Object.freeze({
     88    // Should be executed immediately, because it returns heuristic results
     89    // that must be handed to the user asap.
     90    // WARNING: these providers must be extremely fast, because the urlbar will
     91    // await for them before returning results to the user. In particular it is
     92    // critical to reply quickly to isActive and startQuery.
     93    HEURISTIC: 1,
     94    // Can be delayed, contains results coming from the session or the profile.
     95    PROFILE: 2,
     96    // Can be delayed, contains results coming from the network.
     97    NETWORK: 3,
     98    // Can be delayed, contains results coming from unknown sources.
     99    EXTENSION: 4,
    100  }),
    101 
    102  // Defines UrlbarResult types.
    103  RESULT_TYPE: Object.freeze({
    104    // An open tab.
    105    TAB_SWITCH: 1,
    106    // A search suggestion or engine.
    107    SEARCH: 2,
    108    // A common url/title tuple, may be a bookmark with tags.
    109    URL: 3,
    110    // A bookmark keyword.
    111    KEYWORD: 4,
    112    // A WebExtension Omnibox result.
    113    OMNIBOX: 5,
    114    // A tab from another synced device.
    115    REMOTE_TAB: 6,
    116    // An actionable message to help the user with their query.
    117    TIP: 7,
    118    // A type of result which layout is defined at runtime.
    119    DYNAMIC: 8,
    120    // A restrict keyword result, could be @bookmarks, @history, or @tabs.
    121    RESTRICT: 9,
    122 
    123    // When you add a new type, also add its schema to
    124    // UrlbarUtils.RESULT_PAYLOAD_SCHEMA below.  Also consider checking if
    125    // consumers of "urlbar-user-start-navigation" need updating.
    126  }),
    127 
    128  // This defines the source of results returned by a provider. Each provider
    129  // can return results from more than one source. This is used by the
    130  // ProvidersManager to decide which providers must be queried and which
    131  // results can be returned.
    132  // If you add new source types, consider checking if consumers of
    133  // "urlbar-user-start-navigation" need update as well.
    134  RESULT_SOURCE: Object.freeze({
    135    BOOKMARKS: 1,
    136    HISTORY: 2,
    137    SEARCH: 3,
    138    TABS: 4,
    139    OTHER_LOCAL: 5,
    140    OTHER_NETWORK: 6,
    141    ADDON: 7,
    142    ACTIONS: 8,
    143  }),
    144 
    145  // Per-result exposure telemetry.
    146  EXPOSURE_TELEMETRY: {
    147    // Exposure telemetry will not be recorded for the result.
    148    NONE: 0,
    149    // Exposure telemetry will be recorded for the result and the result will be
    150    // visible in the view as usual.
    151    SHOWN: 1,
    152    // Exposure telemetry will be recorded for the result but the result will
    153    // not be present in the view.
    154    HIDDEN: 2,
    155  },
    156 
    157  // This defines icon locations that are commonly used in the UI.
    158  ICON: {
    159    // DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
    160    EXTENSION: "chrome://mozapps/skin/extensions/extension.svg",
    161    HISTORY: "chrome://browser/skin/history.svg",
    162    SEARCH_GLASS: "chrome://global/skin/icons/search-glass.svg",
    163    TRENDING: "chrome://global/skin/icons/trending.svg",
    164    TIP: "chrome://global/skin/icons/lightbulb.svg",
    165    GLOBE: "chrome://global/skin/icons/defaultFavicon.svg",
    166  },
    167 
    168  // The number of results by which Page Up/Down move the selection.
    169  PAGE_UP_DOWN_DELTA: 5,
    170 
    171  // IME composition states.
    172  COMPOSITION: {
    173    NONE: 1,
    174    COMPOSING: 2,
    175    COMMIT: 3,
    176    CANCELED: 4,
    177  },
    178 
    179  // Limit the length of titles and URLs we display so layout doesn't spend too
    180  // much time building text runs.
    181  MAX_TEXT_LENGTH: 255,
    182 
    183  // Whether a result should be highlighted up to the point the user has typed
    184  // or after that point.
    185  HIGHLIGHT: Object.freeze({
    186    TYPED: 1,
    187    SUGGESTED: 2,
    188    ALL: 3,
    189  }),
    190 
    191  // UrlbarProviderPlaces's autocomplete results store their titles and tags
    192  // together in their comments.  This separator is used to separate them.
    193  // After bug 1717511, we should stop using this old hack and store titles and
    194  // tags separately.  It's important that this be a character that no title
    195  // would ever have.  We use \x1F, the non-printable unit separator.
    196  TITLE_TAGS_SEPARATOR: "\x1F",
    197 
    198  // Regex matching single word hosts with an optional port; no spaces, auth or
    199  // path-like chars are admitted.
    200  REGEXP_SINGLE_WORD: /^[^\s@:/?#]+(:\d+)?$/,
    201 
    202  // Valid entry points for search mode. If adding a value here, please update
    203  // telemetry documentation and Scalars.yaml.
    204  SEARCH_MODE_ENTRY: new Set([
    205    "bookmarkmenu",
    206    "handoff",
    207    "keywordoffer",
    208    "oneoff",
    209    "historymenu",
    210    "other",
    211    "searchbutton",
    212    "shortcut",
    213    "tabmenu",
    214    "tabtosearch",
    215    "tabtosearch_onboard",
    216    "topsites_newtab",
    217    "topsites_urlbar",
    218    "touchbar",
    219    "typed",
    220  ]),
    221 
    222  // The favicon service stores icons for URLs with the following protocols.
    223  PROTOCOLS_WITH_ICONS: ["about:", "http:", "https:", "file:"],
    224 
    225  // Valid URI schemes that are considered safe but don't contain
    226  // an authority component (e.g host:port). There are many URI schemes
    227  // that do not contain an authority, but these in particular have
    228  // some likelihood of being entered or bookmarked by a user.
    229  // `file:` is an exceptional case because an authority is optional
    230  PROTOCOLS_WITHOUT_AUTHORITY: [
    231    "about:",
    232    "data:",
    233    "file:",
    234    "javascript:",
    235    "view-source:",
    236  ],
    237 
    238  // Search mode objects corresponding to the local shortcuts in the view, in
    239  // order they appear.  Pref names are relative to the `browser.urlbar` branch.
    240  get LOCAL_SEARCH_MODES() {
    241    return [
    242      {
    243        source: this.RESULT_SOURCE.BOOKMARKS,
    244        restrict: lazy.UrlbarTokenizer.RESTRICT.BOOKMARK,
    245        icon: "chrome://browser/skin/bookmark.svg",
    246        pref: "shortcuts.bookmarks",
    247        telemetryLabel: "bookmarks",
    248        uiLabel: "urlbar-searchmode-bookmarks",
    249      },
    250      {
    251        source: this.RESULT_SOURCE.TABS,
    252        restrict: lazy.UrlbarTokenizer.RESTRICT.OPENPAGE,
    253        icon: "chrome://browser/skin/tabs.svg",
    254        pref: "shortcuts.tabs",
    255        telemetryLabel: "tabs",
    256        uiLabel: "urlbar-searchmode-tabs",
    257      },
    258      {
    259        source: this.RESULT_SOURCE.HISTORY,
    260        restrict: lazy.UrlbarTokenizer.RESTRICT.HISTORY,
    261        icon: "chrome://browser/skin/history.svg",
    262        pref: "shortcuts.history",
    263        telemetryLabel: "history",
    264        uiLabel: "urlbar-searchmode-history",
    265      },
    266      {
    267        source: this.RESULT_SOURCE.ACTIONS,
    268        restrict: lazy.UrlbarTokenizer.RESTRICT.ACTION,
    269        icon: "chrome://browser/skin/quickactions.svg",
    270        pref: "shortcuts.actions",
    271        telemetryLabel: "actions",
    272        uiLabel: "urlbar-searchmode-actions",
    273      },
    274    ];
    275  },
    276 
    277  /**
    278   * Returns the payload schema for the given type of result.
    279   *
    280   * @param {Values<typeof this.RESULT_TYPE>} type
    281   * @returns {object} The schema for the given type.
    282   */
    283  getPayloadSchema(type) {
    284    return this.RESULT_PAYLOAD_SCHEMA[type];
    285  },
    286 
    287  /**
    288   * Adds a url to history as long as it isn't in a private browsing window,
    289   * and it is valid.
    290   *
    291   * @param {string} url The url to add to history.
    292   * @param {nsIDOMWindow} window The window from where the url is being added.
    293   */
    294  addToUrlbarHistory(url, window) {
    295    if (
    296      !lazy.PrivateBrowsingUtils.isWindowPrivate(window) &&
    297      url &&
    298      !url.includes(" ") &&
    299      // eslint-disable-next-line no-control-regex
    300      !/[\x00-\x1F]/.test(url)
    301    ) {
    302      lazy.PlacesUIUtils.markPageAsTyped(url);
    303    }
    304  },
    305 
    306  /**
    307   * Given a string, will generate a more appropriate urlbar value if a Places
    308   * keyword or a search alias is found at the beginning of it.
    309   *
    310   * @param {string} url
    311   *        A string that may begin with a keyword or an alias.
    312   *
    313   * @returns {Promise<{ url, postData, mayInheritPrincipal }>}
    314   *        If it's not possible to discern a keyword or an alias, url will be
    315   *        the input string.
    316   */
    317  async getShortcutOrURIAndPostData(url) {
    318    let mayInheritPrincipal = false;
    319    let postData = null;
    320    // Split on the first whitespace.
    321    let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);
    322 
    323    if (!keyword) {
    324      return { url, postData, mayInheritPrincipal };
    325    }
    326 
    327    /** @type {nsISearchEngine} */
    328    let engine = await Services.search.getEngineByAlias(keyword);
    329    if (engine) {
    330      let submission = engine.getSubmission(param, null);
    331      return {
    332        url: submission.uri.spec,
    333        postData: submission.postData,
    334        mayInheritPrincipal,
    335      };
    336    }
    337 
    338    // A corrupt Places database could make this throw, breaking navigation
    339    // from the location bar.
    340    let entry = null;
    341    try {
    342      entry = await lazy.PlacesUtils.keywords.fetch(keyword);
    343    } catch (ex) {
    344      console.error(`Unable to fetch Places keyword "${keyword}":`, ex);
    345    }
    346    if (!entry || !entry.url) {
    347      // This is not a Places keyword.
    348      return { url, postData, mayInheritPrincipal };
    349    }
    350 
    351    try {
    352      [url, postData] = await lazy.KeywordUtils.parseUrlAndPostData(
    353        entry.url.href,
    354        entry.postData,
    355        param
    356      );
    357      if (postData) {
    358        postData = this.getPostDataStream(postData);
    359      }
    360 
    361      // Since this URL came from a bookmark, it's safe to let it inherit the
    362      // current document's principal.
    363      mayInheritPrincipal = true;
    364    } catch (ex) {
    365      // It was not possible to bind the param, just use the original url value.
    366    }
    367 
    368    return { url, postData, mayInheritPrincipal };
    369  },
    370 
    371  /**
    372   * Returns an input stream wrapper for the given post data.
    373   *
    374   * @param {string} postDataString The string to wrap.
    375   * @param {string} [type] The encoding type.
    376   * @returns {nsIInputStream} An input stream of the wrapped post data.
    377   */
    378  getPostDataStream(
    379    postDataString,
    380    type = "application/x-www-form-urlencoded"
    381  ) {
    382    let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
    383      Ci.nsIStringInputStream
    384    );
    385    dataStream.setByteStringData(postDataString);
    386 
    387    let mimeStream = Cc[
    388      "@mozilla.org/network/mime-input-stream;1"
    389    ].createInstance(Ci.nsIMIMEInputStream);
    390    mimeStream.addHeader("Content-Type", type);
    391    mimeStream.setData(dataStream);
    392    return mimeStream.QueryInterface(Ci.nsIInputStream);
    393  },
    394 
    395  _compareIgnoringDiacritics: null,
    396 
    397  /**
    398   * Returns a list of all the token substring matches in a string.  Matching is
    399   * case insensitive.  Each match in the returned list is a tuple: [matchIndex,
    400   * matchLength].  matchIndex is the index in the string of the match, and
    401   * matchLength is the length of the match.
    402   *
    403   * @param {Array} tokens The tokens to search for.
    404   * @param {string} str The string to match against.
    405   * @param {Values<typeof this.HIGHLIGHT>} highlightType
    406   *   One of the HIGHLIGHT values:
    407   *     TYPED: match ranges matching the tokens; or
    408   *     SUGGESTED: match ranges for words not matching the tokens and the
    409   *                endings of words that start with a token.
    410   *     ALL: match all ranges of str.
    411   * @returns {Array} An array: [
    412   *            [matchIndex_0, matchLength_0],
    413   *            [matchIndex_1, matchLength_1],
    414   *            ...
    415   *            [matchIndex_n, matchLength_n]
    416   *          ].
    417   *          The array is sorted by match indexes ascending.
    418   */
    419  getTokenMatches(tokens, str, highlightType) {
    420    if (highlightType == this.HIGHLIGHT.ALL) {
    421      return [[0, str.length]];
    422    }
    423 
    424    if (!tokens?.length) {
    425      return [];
    426    }
    427 
    428    // Only search a portion of the string, because not more than a certain
    429    // amount of characters are visible in the UI, matching over what is visible
    430    // would be expensive and pointless.
    431    str = str.substring(0, this.MAX_TEXT_LENGTH).toLocaleLowerCase();
    432    // To generate non-overlapping ranges, we start from a 0-filled array with
    433    // the same length of the string, and use it as a collision marker, setting
    434    // 1 where the text should be highlighted.
    435    let hits = new Array(str.length).fill(
    436      highlightType == this.HIGHLIGHT.SUGGESTED ? 1 : 0
    437    );
    438    let compareIgnoringDiacritics;
    439    for (let i = 0, totalTokensLength = 0; i < tokens.length; i++) {
    440      const { lowerCaseValue: needle } = tokens[i];
    441 
    442      // Ideally we should never hit the empty token case, but just in case
    443      // the `needle` check protects us from an infinite loop.
    444      if (!needle) {
    445        continue;
    446      }
    447      let index = 0;
    448      let found = false;
    449      // First try a diacritic-sensitive search.
    450      for (;;) {
    451        index = str.indexOf(needle, index);
    452        if (index < 0) {
    453          break;
    454        }
    455 
    456        if (highlightType == this.HIGHLIGHT.SUGGESTED) {
    457          // We de-emphasize the match only if it's preceded by a space, thus
    458          // it's a perfect match or the beginning of a longer word.
    459          let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
    460          if (index != previousSpaceIndex) {
    461            index += needle.length;
    462            // We found the token but we won't de-emphasize it, because it's not
    463            // after a word boundary.
    464            found = true;
    465            continue;
    466          }
    467        }
    468 
    469        hits.fill(
    470          highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
    471          index,
    472          index + needle.length
    473        );
    474        index += needle.length;
    475        found = true;
    476      }
    477      // If that fails to match anything, try a (computationally intensive)
    478      // diacritic-insensitive search.
    479      if (!found) {
    480        if (!compareIgnoringDiacritics) {
    481          if (!this._compareIgnoringDiacritics) {
    482            // Diacritic insensitivity in the search engine follows a set of
    483            // general rules that are not locale-dependent, so use a generic
    484            // English collator for highlighting matching words instead of a
    485            // collator for the user's particular locale.
    486            this._compareIgnoringDiacritics = new Intl.Collator("en", {
    487              sensitivity: "base",
    488            }).compare;
    489          }
    490          compareIgnoringDiacritics = this._compareIgnoringDiacritics;
    491        }
    492        index = 0;
    493        while (index < str.length) {
    494          let hay = str.substr(index, needle.length);
    495          if (compareIgnoringDiacritics(needle, hay) === 0) {
    496            if (highlightType == this.HIGHLIGHT.SUGGESTED) {
    497              let previousSpaceIndex = str.lastIndexOf(" ", index) + 1;
    498              if (index != previousSpaceIndex) {
    499                index += needle.length;
    500                continue;
    501              }
    502            }
    503            hits.fill(
    504              highlightType == this.HIGHLIGHT.SUGGESTED ? 0 : 1,
    505              index,
    506              index + needle.length
    507            );
    508            index += needle.length;
    509          } else {
    510            index++;
    511          }
    512        }
    513      }
    514 
    515      totalTokensLength += needle.length;
    516      if (totalTokensLength > this.MAX_TEXT_LENGTH) {
    517        // Limit the number of tokens to reduce calculate time.
    518        break;
    519      }
    520    }
    521    // Starting from the collision array, generate [start, len] tuples
    522    // representing the ranges to be highlighted.
    523    let ranges = [];
    524    for (let index = hits.indexOf(1); index >= 0 && index < hits.length; ) {
    525      let len = 0;
    526      // eslint-disable-next-line no-empty
    527      for (let j = index; j < hits.length && hits[j]; ++j, ++len) {}
    528      ranges.push([index, len]);
    529      // Move to the next 1.
    530      index = hits.indexOf(1, index + len);
    531    }
    532    return ranges;
    533  },
    534 
    535  /**
    536   * Returns the group for a result.
    537   *
    538   * @param {UrlbarResult} result
    539   *   The result.
    540   * @returns {Values<typeof this.RESULT_GROUP>}
    541   *   The result's group.
    542   */
    543  getResultGroup(result) {
    544    // Used for test_suggestedIndexRelativeToGroup.js to make it simpler
    545    if (result.group) {
    546      return result.group;
    547    }
    548 
    549    if (result.hasSuggestedIndex && !result.isSuggestedIndexRelativeToGroup) {
    550      return this.RESULT_GROUP.SUGGESTED_INDEX;
    551    }
    552    if (result.heuristic) {
    553      switch (result.providerName) {
    554        case "UrlbarProviderAliasEngines":
    555          return this.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS;
    556        case "UrlbarProviderAutofill":
    557          return this.RESULT_GROUP.HEURISTIC_AUTOFILL;
    558        case "UrlbarProviderBookmarkKeywords":
    559          return this.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD;
    560        case "UrlbarProviderHeuristicFallback":
    561          return this.RESULT_GROUP.HEURISTIC_FALLBACK;
    562        case "UrlbarProviderHistoryUrlHeuristic":
    563          return this.RESULT_GROUP.HEURISTIC_HISTORY_URL;
    564        case "UrlbarProviderOmnibox":
    565          return this.RESULT_GROUP.HEURISTIC_OMNIBOX;
    566        case "UrlbarProviderRestrictKeywordsAutofill":
    567          return this.RESULT_GROUP.HEURISTIC_RESTRICT_KEYWORD_AUTOFILL;
    568        case "UrlbarProviderTokenAliasEngines":
    569          return this.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE;
    570        case "UrlbarProviderSearchTips":
    571          return this.RESULT_GROUP.HEURISTIC_SEARCH_TIP;
    572        default:
    573          if (result.providerName.startsWith("TestProvider")) {
    574            return this.RESULT_GROUP.HEURISTIC_TEST;
    575          }
    576          break;
    577      }
    578      if (result.providerType == this.PROVIDER_TYPE.EXTENSION) {
    579        return this.RESULT_GROUP.HEURISTIC_EXTENSION;
    580      }
    581      console.error(
    582        "Returning HEURISTIC_FALLBACK for unrecognized heuristic result: ",
    583        result
    584      );
    585      return this.RESULT_GROUP.HEURISTIC_FALLBACK;
    586    }
    587 
    588    switch (result.providerName) {
    589      case "UrlbarProviderAboutPages":
    590        return this.RESULT_GROUP.ABOUT_PAGES;
    591      case "UrlbarProviderInputHistory":
    592        return this.RESULT_GROUP.INPUT_HISTORY;
    593      case "UrlbarProviderQuickSuggest":
    594        return this.RESULT_GROUP.GENERAL_PARENT;
    595      default:
    596        break;
    597    }
    598 
    599    switch (result.type) {
    600      case this.RESULT_TYPE.SEARCH:
    601        if (result.source == this.RESULT_SOURCE.HISTORY) {
    602          return result.providerName == "UrlbarProviderRecentSearches"
    603            ? this.RESULT_GROUP.RECENT_SEARCH
    604            : this.RESULT_GROUP.FORM_HISTORY;
    605        }
    606        if (result.payload.tail && !result.isRichSuggestion) {
    607          return this.RESULT_GROUP.TAIL_SUGGESTION;
    608        }
    609        if (result.payload.suggestion) {
    610          return this.RESULT_GROUP.REMOTE_SUGGESTION;
    611        }
    612        break;
    613      case this.RESULT_TYPE.OMNIBOX:
    614        return this.RESULT_GROUP.OMNIBOX;
    615      case this.RESULT_TYPE.REMOTE_TAB:
    616        return this.RESULT_GROUP.REMOTE_TAB;
    617      case this.RESULT_TYPE.RESTRICT:
    618        return this.RESULT_GROUP.RESTRICT_SEARCH_KEYWORD;
    619    }
    620    return this.RESULT_GROUP.GENERAL;
    621  },
    622 
    623  /**
    624   * Extracts the URL from a result.
    625   *
    626   * @param {UrlbarResult} result
    627   *   The result to extract from.
    628   * @param {object} options
    629   *   Options object.
    630   * @param {HTMLElement} [options.element]
    631   *   The element associated with the result that was selected or picked, if
    632   *   available. For results that have multiple selectable children, the URL
    633   *   may be taken from a child element rather than the result.
    634   * @returns {object}
    635   *   An object: `{ url, postData }`
    636   *   `url` will be null if the result doesn't have a URL. `postData` will be
    637   *   null if the result doesn't have post data.
    638   */
    639  getUrlFromResult(result, { element = null } = {}) {
    640    if (
    641      result.payload.engine &&
    642      (result.type == this.RESULT_TYPE.SEARCH ||
    643        result.type == this.RESULT_TYPE.DYNAMIC)
    644    ) {
    645      let query =
    646        element?.dataset.query ||
    647        result.payload.suggestion ||
    648        result.payload.query;
    649      if (query) {
    650        const engine = Services.search.getEngineByName(result.payload.engine);
    651        let [url, postData] = this.getSearchQueryUrl(engine, query);
    652        return { url, postData };
    653      }
    654    }
    655 
    656    return {
    657      url: result.payload.url ?? null,
    658      postData: result.payload.postData
    659        ? this.getPostDataStream(result.payload.postData)
    660        : null,
    661    };
    662  },
    663 
    664  /**
    665   * Get the url to load for the search query.
    666   *
    667   * @param {nsISearchEngine} engine
    668   *   The engine to generate the query for.
    669   * @param {string} query
    670   *   The query string to search for.
    671   * @returns {Array}
    672   *   Returns an array containing the query url (string) and the
    673   *    post data (object).
    674   */
    675  getSearchQueryUrl(engine, query) {
    676    let submission = engine.getSubmission(query);
    677    return [submission.uri.spec, submission.postData];
    678  },
    679 
    680  /**
    681   * Ranks a URL prefix from 3 - 0 with the following preferences:
    682   * https:// > https://www. > http:// > http://www.
    683   * Higher is better for the purposes of deduping URLs.
    684   * Returns -1 if the prefix does not match any of the above.
    685   *
    686   * @param {string} prefix
    687   */
    688  getPrefixRank(prefix) {
    689    return ["http://www.", "http://", "https://www.", "https://"].indexOf(
    690      prefix
    691    );
    692  },
    693 
    694  /**
    695   * Gets the number of rows a result should span in the view.
    696   *
    697   * @param {UrlbarResult} result
    698   *   The result.
    699   * @param {object} [options]
    700   * @param {boolean} [options.includeHiddenExposures]
    701   *   Whether a span should be returned if the result is a hidden exposure. If
    702   *   false and `result.isHiddenExposure` is true, zero will be returned since
    703   *   the result should be hidden and not take up any rows at all. Otherwise
    704   *   the result's true span is returned.
    705   * @returns {number}
    706   *   The number of rows the result should span in the view.
    707   */
    708  getSpanForResult(result, { includeHiddenExposures = false } = {}) {
    709    if (!includeHiddenExposures && result.isHiddenExposure) {
    710      return 0;
    711    }
    712 
    713    if (result.resultSpan) {
    714      return result.resultSpan;
    715    }
    716 
    717    switch (result.type) {
    718      case this.RESULT_TYPE.TIP:
    719        return 3;
    720    }
    721    return 1;
    722  },
    723 
    724  /**
    725   * Gets a default icon for a URL.
    726   *
    727   * @param {string|URL} url
    728   *   The URL to get the icon for.
    729   * @returns {string} A URI pointing to an icon for `url`.
    730   */
    731  getIconForUrl(url) {
    732    if (typeof url == "string") {
    733      return this.PROTOCOLS_WITH_ICONS.some(p => url.startsWith(p))
    734        ? "page-icon:" + url
    735        : this.ICON.DEFAULT;
    736    }
    737    if (
    738      URL.isInstance(url) &&
    739      this.PROTOCOLS_WITH_ICONS.includes(url.protocol)
    740    ) {
    741      return "page-icon:" + url.href;
    742    }
    743    return this.ICON.DEFAULT;
    744  },
    745 
    746  /**
    747   * Tries to initiate a speculative connection to a given url.
    748   *
    749   * Note: This is not infallible, if a speculative connection cannot be
    750   *       initialized, it will be a no-op.
    751   *
    752   * @param {nsISearchEngine|nsIURI|URL|string} urlOrEngine entity to initiate
    753   *        a speculative connection for.
    754   * @param {window} window the window from where the connection is initialized.
    755   */
    756  setupSpeculativeConnection(urlOrEngine, window) {
    757    if (!lazy.UrlbarPrefs.get("speculativeConnect.enabled")) {
    758      return;
    759    }
    760    if (urlOrEngine instanceof Ci.nsISearchEngine) {
    761      try {
    762        urlOrEngine.speculativeConnect({
    763          window,
    764          originAttributes: window.gBrowser.contentPrincipal.originAttributes,
    765        });
    766      } catch (ex) {
    767        // Can't setup speculative connection for this url, just ignore it.
    768      }
    769      return;
    770    }
    771 
    772    if (URL.isInstance(urlOrEngine)) {
    773      urlOrEngine = urlOrEngine.href;
    774    }
    775 
    776    try {
    777      let uri =
    778        urlOrEngine instanceof Ci.nsIURI
    779          ? urlOrEngine
    780          : Services.io.newURI(urlOrEngine);
    781      Services.io.speculativeConnect(
    782        uri,
    783        window.gBrowser.contentPrincipal,
    784        window.docShell.QueryInterface(Ci.nsIInterfaceRequestor),
    785        false
    786      );
    787    } catch (ex) {
    788      // Can't setup speculative connection for this url, just ignore it.
    789    }
    790  },
    791 
    792  /**
    793   * Splits a url into base and ref strings, according to nsIURI.idl.
    794   * Base refers to the part of the url before the ref, excluding the #.
    795   *
    796   * @param {string} url
    797   *   The url to split.
    798   * @returns {object} { base, ref }
    799   *   Base and ref parts of the given url. Ref is an empty string
    800   *   if there is no ref and undefined if url is not well-formed.
    801   */
    802  extractRefFromUrl(url) {
    803    let uri = URL.parse(url)?.URI;
    804    if (uri) {
    805      return { base: uri.specIgnoringRef, ref: uri.ref };
    806    }
    807    return { base: url };
    808  },
    809 
    810  /**
    811   * Strips parts of a URL defined in `options`.
    812   *
    813   * @param {string} spec
    814   *        The text to modify.
    815   * @param {object} [options]
    816   *        The options object.
    817   * @param {boolean} [options.stripHttp]
    818   *        Whether to strip http.
    819   * @param {boolean} [options.stripHttps]
    820   *        Whether to strip https.
    821   * @param {boolean} [options.stripWww]
    822   *        Whether to strip `www.`.
    823   * @param {boolean} [options.trimSlash]
    824   *        Whether to trim the trailing slash.
    825   * @param {boolean} [options.trimEmptyQuery]
    826   *        Whether to trim a trailing `?`.
    827   * @param {boolean} [options.trimEmptyHash]
    828   *        Whether to trim a trailing `#`.
    829   * @param {boolean} [options.trimTrailingDot]
    830   *        Whether to trim a trailing '.'.
    831   * @returns {string[]} [modified, prefix, suffix]
    832   *          modified: {string} The modified spec.
    833   *          prefix: {string} The parts stripped from the prefix, if any.
    834   *          suffix: {string} The parts trimmed from the suffix, if any.
    835   */
    836  stripPrefixAndTrim(spec, options = {}) {
    837    let prefix = "";
    838    let suffix = "";
    839    if (options.stripHttp && spec.startsWith("http://")) {
    840      spec = spec.slice(7);
    841      prefix = "http://";
    842    } else if (options.stripHttps && spec.startsWith("https://")) {
    843      spec = spec.slice(8);
    844      prefix = "https://";
    845    }
    846    if (options.stripWww && spec.startsWith("www.")) {
    847      spec = spec.slice(4);
    848      prefix += "www.";
    849    }
    850    if (options.trimEmptyHash && spec.endsWith("#")) {
    851      spec = spec.slice(0, -1);
    852      suffix = "#" + suffix;
    853    }
    854    if (options.trimEmptyQuery && spec.endsWith("?")) {
    855      spec = spec.slice(0, -1);
    856      suffix = "?" + suffix;
    857    }
    858    if (options.trimSlash && spec.endsWith("/")) {
    859      spec = spec.slice(0, -1);
    860      suffix = "/" + suffix;
    861    }
    862    if (options.trimTrailingDot && spec.endsWith(".")) {
    863      spec = spec.slice(0, -1);
    864      suffix = "." + suffix;
    865    }
    866    return [spec, prefix, suffix];
    867  },
    868 
    869  /**
    870   * Strips a PSL verified public suffix from an hostname.
    871   *
    872   * Note: Because stripping the full suffix requires to verify it against the
    873   *   Public Suffix List, this call is not the cheapest, and thus it should
    874   *   not be used in hot paths.
    875   *
    876   * @param {string} host A host name.
    877   * @returns {string} Host name without the public suffix.
    878   */
    879  stripPublicSuffixFromHost(host) {
    880    try {
    881      return host.substring(
    882        0,
    883        host.length - Services.eTLD.getKnownPublicSuffixFromHost(host).length
    884      );
    885    } catch (ex) {
    886      if (ex.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
    887        throw ex;
    888      }
    889    }
    890    return host;
    891  },
    892 
    893  /**
    894   * Used to filter out the javascript protocol from URIs, since we don't
    895   * support LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL for those.
    896   *
    897   * @param {string} pasteData The data to check for javacript protocol.
    898   * @returns {string} The modified paste data.
    899   */
    900  stripUnsafeProtocolOnPaste(pasteData) {
    901    for (;;) {
    902      let scheme = "";
    903      try {
    904        scheme = Services.io.extractScheme(pasteData);
    905      } catch (ex) {
    906        // If it throws, this is not a javascript scheme.
    907      }
    908      if (scheme != "javascript") {
    909        break;
    910      }
    911 
    912      pasteData = pasteData.substring(pasteData.indexOf(":") + 1);
    913    }
    914    return pasteData;
    915  },
    916 
    917  /**
    918   * Add a (url, input) tuple to the input history table that drives adaptive
    919   * results.
    920   *
    921   * @param {string} url The url to add input history for
    922   * @param {string} input The associated search term
    923   */
    924  async addToInputHistory(url, input) {
    925    await lazy.PlacesUtils.withConnectionWrapper("addToInputHistory", db => {
    926      // use_count will asymptotically approach the max of 10.
    927      return db.executeCached(
    928        `
    929        INSERT OR REPLACE INTO moz_inputhistory
    930        SELECT h.id, IFNULL(i.input, :input), IFNULL(i.use_count, 0) * .9 + 1
    931        FROM moz_places h
    932        LEFT JOIN moz_inputhistory i ON i.place_id = h.id AND i.input = :input
    933        WHERE url_hash = hash(:url) AND url = :url
    934      `,
    935        { url, input: input.toLowerCase() }
    936      );
    937    });
    938  },
    939 
    940  /**
    941   * Remove a (url, input*) tuple from the input history table that drives
    942   * adaptive results.
    943   * Note the input argument is used as a wildcard so any match starting with
    944   * it will also be removed.
    945   *
    946   * @param {string} url The url to add input history for
    947   * @param {string} input The associated search term
    948   */
    949  async removeInputHistory(url, input) {
    950    await lazy.PlacesUtils.withConnectionWrapper("removeInputHistory", db => {
    951      return db.executeCached(
    952        `
    953        DELETE FROM moz_inputhistory
    954        WHERE input BETWEEN :input AND :input || X'FFFF'
    955          AND place_id =
    956            (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)
    957        `,
    958        { url, input: input.toLowerCase() }
    959      );
    960    });
    961  },
    962 
    963  /**
    964   * Whether the passed-in input event is paste event.
    965   *
    966   * @param {InputEvent} event an input DOM event.
    967   * @returns {boolean} Whether the event is a paste event.
    968   */
    969  isPasteEvent(event) {
    970    return (
    971      event.inputType &&
    972      (event.inputType.startsWith("insertFromPaste") ||
    973        event.inputType == "insertFromYank")
    974    );
    975  },
    976 
    977  /**
    978   * Given a string, checks if it looks like a single word host, not containing
    979   * spaces nor dots (apart from a possible trailing one).
    980   *
    981   * Note: This matching should stay in sync with the related code in
    982   * URIFixup::KeywordURIFixup
    983   *
    984   * @param {string} value
    985   *   The string to check.
    986   * @returns {boolean}
    987   *   Whether the value looks like a single word host.
    988   */
    989  looksLikeSingleWordHost(value) {
    990    let str = value.trim();
    991    return this.REGEXP_SINGLE_WORD.test(str);
    992  },
    993 
    994  /**
    995   * Returns the portion of a string starting at the index where another string
    996   * begins.
    997   *
    998   * @param   {string} sourceStr
    999   *          The string to search within.
   1000   * @param   {string} targetStr
   1001   *          The string to search for.
   1002   * @returns {string} The substring within sourceStr starting at targetStr, or
   1003   *          the empty string if targetStr does not occur in sourceStr.
   1004   */
   1005  substringAt(sourceStr, targetStr) {
   1006    let index = sourceStr.indexOf(targetStr);
   1007    return index < 0 ? "" : sourceStr.substr(index);
   1008  },
   1009 
   1010  /**
   1011   * Returns the portion of a string starting at the index where another string
   1012   * ends.
   1013   *
   1014   * @param   {string} sourceStr
   1015   *          The string to search within.
   1016   * @param   {string} targetStr
   1017   *          The string to search for.
   1018   * @returns {string} The substring within sourceStr where targetStr ends, or
   1019   *          the empty string if targetStr does not occur in sourceStr.
   1020   */
   1021  substringAfter(sourceStr, targetStr) {
   1022    let index = sourceStr.indexOf(targetStr);
   1023    return index < 0 ? "" : sourceStr.substr(index + targetStr.length);
   1024  },
   1025 
   1026  /**
   1027   * Strips the prefix from a URL and returns the prefix and the remainder of
   1028   * the URL. "Prefix" is defined to be the scheme and colon plus zero to two
   1029   * slashes (see `UrlbarTokenizer.REGEXP_PREFIX`). If the given string is not
   1030   * actually a URL or it has a prefix we don't recognize, then an empty prefix
   1031   * and the string itself is returned.
   1032   *
   1033   * @param   {string} str The possible URL to strip.
   1034   * @returns {Array} If `str` is a URL with a prefix we recognize,
   1035   *          then [prefix, remainder].  Otherwise, ["", str].
   1036   */
   1037  stripURLPrefix(str) {
   1038    let match = lazy.UrlUtils.REGEXP_PREFIX.exec(str);
   1039    if (!match) {
   1040      return ["", str];
   1041    }
   1042    let prefix = match[0];
   1043    if (prefix.length < str.length && str[prefix.length] == " ") {
   1044      // A space following a prefix:
   1045      // e.g. "http:// some search string", "about: some search string"
   1046      return ["", str];
   1047    }
   1048    if (
   1049      prefix.endsWith(":") &&
   1050      !this.PROTOCOLS_WITHOUT_AUTHORITY.includes(prefix.toLowerCase())
   1051    ) {
   1052      // Something that looks like a URI scheme but we won't treat as one:
   1053      // e.g. "localhost:8888"
   1054      return ["", str];
   1055    }
   1056    return [prefix, str.substring(prefix.length)];
   1057  },
   1058 
   1059  /**
   1060   * Runs a search for the given string, and returns the heuristic result.
   1061   *
   1062   * @param {string} searchString The string to search for.
   1063   * @param {UrlbarInput} urlbarInput The input requesting it.
   1064   * @returns {Promise<UrlbarResult>} an heuristic result.
   1065   */
   1066  async getHeuristicResultFor(searchString, urlbarInput) {
   1067    if (!searchString) {
   1068      throw new Error("Must pass a non-null search string");
   1069    }
   1070 
   1071    let gBrowser = urlbarInput.window.gBrowser;
   1072    let options = {
   1073      allowAutofill: false,
   1074      isPrivate: urlbarInput.isPrivate,
   1075      sapName: urlbarInput.sapName,
   1076      maxResults: 1,
   1077      searchString,
   1078      userContextId: parseInt(
   1079        gBrowser.selectedBrowser.getAttribute("usercontextid") || 0
   1080      ),
   1081      tabGroup: gBrowser.selectedTab.group?.id ?? null,
   1082      prohibitRemoteResults: true,
   1083      providers: [
   1084        "UrlbarProviderAliasEngines",
   1085        "UrlbarProviderBookmarkKeywords",
   1086        "UrlbarProviderHeuristicFallback",
   1087      ],
   1088    };
   1089    if (urlbarInput.searchMode) {
   1090      let searchMode = urlbarInput.searchMode;
   1091      options.searchMode = searchMode;
   1092      if (searchMode.source) {
   1093        options.sources = [searchMode.source];
   1094      }
   1095    }
   1096    let context = new UrlbarQueryContext(options);
   1097    await urlbarInput.controller.manager.startQuery(context);
   1098    if (!context.heuristicResult) {
   1099      throw new Error("There should always be an heuristic result");
   1100    }
   1101    return context.heuristicResult;
   1102  },
   1103 
   1104  /**
   1105   * Creates a console logger.
   1106   * Logging level can be controlled through the `browser.urlbar.loglevel`
   1107   * preference.
   1108   *
   1109   * @param {object} [options] Options for the logger.
   1110   * @param {string} [options.prefix] Prefix to use for the logged messages.
   1111   * @returns {ConsoleInstance} The console logger.
   1112   */
   1113  getLogger({ prefix = "" } = {}) {
   1114    if (!this._loggers) {
   1115      this._loggers = new Map();
   1116    }
   1117    let logger = this._loggers.get(prefix);
   1118    if (!logger) {
   1119      logger = console.createInstance({
   1120        prefix: `URLBar${prefix ? " - " + prefix : ""}`,
   1121        maxLogLevelPref: "browser.urlbar.loglevel",
   1122      });
   1123      this._loggers.set(prefix, logger);
   1124    }
   1125    return logger;
   1126  },
   1127 
   1128  /**
   1129   * Returns the name of a result source.  The name is the lowercase name of the
   1130   * corresponding property in the RESULT_SOURCE object.
   1131   *
   1132   * @param {Values<typeof this.RESULT_SOURCE>} source
   1133   *   A UrlbarUtils.RESULT_SOURCE value.
   1134   * @returns {string}
   1135   *   The token's name, a lowercased name in the RESULT_SOURCE object.
   1136   */
   1137  getResultSourceName(source) {
   1138    if (!this._resultSourceNamesBySource) {
   1139      this._resultSourceNamesBySource = new Map();
   1140      for (let [name, src] of Object.entries(this.RESULT_SOURCE)) {
   1141        this._resultSourceNamesBySource.set(src, name.toLowerCase());
   1142      }
   1143    }
   1144    return this._resultSourceNamesBySource.get(source);
   1145  },
   1146 
   1147  /**
   1148   * Add the search to form history.  This also updates any existing form
   1149   * history for the search.
   1150   *
   1151   * @param {UrlbarInput} input The UrlbarInput object requesting the addition.
   1152   * @param {string} value The value to add.
   1153   * @param {string} [source] The source of the addition, usually
   1154   *        the name of the engine the search was made with.
   1155   * @returns {Promise<void>} resolved once the operation is complete
   1156   */
   1157  addToFormHistory(input, value, source) {
   1158    // If the user types a search engine alias without a search string,
   1159    // we have an empty search string and we can't bump it.
   1160    // We also don't want to add history in private browsing mode.
   1161    // Finally we don't want to store extremely long strings that would not be
   1162    // particularly useful to the user.
   1163    if (
   1164      !value ||
   1165      input.isPrivate ||
   1166      value.length >
   1167        lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
   1168    ) {
   1169      return Promise.resolve();
   1170    }
   1171    return lazy.FormHistory.update({
   1172      op: "bump",
   1173      fieldname: lazy.DEFAULT_FORM_HISTORY_PARAM,
   1174      value,
   1175      source,
   1176    });
   1177  },
   1178 
   1179  /**
   1180   * Returns whether a URL can be autofilled from a candidate string. This
   1181   * function is specifically designed for origin and up-to-the-next-slash URL
   1182   * autofill. It should not be used for other types of autofill.
   1183   *
   1184   * @param {string} urlString
   1185   *                 The URL to test
   1186   * @param {string} candidateString
   1187   *                 The candidate string to test against
   1188   * @param {boolean} [checkFragmentOnly]
   1189   *                 If want to check the fragment only, pass true.
   1190   *                 Otherwise, check whole url.
   1191   * @returns {boolean} true: can autofill
   1192   */
   1193  canAutofillURL(urlString, candidateString, checkFragmentOnly = false) {
   1194    // If the URL does not start with the candidate, it can't be autofilled.
   1195    // The length check is an optimization to short-circuit the `startsWith()`.
   1196    if (
   1197      !checkFragmentOnly &&
   1198      (urlString.length <= candidateString.length ||
   1199        !urlString
   1200          .toLocaleLowerCase()
   1201          .startsWith(candidateString.toLocaleLowerCase()))
   1202    ) {
   1203      return false;
   1204    }
   1205 
   1206    // Create `URL` objects to make the logic below easier. The strings must
   1207    // include schemes for this to work.
   1208    if (!lazy.UrlUtils.REGEXP_PREFIX.test(urlString)) {
   1209      urlString = "http://" + urlString;
   1210    }
   1211    if (!lazy.UrlUtils.REGEXP_PREFIX.test(candidateString)) {
   1212      candidateString = "http://" + candidateString;
   1213    }
   1214 
   1215    let url = URL.parse(urlString);
   1216    let candidate = URL.parse(candidateString);
   1217    if (!url || !candidate) {
   1218      return false;
   1219    }
   1220 
   1221    if (checkFragmentOnly) {
   1222      return url.hash.startsWith(candidate.hash);
   1223    }
   1224 
   1225    // For both origin and URL autofill, autofill should stop when the user
   1226    // types a trailing slash. This is a fundamental part of autofill's
   1227    // up-to-the-next-slash behavior. We handle that here in the else-if branch.
   1228    // The length and hash checks in the else-if condition aren't strictly
   1229    // necessary -- the else-if branch could simply be an else-branch that
   1230    // returns false -- but they mean this function will return true when the
   1231    // URL and candidate have the same case-insenstive path and no hash. In
   1232    // other words, we allow a URL to autofill itself.
   1233    if (!candidate.href.endsWith("/")) {
   1234      // The candidate doesn't end in a slash. The URL can't be autofilled if
   1235      // its next slash is not at the end.
   1236      let nextSlashIndex = url.pathname.indexOf("/", candidate.pathname.length);
   1237      if (nextSlashIndex >= 0 && nextSlashIndex != url.pathname.length - 1) {
   1238        return false;
   1239      }
   1240    } else if (url.pathname.length > candidate.pathname.length || url.hash) {
   1241      return false;
   1242    }
   1243 
   1244    return url.hash.startsWith(candidate.hash);
   1245  },
   1246 
   1247  /**
   1248   * Extracts a telemetry type from a result, used by scalars and event
   1249   * telemetry.
   1250   *
   1251   * @param {UrlbarResult} result The result to analyze.
   1252   * @returns {string} A string type for telemetry.
   1253   */
   1254  telemetryTypeFromResult(result) {
   1255    if (!result) {
   1256      return "unknown";
   1257    }
   1258    switch (result.type) {
   1259      case this.RESULT_TYPE.TAB_SWITCH:
   1260        return "switchtab";
   1261      case this.RESULT_TYPE.SEARCH:
   1262        if (result.providerName == "UrlbarProviderRecentSearches") {
   1263          return "recent_search";
   1264        }
   1265        if (result.source == this.RESULT_SOURCE.HISTORY) {
   1266          return "formhistory";
   1267        }
   1268        if (result.providerName == "UrlbarProviderTabToSearch") {
   1269          return "tabtosearch";
   1270        }
   1271        if (result.payload.suggestion) {
   1272          let type = result.payload.trending ? "trending" : "searchsuggestion";
   1273          if (result.isRichSuggestion) {
   1274            type += "_rich";
   1275          }
   1276          return type;
   1277        }
   1278        return "searchengine";
   1279      case this.RESULT_TYPE.URL:
   1280        if (result.autofill) {
   1281          let { type } = result.autofill;
   1282          if (!type) {
   1283            type = "other";
   1284            console.error(
   1285              new Error(
   1286                "`result.autofill.type` not set, falling back to 'other'"
   1287              )
   1288            );
   1289          }
   1290          return `autofill_${type}`;
   1291        }
   1292        if (
   1293          result.source == this.RESULT_SOURCE.OTHER_LOCAL &&
   1294          result.heuristic
   1295        ) {
   1296          return "visiturl";
   1297        }
   1298        if (result.providerName == "UrlbarProviderQuickSuggest") {
   1299          return "quicksuggest";
   1300        }
   1301        if (result.providerName == "UrlbarProviderClipboard") {
   1302          return "clipboard";
   1303        }
   1304        {
   1305          let type =
   1306            result.source == this.RESULT_SOURCE.BOOKMARKS
   1307              ? "bookmark"
   1308              : "history";
   1309          if (result.providerName == "UrlbarProviderInputHistory") {
   1310            return type + "adaptive";
   1311          }
   1312          return type;
   1313        }
   1314      case this.RESULT_TYPE.KEYWORD:
   1315        return "keyword";
   1316      case this.RESULT_TYPE.OMNIBOX:
   1317        return "extension";
   1318      case this.RESULT_TYPE.REMOTE_TAB:
   1319        return "remotetab";
   1320      case this.RESULT_TYPE.TIP:
   1321        return "tip";
   1322      case this.RESULT_TYPE.DYNAMIC:
   1323        if (result.providerName == "UrlbarProviderTabToSearch") {
   1324          // This is the onboarding result.
   1325          return "tabtosearch";
   1326        }
   1327        return "dynamic";
   1328      case this.RESULT_TYPE.RESTRICT:
   1329        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.BOOKMARK) {
   1330          return "restrict_keyword_bookmarks";
   1331        }
   1332        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.OPENPAGE) {
   1333          return "restrict_keyword_tabs";
   1334        }
   1335        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.HISTORY) {
   1336          return "restrict_keyword_history";
   1337        }
   1338        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.ACTION) {
   1339          return "restrict_keyword_actions";
   1340        }
   1341    }
   1342    return "unknown";
   1343  },
   1344 
   1345  /**
   1346   * Unescape the given uri to use as UI.
   1347   * NOTE: If the length of uri is over MAX_TEXT_LENGTH,
   1348   *       return the given uri as it is.
   1349   *
   1350   * @param {string} uri will be unescaped.
   1351   * @returns {string} Unescaped uri.
   1352   */
   1353  unEscapeURIForUI(uri) {
   1354    return uri.length > this.MAX_TEXT_LENGTH
   1355      ? uri
   1356      : Services.textToSubURI.unEscapeURIForUI(uri);
   1357  },
   1358 
   1359  /**
   1360   * Checks whether a given text has right-to-left direction or not.
   1361   *
   1362   * @param {string} value The text which should be check for RTL direction.
   1363   * @param {Window} window The window where 'value' is going to be displayed.
   1364   * @returns {boolean} Returns true if text has right-to-left direction and
   1365   *                    false otherwise.
   1366   */
   1367  isTextDirectionRTL(value, window) {
   1368    let directionality = window.windowUtils.getDirectionFromText(value);
   1369    return directionality == window.windowUtils.DIRECTION_RTL;
   1370  },
   1371 
   1372  /**
   1373   * Unescape, decode punycode, and trim (both protocol and trailing slash)
   1374   * the URL. Use for displaying purposes only!
   1375   *
   1376   * @param {string|URL} url The url that should be prepared for display.
   1377   * @param {object} [options] Preparation options.
   1378   * @param {boolean} [options.trimURL] Whether the displayed URL should be
   1379   *                  trimmed or not.
   1380   * @param {boolean} [options.schemeless] Trim `http(s)://`.
   1381   * @returns {string} Prepared url.
   1382   */
   1383  prepareUrlForDisplay(url, { trimURL = true, schemeless = false } = {}) {
   1384    // Some domains are encoded in punycode. The following ensures we display
   1385    // the url in utf-8.
   1386    let displayString;
   1387    if (typeof url == "string") {
   1388      try {
   1389        displayString = new URL(url).URI.displaySpec;
   1390      } catch {
   1391        // In some cases url is not a valid url, so we fallback to using the
   1392        // string as-is.
   1393        displayString = url;
   1394      }
   1395    } else {
   1396      displayString = url.URI.displaySpec;
   1397    }
   1398 
   1399    if (displayString) {
   1400      if (schemeless) {
   1401        displayString = this.stripPrefixAndTrim(displayString, {
   1402          stripHttp: true,
   1403          stripHttps: true,
   1404        })[0];
   1405      } else if (trimURL && lazy.UrlbarPrefs.get("trimURLs")) {
   1406        displayString =
   1407          lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(displayString);
   1408        if (displayString.startsWith("https://")) {
   1409          displayString = displayString.substring(8);
   1410          if (displayString.startsWith("www.")) {
   1411            displayString = displayString.substring(4);
   1412          }
   1413        }
   1414      }
   1415    }
   1416 
   1417    return this.unEscapeURIForUI(displayString);
   1418  },
   1419 
   1420  /**
   1421   * Extracts a group for search engagement telemetry from a result.
   1422   *
   1423   * @param {UrlbarResult} result The result to analyze.
   1424   * @returns {string} Group name as string.
   1425   */
   1426  searchEngagementTelemetryGroup(result) {
   1427    if (!result) {
   1428      return "unknown";
   1429    }
   1430    if (result.isBestMatch) {
   1431      return "top_pick";
   1432    }
   1433    if (result.providerName === "UrlbarProviderTopSites") {
   1434      return "top_site";
   1435    }
   1436 
   1437    switch (this.getResultGroup(result)) {
   1438      case this.RESULT_GROUP.INPUT_HISTORY: {
   1439        return "adaptive_history";
   1440      }
   1441      case this.RESULT_GROUP.RECENT_SEARCH: {
   1442        return "recent_search";
   1443      }
   1444      case this.RESULT_GROUP.FORM_HISTORY: {
   1445        return "search_history";
   1446      }
   1447      case this.RESULT_GROUP.TAIL_SUGGESTION:
   1448      case this.RESULT_GROUP.REMOTE_SUGGESTION: {
   1449        let group = result.payload.trending
   1450          ? "trending_search"
   1451          : "search_suggest";
   1452        if (result.isRichSuggestion) {
   1453          group += "_rich";
   1454        }
   1455        return group;
   1456      }
   1457      case this.RESULT_GROUP.REMOTE_TAB: {
   1458        return "remote_tab";
   1459      }
   1460      case this.RESULT_GROUP.HEURISTIC_EXTENSION:
   1461      case this.RESULT_GROUP.HEURISTIC_OMNIBOX:
   1462      case this.RESULT_GROUP.OMNIBOX: {
   1463        return "addon";
   1464      }
   1465      case this.RESULT_GROUP.GENERAL: {
   1466        return "general";
   1467      }
   1468      // Group of UrlbarProviderQuickSuggest is GENERAL_PARENT.
   1469      case this.RESULT_GROUP.GENERAL_PARENT: {
   1470        return "suggest";
   1471      }
   1472      case this.RESULT_GROUP.ABOUT_PAGES: {
   1473        return "about_page";
   1474      }
   1475      case this.RESULT_GROUP.SUGGESTED_INDEX: {
   1476        return "suggested_index";
   1477      }
   1478      case this.RESULT_GROUP.RESTRICT_SEARCH_KEYWORD: {
   1479        return "restrict_keyword";
   1480      }
   1481    }
   1482 
   1483    return result.heuristic ? "heuristic" : "unknown";
   1484  },
   1485 
   1486  /**
   1487   * Extracts a type for search engagement telemetry from a result.
   1488   *
   1489   * @param {UrlbarResult} result The result to analyze.
   1490   * @param {string} [selType] An optional parameter for the selected type.
   1491   * @returns {string} Type as string.
   1492   */
   1493  searchEngagementTelemetryType(result, selType = null) {
   1494    if (!result) {
   1495      return selType === "oneoff" ? "search_shortcut_button" : "input_field";
   1496    }
   1497 
   1498    // While product doesn't use experimental addons anymore, tests may still do
   1499    // for testing purposes.
   1500    if (
   1501      result.providerType === this.PROVIDER_TYPE.EXTENSION &&
   1502      result.providerName != "UrlbarProviderOmnibox"
   1503    ) {
   1504      return "experimental_addon";
   1505    }
   1506 
   1507    if (result.providerName == "UrlbarProviderQuickSuggest") {
   1508      return this._getQuickSuggestTelemetryType(result);
   1509    }
   1510 
   1511    // Appends subtype to certain result types.
   1512    function checkForSubType(type, res) {
   1513      if (res.providerName == "UrlbarProviderInputHistory") {
   1514        type += "_adaptive";
   1515      } else if (res.providerName == "UrlbarProviderSemanticHistorySearch") {
   1516        type += "_semantic";
   1517      }
   1518      if (
   1519        lazy.UrlbarSearchUtils.resultIsSERP(res, [
   1520          UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
   1521          UrlbarUtils.RESULT_SOURCE.HISTORY,
   1522          UrlbarUtils.RESULT_SOURCE.TABS,
   1523        ])
   1524      ) {
   1525        type += "_serp";
   1526      }
   1527      return type;
   1528    }
   1529 
   1530    switch (result.type) {
   1531      case this.RESULT_TYPE.DYNAMIC:
   1532        switch (result.providerName) {
   1533          case "UrlbarProviderCalculator":
   1534            return "calc";
   1535          case "UrlbarProviderTabToSearch":
   1536            return "tab_to_search";
   1537          case "UrlbarProviderUnitConversion":
   1538            return "unit";
   1539          case "UrlbarProviderQuickSuggestContextualOptIn":
   1540            return "fxsuggest_data_sharing_opt_in";
   1541          case "UrlbarProviderGlobalActions":
   1542          case "UrlbarProviderActionsSearchMode":
   1543            return "action";
   1544        }
   1545        break;
   1546      case this.RESULT_TYPE.KEYWORD:
   1547        return "keyword";
   1548      case this.RESULT_TYPE.OMNIBOX:
   1549        return "addon";
   1550      case this.RESULT_TYPE.REMOTE_TAB:
   1551        return "remote_tab";
   1552      case this.RESULT_TYPE.SEARCH:
   1553        if (result.providerName === "UrlbarProviderTabToSearch") {
   1554          return "tab_to_search";
   1555        }
   1556        if (result.source == this.RESULT_SOURCE.HISTORY) {
   1557          return result.providerName == "UrlbarProviderRecentSearches"
   1558            ? "recent_search"
   1559            : "search_history";
   1560        }
   1561        if (result.payload.suggestion) {
   1562          let type = result.payload.trending
   1563            ? "trending_search"
   1564            : "search_suggest";
   1565          if (result.isRichSuggestion) {
   1566            type += "_rich";
   1567          }
   1568          return type;
   1569        }
   1570        return "search_engine";
   1571      case this.RESULT_TYPE.TAB_SWITCH:
   1572        return checkForSubType("tab", result);
   1573      case this.RESULT_TYPE.TIP:
   1574        if (result.providerName === "UrlbarProviderInterventions") {
   1575          // disable as part of tor-browser#41327
   1576          switch (result.payload.type) {
   1577            case lazy.UrlbarProviderInterventions.TIP_TYPE.CLEAR:
   1578            // return "intervention_clear";
   1579            // fall-through
   1580            case lazy.UrlbarProviderInterventions.TIP_TYPE.REFRESH:
   1581            // return "intervention_refresh";
   1582            // fall-through
   1583            case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_ASK:
   1584            case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_CHECKING:
   1585            case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_REFRESH:
   1586            case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_RESTART:
   1587            case lazy.UrlbarProviderInterventions.TIP_TYPE.UPDATE_WEB:
   1588            // return "intervention_update";
   1589            // fall-through
   1590            default:
   1591              return "intervention_unknown";
   1592          }
   1593        }
   1594        switch (result.payload.type) {
   1595          case lazy.UrlbarProviderSearchTips.TIP_TYPE.ONBOARD:
   1596            return "tip_onboard";
   1597          case lazy.UrlbarProviderSearchTips.TIP_TYPE.REDIRECT:
   1598            return "tip_redirect";
   1599          case "dismissalAcknowledgment":
   1600            return "tip_dismissal_acknowledgment";
   1601          default:
   1602            return "tip_unknown";
   1603        }
   1604      case this.RESULT_TYPE.URL:
   1605        if (
   1606          result.source === this.RESULT_SOURCE.OTHER_LOCAL &&
   1607          result.heuristic
   1608        ) {
   1609          return "url";
   1610        }
   1611        if (result.autofill) {
   1612          return `autofill_${result.autofill.type ?? "unknown"}`;
   1613        }
   1614        if (result.providerName === "UrlbarProviderTopSites") {
   1615          return "top_site";
   1616        }
   1617        if (result.providerName === "UrlbarProviderClipboard") {
   1618          return "clipboard";
   1619        }
   1620        if (result.source === this.RESULT_SOURCE.BOOKMARKS) {
   1621          return checkForSubType("bookmark", result);
   1622        }
   1623        return checkForSubType("history", result);
   1624      case this.RESULT_TYPE.RESTRICT:
   1625        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.BOOKMARK) {
   1626          return "restrict_keyword_bookmarks";
   1627        }
   1628        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.OPENPAGE) {
   1629          return "restrict_keyword_tabs";
   1630        }
   1631        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.HISTORY) {
   1632          return "restrict_keyword_history";
   1633        }
   1634        if (result.payload.keyword === lazy.UrlbarTokenizer.RESTRICT.ACTION) {
   1635          return "restrict_keyword_actions";
   1636        }
   1637    }
   1638 
   1639    return "unknown";
   1640  },
   1641 
   1642  searchEngagementTelemetryAction(result) {
   1643    if (result.providerName != "UrlbarProviderGlobalActions") {
   1644      return result.payload.action?.key ?? "none";
   1645    }
   1646    return result.payload.actionsResults.map(({ key }) => key).join(",");
   1647  },
   1648 
   1649  _getQuickSuggestTelemetryType(result) {
   1650    if (result.payload.telemetryType == "weather") {
   1651      // Return "weather" without the usual source prefix for consistency with
   1652      // past reporting of weather suggestions.
   1653      return "weather";
   1654    }
   1655    return result.payload.source + "_" + result.payload.telemetryType;
   1656  },
   1657 
   1658  /**
   1659   * For use when we want to hash a pair of items in a dictionary
   1660   *
   1661   * @param {string[]} tokens
   1662   *   list of tokens to join into a string eg "a" "b" "c"
   1663   * @returns {string}
   1664   *   the tokens joined in a string "a|b|c"
   1665   */
   1666  tupleString(...tokens) {
   1667    return tokens.filter(t => t).join("|");
   1668  },
   1669 
   1670  /**
   1671   * Creates camelCase versions of snake_case keys in the given object and
   1672   * recursively all nested objects. All objects are modified in place and the
   1673   * original snake_case keys are preserved.
   1674   *
   1675   * @param {object} obj
   1676   *   The object to modify.
   1677   * @param {boolean} [overwrite]
   1678   *   Controls what happens when a camelCase key is already defined for a
   1679   *   snake_case key (excluding keys that don't have underscores). If true the
   1680   *   existing key will be overwritten. If false an error will be thrown.
   1681   * @returns {object} The passed-in modified-in-place object.
   1682   */
   1683  copySnakeKeysToCamel(obj, overwrite = true) {
   1684    for (let [key, value] of Object.entries(obj)) {
   1685      // Trim off leading underscores since they'll interfere with the replace.
   1686      // We'll tack them back on after.
   1687      let match = key.match(/^_+/);
   1688      if (match) {
   1689        key = key.substring(match[0].length);
   1690      }
   1691      let camelKey = key.replace(/_([^_])/g, (m, p1) => p1.toUpperCase());
   1692      if (match) {
   1693        camelKey = match[0] + camelKey;
   1694      }
   1695      if (!overwrite && camelKey != key && obj.hasOwnProperty(camelKey)) {
   1696        throw new Error(
   1697          `Can't copy snake_case key '${key}' to camelCase key ` +
   1698            `'${camelKey}' because '${camelKey}' is already defined`
   1699        );
   1700      }
   1701      obj[camelKey] = value;
   1702      if (value && typeof value == "object") {
   1703        this.copySnakeKeysToCamel(value);
   1704      }
   1705    }
   1706    return obj;
   1707  },
   1708 
   1709  /**
   1710   * Create secondary action button data for tab switch.
   1711   *
   1712   * @param {number} userContextId
   1713   *   The container id for the tab.
   1714   * @returns {object} data to create secondary action button.
   1715   */
   1716  createTabSwitchSecondaryAction(userContextId) {
   1717    let action = { key: "tabswitch" };
   1718    let identity =
   1719      lazy.ContextualIdentityService.getPublicIdentityFromId(userContextId);
   1720 
   1721    if (identity) {
   1722      let label =
   1723        lazy.ContextualIdentityService.getUserContextLabel(
   1724          userContextId
   1725        ).toLowerCase();
   1726      action.l10nId = "urlbar-result-action-switch-tab-with-container";
   1727      action.l10nArgs = {
   1728        container: label,
   1729      };
   1730      action.classList = [
   1731        "urlbarView-userContext",
   1732        `identity-color-${identity.color}`,
   1733      ];
   1734    } else {
   1735      action.l10nId = "urlbar-result-action-switch-tab";
   1736    }
   1737 
   1738    return action;
   1739  },
   1740 
   1741  /**
   1742   * Adds text content to a node, placing substrings that should be highlighted
   1743   * inside <strong> nodes.
   1744   *
   1745   * @param {Element} parentNode
   1746   *   The text content will be added to this node.
   1747   * @param {string} textContent
   1748   *   The text content to give the node.
   1749   * @param {Array} highlights
   1750   *   Array of highlights as returned by `UrlbarUtils.getTokenMatches()` or
   1751   *   `UrlbarResult.getDisplayableValueAndHighlights()`.
   1752   */
   1753  addTextContentWithHighlights(parentNode, textContent, highlights) {
   1754    parentNode.textContent = "";
   1755    if (!textContent) {
   1756      return;
   1757    }
   1758 
   1759    highlights = (highlights || []).concat([[textContent.length, 0]]);
   1760    let index = 0;
   1761    for (let [highlightIndex, highlightLength] of highlights) {
   1762      if (highlightIndex - index > 0) {
   1763        parentNode.appendChild(
   1764          parentNode.ownerDocument.createTextNode(
   1765            textContent.substring(index, highlightIndex)
   1766          )
   1767        );
   1768      }
   1769      if (highlightLength > 0) {
   1770        let strong = parentNode.ownerDocument.createElement("strong");
   1771        strong.textContent = textContent.substring(
   1772          highlightIndex,
   1773          highlightIndex + highlightLength
   1774        );
   1775        parentNode.appendChild(strong);
   1776      }
   1777      index = highlightIndex + highlightLength;
   1778    }
   1779  },
   1780 
   1781  /**
   1782   * Formats the numerical portion of unit conversion results.
   1783   *
   1784   * @param {number} result
   1785   *  The raw unformatted unit conversion result.
   1786   */
   1787  formatUnitConversionResult(result) {
   1788    const DECIMAL_PRECISION = 10;
   1789    const MAX_SIG_FIGURES = 10;
   1790    const FULL_NUMBER_MAX_THRESHOLD = 1 * 10 ** 10;
   1791    const FULL_NUMBER_MIN_THRESHOLD = 10 ** -5;
   1792 
   1793    let locale = Services.locale.appLocaleAsBCP47;
   1794 
   1795    if (
   1796      Math.abs(result) >= FULL_NUMBER_MAX_THRESHOLD ||
   1797      (Math.abs(result) <= FULL_NUMBER_MIN_THRESHOLD && result !== 0)
   1798    ) {
   1799      return new Intl.NumberFormat(locale, {
   1800        style: "decimal",
   1801        notation: "scientific",
   1802        minimumFractionDigits: 1,
   1803        maximumFractionDigits: DECIMAL_PRECISION,
   1804        numberingSystem: "latn",
   1805      })
   1806        .format(result)
   1807        .toLowerCase();
   1808    } else if (Math.abs(result) >= 1) {
   1809      return new Intl.NumberFormat(locale, {
   1810        style: "decimal",
   1811        maximumFractionDigits: DECIMAL_PRECISION,
   1812        numberingSystem: "latn",
   1813      }).format(result);
   1814    }
   1815    return new Intl.NumberFormat(locale, {
   1816      style: "decimal",
   1817      maximumSignificantDigits: MAX_SIG_FIGURES,
   1818      numberingSystem: "latn",
   1819    }).format(result);
   1820  },
   1821 };
   1822 
   1823 ChromeUtils.defineLazyGetter(UrlbarUtils.ICON, "DEFAULT", () => {
   1824  return lazy.PlacesUtils.favicons.defaultFavicon.spec;
   1825 });
   1826 
   1827 ChromeUtils.defineLazyGetter(UrlbarUtils, "strings", () => {
   1828  return Services.strings.createBundle(
   1829    "chrome://global/locale/autocomplete.properties"
   1830  );
   1831 });
   1832 
   1833 const L10N_SCHEMA = {
   1834  type: "object",
   1835  required: ["id"],
   1836  properties: {
   1837    id: {
   1838      type: "string",
   1839    },
   1840    args: {
   1841      type: "object",
   1842      additionalProperties: true,
   1843    },
   1844    // This object is parallel to args and should include an entry for each arg
   1845    // to which highlights should be applied. See L10nCache.setElementL10n().
   1846    argsHighlights: {
   1847      type: "object",
   1848      additionalProperties: true,
   1849    },
   1850    // The remaining properties are related to l10n string caching. See
   1851    // `L10nCache`. All are optional and are false by default.
   1852    parseMarkup: {
   1853      type: "boolean",
   1854    },
   1855  },
   1856 };
   1857 
   1858 /**
   1859 * Payload JSON schemas for each result type.  Payloads are validated against
   1860 * these schemas using JsonSchemaValidator.sys.mjs.
   1861 */
   1862 UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
   1863  [UrlbarUtils.RESULT_TYPE.TAB_SWITCH]: {
   1864    type: "object",
   1865    required: ["url"],
   1866    properties: {
   1867      action: {
   1868        type: "object",
   1869        properties: {
   1870          classList: {
   1871            type: "array",
   1872            items: {
   1873              type: "string",
   1874            },
   1875          },
   1876          l10nArgs: {
   1877            type: "object",
   1878            additionalProperties: true,
   1879          },
   1880          l10nId: {
   1881            type: "string",
   1882          },
   1883          key: {
   1884            type: "string",
   1885          },
   1886        },
   1887      },
   1888      frecency: {
   1889        type: "number",
   1890      },
   1891      icon: {
   1892        type: "string",
   1893      },
   1894      isPinned: {
   1895        type: "boolean",
   1896      },
   1897      isSponsored: {
   1898        type: "boolean",
   1899      },
   1900      lastVisit: {
   1901        type: "number",
   1902      },
   1903      tabGroup: {
   1904        type: "string",
   1905      },
   1906      title: {
   1907        type: "string",
   1908      },
   1909      url: {
   1910        type: "string",
   1911      },
   1912      userContextId: {
   1913        type: "number",
   1914      },
   1915    },
   1916  },
   1917  [UrlbarUtils.RESULT_TYPE.SEARCH]: {
   1918    type: "object",
   1919    properties: {
   1920      blockL10n: L10N_SCHEMA,
   1921      description: {
   1922        type: "string",
   1923      },
   1924      descriptionL10n: L10N_SCHEMA,
   1925      engine: {
   1926        type: "string",
   1927      },
   1928      helpUrl: {
   1929        type: "string",
   1930      },
   1931      icon: {
   1932        type: "string",
   1933      },
   1934      inPrivateWindow: {
   1935        type: "boolean",
   1936      },
   1937      isBlockable: {
   1938        type: "boolean",
   1939      },
   1940      isManageable: {
   1941        type: "boolean",
   1942      },
   1943      isPinned: {
   1944        type: "boolean",
   1945      },
   1946      isPrivateEngine: {
   1947        type: "boolean",
   1948      },
   1949      isGeneralPurposeEngine: {
   1950        type: "boolean",
   1951      },
   1952      keyword: {
   1953        type: "string",
   1954      },
   1955      keywords: {
   1956        type: "string",
   1957      },
   1958      lowerCaseSuggestion: {
   1959        type: "string",
   1960      },
   1961      providesSearchMode: {
   1962        type: "boolean",
   1963      },
   1964      query: {
   1965        type: "string",
   1966      },
   1967      satisfiesAutofillThreshold: {
   1968        type: "boolean",
   1969      },
   1970      searchUrlDomainWithoutSuffix: {
   1971        type: "string",
   1972      },
   1973      suggestion: {
   1974        type: "string",
   1975      },
   1976      tail: {
   1977        type: "string",
   1978      },
   1979      tailPrefix: {
   1980        type: "string",
   1981      },
   1982      tailOffsetIndex: {
   1983        type: "number",
   1984      },
   1985      title: {
   1986        type: "string",
   1987      },
   1988      trending: {
   1989        type: "boolean",
   1990      },
   1991      url: {
   1992        type: "string",
   1993      },
   1994    },
   1995  },
   1996  [UrlbarUtils.RESULT_TYPE.URL]: {
   1997    type: "object",
   1998    required: ["url"],
   1999    properties: {
   2000      blockL10n: L10N_SCHEMA,
   2001      bottomTextL10n: L10N_SCHEMA,
   2002      description: {
   2003        type: "string",
   2004      },
   2005      descriptionL10n: L10N_SCHEMA,
   2006      dismissalKey: {
   2007        type: "string",
   2008      },
   2009      dupedHeuristic: {
   2010        type: "boolean",
   2011      },
   2012      frecency: {
   2013        type: "number",
   2014      },
   2015      helpL10n: L10N_SCHEMA,
   2016      helpUrl: {
   2017        type: "string",
   2018      },
   2019      icon: {
   2020        type: "string",
   2021      },
   2022      iconBlob: {
   2023        type: "object",
   2024      },
   2025      isBlockable: {
   2026        type: "boolean",
   2027      },
   2028      isManageable: {
   2029        type: "boolean",
   2030      },
   2031      isPinned: {
   2032        type: "boolean",
   2033      },
   2034      isSponsored: {
   2035        type: "boolean",
   2036      },
   2037      lastVisit: {
   2038        type: "number",
   2039      },
   2040      originalUrl: {
   2041        type: "string",
   2042      },
   2043      provider: {
   2044        type: "string",
   2045      },
   2046      requestId: {
   2047        type: "string",
   2048      },
   2049      sendAttributionRequest: {
   2050        type: "boolean",
   2051      },
   2052      shouldShowUrl: {
   2053        type: "boolean",
   2054      },
   2055      source: {
   2056        type: "string",
   2057      },
   2058      sponsoredAdvertiser: {
   2059        type: "string",
   2060      },
   2061      sponsoredBlockId: {
   2062        type: "number",
   2063      },
   2064      sponsoredClickUrl: {
   2065        type: "string",
   2066      },
   2067      sponsoredIabCategory: {
   2068        type: "string",
   2069      },
   2070      sponsoredImpressionUrl: {
   2071        type: "string",
   2072      },
   2073      sponsoredTileId: {
   2074        type: "number",
   2075      },
   2076      subtype: {
   2077        type: "string",
   2078      },
   2079      suggestionObject: {
   2080        type: "object",
   2081      },
   2082      tags: {
   2083        type: "array",
   2084        items: {
   2085          type: "string",
   2086        },
   2087      },
   2088      telemetryType: {
   2089        type: "string",
   2090      },
   2091      title: {
   2092        type: "string",
   2093      },
   2094      titleL10n: L10N_SCHEMA,
   2095      url: {
   2096        type: "string",
   2097      },
   2098      urlTimestampIndex: {
   2099        type: "number",
   2100      },
   2101    },
   2102  },
   2103  [UrlbarUtils.RESULT_TYPE.KEYWORD]: {
   2104    type: "object",
   2105    required: ["keyword", "url"],
   2106    properties: {
   2107      icon: {
   2108        type: "string",
   2109      },
   2110      input: {
   2111        type: "string",
   2112      },
   2113      keyword: {
   2114        type: "string",
   2115      },
   2116      postData: {
   2117        type: "string",
   2118      },
   2119      title: {
   2120        type: "string",
   2121      },
   2122      url: {
   2123        type: "string",
   2124      },
   2125    },
   2126  },
   2127  [UrlbarUtils.RESULT_TYPE.OMNIBOX]: {
   2128    type: "object",
   2129    required: ["keyword"],
   2130    properties: {
   2131      blockL10n: L10N_SCHEMA,
   2132      content: {
   2133        type: "string",
   2134      },
   2135      icon: {
   2136        type: "string",
   2137      },
   2138      isBlockable: {
   2139        type: "boolean",
   2140      },
   2141      keyword: {
   2142        type: "string",
   2143      },
   2144      title: {
   2145        type: "string",
   2146      },
   2147    },
   2148  },
   2149  [UrlbarUtils.RESULT_TYPE.REMOTE_TAB]: {
   2150    type: "object",
   2151    required: ["device", "url", "lastUsed"],
   2152    properties: {
   2153      device: {
   2154        type: "string",
   2155      },
   2156      icon: {
   2157        type: "string",
   2158      },
   2159      lastUsed: {
   2160        type: "number",
   2161      },
   2162      title: {
   2163        type: "string",
   2164      },
   2165      url: {
   2166        type: "string",
   2167      },
   2168    },
   2169  },
   2170  [UrlbarUtils.RESULT_TYPE.TIP]: {
   2171    type: "object",
   2172    required: ["type"],
   2173    properties: {
   2174      buttons: {
   2175        type: "array",
   2176        items: {
   2177          type: "object",
   2178          required: ["l10n"],
   2179          properties: {
   2180            l10n: L10N_SCHEMA,
   2181            url: {
   2182              type: "string",
   2183            },
   2184            command: {
   2185              type: "string",
   2186            },
   2187            input: {
   2188              type: "string",
   2189            },
   2190            attributes: {
   2191              type: "object",
   2192              properties: {
   2193                primary: {
   2194                  type: "string",
   2195                },
   2196              },
   2197            },
   2198            menu: {
   2199              type: "array",
   2200              items: {
   2201                type: "object",
   2202                properties: {
   2203                  l10n: L10N_SCHEMA,
   2204                  name: {
   2205                    type: "string",
   2206                  },
   2207                },
   2208              },
   2209            },
   2210          },
   2211        },
   2212      },
   2213      // TODO: This is intended only for WebExtensions. We should remove it and
   2214      // the WebExtensions urlbar API since we're no longer using it.
   2215      buttonText: {
   2216        type: "string",
   2217      },
   2218      // TODO: This is intended only for WebExtensions. We should remove it and
   2219      // the WebExtensions urlbar API since we're no longer using it.
   2220      buttonUrl: {
   2221        type: "string",
   2222      },
   2223      helpL10n: L10N_SCHEMA,
   2224      helpUrl: {
   2225        type: "string",
   2226      },
   2227      icon: {
   2228        type: "string",
   2229      },
   2230      // TODO: This is intended only for WebExtensions. We should remove it and
   2231      // the WebExtensions urlbar API since we're no longer using it.
   2232      text: {
   2233        type: "string",
   2234      },
   2235      titleL10n: L10N_SCHEMA,
   2236      descriptionL10n: L10N_SCHEMA,
   2237      // If the `descriptionL10n` string includes a "Learn more" link, the
   2238      // link anchor must have the attribute `data-l10n-name="learn-more-link"`
   2239      // and the value of `descriptionLearnMoreTopic` must be the SUMO help
   2240      // topic (the string appended to `app.support.baseURL`, e.g.,
   2241      // "firefox-suggest").
   2242      descriptionLearnMoreTopic: {
   2243        type: "string",
   2244      },
   2245      type: {
   2246        type: "string",
   2247        enum: [
   2248          "dismissalAcknowledgment",
   2249          "extension",
   2250          "intervention_clear",
   2251          "intervention_refresh",
   2252          "intervention_update_ask",
   2253          "intervention_update_refresh",
   2254          "intervention_update_restart",
   2255          "intervention_update_web",
   2256          "realtime_opt_in",
   2257          "searchTip_onboard",
   2258          "searchTip_redirect",
   2259          "test", // for tests only
   2260        ],
   2261      },
   2262    },
   2263  },
   2264  [UrlbarUtils.RESULT_TYPE.DYNAMIC]: {
   2265    type: "object",
   2266    required: ["dynamicType"],
   2267    properties: {
   2268      dynamicType: {
   2269        type: "string",
   2270      },
   2271    },
   2272  },
   2273  [UrlbarUtils.RESULT_TYPE.RESTRICT]: {
   2274    type: "object",
   2275    properties: {
   2276      icon: {
   2277        type: "string",
   2278      },
   2279      keyword: {
   2280        type: "string",
   2281      },
   2282      l10nRestrictKeywords: {
   2283        type: "array",
   2284        items: {
   2285          type: "string",
   2286        },
   2287      },
   2288      autofillKeyword: {
   2289        type: "string",
   2290      },
   2291      providesSearchMode: {
   2292        type: "boolean",
   2293      },
   2294    },
   2295  },
   2296 };
   2297 
   2298 /**
   2299 * @typedef UrlbarSearchModeData
   2300 * @property {Values<typeof UrlbarUtils.RESULT_SOURCE>} source
   2301 *   The source from which search mode was entered.
   2302 * @property {string} [engineName]
   2303 *   The search engine name associated with the search mode.
   2304 */
   2305 
   2306 /**
   2307 * UrlbarQueryContext defines a user's autocomplete input from within the urlbar.
   2308 * It supplements it with details of how the search results should be obtained
   2309 * and what they consist of.
   2310 */
   2311 export class UrlbarQueryContext {
   2312  /**
   2313   * Constructs the UrlbarQueryContext instance.
   2314   *
   2315   * @param {object} options
   2316   *   The initial options for UrlbarQueryContext.
   2317   * @param {string} options.sapName
   2318   *   The search access point name of the UrlbarInput for use with telemetry or
   2319   *   logging, e.g. `urlbar`, `searchbar`.
   2320   * @param {string} options.searchString
   2321   *   The string the user entered in autocomplete. Could be the empty string
   2322   *   in the case of the user opening the popup via the mouse.
   2323   * @param {boolean} options.isPrivate
   2324   *   Set to true if this query was started from a private browsing window.
   2325   * @param {number} options.maxResults
   2326   *   The maximum number of results that will be displayed for this query.
   2327   * @param {boolean} options.allowAutofill
   2328   *   Whether or not to allow providers to include autofill results.
   2329   * @param {number} [options.userContextId]
   2330   *   The container id where this context was generated, if any.
   2331   * @param {string | null} [options.tabGroup]
   2332   *   The tab group where this context was generated, if any.
   2333   * @param {Array} [options.sources]
   2334   *   A list of acceptable UrlbarUtils.RESULT_SOURCE for the context.
   2335   * @param {object} [options.searchMode]
   2336   *   The input's current search mode.  See UrlbarInput.setSearchMode for a
   2337   *   description.
   2338   * @param {boolean} [options.prohibitRemoteResults]
   2339   *   This provides a short-circuit override for `context.allowRemoteResults`.
   2340   *   If it's false, then `allowRemoteResults` will do its usual checks to
   2341   *   determine whether remote results are allowed. If it's true, then
   2342   *   `allowRemoteResults` will immediately return false. Defaults to false.
   2343   */
   2344  constructor(options) {
   2345    // Clone to make sure all properties belong to the system realm.
   2346    // This is required because this method is called from a window.
   2347    // Not doing this causes a window leak if providers don't properly
   2348    // clean up after a query and keep references to UrlbarQueryContext
   2349    // properties (e.g. ProviderPlaces).
   2350    options = structuredClone(options);
   2351 
   2352    this._checkRequiredOptions(options, [
   2353      "allowAutofill",
   2354      "isPrivate",
   2355      "maxResults",
   2356      "sapName",
   2357      "searchString",
   2358    ]);
   2359 
   2360    if (isNaN(options.maxResults)) {
   2361      throw new Error(
   2362        `Invalid maxResults property provided to UrlbarQueryContext`
   2363      );
   2364    }
   2365 
   2366    /**
   2367     * @type {[string, (v: any) => boolean, any?][]}
   2368     */
   2369    const optionalProperties = [
   2370      ["currentPage", v => typeof v == "string" && !!v.length],
   2371      ["prohibitRemoteResults", () => true, false],
   2372      ["providers", v => Array.isArray(v) && !!v.length],
   2373      ["searchMode", v => v && typeof v == "object"],
   2374      ["sources", v => Array.isArray(v) && !!v.length],
   2375    ];
   2376 
   2377    // Manage optional properties of options.
   2378    for (let [prop, checkFn, defaultValue] of optionalProperties) {
   2379      if (prop in options) {
   2380        if (!checkFn(options[prop])) {
   2381          throw new Error(`Invalid value for option "${prop}"`);
   2382        }
   2383        this[prop] = options[prop];
   2384      } else if (defaultValue !== undefined) {
   2385        this[prop] = defaultValue;
   2386      }
   2387    }
   2388 
   2389    this.lastResultCount = 0;
   2390    // Note that Set is not serializable through JSON, so these may not be
   2391    // easily shared with add-ons.
   2392    this.pendingHeuristicProviders = new Set();
   2393    this.deferUserSelectionProviders = new Set();
   2394    this.trimmedSearchString = this.searchString.trim();
   2395    this.lowerCaseSearchString = this.searchString.toLowerCase();
   2396    this.trimmedLowerCaseSearchString = this.trimmedSearchString.toLowerCase();
   2397    this.userContextId =
   2398      lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
   2399        options.userContextId,
   2400        this.isPrivate
   2401      ) || Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
   2402    this.tabGroup = options.tabGroup || null;
   2403 
   2404    // Used to store glean timing distribution timer ids.
   2405    this.firstTimerId = 0;
   2406    this.sixthTimerId = 0;
   2407  }
   2408 
   2409  /**
   2410   * @type {boolean}
   2411   *   Whether or not to allow providers to include autofill results.
   2412   */
   2413  allowAutofill;
   2414 
   2415  /**
   2416   * @type {boolean}
   2417   *   Whether or not the query has been cancelled.
   2418   */
   2419  canceled = false;
   2420 
   2421  /**
   2422   * @type {string}
   2423   *   URL of the page that was loaded when the search began.
   2424   */
   2425  currentPage;
   2426 
   2427  /**
   2428   * @type {UrlbarResult}
   2429   *   The current firstResult.
   2430   */
   2431  firstResult;
   2432 
   2433  /**
   2434   * @type {boolean}
   2435   *   Indicates if the first result has been changed changed.
   2436   */
   2437  firstResultChanged = false;
   2438 
   2439  /**
   2440   * @type {UrlbarResult}
   2441   *   The heuristic result associated with the context.
   2442   */
   2443  heuristicResult;
   2444 
   2445  /**
   2446   * @type {boolean}
   2447   *   True if this query was started from a private browsing window.
   2448   */
   2449  isPrivate;
   2450 
   2451  /**
   2452   * @type {number}
   2453   *   The maximum number of results that will be displayed for this query.
   2454   */
   2455  maxResults;
   2456 
   2457  /**
   2458   * @type {string}
   2459   *   The name of the muxer to use for this query.
   2460   */
   2461  muxer;
   2462 
   2463  /**
   2464   * @type {boolean}
   2465   *   Whether or not to prohibit remote results.
   2466   */
   2467  prohibitRemoteResults;
   2468 
   2469  /**
   2470   * @type {string[]}
   2471   *   List of registered provider names. Providers can be registered through
   2472   *   the UrlbarProvidersManager.
   2473   */
   2474  providers;
   2475 
   2476  /**
   2477   * @type {?Values<typeof UrlbarUtils.RESULT_SOURCE>}
   2478   *   Set if this context is restricted to a single source.
   2479   */
   2480  restrictSource;
   2481 
   2482  /**
   2483   * @type {UrlbarSearchStringTokenData}
   2484   *   The restriction token used to restrict the sources for this search.
   2485   */
   2486  restrictToken;
   2487 
   2488  /**
   2489   * @type {UrlbarResult[]}
   2490   *   The results associated with this context.
   2491   */
   2492  results;
   2493 
   2494  /**
   2495   * @type {string}
   2496   *   The search access point name of the UrlbarInput for use with telemetry or
   2497   *   logging, e.g. `urlbar`, `searchbar`.
   2498   */
   2499  sapName;
   2500 
   2501  /**
   2502   * @type {UrlbarSearchModeData}
   2503   *   Details about the search mode associated with this context.
   2504   */
   2505  searchMode;
   2506 
   2507  /**
   2508   * @type {string}
   2509   *   The string the user entered in autocomplete.
   2510   */
   2511  searchString;
   2512 
   2513  /**
   2514   * @type {Values<typeof UrlbarUtils.RESULT_SOURCE>[]}
   2515   *   The possible sources of results for this context.
   2516   */
   2517  sources;
   2518 
   2519  /**
   2520   * @type {UrlbarSearchStringTokenData[]}
   2521   *   A list of tokens extracted from the search string.
   2522   */
   2523  tokens;
   2524 
   2525  /**
   2526   * Checks the required options, saving them as it goes.
   2527   *
   2528   * @param {object} options The options object to check.
   2529   * @param {Array} optionNames The names of the options to check for.
   2530   * @throws {Error} Throws if there is a missing option.
   2531   */
   2532  _checkRequiredOptions(options, optionNames) {
   2533    for (let optionName of optionNames) {
   2534      if (!(optionName in options)) {
   2535        throw new Error(
   2536          `Missing or empty ${optionName} provided to UrlbarQueryContext`
   2537        );
   2538      }
   2539      this[optionName] = options[optionName];
   2540    }
   2541  }
   2542 
   2543  /**
   2544   * Caches and returns fixup info from URIFixup for the current search string.
   2545   * Only returns a subset of the properties from URIFixup. This is both to
   2546   * reduce the memory footprint of UrlbarQueryContexts and to keep them
   2547   * serializable so they can be sent to extensions.
   2548   */
   2549  get fixupInfo() {
   2550    if (!this._fixupError && !this._fixupInfo && this.trimmedSearchString) {
   2551      let flags =
   2552        Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
   2553        Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
   2554      if (this.isPrivate) {
   2555        flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
   2556      }
   2557 
   2558      try {
   2559        let info = Services.uriFixup.getFixupURIInfo(this.searchString, flags);
   2560 
   2561        this._fixupInfo = {
   2562          href: info.fixedURI.spec,
   2563          isSearch: !!info.keywordAsSent,
   2564          scheme: info.fixedURI.scheme,
   2565        };
   2566      } catch (ex) {
   2567        this._fixupError = ex.result;
   2568      }
   2569    }
   2570 
   2571    return this._fixupInfo || null;
   2572  }
   2573 
   2574  /**
   2575   * Returns the error that was thrown when fixupInfo was fetched, if any. If
   2576   * fixupInfo has not yet been fetched for this queryContext, it is fetched
   2577   * here.
   2578   *
   2579   * @returns {any?}
   2580   */
   2581  get fixupError() {
   2582    if (!this.fixupInfo) {
   2583      return this._fixupError;
   2584    }
   2585 
   2586    return null;
   2587  }
   2588 
   2589  /**
   2590   * Returns whether results from remote services are generally allowed for the
   2591   * context. Callers can impose further restrictions as appropriate, but
   2592   * typically they should not fetch remote results if this returns false.
   2593   *
   2594   * @param {string} [searchString]
   2595   *   Usually this is just the context's search string, but if you need to
   2596   *   fetch remote results based on a modified version, you can pass it here.
   2597   * @param {boolean} [allowEmptySearchString]
   2598   *   Whether to check for the minimum length of the search string.
   2599   * @returns {boolean}
   2600   *   Whether remote results are allowed.
   2601   */
   2602  allowRemoteResults(
   2603    searchString = this.searchString,
   2604    allowEmptySearchString = false
   2605  ) {
   2606    if (this.prohibitRemoteResults) {
   2607      return false;
   2608    }
   2609 
   2610    // We're unlikely to get useful remote results for a single character.
   2611    if (
   2612      searchString.length < 2 &&
   2613      !(!searchString.length && allowEmptySearchString)
   2614    ) {
   2615      return false;
   2616    }
   2617 
   2618    // Prohibit remote results if the search string is likely an origin to avoid
   2619    // disclosing sites the user visits. If the search string may or may not be
   2620    // an origin but we've determined a search is allowed, then allow it.
   2621    if (this.tokens.length == 1) {
   2622      switch (this.tokens[0].type) {
   2623        case lazy.UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN:
   2624          return false;
   2625        case lazy.UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN_BUT_SEARCH_ALLOWED:
   2626          return true;
   2627      }
   2628    }
   2629 
   2630    // Disallow remote results for strings containing tokens that look like URIs
   2631    // to avoid disclosing information about networks and passwords.
   2632    // (Unless the search is happening in the searchbar.)
   2633    if (
   2634      this.sapName != "searchbar" &&
   2635      this.fixupInfo?.href &&
   2636      !this.fixupInfo?.isSearch
   2637    ) {
   2638      return false;
   2639    }
   2640 
   2641    // Allow remote results.
   2642    return true;
   2643  }
   2644 }
   2645 
   2646 /**
   2647 * Base class for a muxer.
   2648 * The muxer scope is to sort a given list of results.
   2649 */
   2650 export class UrlbarMuxer {
   2651  /**
   2652   * Unique name for the muxer, used by the context to sort results.
   2653   * Not using a unique name will cause the newest registration to win.
   2654   *
   2655   * @abstract
   2656   */
   2657  get name() {
   2658    return "UrlbarMuxerBase";
   2659  }
   2660 
   2661  /**
   2662   * Sorts queryContext results in-place.
   2663   *
   2664   * @param {UrlbarQueryContext} _queryContext the context to sort results for.
   2665   * @param {Array} _unsortedResults
   2666   *   The array of UrlbarResult that is not sorted yet.
   2667   * @abstract
   2668   */
   2669  sort(_queryContext, _unsortedResults) {
   2670    throw new Error("Trying to access the base class, must be overridden");
   2671  }
   2672 }
   2673 
   2674 /**
   2675 * Base class for a provider.
   2676 * The provider scope is to query a datasource and return results from it.
   2677 */
   2678 export class UrlbarProvider {
   2679  #lazy = XPCOMUtils.declareLazy({
   2680    logger: () => UrlbarUtils.getLogger({ prefix: `Provider.${this.name}` }),
   2681  });
   2682 
   2683  get logger() {
   2684    return this.#lazy.logger;
   2685  }
   2686 
   2687  /**
   2688   * Unique name for the provider, used by the context to filter on providers.
   2689   * By default, it will use the class name but it can also be overridden to
   2690   * use a different name.
   2691   * Not using a unique name will cause the newest registration to win.
   2692   */
   2693  get name() {
   2694    return this.constructor.name;
   2695  }
   2696 
   2697  /**
   2698   * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
   2699   *
   2700   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
   2701   * @abstract
   2702   */
   2703  get type() {
   2704    throw new Error("Trying to access the base class, must be overridden");
   2705  }
   2706 
   2707  /**
   2708   * @type {Query}
   2709   *   This can be used by the provider to check the query is still running
   2710   *   after executing async tasks:
   2711   *
   2712   * ```
   2713   *   let instance = this.queryInstance;
   2714   *   await ...
   2715   *   if (instance != this.queryInstance) {
   2716   *     // Query was canceled or a new one started.
   2717   *     return;
   2718   *   }
   2719   * ```
   2720   */
   2721  queryInstance;
   2722 
   2723  /**
   2724   * Calls a method on the provider in a try-catch block and reports any error.
   2725   * Unlike most other provider methods, `tryMethod` is not intended to be
   2726   * overridden.
   2727   *
   2728   * @param {string} methodName The name of the method to call.
   2729   * @param {*} args The method arguments.
   2730   * @returns {*} The return value of the method, or undefined if the method
   2731   *          throws an error.
   2732   * @abstract
   2733   */
   2734  tryMethod(methodName, ...args) {
   2735    try {
   2736      return this[methodName](...args);
   2737    } catch (ex) {
   2738      console.error(ex);
   2739    }
   2740    return undefined;
   2741  }
   2742 
   2743  /**
   2744   * Whether this provider should be invoked for the given context.
   2745   * If this method returns false, the providers manager won't start a query
   2746   * with this provider, to save on resources.
   2747   *
   2748   * @param {UrlbarQueryContext} [_queryContext]
   2749   *   The query context object
   2750   * @param {UrlbarController} [_controller]
   2751   *   The current controller.
   2752   * @returns {Promise<boolean>}
   2753   *   Whether this provider should be invoked for the search.
   2754   * @abstract
   2755   */
   2756  async isActive(_queryContext, _controller) {
   2757    throw new Error("Trying to access the base class, must be overridden");
   2758  }
   2759 
   2760  /**
   2761   * Gets the provider's priority.  Priorities are numeric values starting at
   2762   * zero and increasing in value.  Smaller values are lower priorities, and
   2763   * larger values are higher priorities.  For a given query, `startQuery` is
   2764   * called on only the active and highest-priority providers.
   2765   *
   2766   * @param {UrlbarQueryContext} _queryContext The query context object
   2767   * @returns {number} The provider's priority for the given query.
   2768   * @abstract
   2769   */
   2770  getPriority(_queryContext) {
   2771    // By default, all providers share the lowest priority.
   2772    return 0;
   2773  }
   2774 
   2775  /**
   2776   * Starts querying.
   2777   *
   2778   * Note: Extended classes should return a Promise resolved when the provider
   2779   *       is done searching AND returning results.
   2780   *
   2781   * @param {UrlbarQueryContext} _queryContext
   2782   *   The query context object
   2783   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} _addCallback
   2784   *   Callback invoked by the provider to add a new result.
   2785   * @returns {void|Promise<void>}
   2786   * @abstract
   2787   */
   2788  startQuery(_queryContext, _addCallback) {
   2789    throw new Error("Trying to access the base class, must be overridden");
   2790  }
   2791 
   2792  /**
   2793   * Cancels a running query,
   2794   *
   2795   * @param {UrlbarQueryContext} _queryContext the query context object to cancel
   2796   *        query for.
   2797   * @abstract
   2798   */
   2799  cancelQuery(_queryContext) {
   2800    // Override this with your clean-up on cancel code.
   2801  }
   2802 
   2803  // The following `on{Event}` notification methods are invoked only when
   2804  // defined, thus there is no base class implementation for them
   2805  /**
   2806   * Called when a user engages with a result in the urlbar. This is called for
   2807   * all providers who have implemented this method.
   2808   *
   2809   * @param {UrlbarQueryContext} _queryContext
   2810   *   The engagement's query context. It will always be defined for
   2811   *   "engagement" and "abandonment".
   2812   * @param {UrlbarController} _controller
   2813   *  The associated controller.
   2814   * @param {object} _details
   2815   *   This object is non-empty only when `state` is "engagement" or
   2816   *   "abandonment", and it describes the search string and engaged result.
   2817   *
   2818   *   For "engagement", it has the following properties:
   2819   *
   2820   *   {UrlbarResult} result
   2821   *       The engaged result. If a result itself was picked, this will be it.
   2822   *       If an element related to a result was picked (like a button or menu
   2823   *       command), this will be that result. This property will be present if
   2824   *       and only if `state` == "engagement", so it can be used to quickly
   2825   *       tell when the user engaged with a result.
   2826   *   {Element} element
   2827   *       The picked DOM element.
   2828   *   {boolean} isSessionOngoing
   2829   *       True if the search session remains ongoing or false if the engagement
   2830   *       ended it. Typically picking a result ends the session but not always.
   2831   *       Picking a button or menu command may not end the session; dismissals
   2832   *       do not, for example.
   2833   *   {string} searchString
   2834   *       The search string for the engagement's query.
   2835   *   {number} selIndex
   2836   *       The index of the picked result.
   2837   *   {string} selType
   2838   *       The type of the selected result.  See TelemetryEvent.record() in
   2839   *       UrlbarController.sys.mjs.
   2840   *   {string} provider
   2841   *       The name of the provider that produced the picked result.
   2842   *
   2843   *   For "abandonment", only `searchString` is defined.
   2844   *
   2845   * onEngagement(_queryContext, _controller, _details) {}
   2846   */
   2847 
   2848  /**
   2849   * Called when the user abandons a search session without selecting a result.
   2850   * This could be due to losing focus on the urlbar, switching tabs, or other
   2851   * actions that imply the user is no longer actively engaging with the search
   2852   * suggestions. The method is called for all providers who have implemented
   2853   * this method and whose results were visible at the time of the abandonment.
   2854   *
   2855   * @param {UrlbarQueryContext} _queryContext
   2856   *    The query context at the time of abandonment.
   2857   * @param {UrlbarController} _controller
   2858   * The associated controller.
   2859   *
   2860   * onAbandonment(_queryContext, _controller) {}
   2861   */
   2862 
   2863  /**
   2864   * Called for providers whose results are visible at the time of either
   2865   * engagement or abandonment. The method is called when a user actively
   2866   * interacts with a search result. This interaction could be clicking on a
   2867   * suggestion, using a keyboard to select a suggestion, or any other form of
   2868   * direct engagement with the results displayed. It is also called
   2869   * when a user decides to abandon the search session without engaging with any
   2870   * of the presented results. This is called for all providers who have
   2871   * implemented this method.
   2872   *
   2873   * @param {string} _state
   2874   *    The state of the user interaction, either "engagement" or "abandonment".
   2875   * @param {UrlbarQueryContext} _queryContext
   2876   *    The current query context.
   2877   * @param {UrlbarController} _controller
   2878   *    The associated controller.
   2879   * @param {Array} _providerVisibleResults
   2880   *    Array of visible results at the time of either an engagement or
   2881   *    abandonment event relevant to the provider. Each object in the array
   2882   *    contains:
   2883   *    - `index`: The position of the visible result within the original list
   2884   *               visible results.
   2885   *    - `result`: The visible result itself
   2886   * @param {object|null} _details
   2887   *    If the impression is due to an engagement, this will be the `details`
   2888   *    object that's also passed to `onEngagement()`. Otherwise it will be
   2889   *    null. See `onEngagement()` documentation for info.
   2890   *
   2891   * onImpression(_state, _queryContext, _controller, _providerVisibleResults, _details)
   2892   * {}
   2893   */
   2894 
   2895  /**
   2896   * Called when a search session concludes regardless of how it ends -
   2897   * whether through engagement or abandonment or otherwise. This is
   2898   * called for all providers who have implemented this method.
   2899   *
   2900   * @param {UrlbarQueryContext} _queryContext
   2901   *    The current query context.
   2902   * @param {UrlbarController} _controller
   2903   *    The associated controller.
   2904   *
   2905   * onSearchSessionEnd(_queryContext, _controller) {}
   2906   */
   2907 
   2908  /**
   2909   * Called before a result from the provider is selected. See `onSelection`
   2910   * for details on what that means.
   2911   *
   2912   * @param {UrlbarResult} _result
   2913   *   The result that was selected.
   2914   * @param {Element} _element
   2915   *   The element in the result's view that was selected.
   2916   * @abstract
   2917   */
   2918  onBeforeSelection(_result, _element) {}
   2919 
   2920  /**
   2921   * Called when a result from the provider is selected. "Selected" refers to
   2922   * the user highlighing the result with the arrow keys/Tab, before it is
   2923   * picked. onSelection is also called when a user clicks a result. In the
   2924   * event of a click, onSelection is called just before onEngagement. Note that
   2925   * this is called when heuristic results are pre-selected.
   2926   *
   2927   * @param {UrlbarResult} _result
   2928   *   The result that was selected.
   2929   * @param {Element} _element
   2930   *   The element in the result's view that was selected.
   2931   * @abstract
   2932   */
   2933  onSelection(_result, _element) {}
   2934 
   2935  /**
   2936   * This is called only for dynamic result types, when the urlbar view updates
   2937   * the view of one of the results of the provider.  It should return an object
   2938   * describing the view update that looks like this:
   2939   *
   2940   *   {
   2941   *     nodeNameFoo: {
   2942   *       attributes: {
   2943   *         someAttribute: someValue,
   2944   *       },
   2945   *       style: {
   2946   *         someStyleProperty: someValue,
   2947   *         "another-style-property": someValue,
   2948   *       },
   2949   *       l10n: {
   2950   *         id: someL10nId,
   2951   *         args: someL10nArgs,
   2952   *       },
   2953   *       textContent: "some text content",
   2954   *     },
   2955   *     nodeNameBar: {
   2956   *       ...
   2957   *     },
   2958   *     nodeNameBaz: {
   2959   *       ...
   2960   *     },
   2961   *   }
   2962   *
   2963   * The object should contain a property for each element to update in the
   2964   * dynamic result type view.  The names of these properties are the names
   2965   * declared in the view template of the dynamic result type; see
   2966   * UrlbarView.addDynamicViewTemplate().  The values are similar to the nested
   2967   * objects specified in the view template but not quite the same; see below.
   2968   * For each property, the element in the view subtree with the specified name
   2969   * is updated according to the object in the property's value.  If an
   2970   * element's name is not specified, then it will not be updated and will
   2971   * retain its current state.
   2972   *
   2973   * @param {UrlbarResult} _result
   2974   *   The result whose view will be updated.
   2975   * @param {Map} _idsByName
   2976   *   A Map from an element's name, as defined by the provider; to its ID in
   2977   *   the DOM, as defined by the browser. The browser manages element IDs for
   2978   *   dynamic results to prevent collisions. However, a provider may need to
   2979   *   access the IDs of the elements created for its results. For example, to
   2980   *   set various `aria` attributes.
   2981   * @returns {object}
   2982   *   A view update object as described above.  The names of properties are the
   2983   *   the names of elements declared in the view template.  The values of
   2984   *   properties are objects that describe how to update each element, and
   2985   *   these objects may include the following properties, all of which are
   2986   *   optional:
   2987   *
   2988   *   {object} [attributes]
   2989   *     A mapping from attribute names to values.  Each name-value pair results
   2990   *     in an attribute being added to the element.  The `id` attribute is
   2991   *     reserved and cannot be set by the provider.
   2992   *   {Array} [classList]
   2993   *     An array of CSS classes to set on the element. If this is defined, the
   2994   *     element's previous classes will be cleared first!
   2995   *   {object} [dataset]
   2996   *     Maps element dataset keys to values. Values should be strings with the
   2997   *     following exceptions: `undefined` is ignored, and `null` causes the key
   2998   *     to be removed from the dataset.
   2999   *   {object} [style]
   3000   *     A plain object that can be used to add inline styles to the element,
   3001   *     like `display: none`.   `element.style` is updated for each name-value
   3002   *     pair in this object.
   3003   *   {object} [l10n]
   3004   *     An { id, args } object that will be passed to
   3005   *     document.l10n.setAttributes().
   3006   *   {string} [textContent]
   3007   *     A string that will be set as `element.textContent`.
   3008   */
   3009  getViewUpdate(_result, _idsByName) {
   3010    return null;
   3011  }
   3012 
   3013  /**
   3014   * Gets the list of commands that should be shown in the result menu for a
   3015   * given result from the provider. All commands returned by this method should
   3016   * be handled by implementing `onEngagement()` with the possible exception of
   3017   * commands automatically handled by the urlbar, like "help".
   3018   *
   3019   * @param {UrlbarResult} _result
   3020   *   The menu will be shown for this result.
   3021   * @returns {?UrlbarResultCommand[]}
   3022   */
   3023  getResultCommands(_result) {
   3024    return null;
   3025  }
   3026 
   3027  /**
   3028   * Defines whether the view should defer user selection events while waiting
   3029   * for the first result from this provider.
   3030   *
   3031   * Note: UrlbarEventBufferer has a timeout after which user events will be
   3032   *       processed regardless.
   3033   *
   3034   * @returns {boolean} Whether the provider wants to defer user selection
   3035   *          events.
   3036   * @see {@link UrlbarEventBufferer}
   3037   */
   3038  get deferUserSelection() {
   3039    return false;
   3040  }
   3041 }
   3042 
   3043 /**
   3044 * Class used to create a timer that can be manually fired, to immediately
   3045 * invoke the callback, or canceled, as necessary.
   3046 * Examples:
   3047 *   let timer = new SkippableTimer();
   3048 *   // Invokes the callback immediately without waiting for the delay.
   3049 *   await timer.fire();
   3050 *   // Cancel the timer, the callback won't be invoked.
   3051 *   await timer.cancel();
   3052 *   // Wait for the timer to have elapsed.
   3053 *   await timer.promise;
   3054 */
   3055 export class SkippableTimer {
   3056  /**
   3057   * This can be used to track whether the timer completed.
   3058   */
   3059  done = false;
   3060 
   3061  /**
   3062   * Creates a skippable timer for the given callback and time.
   3063   *
   3064   * @param {object} [options] An object that configures the timer
   3065   * @param {string} [options.name] The name of the timer, logged when necessary
   3066   * @param {Function} [options.callback] To be invoked when requested
   3067   * @param {number} [options.time] A delay in milliseconds to wait for
   3068   * @param {boolean} [options.reportErrorOnTimeout] If true and the timer times
   3069   *                  out, an error will be logged with Cu.reportError
   3070   * @param {ConsoleInstance} [options.logger] An optional logger
   3071   */
   3072  constructor({
   3073    name = "<anonymous timer>",
   3074    callback = null,
   3075    time = 0,
   3076    reportErrorOnTimeout = false,
   3077    logger = null,
   3078  } = {}) {
   3079    this.name = name;
   3080    this.logger = logger;
   3081 
   3082    let timerPromise = new Promise(resolve => {
   3083      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   3084      this._timer.initWithCallback(
   3085        () => {
   3086          this._log(`Timed out!`, reportErrorOnTimeout);
   3087          this.done = true;
   3088          this._timer = null;
   3089          resolve();
   3090        },
   3091        time,
   3092        Ci.nsITimer.TYPE_ONE_SHOT
   3093      );
   3094      this._log(`Started`);
   3095    });
   3096 
   3097    let firePromise = new Promise(resolve => {
   3098      this.fire = async () => {
   3099        this.done = true;
   3100        if (this._timer) {
   3101          if (!this._canceled) {
   3102            this._log(`Skipped`);
   3103          }
   3104          this._timer.cancel();
   3105          this._timer = null;
   3106          resolve();
   3107        }
   3108        await this.promise;
   3109      };
   3110    });
   3111 
   3112    this.promise = Promise.race([timerPromise, firePromise]).then(() => {
   3113      // If we've been canceled, don't call back.
   3114      if (callback && !this._canceled) {
   3115        callback();
   3116      }
   3117    });
   3118  }
   3119 
   3120  /**
   3121   * Allows to cancel the timer and the callback won't be invoked.
   3122   * It is not strictly necessary to await for this, the promise can just be
   3123   * used to ensure all the internal work is complete.
   3124   */
   3125  async cancel() {
   3126    if (this._timer) {
   3127      this._log(`Canceling`);
   3128      this._canceled = true;
   3129    }
   3130    await this.fire();
   3131  }
   3132 
   3133  _log(msg, isError = false) {
   3134    let line = `SkippableTimer :: ${this.name} :: ${msg}`;
   3135    if (this.logger) {
   3136      this.logger.debug(line);
   3137    }
   3138    if (isError) {
   3139      console.error(line);
   3140    }
   3141  }
   3142 }
   3143 
   3144 /**
   3145 * @typedef L10nCachedMessage
   3146 *   A cached L10n message object is similar to `L10nMessage` (defined in
   3147 *   Localization.webidl) but its attributes are stored differently for
   3148 *   convenience.
   3149 *
   3150 *   For example, if we cache these strings from an ftl file:
   3151 *
   3152 *     foo = Foo's value
   3153 *     bar =
   3154 *       .label = Bar's label value
   3155 *
   3156 *   Then:
   3157 *
   3158 *     cache.get("foo")
   3159 *     // => { value: "Foo's value", attributes: null }
   3160 *     cache.get("bar")
   3161 *     // => { value: null, attributes: { label: "Bar's label value" }}
   3162 * @property {string} [value]
   3163 *   The bare value of the string. If the string does not have a bare value
   3164 *   (i.e., it has only attributes), this will be null.
   3165 * @property {{[key: string]: string}|null} [attributes]
   3166 *   A mapping from attribute names to their values. If the string doesn't have
   3167 *   any attributes, this will be null.
   3168 */
   3169 
   3170 /**
   3171 * This class implements a cache for l10n strings. Cached strings can be
   3172 * accessed synchronously, avoiding the asynchronicity of `data-l10n-id` and
   3173 * `document.l10n.setAttributes`, which can lead to text pop-in and flickering
   3174 * as strings are fetched from Fluent. (`document.l10n.formatValueSync` is also
   3175 * sync but should not be used since it may perform sync I/O.)
   3176 *
   3177 * Values stored and returned by the cache are JS objects similar to
   3178 * `L10nMessage` objects, not bare strings. This allows the cache to store not
   3179 * only l10n strings with bare values but also strings that define attributes
   3180 * (e.g., ".label = My label value"). See `get` for details.
   3181 *
   3182 * The cache stores up to `MAX_ENTRIES_PER_ID` entries per l10n ID, and entries
   3183 * are sorted from least recently cached to most recently cached. This only
   3184 * matters for strings that have arguments. For strings that don't have
   3185 * arguments, there can be only one cached value, so there can be only one cache
   3186 * entry. But for strings that do have arguments, their cached values depend on
   3187 * the arguments they were cached with. The cache will store up to
   3188 * `MAX_ENTRIES_PER_ID` of the most recently cached values for a given l10n ID.
   3189 *
   3190 * For example, given the following string from an ftl file:
   3191 *
   3192 *   foo = My arg value is { $bar }
   3193 *
   3194 * And the following cache calls:
   3195 *
   3196 *   cache.add({ id: "foo", args: { bar: "aaa" }});
   3197 *   cache.add({ id: "foo", args: { bar: "bbb" }});
   3198 *   cache.add({ id: "foo", args: { bar: "ccc" }});
   3199 *
   3200 * Then three different versions of the "foo" string will be cached, from least
   3201 * recently cached to most recently cached:
   3202 *
   3203 *   "My arg value is aaa"
   3204 *   "My arg value is bbb"
   3205 *   "My arg value is ccc"
   3206 *
   3207 * If `MAX_ENTRIES_PER_ID` is 3 and we cache a fourth version like this:
   3208 *
   3209 *   cache.add({ id: "foo", args: { bar: "zzz" }});
   3210 *
   3211 * Then the least recently cached version -- the "aaa" one -- will be evicted
   3212 * from the cache, and the remaining cached versions will be:
   3213 *
   3214 *   "My arg value is bbb"
   3215 *   "My arg value is ccc"
   3216 *   "My arg value is zzz"
   3217 */
   3218 export class L10nCache {
   3219  static MAX_ENTRIES_PER_ID = 5;
   3220 
   3221  /**
   3222   * @param {Localization} l10n
   3223   *   A `Localization` object like `document.l10n`. This class keeps a weak
   3224   *   reference to this object, so the caller or something else must hold onto
   3225   *   it.
   3226   */
   3227  constructor(l10n) {
   3228    this.l10n = Cu.getWeakReference(l10n);
   3229    this.QueryInterface = ChromeUtils.generateQI([
   3230      "nsIObserver",
   3231      "nsISupportsWeakReference",
   3232    ]);
   3233    Services.obs.addObserver(this, "intl:app-locales-changed", true);
   3234  }
   3235 
   3236  /**
   3237   * Gets a cached l10n message.
   3238   *
   3239   * @param {object} options
   3240   *   Options
   3241   * @param {string} options.id
   3242   *   The string's Fluent ID.
   3243   * @param {object} [options.args]
   3244   *   The Fluent arguments as passed to `l10n.setAttributes`. Required if the
   3245   *   l10n string has arguments.
   3246   * @returns {L10nCachedMessage|null}
   3247   *   The cached message or null if it's not cached.
   3248   */
   3249  get({ id, args = undefined }) {
   3250    return this.#messagesByArgsById.get(id)?.get(this.#argsKey(args)) ?? null;
   3251  }
   3252 
   3253  /**
   3254   * Fetches a string from Fluent and caches it.
   3255   *
   3256   * @param {object} options
   3257   *   Options
   3258   * @param {string} options.id
   3259   *   The string's Fluent ID.
   3260   * @param {object} [options.args]
   3261   *   The Fluent arguments as passed to `l10n.setAttributes`. Required if the
   3262   *   l10n string has arguments.
   3263   */
   3264  async add({ id, args = undefined }) {
   3265    let l10n = this.l10n.get();
   3266    if (!l10n) {
   3267      return;
   3268    }
   3269 
   3270    let messages = await l10n.formatMessages([{ id, args }]);
   3271    if (!messages?.length) {
   3272      console.error(
   3273        "l10n.formatMessages returned an unexpected value for ID: ",
   3274        id
   3275      );
   3276      return;
   3277    }
   3278 
   3279    /** @type {L10nCachedMessage} */
   3280    let message = { value: messages[0].value, attributes: null };
   3281    if (messages[0].attributes) {
   3282      // Convert `attributes` from an array of `{ name, value }` objects to one
   3283      // object mapping names to values.
   3284      message.attributes = messages[0].attributes.reduce(
   3285        (valuesByName, { name, value }) => {
   3286          valuesByName[name] = value;
   3287          return valuesByName;
   3288        },
   3289        {}
   3290      );
   3291    }
   3292 
   3293    this.#update({ id, args, message });
   3294  }
   3295 
   3296  /**
   3297   * Ensures that a string is the most recently cached for its ID. If the string
   3298   * is not already cached, then it's fetched from Fluent. This is just a slight
   3299   * optimization over `add` that avoids calling into Fluent unnecessarily.
   3300   *
   3301   * @param {object} options
   3302   *   Options
   3303   * @param {string} options.id
   3304   *   The string's Fluent ID.
   3305   * @param {object} [options.args]
   3306   *   The Fluent arguments as passed to `l10n.setAttributes`. Required if the
   3307   *   l10n string has arguments.
   3308   */
   3309  async ensure({ id, args = undefined }) {
   3310    let message = this.get({ id, args });
   3311    if (message) {
   3312      await this.#update({ id, args, message });
   3313    } else {
   3314      await this.add({ id, args });
   3315    }
   3316  }
   3317 
   3318  /**
   3319   * A version of `ensure` that ensures multiple strings are cached at once.
   3320   *
   3321   * @param {object[]} objects
   3322   *   An array of objects as passed to `ensure()`.
   3323   */
   3324  async ensureAll(objects) {
   3325    let promises = [];
   3326    for (let obj of objects) {
   3327      promises.push(this.ensure(obj));
   3328    }
   3329    await Promise.all(promises);
   3330  }
   3331 
   3332  /**
   3333   * Removes a cached string.
   3334   *
   3335   * @param {object} options
   3336   *   Options
   3337   * @param {string} options.id
   3338   *   The string's Fluent ID.
   3339   * @param {object} [options.args]
   3340   *   The Fluent arguments as passed to `l10n.setAttributes`. Required if the
   3341   *   l10n string has arguments.
   3342   */
   3343  delete({ id, args = undefined }) {
   3344    let messagesByArgs = this.#messagesByArgsById.get(id);
   3345    if (messagesByArgs) {
   3346      messagesByArgs.delete(this.#argsKey(args));
   3347      if (!messagesByArgs.size) {
   3348        this.#messagesByArgsById.delete(id);
   3349      }
   3350    }
   3351  }
   3352 
   3353  /**
   3354   * Removes all cached strings.
   3355   */
   3356  clear() {
   3357    this.#messagesByArgsById.clear();
   3358  }
   3359 
   3360  /**
   3361   * Returns the number of cached messages.
   3362   */
   3363  size() {
   3364    return this.#messagesByArgsById
   3365      .values()
   3366      .reduce((total, messagesByArg) => total + messagesByArg.size, 0);
   3367  }
   3368 
   3369  /**
   3370   * Sets an element's content or attribute to a cached l10n string. If the
   3371   * string isn't cached, then this falls back to the usual
   3372   * `document.l10n.setAttributes()` using the given l10n ID and args, which
   3373   * means the string will pop in on a later animation frame.
   3374   *
   3375   * This also caches the string so that it will be ready the next time. It
   3376   * returns a promise that will be resolved when the string has been cached.
   3377   * Typically there's no need to await it unless you want to be sure the string
   3378   * is cached before continuing.
   3379   *
   3380   * @param {Element} element
   3381   *   The l10n string will be applied to this element.
   3382   * @param {object} options
   3383   *   Options object.
   3384   * @param {string} options.id
   3385   *   The l10n string ID.
   3386   * @param {object} [options.args]
   3387   *   The l10n string arguments.
   3388   * @param {object} [options.argsHighlights]
   3389   *   If this is set, apply substring highlighting to the corresponding l10n
   3390   *   arguments in `args`. Each value in this object should be an array of
   3391   *   highlights as returned by `UrlbarUtils.getTokenMatches()` or
   3392   *   `UrlbarResult.getDisplayableValueAndHighlights()`.
   3393   * @param {string} [options.attribute]
   3394   *   If the string applies to an attribute on the element, pass the name of
   3395   *   the attribute. The string in the Fluent file should define a value for
   3396   *   the attribute, like ".foo = My value". If the string applies to the
   3397   *   element's content, leave this undefined.
   3398   * @param {boolean} [options.parseMarkup]
   3399   *   This controls whether the cached string is applied to the element's
   3400   *   `textContent` or its `innerHTML`. It's not relevant if the string is
   3401   *   applied to an attribute. Typically it should be set to true when the
   3402   *   string is expected to contain markup. When true, the cached string is
   3403   *   essentially assigned to the element's `innerHTML`. When false, it's
   3404   *   assigned to the element's `textContent`.
   3405   * @returns {Promise}
   3406   *   A promise that's resolved when the string has been cached. You can ignore
   3407   *   it and do not need to await it unless you want to make sure the string is
   3408   *   cached before continuing.
   3409   */
   3410  setElementL10n(
   3411    element,
   3412    {
   3413      id,
   3414      args = undefined,
   3415      argsHighlights = undefined,
   3416      attribute = undefined,
   3417      parseMarkup = false,
   3418    }
   3419  ) {
   3420    // If the message is cached, apply it to the element.
   3421    let message = this.get({ id, args });
   3422    if (message) {
   3423      if (message.attributes) {
   3424        for (let [name, value] of Object.entries(message.attributes)) {
   3425          element.setAttribute(name, value);
   3426        }
   3427      }
   3428      if (typeof message.value == "string") {
   3429        if (!parseMarkup) {
   3430          element.textContent = message.value;
   3431        } else {
   3432          element.innerHTML = "";
   3433          element.append(
   3434            lazy.parserUtils.parseFragment(
   3435              message.value,
   3436              Ci.nsIParserUtils.SanitizerDropNonCSSPresentation |
   3437                Ci.nsIParserUtils.SanitizerDropForms |
   3438                Ci.nsIParserUtils.SanitizerDropMedia,
   3439              false,
   3440              Services.io.newURI(element.ownerDocument.documentURI),
   3441              element
   3442            )
   3443          );
   3444        }
   3445      }
   3446    }
   3447 
   3448    // If the message isn't cached and args highlights were specified, apply
   3449    // them now.
   3450    if (!message && !attribute && argsHighlights) {
   3451      // To avoid contaminated args because we cache it, create a new instance.
   3452      args = { ...args };
   3453 
   3454      let span = element.ownerDocument.createElement("span");
   3455      for (let key in argsHighlights) {
   3456        UrlbarUtils.addTextContentWithHighlights(
   3457          span,
   3458          args[key],
   3459          argsHighlights[key]
   3460        );
   3461        args[key] = span.innerHTML;
   3462      }
   3463    }
   3464 
   3465    // If an attribute was passed in, make sure it's allowed to be localized by
   3466    // setting `data-l10n-attrs`. This isn't required for attrbutes already in
   3467    // the Fluent allowlist but it doesn't hurt.
   3468    if (attribute) {
   3469      element.setAttribute("data-l10n-attrs", attribute);
   3470    } else {
   3471      element.removeAttribute("data-l10n-attrs");
   3472    }
   3473 
   3474    // Set the l10n attributes. If the message wasn't cached, `DOMLocalization`
   3475    // will do its asynchronous translation and the text content will pop in. If
   3476    // the message was cached, then we already set the cached attributes and
   3477    // text content above, but we set the l10n attributes anyway because some
   3478    // tests rely on them being set. It shouldn't hurt anyway.
   3479    element.ownerDocument.l10n.setAttributes(element, id, args);
   3480 
   3481    // Cache the string. We specifically do not do this first and await it
   3482    // because the whole point of the l10n cache is to synchronously update the
   3483    // element's content when possible. Here, we return a promise rather than
   3484    // making this function async and awaiting so it's clearer to callers that
   3485    // they probably don't need to wait for caching to finish.
   3486    return this.ensure({ id, args });
   3487  }
   3488 
   3489  /**
   3490   * Removes content and attributes set by `setElementL10n()`.
   3491   *
   3492   * @param {Element} element
   3493   *   The content and attributes will be removed from this element.
   3494   * @param {object} [options]
   3495   *   Options object.
   3496   * @param {string} [options.attribute]
   3497   *   If you passed an attribute to `setElementL10n()`, pass it here too.
   3498   */
   3499  removeElementL10n(element, { attribute = undefined } = {}) {
   3500    if (attribute) {
   3501      element.removeAttribute(attribute);
   3502      element.removeAttribute("data-l10n-attrs");
   3503    } else {
   3504      element.textContent = "";
   3505    }
   3506    element.removeAttribute("data-l10n-id");
   3507    element.removeAttribute("data-l10n-args");
   3508  }
   3509 
   3510  /**
   3511   * Observer method from Services.obs.addObserver.
   3512   *
   3513   * @param {nsISupports} subject
   3514   *   The subject of the notification.
   3515   * @param {string} topic
   3516   *   The topic of the notification.
   3517   */
   3518  async observe(subject, topic) {
   3519    switch (topic) {
   3520      case "intl:app-locales-changed": {
   3521        this.clear();
   3522        break;
   3523      }
   3524    }
   3525  }
   3526 
   3527  /**
   3528   * L10n ID => l10n args cache key => cached message object
   3529   *
   3530   * We rely on the fact that `Map` remembers insertion order to keep track of
   3531   * which cache entries are least recent, per l10n ID. The inner `Map`s will
   3532   * iterate their entries in order from least recently inserted to most
   3533   * recently inserted, i.e., least recently cached to most recently cached.
   3534   *
   3535   * @type {Map<string, Map<string, L10nCachedMessage>>}
   3536   */
   3537  #messagesByArgsById = new Map();
   3538 
   3539  /**
   3540   * Max entries per l10n ID for this cache.
   3541   *
   3542   * @type {number}
   3543   */
   3544  #maxEntriesPerId = L10nCache.MAX_ENTRIES_PER_ID;
   3545 
   3546  /**
   3547   * Inserts a message into the cache and makes it most recently cached.
   3548   *
   3549   * @param {object} options
   3550   *   Options
   3551   * @param {string} options.id
   3552   *   The string's Fluent ID.
   3553   * @param {object} options.args
   3554   *   The Fluent arguments as passed to `l10n.setAttributes`.
   3555   * @param {L10nCachedMessage} options.message
   3556   *   The message to cache.
   3557   */
   3558  #update({ id, args, message }) {
   3559    let messagesByArgs = this.#messagesByArgsById.get(id);
   3560    if (!messagesByArgs) {
   3561      messagesByArgs = new Map();
   3562      this.#messagesByArgsById.set(id, messagesByArgs);
   3563    }
   3564 
   3565    // We rely on the fact that `Map` remembers insertion order to keep track of
   3566    // which cache entries are least recent. To make `message` the most recent
   3567    // for its ID, delete it from `messagesByArgs` (step 1) and then reinsert it
   3568    // (step 2). That way it will move to the end of iteration.
   3569    let argsKey = this.#argsKey(args);
   3570 
   3571    // step 1
   3572    messagesByArgs.delete(argsKey);
   3573 
   3574    if (messagesByArgs.size == this.#maxEntriesPerId) {
   3575      // The cache entries are full for this ID. Remove the least recently
   3576      // cached entry, which will be the first entry returned by the map's
   3577      // iterator.
   3578      messagesByArgs.delete(messagesByArgs.keys().next().value);
   3579    }
   3580 
   3581    // step 2
   3582    messagesByArgs.set(argsKey, message);
   3583  }
   3584 
   3585  /**
   3586   * Returns a cache key for the inner `Maps` inside `#messagesByArgsById`.
   3587   * These `Map`s are keyed on l10n args.
   3588   *
   3589   * @param {object} args
   3590   *   The Fluent arguments as passed to `l10n.setAttributes`.
   3591   * @returns {string}
   3592   *   The args cache key.
   3593   */
   3594  #argsKey(args) {
   3595    // `JSON.stringify` doesn't guarantee a particular ordering of object
   3596    // properties, so instead of stringifying `args` as is, sort its entries by
   3597    // key and then pull out the values. The final key is a JSON'ed array of
   3598    // sorted-by-key `args` values.
   3599    let argValues = Object.entries(args ?? [])
   3600      .sort(([key1], [key2]) => key1.localeCompare(key2))
   3601      .map(([_, value]) => value);
   3602    return JSON.stringify(argValues);
   3603  }
   3604 }
   3605 
   3606 /**
   3607 * This class provides a way of serializing access to a resource. It's a queue
   3608 * of callbacks (or "tasks") where each callback is called and awaited in order,
   3609 * one at a time.
   3610 */
   3611 export class TaskQueue {
   3612  /**
   3613   * @returns {Promise}
   3614   *   Resolves when the queue becomes empty. If the queue is already empty,
   3615   *   then a resolved promise is returned.
   3616   */
   3617  get emptyPromise() {
   3618    return this.#emptyPromise;
   3619  }
   3620 
   3621  /**
   3622   * Adds a callback function to the task queue. The callback will be called
   3623   * after all other callbacks before it in the queue. This method returns a
   3624   * promise that will be resolved after awaiting the callback. The promise will
   3625   * be resolved with the value returned by the callback.
   3626   *
   3627   * @param {Function} callback
   3628   *   The function to queue.
   3629   * @returns {Promise}
   3630   *   Resolved after the task queue calls and awaits `callback`. It will be
   3631   *   resolved with the value returned by `callback`. If `callback` throws an
   3632   *   error, then it will be rejected with the error.
   3633   */
   3634  queue(callback) {
   3635    return new Promise((resolve, reject) => {
   3636      this.#queue.push({ callback, resolve, reject });
   3637      if (this.#queue.length == 1) {
   3638        this.#emptyDeferred = Promise.withResolvers();
   3639        this.#emptyPromise = this.#emptyDeferred.promise;
   3640        this.#doNextTask();
   3641      }
   3642    });
   3643  }
   3644 
   3645  /**
   3646   * Adds a callback function to the task queue that will be called on idle.
   3647   *
   3648   * @param {Function} callback
   3649   *   The function to queue.
   3650   * @returns {Promise}
   3651   *   Resolved after the task queue calls and awaits `callback`. It will be
   3652   *   resolved with the value returned by `callback`. If `callback` throws an
   3653   *   error, then it will be rejected with the error.
   3654   */
   3655  queueIdleCallback(callback) {
   3656    return this.queue(async () => {
   3657      await new Promise((resolve, reject) => {
   3658        ChromeUtils.idleDispatch(async () => {
   3659          try {
   3660            let value = await callback();
   3661            resolve(value);
   3662          } catch (error) {
   3663            console.error(error);
   3664            reject(error);
   3665          }
   3666        });
   3667      });
   3668    });
   3669  }
   3670 
   3671  /**
   3672   * Calls the next function in the task queue and recurses until the queue is
   3673   * empty. Once empty, all empty callback functions are called.
   3674   */
   3675  async #doNextTask() {
   3676    if (!this.#queue.length) {
   3677      this.#emptyDeferred.resolve();
   3678      this.#emptyDeferred = null;
   3679      return;
   3680    }
   3681 
   3682    // Leave the callback in the queue while awaiting it. If we remove it now
   3683    // the queue could become empty, and if `queue()` were called while we're
   3684    // awaiting the callback, `#doNextTask()` would be re-entered.
   3685    let { callback, resolve, reject } = this.#queue[0];
   3686    try {
   3687      let value = await callback();
   3688      resolve(value);
   3689    } catch (error) {
   3690      console.error(error);
   3691      reject(error);
   3692    }
   3693    this.#queue.shift();
   3694    this.#doNextTask();
   3695  }
   3696 
   3697  #queue = [];
   3698  #emptyDeferred = null;
   3699  #emptyPromise = Promise.resolve();
   3700 }