summaryrefslogtreecommitdiffstats
path: root/toolkit/components/messaging-system/lib
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/messaging-system/lib/Logger.jsm22
-rw-r--r--toolkit/components/messaging-system/lib/RemoteSettingsExperimentLoader.jsm229
-rw-r--r--toolkit/components/messaging-system/lib/SharedDataMap.jsm163
-rw-r--r--toolkit/components/messaging-system/lib/SpecialMessageActions.jsm301
4 files changed, 715 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/lib/Logger.jsm b/toolkit/components/messaging-system/lib/Logger.jsm
new file mode 100644
index 0000000000..2afc3aa526
--- /dev/null
+++ b/toolkit/components/messaging-system/lib/Logger.jsm
@@ -0,0 +1,22 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["Logger"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+
+const LOGGING_PREF = "messaging-system.log";
+
+class Logger extends ConsoleAPI {
+ constructor(name) {
+ let consoleOptions = {
+ prefix: name,
+ maxLogLevel: Services.prefs.getCharPref(LOGGING_PREF, "warn"),
+ maxLogLevelPref: LOGGING_PREF,
+ };
+ super(consoleOptions);
+ }
+}
diff --git a/toolkit/components/messaging-system/lib/RemoteSettingsExperimentLoader.jsm b/toolkit/components/messaging-system/lib/RemoteSettingsExperimentLoader.jsm
new file mode 100644
index 0000000000..8a6ff55911
--- /dev/null
+++ b/toolkit/components/messaging-system/lib/RemoteSettingsExperimentLoader.jsm
@@ -0,0 +1,229 @@
+/* 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";
+
+/**
+ * @typedef {import("../experiments/@types/ExperimentManager").Recipe} Recipe
+ */
+
+const EXPORTED_SYMBOLS = [
+ "_RemoteSettingsExperimentLoader",
+ "RemoteSettingsExperimentLoader",
+];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm",
+ TargetingContext: "resource://messaging-system/targeting/Targeting.jsm",
+ ExperimentManager:
+ "resource://messaging-system/experiments/ExperimentManager.jsm",
+ RemoteSettings: "resource://services-settings/remote-settings.js",
+ CleanupManager: "resource://normandy/lib/CleanupManager.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ const { Logger } = ChromeUtils.import(
+ "resource://messaging-system/lib/Logger.jsm"
+ );
+ return new Logger("RSLoader");
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "timerManager",
+ "@mozilla.org/updates/timer-manager;1",
+ "nsIUpdateTimerManager"
+);
+
+const COLLECTION_ID_PREF = "messaging-system.rsexperimentloader.collection_id";
+const COLLECTION_ID_FALLBACK = "nimbus-desktop-experiments";
+const ENABLED_PREF = "messaging-system.rsexperimentloader.enabled";
+const STUDIES_OPT_OUT_PREF = "app.shield.optoutstudies.enabled";
+
+const TIMER_NAME = "rs-experiment-loader-timer";
+const TIMER_LAST_UPDATE_PREF = `app.update.lastUpdateTime.${TIMER_NAME}`;
+// Use the same update interval as normandy
+const RUN_INTERVAL_PREF = "app.normandy.run_interval_seconds";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "COLLECTION_ID",
+ COLLECTION_ID_PREF,
+ COLLECTION_ID_FALLBACK
+);
+
+class _RemoteSettingsExperimentLoader {
+ constructor() {
+ // Has the timer been set?
+ this._initialized = false;
+ // Are we in the middle of updating recipes already?
+ this._updating = false;
+
+ // Make it possible to override for testing
+ this.manager = ExperimentManager;
+
+ XPCOMUtils.defineLazyGetter(this, "remoteSettingsClient", () => {
+ return RemoteSettings(COLLECTION_ID);
+ });
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "enabled",
+ ENABLED_PREF,
+ false,
+ this.onEnabledPrefChange.bind(this)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "studiesEnabled",
+ STUDIES_OPT_OUT_PREF,
+ false,
+ this.onEnabledPrefChange.bind(this)
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "intervalInSeconds",
+ RUN_INTERVAL_PREF,
+ 21600,
+ () => this.setTimer()
+ );
+ }
+
+ async init() {
+ if (this._initialized || !this.enabled || !this.studiesEnabled) {
+ return;
+ }
+
+ this.setTimer();
+ CleanupManager.addCleanupHandler(() => this.uninit());
+ this._initialized = true;
+
+ await this.updateRecipes();
+ }
+
+ uninit() {
+ if (!this._initialized) {
+ return;
+ }
+ timerManager.unregisterTimer(TIMER_NAME);
+ this._initialized = false;
+ }
+
+ /**
+ * Checks targeting of a recipe if it is defined
+ * @param {Recipe} recipe
+ * @param {{[key: string]: any}} customContext A custom filter context
+ * @returns {Promise<boolean>} Should we process the recipe?
+ */
+ async checkTargeting(recipe, customContext = {}) {
+ const context = TargetingContext.combineContexts(
+ { experiment: recipe },
+ customContext,
+ ASRouterTargeting.Environment
+ );
+ const { targeting } = recipe;
+ if (!targeting) {
+ log.debug("No targeting for recipe, so it matches automatically");
+ return true;
+ }
+ log.debug("Testing targeting expression:", targeting);
+ const targetingContext = new TargetingContext(context);
+ let result = false;
+ try {
+ result = await targetingContext.evalWithDefault(targeting);
+ } catch (e) {
+ log.debug("Targeting failed because of an error");
+ Cu.reportError(e);
+ }
+ return Boolean(result);
+ }
+
+ /**
+ * Get all recipes from remote settings
+ * @param {string} trigger What caused the update to occur?
+ */
+ async updateRecipes(trigger) {
+ if (this._updating || !this._initialized) {
+ return;
+ }
+ this._updating = true;
+
+ log.debug("Updating recipes" + (trigger ? ` with trigger ${trigger}` : ""));
+
+ let recipes;
+ let loadingError = false;
+
+ try {
+ recipes = await this.remoteSettingsClient.get();
+ log.debug(`Got ${recipes.length} recipes from Remote Settings`);
+ } catch (e) {
+ log.debug("Error getting recipes from remote settings.");
+ loadingError = true;
+ Cu.reportError(e);
+ }
+
+ let matches = 0;
+ if (recipes && !loadingError) {
+ const context = this.manager.createTargetingContext();
+
+ for (const r of recipes) {
+ if (await this.checkTargeting(r, context)) {
+ matches++;
+ log.debug(`${r.id} matched`);
+ await this.manager.onRecipe(r, "rs-loader");
+ } else {
+ log.debug(`${r.id} did not match due to targeting`);
+ }
+ }
+
+ log.debug(`${matches} recipes matched. Finalizing ExperimentManager.`);
+ this.manager.onFinalize("rs-loader");
+ }
+
+ if (trigger !== "timer") {
+ const lastUpdateTime = Math.round(Date.now() / 1000);
+ Services.prefs.setIntPref(TIMER_LAST_UPDATE_PREF, lastUpdateTime);
+ }
+
+ this._updating = false;
+ }
+
+ /**
+ * Handles feature status based on feature pref and STUDIES_OPT_OUT_PREF.
+ * Changing any of them to false will turn off any recipe fetching and
+ * processing.
+ */
+ onEnabledPrefChange(prefName, oldValue, newValue) {
+ if (this._initialized && !newValue) {
+ this.uninit();
+ } else if (!this._initialized && newValue && this.enabled) {
+ // If the feature pref is turned on then turn on recipe processing.
+ // If the opt in pref is turned on then turn on recipe processing only if
+ // the feature pref is also enabled.
+ this.init();
+ }
+ }
+
+ /**
+ * Sets a timer to update recipes every this.intervalInSeconds
+ */
+ setTimer() {
+ // When this function is called, updateRecipes is also called immediately
+ timerManager.registerTimer(
+ TIMER_NAME,
+ () => this.updateRecipes("timer"),
+ this.intervalInSeconds
+ );
+ log.debug("Registered update timer");
+ }
+}
+
+const RemoteSettingsExperimentLoader = new _RemoteSettingsExperimentLoader();
diff --git a/toolkit/components/messaging-system/lib/SharedDataMap.jsm b/toolkit/components/messaging-system/lib/SharedDataMap.jsm
new file mode 100644
index 0000000000..b61b112a1d
--- /dev/null
+++ b/toolkit/components/messaging-system/lib/SharedDataMap.jsm
@@ -0,0 +1,163 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["SharedDataMap"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+
+const IS_MAIN_PROCESS =
+ Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "JSONFile",
+ "resource://gre/modules/JSONFile.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+class SharedDataMap extends EventEmitter {
+ constructor(sharedDataKey, options = { isParent: IS_MAIN_PROCESS }) {
+ super();
+
+ this._sharedDataKey = sharedDataKey;
+ this._isParent = options.isParent;
+ this._isReady = false;
+ this._readyDeferred = PromiseUtils.defer();
+ this._data = null;
+
+ if (this.isParent) {
+ // Lazy-load JSON file that backs Storage instances.
+ XPCOMUtils.defineLazyGetter(this, "_store", () => {
+ let path = options.path;
+ let store = null;
+ if (!path) {
+ try {
+ const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
+ path = PathUtils.join(profileDir, `${sharedDataKey}.json`);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ try {
+ store = new JSONFile({ path });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ return store;
+ });
+ } else {
+ this._syncFromParent();
+ Services.cpmm.sharedData.addEventListener("change", this);
+ }
+ }
+
+ async init() {
+ if (!this._isReady && this.isParent) {
+ await this._store.load();
+ this._data = this._store.data;
+ this._syncToChildren({ flush: true });
+ this._checkIfReady();
+ }
+ }
+
+ get sharedDataKey() {
+ return this._sharedDataKey;
+ }
+
+ get isParent() {
+ return this._isParent;
+ }
+
+ ready() {
+ return this._readyDeferred.promise;
+ }
+
+ get(key) {
+ if (!this._data) {
+ return null;
+ }
+ return this._data[key];
+ }
+
+ set(key, value) {
+ if (!this.isParent) {
+ throw new Error(
+ "Setting values from within a content process is not allowed"
+ );
+ }
+ this._store.data[key] = value;
+ this._store.saveSoon();
+ this._syncToChildren();
+ this._notifyUpdate();
+ }
+
+ // Only used in tests
+ _deleteForTests(key) {
+ if (!this.isParent) {
+ throw new Error(
+ "Setting values from within a content process is not allowed"
+ );
+ }
+ if (this.has(key)) {
+ delete this._store.data[key];
+ this._store.saveSoon();
+ this._syncToChildren();
+ this._notifyUpdate();
+ }
+ }
+
+ has(key) {
+ return Boolean(this.get(key));
+ }
+
+ /**
+ * Notify store listeners of updates
+ * Called both from Main and Content process
+ */
+ _notifyUpdate(process = "parent") {
+ for (let key of Object.keys(this._data || {})) {
+ this.emit(`${process}-store-update:${key}`, this._data[key]);
+ }
+ }
+
+ _syncToChildren({ flush = false } = {}) {
+ Services.ppmm.sharedData.set(this.sharedDataKey, this._data);
+ if (flush) {
+ Services.ppmm.sharedData.flush();
+ }
+ }
+
+ _syncFromParent() {
+ this._data = Services.cpmm.sharedData.get(this.sharedDataKey);
+ this._checkIfReady();
+ this._notifyUpdate("child");
+ }
+
+ _checkIfReady() {
+ if (!this._isReady && this._data) {
+ this._isReady = true;
+ this._readyDeferred.resolve();
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type === "change") {
+ if (event.changedKeys.includes(this.sharedDataKey)) {
+ this._syncFromParent();
+ }
+ }
+ }
+}
diff --git a/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm b/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
new file mode 100644
index 0000000000..3d7bed5330
--- /dev/null
+++ b/toolkit/components/messaging-system/lib/SpecialMessageActions.jsm
@@ -0,0 +1,301 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["SpecialMessageActions"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const DOH_DOORHANGER_DECISION_PREF = "doh-rollout.doorhanger-decision";
+const NETWORK_TRR_MODE_PREF = "network.trr.mode";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.jsm",
+ UITour: "resource:///modules/UITour.jsm",
+ FxAccounts: "resource://gre/modules/FxAccounts.jsm",
+ MigrationUtils: "resource:///modules/MigrationUtils.jsm",
+});
+
+const SpecialMessageActions = {
+ // This is overridden by ASRouter.init
+ blockMessageById() {
+ throw new Error("ASRouter not intialized yet");
+ },
+
+ /**
+ * loadAddonIconInURLBar - load addons-notification icon by displaying
+ * box containing addons icon in urlbar. See Bug 1513882
+ *
+ * @param {Browser} browser browser element for showing addons icon
+ */
+ loadAddonIconInURLBar(browser) {
+ if (!browser) {
+ return;
+ }
+ const chromeDoc = browser.ownerDocument;
+ let notificationPopupBox = chromeDoc.getElementById(
+ "notification-popup-box"
+ );
+ if (!notificationPopupBox) {
+ return;
+ }
+ if (
+ notificationPopupBox.style.display === "none" ||
+ notificationPopupBox.style.display === ""
+ ) {
+ notificationPopupBox.style.display = "block";
+ }
+ },
+
+ /**
+ *
+ * @param {Browser} browser The revelant Browser
+ * @param {string} url URL to look up install location
+ * @param {string} telemetrySource Telemetry information to pass to getInstallForURL
+ */
+ async installAddonFromURL(browser, url, telemetrySource = "amo") {
+ try {
+ this.loadAddonIconInURLBar(browser);
+ const aUri = Services.io.newURI(url);
+ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+ // AddonManager installation source associated to the addons installed from activitystream's CFR
+ // and RTAMO (source is going to be "amo" if not configured explicitly in the message provider).
+ const telemetryInfo = { source: telemetrySource };
+ const install = await AddonManager.getInstallForURL(aUri.spec, {
+ telemetryInfo,
+ });
+ await AddonManager.installAddonFromWebpage(
+ "application/x-xpinstall",
+ browser,
+ systemPrincipal,
+ install
+ );
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Set browser as the operating system default browser.
+ *
+ * @param {Window} window Reference to a window object
+ */
+ setDefaultBrowser(window) {
+ window.getShellService().setAsDefault();
+ },
+
+ /**
+ * Reset browser homepage and newtab to default with a certain section configuration
+ *
+ * @param {"default"|null} home Value to set for browser homepage
+ * @param {"default"|null} newtab Value to set for browser newtab
+ * @param {obj} layout Configuration options for newtab sections
+ * @returns {undefined}
+ */
+ configureHomepage({ homePage = null, newtab = null, layout = null }) {
+ // Homepage can be default, blank or a custom url
+ if (homePage === "default") {
+ Services.prefs.clearUserPref("browser.startup.homepage");
+ }
+ // Newtab page can only be default or blank
+ if (newtab === "default") {
+ Services.prefs.clearUserPref("browser.newtabpage.enabled");
+ }
+ if (layout) {
+ // Existing prefs that interact with the newtab page layout, we default to true
+ // or payload configuration
+ let newtabConfigurations = [
+ [
+ // controls the search bar
+ "browser.newtabpage.activity-stream.showSearch",
+ layout.search,
+ ],
+ [
+ // controls the topsites
+ "browser.newtabpage.activity-stream.feeds.topsites",
+ layout.topsites,
+ // User can control number of topsite rows
+ ["browser.newtabpage.activity-stream.topSitesRows"],
+ ],
+ [
+ // controls the highlights section
+ "browser.newtabpage.activity-stream.feeds.section.highlights",
+ layout.highlights,
+ // User can control number of rows and highlight sources
+ [
+ "browser.newtabpage.activity-stream.section.highlights.rows",
+ "browser.newtabpage.activity-stream.section.highlights.includeVisited",
+ "browser.newtabpage.activity-stream.section.highlights.includePocket",
+ "browser.newtabpage.activity-stream.section.highlights.includeDownloads",
+ "browser.newtabpage.activity-stream.section.highlights.includeBookmarks",
+ ],
+ ],
+ [
+ // controls the snippets section
+ "browser.newtabpage.activity-stream.feeds.snippets",
+ layout.snippets,
+ ],
+ [
+ // controls the topstories section
+ "browser.newtabpage.activity-stream.feeds.system.topstories",
+ layout.topstories,
+ ],
+ ].filter(
+ // If a section has configs that the user changed we will skip that section
+ ([, , sectionConfigs]) =>
+ !sectionConfigs ||
+ sectionConfigs.every(
+ prefName => !Services.prefs.prefHasUserValue(prefName)
+ )
+ );
+
+ for (let [prefName, prefValue] of newtabConfigurations) {
+ Services.prefs.setBoolPref(prefName, prefValue);
+ }
+ }
+ },
+
+ /**
+ * Processes "Special Message Actions", which are definitions of behaviors such as opening tabs
+ * installing add-ons, or focusing the awesome bar that are allowed to can be triggered from
+ * Messaging System interactions.
+ *
+ * @param {{type: string, data?: any}} action User action defined in message JSON.
+ * @param browser {Browser} The browser most relvant to the message.
+ */
+ async handleAction(action, browser) {
+ const window = browser.ownerGlobal;
+ switch (action.type) {
+ case "SHOW_MIGRATION_WIZARD":
+ MigrationUtils.showMigrationWizard(window, [
+ MigrationUtils.MIGRATION_ENTRYPOINT_NEWTAB,
+ action.data?.source,
+ ]);
+ break;
+ case "OPEN_PRIVATE_BROWSER_WINDOW":
+ // Forcefully open about:privatebrowsing
+ window.OpenBrowserWindow({ private: true });
+ break;
+ case "OPEN_URL":
+ window.openLinkIn(
+ Services.urlFormatter.formatURL(action.data.args),
+ action.data.where || "current",
+ {
+ private: false,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ csp: null,
+ }
+ );
+ break;
+ case "OPEN_ABOUT_PAGE":
+ let aboutPageURL = new URL(`about:${action.data.args}`);
+ if (action.data.entrypoint) {
+ aboutPageURL.search = action.data.entrypoint;
+ }
+ window.openTrustedLinkIn(
+ aboutPageURL.toString(),
+ action.data.where || "tab"
+ );
+ break;
+ case "OPEN_PREFERENCES_PAGE":
+ window.openPreferences(
+ action.data.category || action.data.args,
+ action.data.entrypoint && {
+ urlParams: { entrypoint: action.data.entrypoint },
+ }
+ );
+ break;
+ case "OPEN_APPLICATIONS_MENU":
+ UITour.showMenu(window, action.data.args);
+ break;
+ case "HIGHLIGHT_FEATURE":
+ const highlight = await UITour.getTarget(window, action.data.args);
+ if (highlight) {
+ await UITour.showHighlight(window, highlight, "none", {
+ autohide: true,
+ });
+ }
+ break;
+ case "INSTALL_ADDON_FROM_URL":
+ await this.installAddonFromURL(
+ browser,
+ action.data.url,
+ action.data.telemetrySource
+ );
+ break;
+ case "SET_DEFAULT_BROWSER":
+ this.setDefaultBrowser(window);
+ break;
+ case "PIN_CURRENT_TAB":
+ let tab = window.gBrowser.selectedTab;
+ window.gBrowser.pinTab(tab);
+ window.ConfirmationHint.show(tab, "pinTab", {
+ showDescription: true,
+ });
+ break;
+ case "SHOW_FIREFOX_ACCOUNTS":
+ const data = action.data;
+ const url = await FxAccounts.config.promiseConnectAccountURI(
+ (data && data.entrypoint) || "snippets",
+ (data && data.extraParams) || {}
+ );
+ // We want to replace the current tab.
+ window.openLinkIn(url, "current", {
+ private: false,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ csp: null,
+ });
+ break;
+ case "OPEN_PROTECTION_PANEL":
+ let { gProtectionsHandler } = window;
+ gProtectionsHandler.showProtectionsPopup({});
+ break;
+ case "OPEN_PROTECTION_REPORT":
+ window.gProtectionsHandler.openProtections();
+ break;
+ case "OPEN_AWESOME_BAR":
+ window.gURLBar.search("");
+ break;
+ case "DISABLE_STP_DOORHANGERS":
+ await this.blockMessageById([
+ "SOCIAL_TRACKING_PROTECTION",
+ "FINGERPRINTERS_PROTECTION",
+ "CRYPTOMINERS_PROTECTION",
+ ]);
+ break;
+ case "DISABLE_DOH":
+ Services.prefs.setStringPref(
+ DOH_DOORHANGER_DECISION_PREF,
+ "UIDisabled"
+ );
+ Services.prefs.setIntPref(NETWORK_TRR_MODE_PREF, 5);
+ break;
+ case "ACCEPT_DOH":
+ Services.prefs.setStringPref(DOH_DOORHANGER_DECISION_PREF, "UIOk");
+ break;
+ case "CANCEL":
+ // A no-op used by CFRs that minimizes the notification but does not
+ // trigger a dismiss or block (it keeps the notification around)
+ break;
+ case "CONFIGURE_HOMEPAGE":
+ this.configureHomepage(action.data);
+ const topWindow = browser.ownerGlobal.window.BrowserWindowTracker.getTopWindow();
+ if (topWindow) {
+ topWindow.BrowserHome();
+ }
+ break;
+ default:
+ throw new Error(
+ `Special message action with type ${action.type} is unsupported.`
+ );
+ }
+ },
+};