tor-browser

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

UIState.sys.mjs (8646B)


      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 * @typedef {object} UIState
      7 * @property {string} status The Sync/FxA status, see STATUS_* constants.
      8 * @property {string} [email] The FxA email configured to log-in with Sync.
      9 * @property {string} [displayName] The user's FxA display name.
     10 * @property {string} [avatarURL] The user's FxA avatar URL.
     11 * @property {Date} [lastSync] The last sync time.
     12 * @property {boolean} [syncing] Whether or not we are currently syncing.
     13 * @property {boolean} [hasSyncKeys] Whether the user has sync keys available.
     14 */
     15 
     16 import { SCOPE_APP_SYNC } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     17 
     18 const lazy = {};
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  LOGIN_FAILED_LOGIN_REJECTED: "resource://services-sync/constants.sys.mjs",
     21  Weave: "resource://services-sync/main.sys.mjs",
     22 });
     23 
     24 const TOPICS = [
     25  "weave:connected",
     26  "weave:service:login:got-hashed-id",
     27  "weave:service:login:error",
     28  "weave:service:ready",
     29  "weave:service:sync:start",
     30  "weave:service:sync:finish",
     31  "weave:service:sync:error",
     32  "weave:service:start-over:finish",
     33  "fxaccounts:onverified",
     34  "fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
     35  "fxaccounts:onlogout",
     36  "fxaccounts:profilechange",
     37  "fxaccounts:statechange",
     38 ];
     39 
     40 const ON_UPDATE = "sync-ui-state:update";
     41 
     42 const STATUS_NOT_CONFIGURED = "not_configured";
     43 const STATUS_LOGIN_FAILED = "login_failed";
     44 const STATUS_NOT_VERIFIED = "not_verified";
     45 const STATUS_SIGNED_IN = "signed_in";
     46 
     47 const DEFAULT_STATE = {
     48  status: STATUS_NOT_CONFIGURED,
     49 };
     50 
     51 const UIStateInternal = {
     52  _initialized: false,
     53  _state: null,
     54 
     55  // We keep _syncing out of the state object because we can only track it
     56  // using sync events and we can't determine it at any point in time.
     57  _syncing: false,
     58 
     59  get state() {
     60    if (!this._state) {
     61      return DEFAULT_STATE;
     62    }
     63    return Object.assign({}, this._state, { syncing: this._syncing });
     64  },
     65 
     66  isReady() {
     67    if (!this._initialized) {
     68      this.init();
     69      return false;
     70    }
     71    return true;
     72  },
     73 
     74  init() {
     75    this._initialized = true;
     76    // Because the FxA toolbar is usually visible, this module gets loaded at
     77    // browser startup, and we want to avoid pulling in all of FxA or Sync at
     78    // that time, so we refresh the state after the browser has settled.
     79    Services.tm.idleDispatchToMainThread(() => {
     80      this.refreshState().catch(e => {
     81        console.error(e);
     82      });
     83    }, 2000);
     84  },
     85 
     86  // Used for testing.
     87  reset() {
     88    this._state = null;
     89    this._syncing = false;
     90    this._initialized = false;
     91  },
     92 
     93  observe(subject, topic) {
     94    switch (topic) {
     95      case "weave:service:sync:start":
     96        this.toggleSyncActivity(true);
     97        break;
     98      case "weave:service:sync:finish":
     99      case "weave:service:sync:error":
    100        this.toggleSyncActivity(false);
    101        break;
    102      default:
    103        this.refreshState().catch(e => {
    104          console.error(e);
    105        });
    106        break;
    107    }
    108  },
    109 
    110  // Builds a new state from scratch.
    111  async refreshState() {
    112    const newState = {};
    113    await this._refreshFxAState(newState);
    114    // Optimize the "not signed in" case to avoid refreshing twice just after
    115    // startup - if there's currently no _state, and we still aren't configured,
    116    // just early exit.
    117    if (this._state == null && newState.status == DEFAULT_STATE.status) {
    118      return this.state;
    119    }
    120    if (newState.syncEnabled) {
    121      this._setLastSyncTime(newState); // We want this in case we change accounts.
    122    }
    123    this._state = newState;
    124 
    125    this.notifyStateUpdated();
    126    return this.state;
    127  },
    128 
    129  // Update the current state with the last sync time/currently syncing status.
    130  toggleSyncActivity(syncing) {
    131    this._syncing = syncing;
    132    this._setLastSyncTime(this._state);
    133 
    134    this.notifyStateUpdated();
    135  },
    136 
    137  notifyStateUpdated() {
    138    Services.obs.notifyObservers(null, ON_UPDATE);
    139  },
    140 
    141  async _refreshFxAState(newState) {
    142    let userData = await this._getUserData();
    143    await this._populateWithUserData(newState, userData);
    144  },
    145 
    146  async _populateWithUserData(state, userData) {
    147    let status;
    148    let syncUserName = Services.prefs.getStringPref(
    149      "services.sync.username",
    150      ""
    151    );
    152    if (!userData) {
    153      // If Sync thinks it is configured but there's no FxA user, then we
    154      // want to enter the "login failed" state so the user can get
    155      // reconfigured.
    156      if (syncUserName) {
    157        state.email = syncUserName;
    158        status = STATUS_LOGIN_FAILED;
    159      } else {
    160        // everyone agrees nothing is configured.
    161        status = STATUS_NOT_CONFIGURED;
    162      }
    163    } else {
    164      let loginFailed = await this._loginFailed();
    165      if (loginFailed) {
    166        status = STATUS_LOGIN_FAILED;
    167      } else if (!userData.verified) {
    168        status = STATUS_NOT_VERIFIED;
    169      } else {
    170        status = STATUS_SIGNED_IN;
    171      }
    172      state.uid = userData.uid;
    173      state.email = userData.email;
    174      state.displayName = userData.displayName;
    175      // for better or worse, this module renames these attribues.
    176      state.avatarURL = userData.avatar;
    177      state.avatarIsDefault = userData.avatarDefault;
    178      state.syncEnabled = !!syncUserName;
    179      state.hasSyncKeys =
    180        await this.fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
    181    }
    182    state.status = status;
    183  },
    184 
    185  async _getUserData() {
    186    try {
    187      return await this.fxAccounts.getSignedInUser();
    188    } catch (e) {
    189      // This is most likely in tests, where we quickly log users in and out.
    190      // The most likely scenario is a user logged out, so reflect that.
    191      // Bug 995134 calls for better errors so we could retry if we were
    192      // sure this was the failure reason.
    193      console.error("Error updating FxA account info:", e);
    194      return null;
    195    }
    196  },
    197 
    198  _setLastSyncTime(state) {
    199    if (state?.status == UIState.STATUS_SIGNED_IN) {
    200      const lastSync = Services.prefs.getStringPref(
    201        "services.sync.lastSync",
    202        null
    203      );
    204      state.lastSync = lastSync ? new Date(lastSync) : null;
    205    }
    206  },
    207 
    208  async _loginFailed() {
    209    // First ask FxA if it thinks the user needs re-authentication. In practice,
    210    // this check is probably canonical (ie, we probably don't really need
    211    // the check below at all as we drop local session info on the first sign
    212    // of a problem) - but we keep it for now to keep the risk down.
    213    let hasLocalSession = await this.fxAccounts.hasLocalSession();
    214    if (!hasLocalSession) {
    215      return true;
    216    }
    217 
    218    // Referencing Weave.Service will implicitly initialize sync, and we don't
    219    // want to force that - so first check if it is ready.
    220    let service = Cc["@mozilla.org/weave/service;1"].getService(
    221      Ci.nsISupports
    222    ).wrappedJSObject;
    223    if (!service.ready) {
    224      return false;
    225    }
    226    // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
    227    // All other login failures are assumed to be transient and should go
    228    // away by themselves, so aren't reflected here.
    229    return lazy.Weave.Status.login == lazy.LOGIN_FAILED_LOGIN_REJECTED;
    230  },
    231 
    232  set fxAccounts(mockFxAccounts) {
    233    delete this.fxAccounts;
    234    this.fxAccounts = mockFxAccounts;
    235  },
    236 };
    237 
    238 ChromeUtils.defineLazyGetter(UIStateInternal, "fxAccounts", () => {
    239  return ChromeUtils.importESModule(
    240    "resource://gre/modules/FxAccounts.sys.mjs"
    241  ).getFxAccountsSingleton();
    242 });
    243 
    244 for (let topic of TOPICS) {
    245  Services.obs.addObserver(UIStateInternal, topic);
    246 }
    247 
    248 export var UIState = {
    249  _internal: UIStateInternal,
    250 
    251  ON_UPDATE,
    252 
    253  STATUS_NOT_CONFIGURED,
    254  STATUS_LOGIN_FAILED,
    255  STATUS_NOT_VERIFIED,
    256  STATUS_SIGNED_IN,
    257 
    258  /**
    259   * Returns true if the module has been initialized and the state set.
    260   * If not, return false and trigger an init in the background.
    261   */
    262  isReady() {
    263    return this._internal.isReady();
    264  },
    265 
    266  /**
    267   * @returns {UIState} The current Sync/FxA UI State.
    268   */
    269  get() {
    270    return this._internal.state;
    271  },
    272 
    273  /**
    274   * Refresh the state. Used for testing, don't call this directly since
    275   * UIState already listens to Sync/FxA notifications to determine if the state
    276   * needs to be refreshed. ON_UPDATE will be fired once the state is refreshed.
    277   *
    278   * @returns {Promise<UIState>} Resolved once the state is refreshed.
    279   */
    280  refresh() {
    281    return this._internal.refreshState();
    282  },
    283 
    284  /**
    285   * Reset the state of the whole module. Used for testing.
    286   */
    287  reset() {
    288    this._internal.reset();
    289  },
    290 };