FxAccountsProfile.sys.mjs (6512B)
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/. */ 4 5 /** 6 * Firefox Accounts Profile helper. 7 * 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. 11 */ 12 13 import { 14 ON_PROFILE_CHANGE_NOTIFICATION, 15 log, 16 } from "resource://gre/modules/FxAccountsCommon.sys.mjs"; 17 18 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs"; 19 20 const fxAccounts = getFxAccountsSingleton(); 21 22 const lazy = {}; 23 24 ChromeUtils.defineESModuleGetters(lazy, { 25 FxAccountsProfileClient: 26 "resource://gre/modules/FxAccountsProfileClient.sys.mjs", 27 }); 28 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; 34 this.client = 35 options.profileClient || 36 new lazy.FxAccountsProfileClient({ 37 fxai: this.fxai, 38 serverURL: options.profileServerUrl, 39 }); 40 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); 44 // for testing 45 if (options.channel) { 46 this.channel = options.channel; 47 } 48 }; 49 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 54 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"); 61 this._cachedAt = 0; 62 } 63 }, 64 65 tearDown() { 66 this.fxai = null; 67 this.client = null; 68 Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION); 69 }, 70 71 _notifyProfileChange(uid) { 72 this._isNotifying = true; 73 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid); 74 this._isNotifying = false; 75 }, 76 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) { 83 throw new Error( 84 "The fetched profile does not correspond with the current account." 85 ); 86 } 87 let profileCache = { 88 profile, 89 etag: response.etag, 90 }; 91 await state.updateUserAccountData({ profileCache }); 92 if (profile.email != userData.email) { 93 await this.fxai._handleEmailUpdated(profile.email); 94 } 95 log.debug("notifying profile changed for user ${uid}", userData); 96 this._notifyProfileChange(userData.uid); 97 return profile; 98 }); 99 }, 100 101 async _getProfileCache() { 102 let data = await this.fxai.currentAccountState.getUserAccountData([ 103 "profileCache", 104 ]); 105 return data ? data.profileCache : null; 106 }, 107 108 async _fetchAndCacheProfileInternal() { 109 try { 110 const profileCache = await this._getProfileCache(); 111 const etag = profileCache ? profileCache.etag : null; 112 let response; 113 try { 114 response = await this.client.fetchProfile(etag); 115 } catch (err) { 116 await this.fxai._handleTokenError(err); 117 // _handleTokenError always re-throws. 118 throw new Error("not reached!"); 119 } 120 121 // response may be null if the profile was not modified (same ETag). 122 if (!response) { 123 return null; 124 } 125 return await this._cacheProfile(response); 126 } finally { 127 this._cachedAt = Date.now(); 128 this._currentFetchPromise = null; 129 } 130 }, 131 132 _fetchAndCacheProfile() { 133 if (!this._currentFetchPromise) { 134 this._currentFetchPromise = this._fetchAndCacheProfileInternal(); 135 } 136 return this._currentFetchPromise; 137 }, 138 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. 143 async getProfile() { 144 const profileCache = await this._getProfileCache(); 145 if (!profileCache) { 146 // fetch and cache it in the background. 147 this._fetchAndCacheProfile().catch(err => { 148 log.error("Background refresh of initial profile failed", err); 149 }); 150 return null; 151 } 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); 157 }); 158 } else { 159 log.trace("not checking freshness of profile as it remains recent"); 160 } 161 return profileCache.profile; 162 }, 163 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"); 170 } 171 const profileCache = await this._getProfileCache(); 172 if ( 173 forceFresh || 174 !profileCache || 175 (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD && 176 !staleOk) 177 ) { 178 const profile = await this._fetchAndCacheProfile().catch(err => { 179 log.error("Background refresh of profile failed", err); 180 }); 181 if (profile) { 182 return profile; 183 } 184 } 185 log.trace("not checking freshness of profile as it remains recent"); 186 return profileCache ? profileCache.profile : null; 187 }, 188 189 QueryInterface: ChromeUtils.generateQI([ 190 "nsIObserver", 191 "nsISupportsWeakReference", 192 ]), 193 };