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/. */
6 * Firefox Accounts Profile helper.
8 * This class abstracts interaction with the profile server for an account.
9 * It will handle things like fetching profile data, listening for updates to
10 * the user's profile in open browser tabs, and cacheing/invalidating profile data.
13 const { ON_PROFILE_CHANGE_NOTIFICATION, log } = ChromeUtils.import(
14 "resource://gre/modules/FxAccountsCommon.js"
16 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
18 const fxAccounts = getFxAccountsSingleton();
22 ChromeUtils.defineESModuleGetters(lazy, {
23 FxAccountsProfileClient:
24 "resource://gre/modules/FxAccountsProfileClient.sys.mjs",
27 export var FxAccountsProfile = function(options = {}) {
28 this._currentFetchPromise = null;
29 this._cachedAt = 0; // when we saved the cached version.
30 this._isNotifying = false; // are we sending a notification?
31 this.fxai = options.fxai || fxAccounts._internal;
33 options.profileClient ||
34 new lazy.FxAccountsProfileClient({
36 serverURL: options.profileServerUrl,
39 // An observer to invalidate our _cachedAt optimization. We use a weak-ref
40 // just incase this.tearDown isn't called in some cases.
41 Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
43 if (options.channel) {
44 this.channel = options.channel;
48 FxAccountsProfile.prototype = {
49 // If we get subsequent requests for a profile within this period, don't bother
50 // making another request to determine if it is fresh or not.
51 PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes
53 observe(subject, topic, data) {
54 // If we get a profile change notification from our webchannel it means
55 // the user has just changed their profile via the web, so we want to
56 // ignore our "freshness threshold"
57 if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
58 log.debug("FxAccountsProfile observed profile change");
66 Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
69 _notifyProfileChange(uid) {
70 this._isNotifying = true;
71 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
72 this._isNotifying = false;
75 // Cache fetched data and send out a notification so that UI can update.
76 _cacheProfile(response) {
77 return this.fxai.withCurrentAccountState(async state => {
78 const profile = response.body;
79 const userData = await state.getUserAccountData();
80 if (profile.uid != userData.uid) {
82 "The fetched profile does not correspond with the current account."
89 await state.updateUserAccountData({ profileCache });
90 if (profile.email != userData.email) {
91 await this.fxai._handleEmailUpdated(profile.email);
93 log.debug("notifying profile changed for user ${uid}", userData);
94 this._notifyProfileChange(userData.uid);
99 async _getProfileCache() {
100 let data = await this.fxai.currentAccountState.getUserAccountData([
103 return data ? data.profileCache : null;
106 async _fetchAndCacheProfileInternal() {
108 const profileCache = await this._getProfileCache();
109 const etag = profileCache ? profileCache.etag : null;
112 response = await this.client.fetchProfile(etag);
114 await this.fxai._handleTokenError(err);
115 // _handleTokenError always re-throws.
116 throw new Error("not reached!");
119 // response may be null if the profile was not modified (same ETag).
123 return await this._cacheProfile(response);
125 this._cachedAt = Date.now();
126 this._currentFetchPromise = null;
130 _fetchAndCacheProfile() {
131 if (!this._currentFetchPromise) {
132 this._currentFetchPromise = this._fetchAndCacheProfileInternal();
134 return this._currentFetchPromise;
137 // Returns cached data right away if available, otherwise returns null - if
138 // it returns null, or if the profile is possibly stale, it attempts to
139 // fetch the latest profile data in the background. After data is fetched a
140 // notification will be sent out if the profile has changed.
142 const profileCache = await this._getProfileCache();
144 // fetch and cache it in the background.
145 this._fetchAndCacheProfile().catch(err => {
146 log.error("Background refresh of initial profile failed", err);
150 if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
151 // Note that _fetchAndCacheProfile isn't returned, so continues
152 // in the background.
153 this._fetchAndCacheProfile().catch(err => {
154 log.error("Background refresh of profile failed", err);
157 log.trace("not checking freshness of profile as it remains recent");
159 return profileCache.profile;
162 // Get the user's profile data, fetching from the network if necessary.
163 // Most callers should instead use `getProfile()`; this methods exists to support
164 // callers who need to await the underlying network request.
165 async ensureProfile({ staleOk = false, forceFresh = false } = {}) {
166 if (staleOk && forceFresh) {
167 throw new Error("contradictory options specified");
169 const profileCache = await this._getProfileCache();
173 (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD &&
176 const profile = await this._fetchAndCacheProfile().catch(err => {
177 log.error("Background refresh of profile failed", err);
183 log.trace("not checking freshness of profile as it remains recent");
184 return profileCache ? profileCache.profile : null;
187 QueryInterface: ChromeUtils.generateQI([
189 "nsISupportsWeakReference",