Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
6 const Cc = Components.classes;
7 const Ci = Components.interfaces;
8 const Cr = Components.results;
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");
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 }
27 LoginManagerPromptFactory.prototype = {
29 classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
30 QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]),
32 _debug : false,
33 _asyncPrompts : {},
34 _asyncPromptInProgress : false,
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 },
50 getPrompt : function (aWindow, aIID) {
51 var prefBranch = Services.prefs.getBranch("signon.");
52 this._debug = prefBranch.getBoolPref("debug");
54 var prompt = new LoginManagerPrompter().QueryInterface(aIID);
55 prompt.init(aWindow, this);
56 return prompt;
57 },
59 _doAsyncPrompt : function() {
60 if (this._asyncPromptInProgress) {
61 this.log("_doAsyncPrompt bypassed, already in progress");
62 return;
63 }
65 // Find the first prompt key we have in the queue
66 var hashKey = null;
67 for (hashKey in this._asyncPrompts)
68 break;
70 if (!hashKey) {
71 this.log("_doAsyncPrompt:run bypassed, no prompts in the queue");
72 return;
73 }
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 }
86 this._asyncPromptInProgress = true;
87 prompt.inProgress = true;
89 var self = this;
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 }
107 delete self._asyncPrompts[hashKey];
108 prompt.inProgress = false;
109 self._asyncPromptInProgress = false;
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;
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 }
129 Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
130 this.log("_doAsyncPrompt:run dispatched");
131 },
134 _cancelPendingPrompts : function() {
135 this.log("Canceling all pending prompts...");
136 var asyncPrompts = this._asyncPrompts;
137 this.__proto__._asyncPrompts = {};
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 }
148 for each (var consumer in prompt.consumers) {
149 if (!consumer.callback)
150 continue;
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 },
161 log : function (message) {
162 if (!this._debug)
163 return;
165 dump("Pwmgr PromptFactory: " + message + "\n");
166 Services.console.logStringMessage("Pwmgr PrompFactory: " + message);
167 }
168 }; // end of LoginManagerPromptFactory implementation
173 /* ==================== LoginManagerPrompter ==================== */
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() {}
193 LoginManagerPrompter.prototype = {
195 classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
196 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
197 Ci.nsIAuthPrompt2,
198 Ci.nsILoginManagerPrompter]),
200 _factory : null,
201 _window : null,
202 _debug : false, // mirrors signon.debug
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 },
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 },
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 }
233 return this.__strBundle;
234 },
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 },
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 },
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;
274 dump("Pwmgr Prompter: " + message + "\n");
275 Services.console.logStringMessage("Pwmgr Prompter: " + message);
276 },
281 /* ---------- nsIAuthPrompt prompts ---------- */
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;
295 this.log("===== prompt() called =====");
297 if (aDefaultText) {
298 aResult.value = aDefaultText;
299 }
301 return this._promptService.prompt(this._window,
302 aDialogTitle, aText, aResult, null, {});
303 },
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 =====");
316 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
317 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
319 var selectedLogin = null;
320 var checkBox = { value : false };
321 var checkBoxLabel = null;
322 var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
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);
334 // if checkBoxLabel is null, the checkbox won't be shown at all.
335 if (canRememberLogin)
336 checkBoxLabel = this._getLocalizedString("rememberPassword");
338 // Look for existing logins.
339 var foundLogins = this._pwmgr.findLogins({}, hostname, null,
340 realm);
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];
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);
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 }
364 var ok = this._promptService.promptUsernameAndPassword(this._window,
365 aDialogTitle, aText, aUsername, aPassword,
366 checkBoxLabel, checkBox);
368 if (!ok || !checkBox.value || !hostname)
369 return ok;
371 if (!aPassword.value) {
372 this.log("No password entered, so won't offer to save.");
373 return ok;
374 }
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);
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 }
400 return ok;
401 },
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() =====");
418 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
419 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
421 var checkBox = { value : false };
422 var checkBoxLabel = null;
423 var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
425 username = decodeURIComponent(username);
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);
433 // if checkBoxLabel is null, the checkbox won't be shown at all.
434 if (canRememberLogin)
435 checkBoxLabel = this._getLocalizedString("rememberPassword");
437 if (!aPassword.value) {
438 // Look for existing logins.
439 var foundLogins = this._pwmgr.findLogins({}, hostname, null,
440 realm);
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 }
456 var ok = this._promptService.promptPassword(this._window, aDialogTitle,
457 aText, aPassword,
458 checkBoxLabel, checkBox);
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, "", "");
466 this.log("New login seen for " + realm);
468 this._pwmgr.addLogin(newLogin);
469 }
471 return ok;
472 },
474 /* ---------- nsIAuthPrompt helpers ---------- */
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];
494 var uri = Services.io.newURI(aRealmString, null, null);
495 var pathname = "";
497 if (uri.path != "/")
498 pathname = uri.path;
500 var formattedHostname = this._getFormattedHostname(uri);
502 return [formattedHostname, formattedHostname + pathname, uri.username];
503 },
505 /* ---------- nsIAuthPrompt2 prompts ---------- */
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;
526 try {
528 this.log("===== promptAuth called =====");
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();
535 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
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.");
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);
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) {
555 this.log("Autologin enabled, skipping auth prompt.");
556 canAutologin = true;
557 }
559 checkbox.value = true;
560 }
562 var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
563 if (this._inPrivateBrowsing)
564 canRememberLogin = false;
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 }
577 var ok = canAutologin ||
578 this._promptService.promptAuth(this._window,
579 aChannel, aLevel, aAuthInfo,
580 checkboxLabel, checkbox);
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;
590 try {
591 var [username, password] = this._GetAuthInfo(aAuthInfo);
593 if (!password) {
594 this.log("No password entered, so won't offer to save.");
595 return ok;
596 }
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);
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 + ")");
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);
619 } else if (password != selectedLogin.password) {
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);
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 }
639 return ok;
640 },
642 asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) {
643 var cancelable = null;
645 try {
646 this.log("===== asyncPromptAuth called =====");
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();
653 cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
655 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
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 }
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 }
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 }
687 return cancelable;
688 },
693 /* ---------- nsILoginManagerPrompter prompts ---------- */
698 /*
699 * init
700 *
701 */
702 init : function (aWindow, aFactory) {
703 this._window = aWindow;
704 this._factory = aFactory || null;
706 var prefBranch = Services.prefs.getBranch("signon.");
707 this._debug = prefBranch.getBoolPref("debug");
708 this.log("===== initialized =====");
709 },
712 /*
713 * promptToSavePassword
714 *
715 */
716 promptToSavePassword : function (aLogin) {
717 var notifyObj = this._getPopupNote() || this._getNotifyBox();
719 if (notifyObj)
720 this._showSaveLoginNotification(notifyObj, aLogin);
721 else
722 this._showSaveLoginDialog(aLogin);
723 },
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;
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);
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++;
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
752 if (oldBar) {
753 this.log("(...and removing old " + aName + " notification bar)");
754 aNotifyBox.removeNotification(oldBar);
755 }
756 },
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) {
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");
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 }
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;
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 };
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 ];
826 var notifyWin = this._getNotifyWindow();
827 var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
828 var browser = chromeWin.gBrowser.
829 getBrowserForDocument(notifyWin.top.document);
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 },
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 },
861 // "Not now" button
862 {
863 label: notNowButtonText,
864 accessKey: notNowButtonAccessKey,
865 popup: null,
866 callback: function() { /* NOP */ }
867 }
868 ];
870 this._showLoginNotification(aNotifyObj, "password-save",
871 notificationText, buttons);
872 }
873 },
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();
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 }
895 oldBar = notifyBox.getNotificationWithValue("password-change");
896 if (oldBar) {
897 this.log("Removing change-password notification bar.");
898 notifyBox.removeNotification(oldBar);
899 }
900 }
901 },
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);
917 var displayHost = this._getShortDisplayHost(aLogin.hostname);
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]);
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");
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 },
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();
974 if (notifyObj)
975 this._showChangeLoginNotification(notifyObj, aOldLogin,
976 aNewLogin.password);
977 else
978 this._showChangeLoginDialog(aOldLogin, aNewLogin.password);
979 },
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 }
1002 var changeButtonText =
1003 this._getLocalizedString("notifyBarUpdateButtonText");
1004 var changeButtonAccessKey =
1005 this._getLocalizedString("notifyBarUpdateButtonAccessKey");
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;
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 };
1024 var notifyWin = this._getNotifyWindow();
1025 var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
1026 var browser = chromeWin.gBrowser.
1027 getBrowserForDocument(notifyWin.top.document);
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 },
1049 // "No" button
1050 {
1051 label: dontChangeButtonText,
1052 accessKey: dontChangeButtonAccessKey,
1053 popup: null,
1054 callback: function(aNotifyObj, aButton) {
1055 // do nothing
1056 }
1057 }
1058 ];
1060 this._showLoginNotification(aNotifyObj, "password-change",
1061 notificationText, buttons);
1062 }
1063 },
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;
1075 var dialogText;
1076 if (aOldLogin.username)
1077 dialogText = this._getLocalizedString(
1078 "updatePasswordMsg",
1079 [aOldLogin.username]);
1080 else
1081 dialogText = this._getLocalizedString(
1082 "updatePasswordMsgNoUser");
1084 var dialogTitle = this._getLocalizedString(
1085 "passwordChangeTitle");
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 },
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;
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 };
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 },
1137 /* ---------- Internal Methods ---------- */
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 },
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 },
1176 /*
1177 * _getNotifyWindow
1178 */
1179 _getNotifyWindow: function () {
1181 try {
1182 // Get topmost window, in case we're in a frame.
1183 var notifyWin = this._window.top;
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);
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 }
1206 return notifyWin;
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 },
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;
1225 try {
1226 let notifyWin = this._getNotifyWindow();
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;
1232 popupNote = chromeWin.PopupNotifications;
1233 } catch (e) {
1234 this.log("Popup notifications not available on window");
1235 }
1237 return popupNote;
1238 },
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;
1250 try {
1251 let notifyWin = this._getNotifyWindow();
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;
1257 notifyBox = chromeWin.getNotificationBox(notifyWin);
1258 } catch (e) {
1259 this.log("Notification bars not available on window");
1260 }
1262 return notifyBox;
1263 },
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 },
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 },
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 },
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;
1336 var hostname = scheme + "://" + uri.host;
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 }
1347 return hostname;
1348 },
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;
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 }
1373 if (!displayHost)
1374 displayHost = aURIString;
1376 return displayHost;
1377 },
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;
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";
1396 var info = aChannel.proxyInfo;
1397 if (!info)
1398 throw "proxy auth needs nsIProxyInfo";
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;
1411 return [hostname, realm];
1412 }
1414 hostname = this._getFormattedHostname(aChannel.URI);
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;
1423 return [hostname, realm];
1424 },
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;
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;
1443 password = aAuthInfo.password;
1445 return [username, password];
1446 },
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 },
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 }
1484 }; // end of LoginManagerPrompter implementation
1487 var component = [LoginManagerPromptFactory, LoginManagerPrompter];
1488 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);