|
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 |
|
6 const Cc = Components.classes; |
|
7 const Ci = Components.interfaces; |
|
8 const Cr = Components.results; |
|
9 |
|
10 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
11 Components.utils.import("resource://gre/modules/Services.jsm"); |
|
12 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
13 |
|
14 /* |
|
15 * LoginManagerPromptFactory |
|
16 * |
|
17 * Implements nsIPromptFactory |
|
18 * |
|
19 * Invoked by [toolkit/components/prompts/src/nsPrompter.js] |
|
20 */ |
|
21 function LoginManagerPromptFactory() { |
|
22 Services.obs.addObserver(this, "quit-application-granted", true); |
|
23 Services.obs.addObserver(this, "passwordmgr-crypto-login", true); |
|
24 Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true); |
|
25 } |
|
26 |
|
27 LoginManagerPromptFactory.prototype = { |
|
28 |
|
29 classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"), |
|
30 QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]), |
|
31 |
|
32 _debug : false, |
|
33 _asyncPrompts : {}, |
|
34 _asyncPromptInProgress : false, |
|
35 |
|
36 observe : function (subject, topic, data) { |
|
37 this.log("Observed: " + topic); |
|
38 if (topic == "quit-application-granted") { |
|
39 this._cancelPendingPrompts(); |
|
40 } else if (topic == "passwordmgr-crypto-login") { |
|
41 // Start processing the deferred prompters. |
|
42 this._doAsyncPrompt(); |
|
43 } else if (topic == "passwordmgr-crypto-loginCanceled") { |
|
44 // User canceled a Master Password prompt, so go ahead and cancel |
|
45 // all pending auth prompts to avoid nagging over and over. |
|
46 this._cancelPendingPrompts(); |
|
47 } |
|
48 }, |
|
49 |
|
50 getPrompt : function (aWindow, aIID) { |
|
51 var prefBranch = Services.prefs.getBranch("signon."); |
|
52 this._debug = prefBranch.getBoolPref("debug"); |
|
53 |
|
54 var prompt = new LoginManagerPrompter().QueryInterface(aIID); |
|
55 prompt.init(aWindow, this); |
|
56 return prompt; |
|
57 }, |
|
58 |
|
59 _doAsyncPrompt : function() { |
|
60 if (this._asyncPromptInProgress) { |
|
61 this.log("_doAsyncPrompt bypassed, already in progress"); |
|
62 return; |
|
63 } |
|
64 |
|
65 // Find the first prompt key we have in the queue |
|
66 var hashKey = null; |
|
67 for (hashKey in this._asyncPrompts) |
|
68 break; |
|
69 |
|
70 if (!hashKey) { |
|
71 this.log("_doAsyncPrompt:run bypassed, no prompts in the queue"); |
|
72 return; |
|
73 } |
|
74 |
|
75 // If login manger has logins for this host, defer prompting if we're |
|
76 // already waiting on a master password entry. |
|
77 var prompt = this._asyncPrompts[hashKey]; |
|
78 var prompter = prompt.prompter; |
|
79 var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo); |
|
80 var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0); |
|
81 if (hasLogins && prompter._pwmgr.uiBusy) { |
|
82 this.log("_doAsyncPrompt:run bypassed, master password UI busy"); |
|
83 return; |
|
84 } |
|
85 |
|
86 this._asyncPromptInProgress = true; |
|
87 prompt.inProgress = true; |
|
88 |
|
89 var self = this; |
|
90 |
|
91 var runnable = { |
|
92 run : function() { |
|
93 var ok = false; |
|
94 try { |
|
95 self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'"); |
|
96 ok = prompter.promptAuth(prompt.channel, |
|
97 prompt.level, |
|
98 prompt.authInfo); |
|
99 } catch (e if (e instanceof Components.Exception) && |
|
100 e.result == Cr.NS_ERROR_NOT_AVAILABLE) { |
|
101 self.log("_doAsyncPrompt:run bypassed, UI is not available in this context"); |
|
102 } catch (e) { |
|
103 Components.utils.reportError("LoginManagerPrompter: " + |
|
104 "_doAsyncPrompt:run: " + e + "\n"); |
|
105 } |
|
106 |
|
107 delete self._asyncPrompts[hashKey]; |
|
108 prompt.inProgress = false; |
|
109 self._asyncPromptInProgress = false; |
|
110 |
|
111 for each (var consumer in prompt.consumers) { |
|
112 if (!consumer.callback) |
|
113 // Not having a callback means that consumer didn't provide it |
|
114 // or canceled the notification |
|
115 continue; |
|
116 |
|
117 self.log("Calling back to " + consumer.callback + " ok=" + ok); |
|
118 try { |
|
119 if (ok) |
|
120 consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo); |
|
121 else |
|
122 consumer.callback.onAuthCancelled(consumer.context, true); |
|
123 } catch (e) { /* Throw away exceptions caused by callback */ } |
|
124 } |
|
125 self._doAsyncPrompt(); |
|
126 } |
|
127 } |
|
128 |
|
129 Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); |
|
130 this.log("_doAsyncPrompt:run dispatched"); |
|
131 }, |
|
132 |
|
133 |
|
134 _cancelPendingPrompts : function() { |
|
135 this.log("Canceling all pending prompts..."); |
|
136 var asyncPrompts = this._asyncPrompts; |
|
137 this.__proto__._asyncPrompts = {}; |
|
138 |
|
139 for each (var prompt in asyncPrompts) { |
|
140 // Watch out! If this prompt is currently prompting, let it handle |
|
141 // notifying the callbacks of success/failure, since it's already |
|
142 // asking the user for input. Reusing a callback can be crashy. |
|
143 if (prompt.inProgress) { |
|
144 this.log("skipping a prompt in progress"); |
|
145 continue; |
|
146 } |
|
147 |
|
148 for each (var consumer in prompt.consumers) { |
|
149 if (!consumer.callback) |
|
150 continue; |
|
151 |
|
152 this.log("Canceling async auth prompt callback " + consumer.callback); |
|
153 try { |
|
154 consumer.callback.onAuthCancelled(consumer.context, true); |
|
155 } catch (e) { /* Just ignore exceptions from the callback */ } |
|
156 } |
|
157 } |
|
158 }, |
|
159 |
|
160 |
|
161 log : function (message) { |
|
162 if (!this._debug) |
|
163 return; |
|
164 |
|
165 dump("Pwmgr PromptFactory: " + message + "\n"); |
|
166 Services.console.logStringMessage("Pwmgr PrompFactory: " + message); |
|
167 } |
|
168 }; // end of LoginManagerPromptFactory implementation |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 /* ==================== LoginManagerPrompter ==================== */ |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 /* |
|
179 * LoginManagerPrompter |
|
180 * |
|
181 * Implements interfaces for prompting the user to enter/save/change auth info. |
|
182 * |
|
183 * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox. |
|
184 * |
|
185 * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication |
|
186 * (eg HTTP Authenticate, FTP login). |
|
187 * |
|
188 * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins |
|
189 * found in HTML forms. |
|
190 */ |
|
191 function LoginManagerPrompter() {} |
|
192 |
|
193 LoginManagerPrompter.prototype = { |
|
194 |
|
195 classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"), |
|
196 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt, |
|
197 Ci.nsIAuthPrompt2, |
|
198 Ci.nsILoginManagerPrompter]), |
|
199 |
|
200 _factory : null, |
|
201 _window : null, |
|
202 _debug : false, // mirrors signon.debug |
|
203 |
|
204 __pwmgr : null, // Password Manager service |
|
205 get _pwmgr() { |
|
206 if (!this.__pwmgr) |
|
207 this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. |
|
208 getService(Ci.nsILoginManager); |
|
209 return this.__pwmgr; |
|
210 }, |
|
211 |
|
212 __promptService : null, // Prompt service for user interaction |
|
213 get _promptService() { |
|
214 if (!this.__promptService) |
|
215 this.__promptService = |
|
216 Cc["@mozilla.org/embedcomp/prompt-service;1"]. |
|
217 getService(Ci.nsIPromptService2); |
|
218 return this.__promptService; |
|
219 }, |
|
220 |
|
221 |
|
222 __strBundle : null, // String bundle for L10N |
|
223 get _strBundle() { |
|
224 if (!this.__strBundle) { |
|
225 var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. |
|
226 getService(Ci.nsIStringBundleService); |
|
227 this.__strBundle = bunService.createBundle( |
|
228 "chrome://passwordmgr/locale/passwordmgr.properties"); |
|
229 if (!this.__strBundle) |
|
230 throw "String bundle for Login Manager not present!"; |
|
231 } |
|
232 |
|
233 return this.__strBundle; |
|
234 }, |
|
235 |
|
236 |
|
237 __ellipsis : null, |
|
238 get _ellipsis() { |
|
239 if (!this.__ellipsis) { |
|
240 this.__ellipsis = "\u2026"; |
|
241 try { |
|
242 this.__ellipsis = Services.prefs.getComplexValue( |
|
243 "intl.ellipsis", Ci.nsIPrefLocalizedString).data; |
|
244 } catch (e) { } |
|
245 } |
|
246 return this.__ellipsis; |
|
247 }, |
|
248 |
|
249 |
|
250 // Whether we are in private browsing mode |
|
251 get _inPrivateBrowsing() { |
|
252 if (this._window) { |
|
253 return PrivateBrowsingUtils.isWindowPrivate(this._window); |
|
254 } else { |
|
255 // If we don't that we're in private browsing mode if the caller did |
|
256 // not provide a window. The callers which really care about this |
|
257 // will indeed pass down a window to us, and for those who don't, |
|
258 // we can just assume that we don't want to save the entered login |
|
259 // information. |
|
260 return true; |
|
261 } |
|
262 }, |
|
263 |
|
264 |
|
265 /* |
|
266 * log |
|
267 * |
|
268 * Internal function for logging debug messages to the Error Console window. |
|
269 */ |
|
270 log : function (message) { |
|
271 if (!this._debug) |
|
272 return; |
|
273 |
|
274 dump("Pwmgr Prompter: " + message + "\n"); |
|
275 Services.console.logStringMessage("Pwmgr Prompter: " + message); |
|
276 }, |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 /* ---------- nsIAuthPrompt prompts ---------- */ |
|
282 |
|
283 |
|
284 /* |
|
285 * prompt |
|
286 * |
|
287 * Wrapper around the prompt service prompt. Saving random fields here |
|
288 * doesn't really make sense and therefore isn't implemented. |
|
289 */ |
|
290 prompt : function (aDialogTitle, aText, aPasswordRealm, |
|
291 aSavePassword, aDefaultText, aResult) { |
|
292 if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) |
|
293 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
294 |
|
295 this.log("===== prompt() called ====="); |
|
296 |
|
297 if (aDefaultText) { |
|
298 aResult.value = aDefaultText; |
|
299 } |
|
300 |
|
301 return this._promptService.prompt(this._window, |
|
302 aDialogTitle, aText, aResult, null, {}); |
|
303 }, |
|
304 |
|
305 |
|
306 /* |
|
307 * promptUsernameAndPassword |
|
308 * |
|
309 * Looks up a username and password in the database. Will prompt the user |
|
310 * with a dialog, even if a username and password are found. |
|
311 */ |
|
312 promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm, |
|
313 aSavePassword, aUsername, aPassword) { |
|
314 this.log("===== promptUsernameAndPassword() called ====="); |
|
315 |
|
316 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) |
|
317 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
318 |
|
319 var selectedLogin = null; |
|
320 var checkBox = { value : false }; |
|
321 var checkBoxLabel = null; |
|
322 var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm); |
|
323 |
|
324 // If hostname is null, we can't save this login. |
|
325 if (hostname) { |
|
326 var canRememberLogin; |
|
327 if (this._inPrivateBrowsing) |
|
328 canRememberLogin = false; |
|
329 else |
|
330 canRememberLogin = (aSavePassword == |
|
331 Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) && |
|
332 this._pwmgr.getLoginSavingEnabled(hostname); |
|
333 |
|
334 // if checkBoxLabel is null, the checkbox won't be shown at all. |
|
335 if (canRememberLogin) |
|
336 checkBoxLabel = this._getLocalizedString("rememberPassword"); |
|
337 |
|
338 // Look for existing logins. |
|
339 var foundLogins = this._pwmgr.findLogins({}, hostname, null, |
|
340 realm); |
|
341 |
|
342 // XXX Like the original code, we can't deal with multiple |
|
343 // account selection. (bug 227632) |
|
344 if (foundLogins.length > 0) { |
|
345 selectedLogin = foundLogins[0]; |
|
346 |
|
347 // If the caller provided a username, try to use it. If they |
|
348 // provided only a password, this will try to find a password-only |
|
349 // login (or return null if none exists). |
|
350 if (aUsername.value) |
|
351 selectedLogin = this._repickSelectedLogin(foundLogins, |
|
352 aUsername.value); |
|
353 |
|
354 if (selectedLogin) { |
|
355 checkBox.value = true; |
|
356 aUsername.value = selectedLogin.username; |
|
357 // If the caller provided a password, prefer it. |
|
358 if (!aPassword.value) |
|
359 aPassword.value = selectedLogin.password; |
|
360 } |
|
361 } |
|
362 } |
|
363 |
|
364 var ok = this._promptService.promptUsernameAndPassword(this._window, |
|
365 aDialogTitle, aText, aUsername, aPassword, |
|
366 checkBoxLabel, checkBox); |
|
367 |
|
368 if (!ok || !checkBox.value || !hostname) |
|
369 return ok; |
|
370 |
|
371 if (!aPassword.value) { |
|
372 this.log("No password entered, so won't offer to save."); |
|
373 return ok; |
|
374 } |
|
375 |
|
376 // XXX We can't prompt with multiple logins yet (bug 227632), so |
|
377 // the entered login might correspond to an existing login |
|
378 // other than the one we originally selected. |
|
379 selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value); |
|
380 |
|
381 // If we didn't find an existing login, or if the username |
|
382 // changed, save as a new login. |
|
383 if (!selectedLogin) { |
|
384 // add as new |
|
385 this.log("New login seen for " + realm); |
|
386 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. |
|
387 createInstance(Ci.nsILoginInfo); |
|
388 newLogin.init(hostname, null, realm, |
|
389 aUsername.value, aPassword.value, "", ""); |
|
390 this._pwmgr.addLogin(newLogin); |
|
391 } else if (aPassword.value != selectedLogin.password) { |
|
392 // update password |
|
393 this.log("Updating password for " + realm); |
|
394 this._updateLogin(selectedLogin, aPassword.value); |
|
395 } else { |
|
396 this.log("Login unchanged, no further action needed."); |
|
397 this._updateLogin(selectedLogin); |
|
398 } |
|
399 |
|
400 return ok; |
|
401 }, |
|
402 |
|
403 |
|
404 /* |
|
405 * promptPassword |
|
406 * |
|
407 * If a password is found in the database for the password realm, it is |
|
408 * returned straight away without displaying a dialog. |
|
409 * |
|
410 * If a password is not found in the database, the user will be prompted |
|
411 * with a dialog with a text field and ok/cancel buttons. If the user |
|
412 * allows it, then the password will be saved in the database. |
|
413 */ |
|
414 promptPassword : function (aDialogTitle, aText, aPasswordRealm, |
|
415 aSavePassword, aPassword) { |
|
416 this.log("===== promptPassword called() ====="); |
|
417 |
|
418 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) |
|
419 throw Components.results.NS_ERROR_NOT_IMPLEMENTED; |
|
420 |
|
421 var checkBox = { value : false }; |
|
422 var checkBoxLabel = null; |
|
423 var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm); |
|
424 |
|
425 username = decodeURIComponent(username); |
|
426 |
|
427 // If hostname is null, we can't save this login. |
|
428 if (hostname && !this._inPrivateBrowsing) { |
|
429 var canRememberLogin = (aSavePassword == |
|
430 Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) && |
|
431 this._pwmgr.getLoginSavingEnabled(hostname); |
|
432 |
|
433 // if checkBoxLabel is null, the checkbox won't be shown at all. |
|
434 if (canRememberLogin) |
|
435 checkBoxLabel = this._getLocalizedString("rememberPassword"); |
|
436 |
|
437 if (!aPassword.value) { |
|
438 // Look for existing logins. |
|
439 var foundLogins = this._pwmgr.findLogins({}, hostname, null, |
|
440 realm); |
|
441 |
|
442 // XXX Like the original code, we can't deal with multiple |
|
443 // account selection (bug 227632). We can deal with finding the |
|
444 // account based on the supplied username - but in this case we'll |
|
445 // just return the first match. |
|
446 for (var i = 0; i < foundLogins.length; ++i) { |
|
447 if (foundLogins[i].username == username) { |
|
448 aPassword.value = foundLogins[i].password; |
|
449 // wallet returned straight away, so this mimics that code |
|
450 return true; |
|
451 } |
|
452 } |
|
453 } |
|
454 } |
|
455 |
|
456 var ok = this._promptService.promptPassword(this._window, aDialogTitle, |
|
457 aText, aPassword, |
|
458 checkBoxLabel, checkBox); |
|
459 |
|
460 if (ok && checkBox.value && hostname && aPassword.value) { |
|
461 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. |
|
462 createInstance(Ci.nsILoginInfo); |
|
463 newLogin.init(hostname, null, realm, username, |
|
464 aPassword.value, "", ""); |
|
465 |
|
466 this.log("New login seen for " + realm); |
|
467 |
|
468 this._pwmgr.addLogin(newLogin); |
|
469 } |
|
470 |
|
471 return ok; |
|
472 }, |
|
473 |
|
474 /* ---------- nsIAuthPrompt helpers ---------- */ |
|
475 |
|
476 |
|
477 /** |
|
478 * Given aRealmString, such as "http://user@example.com/foo", returns an |
|
479 * array of: |
|
480 * - the formatted hostname |
|
481 * - the realm (hostname + path) |
|
482 * - the username, if present |
|
483 * |
|
484 * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S] |
|
485 * channels, e.g. "example.com:80 (httprealm)", null is returned for all |
|
486 * arguments to let callers know the login can't be saved because we don't |
|
487 * know whether it's http or https. |
|
488 */ |
|
489 _getRealmInfo : function (aRealmString) { |
|
490 var httpRealm = /^.+ \(.+\)$/; |
|
491 if (httpRealm.test(aRealmString)) |
|
492 return [null, null, null]; |
|
493 |
|
494 var uri = Services.io.newURI(aRealmString, null, null); |
|
495 var pathname = ""; |
|
496 |
|
497 if (uri.path != "/") |
|
498 pathname = uri.path; |
|
499 |
|
500 var formattedHostname = this._getFormattedHostname(uri); |
|
501 |
|
502 return [formattedHostname, formattedHostname + pathname, uri.username]; |
|
503 }, |
|
504 |
|
505 /* ---------- nsIAuthPrompt2 prompts ---------- */ |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 /* |
|
511 * promptAuth |
|
512 * |
|
513 * Implementation of nsIAuthPrompt2. |
|
514 * |
|
515 * nsIChannel aChannel |
|
516 * int aLevel |
|
517 * nsIAuthInformation aAuthInfo |
|
518 */ |
|
519 promptAuth : function (aChannel, aLevel, aAuthInfo) { |
|
520 var selectedLogin = null; |
|
521 var checkbox = { value : false }; |
|
522 var checkboxLabel = null; |
|
523 var epicfail = false; |
|
524 var canAutologin = false; |
|
525 |
|
526 try { |
|
527 |
|
528 this.log("===== promptAuth called ====="); |
|
529 |
|
530 // If the user submits a login but it fails, we need to remove the |
|
531 // notification bar that was displayed. Conveniently, the user will |
|
532 // be prompted for authentication again, which brings us here. |
|
533 this._removeLoginNotifications(); |
|
534 |
|
535 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); |
|
536 |
|
537 |
|
538 // Looks for existing logins to prefill the prompt with. |
|
539 var foundLogins = this._pwmgr.findLogins({}, |
|
540 hostname, null, httpRealm); |
|
541 this.log("found " + foundLogins.length + " matching logins."); |
|
542 |
|
543 // XXX Can't select from multiple accounts yet. (bug 227632) |
|
544 if (foundLogins.length > 0) { |
|
545 selectedLogin = foundLogins[0]; |
|
546 this._SetAuthInfo(aAuthInfo, selectedLogin.username, |
|
547 selectedLogin.password); |
|
548 |
|
549 // Allow automatic proxy login |
|
550 if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY && |
|
551 !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) && |
|
552 Services.prefs.getBoolPref("signon.autologin.proxy") && |
|
553 !this._inPrivateBrowsing) { |
|
554 |
|
555 this.log("Autologin enabled, skipping auth prompt."); |
|
556 canAutologin = true; |
|
557 } |
|
558 |
|
559 checkbox.value = true; |
|
560 } |
|
561 |
|
562 var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname); |
|
563 if (this._inPrivateBrowsing) |
|
564 canRememberLogin = false; |
|
565 |
|
566 // if checkboxLabel is null, the checkbox won't be shown at all. |
|
567 var notifyBox = this._getNotifyBox(); |
|
568 if (canRememberLogin && !notifyBox) |
|
569 checkboxLabel = this._getLocalizedString("rememberPassword"); |
|
570 } catch (e) { |
|
571 // Ignore any errors and display the prompt anyway. |
|
572 epicfail = true; |
|
573 Components.utils.reportError("LoginManagerPrompter: " + |
|
574 "Epic fail in promptAuth: " + e + "\n"); |
|
575 } |
|
576 |
|
577 var ok = canAutologin || |
|
578 this._promptService.promptAuth(this._window, |
|
579 aChannel, aLevel, aAuthInfo, |
|
580 checkboxLabel, checkbox); |
|
581 |
|
582 // If there's a notification box, use it to allow the user to |
|
583 // determine if the login should be saved. If there isn't a |
|
584 // notification box, only save the login if the user set the |
|
585 // checkbox to do so. |
|
586 var rememberLogin = notifyBox ? canRememberLogin : checkbox.value; |
|
587 if (!ok || !rememberLogin || epicfail) |
|
588 return ok; |
|
589 |
|
590 try { |
|
591 var [username, password] = this._GetAuthInfo(aAuthInfo); |
|
592 |
|
593 if (!password) { |
|
594 this.log("No password entered, so won't offer to save."); |
|
595 return ok; |
|
596 } |
|
597 |
|
598 // XXX We can't prompt with multiple logins yet (bug 227632), so |
|
599 // the entered login might correspond to an existing login |
|
600 // other than the one we originally selected. |
|
601 selectedLogin = this._repickSelectedLogin(foundLogins, username); |
|
602 |
|
603 // If we didn't find an existing login, or if the username |
|
604 // changed, save as a new login. |
|
605 if (!selectedLogin) { |
|
606 this.log("New login seen for " + username + |
|
607 " @ " + hostname + " (" + httpRealm + ")"); |
|
608 |
|
609 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. |
|
610 createInstance(Ci.nsILoginInfo); |
|
611 newLogin.init(hostname, null, httpRealm, |
|
612 username, password, "", ""); |
|
613 var notifyObj = this._getPopupNote() || notifyBox; |
|
614 if (notifyObj) |
|
615 this._showSaveLoginNotification(notifyObj, newLogin); |
|
616 else |
|
617 this._pwmgr.addLogin(newLogin); |
|
618 |
|
619 } else if (password != selectedLogin.password) { |
|
620 |
|
621 this.log("Updating password for " + username + |
|
622 " @ " + hostname + " (" + httpRealm + ")"); |
|
623 var notifyObj = this._getPopupNote() || notifyBox; |
|
624 if (notifyObj) |
|
625 this._showChangeLoginNotification(notifyObj, |
|
626 selectedLogin, password); |
|
627 else |
|
628 this._updateLogin(selectedLogin, password); |
|
629 |
|
630 } else { |
|
631 this.log("Login unchanged, no further action needed."); |
|
632 this._updateLogin(selectedLogin); |
|
633 } |
|
634 } catch (e) { |
|
635 Components.utils.reportError("LoginManagerPrompter: " + |
|
636 "Fail2 in promptAuth: " + e + "\n"); |
|
637 } |
|
638 |
|
639 return ok; |
|
640 }, |
|
641 |
|
642 asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) { |
|
643 var cancelable = null; |
|
644 |
|
645 try { |
|
646 this.log("===== asyncPromptAuth called ====="); |
|
647 |
|
648 // If the user submits a login but it fails, we need to remove the |
|
649 // notification bar that was displayed. Conveniently, the user will |
|
650 // be prompted for authentication again, which brings us here. |
|
651 this._removeLoginNotifications(); |
|
652 |
|
653 cancelable = this._newAsyncPromptConsumer(aCallback, aContext); |
|
654 |
|
655 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); |
|
656 |
|
657 var hashKey = aLevel + "|" + hostname + "|" + httpRealm; |
|
658 this.log("Async prompt key = " + hashKey); |
|
659 var asyncPrompt = this._factory._asyncPrompts[hashKey]; |
|
660 if (asyncPrompt) { |
|
661 this.log("Prompt bound to an existing one in the queue, callback = " + aCallback); |
|
662 asyncPrompt.consumers.push(cancelable); |
|
663 return cancelable; |
|
664 } |
|
665 |
|
666 this.log("Adding new prompt to the queue, callback = " + aCallback); |
|
667 asyncPrompt = { |
|
668 consumers: [cancelable], |
|
669 channel: aChannel, |
|
670 authInfo: aAuthInfo, |
|
671 level: aLevel, |
|
672 inProgress : false, |
|
673 prompter: this |
|
674 } |
|
675 |
|
676 this._factory._asyncPrompts[hashKey] = asyncPrompt; |
|
677 this._factory._doAsyncPrompt(); |
|
678 } |
|
679 catch (e) { |
|
680 Components.utils.reportError("LoginManagerPrompter: " + |
|
681 "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n"); |
|
682 // Fail the prompt operation to let the consumer fall back |
|
683 // to synchronous promptAuth method |
|
684 throw e; |
|
685 } |
|
686 |
|
687 return cancelable; |
|
688 }, |
|
689 |
|
690 |
|
691 |
|
692 |
|
693 /* ---------- nsILoginManagerPrompter prompts ---------- */ |
|
694 |
|
695 |
|
696 |
|
697 |
|
698 /* |
|
699 * init |
|
700 * |
|
701 */ |
|
702 init : function (aWindow, aFactory) { |
|
703 this._window = aWindow; |
|
704 this._factory = aFactory || null; |
|
705 |
|
706 var prefBranch = Services.prefs.getBranch("signon."); |
|
707 this._debug = prefBranch.getBoolPref("debug"); |
|
708 this.log("===== initialized ====="); |
|
709 }, |
|
710 |
|
711 |
|
712 /* |
|
713 * promptToSavePassword |
|
714 * |
|
715 */ |
|
716 promptToSavePassword : function (aLogin) { |
|
717 var notifyObj = this._getPopupNote() || this._getNotifyBox(); |
|
718 |
|
719 if (notifyObj) |
|
720 this._showSaveLoginNotification(notifyObj, aLogin); |
|
721 else |
|
722 this._showSaveLoginDialog(aLogin); |
|
723 }, |
|
724 |
|
725 |
|
726 /* |
|
727 * _showLoginNotification |
|
728 * |
|
729 * Displays a notification bar. |
|
730 * |
|
731 */ |
|
732 _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) { |
|
733 var oldBar = aNotifyBox.getNotificationWithValue(aName); |
|
734 const priority = aNotifyBox.PRIORITY_INFO_MEDIUM; |
|
735 |
|
736 this.log("Adding new " + aName + " notification bar"); |
|
737 var newBar = aNotifyBox.appendNotification( |
|
738 aText, aName, |
|
739 "chrome://mozapps/skin/passwordmgr/key.png", |
|
740 priority, aButtons); |
|
741 |
|
742 // The page we're going to hasn't loaded yet, so we want to persist |
|
743 // across the first location change. |
|
744 newBar.persistence++; |
|
745 |
|
746 // Sites like Gmail perform a funky redirect dance before you end up |
|
747 // at the post-authentication page. I don't see a good way to |
|
748 // heuristically determine when to ignore such location changes, so |
|
749 // we'll try ignoring location changes based on a time interval. |
|
750 newBar.timeout = Date.now() + 20000; // 20 seconds |
|
751 |
|
752 if (oldBar) { |
|
753 this.log("(...and removing old " + aName + " notification bar)"); |
|
754 aNotifyBox.removeNotification(oldBar); |
|
755 } |
|
756 }, |
|
757 |
|
758 |
|
759 /* |
|
760 * _showSaveLoginNotification |
|
761 * |
|
762 * Displays a notification bar or a popup notification, to allow the user |
|
763 * to save the specified login. This allows the user to see the results of |
|
764 * their login, and only save a login which they know worked. |
|
765 * |
|
766 * @param aNotifyObj |
|
767 * A notification box or a popup notification. |
|
768 */ |
|
769 _showSaveLoginNotification : function (aNotifyObj, aLogin) { |
|
770 |
|
771 // Ugh. We can't use the strings from the popup window, because they |
|
772 // have the access key marked in the string (eg "Mo&zilla"), along |
|
773 // with some weird rules for handling access keys that do not occur |
|
774 // in the string, for L10N. See commonDialog.js's setLabelForNode(). |
|
775 var neverButtonText = |
|
776 this._getLocalizedString("notifyBarNeverRememberButtonText"); |
|
777 var neverButtonAccessKey = |
|
778 this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"); |
|
779 var rememberButtonText = |
|
780 this._getLocalizedString("notifyBarRememberPasswordButtonText"); |
|
781 var rememberButtonAccessKey = |
|
782 this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); |
|
783 |
|
784 var displayHost = this._getShortDisplayHost(aLogin.hostname); |
|
785 var notificationText; |
|
786 if (aLogin.username) { |
|
787 var displayUser = this._sanitizeUsername(aLogin.username); |
|
788 notificationText = this._getLocalizedString( |
|
789 "rememberPasswordMsg", |
|
790 [displayUser, displayHost]); |
|
791 } else { |
|
792 notificationText = this._getLocalizedString( |
|
793 "rememberPasswordMsgNoUsername", |
|
794 [displayHost]); |
|
795 } |
|
796 |
|
797 // The callbacks in |buttons| have a closure to access the variables |
|
798 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr |
|
799 // without a getService() call. |
|
800 var pwmgr = this._pwmgr; |
|
801 |
|
802 // Notification is a PopupNotification |
|
803 if (aNotifyObj == this._getPopupNote()) { |
|
804 // "Remember" button |
|
805 var mainAction = { |
|
806 label: rememberButtonText, |
|
807 accessKey: rememberButtonAccessKey, |
|
808 callback: function(aNotifyObj, aButton) { |
|
809 pwmgr.addLogin(aLogin); |
|
810 browser.focus(); |
|
811 } |
|
812 }; |
|
813 |
|
814 var secondaryActions = [ |
|
815 // "Never for this site" button |
|
816 { |
|
817 label: neverButtonText, |
|
818 accessKey: neverButtonAccessKey, |
|
819 callback: function(aNotifyObj, aButton) { |
|
820 pwmgr.setLoginSavingEnabled(aLogin.hostname, false); |
|
821 browser.focus(); |
|
822 } |
|
823 } |
|
824 ]; |
|
825 |
|
826 var notifyWin = this._getNotifyWindow(); |
|
827 var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; |
|
828 var browser = chromeWin.gBrowser. |
|
829 getBrowserForDocument(notifyWin.top.document); |
|
830 |
|
831 aNotifyObj.show(browser, "password-save", notificationText, |
|
832 "password-notification-icon", mainAction, |
|
833 secondaryActions, { timeout: Date.now() + 10000, |
|
834 persistWhileVisible: true }); |
|
835 } else { |
|
836 var notNowButtonText = |
|
837 this._getLocalizedString("notifyBarNotNowButtonText"); |
|
838 var notNowButtonAccessKey = |
|
839 this._getLocalizedString("notifyBarNotNowButtonAccessKey"); |
|
840 var buttons = [ |
|
841 // "Remember" button |
|
842 { |
|
843 label: rememberButtonText, |
|
844 accessKey: rememberButtonAccessKey, |
|
845 popup: null, |
|
846 callback: function(aNotifyObj, aButton) { |
|
847 pwmgr.addLogin(aLogin); |
|
848 } |
|
849 }, |
|
850 |
|
851 // "Never for this site" button |
|
852 { |
|
853 label: neverButtonText, |
|
854 accessKey: neverButtonAccessKey, |
|
855 popup: null, |
|
856 callback: function(aNotifyObj, aButton) { |
|
857 pwmgr.setLoginSavingEnabled(aLogin.hostname, false); |
|
858 } |
|
859 }, |
|
860 |
|
861 // "Not now" button |
|
862 { |
|
863 label: notNowButtonText, |
|
864 accessKey: notNowButtonAccessKey, |
|
865 popup: null, |
|
866 callback: function() { /* NOP */ } |
|
867 } |
|
868 ]; |
|
869 |
|
870 this._showLoginNotification(aNotifyObj, "password-save", |
|
871 notificationText, buttons); |
|
872 } |
|
873 }, |
|
874 |
|
875 |
|
876 /* |
|
877 * _removeLoginNotifications |
|
878 * |
|
879 */ |
|
880 _removeLoginNotifications : function () { |
|
881 var popupNote = this._getPopupNote(); |
|
882 if (popupNote) |
|
883 popupNote = popupNote.getNotification("password-save"); |
|
884 if (popupNote) |
|
885 popupNote.remove(); |
|
886 |
|
887 var notifyBox = this._getNotifyBox(); |
|
888 if (notifyBox) { |
|
889 var oldBar = notifyBox.getNotificationWithValue("password-save"); |
|
890 if (oldBar) { |
|
891 this.log("Removing save-password notification bar."); |
|
892 notifyBox.removeNotification(oldBar); |
|
893 } |
|
894 |
|
895 oldBar = notifyBox.getNotificationWithValue("password-change"); |
|
896 if (oldBar) { |
|
897 this.log("Removing change-password notification bar."); |
|
898 notifyBox.removeNotification(oldBar); |
|
899 } |
|
900 } |
|
901 }, |
|
902 |
|
903 |
|
904 /* |
|
905 * _showSaveLoginDialog |
|
906 * |
|
907 * Called when we detect a new login in a form submission, |
|
908 * asks the user what to do. |
|
909 * |
|
910 */ |
|
911 _showSaveLoginDialog : function (aLogin) { |
|
912 const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT + |
|
913 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + |
|
914 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + |
|
915 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2); |
|
916 |
|
917 var displayHost = this._getShortDisplayHost(aLogin.hostname); |
|
918 |
|
919 var dialogText; |
|
920 if (aLogin.username) { |
|
921 var displayUser = this._sanitizeUsername(aLogin.username); |
|
922 dialogText = this._getLocalizedString( |
|
923 "rememberPasswordMsg", |
|
924 [displayUser, displayHost]); |
|
925 } else { |
|
926 dialogText = this._getLocalizedString( |
|
927 "rememberPasswordMsgNoUsername", |
|
928 [displayHost]); |
|
929 |
|
930 } |
|
931 var dialogTitle = this._getLocalizedString( |
|
932 "savePasswordTitle"); |
|
933 var neverButtonText = this._getLocalizedString( |
|
934 "neverForSiteButtonText"); |
|
935 var rememberButtonText = this._getLocalizedString( |
|
936 "rememberButtonText"); |
|
937 var notNowButtonText = this._getLocalizedString( |
|
938 "notNowButtonText"); |
|
939 |
|
940 this.log("Prompting user to save/ignore login"); |
|
941 var userChoice = this._promptService.confirmEx(this._window, |
|
942 dialogTitle, dialogText, |
|
943 buttonFlags, rememberButtonText, |
|
944 notNowButtonText, neverButtonText, |
|
945 null, {}); |
|
946 // Returns: |
|
947 // 0 - Save the login |
|
948 // 1 - Ignore the login this time |
|
949 // 2 - Never save logins for this site |
|
950 if (userChoice == 2) { |
|
951 this.log("Disabling " + aLogin.hostname + " logins by request."); |
|
952 this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false); |
|
953 } else if (userChoice == 0) { |
|
954 this.log("Saving login for " + aLogin.hostname); |
|
955 this._pwmgr.addLogin(aLogin); |
|
956 } else { |
|
957 // userChoice == 1 --> just ignore the login. |
|
958 this.log("Ignoring login."); |
|
959 } |
|
960 }, |
|
961 |
|
962 |
|
963 /* |
|
964 * promptToChangePassword |
|
965 * |
|
966 * Called when we think we detect a password change for an existing |
|
967 * login, when the form being submitted contains multiple password |
|
968 * fields. |
|
969 * |
|
970 */ |
|
971 promptToChangePassword : function (aOldLogin, aNewLogin) { |
|
972 var notifyObj = this._getPopupNote() || this._getNotifyBox(); |
|
973 |
|
974 if (notifyObj) |
|
975 this._showChangeLoginNotification(notifyObj, aOldLogin, |
|
976 aNewLogin.password); |
|
977 else |
|
978 this._showChangeLoginDialog(aOldLogin, aNewLogin.password); |
|
979 }, |
|
980 |
|
981 |
|
982 /* |
|
983 * _showChangeLoginNotification |
|
984 * |
|
985 * Shows the Change Password notification bar or popup notification. |
|
986 * |
|
987 * @param aNotifyObj |
|
988 * A notification box or a popup notification. |
|
989 */ |
|
990 _showChangeLoginNotification : function (aNotifyObj, aOldLogin, aNewPassword) { |
|
991 var notificationText; |
|
992 if (aOldLogin.username) { |
|
993 var displayUser = this._sanitizeUsername(aOldLogin.username); |
|
994 notificationText = this._getLocalizedString( |
|
995 "updatePasswordMsg", |
|
996 [displayUser]); |
|
997 } else { |
|
998 notificationText = this._getLocalizedString( |
|
999 "updatePasswordMsgNoUser"); |
|
1000 } |
|
1001 |
|
1002 var changeButtonText = |
|
1003 this._getLocalizedString("notifyBarUpdateButtonText"); |
|
1004 var changeButtonAccessKey = |
|
1005 this._getLocalizedString("notifyBarUpdateButtonAccessKey"); |
|
1006 |
|
1007 // The callbacks in |buttons| have a closure to access the variables |
|
1008 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr |
|
1009 // without a getService() call. |
|
1010 var self = this; |
|
1011 |
|
1012 // Notification is a PopupNotification |
|
1013 if (aNotifyObj == this._getPopupNote()) { |
|
1014 // "Yes" button |
|
1015 var mainAction = { |
|
1016 label: changeButtonText, |
|
1017 accessKey: changeButtonAccessKey, |
|
1018 popup: null, |
|
1019 callback: function(aNotifyObj, aButton) { |
|
1020 self._updateLogin(aOldLogin, aNewPassword); |
|
1021 } |
|
1022 }; |
|
1023 |
|
1024 var notifyWin = this._getNotifyWindow(); |
|
1025 var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; |
|
1026 var browser = chromeWin.gBrowser. |
|
1027 getBrowserForDocument(notifyWin.top.document); |
|
1028 |
|
1029 aNotifyObj.show(browser, "password-change", notificationText, |
|
1030 "password-notification-icon", mainAction, |
|
1031 null, { timeout: Date.now() + 10000, |
|
1032 persistWhileVisible: true }); |
|
1033 } else { |
|
1034 var dontChangeButtonText = |
|
1035 this._getLocalizedString("notifyBarDontChangeButtonText"); |
|
1036 var dontChangeButtonAccessKey = |
|
1037 this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); |
|
1038 var buttons = [ |
|
1039 // "Yes" button |
|
1040 { |
|
1041 label: changeButtonText, |
|
1042 accessKey: changeButtonAccessKey, |
|
1043 popup: null, |
|
1044 callback: function(aNotifyObj, aButton) { |
|
1045 self._updateLogin(aOldLogin, aNewPassword); |
|
1046 } |
|
1047 }, |
|
1048 |
|
1049 // "No" button |
|
1050 { |
|
1051 label: dontChangeButtonText, |
|
1052 accessKey: dontChangeButtonAccessKey, |
|
1053 popup: null, |
|
1054 callback: function(aNotifyObj, aButton) { |
|
1055 // do nothing |
|
1056 } |
|
1057 } |
|
1058 ]; |
|
1059 |
|
1060 this._showLoginNotification(aNotifyObj, "password-change", |
|
1061 notificationText, buttons); |
|
1062 } |
|
1063 }, |
|
1064 |
|
1065 |
|
1066 /* |
|
1067 * _showChangeLoginDialog |
|
1068 * |
|
1069 * Shows the Change Password dialog. |
|
1070 * |
|
1071 */ |
|
1072 _showChangeLoginDialog : function (aOldLogin, aNewPassword) { |
|
1073 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; |
|
1074 |
|
1075 var dialogText; |
|
1076 if (aOldLogin.username) |
|
1077 dialogText = this._getLocalizedString( |
|
1078 "updatePasswordMsg", |
|
1079 [aOldLogin.username]); |
|
1080 else |
|
1081 dialogText = this._getLocalizedString( |
|
1082 "updatePasswordMsgNoUser"); |
|
1083 |
|
1084 var dialogTitle = this._getLocalizedString( |
|
1085 "passwordChangeTitle"); |
|
1086 |
|
1087 // returns 0 for yes, 1 for no. |
|
1088 var ok = !this._promptService.confirmEx(this._window, |
|
1089 dialogTitle, dialogText, buttonFlags, |
|
1090 null, null, null, |
|
1091 null, {}); |
|
1092 if (ok) { |
|
1093 this.log("Updating password for user " + aOldLogin.username); |
|
1094 this._updateLogin(aOldLogin, aNewPassword); |
|
1095 } |
|
1096 }, |
|
1097 |
|
1098 |
|
1099 /* |
|
1100 * promptToChangePasswordWithUsernames |
|
1101 * |
|
1102 * Called when we detect a password change in a form submission, but we |
|
1103 * don't know which existing login (username) it's for. Asks the user |
|
1104 * to select a username and confirm the password change. |
|
1105 * |
|
1106 * Note: The caller doesn't know the username for aNewLogin, so this |
|
1107 * function fills in .username and .usernameField with the values |
|
1108 * from the login selected by the user. |
|
1109 * |
|
1110 * Note; XPCOM stupidity: |count| is just |logins.length|. |
|
1111 */ |
|
1112 promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { |
|
1113 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; |
|
1114 |
|
1115 var usernames = logins.map(function (l) l.username); |
|
1116 var dialogText = this._getLocalizedString("userSelectText"); |
|
1117 var dialogTitle = this._getLocalizedString("passwordChangeTitle"); |
|
1118 var selectedIndex = { value: null }; |
|
1119 |
|
1120 // If user selects ok, outparam.value is set to the index |
|
1121 // of the selected username. |
|
1122 var ok = this._promptService.select(this._window, |
|
1123 dialogTitle, dialogText, |
|
1124 usernames.length, usernames, |
|
1125 selectedIndex); |
|
1126 if (ok) { |
|
1127 // Now that we know which login to use, modify its password. |
|
1128 var selectedLogin = logins[selectedIndex.value]; |
|
1129 this.log("Updating password for user " + selectedLogin.username); |
|
1130 this._updateLogin(selectedLogin, aNewLogin.password); |
|
1131 } |
|
1132 }, |
|
1133 |
|
1134 |
|
1135 |
|
1136 |
|
1137 /* ---------- Internal Methods ---------- */ |
|
1138 |
|
1139 |
|
1140 |
|
1141 |
|
1142 /* |
|
1143 * _updateLogin |
|
1144 */ |
|
1145 _updateLogin : function (login, newPassword) { |
|
1146 var now = Date.now(); |
|
1147 var propBag = Cc["@mozilla.org/hash-property-bag;1"]. |
|
1148 createInstance(Ci.nsIWritablePropertyBag); |
|
1149 if (newPassword) { |
|
1150 propBag.setProperty("password", newPassword); |
|
1151 // Explicitly set the password change time here (even though it would |
|
1152 // be changed automatically), to ensure that it's exactly the same |
|
1153 // value as timeLastUsed. |
|
1154 propBag.setProperty("timePasswordChanged", now); |
|
1155 } |
|
1156 propBag.setProperty("timeLastUsed", now); |
|
1157 propBag.setProperty("timesUsedIncrement", 1); |
|
1158 this._pwmgr.modifyLogin(login, propBag); |
|
1159 }, |
|
1160 |
|
1161 |
|
1162 /* |
|
1163 * _getChromeWindow |
|
1164 * |
|
1165 * Given a content DOM window, returns the chrome window it's in. |
|
1166 */ |
|
1167 _getChromeWindow: function (aWindow) { |
|
1168 var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
1169 .getInterface(Ci.nsIWebNavigation) |
|
1170 .QueryInterface(Ci.nsIDocShell) |
|
1171 .chromeEventHandler.ownerDocument.defaultView; |
|
1172 return chromeWin; |
|
1173 }, |
|
1174 |
|
1175 |
|
1176 /* |
|
1177 * _getNotifyWindow |
|
1178 */ |
|
1179 _getNotifyWindow: function () { |
|
1180 |
|
1181 try { |
|
1182 // Get topmost window, in case we're in a frame. |
|
1183 var notifyWin = this._window.top; |
|
1184 |
|
1185 // Some sites pop up a temporary login window, when disappears |
|
1186 // upon submission of credentials. We want to put the notification |
|
1187 // bar in the opener window if this seems to be happening. |
|
1188 if (notifyWin.opener) { |
|
1189 var chromeDoc = this._getChromeWindow(notifyWin). |
|
1190 document.documentElement; |
|
1191 var webnav = notifyWin. |
|
1192 QueryInterface(Ci.nsIInterfaceRequestor). |
|
1193 getInterface(Ci.nsIWebNavigation); |
|
1194 |
|
1195 // Check to see if the current window was opened with chrome |
|
1196 // disabled, and if so use the opener window. But if the window |
|
1197 // has been used to visit other pages (ie, has a history), |
|
1198 // assume it'll stick around and *don't* use the opener. |
|
1199 if (chromeDoc.getAttribute("chromehidden") && |
|
1200 webnav.sessionHistory.count == 1) { |
|
1201 this.log("Using opener window for notification bar."); |
|
1202 notifyWin = notifyWin.opener; |
|
1203 } |
|
1204 } |
|
1205 |
|
1206 return notifyWin; |
|
1207 |
|
1208 } catch (e) { |
|
1209 // If any errors happen, just assume no notification box. |
|
1210 this.log("Unable to get notify window"); |
|
1211 return null; |
|
1212 } |
|
1213 }, |
|
1214 |
|
1215 |
|
1216 /* |
|
1217 * _getPopupNote |
|
1218 * |
|
1219 * Returns the popup notification to this prompter, |
|
1220 * or null if there isn't one available. |
|
1221 */ |
|
1222 _getPopupNote : function () { |
|
1223 let popupNote = null; |
|
1224 |
|
1225 try { |
|
1226 let notifyWin = this._getNotifyWindow(); |
|
1227 |
|
1228 // Get the chrome window for the content window we're using. |
|
1229 // .wrappedJSObject needed here -- see bug 422974 comment 5. |
|
1230 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; |
|
1231 |
|
1232 popupNote = chromeWin.PopupNotifications; |
|
1233 } catch (e) { |
|
1234 this.log("Popup notifications not available on window"); |
|
1235 } |
|
1236 |
|
1237 return popupNote; |
|
1238 }, |
|
1239 |
|
1240 |
|
1241 /* |
|
1242 * _getNotifyBox |
|
1243 * |
|
1244 * Returns the notification box to this prompter, or null if there isn't |
|
1245 * a notification box available. |
|
1246 */ |
|
1247 _getNotifyBox : function () { |
|
1248 let notifyBox = null; |
|
1249 |
|
1250 try { |
|
1251 let notifyWin = this._getNotifyWindow(); |
|
1252 |
|
1253 // Get the chrome window for the content window we're using. |
|
1254 // .wrappedJSObject needed here -- see bug 422974 comment 5. |
|
1255 let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; |
|
1256 |
|
1257 notifyBox = chromeWin.getNotificationBox(notifyWin); |
|
1258 } catch (e) { |
|
1259 this.log("Notification bars not available on window"); |
|
1260 } |
|
1261 |
|
1262 return notifyBox; |
|
1263 }, |
|
1264 |
|
1265 |
|
1266 /* |
|
1267 * _repickSelectedLogin |
|
1268 * |
|
1269 * The user might enter a login that isn't the one we prefilled, but |
|
1270 * is the same as some other existing login. So, pick a login with a |
|
1271 * matching username, or return null. |
|
1272 */ |
|
1273 _repickSelectedLogin : function (foundLogins, username) { |
|
1274 for (var i = 0; i < foundLogins.length; i++) |
|
1275 if (foundLogins[i].username == username) |
|
1276 return foundLogins[i]; |
|
1277 return null; |
|
1278 }, |
|
1279 |
|
1280 |
|
1281 /* |
|
1282 * _getLocalizedString |
|
1283 * |
|
1284 * Can be called as: |
|
1285 * _getLocalizedString("key1"); |
|
1286 * _getLocalizedString("key2", ["arg1"]); |
|
1287 * _getLocalizedString("key3", ["arg1", "arg2"]); |
|
1288 * (etc) |
|
1289 * |
|
1290 * Returns the localized string for the specified key, |
|
1291 * formatted if required. |
|
1292 * |
|
1293 */ |
|
1294 _getLocalizedString : function (key, formatArgs) { |
|
1295 if (formatArgs) |
|
1296 return this._strBundle.formatStringFromName( |
|
1297 key, formatArgs, formatArgs.length); |
|
1298 else |
|
1299 return this._strBundle.GetStringFromName(key); |
|
1300 }, |
|
1301 |
|
1302 |
|
1303 /* |
|
1304 * _sanitizeUsername |
|
1305 * |
|
1306 * Sanitizes the specified username, by stripping quotes and truncating if |
|
1307 * it's too long. This helps prevent an evil site from messing with the |
|
1308 * "save password?" prompt too much. |
|
1309 */ |
|
1310 _sanitizeUsername : function (username) { |
|
1311 if (username.length > 30) { |
|
1312 username = username.substring(0, 30); |
|
1313 username += this._ellipsis; |
|
1314 } |
|
1315 return username.replace(/['"]/g, ""); |
|
1316 }, |
|
1317 |
|
1318 |
|
1319 /* |
|
1320 * _getFormattedHostname |
|
1321 * |
|
1322 * The aURI parameter may either be a string uri, or an nsIURI instance. |
|
1323 * |
|
1324 * Returns the hostname to use in a nsILoginInfo object (for example, |
|
1325 * "http://example.com"). |
|
1326 */ |
|
1327 _getFormattedHostname : function (aURI) { |
|
1328 var uri; |
|
1329 if (aURI instanceof Ci.nsIURI) { |
|
1330 uri = aURI; |
|
1331 } else { |
|
1332 uri = Services.io.newURI(aURI, null, null); |
|
1333 } |
|
1334 var scheme = uri.scheme; |
|
1335 |
|
1336 var hostname = scheme + "://" + uri.host; |
|
1337 |
|
1338 // If the URI explicitly specified a port, only include it when |
|
1339 // it's not the default. (We never want "http://foo.com:80") |
|
1340 port = uri.port; |
|
1341 if (port != -1) { |
|
1342 var handler = Services.io.getProtocolHandler(scheme); |
|
1343 if (port != handler.defaultPort) |
|
1344 hostname += ":" + port; |
|
1345 } |
|
1346 |
|
1347 return hostname; |
|
1348 }, |
|
1349 |
|
1350 |
|
1351 /* |
|
1352 * _getShortDisplayHost |
|
1353 * |
|
1354 * Converts a login's hostname field (a URL) to a short string for |
|
1355 * prompting purposes. Eg, "http://foo.com" --> "foo.com", or |
|
1356 * "ftp://www.site.co.uk" --> "site.co.uk". |
|
1357 */ |
|
1358 _getShortDisplayHost: function (aURIString) { |
|
1359 var displayHost; |
|
1360 |
|
1361 var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. |
|
1362 getService(Ci.nsIEffectiveTLDService); |
|
1363 var idnService = Cc["@mozilla.org/network/idn-service;1"]. |
|
1364 getService(Ci.nsIIDNService); |
|
1365 try { |
|
1366 var uri = Services.io.newURI(aURIString, null, null); |
|
1367 var baseDomain = eTLDService.getBaseDomain(uri); |
|
1368 displayHost = idnService.convertToDisplayIDN(baseDomain, {}); |
|
1369 } catch (e) { |
|
1370 this.log("_getShortDisplayHost couldn't process " + aURIString); |
|
1371 } |
|
1372 |
|
1373 if (!displayHost) |
|
1374 displayHost = aURIString; |
|
1375 |
|
1376 return displayHost; |
|
1377 }, |
|
1378 |
|
1379 |
|
1380 /* |
|
1381 * _getAuthTarget |
|
1382 * |
|
1383 * Returns the hostname and realm for which authentication is being |
|
1384 * requested, in the format expected to be used with nsILoginInfo. |
|
1385 */ |
|
1386 _getAuthTarget : function (aChannel, aAuthInfo) { |
|
1387 var hostname, realm; |
|
1388 |
|
1389 // If our proxy is demanding authentication, don't use the |
|
1390 // channel's actual destination. |
|
1391 if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { |
|
1392 this.log("getAuthTarget is for proxy auth"); |
|
1393 if (!(aChannel instanceof Ci.nsIProxiedChannel)) |
|
1394 throw "proxy auth needs nsIProxiedChannel"; |
|
1395 |
|
1396 var info = aChannel.proxyInfo; |
|
1397 if (!info) |
|
1398 throw "proxy auth needs nsIProxyInfo"; |
|
1399 |
|
1400 // Proxies don't have a scheme, but we'll use "moz-proxy://" |
|
1401 // so that it's more obvious what the login is for. |
|
1402 var idnService = Cc["@mozilla.org/network/idn-service;1"]. |
|
1403 getService(Ci.nsIIDNService); |
|
1404 hostname = "moz-proxy://" + |
|
1405 idnService.convertUTF8toACE(info.host) + |
|
1406 ":" + info.port; |
|
1407 realm = aAuthInfo.realm; |
|
1408 if (!realm) |
|
1409 realm = hostname; |
|
1410 |
|
1411 return [hostname, realm]; |
|
1412 } |
|
1413 |
|
1414 hostname = this._getFormattedHostname(aChannel.URI); |
|
1415 |
|
1416 // If a HTTP WWW-Authenticate header specified a realm, that value |
|
1417 // will be available here. If it wasn't set or wasn't HTTP, we'll use |
|
1418 // the formatted hostname instead. |
|
1419 realm = aAuthInfo.realm; |
|
1420 if (!realm) |
|
1421 realm = hostname; |
|
1422 |
|
1423 return [hostname, realm]; |
|
1424 }, |
|
1425 |
|
1426 |
|
1427 /** |
|
1428 * Returns [username, password] as extracted from aAuthInfo (which |
|
1429 * holds this info after having prompted the user). |
|
1430 * |
|
1431 * If the authentication was for a Windows domain, we'll prepend the |
|
1432 * return username with the domain. (eg, "domain\user") |
|
1433 */ |
|
1434 _GetAuthInfo : function (aAuthInfo) { |
|
1435 var username, password; |
|
1436 |
|
1437 var flags = aAuthInfo.flags; |
|
1438 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) |
|
1439 username = aAuthInfo.domain + "\\" + aAuthInfo.username; |
|
1440 else |
|
1441 username = aAuthInfo.username; |
|
1442 |
|
1443 password = aAuthInfo.password; |
|
1444 |
|
1445 return [username, password]; |
|
1446 }, |
|
1447 |
|
1448 |
|
1449 /** |
|
1450 * Given a username (possibly in DOMAIN\user form) and password, parses the |
|
1451 * domain out of the username if necessary and sets domain, username and |
|
1452 * password on the auth information object. |
|
1453 */ |
|
1454 _SetAuthInfo : function (aAuthInfo, username, password) { |
|
1455 var flags = aAuthInfo.flags; |
|
1456 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) { |
|
1457 // Domain is separated from username by a backslash |
|
1458 var idx = username.indexOf("\\"); |
|
1459 if (idx == -1) { |
|
1460 aAuthInfo.username = username; |
|
1461 } else { |
|
1462 aAuthInfo.domain = username.substring(0, idx); |
|
1463 aAuthInfo.username = username.substring(idx+1); |
|
1464 } |
|
1465 } else { |
|
1466 aAuthInfo.username = username; |
|
1467 } |
|
1468 aAuthInfo.password = password; |
|
1469 }, |
|
1470 |
|
1471 _newAsyncPromptConsumer : function(aCallback, aContext) { |
|
1472 return { |
|
1473 QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]), |
|
1474 callback: aCallback, |
|
1475 context: aContext, |
|
1476 cancel: function() { |
|
1477 this.callback.onAuthCancelled(this.context, false); |
|
1478 this.callback = null; |
|
1479 this.context = null; |
|
1480 } |
|
1481 } |
|
1482 } |
|
1483 |
|
1484 }; // end of LoginManagerPrompter implementation |
|
1485 |
|
1486 |
|
1487 var component = [LoginManagerPromptFactory, LoginManagerPrompter]; |
|
1488 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); |