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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
const USER_LOCALE = "$USER_LOCALE";
const USER_REGION = "$USER_REGION";
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 SearchEngineSelectorOld {
/**
* @param {Function} listener
* A listener for configuration update changes.
*/
constructor(listener) {
this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
this._remoteConfig = lazy.RemoteSettings(lazy.SearchUtils.OLD_SETTINGS_KEY);
this._remoteConfigOverrides = lazy.RemoteSettings(
lazy.SearchUtils.OLD_SETTINGS_OVERRIDES_KEY
);
this._listenerAdded = false;
this._onConfigurationUpdated = this._onConfigurationUpdated.bind(this);
this._onConfigurationOverridesUpdated =
this._onConfigurationOverridesUpdated.bind(this);
this._changeListener = listener;
}
/**
* Resets the remote settings listeners.
*/
reset() {
if (this._listenerAdded) {
this._remoteConfig.off("sync", this._onConfigurationUpdated);
this._remoteConfigOverrides.off(
"sync",
this._onConfigurationOverridesUpdated
);
this._listenerAdded = false;
}
}
/**
* Handles getting the configuration from remote settings.
*/
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._configurationOverrides = remoteSettingsData[1];
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._remoteConfigOverrides.on(
"sync",
this._onConfigurationOverridesUpdated
);
this._listenerAdded = true;
}
return this._configuration;
}
/**
* Used by tests to get the configuration overrides.
*/
async getEngineConfigurationOverrides() {
await this.getEngineConfiguration();
return this._configurationOverrides;
}
/**
* 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();
}
}
/**
* 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
*/
_onConfigurationOverridesUpdated({ data: { current } }) {
this._configurationOverrides = current;
lazy.logConsole.debug("Search configuration overrides updated remotely");
if (this._changeListener) {
this._changeListener();
}
}
/**
* Obtains the configuration overrides from remote settings.
*
* @returns {Array}
* 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;
}
/**
* @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.name]
* 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,
name = Services.appinfo.name ?? "",
version = Services.appinfo.version ?? "",
}) {
if (!this._configuration) {
await this.getEngineConfiguration();
}
lazy.logConsole.debug(
`fetchEngineConfiguration ${locale}:${region}:${channel}:${distroID}:${experiment}:${name}:${version}`
);
let engines = [];
const lcName = name.toLowerCase();
const lcVersion = version.toLowerCase();
const lcLocale = locale.toLowerCase();
const lcRegion = region.toLowerCase();
for (let config of this._configuration) {
const appliesTo = config.appliesTo || [];
const applies = appliesTo.filter(section => {
if ("experiment" in section) {
if (experiment != section.experiment) {
return false;
}
if (section.override) {
return true;
}
}
let shouldInclude = () => {
let included =
"included" in section &&
this._isInSection(lcRegion, lcLocale, section.included);
let excluded =
"excluded" in section &&
this._isInSection(lcRegion, lcLocale, section.excluded);
return included && !excluded;
};
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", lcName) ||
distroExcluded ||
belowMinVersion(section, lcVersion) ||
aboveMaxVersion(section, lcVersion)
) {
return false;
}
return shouldInclude();
});
let baseConfig = this._copyObject({}, config);
// Don't include any engines if every section is an override
// entry, these are only supposed to override otherwise
// included engine configurations.
let allOverrides = applies.every(e => "override" in e && e.override);
// Loop through all the appliedTo sections that apply to
// this configuration.
if (applies.length && !allOverrides) {
for (let section of applies) {
this._copyObject(baseConfig, section);
}
if (
"webExtension" in baseConfig &&
"locales" in baseConfig.webExtension
) {
for (const webExtensionLocale of baseConfig.webExtension.locales) {
const engine = { ...baseConfig };
engine.webExtension = { ...baseConfig.webExtension };
delete engine.webExtension.locales;
engine.webExtension.locale = webExtensionLocale
.replace(USER_LOCALE, locale)
.replace(USER_REGION, lcRegion);
engines.push(engine);
}
} else {
const engine = { ...baseConfig };
(engine.webExtension = engine.webExtension || {}).locale =
lazy.SearchUtils.DEFAULT_TAG;
engines.push(engine);
}
}
}
let defaultEngine;
let privateEngine;
function shouldPrefer(setting, hasCurrentDefault, currentDefaultSetting) {
if (
setting == "yes" &&
(!hasCurrentDefault || currentDefaultSetting == "yes-if-no-other")
) {
return true;
}
return setting == "yes-if-no-other" && !hasCurrentDefault;
}
for (const engine of engines) {
engine.telemetryId = engine.telemetryId
?.replace(USER_LOCALE, locale)
.replace(USER_REGION, lcRegion);
if (
"default" in engine &&
shouldPrefer(
engine.default,
!!defaultEngine,
defaultEngine && defaultEngine.default
)
) {
defaultEngine = engine;
}
if (engine.telemetryId) {
for (let override of this._configurationOverrides) {
if (override.telemetryId == engine.telemetryId) {
engine.params = this._copyObject(
engine.params || {},
override.params
);
if (override.clickUrl) {
engine.clickUrl = override.clickUrl;
}
if (override.telemetrySuffix) {
engine.telemetryId += `-${override.telemetrySuffix}`;
}
}
}
}
if (
"defaultPrivate" in engine &&
shouldPrefer(
engine.defaultPrivate,
!!privateEngine,
privateEngine && privateEngine.defaultPrivate
)
) {
privateEngine = engine;
}
}
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.webExtension.id)
);
}
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 to copy from.
* @returns {object} - The source object.
*/
_copyObject(target, source) {
for (let key in source) {
if (["included", "excluded", "appliesTo"].includes(key)) {
continue;
}
if (key == "webExtension") {
if (key in target) {
this._copyObject(target[key], source[key]);
} else {
target[key] = { ...source[key] };
}
} else {
target[key] = source[key];
}
}
return target;
}
/**
* Determines wether the section of the config applies to a user
* given what region + locale they are using.
*
* @param {string} region - The region the user is in.
* @param {string} locale - The language the user has configured.
* @param {object} config - Section of configuration.
* @returns {boolean} - Does the section apply for the region + locale.
*/
_isInSection(region, locale, config) {
if (!config) {
return false;
}
if (config.everywhere) {
return true;
}
let locales = config.locales || {};
let inLocales =
"matches" in locales &&
!!locales.matches.find(e => e.toLowerCase() == locale);
let inRegions =
"regions" in config &&
!!config.regions.find(e => e.toLowerCase() == region);
if (
locales.startsWith &&
locales.startsWith.some(key => locale.startsWith(key))
) {
inLocales = true;
}
if (config.locales && config.regions) {
return inLocales && inRegions;
}
return inLocales || inRegions;
}
}