diff options
Diffstat (limited to 'services/fxaccounts/FxAccountsProfile.sys.mjs')
-rw-r--r-- | services/fxaccounts/FxAccountsProfile.sys.mjs | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/services/fxaccounts/FxAccountsProfile.sys.mjs b/services/fxaccounts/FxAccountsProfile.sys.mjs new file mode 100644 index 0000000000..7127376ad3 --- /dev/null +++ b/services/fxaccounts/FxAccountsProfile.sys.mjs @@ -0,0 +1,191 @@ +/* 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/. */ + +/** + * Firefox Accounts Profile helper. + * + * This class abstracts interaction with the profile server for an account. + * It will handle things like fetching profile data, listening for updates to + * the user's profile in open browser tabs, and cacheing/invalidating profile data. + */ + +const { ON_PROFILE_CHANGE_NOTIFICATION, log } = ChromeUtils.import( + "resource://gre/modules/FxAccountsCommon.js" +); +import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs"; + +const fxAccounts = getFxAccountsSingleton(); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FxAccountsProfileClient: + "resource://gre/modules/FxAccountsProfileClient.sys.mjs", +}); + +export var FxAccountsProfile = function (options = {}) { + this._currentFetchPromise = null; + this._cachedAt = 0; // when we saved the cached version. + this._isNotifying = false; // are we sending a notification? + this.fxai = options.fxai || fxAccounts._internal; + this.client = + options.profileClient || + new lazy.FxAccountsProfileClient({ + fxai: this.fxai, + serverURL: options.profileServerUrl, + }); + + // An observer to invalidate our _cachedAt optimization. We use a weak-ref + // just incase this.tearDown isn't called in some cases. + Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true); + // for testing + if (options.channel) { + this.channel = options.channel; + } +}; + +FxAccountsProfile.prototype = { + // If we get subsequent requests for a profile within this period, don't bother + // making another request to determine if it is fresh or not. + PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes + + observe(subject, topic, data) { + // If we get a profile change notification from our webchannel it means + // the user has just changed their profile via the web, so we want to + // ignore our "freshness threshold" + if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) { + log.debug("FxAccountsProfile observed profile change"); + this._cachedAt = 0; + } + }, + + tearDown() { + this.fxai = null; + this.client = null; + Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION); + }, + + _notifyProfileChange(uid) { + this._isNotifying = true; + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid); + this._isNotifying = false; + }, + + // Cache fetched data and send out a notification so that UI can update. + _cacheProfile(response) { + return this.fxai.withCurrentAccountState(async state => { + const profile = response.body; + const userData = await state.getUserAccountData(); + if (profile.uid != userData.uid) { + throw new Error( + "The fetched profile does not correspond with the current account." + ); + } + let profileCache = { + profile, + etag: response.etag, + }; + await state.updateUserAccountData({ profileCache }); + if (profile.email != userData.email) { + await this.fxai._handleEmailUpdated(profile.email); + } + log.debug("notifying profile changed for user ${uid}", userData); + this._notifyProfileChange(userData.uid); + return profile; + }); + }, + + async _getProfileCache() { + let data = await this.fxai.currentAccountState.getUserAccountData([ + "profileCache", + ]); + return data ? data.profileCache : null; + }, + + async _fetchAndCacheProfileInternal() { + try { + const profileCache = await this._getProfileCache(); + const etag = profileCache ? profileCache.etag : null; + let response; + try { + response = await this.client.fetchProfile(etag); + } catch (err) { + await this.fxai._handleTokenError(err); + // _handleTokenError always re-throws. + throw new Error("not reached!"); + } + + // response may be null if the profile was not modified (same ETag). + if (!response) { + return null; + } + return await this._cacheProfile(response); + } finally { + this._cachedAt = Date.now(); + this._currentFetchPromise = null; + } + }, + + _fetchAndCacheProfile() { + if (!this._currentFetchPromise) { + this._currentFetchPromise = this._fetchAndCacheProfileInternal(); + } + return this._currentFetchPromise; + }, + + // Returns cached data right away if available, otherwise returns null - if + // it returns null, or if the profile is possibly stale, it attempts to + // fetch the latest profile data in the background. After data is fetched a + // notification will be sent out if the profile has changed. + async getProfile() { + const profileCache = await this._getProfileCache(); + if (!profileCache) { + // fetch and cache it in the background. + this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of initial profile failed", err); + }); + return null; + } + if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) { + // Note that _fetchAndCacheProfile isn't returned, so continues + // in the background. + this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of profile failed", err); + }); + } else { + log.trace("not checking freshness of profile as it remains recent"); + } + return profileCache.profile; + }, + + // Get the user's profile data, fetching from the network if necessary. + // Most callers should instead use `getProfile()`; this methods exists to support + // callers who need to await the underlying network request. + async ensureProfile({ staleOk = false, forceFresh = false } = {}) { + if (staleOk && forceFresh) { + throw new Error("contradictory options specified"); + } + const profileCache = await this._getProfileCache(); + if ( + forceFresh || + !profileCache || + (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD && + !staleOk) + ) { + const profile = await this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of profile failed", err); + }); + if (profile) { + return profile; + } + } + log.trace("not checking freshness of profile as it remains recent"); + return profileCache ? profileCache.profile : null; + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), +}; |