ASRouterPreferences.sys.mjs (9804B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // eslint-disable-next-line mozilla/use-static-import 6 const { XPCOMUtils } = ChromeUtils.importESModule( 7 "resource://gre/modules/XPCOMUtils.sys.mjs" 8 ); 9 10 const lazy = {}; 11 12 ChromeUtils.defineESModuleGetters(lazy, { 13 SelectableProfileService: 14 "resource:///modules/profiles/SelectableProfileService.sys.mjs", 15 }); 16 17 const PROVIDER_PREF_BRANCH = 18 "browser.newtabpage.activity-stream.asrouter.providers."; 19 const DEVTOOLS_PREF = 20 "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled"; 21 22 /** 23 * Use `ASRouterPreferences.console.debug()` and friends from ASRouter files to 24 * log messages during development. See LOG_LEVELS in Console.sys.mjs for the 25 * available methods as well as the available values for this pref. 26 */ 27 const DEBUG_PREF = "browser.newtabpage.activity-stream.asrouter.debugLogLevel"; 28 29 const FXA_USERNAME_PREF = "services.sync.username"; 30 // To observe changes to Selectable Profiles 31 const SELECTABLE_PROFILES_UPDATED = "sps-profiles-updated"; 32 const MESSAGING_PROFILE_ID_PREF = "messaging-system.profile.messagingProfileId"; 33 34 const DEFAULT_STATE = { 35 _initialized: false, 36 _providers: null, 37 _providerPrefBranch: PROVIDER_PREF_BRANCH, 38 _devtoolsEnabled: null, 39 _devtoolsPref: DEVTOOLS_PREF, 40 }; 41 42 const USER_PREFERENCES = { 43 cfrAddons: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", 44 cfrFeatures: 45 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", 46 }; 47 48 XPCOMUtils.defineLazyPreferenceGetter( 49 lazy, 50 "messagingProfileId", 51 MESSAGING_PROFILE_ID_PREF, 52 "" 53 ); 54 55 XPCOMUtils.defineLazyPreferenceGetter( 56 lazy, 57 "disableSingleProfileMessaging", 58 "messaging-system.profile.singleProfileMessaging.disable", 59 false, 60 async prefVal => { 61 if (!prefVal) { 62 return; 63 } 64 // unset the user value of the profile ID pref 65 Services.prefs.clearUserPref(MESSAGING_PROFILE_ID_PREF); 66 await lazy.SelectableProfileService.flushSharedPrefToDatabase( 67 MESSAGING_PROFILE_ID_PREF 68 ); 69 } 70 ); 71 72 // Preferences that influence targeting attributes. When these change we need 73 // to re-evaluate if the message targeting still matches 74 export const TARGETING_PREFERENCES = [FXA_USERNAME_PREF]; 75 76 export const TEST_PROVIDERS = [ 77 { 78 id: "panel_local_testing", 79 type: "local", 80 localProvider: "PanelTestProvider", 81 enabled: true, 82 }, 83 ]; 84 85 export class _ASRouterPreferences { 86 constructor() { 87 Object.assign(this, DEFAULT_STATE); 88 this._callbacks = new Set(); 89 90 ChromeUtils.defineLazyGetter(this, "console", () => { 91 let { ConsoleAPI } = ChromeUtils.importESModule( 92 /* eslint-disable mozilla/use-console-createInstance */ 93 "resource://gre/modules/Console.sys.mjs" 94 ); 95 let consoleOptions = { 96 maxLogLevel: "error", 97 maxLogLevelPref: DEBUG_PREF, 98 prefix: "ASRouter", 99 }; 100 return new ConsoleAPI(consoleOptions); 101 }); 102 } 103 104 _transformPersonalizedCfrScores(value) { 105 let result = {}; 106 try { 107 result = JSON.parse(value); 108 } catch (e) { 109 console.error(e); 110 } 111 return result; 112 } 113 114 _getProviderConfig() { 115 const prefList = Services.prefs.getChildList(this._providerPrefBranch); 116 return prefList.reduce((filtered, pref) => { 117 let value; 118 try { 119 value = JSON.parse(Services.prefs.getStringPref(pref, "")); 120 } catch (e) { 121 console.error( 122 `Could not parse ASRouter preference. Try resetting ${pref} in about:config.` 123 ); 124 } 125 if (value) { 126 filtered.push(value); 127 } 128 return filtered; 129 }, []); 130 } 131 132 get providers() { 133 if (!this._initialized || this._providers === null) { 134 const config = this._getProviderConfig(); 135 const providers = config.map(provider => Object.freeze(provider)); 136 if (this.devtoolsEnabled) { 137 providers.unshift(...TEST_PROVIDERS); 138 } 139 this._providers = Object.freeze(providers); 140 } 141 142 return this._providers; 143 } 144 145 enableOrDisableProvider(id, value) { 146 const providers = this._getProviderConfig(); 147 const config = providers.find(p => p.id === id); 148 if (!config) { 149 console.error( 150 `Cannot set enabled state for '${id}' because the pref ${this._providerPrefBranch}${id} does not exist or is not correctly formatted.` 151 ); 152 return; 153 } 154 155 Services.prefs.setStringPref( 156 this._providerPrefBranch + id, 157 JSON.stringify({ ...config, enabled: value }) 158 ); 159 } 160 161 resetProviderPref() { 162 for (const pref of Services.prefs.getChildList(this._providerPrefBranch)) { 163 Services.prefs.clearUserPref(pref); 164 } 165 for (const id of Object.keys(USER_PREFERENCES)) { 166 Services.prefs.clearUserPref(USER_PREFERENCES[id]); 167 } 168 } 169 170 /** 171 * Bug 1800087 - Migrate the ASRouter message provider prefs' values to the 172 * current format (provider.bucket -> provider.collection). 173 * 174 * TODO (Bug 1800937): Remove migration code after the next watershed release. 175 */ 176 _migrateProviderPrefs() { 177 const prefList = Services.prefs.getChildList(this._providerPrefBranch); 178 for (const pref of prefList) { 179 if (!Services.prefs.prefHasUserValue(pref)) { 180 continue; 181 } 182 try { 183 let value = JSON.parse(Services.prefs.getStringPref(pref, "")); 184 if (value && "bucket" in value && !("collection" in value)) { 185 const { bucket, ...rest } = value; 186 Services.prefs.setStringPref( 187 pref, 188 JSON.stringify({ 189 ...rest, 190 collection: bucket, 191 }) 192 ); 193 } 194 } catch (e) { 195 Services.prefs.clearUserPref(pref); 196 } 197 } 198 } 199 200 async _maybeSetMessagingProfileID() { 201 // If the pref for this mitigation is disabled, skip these checks. 202 if (lazy.disableSingleProfileMessaging) { 203 return; 204 } 205 await lazy.SelectableProfileService.init(); 206 let currentProfileID = 207 lazy.SelectableProfileService.currentProfile?.id?.toString(); 208 // if multiple profiles exist and messagingProfileID isn't set, 209 // set it and copy it around to the rest of the profile group. 210 try { 211 if (!lazy.messagingProfileId && currentProfileID) { 212 Services.prefs.setStringPref( 213 MESSAGING_PROFILE_ID_PREF, 214 currentProfileID 215 ); 216 await lazy.SelectableProfileService.trackPref( 217 MESSAGING_PROFILE_ID_PREF 218 ); 219 } 220 // if multiple profiles exist and messagingProfileID is set, make 221 // sure that a profile with that ID exists. 222 if ( 223 lazy.messagingProfileId && 224 lazy.SelectableProfileService.initialized 225 ) { 226 let messagingProfile = await lazy.SelectableProfileService.getProfile( 227 parseInt(lazy.messagingProfileId, 10) 228 ); 229 if (!messagingProfile) { 230 // the messaging profile got deleted; set the current profile instead 231 Services.prefs.setStringPref( 232 MESSAGING_PROFILE_ID_PREF, 233 currentProfileID 234 ); 235 } 236 } 237 } catch (e) { 238 console.error(`Could not set profile ID: ${e}`); 239 } 240 } 241 242 get devtoolsEnabled() { 243 if (!this._initialized || this._devtoolsEnabled === null) { 244 this._devtoolsEnabled = Services.prefs.getBoolPref( 245 this._devtoolsPref, 246 false 247 ); 248 } 249 return this._devtoolsEnabled; 250 } 251 252 observe(aSubject, aTopic, aPrefName) { 253 if (aPrefName && aPrefName.startsWith(this._providerPrefBranch)) { 254 this._providers = null; 255 } else if (aPrefName === this._devtoolsPref) { 256 this._providers = null; 257 this._devtoolsEnabled = null; 258 } 259 this._callbacks.forEach(cb => cb(aPrefName)); 260 } 261 262 getUserPreference(name) { 263 const prefName = USER_PREFERENCES[name] || name; 264 return Services.prefs.getBoolPref(prefName, true); 265 } 266 267 getAllUserPreferences() { 268 const values = {}; 269 for (const id of Object.keys(USER_PREFERENCES)) { 270 values[id] = this.getUserPreference(id); 271 } 272 return values; 273 } 274 275 setUserPreference(providerId, value) { 276 if (!USER_PREFERENCES[providerId]) { 277 return; 278 } 279 Services.prefs.setBoolPref(USER_PREFERENCES[providerId], value); 280 } 281 282 addListener(callback) { 283 this._callbacks.add(callback); 284 } 285 286 removeListener(callback) { 287 this._callbacks.delete(callback); 288 } 289 290 init() { 291 if (this._initialized) { 292 return; 293 } 294 this._migrateProviderPrefs(); 295 Services.prefs.addObserver(this._providerPrefBranch, this); 296 Services.prefs.addObserver(this._devtoolsPref, this); 297 Services.obs.addObserver( 298 this._maybeSetMessagingProfileID, 299 SELECTABLE_PROFILES_UPDATED 300 ); 301 for (const id of Object.keys(USER_PREFERENCES)) { 302 Services.prefs.addObserver(USER_PREFERENCES[id], this); 303 } 304 for (const targetingPref of TARGETING_PREFERENCES) { 305 Services.prefs.addObserver(targetingPref, this); 306 } 307 this._maybeSetMessagingProfileID(); 308 this._initialized = true; 309 } 310 311 uninit() { 312 if (this._initialized) { 313 Services.prefs.removeObserver(this._providerPrefBranch, this); 314 Services.prefs.removeObserver(this._devtoolsPref, this); 315 Services.obs.removeObserver( 316 this._maybeSetMessagingProfileID, 317 SELECTABLE_PROFILES_UPDATED 318 ); 319 for (const id of Object.keys(USER_PREFERENCES)) { 320 Services.prefs.removeObserver(USER_PREFERENCES[id], this); 321 } 322 for (const targetingPref of TARGETING_PREFERENCES) { 323 Services.prefs.removeObserver(targetingPref, this); 324 } 325 } 326 Object.assign(this, DEFAULT_STATE); 327 this._callbacks.clear(); 328 } 329 } 330 331 export const ASRouterPreferences = new _ASRouterPreferences();