michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/FxAccounts.jsm"); michael@0: michael@0: let fxAccountsCommon = {}; michael@0: Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); michael@0: michael@0: const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; michael@0: const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog"; michael@0: michael@0: const OBSERVER_TOPICS = [ michael@0: fxAccountsCommon.ONVERIFIED_NOTIFICATION, michael@0: fxAccountsCommon.ONLOGOUT_NOTIFICATION, michael@0: ]; michael@0: michael@0: function log(msg) { michael@0: //dump("FXA: " + msg + "\n"); michael@0: }; michael@0: michael@0: function error(msg) { michael@0: console.log("Firefox Account Error: " + msg + "\n"); michael@0: }; michael@0: michael@0: function getPreviousAccountNameHash() { michael@0: try { michael@0: return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; michael@0: } catch (_) { michael@0: return ""; michael@0: } michael@0: } michael@0: michael@0: function setPreviousAccountNameHash(acctName) { michael@0: let string = Cc["@mozilla.org/supports-string;1"] michael@0: .createInstance(Ci.nsISupportsString); michael@0: string.data = sha256(acctName); michael@0: Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string); michael@0: } michael@0: michael@0: function needRelinkWarning(acctName) { michael@0: let prevAcctHash = getPreviousAccountNameHash(); michael@0: return prevAcctHash && prevAcctHash != sha256(acctName); michael@0: } michael@0: michael@0: // Given a string, returns the SHA265 hash in base64 michael@0: function sha256(str) { michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: // Data is an array of bytes. michael@0: let data = converter.convertToByteArray(str, {}); michael@0: let hasher = Cc["@mozilla.org/security/hash;1"] michael@0: .createInstance(Ci.nsICryptoHash); michael@0: hasher.init(hasher.SHA256); michael@0: hasher.update(data, data.length); michael@0: michael@0: return hasher.finish(true); michael@0: } michael@0: michael@0: function promptForRelink(acctName) { michael@0: let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); michael@0: let continueLabel = sb.GetStringFromName("continue.label"); michael@0: let title = sb.GetStringFromName("relinkVerify.title"); michael@0: let description = sb.formatStringFromName("relinkVerify.description", michael@0: [acctName], 1); michael@0: let body = sb.GetStringFromName("relinkVerify.heading") + michael@0: "\n\n" + description; michael@0: let ps = Services.prompt; michael@0: let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) + michael@0: (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) + michael@0: ps.BUTTON_POS_1_DEFAULT; michael@0: let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags, michael@0: continueLabel, null, null, null, michael@0: {}); michael@0: return pressed == 0; // 0 is the "continue" button michael@0: } michael@0: michael@0: // If the last fxa account used for sync isn't this account, we display michael@0: // a modal dialog checking they really really want to do this... michael@0: // (This is sync-specific, so ideally would be in sync's identity module, michael@0: // but it's a little more seamless to do here, and sync is currently the michael@0: // only fxa consumer, so... michael@0: function shouldAllowRelink(acctName) { michael@0: return !needRelinkWarning(acctName) || promptForRelink(acctName); michael@0: } michael@0: michael@0: let wrapper = { michael@0: iframe: null, michael@0: michael@0: init: function (url=null) { michael@0: let weave = Cc["@mozilla.org/weave/service;1"] michael@0: .getService(Ci.nsISupports) michael@0: .wrappedJSObject; michael@0: michael@0: // Don't show about:accounts with FxA disabled. michael@0: if (!weave.fxAccountsEnabled) { michael@0: document.body.remove(); michael@0: return; michael@0: } michael@0: michael@0: let iframe = document.getElementById("remote"); michael@0: this.iframe = iframe; michael@0: iframe.addEventListener("load", this); michael@0: michael@0: try { michael@0: iframe.src = url || fxAccounts.getAccountsSignUpURI(); michael@0: } catch (e) { michael@0: error("Couldn't init Firefox Account wrapper: " + e.message); michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function (evt) { michael@0: switch (evt.type) { michael@0: case "load": michael@0: this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this); michael@0: this.iframe.removeEventListener("load", this); michael@0: break; michael@0: case "FirefoxAccountsCommand": michael@0: this.handleRemoteCommand(evt); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * onLogin handler receives user credentials from the jelly after a michael@0: * sucessful login and stores it in the fxaccounts service michael@0: * michael@0: * @param accountData the user's account data and credentials michael@0: */ michael@0: onLogin: function (accountData) { michael@0: log("Received: 'login'. Data:" + JSON.stringify(accountData)); michael@0: michael@0: if (accountData.customizeSync) { michael@0: Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true); michael@0: delete accountData.customizeSync; michael@0: } michael@0: michael@0: // We need to confirm a relink - see shouldAllowRelink for more michael@0: let newAccountEmail = accountData.email; michael@0: // The hosted code may have already checked for the relink situation michael@0: // by sending the can_link_account command. If it did, then michael@0: // it will indicate we don't need to ask twice. michael@0: if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) { michael@0: // we need to tell the page we successfully received the message, but michael@0: // then bail without telling fxAccounts michael@0: this.injectData("message", { status: "login" }); michael@0: // and re-init the page by navigating to about:accounts michael@0: window.location = "about:accounts"; michael@0: return; michael@0: } michael@0: delete accountData.verifiedCanLinkAccount; michael@0: michael@0: // Remember who it was so we can log out next time. michael@0: setPreviousAccountNameHash(newAccountEmail); michael@0: michael@0: // A sync-specific hack - we want to ensure sync has been initialized michael@0: // before we set the signed-in user. michael@0: let xps = Cc["@mozilla.org/weave/service;1"] michael@0: .getService(Ci.nsISupports) michael@0: .wrappedJSObject; michael@0: xps.whenLoaded().then(() => { michael@0: return fxAccounts.setSignedInUser(accountData); michael@0: }).then(() => { michael@0: // If the user data is verified, we want it to immediately look like michael@0: // they are signed in without waiting for messages to bounce around. michael@0: if (accountData.verified) { michael@0: showManage(); michael@0: } michael@0: this.injectData("message", { status: "login" }); michael@0: // until we sort out a better UX, just leave the jelly page in place. michael@0: // If the account email is not yet verified, it will tell the user to michael@0: // go check their email, but then it will *not* change state after michael@0: // the verification completes (the browser will begin syncing, but michael@0: // won't notify the user). If the email has already been verified, michael@0: // the jelly will say "Welcome! You are successfully signed in as michael@0: // EMAIL", but it won't then say "syncing started". michael@0: }, (err) => this.injectData("message", { status: "error", error: err }) michael@0: ); michael@0: }, michael@0: michael@0: onCanLinkAccount: function(accountData) { michael@0: // We need to confirm a relink - see shouldAllowRelink for more michael@0: let ok = shouldAllowRelink(accountData.email); michael@0: this.injectData("message", { status: "can_link_account", data: { ok: ok } }); michael@0: }, michael@0: michael@0: /** michael@0: * onSessionStatus sends the currently signed in user's credentials michael@0: * to the jelly. michael@0: */ michael@0: onSessionStatus: function () { michael@0: log("Received: 'session_status'."); michael@0: michael@0: fxAccounts.getSignedInUser().then( michael@0: (accountData) => this.injectData("message", { status: "session_status", data: accountData }), michael@0: (err) => this.injectData("message", { status: "error", error: err }) michael@0: ); michael@0: }, michael@0: michael@0: /** michael@0: * onSignOut handler erases the current user's session from the fxaccounts service michael@0: */ michael@0: onSignOut: function () { michael@0: log("Received: 'sign_out'."); michael@0: michael@0: fxAccounts.signOut().then( michael@0: () => this.injectData("message", { status: "sign_out" }), michael@0: (err) => this.injectData("message", { status: "error", error: err }) michael@0: ); michael@0: }, michael@0: michael@0: handleRemoteCommand: function (evt) { michael@0: log('command: ' + evt.detail.command); michael@0: let data = evt.detail.data; michael@0: michael@0: switch (evt.detail.command) { michael@0: case "login": michael@0: this.onLogin(data); michael@0: break; michael@0: case "can_link_account": michael@0: this.onCanLinkAccount(data); michael@0: break; michael@0: case "session_status": michael@0: this.onSessionStatus(data); michael@0: break; michael@0: case "sign_out": michael@0: this.onSignOut(data); michael@0: break; michael@0: default: michael@0: log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command."); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: injectData: function (type, content) { michael@0: let authUrl; michael@0: try { michael@0: authUrl = fxAccounts.getAccountsSignUpURI(); michael@0: } catch (e) { michael@0: error("Couldn't inject data: " + e.message); michael@0: return; michael@0: } michael@0: let data = { michael@0: type: type, michael@0: content: content michael@0: }; michael@0: this.iframe.contentWindow.postMessage(data, authUrl); michael@0: }, michael@0: }; michael@0: michael@0: michael@0: // Button onclick handlers michael@0: function handleOldSync() { michael@0: let chromeWin = window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShellTreeItem) michael@0: .rootTreeItem michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow) michael@0: .QueryInterface(Ci.nsIDOMChromeWindow); michael@0: let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync"; michael@0: chromeWin.switchToTabHavingURI(url, true); michael@0: } michael@0: michael@0: function getStarted() { michael@0: hide("intro"); michael@0: hide("stage"); michael@0: show("remote"); michael@0: } michael@0: michael@0: function openPrefs() { michael@0: window.openPreferences("paneSync"); michael@0: } michael@0: michael@0: function init() { michael@0: fxAccounts.getSignedInUser().then(user => { michael@0: // tests in particular might cause the window to start closing before michael@0: // getSignedInUser has returned. michael@0: if (window.closed) { michael@0: return; michael@0: } michael@0: if (window.location.href.contains("action=signin")) { michael@0: if (user) { michael@0: // asking to sign-in when already signed in just shows manage. michael@0: showManage(); michael@0: } else { michael@0: show("remote"); michael@0: wrapper.init(fxAccounts.getAccountsSignInURI()); michael@0: } michael@0: } else if (window.location.href.contains("action=signup")) { michael@0: if (user) { michael@0: // asking to sign-up when already signed in just shows manage. michael@0: showManage(); michael@0: } else { michael@0: show("remote"); michael@0: wrapper.init(); michael@0: } michael@0: } else if (window.location.href.contains("action=reauth")) { michael@0: // ideally we would only show this when we know the user is in a michael@0: // "must reauthenticate" state - but we don't. michael@0: // As the email address will be included in the URL returned from michael@0: // promiseAccountsForceSigninURI, just always show it. michael@0: fxAccounts.promiseAccountsForceSigninURI().then(url => { michael@0: show("remote"); michael@0: wrapper.init(url); michael@0: }); michael@0: } else { michael@0: // No action specified michael@0: if (user) { michael@0: showManage(); michael@0: let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); michael@0: document.title = sb.GetStringFromName("manage.pageTitle"); michael@0: } else { michael@0: show("stage"); michael@0: show("intro"); michael@0: // load the remote frame in the background michael@0: wrapper.init(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: function show(id) { michael@0: document.getElementById(id).style.display = 'block'; michael@0: } michael@0: function hide(id) { michael@0: document.getElementById(id).style.display = 'none'; michael@0: } michael@0: michael@0: function showManage() { michael@0: show("stage"); michael@0: show("manage"); michael@0: hide("remote"); michael@0: hide("intro"); michael@0: } michael@0: michael@0: document.addEventListener("DOMContentLoaded", function onload() { michael@0: document.removeEventListener("DOMContentLoaded", onload, true); michael@0: init(); michael@0: }, true); michael@0: michael@0: function initObservers() { michael@0: function observe(subject, topic, data) { michael@0: log("about:accounts observed " + topic); michael@0: if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) { michael@0: // All about:account windows get changed to action=signin on logout. michael@0: window.location = "about:accounts?action=signin"; michael@0: return; michael@0: } michael@0: // must be onverified - just about:accounts is loaded. michael@0: window.location = "about:accounts"; michael@0: } michael@0: michael@0: for (let topic of OBSERVER_TOPICS) { michael@0: Services.obs.addObserver(observe, topic, false); michael@0: } michael@0: window.addEventListener("unload", function(event) { michael@0: log("about:accounts unloading") michael@0: for (let topic of OBSERVER_TOPICS) { michael@0: Services.obs.removeObserver(observe, topic); michael@0: } michael@0: }); michael@0: } michael@0: initObservers();