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.
14 ON_PROFILE_CHANGE_NOTIFICATION,
16 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
18 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
20 const fxAccounts = getFxAccountsSingleton();
24 ChromeUtils.defineESModuleGetters(lazy, {
25 FxAccountsProfileClient:
26 "resource://gre/modules/FxAccountsProfileClient.sys.mjs",
29 export var FxAccountsProfile = function (options = {}) {
30 this._currentFetchPromise = null;
31 this._cachedAt = 0; // when we saved the cached version.
32 this._isNotifying = false; // are we sending a notification?
33 this.fxai = options.fxai || fxAccounts._internal;
35 options.profileClient ||
36 new lazy.FxAccountsProfileClient({
38 serverURL: options.profileServerUrl,
41 // An observer to invalidate our _cachedAt optimization. We use a weak-ref
42 // just incase this.tearDown isn't called in some cases.
43 Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
45 if (options.channel) {
46 this.channel = options.channel;
50 FxAccountsProfile.prototype = {
51 // If we get subsequent requests for a profile within this period, don't bother
52 // making another request to determine if it is fresh or not.
53 PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes
55 observe(subject, topic) {
56 // If we get a profile change notification from our webchannel it means
57 // the user has just changed their profile via the web, so we want to
58 // ignore our "freshness threshold"
59 if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
60 log.debug("FxAccountsProfile observed profile change");
68 Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
71 _notifyProfileChange(uid) {
72 this._isNotifying = true;
73 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
74 this._isNotifying = false;
77 // Cache fetched data and send out a notification so that UI can update.
78 _cacheProfile(response) {
79 return this.fxai.withCurrentAccountState(async state => {
80 const profile = response.body;
81 const userData = await state.getUserAccountData();
82 if (profile.uid != userData.uid) {
84 "The fetched profile does not correspond with the current account."
91 await state.updateUserAccountData({ profileCache });
92 if (profile.email != userData.email) {
93 await this.fxai._handleEmailUpdated(profile.email);
95 log.debug("notifying profile changed for user ${uid}", userData);
96 this._notifyProfileChange(userData.uid);
101 async _getProfileCache() {
102 let data = await this.fxai.currentAccountState.getUserAccountData([
105 return data ? data.profileCache : null;
108 async _fetchAndCacheProfileInternal() {
110 const profileCache = await this._getProfileCache();
111 const etag = profileCache ? profileCache.etag : null;
114 response = await this.client.fetchProfile(etag);
116 await this.fxai._handleTokenError(err);
117 // _handleTokenError always re-throws.
118 throw new Error("not reached!");
121 // response may be null if the profile was not modified (same ETag).
125 return await this._cacheProfile(response);
127 this._cachedAt = Date.now();
128 this._currentFetchPromise = null;
132 _fetchAndCacheProfile() {
133 if (!this._currentFetchPromise) {
134 this._currentFetchPromise = this._fetchAndCacheProfileInternal();
136 return this._currentFetchPromise;
139 // Returns cached data right away if available, otherwise returns null - if
140 // it returns null, or if the profile is possibly stale, it attempts to
141 // fetch the latest profile data in the background. After data is fetched a
142 // notification will be sent out if the profile has changed.
144 const profileCache = await this._getProfileCache();
146 // fetch and cache it in the background.
147 this._fetchAndCacheProfile().catch(err => {
148 log.error("Background refresh of initial profile failed", err);
152 if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
153 // Note that _fetchAndCacheProfile isn't returned, so continues
154 // in the background.
155 this._fetchAndCacheProfile().catch(err => {
156 log.error("Background refresh of profile failed", err);
159 log.trace("not checking freshness of profile as it remains recent");
161 return profileCache.profile;
164 // Get the user's profile data, fetching from the network if necessary.
165 // Most callers should instead use `getProfile()`; this methods exists to support
166 // callers who need to await the underlying network request.
167 async ensureProfile({ staleOk = false, forceFresh = false } = {}) {
168 if (staleOk && forceFresh) {
169 throw new Error("contradictory options specified");
171 const profileCache = await this._getProfileCache();
175 (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD &&
178 const profile = await this._fetchAndCacheProfile().catch(err => {
179 log.error("Background refresh of profile failed", err);
185 log.trace("not checking freshness of profile as it remains recent");
186 return profileCache ? profileCache.profile : null;
189 QueryInterface: ChromeUtils.generateQI([
191 "nsISupportsWeakReference",