gecko-dev/services/fxaccounts/FxAccountsWebChannel.jsm
Wes Kocher c3cc3b6407 Backed out 13 changesets (bug 1296767) for xpcshell failures a=backout CLOSED TREE
Backed out changeset 41ed77788333 (bug 1296767)
Backed out changeset 1c0c9289b532 (bug 1296767)
Backed out changeset 50294db1d871 (bug 1296767)
Backed out changeset 26c065f79c54 (bug 1296767)
Backed out changeset 0362a78d6978 (bug 1296767)
Backed out changeset 4e71cf94e4ee (bug 1296767)
Backed out changeset f6f59447d22a (bug 1296767)
Backed out changeset 6c9b792cc296 (bug 1296767)
Backed out changeset 46a52b10a868 (bug 1296767)
Backed out changeset 5d70d87d2a8f (bug 1296767)
Backed out changeset 8219686be6a2 (bug 1296767)
Backed out changeset 0a989b0cea67 (bug 1296767)
Backed out changeset 9f59a0b75c1f (bug 1296767)

MozReview-Commit-ID: 2XBNsd8JrZL

--HG--
extra : amend_source : 1afafaa8127fcebac31ce1d7743dc16872fa0522
2017-01-26 11:16:12 -08:00

475 lines
16 KiB
JavaScript

/* 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.
*/
this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
"resource://gre/modules/WebChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsStorageManagerCanStoreField",
"resource://gre/modules/FxAccountsStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
"resource://services-sync/main.js");
const COMMAND_PROFILE_CHANGE = "profile:change";
const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
const COMMAND_LOGIN = "fxaccounts:login";
const COMMAND_LOGOUT = "fxaccounts:logout";
const COMMAND_DELETE = "fxaccounts:delete";
const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
/**
* 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) {
let details = { message: String(error), stack: null };
// Adapted from Console.jsm.
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
*/
this.FxAccountsWebChannel = function(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.
this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
this._setupChannel();
};
this.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) {
let command = message.command;
let data = message.data;
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_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(sendingContext.browser, data.entryPoint);
break;
case COMMAND_CHANGE_PASSWORD:
this._helpers.changePassword(data).catch(error =>
this._sendError(error, message, sendingContext));
break;
default:
log.warn("Unrecognized FxAccountsWebChannel command", command);
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.browser {browser}
* The <browser> object that captured the
* WebChannelMessageToChrome.
* @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 WebChannel(this._webChannelId, this._webChannelOrigin);
this._channel.listen(listener);
log.debug("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
}
};
this.FxAccountsWebChannelHelpers = function(options) {
options = options || {};
this._fxAccounts = options.fxAccounts || fxAccounts;
};
this.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);
},
/**
* New users are asked in the content server whether they want to
* customize which data should be synced. The user is only shown
* the dialog listing the possible data types upon verification.
*
* Save a bit into prefs that is read on verification to see whether
* to show the list of data types that can be saved.
*/
setShowCustomizeSyncPref(showCustomizeSyncPref) {
Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, showCustomizeSyncPref);
},
getShowCustomizeSyncPref() {
return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
},
/**
* stores sync login info it in the fxaccounts service
*
* @param accountData the user's account data and credentials
*/
login(accountData) {
if (accountData.customizeSync) {
this.setShowCustomizeSyncPref(true);
delete accountData.customizeSync;
}
if (accountData.declinedSyncEngines) {
let declinedSyncEngines = accountData.declinedSyncEngines;
log.debug("Received declined engines", declinedSyncEngines);
Weave.Service.engineManager.setDeclined(declinedSyncEngines);
declinedSyncEngines.forEach(engine => {
Services.prefs.setBoolPref("services.sync.engine." + engine, false);
});
// if we got declinedSyncEngines that means we do not need to show the customize screen.
this.setShowCustomizeSyncPref(false);
delete accountData.declinedSyncEngines;
}
// 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.
this.setPreviousAccountNameHashPref(accountData.email);
// A sync-specific hack - we want to ensure sync has been initialized
// before we set the signed-in user.
let xps = Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
return xps.whenLoaded().then(() => {
return this._fxAccounts.setSignedInUser(accountData);
});
},
/**
* logout the fxaccounts service
*
* @param the uid of the account which have been logged out
*/
logout(uid) {
return fxAccounts.getSignedInUser().then(userData => {
if (userData.uid === uid) {
// true argument is `localOnly`, because server-side stuff
// has already been taken care of by the content server
return fxAccounts.signOut(true);
}
});
},
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 = {
deviceId: null
};
for (let name of Object.keys(credentials)) {
if (name == "email" || name == "uid" || FxAccountsStorageManagerCanStoreField(name)) {
newCredentials[name] = credentials[name];
} else {
log.info("changePassword ignoring unsupported field", name);
}
}
return this._fxAccounts.updateUserAccountData(newCredentials)
.then(() => this._fxAccounts.updateDeviceRegistration());
},
/**
* Get the hash of account name of the previously signed in account
*/
getPreviousAccountNameHashPref() {
try {
return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
} 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) {
let string = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
string.data = this.sha256(acctName);
Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
},
/**
* Given a string, returns the SHA265 hash in base64
*/
sha256(str) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
// Data is an array of bytes.
let data = converter.convertToByteArray(str, {});
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
hasher.update(data, data.length);
return hasher.finish(true);
},
/**
* 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(uri);
},
/**
* 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 != this.sha256(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], 1);
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.
var targetWindow = typeof window === "undefined" ? null : window;
let pressed = Services.prompt.confirmEx(targetWindow, 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.
this.EnsureFxAccountsWebChannel = function() {
let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
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 this.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);
}
}
}