summaryrefslogtreecommitdiffstats
path: root/services/fxaccounts/FxAccountsWebChannel.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/fxaccounts/FxAccountsWebChannel.sys.mjs824
1 files changed, 824 insertions, 0 deletions
diff --git a/services/fxaccounts/FxAccountsWebChannel.sys.mjs b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
new file mode 100644
index 0000000000..fdd0b75e93
--- /dev/null
+++ b/services/fxaccounts/FxAccountsWebChannel.sys.mjs
@@ -0,0 +1,824 @@
+/* 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,
+ FX_OAUTH_CLIENT_ID,
+ ON_PROFILE_CHANGE_NOTIFICATION,
+ PREF_LAST_FXA_USER,
+ WEBCHANNEL_ID,
+ log,
+ logPII,
+} from "resource://gre/modules/FxAccountsCommon.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",
+ 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)
+);
+
+// These engines were added years after Sync had been introduced, they need
+// special handling since they are system add-ons and are un-available on
+// older versions of Firefox.
+const EXTRA_ENGINES = ["addresses", "creditcards"];
+
+// These engines will be displayed to the user to pick which they would like to
+// use
+const CHOOSE_WHAT_TO_SYNC = [
+ "addons",
+ "addresses",
+ "bookmarks",
+ "creditcards",
+ "history",
+ "passwords",
+ "preferences",
+ "tabs",
+];
+
+/**
+ * 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,
+
+ /**
+ * 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) {
+ const { command, data } = message;
+ 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;
+ }
+
+ 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:
+ this._helpers
+ .login(data)
+ .catch(error => this._sendError(error, message, sendingContext));
+ break;
+ case COMMAND_OAUTH:
+ this._helpers
+ .oauthLogin(data)
+ .catch(error => this._sendError(error, message, sendingContext));
+ break;
+ case COMMAND_LOGOUT:
+ case COMMAND_DELETE:
+ this._helpers
+ .logout(data.uid)
+ .catch(error => this._sendError(error, message, sendingContext));
+ break;
+ case COMMAND_CAN_LINK_ACCOUNT:
+ let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
+
+ let response = {
+ command,
+ messageId: message.messageId,
+ data: { ok: canLinkAccount },
+ };
+
+ log.debug("FxAccountsWebChannel response", response);
+ 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 window = browser.ownerGlobal;
+ // We should close the FxA tab after we open our pref page
+ let selectedTab = window.gBrowser.selectedTab;
+ window.switchToTabHavingURI(
+ "about:preferences?action=pair#sync",
+ true,
+ {
+ ignoreQueryString: true,
+ replaceQueryString: true,
+ adoptIntoActiveWindow: true,
+ ignoreFragment: "whenComparing",
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ // close the tab
+ window.gBrowser.removeTab(selectedTab);
+ }
+ break;
+ case COMMAND_FIREFOX_VIEW:
+ this._helpers.openFirefoxView(browser, data.entryPoint);
+ break;
+ case COMMAND_CHANGE_PASSWORD:
+ this._helpers
+ .changePassword(data)
+ .catch(error => this._sendError(error, message, sendingContext));
+ 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;
+ this._helpers
+ .getFxaStatus(service, sendingContext, isPairing, context)
+ .then(fxaStatus => {
+ let response = {
+ command,
+ messageId: message.messageId,
+ data: fxaStatus,
+ };
+ this._channel.send(response, sendingContext);
+ })
+ .catch(error => this._sendError(error, message, 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._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)
+ );
+ },
+
+ 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.jsm?
+ 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) {
+ EXTRA_ENGINES.forEach(engine => {
+ if (
+ offeredEngines.includes(engine) &&
+ !declinedEngines.includes(engine)
+ ) {
+ // These extra engines are disabled by default.
+ 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);
+ });
+ }
+ },
+ /**
+ * stores sync login info it in the fxaccounts service
+ *
+ * @param accountData the user's account data and credentials
+ */
+ async login(accountData) {
+ // We don't act on customizeSync anymore, it used to open a dialog inside
+ // the browser to selecte the engines to sync but we do it on the web now.
+ log.debug("Webchannel is logging a user in.");
+ delete accountData.customizeSync;
+
+ // Save requested services for later.
+ const requestedServices = accountData.services;
+ delete accountData.services;
+
+ // the user has already been shown the "can link account"
+ // screen. No need to keep this data around.
+ delete accountData.verifiedCanLinkAccount;
+
+ // Remember who it was so we can log out next time.
+ if (accountData.verified) {
+ this.setPreviousAccountNameHashPref(accountData.email);
+ }
+
+ await this._fxAccounts.telemetry.recordConnection(
+ Object.keys(requestedServices || {}),
+ "webchannel"
+ );
+
+ const xps = await this._initializeSync();
+ await this._fxAccounts._internal.setSignedInUser(accountData);
+
+ if (requestedServices) {
+ // User has enabled Sync.
+ if (requestedServices.sync) {
+ const { offeredEngines, declinedEngines } = requestedServices.sync;
+ this._setEnabledEngines(offeredEngines, declinedEngines);
+ log.debug("Webchannel is enabling sync");
+ await xps.Weave.Service.configure();
+ }
+ }
+ },
+
+ /**
+ * 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 xps = await this._initializeSync();
+ const { code, state, declinedSyncEngines, offeredSyncEngines } = oauthData;
+ const { sessionToken } =
+ await this._fxAccounts._internal.getUserAccountData(["sessionToken"]);
+ // First we finish the ongoing oauth flow
+ const { scopedKeys, refreshToken } =
+ await this._fxAccounts._internal.completeOAuthFlow(
+ sessionToken,
+ code,
+ 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 });
+
+ // Then, we persist the sync keys
+ await this._fxAccounts._internal.setScopedKeys(scopedKeys);
+
+ // Now that we have the scoped keys, we set our status to verified
+ await this._fxAccounts._internal.setUserVerified();
+ this._setEnabledEngines(offeredSyncEngines, declinedSyncEngines);
+ log.debug("Webchannel is enabling sync");
+ xps.Weave.Service.configure();
+ },
+
+ /**
+ * 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);
+ log.debug("is private browsing", isPrivateBrowsing);
+ 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
+ log.debug("service", service);
+ return (
+ !this.isPrivateBrowsingMode(sendingContext) ||
+ service === "sync" ||
+ context === "fx_desktop_v3" ||
+ isPairing
+ );
+ },
+
+ /**
+ * 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: FX_OAUTH_CLIENT_ID,
+ capabilities,
+ };
+ },
+ _getCapabilities() {
+ if (
+ Services.prefs.getBoolPref("identity.fxaccounts.oauth.enabled", false)
+ ) {
+ return {
+ multiService: true,
+ pairing: lazy.pairingEnabled,
+ choose_what_to_sync: true,
+ engines: CHOOSE_WHAT_TO_SYNC,
+ };
+ }
+ return {
+ multiService: true,
+ pairing: lazy.pairingEnabled,
+ engines: this._getAvailableExtraEngines(),
+ };
+ },
+
+ _getAvailableExtraEngines() {
+ return EXTRA_ENGINES.filter(engineName => {
+ try {
+ return Services.prefs.getBoolPref(
+ `services.sync.engine.${engineName}.available`
+ );
+ } catch (e) {
+ return false;
+ }
+ });
+ },
+
+ 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)
+ );
+ },
+
+ /**
+ * Show the user a warning dialog that the data from the previous account
+ * and the new account will be merged.
+ *
+ * @private
+ */
+ _promptForRelink(acctName) {
+ let sb = Services.strings.createBundle(
+ "chrome://browser/locale/syncSetup.properties"
+ );
+ let continueLabel = sb.GetStringFromName("continue.label");
+ let title = sb.GetStringFromName("relinkVerify.title");
+ let description = sb.formatStringFromName("relinkVerify.description", [
+ acctName,
+ ]);
+ let body =
+ sb.GetStringFromName("relinkVerify.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,
+ {}
+ );
+ return pressed === 0; // 0 is the "continue" button
+ },
+};
+
+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);
+ }
+ }
+};