Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  FxAccountsWebChannel.sys.mjs   Sprache: unbekannt

 
/* 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/. */

/**
 * Firefox Accounts Web Channel.
 *
 * Uses the WebChannel component to receive messages
 * about account state changes.
 */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import {
  COMMAND_PROFILE_CHANGE,
  COMMAND_LOGIN,
  COMMAND_LOGOUT,
  COMMAND_OAUTH,
  COMMAND_DELETE,
  COMMAND_CAN_LINK_ACCOUNT,
  COMMAND_SYNC_PREFERENCES,
  COMMAND_CHANGE_PASSWORD,
  COMMAND_FXA_STATUS,
  COMMAND_PAIR_HEARTBEAT,
  COMMAND_PAIR_SUPP_METADATA,
  COMMAND_PAIR_AUTHORIZE,
  COMMAND_PAIR_DECLINE,
  COMMAND_PAIR_COMPLETE,
  COMMAND_PAIR_PREFERENCES,
  COMMAND_FIREFOX_VIEW,
  OAUTH_CLIENT_ID,
  ON_PROFILE_CHANGE_NOTIFICATION,
  PREF_LAST_FXA_USER,
  WEBCHANNEL_ID,
  log,
  logPII,
} from "resource://gre/modules/FxAccountsCommon.sys.mjs";
import { SyncDisconnect } from "resource://services-sync/SyncDisconnect.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CryptoUtils: "resource://services-crypto/utils.sys.mjs",
  FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs",
  FxAccountsStorageManagerCanStoreField:
    "resource://gre/modules/FxAccountsStorage.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  SelectableProfileService:
    "resource:///modules/profiles/SelectableProfileService.sys.mjs",
  Weave: "resource://services-sync/main.sys.mjs",
  WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
  return ChromeUtils.importESModule(
    "resource://gre/modules/FxAccounts.sys.mjs"
  ).getFxAccountsSingleton();
});
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "pairingEnabled",
  "identity.fxaccounts.pairing.enabled"
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "separatePrivilegedMozillaWebContentProcess",
  "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "separatedMozillaDomains",
  "browser.tabs.remote.separatedMozillaDomains",
  "",
  false,
  val => val.split(",")
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "accountServer",
  "identity.fxaccounts.remote.root",
  null,
  false,
  val => Services.io.newURI(val)
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "oauthEnabled",
  "identity.fxaccounts.oauth.enabled",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "browserProfilesEnabled",
  "browser.profiles.enabled",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "allowSyncMerge",
  "browser.profiles.sync.allow-danger-merge",
  false
);

ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
  return new Localization(["browser/sync.ftl", "branding/brand.ftl"], true);
});

// These engines will be displayed to the user to pick which they would like to
// use.
const CHOOSE_WHAT_TO_SYNC_ALWAYS_AVAILABLE = [
  "addons",
  "bookmarks",
  "history",
  "passwords",
  "prefs",
  "tabs",
];

// Engines which we need to inspect a pref to see if they are available, and
// possibly have their default preference value to disabled.
const CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE = ["addresses", "creditcards"];

/**
 * A helper function that extracts the message and stack from an error object.
 * Returns a `{ message, stack }` tuple. `stack` will be null if the error
 * doesn't have a stack trace.
 */
