/* 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 provider class that is used for providers created by * extensions. */ import { SkippableTimer, UrlbarProvider, UrlbarUtils, } from "resource:///modules/UrlbarUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", }); // The set of `UrlbarQueryContext` properties that aren't serializable. const NONSERIALIZABLE_CONTEXT_PROPERTIES = new Set(["view"]); /** * The browser.urlbar extension API allows extensions to create their own urlbar * providers. The results from extension providers are integrated into the * urlbar view just like the results from providers that are built into Firefox. * * This class is the interface between the provider-related parts of the * browser.urlbar extension API implementation and our internal urlbar * implementation. The API implementation should use this class to manage * providers created by extensions. All extension providers must be instances * of this class. * * When an extension requires a provider, the API implementation should call * getOrCreate() to get or create it. When an extension adds an event listener * related to a provider, the API implementation should call setEventListener() * to register its own event listener with the provider. */ export class UrlbarProviderExtension extends UrlbarProvider { /** * Returns the extension provider with the given name, creating it first if * it doesn't exist. * * @param {string} name * The provider name. * @returns {UrlbarProviderExtension} * The provider. */ static getOrCreate(name) { let provider = lazy.UrlbarProvidersManager.getProvider(name); if (!provider) { provider = new UrlbarProviderExtension(name); lazy.UrlbarProvidersManager.registerProvider(provider); } return provider; } /** * Constructor. * * @param {string} name * The provider's name. */ constructor(name) { super(); this._name = name; this._eventListeners = new Map(); this.behavior = "inactive"; } /** * The provider's name. * * @returns {string} */ get name() { return this._name; } /** * The provider's type. The type of extension providers is always * UrlbarUtils.PROVIDER_TYPE.EXTENSION. * * @returns {UrlbarUtils.PROVIDER_TYPE} */ get type() { return UrlbarUtils.PROVIDER_TYPE.EXTENSION; } /** * Whether the provider should be invoked for the given context. If this * method returns false, the providers manager won't start a query with this * provider, to save on resources. * * @param {UrlbarQueryContext} context * The query context object. * @returns {boolean} * Whether this provider should be invoked for the search. */ isActive(context) { return this.behavior != "inactive"; } /** * Gets the provider's priority. * * @param {UrlbarQueryContext} context * The query context object. * @returns {number} * The provider's priority for the given query. */ getPriority(context) { // We give restricting extension providers a very high priority so that they // normally override all built-in providers, but not Infinity so that we can // still override them if necessary. return this.behavior == "restricting" ? 999 : 0; } /** * Sets the listener function for an event. The extension API implementation * should call this from its EventManager.register() implementations. Since * EventManager.register() is called at most only once for each extension * event (the first time the extension adds a listener for the event), each * provider instance needs at most only one listener per event, and that's why * this method is named setEventListener instead of addEventListener. * * The given listener function may return a promise that's resolved once the * extension responds to the event, or if the event requires no response from * the extension, it may return a non-promise value (possibly nothing). * * To remove the previously set listener, call this method again but pass null * as the listener function. * * The event name should be one of the following: * * behaviorRequested * This event is fired when the provider's behavior is needed from the * extension. The listener should return a behavior string. * queryCanceled * This event is fired when an ongoing query is canceled. The listener * shouldn't return anything. * resultsRequested * This event is fired when the provider's results are needed from the * extension. The listener should return an array of results. * * @param {string} eventName * The name of the event to listen to. * @param {Function} listener * The function that will be called when the event is fired. */ setEventListener(eventName, listener) { if (listener) { this._eventListeners.set(eventName, listener); } else { this._eventListeners.delete(eventName); if (!this._eventListeners.size) { lazy.UrlbarProvidersManager.unregisterProvider(this); } } } /** * This method is called by the providers manager before a query starts to * update each extension provider's behavior. It fires the behaviorRequested * event. * * @param {UrlbarQueryContext} context * The query context. */ async updateBehavior(context) { let behavior = await this._notifyListener( "behaviorRequested", makeSerializable(context) ); if (behavior) { this.behavior = behavior; } } /** * This is called only for dynamic result types, when the urlbar view updates * the view of one of the results of the provider. It should return an object * describing the view update. See the base UrlbarProvider class for more. * * @param {UrlbarResult} result The result whose view will be updated. * @param {Map} idsByName * A Map from an element's name, as defined by the provider; to its ID in * the DOM, as defined by the browser. * @returns {object} An object describing the view update. */ async getViewUpdate(result, idsByName) { return this._notifyListener("getViewUpdate", result, idsByName); } /** * This method is called by the providers manager when a query starts to fetch * each extension provider's results. It fires the resultsRequested event. * * @param {UrlbarQueryContext} context * The query context. * @param {Function} addCallback * The callback invoked by this method to add each result. */ async startQuery(context, addCallback) { let extResults = await this._notifyListener( "resultsRequested", makeSerializable(context) ); if (extResults) { for (let extResult of extResults) { let result = await this._makeUrlbarResult(context, extResult).catch( ex => this.logger.error(ex) ); if (result) { addCallback(this, result); } } } } /** * This method is called by the providers manager when an ongoing query is * canceled. It fires the queryCanceled event. * * @param {UrlbarQueryContext} context * The query context. */ cancelQuery(context) { this._notifyListener("queryCanceled", makeSerializable(context)); } #pickResult(result, element) { let dynamicElementName = ""; if (element && result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) { dynamicElementName = element.getAttribute("name"); } this._notifyListener("resultPicked", result.payload, dynamicElementName); } /** * Called 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. This is *not* guaranteed to be defined * when `state` is "start". It will always be defined for "engagement" and * "abandonment". * @param {object} details * This is defined only when `state` is "engagement" or "abandonment", and * it describes the search string and picked result. */ onEngagement(isPrivate, state, queryContext, details) { let { result, element } = details; // By design, the "resultPicked" extension event should not be fired when // the picked element has a URL. if (result?.providerName == this.name && !element?.dataset.url) { this.#pickResult(result, element); } this._notifyListener("engagement", isPrivate, state); } /** * Calls a listener function set by the extension API implementation, if any. * * @param {string} eventName * The name of the listener to call (i.e., the name of the event to fire). * @param {*} args * Arguments to the listener function. * @returns {*} * The value returned by the listener function, if any. */ async _notifyListener(eventName, ...args) { let listener = this._eventListeners.get(eventName); if (!listener) { return undefined; } let result; try { result = listener(...args); } catch (error) { this.logger.error(error); return undefined; } if (result.catch) { // The result is a promise, so wait for it to be resolved. Set up a timer // so that we're not stuck waiting forever. let timer = new SkippableTimer({ name: "UrlbarProviderExtension notification timer", time: lazy.UrlbarPrefs.get("extension.timeout"), reportErrorOnTimeout: true, logger: this.logger, }); result = await Promise.race([ timer.promise, result.catch(ex => this.logger.error(ex)), ]); timer.cancel(); } return result; } /** * Converts a plain-JS-object result created by the extension into a * UrlbarResult object. * * @param {UrlbarQueryContext} context * The query context. * @param {object} extResult * A plain JS object representing a result created by the extension. * @returns {UrlbarResult} * The UrlbarResult object. */ async _makeUrlbarResult(context, extResult) { // If the result is a search result, make sure its payload has a valid // `engine` property, which is the name of an engine, and which we use later // on to look up the nsISearchEngine. We allow the extension to specify the // engine by its name, alias, or domain. Prefer aliases over domains since // one domain can have many engines. if (extResult.type == "search") { let engine; if (extResult.payload.engine) { // Validate the engine name by looking it up. engine = Services.search.getEngineByName(extResult.payload.engine); } else if (extResult.payload.keyword) { // Look up the engine by its alias. engine = await lazy.UrlbarSearchUtils.engineForAlias( extResult.payload.keyword ); } else if (extResult.payload.url) { // Look up the engine by its domain. let host; try { host = new URL(extResult.payload.url).hostname; } catch (err) {} if (host) { engine = ( await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host) )[0]; } } if (!engine) { // No engine found. throw new Error("Invalid or missing engine specified by extension"); } extResult.payload.engine = engine.name; } let type = UrlbarProviderExtension.RESULT_TYPES[extResult.type]; if (type == UrlbarUtils.RESULT_TYPE.TIP) { extResult.payload.type ||= "extension"; extResult.payload.helpL10n = { id: lazy.UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-tip-get-help" : "urlbar-tip-help-icon", }; } let result = new lazy.UrlbarResult( UrlbarProviderExtension.RESULT_TYPES[extResult.type], UrlbarProviderExtension.SOURCE_TYPES[extResult.source], ...lazy.UrlbarResult.payloadAndSimpleHighlights( context.tokens, extResult.payload || {} ) ); if (extResult.heuristic && this.behavior == "restricting") { // The muxer chooses the final heuristic result by taking the first one // that claims to be the heuristic. We don't want extensions to clobber // the default heuristic, so we allow this only if the provider is // restricting. result.heuristic = extResult.heuristic; } if (extResult.suggestedIndex !== undefined) { result.suggestedIndex = extResult.suggestedIndex; } return result; } } // Maps extension result type enums to internal result types. UrlbarProviderExtension.RESULT_TYPES = { dynamic: UrlbarUtils.RESULT_TYPE.DYNAMIC, keyword: UrlbarUtils.RESULT_TYPE.KEYWORD, omnibox: UrlbarUtils.RESULT_TYPE.OMNIBOX, remote_tab: UrlbarUtils.RESULT_TYPE.REMOTE_TAB, search: UrlbarUtils.RESULT_TYPE.SEARCH, tab: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, tip: UrlbarUtils.RESULT_TYPE.TIP, url: UrlbarUtils.RESULT_TYPE.URL, }; // Maps extension source type enums to internal source types. UrlbarProviderExtension.SOURCE_TYPES = { bookmarks: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, history: UrlbarUtils.RESULT_SOURCE.HISTORY, local: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, network: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, search: UrlbarUtils.RESULT_SOURCE.SEARCH, tabs: UrlbarUtils.RESULT_SOURCE.TABS, actions: UrlbarUtils.RESULT_SOURCE.ACTIONS, }; /** * Returns a copy of a query context stripped of non-serializable properties. * This is necessary because query contexts are passed to extensions where they * become `Query` objects, as defined in the urlbar extensions schema. The * WebExtensions framework automatically excludes serializable properties that * aren't defined in the schema, but it chokes on non-serializable properties. * * @param {UrlbarQueryContext} context * The query context. * @returns {object} * A copy of `context` with only serializable properties. */ function makeSerializable(context) { return Object.fromEntries( Object.entries(context).filter( ([key]) => !NONSERIALIZABLE_CONTEXT_PROPERTIES.has(key) ) ); }