summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/urlbar/UrlbarProvidersManager.sys.mjs784
1 files changed, 784 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
new file mode 100644
index 0000000000..c64234753e
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
@@ -0,0 +1,784 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module exports a component used to register search providers and manage
+ * the connection between such providers and a UrlbarController.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ SkippableTimer: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarMuxer: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.UrlbarUtils.getLogger({ prefix: "ProvidersManager" })
+);
+
+// List of available local providers, each is implemented in its own jsm module
+// and will track different queries internally by queryContext.
+// When adding new providers please remember to update the list in metrics.yaml.
+var localProviderModules = {
+ UrlbarProviderAboutPages:
+ "resource:///modules/UrlbarProviderAboutPages.sys.mjs",
+ UrlbarProviderAliasEngines:
+ "resource:///modules/UrlbarProviderAliasEngines.sys.mjs",
+ UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
+ UrlbarProviderBookmarkKeywords:
+ "resource:///modules/UrlbarProviderBookmarkKeywords.sys.mjs",
+ UrlbarProviderCalculator:
+ "resource:///modules/UrlbarProviderCalculator.sys.mjs",
+ UrlbarProviderContextualSearch:
+ "resource:///modules/UrlbarProviderContextualSearch.sys.mjs",
+ UrlbarProviderHeuristicFallback:
+ "resource:///modules/UrlbarProviderHeuristicFallback.sys.mjs",
+ UrlbarProviderHistoryUrlHeuristic:
+ "resource:///modules/UrlbarProviderHistoryUrlHeuristic.sys.mjs",
+ UrlbarProviderInputHistory:
+ "resource:///modules/UrlbarProviderInputHistory.sys.mjs",
+ UrlbarProviderInterventions:
+ "resource:///modules/UrlbarProviderInterventions.sys.mjs",
+ UrlbarProviderOmnibox: "resource:///modules/UrlbarProviderOmnibox.sys.mjs",
+ UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.sys.mjs",
+ UrlbarProviderPreloadedSites:
+ "resource:///modules/UrlbarProviderPreloadedSites.sys.mjs",
+ UrlbarProviderPrivateSearch:
+ "resource:///modules/UrlbarProviderPrivateSearch.sys.mjs",
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+ UrlbarProviderRemoteTabs:
+ "resource:///modules/UrlbarProviderRemoteTabs.sys.mjs",
+ UrlbarProviderSearchTips:
+ "resource:///modules/UrlbarProviderSearchTips.sys.mjs",
+ UrlbarProviderSearchSuggestions:
+ "resource:///modules/UrlbarProviderSearchSuggestions.sys.mjs",
+ UrlbarProviderTabToSearch:
+ "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
+ UrlbarProviderTokenAliasEngines:
+ "resource:///modules/UrlbarProviderTokenAliasEngines.sys.mjs",
+ UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs",
+ UrlbarProviderUnitConversion:
+ "resource:///modules/UrlbarProviderUnitConversion.sys.mjs",
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+};
+
+// List of available local muxers, each is implemented in its own jsm module.
+var localMuxerModules = {
+ UrlbarMuxerUnifiedComplete:
+ "resource:///modules/UrlbarMuxerUnifiedComplete.sys.mjs",
+};
+
+// To improve dataflow and reduce UI work, when a result is added by a
+// non-heuristic provider, we notify it to the controller after a delay, so
+// that we can chunk results coming in that timeframe into a single call.
+const CHUNK_RESULTS_DELAY_MS = 16;
+
+const DEFAULT_MUXER = "UnifiedComplete";
+
+/**
+ * Class used to create a manager.
+ * The manager is responsible to keep a list of providers, instantiate query
+ * objects and pass those to the providers.
+ */
+class ProvidersManager {
+ constructor() {
+ // Tracks the available providers. This is a sorted array, with HEURISTIC
+ // providers at the front.
+ this.providers = [];
+ for (let [symbol, module] of Object.entries(localProviderModules)) {
+ let { [symbol]: provider } = ChromeUtils.importESModule(module);
+ this.registerProvider(provider);
+ }
+ // Tracks ongoing Query instances by queryContext.
+ this.queries = new Map();
+
+ // Interrupt() allows to stop any running SQL query, some provider may be
+ // running a query that shouldn't be interrupted, and if so it should
+ // bump this through disableInterrupt and enableInterrupt.
+ this.interruptLevel = 0;
+
+ // This maps muxer names to muxers.
+ this.muxers = new Map();
+ for (let [symbol, module] of Object.entries(localMuxerModules)) {
+ let { [symbol]: muxer } = ChromeUtils.importESModule(module);
+ this.registerMuxer(muxer);
+ }
+
+ // This is defined as a property so tests can override it.
+ this._chunkResultsDelayMs = CHUNK_RESULTS_DELAY_MS;
+ }
+
+ /**
+ * Registers a provider object with the manager.
+ *
+ * @param {object} provider
+ * The provider object to register.
+ */
+ registerProvider(provider) {
+ if (!provider || !(provider instanceof lazy.UrlbarProvider)) {
+ throw new Error(`Trying to register an invalid provider`);
+ }
+ if (
+ !Object.values(lazy.UrlbarUtils.PROVIDER_TYPE).includes(provider.type)
+ ) {
+ throw new Error(`Unknown provider type ${provider.type}`);
+ }
+ lazy.logger.info(`Registering provider ${provider.name}`);
+ let index = -1;
+ if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
+ // Keep heuristic providers in order at the front of the array. Find the
+ // first non-heuristic provider and insert the new provider there.
+ index = this.providers.findIndex(
+ p => p.type != lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC
+ );
+ }
+ if (index < 0) {
+ index = this.providers.length;
+ }
+ this.providers.splice(index, 0, provider);
+ }
+
+ /**
+ * Unregisters a previously registered provider object.
+ *
+ * @param {object} provider
+ * The provider object to unregister.
+ */
+ unregisterProvider(provider) {
+ lazy.logger.info(`Unregistering provider ${provider.name}`);
+ let index = this.providers.findIndex(p => p.name == provider.name);
+ if (index != -1) {
+ this.providers.splice(index, 1);
+ }
+ }
+
+ /**
+ * Returns the provider with the given name.
+ *
+ * @param {string} name
+ * The provider name.
+ * @returns {UrlbarProvider} The provider.
+ */
+ getProvider(name) {
+ return this.providers.find(p => p.name == name);
+ }
+
+ /**
+ * Registers a muxer object with the manager.
+ *
+ * @param {object} muxer
+ * a UrlbarMuxer object
+ */
+ registerMuxer(muxer) {
+ if (!muxer || !(muxer instanceof lazy.UrlbarMuxer)) {
+ throw new Error(`Trying to register an invalid muxer`);
+ }
+ lazy.logger.info(`Registering muxer ${muxer.name}`);
+ this.muxers.set(muxer.name, muxer);
+ }
+
+ /**
+ * Unregisters a previously registered muxer object.
+ *
+ * @param {object} muxer
+ * a UrlbarMuxer object or name.
+ */
+ unregisterMuxer(muxer) {
+ let muxerName = typeof muxer == "string" ? muxer : muxer.name;
+ lazy.logger.info(`Unregistering muxer ${muxerName}`);
+ this.muxers.delete(muxerName);
+ }
+
+ /**
+ * Starts querying.
+ *
+ * @param {object} queryContext
+ * The query context object
+ * @param {object} [controller]
+ * a UrlbarController instance
+ */
+ async startQuery(queryContext, controller = null) {
+ lazy.logger.info(`Query start "${queryContext.searchString}"`);
+
+ // Define the muxer to use.
+ let muxerName = queryContext.muxer || DEFAULT_MUXER;
+ lazy.logger.debug(`Using muxer ${muxerName}`);
+ let muxer = this.muxers.get(muxerName);
+ if (!muxer) {
+ throw new Error(`Muxer with name ${muxerName} not found`);
+ }
+
+ // If the queryContext specifies a list of providers to use, filter on it,
+ // otherwise just pass the full list of providers.
+ let providers = queryContext.providers
+ ? this.providers.filter(p => queryContext.providers.includes(p.name))
+ : this.providers;
+
+ // Apply tokenization.
+ lazy.UrlbarTokenizer.tokenize(queryContext);
+
+ // If there's a single source, we are in restriction mode.
+ if (queryContext.sources && queryContext.sources.length == 1) {
+ queryContext.restrictSource = queryContext.sources[0];
+ }
+ // Providers can use queryContext.sources to decide whether they want to be
+ // invoked or not.
+ // The sources may be defined in the context, then the whole search string
+ // can be used for searching. Otherwise sources are extracted from prefs and
+ // restriction tokens, then restriction tokens must be filtered out of the
+ // search string.
+ let restrictToken = updateSourcesIfEmpty(queryContext);
+ if (restrictToken) {
+ queryContext.restrictToken = restrictToken;
+ // If the restriction token has an equivalent source, then set it as
+ // restrictSource.
+ if (lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(restrictToken.value)) {
+ queryContext.restrictSource = queryContext.sources[0];
+ }
+ }
+ lazy.logger.debug(`Context sources ${queryContext.sources}`);
+
+ let query = new Query(queryContext, controller, muxer, providers);
+ this.queries.set(queryContext, query);
+
+ // The muxer and many providers depend on the search service and our search
+ // utils. Make sure they're initialized now (via UrlbarSearchUtils) so that
+ // all query-related urlbar modules don't need to do it.
+ try {
+ await lazy.UrlbarSearchUtils.init();
+ } catch {
+ // We continue anyway, because we want the user to be able to search their
+ // history and bookmarks even if search engines are not available.
+ }
+
+ if (query.canceled) {
+ return;
+ }
+
+ // Update the behavior of extension providers.
+ let updateBehaviorPromises = [];
+ for (let provider of this.providers) {
+ if (
+ provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.EXTENSION &&
+ provider.name != "Omnibox"
+ ) {
+ updateBehaviorPromises.push(
+ provider.tryMethod("updateBehavior", queryContext)
+ );
+ }
+ }
+ if (updateBehaviorPromises.length) {
+ await Promise.all(updateBehaviorPromises);
+ if (query.canceled) {
+ return;
+ }
+ }
+
+ await query.start();
+ }
+
+ /**
+ * Cancels a running query.
+ *
+ * @param {object} queryContext The query context object
+ */
+ cancelQuery(queryContext) {
+ lazy.logger.info(`Query cancel "${queryContext.searchString}"`);
+ let query = this.queries.get(queryContext);
+ if (!query) {
+ throw new Error("Couldn't find a matching query for the given context");
+ }
+ query.cancel();
+ if (!this.interruptLevel) {
+ try {
+ let db = lazy.PlacesUtils.promiseLargeCacheDBConnection();
+ db.interrupt();
+ } catch (ex) {}
+ }
+ this.queries.delete(queryContext);
+ }
+
+ /**
+ * A provider can use this util when it needs to run a SQL query that can't
+ * be interrupted. Otherwise, when a query is canceled any running SQL query
+ * is interrupted abruptly.
+ *
+ * @param {Function} taskFn a Task to execute in the critical section.
+ */
+ async runInCriticalSection(taskFn) {
+ this.interruptLevel++;
+ try {
+ await taskFn();
+ } finally {
+ this.interruptLevel--;
+ }
+ }
+
+ /**
+ * Notifies all providers when the user starts and ends an engagement with the
+ * urlbar. For details on parameters, see UrlbarProvider.onEngagement().
+ *
+ * @param {boolean} isPrivate
+ * True if the engagement is in a private context.
+ * @param {string} state
+ * The state of the engagement, one of: start, engagement, abandonment,
+ * discard
+ * @param {UrlbarQueryContext} queryContext
+ * The engagement's query context, if available.
+ * @param {object} details
+ * An object that describes the search string and the picked result, if any.
+ * @param {window} window
+ * Browser window object associated with engagement
+ */
+ notifyEngagementChange(isPrivate, state, queryContext, details = {}, window) {
+ for (let provider of this.providers) {
+ provider.tryMethod(
+ "onEngagement",
+ isPrivate,
+ state,
+ queryContext,
+ details,
+ window
+ );
+ }
+ }
+}
+
+export var UrlbarProvidersManager = new ProvidersManager();
+
+/**
+ * Tracks a query status.
+ * Multiple queries can potentially be executed at the same time by different
+ * controllers. Each query has to track its own status and delays separately,
+ * to avoid conflicting with other ones.
+ */
+class Query {
+ /**
+ * Initializes the query object.
+ *
+ * @param {object} queryContext
+ * The query context
+ * @param {object} controller
+ * The controller to be notified
+ * @param {object} muxer
+ * The muxer to sort results
+ * @param {Array} providers
+ * Array of all the providers.
+ */
+ constructor(queryContext, controller, muxer, providers) {
+ this.context = queryContext;
+ this.context.results = [];
+ // Clear any state in the context object, since it could be reused by the
+ // caller and we don't want to port previous query state over.
+ this.context.pendingHeuristicProviders.clear();
+ this.context.deferUserSelectionProviders.clear();
+ this.unsortedResults = [];
+ this.muxer = muxer;
+ this.controller = controller;
+ this.providers = providers;
+ this.started = false;
+ this.canceled = false;
+
+ // This is used as a last safety filter in add(), thus we keep an unmodified
+ // copy of it.
+ this.acceptableSources = queryContext.sources.slice();
+ }
+
+ /**
+ * Starts querying.
+ */
+ async start() {
+ if (this.started) {
+ throw new Error("This Query has been started already");
+ }
+ this.started = true;
+
+ // Check which providers should be queried by calling isActive on them.
+ let activeProviders = [];
+ let activePromises = [];
+ let maxPriority = -1;
+ for (let provider of this.providers) {
+ // This can be used by the provider to check the query is still running
+ // after executing async tasks:
+ // let instance = this.queryInstance;
+ // await ...
+ // if (instance != this.queryInstance) {
+ // // Query was canceled or a new one started.
+ // return;
+ // }
+ provider.queryInstance = this;
+ activePromises.push(
+ // Not all isActive implementations are async, so wrap the call in a
+ // promise so we can be sure we can call `then` on it. Note that
+ // Promise.resolve returns its arg directly if it's already a promise.
+ Promise.resolve(provider.tryMethod("isActive", this.context))
+ .then(isActive => {
+ if (isActive && !this.canceled) {
+ let priority = provider.tryMethod("getPriority", this.context);
+ if (priority >= maxPriority) {
+ // The provider's priority is at least as high as the max.
+ if (priority > maxPriority) {
+ // The provider's priority is higher than the max. Remove all
+ // previously added providers, since their priority is
+ // necessarily lower, by setting length to zero.
+ activeProviders.length = 0;
+ maxPriority = priority;
+ }
+ activeProviders.push(provider);
+ if (provider.deferUserSelection) {
+ this.context.deferUserSelectionProviders.add(provider.name);
+ }
+ }
+ }
+ })
+ .catch(ex => lazy.logger.error(ex))
+ );
+ }
+
+ // We have to wait for all isActive calls to finish because we want to query
+ // only the highest priority active providers as determined by the priority
+ // logic above.
+ await Promise.all(activePromises);
+
+ if (this.canceled) {
+ this.controller = null;
+ return;
+ }
+
+ // Start querying active providers.
+ let startQuery = async provider => {
+ provider.logger.debug(
+ `Starting query for "${this.context.searchString}"`
+ );
+ let addedResult = false;
+ await provider.tryMethod("startQuery", this.context, (...args) => {
+ addedResult = true;
+ this.add(...args);
+ });
+ if (!addedResult) {
+ this.context.deferUserSelectionProviders.delete(provider.name);
+ }
+ };
+
+ let queryPromises = [];
+ for (let provider of activeProviders) {
+ if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
+ this.context.pendingHeuristicProviders.add(provider.name);
+ queryPromises.push(startQuery(provider));
+ continue;
+ }
+ if (!this._sleepTimer) {
+ // Tracks the delay timer. We will fire (in this specific case, cancel
+ // would do the same, since the callback is empty) the timer when the
+ // search is canceled, unblocking start().
+ this._sleepTimer = new lazy.SkippableTimer({
+ name: "Query provider timer",
+ time: lazy.UrlbarPrefs.get("delay"),
+ logger: provider.logger,
+ });
+ }
+ queryPromises.push(
+ this._sleepTimer.promise.then(() =>
+ this.canceled ? undefined : startQuery(provider)
+ )
+ );
+ }
+
+ lazy.logger.info(
+ `Queried ${queryPromises.length} providers: ${activeProviders.map(
+ p => p.name
+ )}`
+ );
+ await Promise.all(queryPromises);
+
+ // All the providers are done returning results, so we can stop chunking.
+ if (!this.canceled) {
+ if (this._heuristicProviderTimer) {
+ await this._heuristicProviderTimer.fire();
+ }
+ if (this._chunkTimer) {
+ await this._chunkTimer.fire();
+ }
+ }
+
+ // Break cycles with the controller to avoid leaks.
+ this.controller = null;
+ }
+
+ /**
+ * Cancels this query. Note: Invoking cancel multiple times is a no-op.
+ */
+ cancel() {
+ if (this.canceled) {
+ return;
+ }
+ this.canceled = true;
+ this.context.deferUserSelectionProviders.clear();
+ for (let provider of this.providers) {
+ provider.logger.debug(
+ `Canceling query for "${this.context.searchString}"`
+ );
+ // Mark the instance as no more valid, see start() for details.
+ provider.queryInstance = null;
+ provider.tryMethod("cancelQuery", this.context);
+ }
+ if (this._heuristicProviderTimer) {
+ this._heuristicProviderTimer.cancel().catch(ex => lazy.logger.error(ex));
+ }
+ if (this._chunkTimer) {
+ this._chunkTimer.cancel().catch(ex => lazy.logger.error(ex));
+ }
+ if (this._sleepTimer) {
+ this._sleepTimer.fire().catch(ex => lazy.logger.error(ex));
+ }
+ }
+
+ /**
+ * Adds a result returned from a provider to the results set.
+ *
+ * @param {UrlbarProvider} provider The provider that returned the result.
+ * @param {object} result The result object.
+ */
+ add(provider, result) {
+ if (!(provider instanceof lazy.UrlbarProvider)) {
+ throw new Error("Invalid provider passed to the add callback");
+ }
+
+ // When this set is empty, we can display heuristic results early. We remove
+ // the provider from the list without checking result.heuristic since
+ // heuristic providers don't necessarily have to return heuristic results.
+ // We expect a provider with type HEURISTIC will return its heuristic
+ // result(s) first.
+ this.context.pendingHeuristicProviders.delete(provider.name);
+
+ // Stop returning results as soon as we've been canceled.
+ if (this.canceled) {
+ return;
+ }
+
+ // In search mode, don't allow heuristic results in the following cases
+ // since they don't make sense:
+ // * When the search string is empty, or
+ // * In local search mode, except for autofill results
+ if (
+ result.heuristic &&
+ this.context.searchMode &&
+ (!this.context.trimmedSearchString ||
+ (!this.context.searchMode.engineName && !result.autofill))
+ ) {
+ return;
+ }
+
+ // Check if the result source should be filtered out. Pay attention to the
+ // heuristic result though, that is supposed to be added regardless.
+ if (
+ !this.acceptableSources.includes(result.source) &&
+ !result.heuristic &&
+ // Treat form history as searches for the purpose of acceptableSources.
+ (result.type != lazy.UrlbarUtils.RESULT_TYPE.SEARCH ||
+ result.source != lazy.UrlbarUtils.RESULT_SOURCE.HISTORY ||
+ !this.acceptableSources.includes(lazy.UrlbarUtils.RESULT_SOURCE.SEARCH))
+ ) {
+ return;
+ }
+
+ // Filter out javascript results for safety. The provider is supposed to do
+ // it, but we don't want to risk leaking these out.
+ if (
+ result.type != lazy.UrlbarUtils.RESULT_TYPE.KEYWORD &&
+ result.payload.url &&
+ result.payload.url.startsWith("javascript:") &&
+ !this.context.searchString.startsWith("javascript:") &&
+ lazy.UrlbarPrefs.get("filter.javascript")
+ ) {
+ return;
+ }
+
+ result.providerName = provider.name;
+ result.providerType = provider.type;
+ this.unsortedResults.push(result);
+
+ this._notifyResultsFromProvider(provider);
+ }
+
+ _notifyResultsFromProvider(provider) {
+ // We create two chunking timers: one for heuristic results, and one for
+ // other results. We expect heuristic providers to return their heuristic
+ // results before other results/providers in most cases. When all heuristic
+ // providers have returned some results, we fire the heuristic timer early.
+ // If the timer fires first, we stop waiting on the remaining heuristic
+ // providers.
+ // Both timers are used to reduce UI flicker.
+ if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
+ if (!this._heuristicProviderTimer) {
+ this._heuristicProviderTimer = new lazy.SkippableTimer({
+ name: "Heuristic provider timer",
+ callback: () => this._notifyResults(),
+ time: UrlbarProvidersManager._chunkResultsDelayMs,
+ logger: provider.logger,
+ });
+ }
+ } else if (!this._chunkTimer) {
+ this._chunkTimer = new lazy.SkippableTimer({
+ name: "Query chunk timer",
+ callback: () => this._notifyResults(),
+ time: UrlbarProvidersManager._chunkResultsDelayMs,
+ logger: provider.logger,
+ });
+ }
+ // If all active heuristic providers have returned results, we can skip the
+ // heuristic results timer and start showing results immediately.
+ if (
+ this._heuristicProviderTimer &&
+ !this.context.pendingHeuristicProviders.size
+ ) {
+ this._heuristicProviderTimer.fire().catch(ex => lazy.logger.error(ex));
+ }
+ }
+
+ _notifyResults() {
+ this.muxer.sort(this.context, this.unsortedResults);
+
+ if (this._heuristicProviderTimer) {
+ this._heuristicProviderTimer.cancel().catch(ex => lazy.logger.error(ex));
+ this._heuristicProviderTimer = null;
+ }
+
+ if (this._chunkTimer) {
+ this._chunkTimer.cancel().catch(ex => lazy.logger.error(ex));
+ this._chunkTimer = null;
+ }
+
+ // We don't want to notify consumers if there are no results since they
+ // generally expect at least one result when notified, so bail, but only
+ // after nulling out the chunk timer above so that it will be restarted
+ // the next time results are added.
+ if (!this.context.results.length) {
+ return;
+ }
+
+ this.context.firstResultChanged = !lazy.ObjectUtils.deepEqual(
+ this.context.firstResult,
+ this.context.results[0]
+ );
+ this.context.firstResult = this.context.results[0];
+
+ if (this.controller) {
+ this.controller.receiveResults(this.context);
+ }
+ }
+}
+
+/**
+ * Updates in place the sources for a given UrlbarQueryContext.
+ *
+ * @param {UrlbarQueryContext} context The query context to examine
+ * @returns {object} The restriction token that was used to set sources, or
+ * undefined if there's no restriction token.
+ */
+function updateSourcesIfEmpty(context) {
+ if (context.sources && context.sources.length) {
+ return false;
+ }
+ let acceptedSources = [];
+ // There can be only one restrict token per query.
+ let restrictToken = context.tokens.find(t =>
+ [
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_URL,
+ lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION,
+ ].includes(t.type)
+ );
+
+ // RESTRICT_TITLE and RESTRICT_URL do not affect query sources.
+ let restrictTokenType =
+ restrictToken &&
+ restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE &&
+ restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_URL
+ ? restrictToken.type
+ : undefined;
+
+ for (let source of Object.values(lazy.UrlbarUtils.RESULT_SOURCE)) {
+ // Skip sources that the context doesn't care about.
+ if (context.sources && !context.sources.includes(source)) {
+ continue;
+ }
+ // Check prefs and restriction tokens.
+ switch (source) {
+ case lazy.UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
+ if (
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG ||
+ (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.bookmark"))
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.HISTORY:
+ if (
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY ||
+ (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.history"))
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.SEARCH:
+ if (
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH ||
+ !restrictTokenType
+ ) {
+ // We didn't check browser.urlbar.suggest.searches here, because it
+ // just controls search suggestions. If a search suggestion arrives
+ // here, we lost already, because we broke user's privacy by hitting
+ // the network. Thus, it's better to leave things go through and
+ // notice the bug, rather than hiding it with a filter.
+ acceptedSources.push(source);
+ }
+ break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.TABS:
+ if (
+ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE ||
+ (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.openpage"))
+ ) {
+ acceptedSources.push(source);
+ }
+ break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK:
+ if (!context.isPrivate && !restrictTokenType) {
+ acceptedSources.push(source);
+ }
+ break;
+ case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
+ case lazy.UrlbarUtils.RESULT_SOURCE.ADDON:
+ default:
+ if (!restrictTokenType) {
+ acceptedSources.push(source);
+ }
+ break;
+ }
+ }
+ context.sources = acceptedSources;
+ return restrictToken;
+}