Bug 1191064 - Part 1: Add Fennec version of FxAccountsWebChannel. r=markh
authorNick Alexander <nalexander@mozilla.com>
Tue, 15 Sep 2015 15:54:29 -0400
changeset 296878 6c96e106f24d8c96e2c994a25fd9599784a7789c
parent 296877 9ead51e40e481f3ae5b3f9ca8d49a4034fe22a17
child 296879 e80ad43fbb4971cb35874bb86767c59ad8f24ea6
push id962
push userjlund@mozilla.com
push dateFri, 04 Dec 2015 23:28:54 +0000
treeherdermozilla-release@23a2d286e80f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1191064
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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 https://github.com/mozilla/fxa-content-server/blob/2a78a14dafe396ce2bfb1572ad418f14cfab1355/app/scripts/models/auth_brokers/fx-desktop-v2.js#L24 via https://github.com/mozilla/fxa-content-server/blob/2a78a14dafe396ce2bfb1572ad418f14cfab1355/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.
mobile/android/base/AccountsHelper.java
mobile/android/chrome/content/browser.js
mobile/android/modules/FxAccountsWebChannel.jsm
mobile/android/modules/moz.build
--- a/mobile/android/base/AccountsHelper.java
+++ b/mobile/android/base/AccountsHelper.java
@@ -17,17 +17,16 @@ import org.mozilla.gecko.fxa.authenticat
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 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;
 
 /**
  * Helper class to manage Android Accounts corresponding to Firefox Accounts.
  */
 public class AccountsHelper implements NativeEventListener {
@@ -104,37 +103,48 @@ public class AccountsHelper implements N
                 callback.sendSuccess(fxAccount != null);
             }
 
         } else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) {
             try {
                 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;
                 }
 
                 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"));
                 final byte[] keyFetchToken = Utils.hex2Byte(json.getString("keyFetchToken"));
                 final State state = new Engaged(email, uid, verified, unwrapkB, sessionToken, keyFetchToken);
 
                 final AndroidFxAccount fxAccount = new AndroidFxAccount(mContext, account);
                 fxAccount.setState(state);
 
                 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());
                     return;
                 }
             }
 
         } else if ("Accounts:Create".equals(event)) {
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -541,16 +541,26 @@ var BrowserApp = {
       let mm = window.getGroupMessageManager("browsers");
       mm.loadFrameScript("chrome://browser/content/content.js", true);
     });
 
     if (AppConstants.ACCESSIBILITY) {
       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" });
 
     this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
       BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
 
       InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
       InitLater(() => Cu.import("resource://gre/modules/Payment.jsm"));
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/FxAccountsWebChannel.jsm
@@ -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,
+    });
+  }
+};
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -6,16 +6,17 @@
 
 EXTRA_JS_MODULES += [
     'Accounts.jsm',
     'AndroidLog.jsm',
     'ContactService.jsm',
     'dbg-browser-actors.js',
     'DelayedInit.jsm',
     'DownloadNotifications.jsm',
+    'FxAccountsWebChannel.jsm',
     'HelperApps.jsm',
     'Home.jsm',
     'HomeProvider.jsm',
     'JavaAddonManager.jsm',
     'JNI.jsm',
     'LightweightThemeConsumer.jsm',
     'MediaPlayerApp.jsm',
     'Messaging.jsm',