function getErrorDetails(error) {
  // Replace anything that looks like it might be a filepath on Windows or Unix
  let cleanMessage = String(error)
    .replace(/\\.*\\/gm, "[REDACTED]")
    .replace(/\/.*\//gm, "[REDACTED]");
  let details = { message: cleanMessage, stack: null };

  // Adapted from Console.sys.mjs.
  if (error.stack) {
    let frames = [];
    for (let frame = error.stack; frame; frame = frame.caller) {
      frames.push(String(frame).padStart(4));
    }
    details.stack = frames.join("\n");
  }

  return details;
}

/**
 * Create a new FxAccountsWebChannel to listen for account updates
 *
 * @param {Object} options Options
 *   @param {Object} options
 *     @param {String} options.content_uri
 *     The FxA Content server uri
 *     @param {String} options.channel_id
 *     The ID of the WebChannel
 *     @param {String} options.helpers
 *     Helpers functions. Should only be passed in for testing.
 * @constructor
 */
export function FxAccountsWebChannel(options) {
  if (!options) {
    throw new Error("Missing configuration options");
  }
  if (!options.content_uri) {
    throw new Error("Missing 'content_uri' option");
  }
  this._contentUri = options.content_uri;

  if (!options.channel_id) {
    throw new Error("Missing 'channel_id' option");
  }
  this._webChannelId = options.channel_id;

  // options.helpers is only specified by tests.
  ChromeUtils.defineLazyGetter(this, "_helpers", () => {
    return options.helpers || new FxAccountsWebChannelHelpers(options);
  });

  this._setupChannel();
}

FxAccountsWebChannel.prototype = {
  /**
   * WebChannel that is used to communicate with content page
   */
  _channel: null,

  /**
   * Helpers interface that does the heavy lifting.
   */
  _helpers: null,

  /**
   * WebChannel ID.
   */
  _webChannelId: null,
  /**
   * WebChannel origin, used to validate origin of messages
   */
  _webChannelOrigin: null,

  /**
   * The promise which is handling the most recent webchannel message we received.
   * Used to avoid us handling multiple messages concurrently.
   */
  _lastPromise: null,

  /**
   * Release all resources that are in use.
   */
  tearDown() {
    this._channel.stopListening();
    this._channel = null;
    this._channelCallback = null;
  },

  /**
   * Configures and registers a new WebChannel
   *
   * @private
   */
  _setupChannel() {
    // if this.contentUri is present but not a valid URI, then this will throw an error.
    try {
      this._webChannelOrigin = Services.io.newURI(this._contentUri);
      this._registerChannel();
    } catch (e) {
      log.error(e);
      throw e;
    }
  },

  _receiveMessage(message, sendingContext) {
    log.trace(`_receiveMessage for command ${message.command}`);
    let shouldCheckRemoteType =
      lazy.separatePrivilegedMozillaWebContentProcess &&
      lazy.separatedMozillaDomains.some(function (val) {
        return (
          lazy.accountServer.asciiHost == val ||
          lazy.accountServer.asciiHost.endsWith("." + val)
        );
      });
    let { currentRemoteType } = sendingContext.browsingContext;
    if (shouldCheckRemoteType && currentRemoteType != "privilegedmozilla") {
      log.error(
        `Rejected FxA webchannel message from remoteType = ${currentRemoteType}`
      );
      return;
    }

    // Here we do some promise dances to ensure we are never handling multiple messages
    // concurrently, which can happen for async message handlers.
    // Not all handlers are async, which is something we should clean up to make this simpler.
    // Start with ensuring the last promise we saw is complete.
    let lastPromise = this._lastPromise || Promise.resolve();
    this._lastPromise = lastPromise
      .then(() => {
        return this._promiseMessage(message, sendingContext);
      })
      .catch(e => {
        log.error("Handling webchannel message failed", e);
        this._sendError(e, message, sendingContext);
      })
      .finally(() => {
        this._lastPromise = null;
      });
  },

  async _promiseMessage(message, sendingContext) {
    const { command, data } = message;
    let browser = sendingContext.browsingContext.top.embedderElement;
    switch (command) {
      case COMMAND_PROFILE_CHANGE:
        Services.obs.notifyObservers(
          null,
          ON_PROFILE_CHANGE_NOTIFICATION,
          data.uid
        );
        break;
      case COMMAND_LOGIN:
        await this._helpers.login(data);
        break;
      case COMMAND_OAUTH:
        await this._helpers.oauthLogin(data);
        break;
      case COMMAND_LOGOUT:
      case COMMAND_DELETE:
        await this._helpers.logout(data.uid);
        break;
      case COMMAND_CAN_LINK_ACCOUNT:
        {
          let response = { command, messageId: message.messageId };
          // If browser profiles are not enabled, then we use the old merge sync dialog
          if (!lazy.browserProfilesEnabled) {
            response.data = { ok: this._helpers.shouldAllowRelink(data.email) };
            this._channel.send(response, sendingContext);
            break;
          }
          // In the new sync warning, we give users a few more options to
          // control what they want to do with their sync data
          let result = await this._helpers.promptProfileSyncWarningIfNeeded(
            data.email
          );
          switch (result.action) {
            case "create-profile":
              lazy.SelectableProfileService.createNewProfile();
              response.data = { ok: false };
              break;
            case "switch-profile":
              lazy.SelectableProfileService.launchInstance(result.data);
              response.data = { ok: false };
              break;
            // Either no warning was shown, or user selected the continue option
            // to link the account
            case "continue":
              response.data = { ok: true };
              break;
            case "cancel":
              response.data = { ok: false };
              break;
            default:
              log.error(
                "Invalid FxAccountsWebChannel dialog response: ",
                result.action
              );
              response.data = { ok: false };
              break;
          }
          log.debug("FxAccountsWebChannel response", response);
          // Send the response based on what the user selected above
          this._channel.send(response, sendingContext);
        }
        break;
      case COMMAND_SYNC_PREFERENCES:
        this._helpers.openSyncPreferences(browser, data.entryPoint);
        break;
      case COMMAND_PAIR_PREFERENCES:
        if (lazy.pairingEnabled) {
          let win = browser.ownerGlobal;
          win.openTrustedLinkIn(
            "about:preferences?action=pair#sync",
            "current"
          );
        }
        break;
      case COMMAND_FIREFOX_VIEW:
        this._helpers.openFirefoxView(browser, data.entryPoint);
        break;
      case COMMAND_CHANGE_PASSWORD:
        await this._helpers.changePassword(data);
        break;
      case COMMAND_FXA_STATUS:
        log.debug("fxa_status received");
        const service = data && data.service;
        const isPairing = data && data.isPairing;
        const context = data && data.context;
        await this._helpers
          .getFxaStatus(service, sendingContext, isPairing, context)
          .then(fxaStatus => {
            let response = {
              command,
              messageId: message.messageId,
              data: fxaStatus,
            };
            this._channel.send(response, sendingContext);
          });
        break;
      case COMMAND_PAIR_HEARTBEAT:
      case COMMAND_PAIR_SUPP_METADATA:
      case COMMAND_PAIR_AUTHORIZE:
      case COMMAND_PAIR_DECLINE:
      case COMMAND_PAIR_COMPLETE:
        log.debug(`Pairing command ${command} received`);
        const { channel_id: channelId } = data;
        delete data.channel_id;
        const flow = lazy.FxAccountsPairingFlow.get(channelId);
        if (!flow) {
          log.warn(`Could not find a pairing flow for ${channelId}`);
          return;
        }
        flow.onWebChannelMessage(command, data).then(replyData => {
          this._channel.send(
            {
              command,
              messageId: message.messageId,
              data: replyData,
            },
            sendingContext
          );
        });
        break;
      default:
        log.warn("Unrecognized FxAccountsWebChannel command", command);
        // As a safety measure we also terminate any pending FxA pairing flow.
        lazy.FxAccountsPairingFlow.finalizeAll();
        break;
    }
  },

  _sendError(error, incomingMessage, sendingContext) {
    log.error("Failed to handle FxAccountsWebChannel message", error);
    this._channel.send(
      {
        command: incomingMessage.command,
        messageId: incomingMessage.messageId,
        data: {
          error: getErrorDetails(error),
        },
      },
      sendingContext
    );
  },

  /**
   * Create a new channel with the WebChannelBroker, setup a callback listener
   * @private
   */
  _registerChannel() {
    /**
     * Processes messages that are called back from the FxAccountsChannel
     *
     * @param webChannelId {String}
     *        Command webChannelId
     * @param message {Object}
     *        Command message
     * @param sendingContext {Object}
     *        Message sending context.
     *        @param sendingContext.browsingContext {BrowsingContext}
     *               The browsingcontext from which the
     *               WebChannelMessageToChrome was sent.
     *        @param sendingContext.eventTarget {EventTarget}
     *               The <EventTarget> where the message was sent.
     *        @param sendingContext.principal {Principal}
     *               The <Principal> of the EventTarget where the message was sent.
     * @private
     *
     */
    let listener = (webChannelId, message, sendingContext) => {
      if (message) {
        log.debug("FxAccountsWebChannel message received", message.command);
        if (logPII()) {
          log.debug("FxAccountsWebChannel message details", message);
        }
        try {
          this._receiveMessage(message, sendingContext);
        } catch (error) {
          // this should be impossible - _receiveMessage will do this, but better safe than sorry.
          log.error(
            "Unexpected webchannel error escaped from promise error handlers"
          );
          this._sendError(error, message, sendingContext);
        }
      }
    };

    this._channelCallback = listener;
    this._channel = new lazy.WebChannel(
      this._webChannelId,
      this._webChannelOrigin
    );
    this._channel.listen(listener);
    log.debug(
      "FxAccountsWebChannel registered: " +
        this._webChannelId +
        " with origin " +
        this._webChannelOrigin.prePath
    );
  },
};

export function FxAccountsWebChannelHelpers(options) {
  options = options || {};

  this._fxAccounts = options.fxAccounts || lazy.fxAccounts;
  this._weaveXPCOM = options.weaveXPCOM || null;
  this._privateBrowsingUtils =
    options.privateBrowsingUtils || lazy.PrivateBrowsingUtils;
}

FxAccountsWebChannelHelpers.prototype = {
  // If the last fxa account used for sync isn't this account, we display
  // a modal dialog checking they really really want to do this...
  // (This is sync-specific, so ideally would be in sync's identity module,
  // but it's a little more seamless to do here, and sync is currently the
  // only fxa consumer, so...
  shouldAllowRelink(acctName) {
    return (
      !this._needRelinkWarning(acctName) || this._promptForRelink(acctName)
    );
  },

  /**
   * Checks if the user is potentially hitting an issue with the current
   * account they're logging into. Returns the choice of the user if shown
   * @returns {string} - The corresponding option the user pressed. Can be either:
   * cancel, continue, switch-profile, or create-profile
   */
  async promptProfileSyncWarningIfNeeded(acctEmail) {
    // Was a previous account signed into this profile or is there another profile currently signed in
    // to the account we're signing into
    let profileLinkedWithAcct =
      await this._getProfileAssociatedWithAcct(acctEmail);
    if (this._needRelinkWarning(acctEmail) || profileLinkedWithAcct) {
      return this._promptForProfileSyncWarning(
        acctEmail,
        profileLinkedWithAcct
      );
    }
    // The user has no warnings needed and can continue signing in
    return { action: "continue" };
  },

  async _initializeSync() {
    // A sync-specific hack - we want to ensure sync has been initialized
    // before we set the signed-in user.
    // XXX - probably not true any more, especially now we have observerPreloads
    // in FxAccounts.sys.mjs?
    let xps =
      this._weaveXPCOM ||
      Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
        .wrappedJSObject;
    await xps.whenLoaded();
    return xps;
  },

  _setEnabledEngines(offeredEngines, declinedEngines) {
    if (offeredEngines && declinedEngines) {
      log.debug("Received offered engines", offeredEngines);
      CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE.forEach(engine => {
        if (
          offeredEngines.includes(engine) &&
          !declinedEngines.includes(engine)
        ) {
          // These extra engines are disabled by default.
          log.debug(`Enabling optional engine '${engine}'`);
          Services.prefs.setBoolPref(`services.sync.engine.${engine}`, true);
        }
      });
      log.debug("Received declined engines", declinedEngines);
      lazy.Weave.Service.engineManager.setDeclined(declinedEngines);
      declinedEngines.forEach(engine => {
        Services.prefs.setBoolPref(`services.sync.engine.${engine}`, false);
      });
    } else {
      log.debug("Did not receive any engine selection information");
    }
  },

  /** Internal function used to configure the requested services.
   *
   * The "services" param is an object as received from the FxA server.
   */
  async _enableRequestedServices(requestedServices) {
    if (!requestedServices) {
      log.warn(
        "fxa login completed but we don't have a record of which services were enabled."
      );
      return;
    }
    log.debug(`services requested are ${Object.keys(requestedServices)}`);
    if (requestedServices.sync) {
      const xps = await this._initializeSync();
      const { offeredEngines, declinedEngines } = requestedServices.sync;
      this._setEnabledEngines(offeredEngines, declinedEngines);
      log.debug("Webchannel is enabling sync");
      await xps.Weave.Service.configure();
    }
  },

  /**
   * The login message is sent when the user user has initially logged in but may not be fully connected.
   * * In the non-oauth flows, if the user is verified, then the browser itself is able to transition the
   *   user to fully connected.
   * * In the oauth flows, we will need an `oauth_login` message with our scoped keys to be fully connected.
   * @param accountData the user's account data and credentials
   */
  async login(accountData) {
    // This is delicate for oauth flows and edge-cases. Consider (a) user logs in but does not verify,
    // (b) browser restarts, (c) user select "finish setup", at which point they are again prompted for their password.
    // In that scenario, we've been sent this `login` message *both* at (a) and at (c).
    // Importantly, the message from (a) is the one that actually has the service information we care about
    // (eg, the sync engine selections) - (c) *will* have `services.sync` but it will be an empty object.
    // This means we need to take care to not lose the services from (a) when processing (c).
    const signedInUser = await this._fxAccounts.getSignedInUser([
      "requestedServices",
    ]);
    let existingServices;
    if (signedInUser) {
      if (signedInUser.uid != accountData.uid) {
        log.warn(
          "the webchannel found a different user signed in - signing them out."
        );
        await this._disconnect();
      } else {
        existingServices = signedInUser.requestedServices
          ? JSON.parse(signedInUser.requestedServices)
          : {};
        log.debug(
          "Webchannel is updating the info for an already logged in user."
        );
      }
    } else {
      log.debug("Webchannel is logging new a user in.");
    }
    // There are (or were) extra fields here we don't want to actually store.
    delete accountData.customizeSync;
    delete accountData.verifiedCanLinkAccount;
    if (lazy.oauthEnabled) {
      // We once accidentally saw these from the server and got confused about who owned the key fetching.
      delete accountData.keyFetchToken;
      delete accountData.unwrapBKey;
    }

    // The "services" being connected - see above re our careful handling of existing data.
    // Note that we don't attempt to merge any data - we keep the first value we see for a service
    // and ignore that service subsequently (as it will be common for subsequent messages to
    // name a service but not supply any data for it)
    const requestedServices = {
      ...(accountData.services ?? {}),
      ...existingServices,
    };
    delete accountData.services;

    // This `verified` check is really just for our tests and pre-oauth flows.
    // However, in all cases it's misplaced - we should set it as soon as *sync*
    // starts or is configured, as it's the merging done by sync it protects against.
    // We should clean up handling of this pref in a followup.
    if (accountData.verified) {
      this.setPreviousAccountNameHashPref(accountData.email);
    }

    await this._fxAccounts.telemetry.recordConnection(
      Object.keys(requestedServices),
      "webchannel"
    );

    if (lazy.oauthEnabled) {
      // We need to remember the requested services because we can't act on them until we get the `oauth_login` message.
      // And because we might not get that message in this browser session (eg, the browser might restart before the
      // user enters their verification code), they are persisted with the account state.
      log.debug(`storing info for services ${Object.keys(requestedServices)}`);
      accountData.requestedServices = JSON.stringify(requestedServices);
      await this._fxAccounts._internal.setSignedInUser(accountData);
    } else {
      // Note we don't persist anything in requestedServices for non oauth flows because we act on them now.
      await this._fxAccounts._internal.setSignedInUser(accountData);
      await this._enableRequestedServices(requestedServices);
    }
    log.debug("Webchannel finished logging a user in.");
  },

  /**
   * Logins in to sync by completing an OAuth flow
   * @param { Object } oauthData: The oauth code and state as returned by the server
   */
  async oauthLogin(oauthData) {
    log.debug("Webchannel is completing the oauth flow");
    const { uid, sessionToken, email, requestedServices } =
      await this._fxAccounts._internal.getUserAccountData([
        "uid",
        "sessionToken",
        "email",
        "requestedServices",
      ]);
    // First we finish the ongoing oauth flow
    const { scopedKeys, refreshToken } =
      await this._fxAccounts._internal.completeOAuthFlow(
        sessionToken,
        oauthData.code,
        oauthData.state
      );

    // We don't currently use the refresh token in Firefox Desktop, lets be good citizens and revoke it.
    await this._fxAccounts._internal.destroyOAuthToken({ token: refreshToken });

    // Remember the account for future merge warnings etc.
    this.setPreviousAccountNameHashPref(email);

    // Then, we persist the sync keys
    await this._fxAccounts._internal.setScopedKeys(scopedKeys);

    try {
      let parsedRequestedServices;
      if (requestedServices) {
        parsedRequestedServices = JSON.parse(requestedServices);
      }
      await this._enableRequestedServices(parsedRequestedServices);
    } finally {
      // We don't want them hanging around in storage.
      await this._fxAccounts._internal.updateUserAccountData({
        uid,
        requestedServices: null,
      });
    }

    // Now that we have the scoped keys, we set our status to verified.
    // This will kick off Sync or other services we configured.
    await this._fxAccounts._internal.setUserVerified();
    log.debug("Webchannel completed oauth flows");
  },

  /**
   * Disconnects the user from Sync and FxA
   */
  _disconnect() {
    return SyncDisconnect.disconnect(false);
  },

  /**
   * logout the fxaccounts service
   *
   * @param the uid of the account which have been logged out
   */
  async logout(uid) {
    let fxa = this._fxAccounts;
    let userData = await fxa._internal.getUserAccountData(["uid"]);
    if (userData && userData.uid === uid) {
      await fxa.telemetry.recordDisconnection(null, "webchannel");
      // true argument is `localOnly`, because server-side stuff
      // has already been taken care of by the content server
      await fxa.signOut(true);
    }
  },

  /**
   * Check if `sendingContext` is in private browsing mode.
   */
  isPrivateBrowsingMode(sendingContext) {
    if (!sendingContext) {
      log.error("Unable to check for private browsing mode, assuming true");
      return true;
    }

    let browser = sendingContext.browsingContext.top.embedderElement;
    const isPrivateBrowsing =
      this._privateBrowsingUtils.isBrowserPrivate(browser);
    return isPrivateBrowsing;
  },

  /**
   * Check whether sending fxa_status data should be allowed.
   */
  shouldAllowFxaStatus(service, sendingContext, isPairing, context) {
    // Return user data for any service in non-PB mode. In PB mode,
    // only return user data if service==="sync" or is in pairing mode
    // (as service will be equal to the OAuth client ID and not "sync").
    //
    // This behaviour allows users to click the "Manage Account"
    // link from about:preferences#sync while in PB mode and things
    // "just work". While in non-PB mode, users can sign into
    // Pocket w/o entering their password a 2nd time, while in PB
    // mode they *will* have to enter their email/password again.
    //
    // The difference in behaviour is to try to match user
    // expectations as to what is and what isn't part of the browser.
    // Sync is viewed as an integral part of the browser, interacting
    // with FxA as part of a Sync flow should work all the time. If
    // Sync is broken in PB mode, users will think Firefox is broken.
    // See https://bugzilla.mozilla.org/show_bug.cgi?id=1323853
    //
    // XXX - This hard-coded context seems bad?
    let pb = this.isPrivateBrowsingMode(sendingContext);
    let ok =
      !pb || service === "sync" || context === "fx_desktop_v3" || isPairing;
    log.debug(
      `fxa status ok=${ok} - private=${pb}, service=${service}, context=${context}, pairing=${isPairing}`
    );
    return ok;
  },

  /**
   * Get fxa_status information. Resolves to { signedInUser: <user_data> }.
   * If returning status information is not allowed or no user is signed into
   * Sync, `user_data` will be null.
   */
  async getFxaStatus(service, sendingContext, isPairing, context) {
    let signedInUser = null;

    if (
      this.shouldAllowFxaStatus(service, sendingContext, isPairing, context)
    ) {
      const userData = await this._fxAccounts._internal.getUserAccountData([
        "email",
        "sessionToken",
        "uid",
        "verified",
      ]);
      if (userData) {
        signedInUser = {
          email: userData.email,
          sessionToken: userData.sessionToken,
          uid: userData.uid,
          verified: userData.verified,
        };
      }
    }

    const capabilities = this._getCapabilities();

    return {
      signedInUser,
      clientId: OAUTH_CLIENT_ID,
      capabilities,
    };
  },

  _getCapabilities() {
    // pre-oauth flows there we a strange setup where we just supplied the "extra" engines,
    // whereas oauth flows want them all.
    let engines = lazy.oauthEnabled
      ? Array.from(CHOOSE_WHAT_TO_SYNC_ALWAYS_AVAILABLE)
      : [];
    for (let optionalEngine of CHOOSE_WHAT_TO_SYNC_OPTIONALLY_AVAILABLE) {
      if (
        Services.prefs.getBoolPref(
          `services.sync.engine.${optionalEngine}.available`,
          false
        )
      ) {
        engines.push(optionalEngine);
      }
    }
    return {
      multiService: true,
      pairing: lazy.pairingEnabled,
      choose_what_to_sync: true,
      engines,
    };
  },

  async changePassword(credentials) {
    // If |credentials| has fields that aren't handled by accounts storage,
    // updateUserAccountData will throw - mainly to prevent errors in code
    // that hard-codes field names.
    // However, in this case the field names aren't really in our control.
    // We *could* still insist the server know what fields names are valid,
    // but that makes life difficult for the server when Firefox adds new
    // features (ie, new fields) - forcing the server to track a map of
    // versions to supported field names doesn't buy us much.
    // So we just remove field names we know aren't handled.
    let newCredentials = {
      device: null, // Force a brand new device registration.
      // We force the re-encryption of the send tab keys using the new sync key after the password change
      encryptedSendTabKeys: null,
    };
    for (let name of Object.keys(credentials)) {
      if (
        name == "email" ||
        name == "uid" ||
        lazy.FxAccountsStorageManagerCanStoreField(name)
      ) {
        newCredentials[name] = credentials[name];
      } else {
        log.info("changePassword ignoring unsupported field", name);
      }
    }
    await this._fxAccounts._internal.updateUserAccountData(newCredentials);
    await this._fxAccounts._internal.updateDeviceRegistration();
  },

  /**
   * Get the hash of account name of the previously signed in account
   */
  getPreviousAccountNameHashPref() {
    try {
      return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
    } catch (_) {
      return "";
    }
  },

  /**
   * Given an account name, set the hash of the previously signed in account
   *
   * @param acctName the account name of the user's account.
   */
  setPreviousAccountNameHashPref(acctName) {
    Services.prefs.setStringPref(
      PREF_LAST_FXA_USER,
      lazy.CryptoUtils.sha256Base64(acctName)
    );
  },

  /**
   * Open Sync Preferences in the current tab of the browser
   *
   * @param {Object} browser the browser in which to open preferences
   * @param {String} [entryPoint] entryPoint to use for logging
   */
  openSyncPreferences(browser, entryPoint) {
    let uri = "about:preferences";
    if (entryPoint) {
      uri += "?entrypoint=" + encodeURIComponent(entryPoint);
    }
    uri += "#sync";

    browser.loadURI(Services.io.newURI(uri), {
      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    });
  },

  /**
   * Open Firefox View in the browser's window
   *
   * @param {Object} browser the browser in whose window we'll open Firefox View
   */
  openFirefoxView(browser) {
    browser.ownerGlobal.FirefoxViewHandler.openTab("syncedtabs");
  },

  /**
   * If a user signs in using a different account, the data from the
   * previous account and the new account will be merged. Ask the user
   * if they want to continue.
   *
   * @private
   */
  _needRelinkWarning(acctName) {
    let prevAcctHash = this.getPreviousAccountNameHashPref();
    return (
      prevAcctHash && prevAcctHash != lazy.CryptoUtils.sha256Base64(acctName)
    );
  },

  // Get the current name of the profile the user is currently on
  _getCurrentProfileName() {
    return lazy.SelectableProfileService?.currentProfile?.name;
  },

  async _getAllProfiles() {
    return await lazy.SelectableProfileService.getAllProfiles();
  },

  /**
   * Checks if a profile is associated with the given account email.
   *
   * @param {string} acctEmail - The email of the account to check.
   * @returns {Promise<SelectableProfile|null>} - The profile associated with the account, or null if none.
   */
  async _getProfileAssociatedWithAcct(acctEmail) {
    let profiles = await this._getAllProfiles();
    let currentProfile = await this._getCurrentProfileName();
    for (let profile of profiles) {
      if (profile.name === currentProfile.name) {
        continue; // Skip current profile
      }

      let profilePath = profile.path;
      let signedInUserPath = PathUtils.join(profilePath, "signedInUser.json");
      let signedInUser = await this._readJSONFileAsync(signedInUserPath);
      if (
        signedInUser?.accountData &&
        signedInUser.accountData.email === acctEmail
      ) {
        // The account is signed into another profile
        return profile;
      }
    }
    return null;
  },

  async _readJSONFileAsync(filePath) {
    try {
      let data = await IOUtils.readJSON(filePath);
      if (data && data.version !== 1) {
        throw new Error(
          `Unsupported signedInUser.json version: ${data.version}`
        );
      }
      return data;
    } catch (e) {
      // File not found or error reading/parsing
      return null;
    }
  },

  /**
   * Show the user a warning dialog that the data from the previous account
   * and the new account will be merged. _promptForSyncWarning should be
   * used instead of this
   *
   * @private
   */
  _promptForRelink(acctName) {
    let [continueLabel, title, heading, description] =
      lazy.l10n.formatValuesSync([
        { id: "sync-setup-verify-continue" },
        { id: "sync-setup-verify-title" },
        { id: "sync-setup-verify-heading" },
        {
          id: "sync-setup-verify-description",
          args: {
            email: acctName,
          },
        },
      ]);
    let body = heading + "\n\n" + description;
    let ps = Services.prompt;
    let buttonFlags =
      ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
      ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
      ps.BUTTON_POS_1_DEFAULT;

    // If running in context of the browser chrome, window does not exist.
    let pressed = Services.prompt.confirmEx(
      null,
      title,
      body,
      buttonFlags,
      continueLabel,
      null,
      null,
      null,
      {}
    );
    this.emitSyncWarningDialogTelemetry(
      { 0: "continue", 1: "cancel" },
      pressed,
      false // old dialog doesn't have other profiles
    );
    return pressed === 0; // 0 is the "continue" button
  },

  /**
   * Similar to _promptForRelink but more offers more contextual warnings
   * to the user to support browser profiles.
   * @returns {string} - The corresponding option the user pressed. Can be either:
   * cancel, continue, switch-profile, or create-profile
   *
   */
  _promptForProfileSyncWarning(acctEmail, profileLinkedWithAcct) {
    let currentProfile = this._getCurrentProfileName();
    let title, heading, description, mergeLabel, switchLabel;
    if (profileLinkedWithAcct) {
      [title, heading, description, mergeLabel, switchLabel] =
        lazy.l10n.formatValuesSync([
          { id: "sync-account-in-use-header" },
          {
            id: lazy.allowSyncMerge
              ? "sync-account-already-signed-in-header"
              : "sync-account-in-use-header-merge",
            args: {
              acctEmail,
              otherProfile: profileLinkedWithAcct.name,
            },
          },
          {
            id: lazy.allowSyncMerge
              ? "sync-account-in-use-description-merge"
              : "sync-account-in-use-description",
            args: {
              acctEmail,
              currentProfile,
              otherProfile: profileLinkedWithAcct.name,
            },
          },
          {
            id: "sync-button-sync-profile",
            args: { profileName: currentProfile },
          },
          {
            id: "sync-button-switch-profile",
            args: { profileName: profileLinkedWithAcct.name },
          },
        ]);
    } else {
      // This current profile was previously associated with a different account
      [title, heading, description, mergeLabel, switchLabel] =
        lazy.l10n.formatValuesSync([
          {
            id: lazy.allowSyncMerge
              ? "sync-profile-different-account-title-merge"
              : "sync-profile-different-account-title",
          },
          {
            id: "sync-profile-different-account-header",
          },
          {
            id: lazy.allowSyncMerge
              ? "sync-profile-different-account-description-merge"
              : "sync-profile-different-account-description",
            args: {
              acctEmail,
              profileName: currentProfile,
            },
          },
          { id: "sync-button-sync-and-merge" },
          { id: "sync-button-create-profile" },
        ]);
    }
    let result = this.showWarningPrompt({
      title,
      body: `${heading}\n\n${description}`,
      btnLabel1: lazy.allowSyncMerge ? mergeLabel : switchLabel,
      btnLabel2: lazy.allowSyncMerge ? switchLabel : null,
      isAccountLoggedIntoAnotherProfile: !!profileLinkedWithAcct,
    });

    // If the user chose to switch profiles, return the associated profile as well.
    if (result === "switch-profile") {
      return { action: result, data: profileLinkedWithAcct };
    }

    // For all other actions, just return the action name.
    return { action: result };
  },

  /**
   * Shows the user a warning prompt.
   * @returns {string} - The corresponding option the user pressed. Can be either:
   * cancel, continue, switch-profile, or create-profile
   */
  showWarningPrompt({
    title,
    body,
    btnLabel1,
    btnLabel2,
    isAccountLoggedIntoAnotherProfile,
  }) {
    let ps = Services.prompt;
    let buttonFlags;
    let pressed;
    let actionMap = {};

    if (lazy.allowSyncMerge) {
      // Merge allowed: two options + cancel
      buttonFlags =
        ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
        ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING +
        ps.BUTTON_POS_2 * ps.BUTTON_TITLE_CANCEL +
        ps.BUTTON_POS_2_DEFAULT;

      // Define action map based on context
      if (isAccountLoggedIntoAnotherProfile) {
        // Account is associated with another profile
        actionMap = {
          0: "continue", // merge option
          1: "switch-profile",
          2: "cancel",
        };
      } else {
        // Profile was previously logged in with another account
        actionMap = {
          0: "continue", // merge option
          1: "create-profile",
          2: "cancel",
        };
      }

      // Show the prompt
      pressed = ps.confirmEx(
        null,
        title,
        body,
        buttonFlags,
        btnLabel1,
        btnLabel2,
        null,
        null,
        {}
      );
    } else {
      // Merge not allowed: one option + cancel
      buttonFlags =
        ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
        ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
        ps.BUTTON_POS_1_DEFAULT;

      // Define action map based on context
      if (isAccountLoggedIntoAnotherProfile) {
        // Account is associated with another profile
        actionMap = {
          0: "switch-profile",
          1: "cancel",
        };
      } else {
        // Profile was previously logged in with another account
        actionMap = {
          0: "create-profile",
          1: "cancel",
        };
      }

      // Show the prompt
      pressed = ps.confirmEx(
        null,
        title,
        body,
        buttonFlags,
        btnLabel1,
        null,
        null,
        null,
        {}
      );
    }

    this.emitSyncWarningDialogTelemetry(
      actionMap,
      pressed,
      isAccountLoggedIntoAnotherProfile
    );
    return actionMap[pressed] || "unknown";
  },

  emitSyncWarningDialogTelemetry(
    actionMap,
    pressed,
    isAccountLoggedIntoAnotherProfile
  ) {
    let variant;

    if (!lazy.browserProfilesEnabled) {
      // Old merge dialog
      variant = "old-merge";
    } else if (isAccountLoggedIntoAnotherProfile) {
      // Sync warning dialog for profile already associated
      variant = lazy.allowSyncMerge
        ? "sync-warning-allow-merge"
        : "sync-warning";
    } else {
      // Sync warning dialog for a different account previously logged in
      variant = lazy.allowSyncMerge
        ? "merge-warning-allow-merge"
        : "merge-warning";
    }

    // Telemetry extra options
    let extraOptions = {
      variant_shown: variant,
      option_clicked: actionMap[pressed] || "unknown",
    };

    // Record telemetry
    Glean.syncMergeDialog?.clicked?.record(extraOptions);
  },
};

var singleton;

// The entry-point for this module, which ensures only one of our channels is
// ever created - we require this because the WebChannel is global in scope
// (eg, it uses the observer service to tell interested parties of interesting
// things) and allowing multiple channels would cause such notifications to be
// sent multiple times.
export var EnsureFxAccountsWebChannel = () => {
  let contentUri = Services.urlFormatter.formatURLPref(
    "identity.fxaccounts.remote.root"
  );
  if (singleton && singleton._contentUri !== contentUri) {
    singleton.tearDown();
    singleton = null;
  }
  if (!singleton) {
    try {
      if (contentUri) {
        // The FxAccountsWebChannel listens for events and updates
        // the state machine accordingly.
        singleton = new FxAccountsWebChannel({
          content_uri: contentUri,
          channel_id: WEBCHANNEL_ID,
        });
      } else {
        log.warn("FxA WebChannel functionaly is disabled due to no URI pref.");
      }
    } catch (ex) {
      log.error("Failed to create FxA WebChannel", ex);
    }
  }
};

[ Dauer der Verarbeitung: 0.40 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge