forked from mirrors/gecko-dev
For some other patches, we need to be able to have the user's saved engine settings loaded into the search engine objects striaght away. Currently, they are generally not loaded until sometime later, and in the case of engines loaded in `loadEnginesFromSettings`, they are actually loaded twice. Additionally, I think it makes more sense to pass the data in when they are constructed. This also switches to loading the settings by identifier, rather than name. This has been intended for a while now as it will provide more accurate loading that is resilliant to name changes. Differential Revision: https://phabricator.services.mozilla.com/D201270
455 lines
15 KiB
JavaScript
455 lines
15 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 */
|
|
|
|
import { SearchEngine } from "resource://gre/modules/SearchEngine.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
|
|
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
|
|
return console.createInstance({
|
|
prefix: "AddonSearchEngine",
|
|
maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
|
|
});
|
|
});
|
|
|
|
/**
|
|
* AddonSearchEngine represents a search engine defined by an add-on.
|
|
*/
|
|
export class AddonSearchEngine extends SearchEngine {
|
|
// Whether the engine is provided by the application.
|
|
#isAppProvided = false;
|
|
// The extension ID if added by an extension.
|
|
_extensionID = null;
|
|
// The locale, or "DEFAULT", if required.
|
|
_locale = null;
|
|
|
|
/**
|
|
* Creates a AddonSearchEngine.
|
|
*
|
|
* @param {object} options
|
|
* The options object
|
|
* @param {boolean} options.isAppProvided
|
|
* Indicates whether the engine is provided by Firefox, either
|
|
* shipped in omni.ja or via Normandy. If it is, it will
|
|
* be treated as read-only.
|
|
* @param {object} [options.details]
|
|
* An object that simulates the manifest object from a WebExtension.
|
|
* @param {object} [options.json]
|
|
* An object that represents the saved JSON settings for the engine.
|
|
*/
|
|
constructor({ isAppProvided, details, json } = {}) {
|
|
let extensionId =
|
|
details?.extensionID ?? json.extensionID ?? json._extensionID;
|
|
let id = extensionId + (details?.locale ?? json._locale);
|
|
|
|
super({
|
|
loadPath: "[addon]" + extensionId,
|
|
id,
|
|
});
|
|
|
|
this._extensionID = extensionId;
|
|
this.#isAppProvided = isAppProvided;
|
|
|
|
if (json) {
|
|
this._initWithJSON(json);
|
|
}
|
|
}
|
|
|
|
_initWithJSON(json) {
|
|
super._initWithJSON(json);
|
|
this._extensionID = json.extensionID || json._extensionID || null;
|
|
this._locale = json.extensionLocale || json._locale || null;
|
|
}
|
|
|
|
/**
|
|
* Call to initalise an instance with extension details. Does not need to be
|
|
* called if json has been passed to the constructor.
|
|
*
|
|
* @param {object} options
|
|
* The options object.
|
|
* @param {Extension} options.extension
|
|
* The extension object representing the add-on.
|
|
* @param {object} options.locale
|
|
* The locale to use from the extension for getting details of the search
|
|
* engine.
|
|
* @param {object} [options.config]
|
|
* The search engine configuration for application provided engines, that
|
|
* may be overriding some of the WebExtension's settings.
|
|
* @param {object} [options.settings]
|
|
* The saved settings for the user.
|
|
*/
|
|
async init({ extension, locale, config, settings }) {
|
|
let { baseURI, manifest } = await this.#getExtensionDetailsForLocale(
|
|
extension,
|
|
locale
|
|
);
|
|
|
|
this.#initFromManifest(baseURI, manifest, locale, config);
|
|
this._loadSettings(settings);
|
|
}
|
|
|
|
/**
|
|
* Manages updates to this engine.
|
|
*
|
|
* @param {object} options
|
|
* The options object.
|
|
* @param {object} [options.configuration]
|
|
* The search engine configuration for application provided engines, that
|
|
* may be overriding some of the WebExtension's settings.
|
|
* @param {object} [options.extension]
|
|
* The extension associated with this search engine, if known.
|
|
* @param {string} [options.locale]
|
|
* The locale to use from the extension for getting details of the search
|
|
* engine.
|
|
*/
|
|
async update({ configuration, extension, locale } = {}) {
|
|
let { baseURI, manifest } = await this.#getExtensionDetailsForLocale(
|
|
extension,
|
|
locale
|
|
);
|
|
|
|
let originalName = this.name;
|
|
let name = manifest.chrome_settings_overrides.search_provider.name.trim();
|
|
if (originalName != name && Services.search.getEngineByName(name)) {
|
|
throw new Error("Can't upgrade to the same name as an existing engine");
|
|
}
|
|
|
|
this.#updateFromManifest(baseURI, manifest, locale, configuration);
|
|
}
|
|
|
|
/**
|
|
* This will update the add-on search engine if there is no name change.
|
|
*
|
|
* @param {object} options
|
|
* The options object.
|
|
* @param {object} [options.configuration]
|
|
* The search engine configuration for application provided engines, that
|
|
* may be overriding some of the WebExtension's settings.
|
|
* @param {string} [options.locale]
|
|
* The locale to use from the extension for getting details of the search
|
|
* engine.
|
|
* @returns {boolean}
|
|
* Returns true if the engine was updated, false otherwise.
|
|
*/
|
|
async updateIfNoNameChange({ configuration, locale }) {
|
|
let { baseURI, manifest } = await this.#getExtensionDetailsForLocale(
|
|
null,
|
|
locale
|
|
);
|
|
|
|
if (
|
|
this.name !=
|
|
manifest.chrome_settings_overrides.search_provider.name.trim()
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
this.#updateFromManifest(baseURI, manifest, locale, configuration);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether or not this engine is provided by the application, e.g. it is
|
|
* in the list of configured search engines. Overrides the definition in
|
|
* `SearchEngine`.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
get isAppProvided() {
|
|
if (lazy.SearchUtils.newSearchConfigEnabled) {
|
|
return false;
|
|
}
|
|
|
|
return this.#isAppProvided;
|
|
}
|
|
|
|
/**
|
|
* Whether or not this engine is an in-memory only search engine.
|
|
* These engines are typically application provided or policy engines,
|
|
* where they are loaded every time on SearchService initialization
|
|
* using the policy JSON or the extension manifest. 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}
|
|
* Only returns true for application provided engines.
|
|
*/
|
|
get inMemory() {
|
|
return this.#isAppProvided;
|
|
}
|
|
|
|
get isGeneralPurposeEngine() {
|
|
return !!(
|
|
this._extensionID &&
|
|
lazy.SearchUtils.GENERAL_SEARCH_ENGINE_IDS.has(this._extensionID)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Creates a JavaScript object that represents this engine.
|
|
*
|
|
* @returns {object}
|
|
* An object suitable for serialization as JSON.
|
|
*/
|
|
toJSON() {
|
|
// For built-in engines we don't want to store all their data in the settings
|
|
// file so just store the relevant metadata.
|
|
if (this.#isAppProvided) {
|
|
return {
|
|
id: this.id,
|
|
_name: this.name,
|
|
_isAppProvided: true,
|
|
_metaData: this._metaData,
|
|
};
|
|
}
|
|
let json = super.toJSON();
|
|
json._extensionID = this._extensionID;
|
|
json._locale = this._locale;
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if this engine's settings are in sync with what the add-on
|
|
* manager has, and reports the results to telemetry.
|
|
*/
|
|
async checkAndReportIfSettingsValid() {
|
|
let addon = await lazy.AddonManager.getAddonByID(this._extensionID);
|
|
|
|
if (!addon) {
|
|
lazy.logConsole.debug(
|
|
`Add-on ${this._extensionID} for search engine ${this.name} is not installed!`
|
|
);
|
|
Services.telemetry.keyedScalarSet(
|
|
"browser.searchinit.engine_invalid_webextension",
|
|
this._extensionID,
|
|
1
|
|
);
|
|
} else if (!addon.isActive) {
|
|
lazy.logConsole.debug(
|
|
`Add-on ${this._extensionID} for search engine ${this.name} is not active!`
|
|
);
|
|
Services.telemetry.keyedScalarSet(
|
|
"browser.searchinit.engine_invalid_webextension",
|
|
this._extensionID,
|
|
2
|
|
);
|
|
} else {
|
|
let policy = await AddonSearchEngine.getWebExtensionPolicy(
|
|
this._extensionID
|
|
);
|
|
let providerSettings =
|
|
policy.extension.manifest?.chrome_settings_overrides?.search_provider;
|
|
|
|
if (!providerSettings) {
|
|
lazy.logConsole.debug(
|
|
`Add-on ${this._extensionID} for search engine ${this.name} no longer has an engine defined`
|
|
);
|
|
Services.telemetry.keyedScalarSet(
|
|
"browser.searchinit.engine_invalid_webextension",
|
|
this._extensionID,
|
|
4
|
|
);
|
|
} else if (this.name != providerSettings.name) {
|
|
lazy.logConsole.debug(
|
|
`Add-on ${this._extensionID} for search engine ${this.name} has a different name!`
|
|
);
|
|
Services.telemetry.keyedScalarSet(
|
|
"browser.searchinit.engine_invalid_webextension",
|
|
this._extensionID,
|
|
5
|
|
);
|
|
} else if (!this.checkSearchUrlMatchesManifest(providerSettings)) {
|
|
lazy.logConsole.debug(
|
|
`Add-on ${this._extensionID} for search engine ${this.name} has out-of-date manifest!`
|
|
);
|
|
Services.telemetry.keyedScalarSet(
|
|
"browser.searchinit.engine_invalid_webextension",
|
|
this._extensionID,
|
|
6
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes the engine based on the manifest and other values.
|
|
*
|
|
* @param {string} extensionBaseURI
|
|
* The Base URI of the WebExtension.
|
|
* @param {object} manifest
|
|
* An object representing the WebExtensions' manifest.
|
|
* @param {string} locale
|
|
* The locale that is being used for the WebExtension.
|
|
* @param {object} [configuration]
|
|
* The search engine configuration for application provided engines, that
|
|
* may be overriding some of the WebExtension's settings.
|
|
*/
|
|
#initFromManifest(extensionBaseURI, manifest, locale, configuration = {}) {
|
|
let searchProvider = manifest.chrome_settings_overrides.search_provider;
|
|
|
|
this._locale = locale;
|
|
|
|
// We only set _telemetryId for app-provided engines. See also telemetryId
|
|
// getter.
|
|
if (this.#isAppProvided) {
|
|
if (configuration.telemetryId) {
|
|
this._telemetryId = configuration.telemetryId;
|
|
} else {
|
|
let telemetryId = this._extensionID.split("@")[0];
|
|
if (locale != lazy.SearchUtils.DEFAULT_TAG) {
|
|
telemetryId += "-" + locale;
|
|
}
|
|
this._telemetryId = telemetryId;
|
|
}
|
|
}
|
|
|
|
// Set the main icon URL for the engine.
|
|
let iconURL = searchProvider.favicon_url;
|
|
|
|
if (!iconURL) {
|
|
iconURL =
|
|
manifest.icons &&
|
|
extensionBaseURI.resolve(
|
|
lazy.ExtensionParent.IconDetails.getPreferredIcon(manifest.icons).icon
|
|
);
|
|
}
|
|
|
|
// Record other icons that the WebExtension has.
|
|
if (manifest.icons) {
|
|
let iconList = Object.entries(manifest.icons).map(icon => {
|
|
return {
|
|
width: icon[0],
|
|
height: icon[0],
|
|
url: extensionBaseURI.resolve(icon[1]),
|
|
};
|
|
});
|
|
for (let icon of iconList) {
|
|
this._addIconToMap(icon.size, icon.size, icon.url);
|
|
}
|
|
}
|
|
|
|
// Filter out any untranslated parameters, the extension has to list all
|
|
// possible mozParams for each engine where a 'locale' may only provide
|
|
// actual values for some (or none).
|
|
if (searchProvider.params) {
|
|
searchProvider.params = searchProvider.params.filter(param => {
|
|
return !(param.value && param.value.startsWith("__MSG_"));
|
|
});
|
|
}
|
|
|
|
this._initWithDetails(
|
|
{ ...searchProvider, iconURL, description: manifest.description },
|
|
configuration
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update this engine based on new manifest, used during
|
|
* webextension upgrades.
|
|
*
|
|
* @param {string} extensionBaseURI
|
|
* The Base URI of the WebExtension.
|
|
* @param {object} manifest
|
|
* An object representing the WebExtensions' manifest.
|
|
* @param {string} locale
|
|
* The locale that is being used for the WebExtension.
|
|
* @param {object} [configuration]
|
|
* The search engine configuration for application provided engines, that
|
|
* may be overriding some of the WebExtension's settings.
|
|
*/
|
|
#updateFromManifest(extensionBaseURI, manifest, locale, configuration = {}) {
|
|
this._urls = [];
|
|
this._iconMapObj = null;
|
|
this.#initFromManifest(extensionBaseURI, manifest, locale, configuration);
|
|
lazy.SearchUtils.notifyAction(this, lazy.SearchUtils.MODIFIED_TYPE.CHANGED);
|
|
}
|
|
|
|
/**
|
|
* Get the localized manifest from the WebExtension for the given locale or
|
|
* manifest default locale.
|
|
*
|
|
* The search service configuration overloads the add-on manager concepts of
|
|
* locales, and forces particular locales within the WebExtension to be used,
|
|
* ignoring the user's current locale. The user's current locale is taken into
|
|
* account within the configuration, just not in the WebExtension.
|
|
*
|
|
* @param {object} [extension]
|
|
* The extension to get the manifest from.
|
|
* @param {string} locale
|
|
* The locale to load from the WebExtension. If this is `DEFAULT_TAG`, then
|
|
* the default locale is loaded.
|
|
* @returns {object}
|
|
* The loaded manifest.
|
|
*/
|
|
async #getExtensionDetailsForLocale(extension, locale) {
|
|
// If we haven't been passed an extension object, then go and find it.
|
|
if (!extension) {
|
|
extension = (
|
|
await AddonSearchEngine.getWebExtensionPolicy(this._extensionID)
|
|
).extension;
|
|
}
|
|
|
|
let manifest = extension.manifest;
|
|
|
|
let localeToLoad;
|
|
if (this.#isAppProvided) {
|
|
// If the locale we want from the WebExtension is the extension's default
|
|
// then we get that from the manifest here. We do this because if we
|
|
// are reloading due to the locale change, the add-on manager might not
|
|
// have updated the WebExtension's manifest to the new version by the
|
|
// time we hit this code.
|
|
localeToLoad =
|
|
locale == lazy.SearchUtils.DEFAULT_TAG
|
|
? manifest.default_locale
|
|
: locale;
|
|
} else {
|
|
// For user installed add-ons, we have to simulate the add-on manager
|
|
// code for loading the correct locale.
|
|
// We do this, as in the case of a live language switch, the add-on manager
|
|
// may not have yet reloaded the extension, and there's no way for us to
|
|
// listen for that reload to complete.
|
|
// See also https://bugzilla.mozilla.org/show_bug.cgi?id=1781768#c3 for
|
|
// more background.
|
|
localeToLoad = Services.locale.negotiateLanguages(
|
|
Services.locale.appLocalesAsBCP47,
|
|
[...extension.localeData.locales.keys()],
|
|
manifest.default_locale
|
|
)[0];
|
|
}
|
|
|
|
if (localeToLoad) {
|
|
manifest = await extension.getLocalizedManifest(localeToLoad);
|
|
}
|
|
return { baseURI: extension.baseURI, manifest };
|
|
}
|
|
|
|
/**
|
|
* Gets the WebExtensionPolicy for an add-on.
|
|
*
|
|
* @param {string} id
|
|
* The WebExtension id.
|
|
* @returns {WebExtensionPolicy}
|
|
*/
|
|
static async getWebExtensionPolicy(id) {
|
|
let policy = WebExtensionPolicy.getByID(id);
|
|
if (!policy) {
|
|
let idPrefix = id.split("@")[0];
|
|
let path = `resource://search-extensions/${idPrefix}/`;
|
|
await lazy.AddonManager.installBuiltinAddon(path);
|
|
policy = WebExtensionPolicy.getByID(id);
|
|
}
|
|
// On startup the extension may have not finished parsing the
|
|
// manifest, wait for that here.
|
|
await policy.readyPromise;
|
|
return policy;
|
|
}
|
|
}
|