/* 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"; /** * 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. */ var EXPORTED_SYMBOLS = ["FxAccountsProfile"]; const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { ON_PROFILE_CHANGE_NOTIFICATION, log } = ChromeUtils.import( "resource://gre/modules/FxAccountsCommon.js" ); const { fxAccounts } = ChromeUtils.import( "resource://gre/modules/FxAccounts.jsm" ); ChromeUtils.defineModuleGetter( this, "FxAccountsProfileClient", "resource://gre/modules/FxAccountsProfileClient.jsm" ); 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 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", ]), };