Bug 1631898 - Handle Normandy installed system search extensions. r=Standard8

Differential Revision: https://phabricator.services.mozilla.com/D74489
This commit is contained in:
Dale Harvey 2020-06-17 23:07:39 +00:00
Родитель bfb8185ee4
Коммит 06c7cbc976
12 изменённых файлов: 338 добавлений и 157 удалений

Просмотреть файл

@ -774,15 +774,16 @@ EngineURL.prototype = {
* The file URI that points to the search engine data.
* @param {nsIURI|string} [options.uri]
* Represents the location of the search engine data file.
* @param {boolean} options.isBuiltin
* Indicates whether the engine is a app-provided or not. If it is, it will
* @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.
*/
function SearchEngine(options = {}) {
if (!("isBuiltin" in options)) {
throw new Error("isBuiltin missing from options.");
if (!("isAppProvided" in options)) {
throw new Error("isAppProvided missing from options.");
}
this._isBuiltin = options.isBuiltin;
this._isAppProvided = options.isAppProvided;
// The alias coming from the engine definition (via webextension
// keyword field for example) may be overridden in the metaData
// with a user defined alias.
@ -840,7 +841,7 @@ function SearchEngine(options = {}) {
shortName = file.leafName;
} else if (uri && uri instanceof Ci.nsIURL) {
if (
this._isBuiltin ||
this._isAppProvided ||
(gEnvironment.get("XPCSHELL_TEST_PROFILE_DIR") &&
uri.scheme == "resource")
) {
@ -910,7 +911,7 @@ SearchEngine.prototype = {
// The locale, or "DEFAULT", if required.
_locale: null,
// Whether the engine is provided by the application.
_isBuiltin: false,
_isAppProvided: false,
// The order hint from the configuration (if any).
_orderHint: null,
// The telemetry id from the configuration (if any).
@ -1859,7 +1860,7 @@ SearchEngine.prototype = {
_iconMapObj: this._iconMapObj,
_metaData: this._metaData,
_urls: this._urls,
_isBuiltin: this._isBuiltin,
_isAppProvided: this._isAppProvided,
_orderHint: this._orderHint,
_telemetryId: this._telemetryId,
};
@ -2108,11 +2109,11 @@ SearchEngine.prototype = {
// For the modern configuration, distribution engines are app-provided as
// well and we don't have xml files as app-provided engines.
if (gModernConfig) {
return !!(this._extensionID && this._isBuiltin);
return !!(this._extensionID && this._isAppProvided);
}
if (this._extensionID) {
return this._isBuiltin || this._isDistribution;
return this._isAppProvided || this._isDistribution;
}
// If we don't have a shortName, the engine is being parsed from a

Просмотреть файл

@ -688,7 +688,7 @@ SearchService.prototype = {
canInstallEngine: !engine?.hidden,
};
}
let params = this.getEngineParams(
let params = await this.getEngineParams(
extension,
extension.manifest,
SearchUtils.DEFAULT_TAG
@ -948,7 +948,7 @@ SearchService.prototype = {
if (
!rebuildCache &&
cache.engines.filter(e => e._isBuiltin).length !=
cache.engines.filter(e => e._isAppProvided).length !=
cache.builtInEngineList.length
) {
rebuildCache = true;
@ -1045,7 +1045,7 @@ SearchService.prototype = {
if (
!rebuildCache &&
SearchUtils.distroID == "" &&
cache.engines.filter(e => e._isBuiltin).length !=
cache.engines.filter(e => e._isAppProvided).length !=
cache.visibleDefaultEngines.length
) {
rebuildCache = true;
@ -1564,7 +1564,7 @@ SearchService.prototype = {
}
},
_loadEnginesFromCache(cache, skipBuiltIn) {
_loadEnginesFromCache(cache, skipAppProvided) {
if (!cache.engines) {
return;
}
@ -1577,7 +1577,7 @@ SearchService.prototype = {
let skippedEngines = 0;
for (let engine of cache.engines) {
if (skipBuiltIn && engine._isBuiltin) {
if (skipAppProvided && engine._isAppProvided) {
++skippedEngines;
continue;
}
@ -1598,7 +1598,9 @@ SearchService.prototype = {
try {
let engine = new SearchEngine({
shortName: json._shortName,
isBuiltin: !!json._isBuiltin,
// We renamed isBuiltin to isAppProvided in 1631898,
// keep checking isBuiltin for older caches.
isAppProvided: !!json._isAppProvided || !!json._isBuiltin,
});
engine._initWithJSON(json);
this._addEngineToStore(engine);
@ -1651,7 +1653,7 @@ SearchService.prototype = {
file.initWithPath(osfile.path);
addedEngine = new SearchEngine({
fileURI: file,
isBuiltin: true,
isAppProvided: true,
});
await addedEngine._initFromFile(file);
engines.push(addedEngine);
@ -2285,11 +2287,11 @@ SearchService.prototype = {
logConsole.debug("addEngineWithDetails: Adding", name);
let isCurrent = false;
let isBuiltin = !!params.isBuiltin;
let isAppProvided = !!params.isAppProvided;
// We install search extensions during the init phase, both built in
// web extensions freshly installed (via addEnginesFromExtension) or
// user installed extensions being reenabled calling this directly.
if (!gInitialized && !isBuiltin && !params.initEngine) {
if (!gInitialized && !isAppProvided && !params.initEngine) {
await this.init();
}
let existingEngine = this._engines.get(name);
@ -2325,7 +2327,7 @@ SearchService.prototype = {
let newEngine = new SearchEngine({
name,
isBuiltin,
isAppProvided,
});
newEngine._initFromMetadata(name, params);
newEngine._loadPath = "[other]addEngineWithDetails";
@ -2343,37 +2345,63 @@ SearchService.prototype = {
return newEngine;
},
/**
* Called from the AddonManager when it either installs a new
* extension containing a search engine definition or an upgrade
* to an existing one.
*
* @param {object} extension
* An Extension object containing data about the extension.
*/
async addEnginesFromExtension(extension) {
if (extension.addonData.builtIn) {
logConsole.debug(
"addEnginesFromExtension: Ignoring builtIn engine:",
extension.id
);
logConsole.debug("addEnginesFromExtension: " + extension.id);
if (extension.startupReason == "ADDON_UPGRADE") {
return this._upgradeExtensionEngine(extension);
}
let addon = await AddonManager.getAddonByID(extension.id);
if (addon.isBuiltin || addon.isSystem) {
let inConfig = this._searchOrder.filter(el => el.id == extension.id);
if (gInitialized && inConfig.length) {
return this._installExtensionEngine(
extension,
inConfig.map(el => el.locale)
);
}
logConsole.debug("addEnginesFromExtension: Ignoring builtIn engine.");
return [];
}
logConsole.debug("addEnginesFromExtension:", extension.id);
// If we havent started SearchService yet, store this extension
// to install in SearchService.init().
if (!gInitialized) {
this._startupExtensions.add(extension);
return [];
}
if (extension.startupReason == "ADDON_UPGRADE") {
let engines = await this.getEnginesByExtensionID(extension.id);
for (let engine of engines) {
let manifest = extension.manifest;
let locale = engine._locale || SearchUtils.DEFAULT_TAG;
if (locale != SearchUtils.DEFAULT_TAG) {
manifest = await extension.getLocalizedManifest(locale);
}
let params = this.getEngineParams(extension, manifest, locale);
engine._updateFromMetadata(params);
}
return engines;
}
return this._installExtensionEngine(extension, [SearchUtils.DEFAULT_TAG]);
},
/**
* Called when we see an upgrade to an existing search extension.
*
* @param {object} extension
* An Extension object containing data about the extension.
*/
async _upgradeExtensionEngine(extension) {
let engines = await this.getEnginesByExtensionID(extension.id);
for (let engine of engines) {
let manifest = extension.manifest;
let locale = engine._locale || SearchUtils.DEFAULT_TAG;
if (locale != SearchUtils.DEFAULT_TAG) {
manifest = await extension.getLocalizedManifest(locale);
}
let params = await this.getEngineParams(extension, manifest, locale);
engine._updateFromMetadata(params);
}
return engines;
},
/**
* Create an engine object from the search configuration details.
*
@ -2432,7 +2460,7 @@ SearchService.prototype = {
manifest = await policy.extension.getLocalizedManifest(locale);
}
let engineParams = this.getEngineParams(
let engineParams = await this.getEngineParams(
policy.extension,
manifest,
locale,
@ -2443,7 +2471,7 @@ SearchService.prototype = {
// No need to sanitize the name, as shortName uses the WebExtension id
// which should already be sanitized.
shortName: engineParams.shortName,
isBuiltin: engineParams.isBuiltin,
isAppProvided: engineParams.isAppProvided,
});
engine._initFromMetadata(engineParams.name, engineParams);
engine._loadPath = "[other]addEngineWithDetails";
@ -2493,13 +2521,13 @@ SearchService.prototype = {
initEngine = false,
isReload
) {
let params = this.getEngineParams(extension, manifest, locale, {
let params = await this.getEngineParams(extension, manifest, locale, {
initEngine,
});
return this.addEngineWithDetails(params.name, params, isReload);
},
getEngineParams(extension, manifest, locale, engineParams = {}) {
async getEngineParams(extension, manifest, locale, engineParams = {}) {
let { IconDetails } = ExtensionParent;
// General set of icons for an engine.
@ -2557,6 +2585,8 @@ SearchService.prototype = {
"";
let mozParams = engineParams.extraParams || searchProvider.params || [];
let addon = await AddonManager.getAddonByID(extension.id);
let isAppProvided = addon.isBuiltin || addon.isSystem;
let params = {
name: searchProvider.name.trim(),
shortName,
@ -2572,7 +2602,7 @@ SearchService.prototype = {
alias: searchProvider.keyword,
extensionID: extension.id,
locale,
isBuiltin: extension.addonData.builtIn,
isAppProvided,
orderHint: engineParams.orderHint,
// suggest_url doesn't currently get encoded.
suggestURL: searchProvider.suggest_url,
@ -2594,7 +2624,7 @@ SearchService.prototype = {
try {
var engine = new SearchEngine({
uri: engineURL,
isBuiltin: false,
isAppProvided: false,
});
engine._setIcon(iconURL, false);
engine._confirm = confirm;
@ -2671,7 +2701,7 @@ SearchService.prototype = {
this._currentPrivateEngine = null;
}
if (engineToRemove._isBuiltin) {
if (engineToRemove._isAppProvided) {
// Just hide it (the "hidden" setter will notify) and remove its alias to
// avoid future conflicts with other engines.
engineToRemove.hidden = true;
@ -3531,7 +3561,7 @@ var engineUpdateService = {
logConsole.debug("updating", engine.name, updateURI.spec);
testEngine = new SearchEngine({
uri: updateURI,
isBuiltin: false,
isAppProvided: false,
});
testEngine._engineToUpdate = engine;
testEngine._initFromURIAndLoad(updateURI);

Просмотреть файл

@ -6,7 +6,12 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { MockRegistrar } = ChromeUtils.import(
"resource://testing-common/MockRegistrar.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm",
NetUtil: "resource://gre/modules/NetUtil.jsm",
@ -135,10 +140,18 @@ var SearchTestUtils = Object.freeze({
*
* @param {object} scope
* The global scope of the test being run.
* @param {*} usePrivilegedSignatures
* How to sign created addons.
*/
initXPCShellAddonManager(scope) {
initXPCShellAddonManager(scope, usePrivilegedSignatures = false) {
let scopes = AddonManager.SCOPE_PROFILE | AddonManager.SCOPE_APPLICATION;
Services.prefs.setIntPref("extensions.enabledScopes", scopes);
Services.prefs.setBoolPref(
"extensions.webextensions.background-delayed-startup",
false
);
ExtensionTestUtils.init(scope);
AddonTestUtils.usePrivilegedSignatures = false;
AddonTestUtils.usePrivilegedSignatures = usePrivilegedSignatures;
AddonTestUtils.overrideCertDB();
},
@ -148,46 +161,151 @@ var SearchTestUtils = Object.freeze({
* Note: You should call `initXPCShellAddonManager` before calling this.
*
* @param {object} [options]
* @param {string} [options.id]
* The id to use for the WebExtension (postfixed by `@tests.mozilla.org`).
* @param {string} [options.name]
* The display name to use for the WebExtension.
* @param {string} [options.version]
* The version to use for the WebExtension.
* @param {string} [options.keyword]
* The keyword to use for the WebExtension.
*/
async installSearchExtension(options = {}) {
options.id = options.id ?? "example";
options.name = options.name ?? "Example";
options.version = options.version ?? "1.0";
options.id = (options.id ?? "example") + "@tests.mozilla.org";
let extensionInfo = {
useAddonManager: "permanent",
manifest: {
version: options.version,
applications: {
gecko: {
id: options.id + "@tests.mozilla.org",
},
},
chrome_settings_overrides: {
search_provider: {
name: options.name,
search_url: "https://example.com/",
search_url_get_params: "?q={searchTerms}",
},
},
},
manifest: this.createEngineManifest(options),
};
if (options.keyword) {
extensionInfo.manifest.chrome_settings_overrides.search_provider.keyword =
options.keyword;
}
let extension = ExtensionTestUtils.loadExtension(extensionInfo);
await extension.startup();
await AddonTestUtils.waitForSearchProviderStartup(extension);
return extension;
},
/**
* Install a search engine as a system extension to simulate
* Normandy updates. For xpcshell-tests only.
*
* Note: You should call `initXPCShellAddonManager` before calling this.
*
* @param {object} [options]
*/
async installSystemSearchExtension(options = {}) {
options.id = (options.id ?? "example") + "@search.mozilla.org";
let xpi = await AddonTestUtils.createTempWebExtensionFile({
manifest: this.createEngineManifest(options),
background() {
// eslint-disable-next-line no-undef
browser.test.sendMessage("started");
},
});
let wrapper = ExtensionTestUtils.expectExtension(options.id);
const install = await AddonManager.getInstallForURL(`file://${xpi.path}`, {
useSystemLocation: true,
});
install.install();
await wrapper.awaitStartup();
await wrapper.awaitMessage("started");
return wrapper;
},
/**
* Create a search engine extension manifest.
*
* @param {object} [options]
* @param {string} [options.id]
* The id to use for the WebExtension.
* @param {string} [options.name]
* The display name to use for the WebExtension.
* @param {string} [options.version]
* The version to use for the WebExtension.
* @param {string} [options.keyword]
* The keyword to use for the WebExtension.
* @returns {object}
* The generated manifest.
*/
createEngineManifest(options = {}) {
options.id = options.id ?? "example@tests.mozilla.org";
options.name = options.name ?? "Example";
options.version = options.version ?? "1.0";
let manifest = {
version: options.version,
applications: {
gecko: {
id: options.id,
},
},
chrome_settings_overrides: {
search_provider: {
name: options.name,
search_url: "https://example.com/",
search_url_get_params: "?q={searchTerms}",
},
},
};
if (options.keyword) {
manifest.chrome_settings_overrides.search_provider.keyword =
options.keyword;
}
return manifest;
},
/**
* A mock idleService that allows us to simulate RemoteSettings
* configuration updates.
*/
idleService: {
_observers: new Set(),
_reset() {
this._observers.clear();
},
_fireObservers(state) {
for (let observer of this._observers.values()) {
observer.observe(observer, state, null);
}
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIIdleService]),
idleTime: 19999,
addIdleObserver(observer, time) {
this._observers.add(observer);
},
removeIdleObserver(observer, time) {
this._observers.delete(observer);
},
},
/**
* Register the mock idleSerice.
*
* @param {Fun} registerCleanupFunction
*/
useMockIdleService(registerCleanupFunction) {
let fakeIdleService = MockRegistrar.register(
"@mozilla.org/widget/idleservice;1",
SearchTestUtils.idleService
);
registerCleanupFunction(() => {
MockRegistrar.unregister(fakeIdleService);
});
},
/**
* Simulates an update to the RemoteSettings configuration.
*
* @param {object} config
* The new configuration.
*/
async updateRemoteSettingsConfig(config) {
const reloadObserved = SearchTestUtils.promiseSearchNotification(
"engines-reloaded"
);
await RemoteSettings(SearchUtils.SETTINGS_KEY).emit("sync", {
data: { current: config },
});
this.idleService._fireObservers("idle");
await reloadObserved;
},
});

Просмотреть файл

@ -30,7 +30,7 @@
]
}
],
"_isBuiltin": true,
"_isAppProvided": true,
"queryCharset": "UTF-8",
"extensionID": "engine1@search.mozilla.org"
},
@ -61,7 +61,7 @@
]
}
],
"_isBuiltin": true,
"_isAppProvided": true,
"queryCharset": "UTF-8",
"extensionID": "engine2@search.mozilla.org"
},

Просмотреть файл

@ -9,7 +9,7 @@
"_metaData": {
"alias": "testAlias"
},
"_isBuiltin": true
"_isAppProvided": true
},
{
"_name": "engine2",
@ -17,7 +17,7 @@
"alias": null,
"hidden": true
},
"_isBuiltin": true
"_isAppProvided": true
},
{
"_name": "Test search engine",

Просмотреть файл

@ -29,7 +29,7 @@ add_task(async function test_async_distribution() {
Assert.ok(engine.isAppProvided, "Should be shown as an app-provided engine");
Assert.equal(
engine.wrappedJSObject._isBuiltin,
engine.wrappedJSObject._isAppProvided,
true,
"Distribution engines should still be marked as built-in"
);

Просмотреть файл

@ -45,12 +45,12 @@ const enginesCache = {
engines: [
{
_metaData: { alias: null },
_isBuiltin: true,
_isAppProvided: true,
_name: "engine1",
},
{
_metaData: { alias: null },
_isBuiltin: true,
_isAppProvided: true,
_name: "engine2",
},
],

Просмотреть файл

@ -40,32 +40,6 @@ const BAD_CONFIG = [
},
];
// The mock idle service.
var idleService = {
_observers: new Set(),
_reset() {
this._observers.clear();
},
_fireObservers(state) {
for (let observer of this._observers.values()) {
observer.observe(observer, state, null);
}
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIIdleService]),
idleTime: 19999,
addIdleObserver(observer, time) {
this._observers.add(observer);
},
removeIdleObserver(observer, time) {
this._observers.delete(observer);
},
};
function listenFor(name, key) {
let notifyObserved = false;
let obs = (subject, topic, data) => {
@ -84,14 +58,7 @@ function listenFor(name, key) {
let configurationStub;
add_task(async function setup() {
let fakeIdleService = MockRegistrar.register(
"@mozilla.org/widget/idleservice;1",
idleService
);
registerCleanupFunction(() => {
MockRegistrar.unregister(fakeIdleService);
});
SearchTestUtils.useMockIdleService(registerCleanupFunction);
await AddonTestUtils.promiseStartupManager();
});

Просмотреть файл

@ -94,32 +94,6 @@ const DEFAULT = "Test search engine";
// Default engine with region set to FR.
const FR_DEFAULT = "engine-pref";
// The mock idle service.
var idleService = {
_observers: new Set(),
_reset() {
this._observers.clear();
},
_fireObservers(state) {
for (let observer of this._observers.values()) {
observer.observe(observer, state, null);
}
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIIdleService]),
idleTime: 19999,
addIdleObserver(observer, time) {
this._observers.add(observer);
},
removeIdleObserver(observer, time) {
this._observers.delete(observer);
},
};
function listenFor(name, key) {
let notifyObserved = false;
let obs = (subject, topic, data) => {
@ -143,14 +117,7 @@ add_task(async function setup() {
true
);
let fakeIdleService = MockRegistrar.register(
"@mozilla.org/widget/idleservice;1",
idleService
);
registerCleanupFunction(() => {
MockRegistrar.unregister(fakeIdleService);
});
SearchTestUtils.useMockIdleService(registerCleanupFunction);
await useTestEngines("data", null, CONFIG);
await AddonTestUtils.promiseStartupManager();
});
@ -263,7 +230,7 @@ add_task(async function test_config_updated_engine_changes() {
},
});
idleService._fireObservers("idle");
SearchTestUtils.idleService._fireObservers("idle");
await reloadObserved;
Services.obs.removeObserver(

Просмотреть файл

@ -3,8 +3,12 @@
"use strict";
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm",
});
const {
createAppInfo,
promiseRestartManager,
promiseShutdownManager,
promiseStartupManager,

Просмотреть файл

@ -0,0 +1,93 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
SearchTestUtils.initXPCShellAddonManager(this, "system");
async function restart() {
Services.search.wrappedJSObject.reset();
await AddonTestUtils.promiseRestartManager();
await Services.search.init(false);
}
const CONFIG_DEFAULT = [
{
webExtension: { id: "plainengine@search.mozilla.org" },
appliesTo: [{ included: { everywhere: true } }],
},
];
const CONFIG_UPDATED = [
{
webExtension: { id: "plainengine@search.mozilla.org" },
appliesTo: [{ included: { everywhere: true } }],
},
{
webExtension: { id: "example@search.mozilla.org" },
appliesTo: [{ included: { everywhere: true } }],
},
];
async function getEngineNames() {
let engines = await Services.search.getDefaultEngines();
return engines.map(engine => engine._name);
}
add_task(async function setup() {
await useTestEngines("test-extensions", null, CONFIG_DEFAULT);
await AddonTestUtils.promiseStartupManager();
registerCleanupFunction(AddonTestUtils.promiseShutdownManager);
SearchTestUtils.useMockIdleService(registerCleanupFunction);
await Services.search.init();
});
// Test the situation where we receive an updated configuration
// that references an engine that doesnt exist locally as it
// will be installed by Normandy.
add_task(async function test_config_before_normandy() {
// Ensure initial default setup.
await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT);
await restart();
Assert.deepEqual(await getEngineNames(), ["Plain"]);
// Updated configuration references nonexistant engine.
await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_UPDATED);
Assert.deepEqual(
await getEngineNames(),
["Plain"],
"Updated engine hasnt been installed yet"
);
// Normandy then installs the engine.
let addon = await SearchTestUtils.installSystemSearchExtension();
Assert.deepEqual(
await getEngineNames(),
["Plain", "Example"],
"Both engines are now enabled"
);
await addon.unload();
});
// Test the situation where we receive a newly installed
// engine from Normandy followed by the update to the
// configuration that uses that engine.
add_task(async function test_normandy_before_config() {
// Ensure initial default setup.
await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT);
await restart();
Assert.deepEqual(await getEngineNames(), ["Plain"]);
// Normandy installs the enigne.
let addon = await SearchTestUtils.installSystemSearchExtension();
Assert.deepEqual(
await getEngineNames(),
["Plain"],
"Normandy engine ignored as not in config yet"
);
// Configuration is updated to use the engine.
await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_UPDATED);
Assert.deepEqual(
await getEngineNames(),
["Plain", "Example"],
"Both engines are now enabled"
);
await addon.unload();
});

Просмотреть файл

@ -63,6 +63,7 @@ support-files =
[test_missing_engine.js]
[test_sort_orders-no-hints.js]
[test_webextensions_builtin_upgrade.js]
[test_webextensions_normandy_upgrade.js]
[test_distributions.js]
skip-if = appname == "thunderbird"
[test_maybereloadengine_order.js]