From daf2df652880528d2b66b4e1e640b8b1e4b49cde Mon Sep 17 00:00:00 2001 From: Michael Kaply Date: Tue, 18 Jul 2017 11:28:33 -0500 Subject: [PATCH] Bug 1378882 - Support is_default for built-in engines only. r=mixedpuppy MozReview-Commit-ID: C4iM2boQhK3 --HG-- extra : rebase_source : d1d0b629184c28e6ddd9d0e46ed783d5b767e77a --- .../ext-chrome-settings-overrides.js | 81 +++ .../schemas/chrome_settings_overrides.json | 2 +- .../test/browser/browser-common.ini | 1 + ...r_ext_settings_overrides_default_search.js | 507 ++++++++++++++++++ 4 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js diff --git a/browser/components/extensions/ext-chrome-settings-overrides.js b/browser/components/extensions/ext-chrome-settings-overrides.js index b3eee824a5c4..5598ec3632ea 100644 --- a/browser/components/extensions/ext-chrome-settings-overrides.js +++ b/browser/components/extensions/ext-chrome-settings-overrides.js @@ -4,8 +4,15 @@ "use strict"; +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPreferencesManager", "resource://gre/modules/ExtensionPreferencesManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore", + "resource://gre/modules/ExtensionSettingsStore.jsm"); + +const DEFAULT_SEARCH_STORE_TYPE = "default_search"; +const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch"; const searchInitialized = () => { return new Promise(resolve => { @@ -25,10 +32,39 @@ const searchInitialized = () => { }; this.chrome_settings_overrides = class extends ExtensionAPI { + processDefaultSearchSetting(action) { + let {extension} = this; + let {manifest} = extension; + let item = ExtensionSettingsStore.getSetting(DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME); + if (!item) { + return; + } + if (Services.search.currentEngine.name != item.value && + Services.search.currentEngine.name != item.initialValue) { + // The current engine is not the same as the value that the ExtensionSettingsStore has. + // This means that the user changed the engine, so we shouldn't control it anymore. + // Do nothing and remove our entry from the ExtensionSettingsStore. + ExtensionSettingsStore.removeSetting(extension, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME); + return; + } + item = ExtensionSettingsStore[action](extension, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME); + if (item) { + try { + let engine = Services.search.getEngineByName(item.value || item.initialValue); + if (engine) { + Services.search.currentEngine = engine; + } + } catch (e) { + Components.utils.reportError(e); + } + } + } + async onManifestEntry(entryName) { let {extension} = this; let {manifest} = extension; + await ExtensionSettingsStore.initialize(); if (manifest.chrome_settings_overrides.homepage) { ExtensionPreferencesManager.setSetting(extension, "homepage_override", manifest.chrome_settings_overrides.homepage); @@ -36,6 +72,40 @@ this.chrome_settings_overrides = class extends ExtensionAPI { if (manifest.chrome_settings_overrides.search_provider) { await searchInitialized(); let searchProvider = manifest.chrome_settings_overrides.search_provider; + if (searchProvider.is_default) { + let engineName = searchProvider.name.trim(); + let engine = Services.search.getEngineByName(engineName); + if (engine && Services.search.getDefaultEngines().includes(engine)) { + // Only add onclose handlers if we would definitely + // be setting the default engine. + extension.callOnClose({ + close: () => { + switch (extension.shutdownReason) { + case "ADDON_DISABLE": + this.processDefaultSearchSetting("disable"); + break; + + case "ADDON_UNINSTALL": + this.processDefaultSearchSetting("removeSetting"); + break; + } + }, + }); + if (extension.startupReason === "ADDON_INSTALL") { + let item = await ExtensionSettingsStore.addSetting( + extension, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME, engineName, () => { + return Services.search.currentEngine.name; + }); + Services.search.currentEngine = Services.search.getEngineByName(item.value); + } else if (extension.startupReason === "ADDON_ENABLE") { + this.processDefaultSearchSetting("enable"); + } + // If we would have set the default engine, + // we don't allow a search provider to be added. + return; + } + Components.utils.reportError("is_default can only be used for built-in engines."); + } let isCurrent = false; let index = -1; if (extension.startupReason === "ADDON_UPGRADE") { @@ -70,6 +140,17 @@ this.chrome_settings_overrides = class extends ExtensionAPI { Components.utils.reportError(e); } } + // If the setting exists for the extension, but is missing from the manifest, + // remove it. This can happen if the extension removes is_default. + // There's really no good place to put this, because the entire search section + // could be removed. + // We'll never get here in the normal case because we always return early + // if we have an is_default value that we use. + if (ExtensionSettingsStore.hasSetting( + extension, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME)) { + await searchInitialized(); + this.processDefaultSearchSetting("removeSetting"); + } } async onShutdown(reason) { let {extension} = this; diff --git a/browser/components/extensions/schemas/chrome_settings_overrides.json b/browser/components/extensions/schemas/chrome_settings_overrides.json index b9c8f3656d97..5c6b66a71018 100644 --- a/browser/components/extensions/schemas/chrome_settings_overrides.json +++ b/browser/components/extensions/schemas/chrome_settings_overrides.json @@ -98,7 +98,7 @@ "is_default": { "type": "boolean", "optional": true, - "deprecated": "Unsupported on Firefox at this time." + "description": "Sets the default engine to a built-in engine only." } } } diff --git a/browser/components/extensions/test/browser/browser-common.ini b/browser/components/extensions/test/browser/browser-common.ini index a41cbbf6a655..a0279e2d1b3f 100644 --- a/browser/components/extensions/test/browser/browser-common.ini +++ b/browser/components/extensions/test/browser/browser-common.ini @@ -106,6 +106,7 @@ skip-if = debug || asan # Bug 1354681 [browser_ext_sessions_getRecentlyClosed_private.js] [browser_ext_sessions_getRecentlyClosed_tabs.js] [browser_ext_sessions_restore.js] +[browser_ext_settings_overrides_default_search.js] [browser_ext_settings_overrides_search.js] [browser_ext_sidebarAction.js] [browser_ext_sidebarAction_browser_style.js] diff --git a/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js new file mode 100644 index 000000000000..373ee7f31d41 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js @@ -0,0 +1,507 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "Management", () => { + const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {}); + return Management; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); + +const EXTENSION1_ID = "extension1@mozilla.com"; +const EXTENSION2_ID = "extension2@mozilla.com"; + +function awaitEvent(eventName, id) { + return new Promise(resolve => { + let listener = (_eventName, ...args) => { + let extension = args[0]; + if (_eventName === eventName && + extension.id == id) { + Management.off(eventName, listener); + resolve(...args); + } + }; + + Management.on(eventName, listener); + }); +} + +/* This tests setting a default engine. */ +add_task(async function test_extension_setting_default_engine() { + let defaultEngineName = Services.search.currentEngine.name; + + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests that using an invalid engine does nothing. */ +add_task(async function test_extension_setting_invalid_name_default_engine() { + let defaultEngineName = Services.search.currentEngine.name; + + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "InvalidName", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); + + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests that uninstalling add-ons maintains the proper + * search default. */ +add_task(async function test_extension_setting_multiple_default_engine() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + let ext2 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "Twitter", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + await ext2.startup(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + await ext2.unload(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests that uninstalling add-ons in reverse order maintains the proper + * search default. */ +add_task(async function test_extension_setting_multiple_default_engine_reversed() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + let ext2 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "Twitter", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + await ext2.startup(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + await ext1.unload(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + await ext2.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests adding an engine with one add-on and trying to make it the + *default with anoth. */ +add_task(async function test_extension_setting_invalid_default_engine() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "MozSearch", + "keyword": "MozSearch", + "search_url": "https://example.com/?q={searchTerms}", + }, + }, + }, + useAddonManager: "temporary", + }); + + let ext2 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "MozSearch", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); + + let engine = Services.search.getEngineByName("MozSearch"); + ok(engine, "Engine should exist."); + + await ext2.startup(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); + + await ext2.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); + + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests that when the user changes the search engine and the add-on + * is unistalled, search stays with the user's choice. */ +add_task(async function test_user_changing_default_engine() { + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + let engine = Services.search.getEngineByName("Twitter"); + Services.search.currentEngine = engine; + + await ext1.unload(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); +}); + +/* This tests that when the user changes the search engine while it is + * disabled, user choice is maintained when the add-on is reenabled. */ +add_task(async function test_user_change_with_disabling() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION1_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + let engine = Services.search.getEngineByName("Twitter"); + Services.search.currentEngine = engine; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID); + let addon = await AddonManager.getAddonByID(EXTENSION1_ID); + addon.userDisabled = true; + await disabledPromise; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + let enabledPromise = awaitEvent("ready", EXTENSION1_ID); + addon.userDisabled = false; + await enabledPromise; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + await ext1.unload(); +}); + +/* This tests that when two add-ons are installed that change default + * search and the first one is disabled, before the second one is installed, + * when the first one is reenabled, the second add-on keeps the search. */ +add_task(async function test_two_addons_with_first_disabled_before_second() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION1_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + let ext2 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION2_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Twitter", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID); + let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID); + addon1.userDisabled = true; + await disabledPromise; + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); + + await ext2.startup(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + let enabledPromise = awaitEvent("ready", EXTENSION1_ID); + addon1.userDisabled = false; + await enabledPromise; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + await ext2.unload(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests that when two add-ons are installed that change default + * search and the first one is disabled, the second one maintains + * the search. */ +add_task(async function test_two_addons_with_first_disabled() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION1_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + let ext2 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION2_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Twitter", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + await ext2.startup(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID); + let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID); + addon1.userDisabled = true; + await disabledPromise; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + let enabledPromise = awaitEvent("ready", EXTENSION1_ID); + addon1.userDisabled = false; + await enabledPromise; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + await ext2.unload(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +}); + +/* This tests that when two add-ons are installed that change default + * search and the second one is disabled, the first one properly + * gets the search. */ +add_task(async function test_two_addons_with_second_disabled() { + let defaultEngineName = Services.search.currentEngine.name; + let ext1 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION1_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "DuckDuckGo", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + let ext2 = ExtensionTestUtils.loadExtension({ + manifest: { + applications: { + gecko: { + id: EXTENSION2_ID, + }, + }, + "chrome_settings_overrides": { + "search_provider": { + "name": "Twitter", + "search_url": "https://example.com/?q={searchTerms}", + "is_default": true, + }, + }, + }, + useAddonManager: "temporary", + }); + + await ext1.startup(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + await ext2.startup(); + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + + let disabledPromise = awaitEvent("shutdown", EXTENSION2_ID); + let addon2 = await AddonManager.getAddonByID(EXTENSION2_ID); + addon2.userDisabled = true; + await disabledPromise; + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + + let enabledPromise = awaitEvent("ready", EXTENSION2_ID); + addon2.userDisabled = false; + await enabledPromise; + + is(Services.search.currentEngine.name, "Twitter", "Default engine is Twitter"); + await ext2.unload(); + + is(Services.search.currentEngine.name, "DuckDuckGo", "Default engine is DuckDuckGo"); + await ext1.unload(); + + is(Services.search.currentEngine.name, defaultEngineName, `Default engine is ${defaultEngineName}`); +});