1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/base/content/aboutaccounts/aboutaccounts.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,371 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.11 + 1.12 +Cu.import("resource://gre/modules/Services.jsm"); 1.13 +Cu.import("resource://gre/modules/FxAccounts.jsm"); 1.14 + 1.15 +let fxAccountsCommon = {}; 1.16 +Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); 1.17 + 1.18 +const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; 1.19 +const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog"; 1.20 + 1.21 +const OBSERVER_TOPICS = [ 1.22 + fxAccountsCommon.ONVERIFIED_NOTIFICATION, 1.23 + fxAccountsCommon.ONLOGOUT_NOTIFICATION, 1.24 +]; 1.25 + 1.26 +function log(msg) { 1.27 + //dump("FXA: " + msg + "\n"); 1.28 +}; 1.29 + 1.30 +function error(msg) { 1.31 + console.log("Firefox Account Error: " + msg + "\n"); 1.32 +}; 1.33 + 1.34 +function getPreviousAccountNameHash() { 1.35 + try { 1.36 + return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; 1.37 + } catch (_) { 1.38 + return ""; 1.39 + } 1.40 +} 1.41 + 1.42 +function setPreviousAccountNameHash(acctName) { 1.43 + let string = Cc["@mozilla.org/supports-string;1"] 1.44 + .createInstance(Ci.nsISupportsString); 1.45 + string.data = sha256(acctName); 1.46 + Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string); 1.47 +} 1.48 + 1.49 +function needRelinkWarning(acctName) { 1.50 + let prevAcctHash = getPreviousAccountNameHash(); 1.51 + return prevAcctHash && prevAcctHash != sha256(acctName); 1.52 +} 1.53 + 1.54 +// Given a string, returns the SHA265 hash in base64 1.55 +function sha256(str) { 1.56 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.57 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.58 + converter.charset = "UTF-8"; 1.59 + // Data is an array of bytes. 1.60 + let data = converter.convertToByteArray(str, {}); 1.61 + let hasher = Cc["@mozilla.org/security/hash;1"] 1.62 + .createInstance(Ci.nsICryptoHash); 1.63 + hasher.init(hasher.SHA256); 1.64 + hasher.update(data, data.length); 1.65 + 1.66 + return hasher.finish(true); 1.67 +} 1.68 + 1.69 +function promptForRelink(acctName) { 1.70 + let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); 1.71 + let continueLabel = sb.GetStringFromName("continue.label"); 1.72 + let title = sb.GetStringFromName("relinkVerify.title"); 1.73 + let description = sb.formatStringFromName("relinkVerify.description", 1.74 + [acctName], 1); 1.75 + let body = sb.GetStringFromName("relinkVerify.heading") + 1.76 + "\n\n" + description; 1.77 + let ps = Services.prompt; 1.78 + let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) + 1.79 + (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) + 1.80 + ps.BUTTON_POS_1_DEFAULT; 1.81 + let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags, 1.82 + continueLabel, null, null, null, 1.83 + {}); 1.84 + return pressed == 0; // 0 is the "continue" button 1.85 +} 1.86 + 1.87 +// If the last fxa account used for sync isn't this account, we display 1.88 +// a modal dialog checking they really really want to do this... 1.89 +// (This is sync-specific, so ideally would be in sync's identity module, 1.90 +// but it's a little more seamless to do here, and sync is currently the 1.91 +// only fxa consumer, so... 1.92 +function shouldAllowRelink(acctName) { 1.93 + return !needRelinkWarning(acctName) || promptForRelink(acctName); 1.94 +} 1.95 + 1.96 +let wrapper = { 1.97 + iframe: null, 1.98 + 1.99 + init: function (url=null) { 1.100 + let weave = Cc["@mozilla.org/weave/service;1"] 1.101 + .getService(Ci.nsISupports) 1.102 + .wrappedJSObject; 1.103 + 1.104 + // Don't show about:accounts with FxA disabled. 1.105 + if (!weave.fxAccountsEnabled) { 1.106 + document.body.remove(); 1.107 + return; 1.108 + } 1.109 + 1.110 + let iframe = document.getElementById("remote"); 1.111 + this.iframe = iframe; 1.112 + iframe.addEventListener("load", this); 1.113 + 1.114 + try { 1.115 + iframe.src = url || fxAccounts.getAccountsSignUpURI(); 1.116 + } catch (e) { 1.117 + error("Couldn't init Firefox Account wrapper: " + e.message); 1.118 + } 1.119 + }, 1.120 + 1.121 + handleEvent: function (evt) { 1.122 + switch (evt.type) { 1.123 + case "load": 1.124 + this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this); 1.125 + this.iframe.removeEventListener("load", this); 1.126 + break; 1.127 + case "FirefoxAccountsCommand": 1.128 + this.handleRemoteCommand(evt); 1.129 + break; 1.130 + } 1.131 + }, 1.132 + 1.133 + /** 1.134 + * onLogin handler receives user credentials from the jelly after a 1.135 + * sucessful login and stores it in the fxaccounts service 1.136 + * 1.137 + * @param accountData the user's account data and credentials 1.138 + */ 1.139 + onLogin: function (accountData) { 1.140 + log("Received: 'login'. Data:" + JSON.stringify(accountData)); 1.141 + 1.142 + if (accountData.customizeSync) { 1.143 + Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true); 1.144 + delete accountData.customizeSync; 1.145 + } 1.146 + 1.147 + // We need to confirm a relink - see shouldAllowRelink for more 1.148 + let newAccountEmail = accountData.email; 1.149 + // The hosted code may have already checked for the relink situation 1.150 + // by sending the can_link_account command. If it did, then 1.151 + // it will indicate we don't need to ask twice. 1.152 + if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) { 1.153 + // we need to tell the page we successfully received the message, but 1.154 + // then bail without telling fxAccounts 1.155 + this.injectData("message", { status: "login" }); 1.156 + // and re-init the page by navigating to about:accounts 1.157 + window.location = "about:accounts"; 1.158 + return; 1.159 + } 1.160 + delete accountData.verifiedCanLinkAccount; 1.161 + 1.162 + // Remember who it was so we can log out next time. 1.163 + setPreviousAccountNameHash(newAccountEmail); 1.164 + 1.165 + // A sync-specific hack - we want to ensure sync has been initialized 1.166 + // before we set the signed-in user. 1.167 + let xps = Cc["@mozilla.org/weave/service;1"] 1.168 + .getService(Ci.nsISupports) 1.169 + .wrappedJSObject; 1.170 + xps.whenLoaded().then(() => { 1.171 + return fxAccounts.setSignedInUser(accountData); 1.172 + }).then(() => { 1.173 + // If the user data is verified, we want it to immediately look like 1.174 + // they are signed in without waiting for messages to bounce around. 1.175 + if (accountData.verified) { 1.176 + showManage(); 1.177 + } 1.178 + this.injectData("message", { status: "login" }); 1.179 + // until we sort out a better UX, just leave the jelly page in place. 1.180 + // If the account email is not yet verified, it will tell the user to 1.181 + // go check their email, but then it will *not* change state after 1.182 + // the verification completes (the browser will begin syncing, but 1.183 + // won't notify the user). If the email has already been verified, 1.184 + // the jelly will say "Welcome! You are successfully signed in as 1.185 + // EMAIL", but it won't then say "syncing started". 1.186 + }, (err) => this.injectData("message", { status: "error", error: err }) 1.187 + ); 1.188 + }, 1.189 + 1.190 + onCanLinkAccount: function(accountData) { 1.191 + // We need to confirm a relink - see shouldAllowRelink for more 1.192 + let ok = shouldAllowRelink(accountData.email); 1.193 + this.injectData("message", { status: "can_link_account", data: { ok: ok } }); 1.194 + }, 1.195 + 1.196 + /** 1.197 + * onSessionStatus sends the currently signed in user's credentials 1.198 + * to the jelly. 1.199 + */ 1.200 + onSessionStatus: function () { 1.201 + log("Received: 'session_status'."); 1.202 + 1.203 + fxAccounts.getSignedInUser().then( 1.204 + (accountData) => this.injectData("message", { status: "session_status", data: accountData }), 1.205 + (err) => this.injectData("message", { status: "error", error: err }) 1.206 + ); 1.207 + }, 1.208 + 1.209 + /** 1.210 + * onSignOut handler erases the current user's session from the fxaccounts service 1.211 + */ 1.212 + onSignOut: function () { 1.213 + log("Received: 'sign_out'."); 1.214 + 1.215 + fxAccounts.signOut().then( 1.216 + () => this.injectData("message", { status: "sign_out" }), 1.217 + (err) => this.injectData("message", { status: "error", error: err }) 1.218 + ); 1.219 + }, 1.220 + 1.221 + handleRemoteCommand: function (evt) { 1.222 + log('command: ' + evt.detail.command); 1.223 + let data = evt.detail.data; 1.224 + 1.225 + switch (evt.detail.command) { 1.226 + case "login": 1.227 + this.onLogin(data); 1.228 + break; 1.229 + case "can_link_account": 1.230 + this.onCanLinkAccount(data); 1.231 + break; 1.232 + case "session_status": 1.233 + this.onSessionStatus(data); 1.234 + break; 1.235 + case "sign_out": 1.236 + this.onSignOut(data); 1.237 + break; 1.238 + default: 1.239 + log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command."); 1.240 + break; 1.241 + } 1.242 + }, 1.243 + 1.244 + injectData: function (type, content) { 1.245 + let authUrl; 1.246 + try { 1.247 + authUrl = fxAccounts.getAccountsSignUpURI(); 1.248 + } catch (e) { 1.249 + error("Couldn't inject data: " + e.message); 1.250 + return; 1.251 + } 1.252 + let data = { 1.253 + type: type, 1.254 + content: content 1.255 + }; 1.256 + this.iframe.contentWindow.postMessage(data, authUrl); 1.257 + }, 1.258 +}; 1.259 + 1.260 + 1.261 +// Button onclick handlers 1.262 +function handleOldSync() { 1.263 + let chromeWin = window 1.264 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.265 + .getInterface(Ci.nsIWebNavigation) 1.266 + .QueryInterface(Ci.nsIDocShellTreeItem) 1.267 + .rootTreeItem 1.268 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.269 + .getInterface(Ci.nsIDOMWindow) 1.270 + .QueryInterface(Ci.nsIDOMChromeWindow); 1.271 + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync"; 1.272 + chromeWin.switchToTabHavingURI(url, true); 1.273 +} 1.274 + 1.275 +function getStarted() { 1.276 + hide("intro"); 1.277 + hide("stage"); 1.278 + show("remote"); 1.279 +} 1.280 + 1.281 +function openPrefs() { 1.282 + window.openPreferences("paneSync"); 1.283 +} 1.284 + 1.285 +function init() { 1.286 + fxAccounts.getSignedInUser().then(user => { 1.287 + // tests in particular might cause the window to start closing before 1.288 + // getSignedInUser has returned. 1.289 + if (window.closed) { 1.290 + return; 1.291 + } 1.292 + if (window.location.href.contains("action=signin")) { 1.293 + if (user) { 1.294 + // asking to sign-in when already signed in just shows manage. 1.295 + showManage(); 1.296 + } else { 1.297 + show("remote"); 1.298 + wrapper.init(fxAccounts.getAccountsSignInURI()); 1.299 + } 1.300 + } else if (window.location.href.contains("action=signup")) { 1.301 + if (user) { 1.302 + // asking to sign-up when already signed in just shows manage. 1.303 + showManage(); 1.304 + } else { 1.305 + show("remote"); 1.306 + wrapper.init(); 1.307 + } 1.308 + } else if (window.location.href.contains("action=reauth")) { 1.309 + // ideally we would only show this when we know the user is in a 1.310 + // "must reauthenticate" state - but we don't. 1.311 + // As the email address will be included in the URL returned from 1.312 + // promiseAccountsForceSigninURI, just always show it. 1.313 + fxAccounts.promiseAccountsForceSigninURI().then(url => { 1.314 + show("remote"); 1.315 + wrapper.init(url); 1.316 + }); 1.317 + } else { 1.318 + // No action specified 1.319 + if (user) { 1.320 + showManage(); 1.321 + let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); 1.322 + document.title = sb.GetStringFromName("manage.pageTitle"); 1.323 + } else { 1.324 + show("stage"); 1.325 + show("intro"); 1.326 + // load the remote frame in the background 1.327 + wrapper.init(); 1.328 + } 1.329 + } 1.330 + }); 1.331 +} 1.332 + 1.333 +function show(id) { 1.334 + document.getElementById(id).style.display = 'block'; 1.335 +} 1.336 +function hide(id) { 1.337 + document.getElementById(id).style.display = 'none'; 1.338 +} 1.339 + 1.340 +function showManage() { 1.341 + show("stage"); 1.342 + show("manage"); 1.343 + hide("remote"); 1.344 + hide("intro"); 1.345 +} 1.346 + 1.347 +document.addEventListener("DOMContentLoaded", function onload() { 1.348 + document.removeEventListener("DOMContentLoaded", onload, true); 1.349 + init(); 1.350 +}, true); 1.351 + 1.352 +function initObservers() { 1.353 + function observe(subject, topic, data) { 1.354 + log("about:accounts observed " + topic); 1.355 + if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) { 1.356 + // All about:account windows get changed to action=signin on logout. 1.357 + window.location = "about:accounts?action=signin"; 1.358 + return; 1.359 + } 1.360 + // must be onverified - just about:accounts is loaded. 1.361 + window.location = "about:accounts"; 1.362 + } 1.363 + 1.364 + for (let topic of OBSERVER_TOPICS) { 1.365 + Services.obs.addObserver(observe, topic, false); 1.366 + } 1.367 + window.addEventListener("unload", function(event) { 1.368 + log("about:accounts unloading") 1.369 + for (let topic of OBSERVER_TOPICS) { 1.370 + Services.obs.removeObserver(observe, topic); 1.371 + } 1.372 + }); 1.373 +} 1.374 +initObservers();