Bug 1191064 - Part 1: Add Fennec version of FxAccountsWebChannel. r=markh

This ticket does the following things:

* register early.  If the first page that Gecko loads is
  about:accounts, the channel needs to be in place.  If we delay this,
  we can and do miss content server messages.

* listen to the following messages:

  CAN_LINK_ACCOUNT: 'fxaccounts:can_link_account'
  CHANGE_PASSWORD: 'fxaccounts:change_password'
  DELETE_ACCOUNT: 'fxaccounts:delete_account'
  LOADED: 'fxaccounts:loaded'
  LOGIN: 'fxaccounts:login'

The list of messages is from
2a78a14daf/app/scripts/models/auth_brokers/fx-desktop-v2.js (L24)
via
2a78a14daf/app/scripts/models/auth_brokers/fx-fennec-v1.js

This patch implements only LOADED, LOGIN, and CHANGE_PASSWORD.  The
messages have the following behaviour:

A LOADED message is ferried to the individual XUL <browser> element it
originated from.  In general, WebChannel is a global listener: it does
not matter where a message originates.  We want to have fine-grained
control over when an embedding <iframe> is displayed (as opposed to
loaded, in the Gecko sense of loaded).  The fxa-content-server
participates in this exchange via the LOADED message; we complete the
loop by specially handling LOADED.

A LOGIN or CHANGE_PASSWORD message either creates a new Android
Account in the Engaged state, or moves an existing Android Account to
the Engaged state.  An Android sync is not yet requested -- we'll
arrange that from the Java side.
This commit is contained in:
Nick Alexander 2015-09-15 15:54:29 -04:00
parent 9720b8dd9b
commit d6b4d9e981
4 changed files with 259 additions and 3 deletions

View File

@ -22,7 +22,6 @@ import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import java.io.IOError;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
@ -109,7 +108,7 @@ public class AccountsHelper implements NativeEventListener {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
if (account == null) {
if (callback != null) {
callback.sendError("Could not update Firefox Account since non exists");
callback.sendError("Could not update Firefox Account since none exists");
}
return;
}
@ -117,6 +116,17 @@ public class AccountsHelper implements NativeEventListener {
final NativeJSObject json = message.getObject("json");
final String email = json.getString("email");
final String uid = json.getString("uid");
// Protect against cross-connecting accounts.
if (account.name == null || !account.name.equals(email)) {
final String errorMessage = "Cannot update Firefox Account from JSON: datum has different email address!";
Log.e(LOGTAG, errorMessage);
if (callback != null) {
callback.sendError(errorMessage);
}
return;
}
final boolean verified = json.optBoolean("verified", false);
final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
@ -129,7 +139,7 @@ public class AccountsHelper implements NativeEventListener {
if (callback != null) {
callback.sendSuccess(true);
}
} catch (Exception e) {
} catch (NativeJSObject.InvalidPropertyException e) {
Log.w(LOGTAG, "Got exception updating Firefox Account from JSON; ignoring.", e);
if (callback != null) {
callback.sendError("Could not update Firefox Account from JSON: " + e.toString());

View File

@ -546,6 +546,16 @@ var BrowserApp = {
InitLater(() => AccessFu.attach(window), window, "AccessFu");
}
if (!AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
// We can't delay registering WebChannel listeners: if the first page is
// about:accounts, which can happen when starting the Firefox Account flow
// from the first run experience, or via the Firefox Account Status
// Activity, we can and do miss messages from the fxa-content-server.
console.log("browser.js: loading Firefox Accounts WebChannel");
Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
EnsureFxAccountsWebChannel();
}
// Notify Java that Gecko has loaded.
Messaging.sendRequest({ type: "Gecko:Ready" });

View File

@ -0,0 +1,235 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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.
*
* Use the WebChannel component to receive messages about account
* state changes.
*/
this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */
Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
Cu.import("resource://gre/modules/Notifications.jsm"); /*global Notifications */
Cu.import("resource://gre/modules/Prompt.jsm"); /*global Prompt */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
const WEBCHANNEL_ID = "account_updates";
const COMMAND_LOADED = "fxaccounts:loaded";
const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
const COMMAND_LOGIN = "fxaccounts:login";
const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
/**
* 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;
this._setupChannel();
};
this.FxAccountsWebChannel.prototype = {
/**
* WebChannel that is used to communicate with content page
*/
_channel: 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, null, null);
this._registerChannel();
} catch (e) {
log.e(e);
throw e;
}
},
/**
* 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) {
let command = message.command;
let data = message.data;
log.d("FxAccountsWebChannel message received, command: " + command);
// Respond to the message with true or false.
let respond = (data) => {
let response = {
command: command,
messageId: message.messageId,
data: data
};
log.d("Sending response to command: " + command);
this._channel.send(response, sendingContext);
};
switch (command) {
case COMMAND_LOADED:
let mm = sendingContext.browser.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
mm.sendAsyncMessage(COMMAND_LOADED);
break;
case COMMAND_CAN_LINK_ACCOUNT:
// Temporarily accept any login.
respond({ ok: true });
break;
case COMMAND_LOGIN:
// Either create a new Android Account or re-connect an existing
// Android Account here. There's not much to be done if we don't
// succeed or get an error.
Accounts.getFirefoxAccount().then(account => {
if (!account) {
return Accounts.createFirefoxAccountFromJSON(data).then(success => {
if (!success) {
throw new Error("Could not create Firefox Account!");
}
return success;
});
} else {
return Accounts.updateFirefoxAccountFromJSON(data).then(success => {
if (!success) {
throw new Error("Could not update Firefox Account!");
}
return success;
});
}
})
.then(success => {
if (!success) {
throw new Error("Could not create or update Firefox Account!");
}
log.i("Created or updated Firefox Account.");
})
.catch(e => {
log.e(e.toString());
});
break;
case COMMAND_CHANGE_PASSWORD:
// Only update an existing Android Account.
Accounts.getFirefoxAccount().then(account => {
if (!account) {
throw new Error("Can't change password of non-existent Firefox Account!");
}
return Accounts.updateFirefoxAccountFromJSON(data);
})
.then(success => {
if (!success) {
throw new Error("Could not change Firefox Account password!");
}
log.i("Changed Firefox Account password.");
})
.catch(e => {
log.e(e.toString());
});
break;
default:
log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
break;
}
}
};
this._channelCallback = listener;
this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
this._channel.listen(listener);
log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
}
};
let 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 and
// allowing multiple channels would cause such notifications to be sent multiple
// times.
this.EnsureFxAccountsWebChannel = function() {
if (!singleton) {
let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
// The FxAccountsWebChannel listens for events and updates the Java layer.
singleton = new this.FxAccountsWebChannel({
content_uri: contentUri,
channel_id: WEBCHANNEL_ID,
});
}
};

View File

@ -11,6 +11,7 @@ EXTRA_JS_MODULES += [
'dbg-browser-actors.js',
'DelayedInit.jsm',
'DownloadNotifications.jsm',
'FxAccountsWebChannel.jsm',
'HelperApps.jsm',
'Home.jsm',
'HomeProvider.jsm',