summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/ASRouterPreferences.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/newtab/lib/ASRouterPreferences.jsm259
1 files changed, 259 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/ASRouterPreferences.jsm b/browser/components/newtab/lib/ASRouterPreferences.jsm
new file mode 100644
index 0000000000..c2fc071fbd
--- /dev/null
+++ b/browser/components/newtab/lib/ASRouterPreferences.jsm
@@ -0,0 +1,259 @@
+/* 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 { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const PROVIDER_PREF_BRANCH =
+ "browser.newtabpage.activity-stream.asrouter.providers.";
+const DEVTOOLS_PREF =
+ "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
+
+/**
+ * Use `ASRouterPreferences.console.debug()` and friends from ASRouter files to
+ * log messages during development. See LOG_LEVELS in ConsoleAPI.jsm for the
+ * available methods as well as the available values for this pref.
+ */
+const DEBUG_PREF = "browser.newtabpage.activity-stream.asrouter.debugLogLevel";
+
+const FXA_USERNAME_PREF = "services.sync.username";
+
+const DEFAULT_STATE = {
+ _initialized: false,
+ _providers: null,
+ _providerPrefBranch: PROVIDER_PREF_BRANCH,
+ _devtoolsEnabled: null,
+ _devtoolsPref: DEVTOOLS_PREF,
+};
+
+const USER_PREFERENCES = {
+ snippets: "browser.newtabpage.activity-stream.feeds.snippets",
+ cfrAddons: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
+ cfrFeatures:
+ "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
+};
+
+// Preferences that influence targeting attributes. When these change we need
+// to re-evaluate if the message targeting still matches
+const TARGETING_PREFERENCES = [FXA_USERNAME_PREF];
+
+const TEST_PROVIDERS = [
+ {
+ id: "snippets_local_testing",
+ type: "local",
+ localProvider: "SnippetsTestMessageProvider",
+ enabled: true,
+ },
+ {
+ id: "panel_local_testing",
+ type: "local",
+ localProvider: "PanelTestProvider",
+ enabled: true,
+ },
+];
+
+class _ASRouterPreferences {
+ constructor() {
+ Object.assign(this, DEFAULT_STATE);
+ this._callbacks = new Set();
+
+ XPCOMUtils.defineLazyGetter(this, "console", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ maxLogLevel: "error",
+ maxLogLevelPref: DEBUG_PREF,
+ prefix: "ASRouter",
+ };
+ return new ConsoleAPI(consoleOptions);
+ });
+ }
+
+ _transformPersonalizedCfrScores(value) {
+ let result = {};
+ try {
+ result = JSON.parse(value);
+ } catch (e) {
+ console.error(e);
+ }
+ return result;
+ }
+
+ _getProviderConfig() {
+ const prefList = Services.prefs.getChildList(this._providerPrefBranch);
+ return prefList.reduce((filtered, pref) => {
+ let value;
+ try {
+ value = JSON.parse(Services.prefs.getStringPref(pref, ""));
+ } catch (e) {
+ console.error(
+ `Could not parse ASRouter preference. Try resetting ${pref} in about:config.`
+ );
+ }
+ if (value) {
+ filtered.push(value);
+ }
+ return filtered;
+ }, []);
+ }
+
+ get providers() {
+ if (!this._initialized || this._providers === null) {
+ const config = this._getProviderConfig();
+ const providers = config.map(provider => Object.freeze(provider));
+ if (this.devtoolsEnabled) {
+ providers.unshift(...TEST_PROVIDERS);
+ }
+ this._providers = Object.freeze(providers);
+ }
+
+ return this._providers;
+ }
+
+ enableOrDisableProvider(id, value) {
+ const providers = this._getProviderConfig();
+ const config = providers.find(p => p.id === id);
+ if (!config) {
+ console.error(
+ `Cannot set enabled state for '${id}' because the pref ${this._providerPrefBranch}${id} does not exist or is not correctly formatted.`
+ );
+ return;
+ }
+
+ Services.prefs.setStringPref(
+ this._providerPrefBranch + id,
+ JSON.stringify({ ...config, enabled: value })
+ );
+ }
+
+ resetProviderPref() {
+ for (const pref of Services.prefs.getChildList(this._providerPrefBranch)) {
+ Services.prefs.clearUserPref(pref);
+ }
+ for (const id of Object.keys(USER_PREFERENCES)) {
+ Services.prefs.clearUserPref(USER_PREFERENCES[id]);
+ }
+ }
+
+ /**
+ * Bug 1800087 - Migrate the ASRouter message provider prefs' values to the
+ * current format (provider.bucket -> provider.collection).
+ *
+ * TODO (Bug 1800937): Remove migration code after the next watershed release.
+ */
+ _migrateProviderPrefs() {
+ const prefList = Services.prefs.getChildList(this._providerPrefBranch);
+ for (const pref of prefList) {
+ if (!Services.prefs.prefHasUserValue(pref)) {
+ continue;
+ }
+ try {
+ let value = JSON.parse(Services.prefs.getStringPref(pref, ""));
+ if (value && "bucket" in value && !("collection" in value)) {
+ const { bucket, ...rest } = value;
+ Services.prefs.setStringPref(
+ pref,
+ JSON.stringify({
+ ...rest,
+ collection: bucket,
+ })
+ );
+ }
+ } catch (e) {
+ Services.prefs.clearUserPref(pref);
+ }
+ }
+ }
+
+ get devtoolsEnabled() {
+ if (!this._initialized || this._devtoolsEnabled === null) {
+ this._devtoolsEnabled = Services.prefs.getBoolPref(
+ this._devtoolsPref,
+ false
+ );
+ }
+ return this._devtoolsEnabled;
+ }
+
+ observe(aSubject, aTopic, aPrefName) {
+ if (aPrefName && aPrefName.startsWith(this._providerPrefBranch)) {
+ this._providers = null;
+ } else if (aPrefName === this._devtoolsPref) {
+ this._providers = null;
+ this._devtoolsEnabled = null;
+ }
+ this._callbacks.forEach(cb => cb(aPrefName));
+ }
+
+ getUserPreference(name) {
+ const prefName = USER_PREFERENCES[name] || name;
+ return Services.prefs.getBoolPref(prefName, true);
+ }
+
+ getAllUserPreferences() {
+ const values = {};
+ for (const id of Object.keys(USER_PREFERENCES)) {
+ values[id] = this.getUserPreference(id);
+ }
+ return values;
+ }
+
+ setUserPreference(providerId, value) {
+ if (!USER_PREFERENCES[providerId]) {
+ return;
+ }
+ Services.prefs.setBoolPref(USER_PREFERENCES[providerId], value);
+ }
+
+ addListener(callback) {
+ this._callbacks.add(callback);
+ }
+
+ removeListener(callback) {
+ this._callbacks.delete(callback);
+ }
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._migrateProviderPrefs();
+ Services.prefs.addObserver(this._providerPrefBranch, this);
+ Services.prefs.addObserver(this._devtoolsPref, this);
+ for (const id of Object.keys(USER_PREFERENCES)) {
+ Services.prefs.addObserver(USER_PREFERENCES[id], this);
+ }
+ for (const targetingPref of TARGETING_PREFERENCES) {
+ Services.prefs.addObserver(targetingPref, this);
+ }
+ this._initialized = true;
+ }
+
+ uninit() {
+ if (this._initialized) {
+ Services.prefs.removeObserver(this._providerPrefBranch, this);
+ Services.prefs.removeObserver(this._devtoolsPref, this);
+ for (const id of Object.keys(USER_PREFERENCES)) {
+ Services.prefs.removeObserver(USER_PREFERENCES[id], this);
+ }
+ for (const targetingPref of TARGETING_PREFERENCES) {
+ Services.prefs.removeObserver(targetingPref, this);
+ }
+ }
+ Object.assign(this, DEFAULT_STATE);
+ this._callbacks.clear();
+ }
+}
+
+const ASRouterPreferences = new _ASRouterPreferences();
+
+const EXPORTED_SYMBOLS = [
+ "_ASRouterPreferences",
+ "ASRouterPreferences",
+ "TEST_PROVIDERS",
+ "TARGETING_PREFERENCES",
+];