diff options
Diffstat (limited to 'services/fxaccounts/FxAccountsProfile.jsm')
-rw-r--r-- | services/fxaccounts/FxAccountsProfile.jsm | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/services/fxaccounts/FxAccountsProfile.jsm b/services/fxaccounts/FxAccountsProfile.jsm new file mode 100644 index 000000000..b63cd64c1 --- /dev/null +++ b/services/fxaccounts/FxAccountsProfile.jsm @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Firefox Accounts Profile helper. + * + * This class abstracts interaction with the profile server for an account. + * It will handle things like fetching profile data, listening for updates to + * the user's profile in open browser tabs, and cacheing/invalidating profile data. + */ + +this.EXPORTED_SYMBOLS = ["FxAccountsProfile"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +Cu.import("resource://gre/modules/FxAccounts.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient", + "resource://gre/modules/FxAccountsProfileClient.jsm"); + +// Based off of deepEqual from Assert.jsm +function deepEqual(actual, expected) { + if (actual === expected) { + return true; + } else if (typeof actual != "object" && typeof expected != "object") { + return actual == expected; + } else { + return objEquiv(actual, expected); + } +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { + return false; + } + if (a.prototype !== b.prototype) { + return false; + } + let ka, kb, key, i; + try { + ka = Object.keys(a); + kb = Object.keys(b); + } catch (e) { + return false; + } + if (ka.length != kb.length) { + return false; + } + ka.sort(); + kb.sort(); + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +function hasChanged(oldData, newData) { + return !deepEqual(oldData, newData); +} + +this.FxAccountsProfile = function (options = {}) { + this._cachedProfile = null; + this._cachedAt = 0; // when we saved the cached version. + this._currentFetchPromise = null; + this._isNotifying = false; // are we sending a notification? + this.fxa = options.fxa || fxAccounts; + this.client = options.profileClient || new FxAccountsProfileClient({ + fxa: this.fxa, + serverURL: options.profileServerUrl, + }); + + // An observer to invalidate our _cachedAt optimization. We use a weak-ref + // just incase this.tearDown isn't called in some cases. + Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true); + // for testing + if (options.channel) { + this.channel = options.channel; + } +} + +this.FxAccountsProfile.prototype = { + // If we get subsequent requests for a profile within this period, don't bother + // making another request to determine if it is fresh or not. + PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes + + observe(subject, topic, data) { + // If we get a profile change notification from our webchannel it means + // the user has just changed their profile via the web, so we want to + // ignore our "freshness threshold" + if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) { + log.debug("FxAccountsProfile observed profile change"); + this._cachedAt = 0; + } + }, + + tearDown: function () { + this.fxa = null; + this.client = null; + this._cachedProfile = null; + Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION); + }, + + _getCachedProfile: function () { + // The cached profile will end up back in the generic accountData + // once bug 1157529 is fixed. + return Promise.resolve(this._cachedProfile); + }, + + _notifyProfileChange: function (uid) { + this._isNotifying = true; + Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid); + this._isNotifying = false; + }, + + // Cache fetched data if it is different from what's in the cache. + // Send out a notification if it has changed so that UI can update. + _cacheProfile: function (profileData) { + if (!hasChanged(this._cachedProfile, profileData)) { + log.debug("fetched profile matches cached copy"); + return Promise.resolve(null); // indicates no change (but only tests care) + } + this._cachedProfile = profileData; + this._cachedAt = Date.now(); + return this.fxa.getSignedInUser() + .then(userData => { + log.debug("notifying profile changed for user ${uid}", userData); + this._notifyProfileChange(userData.uid); + return profileData; + }); + }, + + _fetchAndCacheProfile: function () { + if (!this._currentFetchPromise) { + this._currentFetchPromise = this.client.fetchProfile().then(profile => { + return this._cacheProfile(profile).then(() => { + return profile; + }); + }).then(profile => { + this._currentFetchPromise = null; + return profile; + }, err => { + this._currentFetchPromise = null; + throw err; + }); + } + return this._currentFetchPromise + }, + + // Returns cached data right away if available, then fetches the latest profile + // data in the background. After data is fetched a notification will be sent + // out if the profile has changed. + getProfile: function () { + return this._getCachedProfile() + .then(cachedProfile => { + if (cachedProfile) { + if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) { + // Note that _fetchAndCacheProfile isn't returned, so continues + // in the background. + this._fetchAndCacheProfile().catch(err => { + log.error("Background refresh of profile failed", err); + }); + } else { + log.trace("not checking freshness of profile as it remains recent"); + } + return cachedProfile; + } + return this._fetchAndCacheProfile(); + }) + .then(profile => { + return profile; + }); + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]), +}; |