summaryrefslogtreecommitdiffstats
path: root/mobile/android/chrome/content/aboutAccounts.js
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/chrome/content/aboutAccounts.js')
-rw-r--r--mobile/android/chrome/content/aboutAccounts.js351
1 files changed, 351 insertions, 0 deletions
diff --git a/mobile/android/chrome/content/aboutAccounts.js b/mobile/android/chrome/content/aboutAccounts.js
new file mode 100644
index 000000000..4801a76a1
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAccounts.js
@@ -0,0 +1,351 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+
+/**
+ * Wrap a remote fxa-content-server.
+ *
+ * An about:accounts tab loads and displays an fxa-content-server page,
+ * depending on the current Android Account status and an optional 'action'
+ * parameter.
+ *
+ * We show a spinner while the remote iframe is loading. We expect the
+ * WebChannel message listening to the fxa-content-server to send this tab's
+ * <browser>'s messageManager a LOADED message when the remote iframe provides
+ * the WebChannel LOADED message. See the messageManager registration and the
+ * |loadedDeferred| promise. This loosely couples the WebChannel implementation
+ * and about:accounts! (We need this coupling in order to distinguish
+ * WebChannel LOADED messages produced by multiple about:accounts tabs.)
+ *
+ * We capture error conditions by accessing the inner nsIWebNavigation of the
+ * iframe directly.
+ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
+Cu.import("resource://gre/modules/PromiseUtils.jsm"); /*global PromiseUtils */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+
+const ACTION_URL_PARAM = "action";
+
+const COMMAND_LOADED = "fxaccounts:loaded";
+
+const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+// Shows the toplevel element with |id| to be shown - all other top-level
+// elements are hidden.
+// If |id| is 'spinner', then 'remote' is also shown, with opacity 0.
+function show(id) {
+ let allTop = document.querySelectorAll(".toplevel");
+ for (let elt of allTop) {
+ if (elt.getAttribute("id") == id) {
+ elt.style.display = 'block';
+ } else {
+ elt.style.display = 'none';
+ }
+ }
+ if (id == 'spinner') {
+ document.getElementById('remote').style.display = 'block';
+ document.getElementById('remote').style.opacity = 0;
+ }
+}
+
+// Each time we try to load the remote <iframe>, loadedDeferred is replaced. It
+// is resolved by a LOADED message, and rejected by a failure to load.
+var loadedDeferred = null;
+
+// We have a new load starting. Replace the existing promise with a new one,
+// and queue up the transition to remote content.
+function deferTransitionToRemoteAfterLoaded() {
+ log.d('Waiting for LOADED message.');
+
+ loadedDeferred = PromiseUtils.defer();
+ loadedDeferred.promise.then(() => {
+ log.d('Got LOADED message!');
+ document.getElementById("remote").style.opacity = 0;
+ show("remote");
+ document.getElementById("remote").style.opacity = 1;
+ })
+ .catch((e) => {
+ log.w('Did not get LOADED message: ' + e.toString());
+ });
+}
+
+function handleLoadedMessage(message) {
+ loadedDeferred.resolve();
+};
+
+var wrapper = {
+ iframe: null,
+
+ url: null,
+
+ init: function (url) {
+ this.url = url;
+ deferTransitionToRemoteAfterLoaded();
+
+ let iframe = document.getElementById("remote");
+ this.iframe = iframe;
+ this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner);
+ let docShell = this.iframe.frameLoader.docShell;
+ docShell.QueryInterface(Ci.nsIWebProgress);
+ docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+
+ // Set the iframe's location with loadURI/LOAD_FLAGS_BYPASS_HISTORY to
+ // avoid having a new history entry being added.
+ let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ retry: function () {
+ deferTransitionToRemoteAfterLoaded();
+
+ let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null);
+ },
+
+ iframeListener: {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus) {
+ let failure = false;
+
+ // Captive portals sometimes redirect users
+ if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) {
+ failure = true;
+ } else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
+ if (aRequest instanceof Ci.nsIHttpChannel) {
+ try {
+ failure = aRequest.responseStatus != 200;
+ } catch (e) {
+ failure = aStatus != Components.results.NS_OK;
+ }
+ }
+ }
+
+ // Calling cancel() will raise some OnStateChange notifications by itself,
+ // so avoid doing that more than once
+ if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ // Since after a promise is fulfilled, subsequent fulfillments are
+ // treated as no-ops, we don't care that we might see multiple failures
+ // due to multiple listener callbacks. (It's not easy to extract this
+ // from the Promises spec, but it is widely quoted. Start with
+ // http://stackoverflow.com/a/18218542.)
+ loadedDeferred.reject(new Error("Failed in onStateChange!"));
+ show("networkError");
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ // As above, we're not concerned by multiple listener callbacks.
+ loadedDeferred.reject(new Error("Failed in onLocationChange!"));
+ show("networkError");
+ }
+ },
+
+ onProgressChange: function() {},
+ onStatusChange: function() {},
+ onSecurityChange: function() {},
+ },
+};
+
+
+function retry() {
+ log.i("Retrying.");
+ show("spinner");
+ wrapper.retry();
+}
+
+function openPrefs() {
+ log.i("Opening Sync preferences.");
+ // If an Android Account exists, this will open the Status Activity.
+ // Otherwise, it will begin the Get Started flow. This should only be shown
+ // when an Account actually exists.
+ Accounts.launchSetup();
+}
+
+function getURLForAction(action, urlParams) {
+ let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+ url = url + (url.endsWith("/") ? "" : "/") + action;
+ const CONTEXT = "fx_fennec_v1";
+ // The only service managed by Fennec, to date, is Firefox Sync.
+ const SERVICE = "sync";
+ urlParams = urlParams || new URLSearchParams("");
+ urlParams.set('service', SERVICE);
+ urlParams.set('context', CONTEXT);
+ // Ideally we'd just merge urlParams with new URL(url).searchParams, but our
+ // URLSearchParams implementation doesn't support iteration (bug 1085284).
+ let urlParamStr = urlParams.toString();
+ if (urlParamStr) {
+ url += (url.includes("?") ? "&" : "?") + urlParamStr;
+ }
+ return url;
+}
+
+function updateDisplayedEmail(user) {
+ let emailDiv = document.getElementById("email");
+ if (emailDiv && user) {
+ emailDiv.textContent = user.email;
+ }
+}
+
+function init() {
+ // Test for restrictions before getFirefoxAccount(), since that will fail if
+ // we are restricted.
+ if (!ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
+ // It's better to log and show an error message than to invite user
+ // confusion by removing about:accounts entirely. That is, if the user is
+ // restricted, this way they'll discover as much and may be able to get
+ // out of their restricted profile. If we remove about:accounts entirely,
+ // it will look like Fennec is buggy, and the user will be very confused.
+ log.e("This profile cannot connect to Firefox Accounts: showing restricted error.");
+ show("restrictedError");
+ return;
+ }
+
+ Accounts.getFirefoxAccount().then(user => {
+ // It's possible for the window to start closing before getting the user
+ // completes. Tests in particular can cause this.
+ if (window.closed) {
+ return;
+ }
+
+ updateDisplayedEmail(user);
+
+ // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
+ // searchParams is empty.
+ let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
+ let action = urlParams.get(ACTION_URL_PARAM);
+ urlParams.delete(ACTION_URL_PARAM);
+
+ switch (action) {
+ case "signup":
+ if (user) {
+ // Asking to sign-up when already signed in just shows prefs.
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "signin":
+ if (user) {
+ // Asking to sign-in when already signed in just shows prefs.
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signin", urlParams));
+ }
+ break;
+ case "force_auth":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("force_auth", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "manage":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("settings", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ case "avatar":
+ if (user) {
+ show("spinner");
+ urlParams.set("email", user.email); // In future, pin using the UID.
+ wrapper.init(getURLForAction("settings/avatar/change", urlParams));
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ default:
+ // Unrecognized or no action specified.
+ if (action) {
+ log.w("Ignoring unrecognized action: " + action);
+ }
+ if (user) {
+ show("prefs");
+ } else {
+ show("spinner");
+ wrapper.init(getURLForAction("signup", urlParams));
+ }
+ break;
+ }
+ }).catch(e => {
+ log.e("Failed to get the signed in user: " + e.toString());
+ });
+}
+
+document.addEventListener("DOMContentLoaded", function onload() {
+ document.removeEventListener("DOMContentLoaded", onload, true);
+ init();
+ var buttonRetry = document.getElementById('buttonRetry');
+ buttonRetry.addEventListener('click', retry);
+
+ var buttonOpenPrefs = document.getElementById('buttonOpenPrefs');
+ buttonOpenPrefs.addEventListener('click', openPrefs);
+}, true);
+
+// This window is contained in a XUL <browser> element. Return the
+// messageManager of that <browser> element, or null.
+function getBrowserMessageManager() {
+ let browser = window
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow)
+ .BrowserApp
+ .getBrowserForDocument(document);
+ if (browser) {
+ return browser.messageManager;
+ }
+ return null;
+}
+
+// Add a single listener for 'loaded' messages from the iframe in this
+// <browser>. These 'loaded' messages are ferried from the WebChannel to just
+// this <browser>.
+var mm = getBrowserMessageManager();
+if (mm) {
+ mm.addMessageListener(COMMAND_LOADED, handleLoadedMessage);
+} else {
+ log.e('No messageManager, not listening for LOADED message!');
+}
+
+window.addEventListener("unload", function(event) {
+ try {
+ let mm = getBrowserMessageManager();
+ if (mm) {
+ mm.removeMessageListener(COMMAND_LOADED, handleLoadedMessage);
+ }
+ } catch (e) {
+ // This could fail if the page is being torn down, the tab is being
+ // destroyed, etc.
+ log.w('Not removing listener for LOADED message: ' + e.toString());
+ }
+});