Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/**
* This module exports UrlbarQueryContext, the object describing a single urlbar
* query. It can be imported into system and content realms, so it should not
* use content-only globals like `window`.
*/
import UrlbarPrefs from "chrome://browser/content/urlbar/UrlbarContentPrefs.mjs";
import { UrlbarShared } from "chrome://browser/content/urlbar/UrlbarShared.mjs";
import { UrlbarResult } from "chrome://browser/content/urlbar/UrlbarResult.mjs";
/**
* @import {UrlbarSearchStringTokenData} from "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs"
*/
/**
* @typedef UrlbarSearchModeData
* @property {Values<typeof UrlbarShared.RESULT_SOURCE>} source
* The source from which search mode was entered.
* @property {string} [engineName]
* The search engine name associated with the search mode.
*/
/**
* UrlbarQueryContext defines a user's autocomplete input from within the urlbar.
* It supplements it with details of how the search results should be obtained
* and what they consist of.
*/
export class UrlbarQueryContext {
/**
* Constructs the UrlbarQueryContext instance.
*
* @param {object} options
* The initial options for UrlbarQueryContext.
* @param {string} options.sapName
* The search access point name of the UrlbarInput for use with telemetry or
* logging, e.g. `urlbar`, `searchbar`.
* @param {string} options.searchString
* The string the user entered in autocomplete. Could be the empty string
* in the case of the user opening the popup via the mouse.
* @param {boolean} options.isPrivate
* Set to true if this query was started from a private browsing window.
* @param {number} options.maxResults
* The maximum number of results that will be displayed for this query.
* @param {boolean} options.allowAutofill
* Whether or not to allow providers to include autofill results.
* @param {number} [options.userContextId]
* The container id where this context was generated, if any.
* @param {string | null} [options.tabGroup]
* The tab group where this context was generated, if any.
* @param {Array} [options.sources]
* A list of acceptable UrlbarShared.RESULT_SOURCE for the context.
* @param {object} [options.searchMode]
* The input's current search mode. See UrlbarInput.setSearchMode for a
* description.
* @param {boolean} [options.prohibitRemoteResults]
* This provides a short-circuit override for `context.allowRemoteResults`.
* If it's false, then `allowRemoteResults` will do its usual checks to
* determine whether remote results are allowed. If it's true, then
* `allowRemoteResults` will immediately return false. Defaults to false.
*/
constructor(options) {
// Clone to make sure all properties belong to the system realm.
// This is required because this method is called from a window.
// Not doing this causes a window leak if providers don't properly
// clean up after a query and keep references to UrlbarQueryContext
// properties (e.g. ProviderPlaces).
options = structuredClone(options);
this._checkRequiredOptions(options, [
"allowAutofill",
"isPrivate",
"maxResults",
"sapName",
"searchString",
]);
if (isNaN(options.maxResults)) {
throw new Error(
`Invalid maxResults property provided to UrlbarQueryContext`
);
}
/**
* @type {[string, (v: any) => boolean, any?][]}
*/
const optionalProperties = [
["currentPage", v => typeof v == "string" && !!v.length],
["excludeSponsoredResults", v => typeof v == "boolean", false],
["prohibitRemoteResults", v => typeof v == "boolean", false],
["providers", v => Array.isArray(v) && !!v.length],
["searchMode", v => v && typeof v == "object"],
["sources", v => Array.isArray(v) && !!v.length],
];
// Manage optional properties of options.
for (let [prop, checkFn, defaultValue] of optionalProperties) {
if (prop in options) {
if (!checkFn(options[prop])) {
throw new Error(`Invalid value for option "${prop}"`);
}
this[prop] = options[prop];
} else if (defaultValue !== undefined) {
this[prop] = defaultValue;
}
}
this.lastResultCount = 0;
// Note that Set is not serializable through JSON, so these may not be
// easily shared with add-ons.
this.pendingHeuristicProviders = new Set();
this.deferUserSelectionProviders = new Set();
this.trimmedSearchString = this.searchString.trim();
this.lowerCaseSearchString = this.searchString.toLowerCase();
this.trimmedLowerCaseSearchString = this.trimmedSearchString.toLowerCase();
this.userContextId = UrlbarShared.normalizedUserContextId(
options.userContextId,
this.isPrivate
);
this.tabGroup = options.tabGroup || null;
// Used to store glean timing distribution timer ids.
this.firstTimerId = 0;
this.sixthTimerId = 0;
}
/**
* @type {boolean}
* Whether or not to allow providers to include autofill results.
*/
allowAutofill;
/**
* @type {boolean}
* Whether or not the query has been cancelled.
*/
canceled = false;
/**
* @type {string}
* URL of the page that was loaded when the search began. Only set in a
* browser window; a content urlbar leaves it undefined.
*/
currentPage;
/**
* @type {number}
* The container id the query runs in, normalized to the id the open-pages
* table is keyed by (some providers read it directly). Only set in a browser
* window; a content urlbar keeps the default 0. Assign it from
* UrlbarShared.normalizedUserContextId().
*/
userContextId = 0;
/**
* @type {string | null}
* The tab group the query runs in. Only set in a browser window; a content
* urlbar keeps the default null.
*/
tabGroup = null;
/**
* @type {UrlbarResult}
* The current firstResult.
*/
firstResult;
/**
* @type {boolean}
* Indicates if the first result has been changed changed.
*/
firstResultChanged = false;
/**
* @type {UrlbarResult}
* The heuristic result associated with the context.
*/
heuristicResult;
/**
* @type {boolean}
* True if this query was started from a private browsing window.
*/
isPrivate;
/**
* @type {number}
* The maximum number of results that will be displayed for this query.
*/
maxResults;
/**
* @type {string}
* The name of the muxer to use for this query.
*/
muxer;
/**
* @type {boolean}
* Whether or not to exclude sponsored results.
*/
excludeSponsoredResults;
/**
* @type {boolean}
* Whether or not to prohibit remote results.
*/
prohibitRemoteResults;
/**
* @type {string[]}
* List of registered provider names. Providers can be registered through
* the ProvidersManager.
*/
providers;
/**
* @type {?Values<typeof UrlbarShared.RESULT_SOURCE>}
* Set if this context is restricted to a single source.
*/
restrictSource;
/**
* @type {UrlbarSearchStringTokenData}
* The restriction token used to restrict the sources for this search.
*/
restrictToken;
/**
* @type {UrlbarResult[]}
* The results associated with this context.
*/
results;
/**
* @type {string}
* The search access point name of the UrlbarInput for use with telemetry or
* logging, e.g. `urlbar`, `searchbar`.
*/
sapName;
/**
* @type {UrlbarSearchModeData}
* Details about the search mode associated with this context.
*/
searchMode;
/**
* Utility function to determine whether we should use the existence of
* searchMode to restrict the type of results to only search suggestions
* or the new behaviour behind `historyInSearchMode` pref that shows all
* types of results in searchMode, treating engine searchMode as
* "temporarily changed default search engine".
*
* @returns {boolean}
*/
restrictInSearchMode() {
if (UrlbarPrefs.get("unifiedSearchButton.historyInSearchMode")) {
// We still want to restrict local searchModes even if the pref is on.
return !!this.searchMode && !this.searchMode.engineName;
}
return !!this.searchMode;
}
/**
* @type {string}
* The string the user entered in autocomplete.
*/
searchString;
/**
* @type {Values<typeof UrlbarShared.RESULT_SOURCE>[]}
* The possible sources of results for this context.
*/
sources;
/**
* @type {UrlbarSearchStringTokenData[]}
* A list of tokens extracted from the search string.
*/
tokens;
/**
* Checks the required options, saving them as it goes.
*
* @param {object} options The options object to check.
* @param {Array} optionNames The names of the options to check for.
* @throws {Error} Throws if there is a missing option.
*/
_checkRequiredOptions(options, optionNames) {
for (let optionName of optionNames) {
if (!(optionName in options)) {
throw new Error(
`Missing or empty ${optionName} provided to UrlbarQueryContext`
);
}
this[optionName] = options[optionName];
}
}
/**
* Caches and returns fixup info from URIFixup for the current search string.
* Only returns a subset of the properties from URIFixup. This is both to
* reduce the memory footprint of UrlbarQueryContexts and to keep them
* serializable so they can be sent to extensions.
*/
get fixupInfo() {
if (!this._fixupError && !this._fixupInfo && this.trimmedSearchString) {
let flags =
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
if (this.isPrivate) {
flags |= Ci.nsIURIFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
}
try {
let info = Services.uriFixup.getFixupURIInfo(this.searchString, flags);
this._fixupInfo = {
href: info.fixedURI.spec,
isSearch: !!info.keywordAsSent,
scheme: info.fixedURI.scheme,
};
} catch (ex) {
this._fixupError = ex.result;
}
}
return this._fixupInfo || null;
}
/**
* Returns the error that was thrown when fixupInfo was fetched, if any. If
* fixupInfo has not yet been fetched for this queryContext, it is fetched
* here.
*
* @returns {any?}
*/
get fixupError() {
if (!this.fixupInfo) {
return this._fixupError;
}
return null;
}
/**
* Returns whether results from remote services are generally allowed for the
* context. Callers can impose further restrictions as appropriate, but
* typically they should not fetch remote results if this returns false.
*
* @param {string} [searchString]
* Usually this is just the context's search string, but if you need to
* fetch remote results based on a modified version, you can pass it here.
* @param {boolean} [allowEmptySearchString]
* Whether to check for the minimum length of the search string.
* @returns {boolean}
* Whether remote results are allowed.
*/
allowRemoteResults(
searchString = this.searchString,
allowEmptySearchString = false
) {
if (this.prohibitRemoteResults) {
return false;
}
// We're unlikely to get useful remote results for a single character.
if (
searchString.length < 2 &&
!(!searchString.length && allowEmptySearchString)
) {
return false;
}
// Prohibit remote results if the search string is likely an origin to avoid
// disclosing sites the user visits. If the search string may or may not be
// an origin but we've determined a search is allowed, then allow it.
if (this.tokens.length == 1) {
switch (this.tokens[0].type) {
case UrlbarShared.TOKEN_TYPE.POSSIBLE_ORIGIN:
return false;
case UrlbarShared.TOKEN_TYPE.POSSIBLE_ORIGIN_BUT_SEARCH_ALLOWED:
return true;
}
}
// Disallow remote results for strings containing tokens that look like URIs
// to avoid disclosing information about networks and passwords.
// (Unless the search is happening in the searchbar.)
if (
this.sapName != "searchbar" &&
this.fixupInfo?.href &&
!this.fixupInfo?.isSearch
) {
return false;
}
// Allow remote results.
return true;
}
/**
* Serializes this context to a plain, structured-cloneable object for sending
* across the Urlbar actor boundary. The context's own fields survive
* structured-clone on their own, but its nested UrlbarResults keep their data
* in private fields, so they're replaced with their wire forms.
*
* @returns {object} The wire representation; reconstruct with fromWire().
*/
toWire() {
return {
...this,
results: this.results?.map(result => result.toWire()),
heuristicResult: this.heuristicResult?.toWire(),
};
}
/**
* Reconstructs a UrlbarQueryContext from the plain object produced by
* toWire(), e.g. after it has crossed the Urlbar actor boundary: structured-
* clone preserved the context's own fields but dropped its class identity and
* its results' private-field data, so restore the prototype and rebuild the
* results.
*
* @param {object} wire The wire representation from toWire().
* @returns {UrlbarQueryContext} The reconstructed context.
*/
static fromWire(wire) {
Object.setPrototypeOf(wire, UrlbarQueryContext.prototype);
wire.results = wire.results?.map(UrlbarResult.fromWire) ?? [];
if (wire.heuristicResult) {
wire.heuristicResult = UrlbarResult.fromWire(wire.heuristicResult);
}
return wire;
}
}
// In chrome window globals, we re-export the UrlbarQueryContext from the system
// global. Otherwise, UrlbarQueryContexts created in a window global but cached
// in the system global would leak the window.
if (typeof ChromeUtils != "undefined" && typeof window != "undefined") {
// @ts-ignore
// eslint-disable-next-line no-class-assign
({ UrlbarQueryContext } = ChromeUtils.importESModule(
"chrome://browser/content/urlbar/UrlbarQueryContext.mjs"
));
}