With the previous patch on this bug, these preventions are no longer necessary. Whilst we could keep them, it means that the behaviour of configuration engines are different to other engines. It might also prevent internal updates happening, which we might need sometimes (e.g. for bug 2043189). In future we might want to do something more extensive with not sending out unnecessary internal notifications, however these are generally not hot paths, so that might be something we think about at an opportune time. Differential Revision: https://phabricator.services.mozilla.com/D310847
810 lines
24 KiB
JavaScript
810 lines
24 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/. */
|
|
|
|
/* eslint no-shadow: error, mozilla/no-aArgs: error */
|
|
|
|
/**
|
|
* @typedef {import("resource://services-settings/RemoteSettingsClient.sys.mjs").RemoteSettingsClient} RemoteSettingsClient
|
|
* @typedef {import("../uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs").SearchEngineDefinition} SearchEngineDefinition
|
|
*/
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
import {
|
|
SearchEngine,
|
|
EngineURL,
|
|
QueryParameter,
|
|
} from "moz-src:///toolkit/components/search/SearchEngine.sys.mjs";
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = XPCOMUtils.declareLazy({
|
|
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
|
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
|
SearchEngineClassification:
|
|
"moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSearch.sys.mjs",
|
|
SearchService: "moz-src:///toolkit/components/search/SearchService.sys.mjs",
|
|
SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
|
|
idleService: {
|
|
service: "@mozilla.org/widget/useridleservice;1",
|
|
iid: Ci.nsIUserIdleService,
|
|
},
|
|
logConsole: () =>
|
|
console.createInstance({
|
|
prefix: "SearchEngine",
|
|
maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
|
|
}),
|
|
});
|
|
|
|
// If a user used a search engine at least once, we'll toggle a boolean.
|
|
const HAS_BEEN_USED = "hasBeenUsed";
|
|
|
|
// After the user has been idle for 30s, we'll update icons if we need to.
|
|
const ICON_UPDATE_ON_IDLE_DELAY = 30;
|
|
|
|
/**
|
|
* Handles loading config search engine icons from remote settings.
|
|
*/
|
|
class IconHandler {
|
|
/**
|
|
* The remote settings client for the search engine icons.
|
|
*
|
|
* @type {?RemoteSettingsClient}
|
|
*/
|
|
#iconCollection = null;
|
|
|
|
/**
|
|
* The list of icon records from the remote settings collection indexed by
|
|
* the first two characters of their engineIdentifier for fast search by
|
|
* engineID.
|
|
*
|
|
* If a record has multiple engineIdentifiers with different
|
|
* first characters, the record will be available once under every key.
|
|
*
|
|
* @type {?Map<string, object[]>}
|
|
*/
|
|
#iconMap = null;
|
|
|
|
/**
|
|
* A flag that indicates if we have queued an idle observer to update icons.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#queuedIdle = false;
|
|
|
|
/**
|
|
* A map of pending updates that need to be applied to the engines. This is
|
|
* keyed via record id, so that if multiple updates are queued for the same
|
|
* record, then we will only update the engine once.
|
|
*
|
|
* @type {Map<string, object>}
|
|
*/
|
|
#pendingUpdatesMap = new Map();
|
|
|
|
constructor() {
|
|
this.#iconCollection = lazy.RemoteSettings("search-config-icons");
|
|
this.#iconCollection.on("sync", this._onIconListUpdated.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Extracts the first two chars of an engineID for use with `this.#iconMap`.
|
|
*
|
|
* @param {string} engineID
|
|
* ID of the engine.
|
|
* @returns {string}
|
|
* The key used by `this.#iconMap`.
|
|
*/
|
|
getKey(engineID) {
|
|
return engineID.substring(0, 2);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of the sizes of the records available for the supplied engine.
|
|
*
|
|
* @param {string} engineIdentifier
|
|
* The ID of the engine.
|
|
* @returns {Promise<object[]>}
|
|
* The available records.
|
|
*/
|
|
async getAvailableRecords(engineIdentifier) {
|
|
if (!this.#iconMap) {
|
|
await this.#buildIconMap();
|
|
}
|
|
|
|
let iconList = this.#iconMap.get(this.getKey(engineIdentifier)) || [];
|
|
return iconList.filter(r =>
|
|
this.#identifierMatches(engineIdentifier, r.engineIdentifiers)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Creates an object URL for the icon of the given record.
|
|
*
|
|
* @param {object} iconRecord
|
|
* The record of the icon.
|
|
* @returns {Promise<?string>}
|
|
* An object URL that can be used to reference the contents of the specified
|
|
* source object or null of there is no icon with the supplied width.
|
|
*/
|
|
async createIconURL(iconRecord) {
|
|
let iconData;
|
|
try {
|
|
iconData = await this.#iconCollection.attachments.get(iconRecord);
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
if (!iconData) {
|
|
console.warn("Unable to find the attachment for", iconRecord.id);
|
|
// Queue an update in case we haven't downloaded it yet.
|
|
this.#pendingUpdatesMap.set(iconRecord.id, iconRecord);
|
|
this.#maybeQueueIdle();
|
|
return null;
|
|
}
|
|
|
|
if (iconData.record.last_modified != iconRecord.last_modified) {
|
|
// The icon we have stored is out of date, queue an update so that we'll
|
|
// download the new icon.
|
|
this.#pendingUpdatesMap.set(iconRecord.id, iconRecord);
|
|
this.#maybeQueueIdle();
|
|
}
|
|
return URL.createObjectURL(
|
|
new Blob([iconData.buffer], { type: iconRecord.attachment.mimetype })
|
|
);
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
|
|
|
|
/**
|
|
* Called when there is an update queued and the user has been observed to be
|
|
* idle for ICON_UPDATE_ON_IDLE_DELAY seconds.
|
|
*
|
|
* This will always download new icons (added or updated), even if there is
|
|
* no current engine that matches the identifiers. This is to ensure that we
|
|
* have pre-populated the cache if the engine is added later for this user.
|
|
*
|
|
* We do not handle deletes, as remote settings will handle the cleanup of
|
|
* removed records. We also do not expect the case where an icon is removed
|
|
* for an active engine.
|
|
*
|
|
* @param {nsISupports} subject
|
|
* The subject of the observer.
|
|
* @param {string} topic
|
|
* The topic of the observer.
|
|
*/
|
|
async observe(subject, topic) {
|
|
if (topic != "idle") {
|
|
return;
|
|
}
|
|
|
|
this.#queuedIdle = false;
|
|
lazy.idleService.removeIdleObserver(this, ICON_UPDATE_ON_IDLE_DELAY);
|
|
|
|
// Update the icon list, in case engines will call getIcon() again.
|
|
await this.#buildIconMap();
|
|
|
|
let appProvidedEngines = await lazy.SearchService.getAppProvidedEngines();
|
|
for (let record of this.#pendingUpdatesMap.values()) {
|
|
let iconData;
|
|
try {
|
|
iconData = await this.#iconCollection.attachments.download(record);
|
|
} catch (ex) {
|
|
console.error("Could not download new icon", ex);
|
|
continue;
|
|
}
|
|
|
|
for (let engine of appProvidedEngines) {
|
|
if (this.#identifierMatches(engine.id, record.engineIdentifiers)) {
|
|
await engine.maybeUpdateIconURL(
|
|
URL.createObjectURL(
|
|
new Blob([iconData.buffer], {
|
|
type: record.attachment.mimetype,
|
|
})
|
|
),
|
|
record.imageSize
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.#pendingUpdatesMap.clear();
|
|
}
|
|
|
|
/**
|
|
* Checks if the identifier matches any of the engine identifiers.
|
|
*
|
|
* @param {string} identifier
|
|
* The identifier of the engine.
|
|
* @param {string[]} engineIdentifiers
|
|
* The list of engine identifiers to match against. This can include
|
|
* wildcards at the end of strings.
|
|
* @returns {boolean}
|
|
* Returns true if the identifier matches any of the engine identifiers.
|
|
*/
|
|
#identifierMatches(identifier, engineIdentifiers) {
|
|
return engineIdentifiers.some(i => {
|
|
if (i.endsWith("*")) {
|
|
return identifier.startsWith(i.slice(0, -1));
|
|
}
|
|
return identifier == i;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtains the icon list from the remote settings collection.
|
|
*/
|
|
async #buildIconMap() {
|
|
let iconList = [];
|
|
try {
|
|
iconList = await this.#iconCollection.get();
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
if (!iconList.length) {
|
|
console.error("Failed to obtain search engine icon list records");
|
|
}
|
|
|
|
this.#iconMap = new Map();
|
|
for (let record of iconList) {
|
|
let keys = new Set(record.engineIdentifiers.map(this.getKey));
|
|
for (let key of keys) {
|
|
if (this.#iconMap.has(key)) {
|
|
this.#iconMap.get(key).push(record);
|
|
} else {
|
|
this.#iconMap.set(key, [record]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called via a callback when remote settings updates the icon list. This
|
|
* stores potential updates and queues an idle observer to apply them.
|
|
*
|
|
* @param {object} payload
|
|
* The payload from the remote settings collection.
|
|
* @param {object} payload.data
|
|
* The payload data from the remote settings collection.
|
|
* @param {object[]} payload.data.created
|
|
* The list of created records.
|
|
* @param {object[]} payload.data.updated
|
|
* The list of updated records.
|
|
*/
|
|
async _onIconListUpdated({ data: { created, updated } }) {
|
|
created.forEach(record => {
|
|
this.#pendingUpdatesMap.set(record.id, record);
|
|
});
|
|
for (let record of updated) {
|
|
if (record.new) {
|
|
this.#pendingUpdatesMap.set(record.new.id, record.new);
|
|
}
|
|
}
|
|
this.#maybeQueueIdle();
|
|
}
|
|
|
|
/**
|
|
* Queues an idle observer if there are pending updates.
|
|
*/
|
|
#maybeQueueIdle() {
|
|
if (this.#pendingUpdatesMap && !this.#queuedIdle) {
|
|
this.#queuedIdle = true;
|
|
lazy.idleService.addIdleObserver(this, ICON_UPDATE_ON_IDLE_DELAY);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A simple class to handle caching of preferences that may be read from
|
|
* parameters.
|
|
*/
|
|
const ParamPreferenceCache = {
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
]),
|
|
|
|
initCache() {
|
|
// Preference params are normally only on the default branch to avoid these being easily changed.
|
|
// We allow them on the normal branch in nightly builds to make testing easier.
|
|
let branchFetcher = AppConstants.NIGHTLY_BUILD
|
|
? "getBranch"
|
|
: "getDefaultBranch";
|
|
this.branch = Services.prefs[branchFetcher](
|
|
lazy.SearchUtils.BROWSER_SEARCH_PREF + "param."
|
|
);
|
|
this.cache = new Map();
|
|
this.nimbusCache = new Map();
|
|
for (let prefName of this.branch.getChildList("")) {
|
|
this.cache.set(prefName, this.branch.getCharPref(prefName, null));
|
|
}
|
|
this.branch.addObserver("", this, true);
|
|
|
|
this.onNimbusUpdate = this.onNimbusUpdate.bind(this);
|
|
this.onNimbusUpdate();
|
|
lazy.NimbusFeatures.searchConfiguration.onUpdate(this.onNimbusUpdate);
|
|
lazy.NimbusFeatures.searchConfiguration.ready().then(this.onNimbusUpdate);
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
this.cache.set(data, this.branch.getCharPref(data, null));
|
|
},
|
|
|
|
onNimbusUpdate() {
|
|
let extraParams =
|
|
lazy.NimbusFeatures.searchConfiguration.getVariable("extraParams") || [];
|
|
this.nimbusCache.clear();
|
|
// The try catch ensures that if the params were incorrect for some reason,
|
|
// the search service can still startup properly.
|
|
try {
|
|
for (const { key, value } of extraParams) {
|
|
this.nimbusCache.set(key, value);
|
|
}
|
|
} catch (ex) {
|
|
console.error("Failed to load nimbus variables for extraParams:", ex);
|
|
}
|
|
},
|
|
|
|
getPref(prefName) {
|
|
if (!this.cache) {
|
|
this.initCache();
|
|
}
|
|
return this.nimbusCache.has(prefName)
|
|
? this.nimbusCache.get(prefName)
|
|
: this.cache.get(prefName);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents a special paramater that can be set by preferences. The
|
|
* value is read from the 'browser.search.param.*' default preference
|
|
* branch.
|
|
*/
|
|
class QueryPreferenceParameter extends QueryParameter {
|
|
/**
|
|
* @param {string} name
|
|
* The name of the parameter as injected into the query string.
|
|
* @param {string} prefName
|
|
* The name of the preference to read from the branch.
|
|
*/
|
|
constructor(name, prefName) {
|
|
super(name, prefName);
|
|
}
|
|
|
|
get value() {
|
|
const prefValue = ParamPreferenceCache.getPref(this._value);
|
|
return prefValue ? encodeURIComponent(prefValue) : null;
|
|
}
|
|
|
|
/**
|
|
* Creates a JavaScript object that represents this parameter.
|
|
*
|
|
* @returns {object}
|
|
* An object suitable for serialization as JSON.
|
|
*/
|
|
toJSON() {
|
|
lazy.logConsole.warn(
|
|
"QueryPreferenceParameter should only exist for config engines which are never saved as JSON"
|
|
);
|
|
return {
|
|
condition: "pref",
|
|
name: this.name,
|
|
pref: this._value,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ConfigSearchEngine represents a search engine defined by the search-config-v2.
|
|
* This class is intended as an abstract class.
|
|
*/
|
|
export class ConfigSearchEngine extends SearchEngine {
|
|
static URL_TYPE_MAP = new Map([
|
|
["search", lazy.SearchUtils.URL_TYPE.SEARCH],
|
|
["suggestions", lazy.SearchUtils.URL_TYPE.SUGGEST_JSON],
|
|
["trending", lazy.SearchUtils.URL_TYPE.TRENDING_JSON],
|
|
["searchForm", lazy.SearchUtils.URL_TYPE.SEARCH_FORM],
|
|
["visualSearch", lazy.SearchUtils.URL_TYPE.VISUAL_SEARCH],
|
|
]);
|
|
static iconHandler = new IconHandler();
|
|
|
|
/**
|
|
* Promises for the blob URL of the icon by icon width.
|
|
* We save the promises to avoid reentrancy issues.
|
|
*
|
|
* @type {Map<number, Promise<?string>>}
|
|
*/
|
|
#blobURLPromises = new Map();
|
|
|
|
/**
|
|
* Whether or not this is a general purpose search engine.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#isGeneralPurposeSearchEngine = false;
|
|
|
|
#partnerCode = "";
|
|
|
|
/**
|
|
* The telemetry id to use for this engine for legacy telemetry. This is
|
|
* deprecated and should not be used for new telemetry.
|
|
*
|
|
* @type {string}
|
|
*/
|
|
#telemetryId;
|
|
|
|
/**
|
|
* The order hint for this engine, as determined by the search configuration.
|
|
*
|
|
* @type {?number}
|
|
*/
|
|
#orderHint;
|
|
|
|
/**
|
|
* @param {object} options
|
|
* The options for this search engine.
|
|
* @param {SearchEngineDefinition} options.config
|
|
* The engine config from Remote Settings.
|
|
* @param {object} [options.settings]
|
|
* The saved settings for the user.
|
|
*/
|
|
constructor({ config, settings }) {
|
|
if (new.target === ConfigSearchEngine) {
|
|
throw new Error("Cannot instanciate abstract class ConfigSearchEngine");
|
|
}
|
|
// The load path for both all types of ConfigSearchEngine is prefixed
|
|
// with [app] for historical reasons.
|
|
super({
|
|
loadPath: "[app]" + config.identifier,
|
|
id: config.identifier,
|
|
});
|
|
|
|
this.#init(config);
|
|
this._loadSettings(settings);
|
|
}
|
|
|
|
/**
|
|
* Used to clean up the engine when it is removed. This will revoke the blob
|
|
* URL for the icon.
|
|
*/
|
|
async cleanup() {
|
|
for (let [size, blobURLPromise] of this.#blobURLPromises) {
|
|
URL.revokeObjectURL(await blobURLPromise);
|
|
this.#blobURLPromises.delete(size);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update this engine based on new config, used during config upgrades.
|
|
*
|
|
* @param {object} options
|
|
* The options object.
|
|
* @param {SearchEngineDefinition} options.configuration
|
|
* The updated engine definition for this engine from the search config.
|
|
*/
|
|
update({ configuration }) {
|
|
this._urls = [];
|
|
this.#init(configuration);
|
|
|
|
lazy.SearchUtils.notifyAction(this, lazy.SearchUtils.MODIFIED_TYPE.CHANGED);
|
|
}
|
|
|
|
/**
|
|
* Whether or not this engine is an in-memory only search engine.
|
|
* These engines are typically config or policy engines,
|
|
* where they are loaded every time on SearchService initialization
|
|
* using the search config or the policy JSON. Minimal details of the
|
|
* in-memory engines are saved to disk, but they are never loaded
|
|
* from the user's saved settings file.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
get inMemory() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether or not this engine is a "general" search engine, e.g. is it for
|
|
* generally searching the web, or does it have a specific purpose like
|
|
* shopping.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
get isGeneralPurposeEngine() {
|
|
return this.#isGeneralPurposeSearchEngine;
|
|
}
|
|
|
|
/**
|
|
* @type {string}
|
|
* The partner code being used by this search engine in the Search URL.
|
|
*/
|
|
get partnerCode() {
|
|
return this.#partnerCode;
|
|
}
|
|
|
|
/**
|
|
* Returns the appropriate identifier to use for telemetry. It is based on
|
|
* the following order:
|
|
*
|
|
* - telemetryId: The telemetry id from the configuration, or derived from
|
|
* the WebExtension name.
|
|
* - other-<name>: The engine name prefixed by `other-` for non-config-engines.
|
|
*
|
|
* @returns {string}
|
|
* @deprecated This should not be used for new telemetry. It is a combined
|
|
* field that contains multiple values. Report separate
|
|
* id/partner_code/other fields instead.
|
|
*/
|
|
get telemetryId() {
|
|
if (this.getAttr("overriddenBy")) {
|
|
return this.#telemetryId + "-addon";
|
|
}
|
|
return this.#telemetryId;
|
|
}
|
|
|
|
/**
|
|
* Gets the order hint for this engine. This is determined from the search
|
|
* configuration when the engine is initialized.
|
|
*
|
|
* @type {?number}
|
|
*/
|
|
get orderHint() {
|
|
return this.#orderHint;
|
|
}
|
|
|
|
/**
|
|
* Returns the icon URL for the search engine closest to the preferred width.
|
|
*
|
|
* @param {number} preferredWidth
|
|
* The preferred width of the image. Defaults to 16.
|
|
* @returns {Promise<?string>}
|
|
* A promise that resolves to the URL of the icon.
|
|
*/
|
|
async getIconURL(preferredWidth) {
|
|
// XPCOM interfaces pass optional number parameters as 0.
|
|
preferredWidth ||= 16;
|
|
|
|
let availableRecords =
|
|
await ConfigSearchEngine.iconHandler.getAvailableRecords(this.id);
|
|
if (!availableRecords.length) {
|
|
console.warn("No icon found for", this.id);
|
|
return null;
|
|
}
|
|
|
|
let availableSizes = availableRecords.map(r => r.imageSize);
|
|
let width = lazy.SearchUtils.chooseIconSize(preferredWidth, availableSizes);
|
|
|
|
if (this.#blobURLPromises.has(width)) {
|
|
return this.#blobURLPromises.get(width);
|
|
}
|
|
|
|
let record = availableRecords.find(r => r.imageSize == width);
|
|
let promise = ConfigSearchEngine.iconHandler.createIconURL(record);
|
|
this.#blobURLPromises.set(width, promise);
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Updates the icon URL for the given size.
|
|
*
|
|
* @param {string} blobURL
|
|
* The new icon URL for the search engine.
|
|
* @param {number} size
|
|
* The size of the icon in blobURL.
|
|
*/
|
|
async maybeUpdateIconURL(blobURL, size) {
|
|
if (this.#blobURLPromises.has(size)) {
|
|
URL.revokeObjectURL(await this.#blobURLPromises.get(size));
|
|
}
|
|
this.#blobURLPromises.set(size, Promise.resolve(blobURL));
|
|
|
|
lazy.SearchUtils.notifyAction(
|
|
this,
|
|
lazy.SearchUtils.MODIFIED_TYPE.ICON_CHANGED
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Marks the search engine has having been used.
|
|
*/
|
|
markAsUsed() {
|
|
this.setAttr(HAS_BEEN_USED, true, true);
|
|
}
|
|
|
|
/**
|
|
* Whether this search engine has ever been used. This returns true if
|
|
* `markAsUsed()` has been called at least once for this engine.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
get hasBeenUsed() {
|
|
return this.getAttr(HAS_BEEN_USED) ?? false;
|
|
}
|
|
|
|
/**
|
|
* Clears the usage record for this search engine. The property hasBeenUsed
|
|
* will return false unless `markAsUsed()` is called again.
|
|
*/
|
|
clearUsage() {
|
|
this.clearAttr(HAS_BEEN_USED);
|
|
}
|
|
|
|
/**
|
|
* Creates a JavaScript object that represents this engine.
|
|
*
|
|
* @returns {object}
|
|
* An object suitable for serialization as JSON.
|
|
*/
|
|
toJSON() {
|
|
// For config engines we don't want to store all their data in
|
|
// the settings file so just store the relevant metadata.
|
|
return {
|
|
id: this.id,
|
|
_name: this.name,
|
|
_isConfigEngine: true,
|
|
_metaData: this._metaData,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initializes the engine.
|
|
*
|
|
* @param {SearchEngineDefinition} engineConfig
|
|
* The engine definition from the search config for this engine.
|
|
*/
|
|
#init(engineConfig) {
|
|
this.#orderHint = engineConfig.orderHint;
|
|
this.#telemetryId = engineConfig.identifier;
|
|
this.#isGeneralPurposeSearchEngine =
|
|
engineConfig.classification == lazy.SearchEngineClassification.GENERAL;
|
|
|
|
if (engineConfig.charset) {
|
|
this._queryCharset = engineConfig.charset;
|
|
}
|
|
|
|
if (engineConfig.telemetrySuffix) {
|
|
this.#telemetryId += `-${engineConfig.telemetrySuffix}`;
|
|
}
|
|
|
|
if (engineConfig.clickUrl) {
|
|
this.clickUrl = engineConfig.clickUrl;
|
|
}
|
|
|
|
this._name = engineConfig.name.trim();
|
|
this._definedAliases =
|
|
engineConfig.aliases?.map(alias => `@${alias}`) ?? [];
|
|
this.#partnerCode = engineConfig.partnerCode ?? "";
|
|
/** @type {string} */
|
|
this.isNewUntil = engineConfig.isNewUntil ?? "";
|
|
|
|
for (const [type, urlData] of Object.entries(engineConfig.urls)) {
|
|
if (urlData) {
|
|
this.#setUrl(type, urlData, engineConfig.partnerCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This sets the urls for the search engine based on the supplied parameters.
|
|
*
|
|
* @param {string} type
|
|
* The type of url. This could be a url for search, suggestions, or trending.
|
|
* @param {object} urlData
|
|
* The url data contains the template/base url and url params.
|
|
* @param {string} partnerCode
|
|
* The partner code associated with the search engine.
|
|
*/
|
|
#setUrl(type, urlData, partnerCode) {
|
|
let urlType = ConfigSearchEngine.URL_TYPE_MAP.get(type);
|
|
if (!urlType) {
|
|
console.warn("unexpected engine url type.", type);
|
|
return;
|
|
}
|
|
|
|
let engineURL = new EngineURL({
|
|
...urlData,
|
|
type: urlType,
|
|
template: urlData.base,
|
|
});
|
|
|
|
if (urlData.params) {
|
|
let isEnterprise = Services.policies.isEnterprise;
|
|
let enterpriseParams = urlData.params
|
|
.filter(p => p.enterpriseValue != undefined)
|
|
.map(p => p.name);
|
|
|
|
for (const param of urlData.params) {
|
|
switch (true) {
|
|
case param.value != undefined:
|
|
if (!isEnterprise || !enterpriseParams.includes(param.name)) {
|
|
engineURL.addParam(
|
|
param.name,
|
|
param.value == "{partnerCode}" ? partnerCode : param.value
|
|
);
|
|
}
|
|
break;
|
|
case param.experimentConfig != undefined:
|
|
if (!isEnterprise || !enterpriseParams.includes(param.name)) {
|
|
engineURL.addQueryParameter(
|
|
new QueryPreferenceParameter(param.name, param.experimentConfig)
|
|
);
|
|
}
|
|
break;
|
|
case param.enterpriseValue != undefined:
|
|
if (isEnterprise) {
|
|
engineURL.addParam(
|
|
param.name,
|
|
param.enterpriseValue == "{partnerCode}"
|
|
? partnerCode
|
|
: param.enterpriseValue
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (urlData.searchTermParamName) {
|
|
// The search term parameter is always added last, which will add it to the
|
|
// end of the URL. This is because in the past we have seen users trying to
|
|
// modify their searches by altering the end of the URL.
|
|
engineURL.setSearchTermParamName(urlData.searchTermParamName);
|
|
} else if (
|
|
!urlData.base.includes("{searchTerms}") &&
|
|
(urlType == lazy.SearchUtils.URL_TYPE.SEARCH ||
|
|
urlType == lazy.SearchUtils.URL_TYPE.SUGGEST_JSON)
|
|
) {
|
|
throw new Error("Search terms missing from engine URL.");
|
|
}
|
|
|
|
this._urls.push(engineURL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class represents ConfigSearchEngines that were pre-configured by the
|
|
* application based on the user's environment, rather than user-installed.
|
|
*/
|
|
export class AppProvidedConfigEngine extends ConfigSearchEngine {
|
|
/**
|
|
* Converts this engine into a UserInstalledConfigEngine.
|
|
*
|
|
* This can be called when the search service reloads and this engine is no
|
|
* longer available in the user's region but it has a "user-installed" attribute.
|
|
*/
|
|
downgrade() {
|
|
if (!this.getAttr("user-installed")) {
|
|
throw new Error("Cannot downgrade without user-installed attribute.");
|
|
}
|
|
Object.setPrototypeOf(this, UserInstalledConfigEngine.prototype);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class represents ConfigSearchEngines that were manually installed
|
|
* by the user, e.g. using the contextual search feature.
|
|
*/
|
|
export class UserInstalledConfigEngine extends ConfigSearchEngine {
|
|
/**
|
|
* @param {object} options
|
|
* The options for this search engine.
|
|
* @param {SearchEngineDefinition} options.config
|
|
* The engine config from Remote Settings.
|
|
* @param {object} [options.settings]
|
|
* The saved settings for the user.
|
|
*/
|
|
constructor(options) {
|
|
super(options);
|
|
this.setAttr("user-installed", true);
|
|
}
|
|
|
|
/**
|
|
* Converts this engine into a AppProvidedSearchEngine.
|
|
*
|
|
* This can be called when the search service reloads and this engine
|
|
* is now available in the user's region.
|
|
*/
|
|
upgrade() {
|
|
Object.setPrototypeOf(this, AppProvidedConfigEngine.prototype);
|
|
}
|
|
}
|