Bug 1688832: part 5) Add `static` `AccessibleCaretManager::GetSelection`, `::GetFrame...
[gecko.git] / services / fxaccounts / FxAccountsProfile.jsm
blobd4801d404b01ea11ef5ef9b577be71f7765cb2fe
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/. */
5 "use strict";
7 /**
8  * Firefox Accounts Profile helper.
9  *
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.
13  */
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(
26   this,
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;
36   this.client =
37     options.profileClient ||
38     new FxAccountsProfileClient({
39       fxai: this.fxai,
40       serverURL: options.profileServerUrl,
41     });
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);
46   // for testing
47   if (options.channel) {
48     this.channel = options.channel;
49   }
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");
63       this._cachedAt = 0;
64     }
65   },
67   tearDown() {
68     this.fxai = null;
69     this.client = null;
70     Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
71   },
73   _notifyProfileChange(uid) {
74     this._isNotifying = true;
75     Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
76     this._isNotifying = false;
77   },
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) {
85         throw new Error(
86           "The fetched profile does not correspond with the current account."
87         );
88       }
89       let profileCache = {
90         profile,
91         etag: response.etag,
92       };
93       await state.updateUserAccountData({ profileCache });
94       if (profile.email != userData.email) {
95         await this.fxai._handleEmailUpdated(profile.email);
96       }
97       log.debug("notifying profile changed for user ${uid}", userData);
98       this._notifyProfileChange(userData.uid);
99       return profile;
100     });
101   },
103   async _getProfileCache() {
104     let data = await this.fxai.currentAccountState.getUserAccountData([
105       "profileCache",
106     ]);
107     return data ? data.profileCache : null;
108   },
110   async _fetchAndCacheProfileInternal() {
111     try {
112       const profileCache = await this._getProfileCache();
113       const etag = profileCache ? profileCache.etag : null;
114       let response;
115       try {
116         response = await this.client.fetchProfile(etag);
117       } catch (err) {
118         await this.fxai._handleTokenError(err);
119         // _handleTokenError always re-throws.
120         throw new Error("not reached!");
121       }
123       // response may be null if the profile was not modified (same ETag).
124       if (!response) {
125         return null;
126       }
127       return await this._cacheProfile(response);
128     } finally {
129       this._cachedAt = Date.now();
130       this._currentFetchPromise = null;
131     }
132   },
134   _fetchAndCacheProfile() {
135     if (!this._currentFetchPromise) {
136       this._currentFetchPromise = this._fetchAndCacheProfileInternal();
137     }
138     return this._currentFetchPromise;
139   },
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.
145   async getProfile() {
146     const profileCache = await this._getProfileCache();
147     if (!profileCache) {
148       // fetch and cache it in the background.
149       this._fetchAndCacheProfile().catch(err => {
150         log.error("Background refresh of initial profile failed", err);
151       });
152       return null;
153     }
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);
159       });
160     } else {
161       log.trace("not checking freshness of profile as it remains recent");
162     }
163     return profileCache.profile;
164   },
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");
172     }
173     const profileCache = await this._getProfileCache();
174     if (
175       forceFresh ||
176       !profileCache ||
177       (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD &&
178         !staleOk)
179     ) {
180       const profile = await this._fetchAndCacheProfile().catch(err => {
181         log.error("Background refresh of profile failed", err);
182       });
183       if (profile) {
184         return profile;
185       }
186     }
187     log.trace("not checking freshness of profile as it remains recent");
188     return profileCache ? profileCache.profile : null;
189   },
191   QueryInterface: ChromeUtils.generateQI([
192     "nsIObserver",
193     "nsISupportsWeakReference",
194   ]),