fune/browser/components/extensions/parent/ext-chrome-settings-overrides.js
Mike de Boer 481ae95c00 Bug 1524593 - nsISearchService (aka nsIBrowserSearchService, previously) refactor to be mostly an asynchronous, in preparation of WebExtension engines. r=daleharvey
This is a rollup of all the patches that have landed on the cedar project branch:

891252fdd0
Bug 1492475 - Part 1: Migrate most, if not all nsSearchService consumers to use async APIs. r=florian

79b2eb2367
Bug 1492475 - Part 2: Move nsIBrowserSearchService.idl to toolkit/components/search/nsISearchService.idl and update references. r=florian

a947d3cdf0
Bug 1492475 - Part 3: The search service init() method should simply return a Promise. r=florian

c1e172dfac
Bug 1492475 - Part 4: Remove the synchronous initialization flow. r=florian

cd41189eac
Bug 1492475 - Part 5: Since async initialization of the search service now is implicit behavior, remove the distinctive verbiage used internally. r=florian

2ae7189dfa
Bug 1492475 - Part 6: Update the cache build task to work with an actual Promise and re-initialize only once at the same time - all to fix race conditions here. r=florian

c8ee92973f
Bug 1492475 - Part 7: Make the region fetch not block the init flow, to ensure it's as fast as possible. r=florian

c44e674e16
Bug 1492475 - Part 8: Introduce an init flag, which can only be used privately, that allows to explicitly skip waiting for the region check process to complete. r=florian

6c79eaf1d3
Bug 1492475 - Part 9: Update unit tests to stop using 'currentEngine', in favor of 'defaultEngine'. r=Standard8

21b3aa17ee
Bug 1492475 - Part 10: Update unit tests to be fully aware of the new, async signatures of the search service API and remove sync init flow tests. r=mkaply,florian

ce5ba69019
Bug 1492475 - Part 11: Repair incorrect usage of the `identifier` property of nsISearchEngine instances. r=florian

fd177a7994
Bug 1518543 - Fix up the Android (Fennec) nsISearchService shim to work with the new asynchronous API. r=florian

3653d8ee22
Bug 1523708 - Change the search service interaction in the show-heartbeat action to use the new async API. r=florian

Differential Revision: https://phabricator.services.mozilla.com/D18355

--HG--
rename : netwerk/base/nsIBrowserSearchService.idl => toolkit/components/search/nsISearchService.idl
extra : moz-landing-system : lando
2019-02-02 11:27:21 +00:00

