/* 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 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} 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} * 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[]} * 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" )); }