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/. */
8 * Firefox Accounts Profile helper.
10 * This class abstracts interaction with the profile server for an account.
11 * It will handle things like fetching profile data, listening for updates to
12 * the user's profile in open browser tabs, and cacheing/invalidating profile data.
15 var EXPORTED_SYMBOLS = ["FxAccountsProfile"];
17 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
18 const { ON_PROFILE_CHANGE_NOTIFICATION, log } = ChromeUtils.import(
19 "resource://gre/modules/FxAccountsCommon.js"
21 const { fxAccounts } = ChromeUtils.import(
22 "resource://gre/modules/FxAccounts.jsm"
25 ChromeUtils.defineModuleGetter(
27 "FxAccountsProfileClient",
28 "resource://gre/modules/FxAccountsProfileClient.jsm"
31 var FxAccountsProfile = function(options = {}) {
32 this._currentFetchPromise = null;
33 this._cachedAt = 0; // when we saved the cached version.
34 this._isNotifying = false; // are we sending a notification?
35 this.fxai = options.fxai || fxAccounts._internal;
37 options.profileClient ||
38 new FxAccountsProfileClient({
40 serverURL: options.profileServerUrl,
43 // An observer to invalidate our _cachedAt optimization. We use a weak-ref
44 // just incase this.tearDown isn't called in some cases.
45 Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
47 if (options.channel) {
48 this.channel = options.channel;
52 FxAccountsProfile.prototype = {
53 // If we get subsequent requests for a profile within this period, don't bother
54 // making another request to determine if it is fresh or not.
55 PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes
57 observe(subject, topic, data) {
58 // If we get a profile change notification from our webchannel it means
59 // the user has just changed their profile via the web, so we want to
60 // ignore our "freshness threshold"
61 if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
62 log.debug("FxAccountsProfile observed profile change");
70 Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
73 _notifyProfileChange(uid) {
74 this._isNotifying = true;
75 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
76 this._isNotifying = false;
79 // Cache fetched data and send out a notification so that UI can update.
80 _cacheProfile(response) {
81 return this.fxai.withCurrentAccountState(async state => {
82 const profile = response.body;
83 const userData = await state.getUserAccountData();
84 if (profile.uid != userData.uid) {
86 "The fetched profile does not correspond with the current account."
93 await state.updateUserAccountData({ profileCache });
94 if (profile.email != userData.email) {
95 await this.fxai._handleEmailUpdated(profile.email);
97 log.debug("notifying profile changed for user ${uid}", userData);
98 this._notifyProfileChange(userData.uid);
103 async _getProfileCache() {
104 let data = await this.fxai.currentAccountState.getUserAccountData([
107 return data ? data.profileCache : null;
110 async _fetchAndCacheProfileInternal() {
112 const profileCache = await this._getProfileCache();
113 const etag = profileCache ? profileCache.etag : null;
116 response = await this.client.fetchProfile(etag);
118 await this.fxai._handleTokenError(err);
119 // _handleTokenError always re-throws.
120 throw new Error("not reached!");
123 // response may be null if the profile was not modified (same ETag).
127 return await this._cacheProfile(response);
129 this._cachedAt = Date.now();
130 this._currentFetchPromise = null;
134 _fetchAndCacheProfile() {
135 if (!this._currentFetchPromise) {
136 this._currentFetchPromise = this._fetchAndCacheProfileInternal();
138 return this._currentFetchPromise;
141 // Returns cached data right away if available, otherwise returns null - if
142 // it returns null, or if the profile is possibly stale, it attempts to
143 // fetch the latest profile data in the background. After data is fetched a
144 // notification will be sent out if the profile has changed.
146 const profileCache = await this._getProfileCache();
148 // fetch and cache it in the background.
149 this._fetchAndCacheProfile().catch(err => {
150 log.error("Background refresh of initial profile failed", err);
154 if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
155 // Note that _fetchAndCacheProfile isn't returned, so continues
156 // in the background.
157 this._fetchAndCacheProfile().catch(err => {
158 log.error("Background refresh of profile failed", err);
161 log.trace("not checking freshness of profile as it remains recent");
163 return profileCache.profile;
166 // Get the user's profile data, fetching from the network if necessary.
167 // Most callers should instead use `getProfile()`; this methods exists to support
168 // callers who need to await the underlying network request.
169 async ensureProfile({ staleOk = false, forceFresh = false } = {}) {
170 if (staleOk && forceFresh) {
171 throw new Error("contradictory options specified");
173 const profileCache = await this._getProfileCache();
177 (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD &&
180 const profile = await this._fetchAndCacheProfile().catch(err => {
181 log.error("Background refresh of profile failed", err);
187 log.trace("not checking freshness of profile as it remains recent");
188 return profileCache ? profileCache.profile : null;
191 QueryInterface: ChromeUtils.generateQI([
193 "nsISupportsWeakReference",