summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/FxAccountsProfile.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/FxAccountsProfile.jsm')
-rw-r--r--services/fxaccounts/FxAccountsProfile.jsm191
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,
+ ]),
+};