These are no longer necessary now that we are not using XPCOM components. Differential Revision: https://phabricator.services.mozilla.com/D280780
473 lines
14 KiB
JavaScript
473 lines
14 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* @import {
|
|
* RefinedSearchConfig,
|
|
* SearchEngineDefinition
|
|
* } from "../uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs";
|
|
*/
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = XPCOMUtils.declareLazy({
|
|
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
|
SearchDeviceType:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
|
|
SearchEngineSelector:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
|
|
SearchUserEnvironment:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
|
|
SearchApplicationName:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
|
|
SearchUpdateChannel:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
|
|
SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
|
|
logConsole: () =>
|
|
console.createInstance({
|
|
prefix: "SearchEngineSelector",
|
|
maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* SearchEngineSelector parses the JSON configuration for
|
|
* search engines and returns the applicable engines depending
|
|
* on their region + locale.
|
|
*/
|
|
export class SearchEngineSelector {
|
|
/**
|
|
* @param {() => void} listener
|
|
* A listener for configuration update changes.
|
|
*/
|
|
constructor(listener) {
|
|
this.#remoteConfig = lazy.RemoteSettings(lazy.SearchUtils.SETTINGS_KEY);
|
|
this.#remoteConfigOverrides = lazy.RemoteSettings(
|
|
lazy.SearchUtils.SETTINGS_OVERRIDES_KEY
|
|
);
|
|
this.#boundOnConfigurationUpdated = this._onConfigurationUpdated.bind(this);
|
|
this.#boundOnConfigurationOverridesUpdated =
|
|
this._onConfigurationOverridesUpdated.bind(this);
|
|
this.#changeListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Resets the remote settings listeners, intended for test use only.
|
|
*/
|
|
reset() {
|
|
/**
|
|
* Records whether the listeners have been added or not.
|
|
*/
|
|
if (this.#listenerAdded) {
|
|
this.#remoteConfig.off("sync", this.#boundOnConfigurationUpdated);
|
|
this.#remoteConfigOverrides.off(
|
|
"sync",
|
|
this.#boundOnConfigurationOverridesUpdated
|
|
);
|
|
/**
|
|
* Records whether the listeners have been added or not.
|
|
*/
|
|
this.#listenerAdded = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the cached configuration, thus clearing the way to fetch a new
|
|
* configuration the next time `fetchEngineConfiguration` is called.
|
|
*/
|
|
clearCachedConfigurationForTests() {
|
|
this.#configuration = null;
|
|
}
|
|
|
|
/**
|
|
* Handles getting the configuration from remote settings.
|
|
*
|
|
* @returns {Promise<object>}
|
|
* The configuration data.
|
|
*/
|
|
async getEngineConfiguration() {
|
|
if (this.#getConfigurationPromise) {
|
|
return this.#getConfigurationPromise;
|
|
}
|
|
|
|
this.#getConfigurationPromise = Promise.all([
|
|
this.#getConfiguration(),
|
|
this.#getConfigurationOverrides(),
|
|
]);
|
|
let remoteSettingsData = await this.#getConfigurationPromise;
|
|
this.#configuration = remoteSettingsData[0];
|
|
this.#getConfigurationPromise = null;
|
|
|
|
if (!this.#configuration?.length) {
|
|
throw new Error("Failed to get engine data from Remote Settings");
|
|
}
|
|
|
|
/**
|
|
* Records whether the listeners have been added or not.
|
|
*/
|
|
if (!this.#listenerAdded) {
|
|
this.#remoteConfig.on("sync", this.#boundOnConfigurationUpdated);
|
|
this.#remoteConfigOverrides.on(
|
|
"sync",
|
|
this.#boundOnConfigurationOverridesUpdated
|
|
);
|
|
/**
|
|
* Records whether the listeners have been added or not.
|
|
*/
|
|
this.#listenerAdded = true;
|
|
}
|
|
|
|
this.#selector.setSearchConfig(
|
|
JSON.stringify({ data: this.#configuration })
|
|
);
|
|
this.#selector.setConfigOverrides(
|
|
JSON.stringify({ data: remoteSettingsData[1] })
|
|
);
|
|
|
|
return this.#configuration;
|
|
}
|
|
|
|
/**
|
|
* Finds an engine configuration that has a matching host.
|
|
*
|
|
* @param {string} host
|
|
* The host to match.
|
|
*
|
|
* @returns {Promise<?SearchEngineDefinition>}
|
|
* The configuration data for an engine.
|
|
*/
|
|
async findContextualSearchEngineByHost(host) {
|
|
for (let config of this.#configuration) {
|
|
if (config.recordType !== "engine") {
|
|
continue;
|
|
}
|
|
let searchHost = new URL(config.base.urls.search.base).hostname;
|
|
if (searchHost.startsWith("www.")) {
|
|
searchHost = searchHost.slice(4);
|
|
}
|
|
if (searchHost.startsWith(host)) {
|
|
let engine = structuredClone(config.base);
|
|
engine.identifier = config.identifier;
|
|
return engine;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Finds an engine configuration that has a matching identifier.
|
|
*
|
|
* @param {string} id
|
|
* The identifier to match.
|
|
*
|
|
* @returns {Promise<?SearchEngineDefinition>}
|
|
* The configuration data for an engine.
|
|
*/
|
|
async findContextualSearchEngineById(id) {
|
|
for (let config of this.#configuration) {
|
|
if (config.recordType !== "engine") {
|
|
continue;
|
|
}
|
|
if (config.identifier == id) {
|
|
let engine = structuredClone(config.base);
|
|
engine.identifier = config.identifier;
|
|
return engine;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {object} options
|
|
* The options object
|
|
* @param {string} options.locale
|
|
* Users locale.
|
|
* @param {string} options.region
|
|
* Users region.
|
|
* @param {string} [options.channel]
|
|
* The update channel the application is running on.
|
|
* @param {string} [options.distroID]
|
|
* The distribution ID of the application.
|
|
* @param {string} [options.experiment]
|
|
* Any associated experiment id.
|
|
* @param {string} [options.appName]
|
|
* The name of the application.
|
|
* @param {string} [options.version]
|
|
* The version of the application.
|
|
* @returns {Promise<RefinedSearchConfig>}
|
|
* An object which contains the refined configuration with a filtered list
|
|
* of search engines, and the identifiers for the application default engines.
|
|
*/
|
|
async fetchEngineConfiguration({
|
|
locale,
|
|
region,
|
|
channel = "default",
|
|
distroID,
|
|
experiment,
|
|
appName = Services.appinfo.name ?? "",
|
|
version = Services.appinfo.version ?? "",
|
|
}) {
|
|
if (!this.#configuration) {
|
|
await this.getEngineConfiguration();
|
|
}
|
|
|
|
lazy.logConsole.debug(
|
|
`fetchEngineConfiguration ${locale}:${region}:${channel}:${distroID}:${experiment}:${appName}:${version}`
|
|
);
|
|
|
|
let refinedSearchConfig = this.#selector.filterEngineConfiguration(
|
|
new lazy.SearchUserEnvironment({
|
|
locale,
|
|
region,
|
|
updateChannel: this.#convertUpdateChannel(channel),
|
|
distributionId: distroID ?? "",
|
|
experiment: experiment ?? "",
|
|
appName: this.#convertApplicationName(appName),
|
|
version,
|
|
deviceType: lazy.SearchDeviceType.NONE,
|
|
})
|
|
);
|
|
|
|
refinedSearchConfig.engines = refinedSearchConfig.engines.filter(
|
|
e => !e.optional
|
|
);
|
|
|
|
if (
|
|
!refinedSearchConfig.appDefaultEngineId ||
|
|
!refinedSearchConfig.engines.find(
|
|
e => e.identifier == refinedSearchConfig.appDefaultEngineId
|
|
)
|
|
) {
|
|
if (refinedSearchConfig.engines.length) {
|
|
lazy.logConsole.error(
|
|
"Could not find a matching default engine, using the first one in the list"
|
|
);
|
|
refinedSearchConfig.appDefaultEngineId =
|
|
refinedSearchConfig.engines[0].identifier;
|
|
} else {
|
|
throw new Error(
|
|
"Could not find any engines in the filtered configuration"
|
|
);
|
|
}
|
|
}
|
|
if (lazy.SearchUtils.loggingEnabled) {
|
|
lazy.logConsole.debug(
|
|
"fetchEngineConfiguration: " +
|
|
refinedSearchConfig.engines.map(e => e.identifier)
|
|
);
|
|
}
|
|
|
|
return refinedSearchConfig;
|
|
}
|
|
|
|
/**
|
|
* The remote settings client for handling the search configuration collection.
|
|
*/
|
|
#remoteConfig;
|
|
|
|
/**
|
|
* The remote settings client for handling the search configuration overrides
|
|
* collection.
|
|
*/
|
|
#remoteConfigOverrides;
|
|
|
|
/**
|
|
* The currently cached configuration. This is cached so that
|
|
* `findContextualSearchEngineByHost` and `findContextualSearchEngineById`
|
|
* do not need to get the configuration from remote settings every time
|
|
* they are called.
|
|
*
|
|
* @type {object[]}
|
|
*/
|
|
#configuration;
|
|
|
|
/**
|
|
* The bound version of the configuration updated listener.
|
|
*/
|
|
#boundOnConfigurationUpdated;
|
|
|
|
/**
|
|
* The bound version of the configuration overrides updated listener.
|
|
*/
|
|
#boundOnConfigurationOverridesUpdated;
|
|
|
|
/**
|
|
* Records whether the listeners have been added or not.
|
|
*/
|
|
#listenerAdded = false;
|
|
|
|
/**
|
|
* A listener that is notified when changes to the search collections are
|
|
* detected.
|
|
*/
|
|
#changeListener;
|
|
|
|
/**
|
|
* A promise that is used to de-bounce getting the configuration, it is
|
|
* resolved once the has been completed.
|
|
*
|
|
* @type {Promise<[object[], object[]]>}
|
|
*/
|
|
#getConfigurationPromise;
|
|
|
|
/**
|
|
* Obtains the configuration from remote settings. This includes
|
|
* verifying the signature of the record within the database.
|
|
*
|
|
* If the signature in the database is invalid, the database will be wiped
|
|
* and the stored dump will be used, until the settings next update.
|
|
*
|
|
* Note that this may cause a network check of the certificate, but that
|
|
* should generally be quick.
|
|
*
|
|
* @param {boolean} [firstTime]
|
|
* Internal boolean to indicate if this is the first time check or not.
|
|
* @returns {Promise<object[]>}
|
|
* An array of objects in the database, or an empty array if none
|
|
* could be obtained.
|
|
*/
|
|
async #getConfiguration(firstTime = true) {
|
|
let result = [];
|
|
let failed = false;
|
|
try {
|
|
result = await this.#remoteConfig.get({
|
|
order: "id",
|
|
});
|
|
} catch (ex) {
|
|
lazy.logConsole.error(ex);
|
|
failed = true;
|
|
}
|
|
if (!result.length) {
|
|
lazy.logConsole.error("Received empty search configuration!");
|
|
failed = true;
|
|
}
|
|
// If we failed, or the result is empty, try loading from the local dump.
|
|
if (firstTime && failed) {
|
|
await this.#remoteConfig.db.clear();
|
|
// Now call this again.
|
|
return this.#getConfiguration(false);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Handles updating of the configuration. Note that the search service is
|
|
* only updated after a period where the user is observed to be idle.
|
|
*
|
|
* This is exposed so that searchengine-devtools can easily change the
|
|
* configuration within its instance of the class object.
|
|
*
|
|
* @param {object} options
|
|
* The options object
|
|
* @param {object} options.data
|
|
* The data to update
|
|
* @param {Array} options.data.current
|
|
* The new configuration object
|
|
*/
|
|
_onConfigurationUpdated({ data: { current } }) {
|
|
this.#configuration = current;
|
|
|
|
this.#selector.setSearchConfig(
|
|
JSON.stringify({ data: this.#configuration })
|
|
);
|
|
|
|
lazy.logConsole.debug("Search configuration updated remotely");
|
|
if (this.#changeListener) {
|
|
this.#changeListener();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles updating of the configuration. Note that the search service is
|
|
* only updated after a period where the user is observed to be idle.
|
|
*
|
|
* This is exposed so that searchengine-devtools can easily change the
|
|
* configuration within its instance of the class object.
|
|
*
|
|
* @param {object} options
|
|
* The options object
|
|
* @param {object} options.data
|
|
* The data to update
|
|
* @param {Array} options.data.current
|
|
* The new configuration object
|
|
*/
|
|
_onConfigurationOverridesUpdated({ data: { current } }) {
|
|
this.#selector.setConfigOverrides(JSON.stringify({ data: current }));
|
|
|
|
lazy.logConsole.debug("Search configuration overrides updated remotely");
|
|
if (this.#changeListener) {
|
|
this.#changeListener();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtains the configuration overrides from remote settings.
|
|
*
|
|
* @returns {Promise<object[]>}
|
|
* An array of objects in the database, or an empty array if none
|
|
* could be obtained.
|
|
*/
|
|
async #getConfigurationOverrides() {
|
|
let result = [];
|
|
try {
|
|
result = await this.#remoteConfigOverrides.get();
|
|
} catch (ex) {
|
|
// This data is remote only, so we just return an empty array if it fails.
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @type {InstanceType<typeof lazy.SearchEngineSelector>?}
|
|
*/
|
|
#cachedSelector = null;
|
|
|
|
/**
|
|
* Returns the Rust based selector.
|
|
*
|
|
* @returns {InstanceType<typeof lazy.SearchEngineSelector>}
|
|
*/
|
|
get #selector() {
|
|
if (!this.#cachedSelector) {
|
|
this.#cachedSelector = lazy.SearchEngineSelector.init();
|
|
}
|
|
return this.#cachedSelector;
|
|
}
|
|
|
|
/**
|
|
* Converts the update channel from a string into a type the search engine
|
|
* selector can understand.
|
|
*
|
|
* @param {string} channel
|
|
* The channel name to convert.
|
|
* @returns {Values<typeof lazy.SearchUpdateChannel>}
|
|
*/
|
|
#convertUpdateChannel(channel) {
|
|
let uppercaseChannel = channel.toUpperCase();
|
|
|
|
if (uppercaseChannel in lazy.SearchUpdateChannel) {
|
|
return lazy.SearchUpdateChannel[uppercaseChannel];
|
|
}
|
|
|
|
return lazy.SearchUpdateChannel.DEFAULT;
|
|
}
|
|
|
|
/**
|
|
* Converts the application name from a string into a type the search engine
|
|
* selector can understand.
|
|
*
|
|
* @param {string} appName
|
|
* The application name to convert.
|
|
* @returns {Values<typeof lazy.SearchApplicationName>}
|
|
*/
|
|
#convertApplicationName(appName) {
|
|
let uppercaseAppName = appName.toUpperCase().replace("-", "_");
|
|
|
|
if (uppercaseAppName in lazy.SearchApplicationName) {
|
|
return lazy.SearchApplicationName[uppercaseAppName];
|
|
}
|
|
return lazy.SearchApplicationName.FIREFOX;
|
|
}
|
|
}
|