332 lines
13 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/. */
"use strict";
var {ExtensionPreferencesManager} = ChromeUtils.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
"resource://gre/modules/ExtensionSettingsStore.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionControlledPopup",
"resource:///modules/ExtensionControlledPopup.jsm");
const DEFAULT_SEARCH_STORE_TYPE = "default_search";
const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
const ENGINE_ADDED_SETTING_NAME = "engineAdded";
const HOMEPAGE_PREF = "browser.startup.homepage";
const HOMEPAGE_CONFIRMED_TYPE = "homepageNotification";
const HOMEPAGE_SETTING_TYPE = "prefs";
const HOMEPAGE_SETTING_NAME = "homepage_override";
XPCOMUtils.defineLazyGetter(this, "homepagePopup", () => {
return new ExtensionControlledPopup({
confirmedType: HOMEPAGE_CONFIRMED_TYPE,
observerTopic: "browser-open-homepage-start",
popupnotificationId: "extension-homepage-notification",
settingType: HOMEPAGE_SETTING_TYPE,
settingKey: HOMEPAGE_SETTING_NAME,
descriptionId: "extension-homepage-notification-description",
descriptionMessageId: "homepageControlled.message",
learnMoreMessageId: "homepageControlled.learnMore",
learnMoreLink: "extension-home",
async beforeDisableAddon(popup, win) {
// Disabling an add-on should remove the tabs that it has open, but we want
// to open the new homepage in this tab (which might get closed).
// 1. Replace the tab's URL with about:blank, wait for it to change
// 2. Now that this tab isn't associated with the add-on, disable the add-on
// 3. Trigger the browser's homepage method
let gBrowser = win.gBrowser;
let tab = gBrowser.selectedTab;
await replaceUrlInTab(gBrowser, tab, "about:blank");
Services.prefs.addObserver(HOMEPAGE_PREF, async function prefObserver() {
Services.prefs.removeObserver(HOMEPAGE_PREF, prefObserver);
let loaded = waitForTabLoaded(tab);
win.BrowserHome();
await loaded;
// Manually trigger an event in case this is controlled again.
popup.open();
});
},
});
});
// When the browser starts up it will trigger the observer topic we're expecting
// but that happens before our observer has been registered. To handle the
// startup case we need to check if the preferences are set to load the homepage
// and check if the homepage is active, then show the doorhanger in that case.
async function handleInitialHomepagePopup(extensionId, homepageUrl) {
// browser.startup.page == 1 is show homepage.
if (Services.prefs.getIntPref("browser.startup.page") == 1) {
let {gBrowser} = windowTracker.topWindow;
let tab = gBrowser.selectedTab;
let currentUrl = gBrowser.currentURI.spec;
// When the first window is still loading the URL might be about:blank.
// Wait for that the actual page to load before checking the URL, unless
// the homepage is set to about:blank.
if (currentUrl != homepageUrl && currentUrl == "about:blank") {
await waitForTabLoaded(tab);
currentUrl = gBrowser.currentURI.spec;
}
// Once the page has loaded, if necessary and the active tab hasn't changed,
// then show the popup now.
if (currentUrl == homepageUrl && gBrowser.selectedTab == tab) {
homepagePopup.open();
return;
}
}
homepagePopup.addObserver(extensionId);
}
this.chrome_settings_overrides = class extends ExtensionAPI {
static async processDefaultSearchSetting(action, id) {
await ExtensionSettingsStore.initialize();
let item = ExtensionSettingsStore.getSetting(DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
if (!item) {
return;
}
if (Services.search.defaultEngine.name != item.value &&
Services.search.defaultEngine.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(id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
return;
}
item = ExtensionSettingsStore[action](id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME);
if (item) {
try {
let engine = Services.search.getEngineByName(item.value || item.initialValue);
if (engine) {
Services.search.defaultEngine = engine;
}
} catch (e) {
Cu.reportError(e);
}
}
}
static async removeEngine(id) {
await ExtensionSettingsStore.initialize();
let item = await ExtensionSettingsStore.getSetting(
DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME, id);
if (item) {
ExtensionSettingsStore.removeSetting(
id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME);
await searchInitialized;
let engine = Services.search.getEngineByName(item.value);
try {
await Services.search.removeEngine(engine);
} catch (e) {
Cu.reportError(e);
}
}
}
static removeSearchSettings(id) {
return Promise.all([
this.processDefaultSearchSetting("removeSetting", id),
this.removeEngine(id),
]);
}
static onUninstall(id) {
// Note: We do not have to deal with homepage here as it is managed by
// the ExtensionPreferencesManager.
return Promise.all([
this.removeSearchSettings(id),
homepagePopup.clearConfirmation(id),
]);
}
static onUpdate(id, manifest) {
let haveHomepage = manifest && manifest.chrome_settings_overrides &&
manifest.chrome_settings_overrides.homepage;
if (!haveHomepage) {
ExtensionPreferencesManager.removeSetting(id, "homepage_override");
}
let haveSearchProvider = manifest && manifest.chrome_settings_overrides &&
manifest.chrome_settings_overrides.search_provider;
if (!haveSearchProvider) {
this.removeSearchSettings(id);
}
}
async onManifestEntry(entryName) {
let {extension} = this;
let {manifest} = extension;
await ExtensionSettingsStore.initialize();
let homepageUrl = manifest.chrome_settings_overrides.homepage;
if (homepageUrl) {
let inControl;
if (extension.startupReason == "ADDON_INSTALL" ||
extension.startupReason == "ADDON_ENABLE") {
inControl = await ExtensionPreferencesManager.setSetting(
extension.id, "homepage_override", homepageUrl);
} else {
let item = await ExtensionPreferencesManager.getSetting("homepage_override");
inControl = item.id == extension.id;
}
// We need to add the listener here too since onPrefsChanged won't trigger on a
// restart (the prefs are already set).
if (inControl) {
if (extension.startupReason == "APP_STARTUP") {
handleInitialHomepagePopup(extension.id, homepageUrl);
} else {
homepagePopup.addObserver(extension.id);
}
}
extension.callOnClose({
close: () => {
if (extension.shutdownReason == "ADDON_DISABLE") {
homepagePopup.clearConfirmation(extension.id);
}
},
});
}
if (manifest.chrome_settings_overrides.search_provider) {
await searchInitialized;
extension.callOnClose({
close: () => {
if (extension.shutdownReason == "ADDON_DISABLE") {
chrome_settings_overrides.processDefaultSearchSetting("disable", extension.id);
chrome_settings_overrides.removeEngine(extension.id);
}
},
});
let searchProvider = manifest.chrome_settings_overrides.search_provider;
let engineName = searchProvider.name.trim();
if (searchProvider.is_default) {
let engine = Services.search.getEngineByName(engineName);
let defaultEngines = await Services.search.getDefaultEngines();
if (engine && defaultEngines.some(defaultEngine => defaultEngine.name == engineName)) {
// Needs to be called every time to handle reenabling, but
// only sets default for install or enable.
await this.setDefault(engineName);
// For built in search engines, we don't do anything further
return;
}
}
await this.addSearchEngine();
if (searchProvider.is_default) {
if (extension.startupReason === "ADDON_INSTALL") {
// Don't ask if it already the current engine
let engine = Services.search.getEngineByName(engineName);
let defaultEngine = await Services.search.getDefault();
if (defaultEngine.name != engine.name) {
let subject = {
wrappedJSObject: {
// This is a hack because we don't have the browser of
// the actual install. This means the popup might show
// in a different window. Will be addressed in a followup bug.
browser: windowTracker.topWindow.gBrowser.selectedBrowser,
name: this.extension.name,
icon: this.extension.iconURL,
currentEngine: defaultEngine.name,
newEngine: engineName,
resolve(allow) {
if (allow) {
ExtensionSettingsStore.addSetting(
extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME, engineName, () => defaultEngine.name);
Services.search.defaultEngine = Services.search.getEngineByName(engineName);
}
},
},
};
Services.obs.notifyObservers(subject, "webextension-defaultsearch-prompt");
}
} else {
// Needs to be called every time to handle reenabling, but
// only sets default for install or enable.
this.setDefault(engineName);
}
} else if (ExtensionSettingsStore.hasSetting(extension.id,
DEFAULT_SEARCH_STORE_TYPE,
DEFAULT_SEARCH_SETTING_NAME)) {
// is_default has been removed, but we still have a setting. Remove it.
chrome_settings_overrides.processDefaultSearchSetting("removeSetting", extension.id);
}
}
}
async setDefault(engineName) {
let {extension} = this;
if (extension.startupReason === "ADDON_INSTALL") {
let defaultEngine = await Services.search.getDefault();
let item = await ExtensionSettingsStore.addSetting(
extension.id, DEFAULT_SEARCH_STORE_TYPE, DEFAULT_SEARCH_SETTING_NAME, engineName, () => defaultEngine.name);
await Services.search.setDefault(Services.search.getEngineByName(item.value));
} else if (extension.startupReason === "ADDON_ENABLE") {
chrome_settings_overrides.processDefaultSearchSetting("enable", extension.id);
}
}
async addSearchEngine() {
let {extension} = this;
let isCurrent = false;
let index = -1;
if (extension.startupReason === "ADDON_UPGRADE") {
let engines = await Services.search.getEnginesByExtensionID(extension.id);
if (engines.length > 0) {
let firstEngine = engines[0];
let firstEngineName = firstEngine.name;
// There can be only one engine right now
isCurrent = (await Services.search.getDefault()).name == firstEngineName;
// Get position of engine and store it
index = (await Services.search.getEngines()).map(engine => engine.name).indexOf(firstEngineName);
await Services.search.removeEngine(firstEngine);
}
}
try {
await Services.search.addEnginesFromExtension(extension);
// Bug 1488516. Preparing to support multiple engines per extension so
// multiple locales can be loaded.
let engines = await Services.search.getEnginesByExtensionID(extension.id);
await ExtensionSettingsStore.addSetting(
extension.id, DEFAULT_SEARCH_STORE_TYPE, ENGINE_ADDED_SETTING_NAME,
engines[0].name);
if (extension.startupReason === "ADDON_UPGRADE") {
let engine = Services.search.getEngineByName(engines[0].name);
if (isCurrent) {
await Services.search.setDefault(engine);
}
if (index != -1) {
await Services.search.moveEngine(engine, index);
}
}
} catch (e) {
Cu.reportError(e);
return false;
}
return true;
}
};
ExtensionPreferencesManager.addSetting("homepage_override", {
prefNames: [
HOMEPAGE_PREF,
],
// ExtensionPreferencesManager will call onPrefsChanged when control changes
// and it updates the preferences. We are passed the item from
// ExtensionSettingsStore that details what is in control. If there is an id
// then control has changed to an extension, if there is no id then control
// has been returned to the user.
onPrefsChanged(item) {
if (item.id) {
homepagePopup.addObserver(item.id);
} else {
homepagePopup.removeObserver();
}
},
setCallback(value) {
return {
[HOMEPAGE_PREF]: value,
};
},
});