|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
8 |
|
9 Cu.import("resource://gre/modules/Services.jsm"); |
|
10 Cu.import("resource://gre/modules/FxAccounts.jsm"); |
|
11 |
|
12 let fxAccountsCommon = {}; |
|
13 Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); |
|
14 |
|
15 const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; |
|
16 const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync.ui.showCustomizationDialog"; |
|
17 |
|
18 const OBSERVER_TOPICS = [ |
|
19 fxAccountsCommon.ONVERIFIED_NOTIFICATION, |
|
20 fxAccountsCommon.ONLOGOUT_NOTIFICATION, |
|
21 ]; |
|
22 |
|
23 function log(msg) { |
|
24 //dump("FXA: " + msg + "\n"); |
|
25 }; |
|
26 |
|
27 function error(msg) { |
|
28 console.log("Firefox Account Error: " + msg + "\n"); |
|
29 }; |
|
30 |
|
31 function getPreviousAccountNameHash() { |
|
32 try { |
|
33 return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; |
|
34 } catch (_) { |
|
35 return ""; |
|
36 } |
|
37 } |
|
38 |
|
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 } |
|
45 |
|
46 function needRelinkWarning(acctName) { |
|
47 let prevAcctHash = getPreviousAccountNameHash(); |
|
48 return prevAcctHash && prevAcctHash != sha256(acctName); |
|
49 } |
|
50 |
|
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); |
|
62 |
|
63 return hasher.finish(true); |
|
64 } |
|
65 |
|
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 } |
|
83 |
|
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 } |
|
92 |
|
93 let wrapper = { |
|
94 iframe: null, |
|
95 |
|
96 init: function (url=null) { |
|
97 let weave = Cc["@mozilla.org/weave/service;1"] |
|
98 .getService(Ci.nsISupports) |
|
99 .wrappedJSObject; |
|
100 |
|
101 // Don't show about:accounts with FxA disabled. |
|
102 if (!weave.fxAccountsEnabled) { |
|
103 document.body.remove(); |
|
104 return; |
|
105 } |
|
106 |
|
107 let iframe = document.getElementById("remote"); |
|
108 this.iframe = iframe; |
|
109 iframe.addEventListener("load", this); |
|
110 |
|
111 try { |
|
112 iframe.src = url || fxAccounts.getAccountsSignUpURI(); |
|
113 } catch (e) { |
|
114 error("Couldn't init Firefox Account wrapper: " + e.message); |
|
115 } |
|
116 }, |
|
117 |
|
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 }, |
|
129 |
|
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)); |
|
138 |
|
139 if (accountData.customizeSync) { |
|
140 Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true); |
|
141 delete accountData.customizeSync; |
|
142 } |
|
143 |
|
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; |
|
158 |
|
159 // Remember who it was so we can log out next time. |
|
160 setPreviousAccountNameHash(newAccountEmail); |
|
161 |
|
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 }, |
|
186 |
|
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 }, |
|
192 |
|
193 /** |
|
194 * onSessionStatus sends the currently signed in user's credentials |
|
195 * to the jelly. |
|
196 */ |
|
197 onSessionStatus: function () { |
|
198 log("Received: 'session_status'."); |
|
199 |
|
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 }, |
|
205 |
|
206 /** |
|
207 * onSignOut handler erases the current user's session from the fxaccounts service |
|
208 */ |
|
209 onSignOut: function () { |
|
210 log("Received: 'sign_out'."); |
|
211 |
|
212 fxAccounts.signOut().then( |
|
213 () => this.injectData("message", { status: "sign_out" }), |
|
214 (err) => this.injectData("message", { status: "error", error: err }) |
|
215 ); |
|
216 }, |
|
217 |
|
218 handleRemoteCommand: function (evt) { |
|
219 log('command: ' + evt.detail.command); |
|
220 let data = evt.detail.data; |
|
221 |
|
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 }, |
|
240 |
|
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 }; |
|
256 |
|
257 |
|
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 } |
|
271 |
|
272 function getStarted() { |
|
273 hide("intro"); |
|
274 hide("stage"); |
|
275 show("remote"); |
|
276 } |
|
277 |
|
278 function openPrefs() { |
|
279 window.openPreferences("paneSync"); |
|
280 } |
|
281 |
|
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 } |
|
329 |
|
330 function show(id) { |
|
331 document.getElementById(id).style.display = 'block'; |
|
332 } |
|
333 function hide(id) { |
|
334 document.getElementById(id).style.display = 'none'; |
|
335 } |
|
336 |
|
337 function showManage() { |
|
338 show("stage"); |
|
339 show("manage"); |
|
340 hide("remote"); |
|
341 hide("intro"); |
|
342 } |
|
343 |
|
344 document.addEventListener("DOMContentLoaded", function onload() { |
|
345 document.removeEventListener("DOMContentLoaded", onload, true); |
|
346 init(); |
|
347 }, true); |
|
348 |
|
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 } |
|
360 |
|
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(); |