Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/**
* 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";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs",
FxAccountsStorageManagerCanStoreField:
"resource://gre/modules/FxAccountsStorage.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.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
);
ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
return new Localization(["browser/sync.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 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:
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)
);
},
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.
//
// 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)
);
},
/**
* Show the user a warning dialog that the data from the previous account
* and the new account will be merged.
*
* @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,
{}
);
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);
}
}
};