Files
sousa-gecko/toolkit/components/search/SearchSettings.sys.mjs
T
Mark Banner 3f6ba98393 Bug 2046554 - Search engines should be removed from the settings UI when removed via enterprise policy. r=mkaply,search-reviewers,akulyk,scunnane
This takes the approach of the search preferences examining the list, as that is simpler than working an extra flag into the search service for "hidden by enterprise policy".

The approach has the advantage that we're using the exact enterprise policy settings list directly to hide the engines from the UI, and thus avoids any cases that the local saved settings could get out of sync with the enterprise policy settings.

Differential Revision: https://phabricator.services.mozilla.com/D309450
2026-07-07 21:15:50 +00:00

980 lines
31 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = XPCOMUtils.declareLazy({
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AppProvidedConfigEngine:
"moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
ConfigSearchEngine:
"moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
logConsole: () =>
console.createInstance({
prefix: "SearchSettings",
maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn",
}),
});
/**
* @import {SearchEngine} from "./SearchEngine.sys.mjs"
*/
const SETTINGS_FILENAME = "search.json.mozlz4";
/**
* A map of engine ids to their previous names. These are required for
* ensuring that user's settings are correctly migrated for users upgrading
* from a settings file prior to settings version 7 (Firefox 108).
*
* @type {Map<string, string>}
*/
const ENGINE_ID_TO_OLD_NAME_MAP = new Map([
["wikipedia-hy", "Wikipedia (hy)"],
["wikipedia-kn", "Wikipedia (kn)"],
["wikipedia-lv", "Vikipēdija"],
["wikipedia-NO", "Wikipedia (no)"],
["wikipedia-el", "Wikipedia (el)"],
["wikipedia-lt", "Wikipedia (lt)"],
["wikipedia-my", "Wikipedia (my)"],
["wikipedia-pa", "Wikipedia (pa)"],
["wikipedia-pt", "Wikipedia (pt)"],
["wikipedia-si", "Wikipedia (si)"],
["wikipedia-tr", "Wikipedia (tr)"],
]);
/**
* This class manages the saves search settings.
*
* Global settings can be saved and obtained from this class via the
* `*Attribute` methods.
*/
export class SearchSettings {
constructor(searchService) {
this.#searchService = searchService;
// Once the search service has initialized, schedule a write to ensure
// that any settings that may have changed or need updating are handled.
searchService.promiseInitialized.then(() => {
this._delayedWrite();
});
}
QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);
// Delay for batching invalidation of the JSON settings (ms)
static SETTINGS_INVALIDATION_DELAY = 1000;
get #settingsFilePath() {
return PathUtils.join(PathUtils.profileDir, SETTINGS_FILENAME);
}
/**
* A reference to the pending DeferredTask, if there is one.
*/
_batchTask = null;
/**
* A reference to the search service so that we can save the engines list.
*/
#searchService = null;
/**
* The user's settings file read from disk so we can persist metadata for
* engines that are default or hidden, the user's locale and region, hashes
* for the loadPath, and hashes for default and private default engines.
* This is the JSON we read from disk and save to disk when there's an update
* to the settings.
*
* Structure of settings:
*
* ```
* Object {
* version: <number>,
* engines: [...],
* metaData: {...},
* }
* ```
*
* Settings metaData is the active metadata for setting and getting attributes.
* When a new metadata attribute is set, we save it to #settings.metaData and
* write #settings to disk.
*
* #settings.metaData attributes:
*
* @property {string} current
* The current user-set default engine. The associated hash is called
* 'hash'.
* @property {string} private
* The current user-set private engine. The associated hash is called
* 'privateHash'.
* The current and prviate objects have associated hash fields to validate
* the value is set by the application.
* @property {string} appDefaultEngine
* The identifier of the current application default engine.
* @property {string} channel
* Configuration is restricted to the specified channel. ESR is an example
* of a channel.
* @property {string} distroID
* Specifies which distribution the default engine is included in.
* @property {string} experiment
* Specifies if the application is running on an experiment.
* @property {string} locale
* The current locale.
* @property {string} region
* The current region.
* @property {boolean} useSavedOrder
* True if the user's order information stored in settings is used.
*/
#settings = null;
/**
* #cachedSettings is updated when we read the settings from disk and when
* we write settings to disk. #cachedSettings is compared with #settings
* before we do a write to disk. If there's no change to the settings
* attributes, then we don't write the settings to disk.
*
* This is a deep copy of #settings.
*
* @type {object}
*/
#cachedSettings = {};
addObservers() {
Services.obs.addObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED);
Services.obs.addObserver(this, lazy.SearchUtils.TOPIC_SEARCH_SERVICE);
}
/**
* Cleans up, removing observers.
*/
removeObservers() {
Services.obs.removeObserver(this, lazy.SearchUtils.TOPIC_ENGINE_MODIFIED);
Services.obs.removeObserver(this, lazy.SearchUtils.TOPIC_SEARCH_SERVICE);
}
/**
* Whether the last `get` reset the settings because they were corrupt.
*/
lastGetCorrupt = false;
/**
* Reads the settings file.
*
* @param {string} origin
* If this parameter is "test", then the settings will not be written. As
* some tests manipulate the settings directly, we allow turning off writing to
* avoid writing stale settings data.
* @returns {Promise<object>}
* Returns the settings file data.
*/
async get(origin = "") {
this.lastGetCorrupt = false;
let json;
await this._ensurePendingWritesCompleted(origin);
try {
json = await IOUtils.readJSON(this.#settingsFilePath, {
decompress: true,
});
if (!json.engines || !json.engines.length) {
throw new Error("no engine in the file");
}
} catch (ex) {
if (DOMException.isInstance(ex) && ex.name === "NotFoundError") {
lazy.logConsole.debug("get: No settings file exists, new profile?", ex);
return this.#resetSettings(false);
}
lazy.logConsole.error("get: Settings file empty or corrupt.", ex);
return this.#resetSettings(true);
}
this.#settings = json;
this.#cachedSettings = structuredClone(json);
if (!this.#settings.metaData) {
this.#settings.metaData = {};
}
try {
await this.#migrateSettings();
} catch (ex) {
lazy.logConsole.error("get: Migration failed.", ex);
return this.#resetSettings(true);
}
return structuredClone(json);
}
/**
* Resets the search settings without writing to disk yet.
*
* If the reset is due to a corrupt settings file, the corrupt file is
* backed up, the lastSettingsCorruptTime pref is set to the current time,
* and this.lastGetCorrupt is set to true.
*
* @param {boolean} corrupt
* Whether the reset is carried out because the settings are corrupt.
* @returns {Promise<object>}
* New empty search settings.
*/
async #resetSettings(corrupt) {
this.#settings = { metaData: {} };
this.#cachedSettings = {};
if (corrupt) {
this.lastGetCorrupt = true;
Services.prefs.setIntPref(
lazy.SearchUtils.BROWSER_SEARCH_PREF + "lastSettingsCorruptTime",
Date.now() / 1000
);
try {
await IOUtils.move(
this.#settingsFilePath,
this.#settingsFilePath + ".bak"
);
} catch (ex) {
lazy.logConsole.warn(
"#resetSettings: Unable to create backup of corrupt settings file.",
ex
);
}
}
return structuredClone(this.#settings);
}
/**
* Test-only function to reset the settings.
*/
_testResetSettings() {
this.#resetSettings(false);
}
/**
* Queues writing the settings until after SETTINGS_INVALIDATION_DELAY. If there
* is a currently queued task then it will be restarted.
*/
_delayedWrite() {
if (this._batchTask) {
this._batchTask.disarm();
} else {
let task = async () => {
if (
!this.#searchService.isInitialized ||
this.#searchService._reloadingEngines
) {
// Re-arm the task as we don't want to save potentially incomplete
// information during the middle of (re-)initializing.
this._batchTask.arm();
return;
}
lazy.logConsole.debug("batchTask: Invalidating engine settings");
await this._write();
};
this._batchTask = new lazy.DeferredTask(
task,
SearchSettings.SETTINGS_INVALIDATION_DELAY
);
}
this._batchTask.arm();
}
/**
* Ensures any pending writes of the settings are completed.
*
* @param {string} origin
* If this parameter is "test", then the settings will not be written. As
* some tests manipulate the settings directly, we allow turning off writing to
* avoid writing stale settings data.
*/
async _ensurePendingWritesCompleted(origin = "") {
// Before we read the settings file, first make sure all pending tasks are clear.
if (!this._batchTask) {
return;
}
lazy.logConsole.debug("finalizing batch task");
let task = this._batchTask;
this._batchTask = null;
// Tests manipulate the settings directly, so let's not double-write with
// stale settings data here.
if (origin == "test") {
task.disarm();
} else {
await task.finalize();
}
}
/**
* Writes the settings to disk (no delay).
*/
async _write() {
if (this._batchTask) {
this._batchTask.disarm();
}
let settings = {};
// Allows us to force a settings refresh should the settings format change.
settings.version = lazy.SearchUtils.SETTINGS_VERSION;
settings.engines = [...this.#searchService._engines.values()].map(engine =>
JSON.parse(JSON.stringify(engine))
);
settings.metaData = this.#settings.metaData;
// Persist metadata for config engines even if they aren't currently
// active, this means if they become active again their settings
// will be restored. This can happen if a user switches between regions.
if (this.#settings?.engines) {
for (let engine of this.#settings.engines) {
if (
engine._isConfigEngine &&
// If a config engine is user-installed and not included, it was
// explicitly removed by the user and we should not persist its metadata.
!engine._metaData["user-installed"]
) {
// If the saved settings is already included in the settings, don't re-add it.
// TODO (Bug 2046142): Refactor settings updates for overridden engines.
// Checking the engine name as a fallback alongside the canonical ID is a temporary
// workaround to handle third-party overrides. We should remove this name check to
// rely strictly on IDs and prevent potential duplicates, which may require changing
// how we handle search engines with duplicate names first.
let included = settings.engines.some(
e => e.id == engine.id || e._name == engine._name
);
if (!included) {
settings.engines.push(engine);
}
}
}
}
// Update the local copy.
this.#settings = settings;
try {
if (!settings.engines.length) {
throw new Error("cannot write without any engine.");
}
if (this.isCurrentAndCachedSettingsEqual()) {
lazy.logConsole.debug(
"_write: Settings unchanged. Did not write to disk."
);
Services.obs.notifyObservers(
null,
lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
"write-prevented-when-settings-unchanged"
);
Services.obs.notifyObservers(
null,
lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
"write-settings-to-disk-complete"
);
return;
}
// At this point, the settings and cached settings are different. We
// write settings to disk and update #cachedSettings.
this.#cachedSettings = structuredClone(this.#settings);
lazy.logConsole.debug("_write: Writing to settings file.");
await IOUtils.writeJSON(this.#settingsFilePath, settings, {
compress: true,
tmpPath: this.#settingsFilePath + ".tmp",
});
lazy.logConsole.debug("_write: settings file written to disk.");
Services.obs.notifyObservers(
null,
lazy.SearchUtils.TOPIC_SEARCH_SERVICE,
"write-settings-to-disk-complete"
);
} catch (ex) {
lazy.logConsole.error("_write: Could not write to settings file:", ex);
}
}
/**
* Sets an attribute without verification.
*
* @param {string} name
* The name of the attribute to set.
* @param {*} val
* The value to set.
*/
setMetaDataAttribute(name, val) {
this.#settings.metaData[name] = val;
this._delayedWrite();
}
/**
* Sets a verified attribute. This will save an additional hash
* value, that can be verified when reading back.
*
* @param {string} name
* The name of the attribute to set.
* @param {*} val
* The value to set.
*/
setVerifiedMetaDataAttribute(name, val) {
this.#settings.metaData[name] = val;
this.#settings.metaData[this.getHashName(name)] =
lazy.SearchUtils.getVerificationHash(val);
this._delayedWrite();
}
/**
* Gets an attribute without verification.
*
* @param {string} name
* The name of the attribute to get.
* @returns {*}
* The value of the attribute, or undefined if not known.
*/
getMetaDataAttribute(name) {
return this.#settings.metaData[name] ?? undefined;
}
/**
* Gets a copy of the settings metadata.
*
* @returns {*}
* A copy of the settings metadata object.
*/
getSettingsMetaData() {
return { ...this.#settings.metaData };
}
/**
* Gets a verified attribute.
*
* @param {string} name
* The name of the attribute to get.
* @param {boolean} isConfigEngine
* Whether the engine associated with the attribute is a config engine.
* @returns {*}
* The value of the attribute.
* We return undefined if the value of the attribute is not known or does
* not match the verification hash.
*/
getVerifiedMetaDataAttribute(name, isConfigEngine) {
let attribute = this.getMetaDataAttribute(name);
// If the selected engine is a config engine, we can relax the
// verification hash check to reduce the annoyance for users who
// backup/sync their profile in custom ways.
if (isConfigEngine) {
return attribute;
}
if (
attribute &&
this.getMetaDataAttribute(this.getHashName(name)) !=
lazy.SearchUtils.getVerificationHash(attribute)
) {
lazy.logConsole.warn(
"getVerifiedMetaDataAttribute, invalid hash for",
name
);
return undefined;
}
return attribute;
}
/**
* Sets an attribute in #settings.engines._metaData
*
* @param {string} engineName
* The name of the engine.
* @param {string} property
* The name of the attribute to set.
* @param {*} value
* The value to set.
*/
setEngineMetaDataAttribute(engineName, property, value) {
let engines = [...this.#searchService._engines.values()];
let engine = engines.find(e => e._name == engineName);
if (engine) {
engine._metaData[property] = value;
this._delayedWrite();
}
}
/**
* Gets an attribute from #settings.engines._metaData
*
* @param {string} engineName
* The name of the engine.
* @param {string} property
* The name of the attribute to get.
* @returns {*}
* The value of the attribute, or undefined if not known.
*/
getEngineMetaDataAttribute(engineName, property) {
let engine = this.#settings.engines.find(e => e._name == engineName);
return engine._metaData[property] ?? undefined;
}
/**
* Returns the name for the hash for a particular attribute. This is
* necessary because the default engine ID property is named `current`
* with its hash as `hash`. All other hashes are in the `<name>Hash` format.
*
* @param {string} name
* The name of the attribute to get the hash name for.
* @returns {string}
* The hash name to use.
*/
getHashName(name) {
// The "current" check remains here because we need to retrieve the
// "current" hash name for the migration of engine ids. After the migration,
// the "current" property is no longer used because we now store
// "defaultEngineId" instead.
if (name == "current") {
return "hash";
}
return name + "Hash";
}
/**
* Handles shutdown; writing the settings if necessary.
*
* @param {object} state
* The shutdownState object that is used to help analyzing the shutdown
* state in case of a crash or shutdown timeout.
*/
async shutdown(state) {
if (!this._batchTask) {
return;
}
state.step = "Finalizing batched task";
try {
await this._batchTask.finalize();
state.step = "Batched task finalized";
} catch (ex) {
state.step = "Batched task failed to finalize";
state.latestError.message = "" + ex;
if (ex && typeof ex == "object") {
state.latestError.stack = ex.stack || undefined;
}
}
}
// nsIObserver
observe(subject, topic, verb) {
switch (topic) {
case lazy.SearchUtils.TOPIC_ENGINE_MODIFIED:
switch (verb) {
case lazy.SearchUtils.MODIFIED_TYPE.ADDED:
case lazy.SearchUtils.MODIFIED_TYPE.CHANGED:
case lazy.SearchUtils.MODIFIED_TYPE.REMOVED:
this._delayedWrite();
break;
case lazy.SearchUtils.MODIFIED_TYPE.ICON_CHANGED:
// Config Search Engines have their icons stored in Remote
// Settings, so we don't need to update the saved settings.
if (!(subject.wrappedJSObject instanceof lazy.ConfigSearchEngine)) {
this._delayedWrite();
}
break;
}
break;
case lazy.SearchUtils.TOPIC_SEARCH_SERVICE:
switch (verb) {
case "engines-reloaded":
this._delayedWrite();
break;
}
break;
}
}
/**
* Compares the #settings and #cachedSettings objects.
*
* @returns {boolean}
* True if the objects have the same property and values.
*/
isCurrentAndCachedSettingsEqual() {
return lazy.ObjectUtils.deepEqual(this.#settings, this.#cachedSettings);
}
/**
* This function writes to settings versions 6 and below. It does two
* updates:
* 1) Store engine ids.
* 2) Store "defaultEngineId" and "privateDefaultEngineId" to replace
* "current" and "private" because we are no longer referencing the
* "current" and "private" attributes with engine names as their values.
*
* @param {object} clonedSettings
* The SearchService holds a deep copy of the settings file object. This
* clonedSettings is passed in as an argument from SearchService.
*/
migrateEngineIds(clonedSettings) {
if (clonedSettings.version <= 6) {
lazy.logConsole.debug("migrateEngineIds: start");
for (let engineSettings of clonedSettings.engines) {
let engine = this.#getEngineByName(engineSettings._name);
if (engine) {
// Store the engine id
engineSettings.id = engine.id;
}
}
let currentDefaultEngine = this.#getEngineByName(
clonedSettings.metaData.current
);
let privateDefaultEngine = this.#getEngineByName(
clonedSettings.metaData.private
);
// As per SearchService._getEngineDefault, we relax the verification hash
// check for application provided engines to reduce the annoyance for
// users who backup/sync their profile in custom ways.
if (
currentDefaultEngine &&
(currentDefaultEngine instanceof lazy.AppProvidedConfigEngine ||
lazy.SearchUtils.getVerificationHash(
clonedSettings.metaData.current
) == clonedSettings.metaData[this.getHashName("current")])
) {
// Store the defaultEngineId
this.setVerifiedMetaDataAttribute(
"defaultEngineId",
currentDefaultEngine.id
);
} else {
this.setVerifiedMetaDataAttribute("defaultEngineId", "");
}
if (
privateDefaultEngine &&
(privateDefaultEngine instanceof lazy.AppProvidedConfigEngine ||
lazy.SearchUtils.getVerificationHash(
clonedSettings.metaData.private
) == clonedSettings.metaData[this.getHashName("private")])
) {
// Store the privateDefaultEngineId
this.setVerifiedMetaDataAttribute(
"privateDefaultEngineId",
privateDefaultEngine.id
);
} else {
this.setVerifiedMetaDataAttribute("privateDefaultEngineId", "");
}
lazy.logConsole.debug("migrateEngineIds: done");
}
}
/**
* Finds the settings for the engine, based on the version of the settings
* passed in. Older versions of settings used the engine name as the key,
* whereas newer versions now use the engine id.
*
* @param {object} settings
* The saved settings object.
* @param {string} engineId
* The id of the engine.
* @param {string} engineName
* The name of the engine.
* @returns {object|undefined}
* The engine settings if found, undefined otherwise.
*/
static findSettingsForEngine(settings, engineId, engineName) {
if (settings.version <= 6) {
let engineSettings = settings.engines?.find(e => e._name == engineName);
if (!engineSettings) {
// If we can't find the engine settings with the current name,
// see if there was an older name.
let oldEngineName = ENGINE_ID_TO_OLD_NAME_MAP.get(engineId);
if (oldEngineName) {
engineSettings = settings.engines?.find(
e => e._name == oldEngineName
);
}
}
return engineSettings;
}
return settings.engines?.find(e => e.id == engineId);
}
/**
* Returns the engine associated with the name without SearchService
* initialization checks.
*
* @param {string} engineName
* The name of the engine.
* @returns {?SearchEngine}
* The associated engine if found, null otherwise.
*/
#getEngineByName(engineName) {
for (let engine of this.#searchService._engines.values()) {
if (engine.name == engineName) {
return engine;
}
}
return null;
}
/**
* Migrates older settings to the latest version.
* Does not migrate the engine IDs yet because that happens
* after the ApplicationProvidedEngines have been loaded.
*/
async #migrateSettings() {
this.#migrateTo6();
this.#migrateTo8();
this.#migrateTo9();
this.#migrateTo10();
this.#migrateTo11();
await this.#migrateTo12();
this.#migrateTo13();
this.#migrateTo14();
}
#migrateTo6() {
// Versions of gecko older than 82 stored the order flag as a preference.
// See bug 1642995.
if (
this.#settings.version < 6 ||
!("useSavedOrder" in this.#settings.metaData)
) {
const prefName = lazy.SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder";
let useSavedOrder = Services.prefs.getBoolPref(prefName, false);
this.setMetaDataAttribute("useSavedOrder", useSavedOrder);
// Clear the old pref so it isn't lying around.
Services.prefs.clearUserPref(prefName);
}
}
#migrateTo8() {
// The load path is changed to better differentiate policy/user/
// add-on engines for telemetry. See bug 1801813.
if (this.#settings.version < 8 && Array.isArray(this.#settings.engines)) {
for (let engine of this.#settings.engines) {
if (!engine._loadPath) {
continue;
}
if (engine._loadPath.includes("set-via-policy")) {
engine._loadPath = "[policy]";
} else if (engine._loadPath.includes("set-via-user")) {
engine._loadPath = "[user]";
} else if (
engine._loadPath.startsWith("[other]addEngineWithDetails:")
) {
engine._loadPath = engine._loadPath.replace(
"[other]addEngineWithDetails:",
"[addon]"
);
}
}
}
}
#migrateTo9() {
// The hiddenOneOffs pref is moved to the search settings.
// See bug 1643887.
if (this.#settings.version < 9 && this.#settings.engines) {
const hiddenOneOffsPrefs = Services.prefs.getStringPref(
"browser.search.hiddenOneOffs",
""
);
for (const engine of this.#settings.engines) {
engine._metaData.hideOneOffButton = hiddenOneOffsPrefs.includes(
engine._name
);
}
Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
}
}
#migrateTo10() {
// The format of the IDs of app provided engines is changed.
// See bug 1870687.
if (
this.#settings.version > 6 &&
this.#settings.version < 10 &&
this.#settings.engines
) {
let changedEngines = new Map();
for (let engine of this.#settings.engines) {
if (engine._isAppProvided && engine.id) {
let oldId = engine.id;
engine.id = engine.id
.replace("@search.mozilla.orgdefault", "")
.replace("@search.mozilla.org", "-");
changedEngines.set(oldId, engine.id);
}
}
const PROPERTIES_CONTAINING_IDS = [
"privateDefaultEngineId",
"appDefaultEngineId",
"defaultEngineId",
];
for (let prop of PROPERTIES_CONTAINING_IDS) {
if (changedEngines.has(this.#settings.metaData[prop])) {
this.#settings.metaData[prop] = changedEngines.get(
this.#settings.metaData[prop]
);
}
}
}
}
#migrateTo11() {
// The keys of _iconMapObj are changed from width and height to width only.
// See bug 1655066.
if (this.#settings.version < 11 && this.#settings.engines) {
for (let engine of this.#settings.engines) {
if (!engine._iconMapObj) {
continue;
}
let oldIconMap = engine._iconMapObj;
engine._iconMapObj = {};
for (let [sizeStr, icon] of Object.entries(oldIconMap)) {
let sizeObj;
try {
sizeObj = JSON.parse(sizeStr);
} catch {}
if (
typeof sizeObj === "object" &&
"width" in sizeObj &&
parseInt(sizeObj.width) > 0 &&
sizeObj.width == sizeObj.height
) {
engine._iconMapObj[sizeObj.width] = icon;
} else if (typeof sizeObj === "number") {
// This happens if the user copies a version 11+ search config to
// an old install, which gets updated eventually; see bug 1940533.
engine._iconMapObj[sizeObj] = icon;
}
}
}
}
}
async #migrateTo12() {
// _iconURL is removed and its icon is stored in _iconMapObj instead.
// See bug 1655076.
if (this.#settings.version < 12 && this.#settings.engines) {
for (let engine of this.#settings.engines) {
if (engine._iconURL) {
let iconURL = engine._iconURL;
delete engine._iconURL;
let uri = lazy.SearchUtils.makeURI(iconURL);
if (!uri) {
continue;
}
// The URL should be either a data or moz-extension URL so this should
// always succeed and be fast. We skip other schemes just to be sure.
// If we fail to fetch or decode the icon, we assume it's 16x16.
switch (uri.scheme) {
case "moz-extension":
try {
await lazy.AddonManager.readyPromise;
} catch (e) {
if (e == "shutting down") {
throw new Error("Addon manager shutting down");
} else {
throw new Error("Addon manager failed");
}
}
break;
case "data":
break;
default:
continue;
}
let byteArray, contentType;
try {
[byteArray, contentType] = await lazy.SearchUtils.fetchIcon(uri);
} catch {
lazy.logConsole.warn(
`_iconURL migration: failed to load icon of search engine ${engine._name}.`
);
engine._iconMapObj ||= {};
engine._iconMapObj[16] = iconURL;
continue;
}
// MAX_ICON_SIZE is not enforced in some cases. In those cases, we
// rescale the icon to 32x32.
if (byteArray.length > lazy.SearchUtils.MAX_ICON_SIZE) {
try {
[byteArray, contentType] = lazy.SearchUtils.rescaleIcon(
byteArray,
contentType
);
let url =
"data:" + contentType + ";base64," + byteArray.toBase64();
engine._iconMapObj ||= {};
engine._iconMapObj[32] = url;
} catch {
lazy.logConsole.warn(
`_iconURL migration: failed to resize icon of search engine ${engine._name}.`
);
}
continue;
}
let size = lazy.SearchUtils.decodeSize(byteArray, contentType, 16);
engine._iconMapObj ||= {};
engine._iconMapObj[size] = iconURL;
}
}
}
}
#migrateTo13() {
// App provided engines are renamed to config engines, see bug 1973315.
// At the same time, we also rename _isBuiltin _isConfigEngine.
// This originally happed in bug 1631898, but instead of adding a migration,
// the initial implementation simply checked both values.
if (this.#settings.version < 13 && this.#settings.engines) {
for (let engine of this.#settings.engines) {
if (engine._isAppProvided) {
delete engine._isAppProvided;
engine._isConfigEngine = true;
} else if (engine._isBuiltin) {
delete engine._isBuiltin;
engine._isConfigEngine = true;
}
}
}
}
#migrateTo14() {
// Bug 2046554 - Force the remove search engine enterprise policy to be
// re-applied, to ensure previously hidden engines are fully disabled. This
// is in case a user re-enabled them in version 152 when they were visible
// for a while.
//
// Note: This is here rather than in ProfileDataUpgrader.sys.mjs, as it
// means we can uplift without having to worry about 153 and 154 having
// different UI migration versions.
Services.prefs.clearUserPref(
"browser.policies.runOncePerModification.removeSearchEngines"
);
}
}