448 lines
13 KiB
JavaScript
448 lines
13 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
|
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
|
|
return console.createInstance({
|
|
prefix: "SearchEngineSelector",
|
|
maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
|
|
});
|
|
});
|
|
|
|
// function hasAppKey(config, key) {
|
|
// return "application" in config && key in config.application;
|
|
// }
|
|
|
|
// function sectionExcludes(config, key, value) {
|
|
// return hasAppKey(config, key) && !config.application[key].includes(value);
|
|
// }
|
|
|
|
// function sectionIncludes(config, key, value) {
|
|
// return hasAppKey(config, key) && config.application[key].includes(value);
|
|
// }
|
|
|
|
// function isDistroExcluded(config, key, distroID) {
|
|
// // Should be excluded when:
|
|
// // - There's a distroID and that is not in the non-empty distroID list.
|
|
// // - There's no distroID and the distroID list is not empty.
|
|
// const appKey = hasAppKey(config, key);
|
|
// if (!appKey) {
|
|
// return false;
|
|
// }
|
|
// const distroList = config.application[key];
|
|
// if (distroID) {
|
|
// return distroList.length && !distroList.includes(distroID);
|
|
// }
|
|
// return !!distroList.length;
|
|
// }
|
|
|
|
// function belowMinVersion(config, version) {
|
|
// return (
|
|
// hasAppKey(config, "minVersion") &&
|
|
// Services.vc.compare(version, config.application.minVersion) < 0
|
|
// );
|
|
// }
|
|
|
|
// function aboveMaxVersion(config, version) {
|
|
// return (
|
|
// hasAppKey(config, "maxVersion") &&
|
|
// Services.vc.compare(version, config.application.maxVersion) > 0
|
|
// );
|
|
// }
|
|
|
|
/**
|
|
* SearchEngineSelector parses the JSON configuration for
|
|
* search engines and returns the applicable engines depending
|
|
* on their region + locale.
|
|
*/
|
|
export class SearchEngineSelector {
|
|
/**
|
|
* @param {Function} listener
|
|
* A listener for configuration update changes.
|
|
*/
|
|
constructor(listener) {
|
|
this._remoteConfig = lazy.RemoteSettings(lazy.SearchUtils.NEW_SETTINGS_KEY);
|
|
this._listenerAdded = false;
|
|
this._onConfigurationUpdated = this._onConfigurationUpdated.bind(this);
|
|
this._changeListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Handles getting the configuration from remote settings.
|
|
*/
|
|
async getEngineConfiguration() {
|
|
if (this._getConfigurationPromise) {
|
|
return this._getConfigurationPromise;
|
|
}
|
|
|
|
this._configuration = await (this._getConfigurationPromise =
|
|
this._getConfiguration());
|
|
delete this._getConfigurationPromise;
|
|
|
|
if (!this._configuration?.length) {
|
|
throw Components.Exception(
|
|
"Failed to get engine data from Remote Settings",
|
|
Cr.NS_ERROR_UNEXPECTED
|
|
);
|
|
}
|
|
|
|
if (!this._listenerAdded) {
|
|
this._remoteConfig.on("sync", this._onConfigurationUpdated);
|
|
this._listenerAdded = true;
|
|
}
|
|
|
|
return this._configuration;
|
|
}
|
|
|
|
/**
|
|
* 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 {Array}
|
|
* 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.
|
|
*
|
|
* @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;
|
|
lazy.logConsole.debug("Search configuration updated remotely");
|
|
if (this._changeListener) {
|
|
this._changeListener();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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 {object}
|
|
* An object with "engines" field, a sorted list of engines and
|
|
* optionally "privateDefault" which is an object containing the engine
|
|
* details for the engine which should be the default in Private Browsing mode.
|
|
*/
|
|
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 engines = [];
|
|
|
|
appName = appName.toLowerCase();
|
|
version = version.toLowerCase();
|
|
locale = locale.toLowerCase();
|
|
region = region.toLowerCase();
|
|
|
|
for (let config of this._configuration) {
|
|
if (config.recordType !== "engine") {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
this.#matchesUserEnvironment(config.base, {
|
|
appName,
|
|
version,
|
|
locale,
|
|
region,
|
|
channel,
|
|
distroID,
|
|
experiment,
|
|
})
|
|
) {
|
|
let engine = this.#copyObject({}, config.base);
|
|
engine.identifier = config.identifier;
|
|
engines.push(engine);
|
|
}
|
|
}
|
|
|
|
// let defaultEngine;
|
|
// let privateEngine;
|
|
|
|
// engines.sort(this._sort.bind(this, defaultEngine, privateEngine));
|
|
|
|
let result = { engines };
|
|
|
|
// if (privateEngine) {
|
|
// result.privateDefault = privateEngine;
|
|
// }
|
|
|
|
if (lazy.SearchUtils.loggingEnabled) {
|
|
lazy.logConsole.debug(
|
|
"fetchEngineConfiguration: " + result.engines.map(e => e.identifier)
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
_sort(defaultEngine, privateEngine, a, b) {
|
|
return (
|
|
this._sortIndex(b, defaultEngine, privateEngine) -
|
|
this._sortIndex(a, defaultEngine, privateEngine)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create an index order to ensure default (and backup default)
|
|
* engines are ordered correctly.
|
|
*
|
|
* @param {object} obj
|
|
* Object representing the engine configation.
|
|
* @param {object} defaultEngine
|
|
* The default engine, for comparison to obj.
|
|
* @param {object} privateEngine
|
|
* The private engine, for comparison to obj.
|
|
* @returns {integer}
|
|
* Number indicating how this engine should be sorted.
|
|
*/
|
|
_sortIndex(obj, defaultEngine, privateEngine) {
|
|
if (obj == defaultEngine) {
|
|
return Number.MAX_SAFE_INTEGER;
|
|
}
|
|
if (obj == privateEngine) {
|
|
return Number.MAX_SAFE_INTEGER - 1;
|
|
}
|
|
return obj.orderHint || 0;
|
|
}
|
|
|
|
/**
|
|
* Is the engine marked to be the default search engine.
|
|
*
|
|
* @param {object} obj - Object representing the engine configation.
|
|
* @returns {boolean} - Whether the engine should be default.
|
|
*/
|
|
_isDefault(obj) {
|
|
return "default" in obj && obj.default === "yes";
|
|
}
|
|
|
|
/**
|
|
* Object.assign but ignore some keys
|
|
*
|
|
* @param {object} target - Object to copy to.
|
|
* @param {object} source - Object top copy from.
|
|
* @returns {object} - The source object.
|
|
*/
|
|
#copyObject(target, source) {
|
|
for (let sourceKey in source) {
|
|
if (["environment"].includes(sourceKey)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
typeof source[sourceKey] == "object" &&
|
|
!Array.isArray(source[sourceKey])
|
|
) {
|
|
if (sourceKey in target) {
|
|
this.#copyObject(target[sourceKey], source[sourceKey]);
|
|
} else {
|
|
target[sourceKey] = { ...source[sourceKey] };
|
|
}
|
|
} else {
|
|
target[sourceKey] = source[sourceKey];
|
|
}
|
|
}
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* Matches the user's environment against the engine config's environment.
|
|
*
|
|
* @param {object} config
|
|
* The config for the given base or variant engine.
|
|
* @param {object} user
|
|
* The user's environment we use to match with the engine's environment.
|
|
* @param {string} user.appName
|
|
* The name of the application.
|
|
* @param {string} user.version
|
|
* The version of the application.
|
|
* @param {string} user.locale
|
|
* The locale of the user.
|
|
* @param {string} user.region
|
|
* The region of the user.
|
|
* @param {string} user.channel
|
|
* The channel the application is running on.
|
|
* @param {string} user.distroID
|
|
* The distribution ID of the application.
|
|
* @param {string} user.experiment
|
|
* Any associated experiment id.
|
|
* @returns {boolean}
|
|
* True if the engine config's environment matches the user's environment.
|
|
*/
|
|
#matchesUserEnvironment(config, user = {}) {
|
|
if ("experiment" in config.environment) {
|
|
if (user.experiment != config.environment.experiment) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// const distroExcluded =
|
|
// (distroID &&
|
|
// sectionIncludes(section, "excludedDistributions", distroID)) ||
|
|
// isDistroExcluded(section, "distributions", distroID);
|
|
// if (distroID && !distroExcluded && section.override) {
|
|
// if ("included" in section || "excluded" in section) {
|
|
// return shouldInclude();
|
|
// }
|
|
// return true;
|
|
// }
|
|
// if (
|
|
// sectionExcludes(section, "channel", channel) ||
|
|
// sectionExcludes(section, "name", lcAppName) ||
|
|
// distroExcluded ||
|
|
// belowMinVersion(section, lcVersion) ||
|
|
// aboveMaxVersion(section, lcVersion)
|
|
// ) {
|
|
// return false;
|
|
// }
|
|
|
|
return this.#matchesRegionAndLocale(
|
|
user.region,
|
|
user.locale,
|
|
config.environment
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Determines whether the region and locale constraints in the config
|
|
* environment applies to a user given what region and locale they are using.
|
|
*
|
|
* @param {string} region
|
|
* The region the user is in.
|
|
* @param {string} locale
|
|
* The language the user has configured.
|
|
* @param {object} configEnv
|
|
* The environment of the engine configuration.
|
|
* @returns {boolean}
|
|
* True if the user's region and locale matches the config's region and
|
|
* locale contraints. Otherwise false.
|
|
*/
|
|
#matchesRegionAndLocale(region, locale, configEnv) {
|
|
if (
|
|
this.#doesConfigInclude(configEnv.excludedLocales, locale) ||
|
|
this.#doesConfigInclude(configEnv.excludedRegions, region)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (configEnv.allRegionsAndLocales) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
this.#doesConfigInclude(configEnv?.locales, locale) &&
|
|
this.#doesConfigInclude(configEnv?.regions, region)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
this.#doesConfigInclude(configEnv?.locales, locale) &&
|
|
!Object.hasOwn(configEnv, "regions")
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
this.#doesConfigInclude(configEnv?.regions, region) &&
|
|
!Object.hasOwn(configEnv, "locales")
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This function converts the characters in the config to lowercase and
|
|
* checks if the user's locale or region is included in config the
|
|
* environment.
|
|
*
|
|
* @param {Array} configArray
|
|
* An Array of locales or regions from the config environment.
|
|
* @param {string} compareItem
|
|
* The user's locale or region.
|
|
* @returns {boolean}
|
|
* True if user's region or locale is found in the config environment.
|
|
* Otherwise false.
|
|
*/
|
|
#doesConfigInclude(configArray, compareItem) {
|
|
if (!configArray) {
|
|
return false;
|
|
}
|
|
|
|
return configArray.find(
|
|
configItem => configItem.toLowerCase() === compareItem
|
|
);
|
|
}
|
|
}
|