Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
9 Cu.import("resource://gre/modules/Services.jsm");
10 Cu.import("resource://gre/modules/FxAccounts.jsm");
12 let fxAccountsCommon = {};
13 Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
15 const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
16 const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog";
18 const OBSERVER_TOPICS = [
19 fxAccountsCommon.ONVERIFIED_NOTIFICATION,
20 fxAccountsCommon.ONLOGOUT_NOTIFICATION,
21 ];
23 function log(msg) {
24 //dump("FXA: " + msg + "\n");
25 };
27 function error(msg) {
28 console.log("Firefox Account Error: " + msg + "\n");
29 };
31 function getPreviousAccountNameHash() {
32 try {
33 return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
34 } catch (_) {
35 return "";
36 }
37 }
39 function setPreviousAccountNameHash(acctName) {
40 let string = Cc["@mozilla.org/supports-string;1"]
41 .createInstance(Ci.nsISupportsString);
42 string.data = sha256(acctName);
43 Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
44 }
46 function needRelinkWarning(acctName) {
47 let prevAcctHash = getPreviousAccountNameHash();
48 return prevAcctHash && prevAcctHash != sha256(acctName);
49 }
51 // Given a string, returns the SHA265 hash in base64
52 function sha256(str) {
53 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
54 .createInstance(Ci.nsIScriptableUnicodeConverter);
55 converter.charset = "UTF-8";
56 // Data is an array of bytes.
57 let data = converter.convertToByteArray(str, {});
58 let hasher = Cc["@mozilla.org/security/hash;1"]
59 .createInstance(Ci.nsICryptoHash);
60 hasher.init(hasher.SHA256);
61 hasher.update(data, data.length);
63 return hasher.finish(true);
64 }
66 function promptForRelink(acctName) {
67 let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
68 let continueLabel = sb.GetStringFromName("continue.label");
69 let title = sb.GetStringFromName("relinkVerify.title");
70 let description = sb.formatStringFromName("relinkVerify.description",
71 [acctName], 1);
72 let body = sb.GetStringFromName("relinkVerify.heading") +
73 "\n\n" + description;
74 let ps = Services.prompt;
75 let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) +
76 (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) +
77 ps.BUTTON_POS_1_DEFAULT;
78 let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags,
79 continueLabel, null, null, null,
80 {});
81 return pressed == 0; // 0 is the "continue" button
82 }
84 // If the last fxa account used for sync isn't this account, we display
85 // a modal dialog checking they really really want to do this...
86 // (This is sync-specific, so ideally would be in sync's identity module,
87 // but it's a little more seamless to do here, and sync is currently the
88 // only fxa consumer, so...
89 function shouldAllowRelink(acctName) {
90 return !needRelinkWarning(acctName) || promptForRelink(acctName);
91 }
93 let wrapper = {
94 iframe: null,
96 init: function (url=null) {
97 let weave = Cc["@mozilla.org/weave/service;1"]
98 .getService(Ci.nsISupports)
99 .wrappedJSObject;
101 // Don't show about:accounts with FxA disabled.
102 if (!weave.fxAccountsEnabled) {
103 document.body.remove();
104 return;
105 }
107 let iframe = document.getElementById("remote");
108 this.iframe = iframe;
109 iframe.addEventListener("load", this);
111 try {
112 iframe.src = url || fxAccounts.getAccountsSignUpURI();
113 } catch (e) {
114 error("Couldn't init Firefox Account wrapper: " + e.message);
115 }
116 },
118 handleEvent: function (evt) {
119 switch (evt.type) {
120 case "load":
121 this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this);
122 this.iframe.removeEventListener("load", this);
123 break;
124 case "FirefoxAccountsCommand":
125 this.handleRemoteCommand(evt);
126 break;
127 }
128 },
130 /**
131 * onLogin handler receives user credentials from the jelly after a
132 * sucessful login and stores it in the fxaccounts service
133 *
134 * @param accountData the user's account data and credentials
135 */
136 onLogin: function (accountData) {
137 log("Received: 'login'. Data:" + JSON.stringify(accountData));
139 if (accountData.customizeSync) {
140 Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true);
141 delete accountData.customizeSync;
142 }
144 // We need to confirm a relink - see shouldAllowRelink for more
145 let newAccountEmail = accountData.email;
146 // The hosted code may have already checked for the relink situation
147 // by sending the can_link_account command. If it did, then
148 // it will indicate we don't need to ask twice.
149 if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) {
150 // we need to tell the page we successfully received the message, but
151 // then bail without telling fxAccounts
152 this.injectData("message", { status: "login" });
153 // and re-init the page by navigating to about:accounts
154 window.location = "about:accounts";
155 return;
156 }
157 delete accountData.verifiedCanLinkAccount;
159 // Remember who it was so we can log out next time.
160 setPreviousAccountNameHash(newAccountEmail);
162 // A sync-specific hack - we want to ensure sync has been initialized
163 // before we set the signed-in user.
164 let xps = Cc["@mozilla.org/weave/service;1"]
165 .getService(Ci.nsISupports)
166 .wrappedJSObject;
167 xps.whenLoaded().then(() => {
168 return fxAccounts.setSignedInUser(accountData);
169 }).then(() => {
170 // If the user data is verified, we want it to immediately look like
171 // they are signed in without waiting for messages to bounce around.
172 if (accountData.verified) {
173 showManage();
174 }
175 this.injectData("message", { status: "login" });
176 // until we sort out a better UX, just leave the jelly page in place.
177 // If the account email is not yet verified, it will tell the user to
178 // go check their email, but then it will *not* change state after
179 // the verification completes (the browser will begin syncing, but
180 // won't notify the user). If the email has already been verified,
181 // the jelly will say "Welcome! You are successfully signed in as
182 // EMAIL", but it won't then say "syncing started".
183 }, (err) => this.injectData("message", { status: "error", error: err })
184 );
185 },
187 onCanLinkAccount: function(accountData) {
188 // We need to confirm a relink - see shouldAllowRelink for more
189 let ok = shouldAllowRelink(accountData.email);
190 this.injectData("message", { status: "can_link_account", data: { ok: ok } });
191 },
193 /**
194 * onSessionStatus sends the currently signed in user's credentials
195 * to the jelly.
196 */
197 onSessionStatus: function () {
198 log("Received: 'session_status'.");
200 fxAccounts.getSignedInUser().then(
201 (accountData) => this.injectData("message", { status: "session_status", data: accountData }),
202 (err) => this.injectData("message", { status: "error", error: err })
203 );
204 },
206 /**
207 * onSignOut handler erases the current user's session from the fxaccounts service
208 */
209 onSignOut: function () {
210 log("Received: 'sign_out'.");
212 fxAccounts.signOut().then(
213 () => this.injectData("message", { status: "sign_out" }),
214 (err) => this.injectData("message", { status: "error", error: err })
215 );
216 },
218 handleRemoteCommand: function (evt) {
219 log('command: ' + evt.detail.command);
220 let data = evt.detail.data;
222 switch (evt.detail.command) {
223 case "login":
224 this.onLogin(data);
225 break;
226 case "can_link_account":
227 this.onCanLinkAccount(data);
228 break;
229 case "session_status":
230 this.onSessionStatus(data);
231 break;
232 case "sign_out":
233 this.onSignOut(data);
234 break;
235 default:
236 log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
237 break;
238 }
239 },
241 injectData: function (type, content) {
242 let authUrl;
243 try {
244 authUrl = fxAccounts.getAccountsSignUpURI();
245 } catch (e) {
246 error("Couldn't inject data: " + e.message);
247 return;
248 }
249 let data = {
250 type: type,
251 content: content
252 };
253 this.iframe.contentWindow.postMessage(data, authUrl);
254 },
255 };
258 // Button onclick handlers
259 function handleOldSync() {
260 let chromeWin = window
261 .QueryInterface(Ci.nsIInterfaceRequestor)
262 .getInterface(Ci.nsIWebNavigation)
263 .QueryInterface(Ci.nsIDocShellTreeItem)
264 .rootTreeItem
265 .QueryInterface(Ci.nsIInterfaceRequestor)
266 .getInterface(Ci.nsIDOMWindow)
267 .QueryInterface(Ci.nsIDOMChromeWindow);
268 let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync";
269 chromeWin.switchToTabHavingURI(url, true);
270 }
272 function getStarted() {
273 hide("intro");
274 hide("stage");
275 show("remote");
276 }
278 function openPrefs() {
279 window.openPreferences("paneSync");
280 }
282 function init() {
283 fxAccounts.getSignedInUser().then(user => {
284 // tests in particular might cause the window to start closing before
285 // getSignedInUser has returned.
286 if (window.closed) {
287 return;
288 }
289 if (window.location.href.contains("action=signin")) {
290 if (user) {
291 // asking to sign-in when already signed in just shows manage.
292 showManage();
293 } else {
294 show("remote");
295 wrapper.init(fxAccounts.getAccountsSignInURI());
296 }
297 } else if (window.location.href.contains("action=signup")) {
298 if (user) {
299 // asking to sign-up when already signed in just shows manage.
300 showManage();
301 } else {
302 show("remote");
303 wrapper.init();
304 }
305 } else if (window.location.href.contains("action=reauth")) {
306 // ideally we would only show this when we know the user is in a
307 // "must reauthenticate" state - but we don't.
308 // As the email address will be included in the URL returned from
309 // promiseAccountsForceSigninURI, just always show it.
310 fxAccounts.promiseAccountsForceSigninURI().then(url => {
311 show("remote");
312 wrapper.init(url);
313 });
314 } else {
315 // No action specified
316 if (user) {
317 showManage();
318 let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
319 document.title = sb.GetStringFromName("manage.pageTitle");
320 } else {
321 show("stage");
322 show("intro");
323 // load the remote frame in the background
324 wrapper.init();
325 }
326 }
327 });
328 }
330 function show(id) {
331 document.getElementById(id).style.display = 'block';
332 }
333 function hide(id) {
334 document.getElementById(id).style.display = 'none';
335 }
337 function showManage() {
338 show("stage");
339 show("manage");
340 hide("remote");
341 hide("intro");
342 }
344 document.addEventListener("DOMContentLoaded", function onload() {
345 document.removeEventListener("DOMContentLoaded", onload, true);
346 init();
347 }, true);
349 function initObservers() {
350 function observe(subject, topic, data) {
351 log("about:accounts observed " + topic);
352 if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) {
353 // All about:account windows get changed to action=signin on logout.
354 window.location = "about:accounts?action=signin";
355 return;
356 }
357 // must be onverified - just about:accounts is loaded.
358 window.location = "about:accounts";
359 }
361 for (let topic of OBSERVER_TOPICS) {
362 Services.obs.addObserver(observe, topic, false);
363 }
364 window.addEventListener("unload", function(event) {
365 log("about:accounts unloading")
366 for (let topic of OBSERVER_TOPICS) {
367 Services.obs.removeObserver(observe, topic);
368 }
369 });
370 }
371 initObservers();