tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };