tor-browser

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

UrlbarProvidersManager.sys.mjs (35980B)


      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 a component used to register search providers and manage
      7 * the connection between such providers and a UrlbarController.
      8 */
      9 
     10 /**
     11 * @import { UrlbarProvider } from "UrlbarUtils.sys.mjs"
     12 * @import { UrlbarMuxer } from "UrlbarUtils.sys.mjs"
     13 * @import { UrlbarSearchStringTokenData } from "UrlbarTokenizer.sys.mjs"
     14 */
     15 
     16 const lazy = {};
     17 
     18 ChromeUtils.defineESModuleGetters(lazy, {
     19  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
     20  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     21  Region: "resource://gre/modules/Region.sys.mjs",
     22  SkippableTimer: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     23  UrlbarMuxer: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     24  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     25  UrlbarProvider: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     26  UrlbarSearchUtils:
     27    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     28  UrlbarTokenizer:
     29    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     30  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     31 });
     32 
     33 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     34  lazy.UrlbarUtils.getLogger({ prefix: "ProvidersManager" })
     35 );
     36 
     37 // List of available local providers, each is implemented in its own module and
     38 // will track different queries internally by queryContext.
     39 // When adding new providers please remember to update the list in metrics.yaml.
     40 var localProviderModules = [
     41  {
     42    name: "UrlbarProviderAboutPages",
     43    module:
     44      "moz-src:///browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs",
     45    supportedInputTypes: ["urlbar"],
     46  },
     47  {
     48    name: "UrlbarProviderActionsSearchMode",
     49    module:
     50      "moz-src:///browser/components/urlbar/UrlbarProviderActionsSearchMode.sys.mjs",
     51    supportedInputTypes: ["urlbar"],
     52  },
     53  {
     54    name: "UrlbarProviderGlobalActions",
     55    module:
     56      "moz-src:///browser/components/urlbar/UrlbarProviderGlobalActions.sys.mjs",
     57    supportedInputTypes: ["urlbar", "searchbar"],
     58  },
     59  {
     60    name: "UrlbarProviderAliasEngines",
     61    module:
     62      "moz-src:///browser/components/urlbar/UrlbarProviderAliasEngines.sys.mjs",
     63    supportedInputTypes: ["urlbar", "searchbar"],
     64  },
     65  {
     66    name: "UrlbarProviderAutofill",
     67    module:
     68      "moz-src:///browser/components/urlbar/UrlbarProviderAutofill.sys.mjs",
     69    supportedInputTypes: ["urlbar"],
     70  },
     71  {
     72    name: "UrlbarProviderBookmarkKeywords",
     73    module:
     74      "moz-src:///browser/components/urlbar/UrlbarProviderBookmarkKeywords.sys.mjs",
     75    supportedInputTypes: ["urlbar"],
     76  },
     77  {
     78    name: "UrlbarProviderCalculator",
     79    module:
     80      "moz-src:///browser/components/urlbar/UrlbarProviderCalculator.sys.mjs",
     81    supportedInputTypes: ["urlbar", "searchbar"],
     82  },
     83  {
     84    name: "UrlbarProviderClipboard",
     85    module:
     86      "moz-src:///browser/components/urlbar/UrlbarProviderClipboard.sys.mjs",
     87    supportedInputTypes: ["urlbar"],
     88  },
     89  {
     90    name: "UrlbarProviderHeuristicFallback",
     91    module:
     92      "moz-src:///browser/components/urlbar/UrlbarProviderHeuristicFallback.sys.mjs",
     93    supportedInputTypes: ["urlbar", "searchbar"],
     94  },
     95  {
     96    name: "UrlbarProviderHistoryUrlHeuristic",
     97    module:
     98      "moz-src:///browser/components/urlbar/UrlbarProviderHistoryUrlHeuristic.sys.mjs",
     99    supportedInputTypes: ["urlbar"],
    100  },
    101  {
    102    name: "UrlbarProviderInputHistory",
    103    module:
    104      "moz-src:///browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs",
    105    supportedInputTypes: ["urlbar"],
    106  },
    107  // disable UrlbarProviderInterventions as part of tor-browser#41327
    108  {
    109    name: "UrlbarProviderOmnibox",
    110    module:
    111      "moz-src:///browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs",
    112    supportedInputTypes: ["urlbar"],
    113  },
    114  {
    115    name: "UrlbarProviderPlaces",
    116    module: "moz-src:///browser/components/urlbar/UrlbarProviderPlaces.sys.mjs",
    117    supportedInputTypes: ["urlbar"],
    118  },
    119  {
    120    name: "UrlbarProviderPrivateSearch",
    121    module:
    122      "moz-src:///browser/components/urlbar/UrlbarProviderPrivateSearch.sys.mjs",
    123    supportedInputTypes: ["urlbar", "searchbar"],
    124  },
    125  {
    126    name: "UrlbarProviderQuickSuggest",
    127    module:
    128      "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs",
    129    supportedInputTypes: ["urlbar"],
    130  },
    131  {
    132    name: "UrlbarProviderQuickSuggestContextualOptIn",
    133    module:
    134      "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs",
    135    supportedInputTypes: ["urlbar"],
    136  },
    137  {
    138    name: "UrlbarProviderRecentSearches",
    139    module:
    140      "moz-src:///browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs",
    141    supportedInputTypes: ["urlbar", "searchbar"],
    142  },
    143  {
    144    name: "UrlbarProviderRemoteTabs",
    145    module:
    146      "moz-src:///browser/components/urlbar/UrlbarProviderRemoteTabs.sys.mjs",
    147    supportedInputTypes: ["urlbar"],
    148  },
    149  {
    150    name: "UrlbarProviderRestrictKeywords",
    151    module:
    152      "moz-src:///browser/components/urlbar/UrlbarProviderRestrictKeywords.sys.mjs",
    153    supportedInputTypes: ["urlbar"],
    154  },
    155  {
    156    name: "UrlbarProviderRestrictKeywordsAutofill",
    157    module:
    158      "moz-src:///browser/components/urlbar/UrlbarProviderRestrictKeywordsAutofill.sys.mjs",
    159    supportedInputTypes: ["urlbar"],
    160  },
    161  {
    162    name: "UrlbarProviderSearchTips",
    163    module:
    164      "moz-src:///browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs",
    165    supportedInputTypes: ["urlbar"],
    166  },
    167  {
    168    name: "UrlbarProviderSearchSuggestions",
    169    module:
    170      "moz-src:///browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs",
    171    supportedInputTypes: ["urlbar", "searchbar"],
    172  },
    173  // UrlbarProviderSemanticHistorySearch.sys.mjs is missing. tor-browser#44045.
    174  {
    175    name: "UrlbarProviderTabToSearch",
    176    module:
    177      "moz-src:///browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs",
    178    supportedInputTypes: ["urlbar", "searchbar"],
    179  },
    180  {
    181    name: "UrlbarProviderTokenAliasEngines",
    182    module:
    183      "moz-src:///browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs",
    184    supportedInputTypes: ["urlbar", "searchbar"],
    185  },
    186  {
    187    name: "UrlbarProviderTopSites",
    188    module:
    189      "moz-src:///browser/components/urlbar/UrlbarProviderTopSites.sys.mjs",
    190    supportedInputTypes: ["urlbar"],
    191  },
    192  {
    193    name: "UrlbarProviderUnitConversion",
    194    module:
    195      "moz-src:///browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs",
    196    supportedInputTypes: ["urlbar", "searchbar"],
    197  },
    198 ];
    199 
    200 // List of available local muxers, each is implemented in its own jsm module.
    201 var localMuxerModules = {
    202  UrlbarMuxerStandard:
    203    "moz-src:///browser/components/urlbar/UrlbarMuxerStandard.sys.mjs",
    204 };
    205 
    206 const DEFAULT_MUXER = "UnifiedComplete";
    207 const DEFAULT_CHUNK_RESULTS_DELAY_MS = 16;
    208 
    209 /**
    210 * Class used to create a manager. There always exists one manager instance
    211 * per input type. It is responsible to keep a list of provider instances,
    212 * instantiate query objects and pass those to the providers.
    213 */
    214 export class ProvidersManager {
    215  /**
    216   * Interrupt() allows to stop any running SQL query, some provider may be
    217   * running a query that shouldn't be interrupted, and if so it should
    218   * bump this through disableInterrupt and enableInterrupt.
    219   */
    220  static interruptLevel = 0;
    221 
    222  /**
    223   * @param {object} providerModules
    224   *   Object with symbol names as keys and module paths as values.
    225   *   Symbols should be UrlbarProvider classes that will be instanciated.
    226   * @param {object} muxerModules
    227   *   Object with symbol names as keys and module paths as values.
    228   *   Symbols should be UrlbarMuxer instances.
    229   */
    230  constructor(providerModules, muxerModules = localMuxerModules) {
    231    /**
    232     * Tracks the available providers. This is a sorted array, with HEURISTIC
    233     * providers at the front.
    234     *
    235     * @type {UrlbarProvider[]}
    236     */
    237    this.providers = [];
    238    /**
    239     * @type {{onEngagement: Set<UrlbarProvider>, onImpression: Set<UrlbarProvider>, onAbandonment: Set<UrlbarProvider>, onSearchSessionEnd: Set<UrlbarProvider>}}
    240     */
    241    this.providersByNotificationType = {
    242      onEngagement: new Set(),
    243      onImpression: new Set(),
    244      onAbandonment: new Set(),
    245      onSearchSessionEnd: new Set(),
    246    };
    247    for (let providerInfo of providerModules) {
    248      let { [providerInfo.name]: providerClass } = ChromeUtils.importESModule(
    249        providerInfo.module
    250      );
    251      this.registerProvider(new providerClass());
    252    }
    253 
    254    /**
    255     * Tracks ongoing Query instances by queryContext.
    256     *
    257     * @type {Map<object, Query>}
    258     */
    259    this.queries = new Map();
    260 
    261    /**
    262     * This maps muxer names to muxers.
    263     *
    264     * @type {Map<string, UrlbarMuxer>}
    265     */
    266    this.muxers = new Map();
    267 
    268    for (let [symbol, module] of Object.entries(muxerModules)) {
    269      let { [symbol]: muxer } = ChromeUtils.importESModule(module);
    270      this.registerMuxer(muxer);
    271    }
    272    /**
    273     * These can be set by tests to increase or reduce the chunk delays.
    274     * See _notifyResultsFromProvider for additional details.
    275     * To improve dataflow and reduce UI work, when a result is added we may notify
    276     * it to the controller after a delay, so that we can chunk results in that
    277     * timeframe into a single call. See _notifyResultsFromProvider for details.
    278     */
    279    this.CHUNK_RESULTS_DELAY_MS = DEFAULT_CHUNK_RESULTS_DELAY_MS;
    280  }
    281 
    282  /**
    283   * Registers a provider object with the manager.
    284   *
    285   * @param {object} provider
    286   *   The provider object to register.
    287   */
    288  registerProvider(provider) {
    289    if (!provider || !(provider instanceof lazy.UrlbarProvider)) {
    290      throw new Error(`Trying to register an invalid provider`);
    291    }
    292    if (
    293      !Object.values(lazy.UrlbarUtils.PROVIDER_TYPE).includes(provider.type)
    294    ) {
    295      throw new Error(`Unknown provider type ${provider.type}`);
    296    }
    297    lazy.logger.info(`Registering provider ${provider.name}`);
    298    let index = -1;
    299    if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
    300      // Keep heuristic providers in order at the front of the array.  Find the
    301      // first non-heuristic provider and insert the new provider there.
    302      index = this.providers.findIndex(
    303        p => p.type != lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC
    304      );
    305    }
    306    if (index < 0) {
    307      index = this.providers.length;
    308    }
    309    this.providers.splice(index, 0, provider);
    310 
    311    for (const notificationType of Object.keys(
    312      this.providersByNotificationType
    313    )) {
    314      if (typeof provider[notificationType] === "function") {
    315        this.providersByNotificationType[notificationType].add(provider);
    316      }
    317    }
    318  }
    319 
    320  /**
    321   * Unregisters a previously registered provider object.
    322   *
    323   * @param {object} provider
    324   *   The provider object to unregister.
    325   */
    326  unregisterProvider(provider) {
    327    lazy.logger.info(`Unregistering provider ${provider.name}`);
    328    let index = this.providers.findIndex(p => p.name == provider.name);
    329    if (index != -1) {
    330      this.providers.splice(index, 1);
    331    }
    332 
    333    Object.values(this.providersByNotificationType).forEach(providers =>
    334      providers.delete(provider)
    335    );
    336  }
    337 
    338  /**
    339   * Returns the provider with the given name.
    340   *
    341   * @param {string} name
    342   *   The provider name.
    343   * @returns {UrlbarProvider | undefined}
    344   *   The provider.
    345   */
    346  getProvider(name) {
    347    return this.providers.find(p => p.name == name);
    348  }
    349 
    350  /**
    351   * Registers a muxer object with the manager.
    352   *
    353   * @param {UrlbarMuxer} muxer
    354   *   a UrlbarMuxer object
    355   */
    356  registerMuxer(muxer) {
    357    if (!muxer || !(muxer instanceof lazy.UrlbarMuxer)) {
    358      throw new Error(`Trying to register an invalid muxer`);
    359    }
    360    lazy.logger.info(`Registering muxer ${muxer.name}`);
    361    this.muxers.set(muxer.name, muxer);
    362  }
    363 
    364  /**
    365   * Unregisters a previously registered muxer object.
    366   *
    367   * @param {UrlbarMuxer|string} muxer
    368   *   a UrlbarMuxer object or name.
    369   */
    370  unregisterMuxer(muxer) {
    371    let muxerName = typeof muxer == "string" ? muxer : muxer.name;
    372    lazy.logger.info(`Unregistering muxer ${muxerName}`);
    373    this.muxers.delete(muxerName);
    374  }
    375 
    376  /**
    377   * Starts querying.
    378   *
    379   * @param {UrlbarQueryContext} queryContext
    380   *   The query context object
    381   * @param {?UrlbarController} [controller]
    382   *   a UrlbarController instance
    383   */
    384  async startQuery(queryContext, controller = null) {
    385    lazy.logger.info(`Query start "${queryContext.searchString}"`);
    386 
    387    // Define the muxer to use.
    388    let muxerName = queryContext.muxer || DEFAULT_MUXER;
    389    lazy.logger.debug(`Using muxer ${muxerName}`);
    390    let muxer = this.muxers.get(muxerName);
    391    if (!muxer) {
    392      throw new Error(`Muxer with name ${muxerName} not found`);
    393    }
    394 
    395    // If the queryContext specifies a list of providers to use, filter on it,
    396    // otherwise just pass the full list of providers.
    397    let providers = queryContext.providers
    398      ? this.providers.filter(p => queryContext.providers.includes(p.name))
    399      : this.providers;
    400 
    401    queryContext.canceled = false;
    402    try {
    403      // The tokenizer needs to synchronously check whether the first token is a
    404      // keyword, thus here we must ensure the keywords cache is up.
    405      await lazy.PlacesUtils.keywords.ensureCacheInitialized();
    406    } catch (ex) {
    407      lazy.logger.error(
    408        "Unable to ensure keyword cache is initialization. A keyword may not be \
    409         detected at the beginning of the search string.",
    410        ex
    411      );
    412    }
    413 
    414    // The query may have been canceled while awaiting for asynchronous work.
    415    if (queryContext.canceled) {
    416      return;
    417    }
    418 
    419    // Apply tokenization.
    420    let tokens = lazy.UrlbarTokenizer.tokenize(queryContext);
    421    queryContext.tokens = tokens;
    422 
    423    // If there's a single source, we are in restriction mode.
    424    if (queryContext.sources && queryContext.sources.length == 1) {
    425      queryContext.restrictSource = queryContext.sources[0];
    426    }
    427    // Providers can use queryContext.sources to decide whether they want to be
    428    // invoked or not.
    429    // The sources may be defined in the context, then the whole search string
    430    // can be used for searching. Otherwise sources are extracted from prefs and
    431    // restriction tokens, then restriction tokens must be filtered out of the
    432    // search string.
    433    let restrictToken = updateSourcesIfEmpty(queryContext);
    434    if (restrictToken) {
    435      queryContext.restrictToken = restrictToken;
    436      // If the restriction token has an equivalent source, then set it as
    437      // restrictSource.
    438      if (lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(restrictToken.value)) {
    439        queryContext.restrictSource = queryContext.sources[0];
    440      }
    441    }
    442    lazy.logger.debug(`Context sources ${queryContext.sources}`);
    443 
    444    let query = new Query(queryContext, controller, muxer, providers);
    445    this.queries.set(queryContext, query);
    446 
    447    // The muxer and many providers depend on the search service and our search
    448    // utils.  Make sure they're initialized now (via UrlbarSearchUtils) so that
    449    // all query-related urlbar modules don't need to do it.
    450    try {
    451      await lazy.UrlbarSearchUtils.init();
    452    } catch {
    453      // We continue anyway, because we want the user to be able to search their
    454      // history and bookmarks even if search engines are not available.
    455    }
    456 
    457    // Some providers depend on Region/Locale info and must access Region.home
    458    // synchronously, so we ensure Region is initialized.
    459    try {
    460      await lazy.Region.init();
    461    } catch (ex) {
    462      // We continue anyway, region will be null and providers should handle
    463      // that gracefully.
    464    }
    465 
    466    if (query.canceled) {
    467      return;
    468    }
    469 
    470    await query.start();
    471  }
    472 
    473  /**
    474   * Cancels a running query.
    475   *
    476   * @param {UrlbarQueryContext} queryContext The query context object
    477   */
    478  cancelQuery(queryContext) {
    479    lazy.logger.info(`Query cancel "${queryContext.searchString}"`);
    480    queryContext.canceled = true;
    481 
    482    let query = this.queries.get(queryContext);
    483    if (!query) {
    484      // The query object may have not been created yet, if the query was
    485      // canceled immediately.
    486      return;
    487    }
    488    query.cancel();
    489    if (!ProvidersManager.interruptLevel) {
    490      try {
    491        let db = lazy.PlacesUtils.promiseLargeCacheDBConnection();
    492        db.interrupt();
    493      } catch (ex) {}
    494    }
    495    this.queries.delete(queryContext);
    496  }
    497 
    498  /**
    499   * A provider can use this util when it needs to run a SQL query that can't
    500   * be interrupted. Otherwise, when a query is canceled any running SQL query
    501   * is interrupted abruptly.
    502   *
    503   * @param {Function} taskFn a Task to execute in the critical section.
    504   */
    505  static async runInCriticalSection(taskFn) {
    506    this.interruptLevel++;
    507    try {
    508      await taskFn();
    509    } finally {
    510      this.interruptLevel--;
    511    }
    512  }
    513 
    514  /**
    515   * Notifies all providers about changes in user engagement with the urlbar.
    516   * This function centralizes the dispatch of engagement-related events to the
    517   * appropriate providers based on the current state of interaction.
    518   *
    519   * @param {"engagement"|"abandonment"} state
    520   *   The state of the engagement, one of: engagement, abandonment
    521   * @param {UrlbarQueryContext} queryContext
    522   *   The engagement's query context, if available.
    523   * @param {object} details
    524   *   An object that describes the search string and the picked result, if any.
    525   * @param {UrlbarController} controller
    526   *   The controller associated with the engagement
    527   */
    528  notifyEngagementChange(state, queryContext, details = {}, controller) {
    529    if (!["engagement", "abandonment"].includes(state)) {
    530      lazy.logger.error(`Unsupported state for engagement change: ${state}`);
    531      return;
    532    }
    533 
    534    const visibleResults = controller.view?.visibleResults ?? [];
    535    const visibleResultsByProviderName = new Map();
    536 
    537    visibleResults.forEach((result, index) => {
    538      const providerName = result.providerName;
    539      let results = visibleResultsByProviderName.get(providerName);
    540      if (!results) {
    541        results = [];
    542        visibleResultsByProviderName.set(providerName, results);
    543      }
    544      results.push({ index, result });
    545    });
    546 
    547    if (!details.isSessionOngoing) {
    548      this.#notifyImpression(
    549        this.providersByNotificationType.onImpression,
    550        state,
    551        queryContext,
    552        controller,
    553        visibleResultsByProviderName,
    554        state == "engagement" && details.result ? details : null
    555      );
    556    }
    557 
    558    if (state === "engagement") {
    559      if (details.result) {
    560        this.#notifyEngagement(
    561          this.providersByNotificationType.onEngagement,
    562          queryContext,
    563          controller,
    564          details
    565        );
    566      }
    567    } else {
    568      this.#notifyAbandonment(
    569        this.providersByNotificationType.onAbandonment,
    570        queryContext,
    571        controller,
    572        visibleResultsByProviderName
    573      );
    574    }
    575 
    576    if (!details.isSessionOngoing) {
    577      this.#notifySearchSessionEnd(
    578        this.providersByNotificationType.onSearchSessionEnd,
    579        queryContext,
    580        controller,
    581        details
    582      );
    583    }
    584  }
    585 
    586  #notifyEngagement(engagementProviders, queryContext, controller, details) {
    587    for (const provider of engagementProviders) {
    588      if (details.result.providerName == provider.name) {
    589        provider.tryMethod("onEngagement", queryContext, controller, details);
    590        break;
    591      }
    592    }
    593  }
    594 
    595  #notifyImpression(
    596    impressionProviders,
    597    state,
    598    queryContext,
    599    controller,
    600    visibleResultsByProviderName,
    601    details
    602  ) {
    603    for (const provider of impressionProviders) {
    604      const providerVisibleResults =
    605        visibleResultsByProviderName.get(provider.name) ?? [];
    606 
    607      if (providerVisibleResults.length) {
    608        provider.tryMethod(
    609          "onImpression",
    610          state,
    611          queryContext,
    612          controller,
    613          providerVisibleResults,
    614          details
    615        );
    616      }
    617    }
    618  }
    619 
    620  #notifyAbandonment(
    621    abandomentProviders,
    622    queryContext,
    623    controller,
    624    visibleResultsByProviderName
    625  ) {
    626    for (const provider of abandomentProviders) {
    627      if (visibleResultsByProviderName.has(provider.name)) {
    628        provider.tryMethod("onAbandonment", queryContext, controller);
    629      }
    630    }
    631  }
    632 
    633  #notifySearchSessionEnd(
    634    searchSessionEndProviders,
    635    queryContext,
    636    controller,
    637    details
    638  ) {
    639    for (const provider of searchSessionEndProviders) {
    640      provider.tryMethod(
    641        "onSearchSessionEnd",
    642        queryContext,
    643        controller,
    644        details
    645      );
    646    }
    647  }
    648 }
    649 
    650 export var UrlbarProvidersManager = new ProvidersManager(
    651  localProviderModules.filter(info =>
    652    info.supportedInputTypes.includes("urlbar")
    653  )
    654 );
    655 
    656 export var SearchbarProvidersManager = new ProvidersManager(
    657  localProviderModules.filter(info =>
    658    info.supportedInputTypes.includes("searchbar")
    659  )
    660 );
    661 
    662 /**
    663 * Tracks a query status.
    664 * Multiple queries can potentially be executed at the same time by different
    665 * controllers. Each query has to track its own status and delays separately,
    666 * to avoid conflicting with other ones.
    667 */
    668 export class Query {
    669  /**
    670   * Initializes the query object.
    671   *
    672   * @param {UrlbarQueryContext} queryContext
    673   *   The query context.
    674   * @param {?UrlbarController} controller
    675   *   The controller to be notified. May be null.
    676   * @param {UrlbarMuxer} muxer
    677   *   The muxer to sort results.
    678   * @param {UrlbarProvider[]} providers
    679   *   Array of all the providers.
    680   */
    681  constructor(queryContext, controller, muxer, providers) {
    682    this.context = queryContext;
    683    this.context.results = [];
    684    // Clear any state in the context object, since it could be reused by the
    685    // caller and we don't want to port previous query state over.
    686    this.context.pendingHeuristicProviders.clear();
    687    this.context.deferUserSelectionProviders.clear();
    688    this.unsortedResults = [];
    689    this.muxer = muxer;
    690    this.controller = controller;
    691    this.providers = providers;
    692    this.started = false;
    693    this.canceled = false;
    694 
    695    // This is used as a last safety filter in add(), thus we keep an unmodified
    696    // copy of it.
    697    this.acceptableSources = queryContext.sources.slice();
    698  }
    699 
    700  /**
    701   * Starts querying.
    702   */
    703  async start() {
    704    if (this.started) {
    705      throw new Error("This Query has been started already");
    706    }
    707    this.started = true;
    708 
    709    // Check which providers should be queried by calling isActive on them.
    710    let activeProviders = [];
    711    let activePromises = [];
    712    let maxPriority = -1;
    713    for (let provider of this.providers) {
    714      // This can be used by the provider to check the query is still running
    715      // after executing async tasks:
    716      //   let instance = this.queryInstance;
    717      //   await ...
    718      //   if (instance != this.queryInstance) {
    719      //     // Query was canceled or a new one started.
    720      //     return;
    721      //   }
    722      provider.queryInstance = this;
    723      activePromises.push(
    724        provider
    725          .isActive(this.context, this.controller)
    726          .then(isActive => {
    727            if (isActive && !this.canceled) {
    728              let priority = provider.tryMethod("getPriority", this.context);
    729              if (priority >= maxPriority) {
    730                // The provider's priority is at least as high as the max.
    731                if (priority > maxPriority) {
    732                  // The provider's priority is higher than the max.  Remove all
    733                  // previously added providers, since their priority is
    734                  // necessarily lower, by setting length to zero.
    735                  activeProviders.length = 0;
    736                  maxPriority = priority;
    737                }
    738                activeProviders.push(provider);
    739                if (provider.deferUserSelection) {
    740                  this.context.deferUserSelectionProviders.add(provider.name);
    741                }
    742              }
    743            }
    744          })
    745          .catch(ex => lazy.logger.error(ex))
    746      );
    747    }
    748 
    749    // We have to wait for all isActive calls to finish because we want to query
    750    // only the highest priority active providers as determined by the priority
    751    // logic above.
    752    await Promise.all(activePromises);
    753 
    754    if (this.canceled) {
    755      this.controller = null;
    756      return;
    757    }
    758 
    759    // Start querying active providers.
    760    /**
    761     * @type {(provider: UrlbarProvider) => Promise<void>}
    762     */
    763    let startQuery = async provider => {
    764      provider.logger.debug(
    765        `Starting query for "${this.context.searchString}"`
    766      );
    767      let addedResult = false;
    768      await provider.tryMethod(
    769        "startQuery",
    770        this.context,
    771        /** @type {Parameters<UrlbarProvider['startQuery']>[1]} */
    772        (innerProvider, result) => {
    773          addedResult = true;
    774          this.add(innerProvider, result);
    775        }
    776      );
    777      if (!addedResult) {
    778        this.context.deferUserSelectionProviders.delete(provider.name);
    779      }
    780    };
    781 
    782    let queryPromises = [];
    783    for (let provider of activeProviders) {
    784      // Track heuristic providers. later we'll use this Set to wait for them
    785      // before returning results to the user.
    786      if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
    787        this.context.pendingHeuristicProviders.add(provider.name);
    788        queryPromises.push(
    789          startQuery(provider).finally(() => {
    790            this.context.pendingHeuristicProviders.delete(provider.name);
    791          })
    792        );
    793        continue;
    794      }
    795      if (!this._sleepTimer) {
    796        // Tracks the delay timer. We will fire (in this specific case, cancel
    797        // would do the same, since the callback is empty) the timer when the
    798        // search is canceled, unblocking start().
    799        this._sleepTimer = new lazy.SkippableTimer({
    800          name: "Query provider timer",
    801          time: lazy.UrlbarPrefs.get("delay"),
    802          logger: provider.logger,
    803        });
    804      }
    805      queryPromises.push(
    806        this._sleepTimer.promise.then(() =>
    807          this.canceled ? undefined : startQuery(provider)
    808        )
    809      );
    810    }
    811 
    812    lazy.logger.info(
    813      `Queried ${queryPromises.length} providers: ${activeProviders.map(
    814        p => p.name
    815      )}`
    816    );
    817 
    818    // Normally we wait for all the queries, but in case this is canceled we can
    819    // return earlier.
    820    let cancelPromise = new Promise(resolve => {
    821      this._cancelQueries = resolve;
    822    });
    823    await Promise.race([Promise.all(queryPromises), cancelPromise]);
    824 
    825    // All the providers are done returning results, so we can stop chunking.
    826    if (!this.canceled) {
    827      await this._chunkTimer?.fire();
    828    }
    829 
    830    // Break cycles with the controller to avoid leaks.
    831    this.controller = null;
    832  }
    833 
    834  /**
    835   * Cancels this query. Note: Invoking cancel multiple times is a no-op.
    836   */
    837  cancel() {
    838    if (this.canceled) {
    839      return;
    840    }
    841    this.canceled = true;
    842    this.context.deferUserSelectionProviders.clear();
    843    for (let provider of this.providers) {
    844      provider.logger.debug(
    845        `Canceling query for "${this.context.searchString}"`
    846      );
    847      // Mark the instance as no more valid, see start() for details.
    848      provider.queryInstance = null;
    849      provider.tryMethod("cancelQuery", this.context);
    850    }
    851    this._chunkTimer?.cancel().catch(ex => lazy.logger.error(ex));
    852    this._sleepTimer?.fire().catch(ex => lazy.logger.error(ex));
    853    this._cancelQueries?.();
    854  }
    855 
    856  /**
    857   * Adds a result returned from a provider to the results set.
    858   *
    859   * @param {UrlbarProvider} provider The provider that returned the result.
    860   * @param {UrlbarResult} result The result object.
    861   */
    862  add(provider, result) {
    863    if (!(provider instanceof lazy.UrlbarProvider)) {
    864      throw new Error("Invalid provider passed to the add callback");
    865    }
    866 
    867    // When this set is empty, we can display heuristic results early. We remove
    868    // the provider from the list without checking result.heuristic since
    869    // heuristic providers don't necessarily have to return heuristic results.
    870    // We expect a provider with type HEURISTIC will return its heuristic
    871    // result(s) first.
    872    this.context.pendingHeuristicProviders.delete(provider.name);
    873 
    874    // Stop returning results as soon as we've been canceled.
    875    if (this.canceled) {
    876      return;
    877    }
    878 
    879    // In search mode, don't allow heuristic results in the following cases
    880    // since they don't make sense:
    881    //   * When the search string is empty, or
    882    //   * In local search mode, except for autofill results
    883    if (
    884      result.heuristic &&
    885      this.context.searchMode &&
    886      (!this.context.trimmedSearchString ||
    887        (!this.context.searchMode.engineName && !result.autofill))
    888    ) {
    889      return;
    890    }
    891 
    892    // Check if the result source should be filtered out. Pay attention to the
    893    // heuristic result though, that is supposed to be added regardless.
    894    if (
    895      !this.acceptableSources.includes(result.source) &&
    896      !result.heuristic &&
    897      // Treat form history as searches for the purpose of acceptableSources.
    898      (result.type != lazy.UrlbarUtils.RESULT_TYPE.SEARCH ||
    899        result.source != lazy.UrlbarUtils.RESULT_SOURCE.HISTORY ||
    900        !this.acceptableSources.includes(
    901          lazy.UrlbarUtils.RESULT_SOURCE.SEARCH
    902        )) &&
    903      // To enable tab group search in tabs mode, allow actions to bypass
    904      // acceptableSources.
    905      !(
    906        result.source == lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS &&
    907        this.acceptableSources.includes(lazy.UrlbarUtils.RESULT_SOURCE.TABS)
    908      )
    909    ) {
    910      return;
    911    }
    912 
    913    // Filter out javascript results for safety. The provider is supposed to do
    914    // it, but we don't want to risk leaking these out.
    915    if (
    916      result.type != lazy.UrlbarUtils.RESULT_TYPE.KEYWORD &&
    917      result.payload.url &&
    918      result.payload.url.startsWith("javascript:") &&
    919      !this.context.searchString.startsWith("javascript:") &&
    920      lazy.UrlbarPrefs.get("filter.javascript")
    921    ) {
    922      return;
    923    }
    924 
    925    result.providerName = provider.name;
    926    result.providerType = provider.type;
    927    this.unsortedResults.push(result);
    928 
    929    this._notifyResultsFromProvider(provider);
    930  }
    931 
    932  _notifyResultsFromProvider(provider) {
    933    // We use a timer to reduce UI flicker, by adding results in chunks.
    934    if (!this._chunkTimer || this._chunkTimer.done) {
    935      // Either there's no heuristic provider pending at all, or the previous
    936      // timer is done, but we're still getting results. Start a short timer
    937      // to chunk remaining results.
    938      this._chunkTimer = new lazy.SkippableTimer({
    939        name: "chunking",
    940        callback: () => this._notifyResults(),
    941        time:
    942          this.controller?.manager.CHUNK_RESULTS_DELAY_MS ??
    943          DEFAULT_CHUNK_RESULTS_DELAY_MS,
    944        logger: provider.logger,
    945      });
    946    } else if (
    947      !this.context.pendingHeuristicProviders.size &&
    948      provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC
    949    ) {
    950      // All the active heuristic providers have returned results, we can skip
    951      // the heuristic chunk timer and start showing results immediately.
    952      this._chunkTimer.fire().catch(ex => lazy.logger.error(ex));
    953    }
    954 
    955    // Otherwise some timer is still ongoing and we'll wait for it.
    956  }
    957 
    958  _notifyResults() {
    959    this.muxer.sort(this.context, this.unsortedResults);
    960    // We don't want to notify consumers if there are no results since they
    961    // generally expect at least one result when notified, so bail, but only
    962    // after nulling out the chunk timer above so that it will be restarted
    963    // the next time results are added.
    964    if (!this.context.results.length) {
    965      return;
    966    }
    967 
    968    this.context.firstResultChanged = !lazy.ObjectUtils.deepEqual(
    969      this.context.firstResult,
    970      this.context.results[0]
    971    );
    972    this.context.firstResult = this.context.results[0];
    973 
    974    if (this.controller) {
    975      this.controller.receiveResults(this.context);
    976    }
    977  }
    978 
    979  /**
    980   * Returns the provider with the given name.
    981   *
    982   * @param {string} name
    983   *   The provider name.
    984   * @returns {UrlbarProvider | undefined}
    985   *   The provider.
    986   */
    987  getProvider(name) {
    988    return this.providers.find(p => p.name == name);
    989  }
    990 }
    991 
    992 /**
    993 * Updates in place the sources for a given UrlbarQueryContext.
    994 *
    995 * @param {UrlbarQueryContext} context The query context to examine
    996 * @returns {UrlbarSearchStringTokenData|undefined} The restriction token that
    997 *   was used to set sources, or undefined if there's no restriction token.
    998 */
    999 function updateSourcesIfEmpty(context) {
   1000  if (context.sources && context.sources.length) {
   1001    return undefined;
   1002  }
   1003  let acceptedSources = [];
   1004  // There can be only one restrict token per query.
   1005  let restrictToken =
   1006    context.sapName != "urlbar"
   1007      ? undefined
   1008      : context.tokens.find(t =>
   1009          [
   1010            lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
   1011            lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
   1012            lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG,
   1013            lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
   1014            lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
   1015            lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE,
   1016            lazy.UrlbarTokenizer.TYPE.RESTRICT_URL,
   1017            lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION,
   1018          ].includes(t.type)
   1019        );
   1020 
   1021  // RESTRICT_TITLE and RESTRICT_URL do not affect query sources.
   1022  let restrictTokenType =
   1023    restrictToken &&
   1024    restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE &&
   1025    restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_URL
   1026      ? restrictToken.type
   1027      : undefined;
   1028 
   1029  for (let source of Object.values(lazy.UrlbarUtils.RESULT_SOURCE)) {
   1030    // Check prefs and restriction tokens.
   1031    switch (source) {
   1032      case lazy.UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
   1033        if (
   1034          restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
   1035          restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG ||
   1036          (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.bookmark"))
   1037        ) {
   1038          acceptedSources.push(source);
   1039        }
   1040        break;
   1041      case lazy.UrlbarUtils.RESULT_SOURCE.HISTORY:
   1042        if (
   1043          restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY ||
   1044          (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.history"))
   1045        ) {
   1046          acceptedSources.push(source);
   1047        }
   1048        break;
   1049      case lazy.UrlbarUtils.RESULT_SOURCE.SEARCH:
   1050        if (
   1051          restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH ||
   1052          !restrictTokenType
   1053        ) {
   1054          // We didn't check browser.urlbar.suggest.searches here, because it
   1055          // just controls search suggestions. If a search suggestion arrives
   1056          // here, we lost already, because we broke user's privacy by hitting
   1057          // the network. Thus, it's better to leave things go through and
   1058          // notice the bug, rather than hiding it with a filter.
   1059          acceptedSources.push(source);
   1060        }
   1061        break;
   1062      case lazy.UrlbarUtils.RESULT_SOURCE.TABS:
   1063        if (
   1064          restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE ||
   1065          (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.openpage"))
   1066        ) {
   1067          acceptedSources.push(source);
   1068        }
   1069        break;
   1070      case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK:
   1071        if (!context.isPrivate && !restrictTokenType) {
   1072          acceptedSources.push(source);
   1073        }
   1074        break;
   1075      case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
   1076      case lazy.UrlbarUtils.RESULT_SOURCE.ADDON:
   1077      default:
   1078        if (!restrictTokenType) {
   1079          acceptedSources.push(source);
   1080        }
   1081        break;
   1082    }
   1083  }
   1084  context.sources = acceptedSources;
   1085  return restrictToken;
   1086 }