Bug 462525 - username truncation code is unnecessarily duplicated in nsLoginManagerP...
[wine-gecko.git] / toolkit / components / passwordmgr / src / nsLoginManagerPrompter.js
blobc2b9a2f3b63e91e73804bcdcd08869b87339f77d
1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
14 * The Original Code is mozilla.org code.
16 * The Initial Developer of the Original Code is Mozilla Corporation.
17 * Portions created by the Initial Developer are Copyright (C) 2007
18 * the Initial Developer. All Rights Reserved.
20 * Contributor(s):
21 * Justin Dolske <dolske@mozilla.com> (original author)
22 * Ehsan Akhgari <ehsan.akhgari@gmail.com>
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
36 * ***** END LICENSE BLOCK ***** */
39 const Cc = Components.classes;
40 const Ci = Components.interfaces;
41 const Cr = Components.results;
43 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
46 * LoginManagerPromptFactory
48 * Implements nsIPromptFactory
50 * Invoked by NS_NewAuthPrompter2()
51 * [embedding/components/windowwatcher/src/nsPrompt.cpp]
53 function LoginManagerPromptFactory() {}
55 LoginManagerPromptFactory.prototype = {
57 classDescription : "LoginManagerPromptFactory",
58 contractID : "@mozilla.org/passwordmanager/authpromptfactory;1",
59 classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"),
60 QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory]),
62 getPrompt : function (aWindow, aIID) {
63 var prompt = new LoginManagerPrompter().QueryInterface(aIID);
64 prompt.init(aWindow);
65 return prompt;
67 }; // end of LoginManagerPromptFactory implementation
72 /* ==================== LoginManagerPrompter ==================== */
78 * LoginManagerPrompter
80 * Implements interfaces for prompting the user to enter/save/change auth info.
82 * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox.
84 * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication
85 * (eg HTTP Authenticate, FTP login).
87 * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
88 * found in HTML forms.
90 function LoginManagerPrompter() {}
92 LoginManagerPrompter.prototype = {
94 classDescription : "LoginManagerPrompter",
95 contractID : "@mozilla.org/login-manager/prompter;1",
96 classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"),
97 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt,
98 Ci.nsIAuthPrompt2,
99 Ci.nsILoginManagerPrompter]),
101 _window : null,
102 _debug : false, // mirrors signon.debug
104 __pwmgr : null, // Password Manager service
105 get _pwmgr() {
106 if (!this.__pwmgr)
107 this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
108 getService(Ci.nsILoginManager);
109 return this.__pwmgr;
112 __logService : null, // Console logging service, used for debugging.
113 get _logService() {
114 if (!this.__logService)
115 this.__logService = Cc["@mozilla.org/consoleservice;1"].
116 getService(Ci.nsIConsoleService);
117 return this.__logService;
120 __promptService : null, // Prompt service for user interaction
121 get _promptService() {
122 if (!this.__promptService)
123 this.__promptService =
124 Cc["@mozilla.org/embedcomp/prompt-service;1"].
125 getService(Ci.nsIPromptService2);
126 return this.__promptService;
130 __strBundle : null, // String bundle for L10N
131 get _strBundle() {
132 if (!this.__strBundle) {
133 var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
134 getService(Ci.nsIStringBundleService);
135 this.__strBundle = bunService.createBundle(
136 "chrome://passwordmgr/locale/passwordmgr.properties");
137 if (!this.__strBundle)
138 throw "String bundle for Login Manager not present!";
141 return this.__strBundle;
145 __brandBundle : null, // String bundle for L10N
146 get _brandBundle() {
147 if (!this.__brandBundle) {
148 var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
149 getService(Ci.nsIStringBundleService);
150 this.__brandBundle = bunService.createBundle(
151 "chrome://branding/locale/brand.properties");
152 if (!this.__brandBundle)
153 throw "Branding string bundle not present!";
156 return this.__brandBundle;
160 __ioService: null, // IO service for string -> nsIURI conversion
161 get _ioService() {
162 if (!this.__ioService)
163 this.__ioService = Cc["@mozilla.org/network/io-service;1"].
164 getService(Ci.nsIIOService);
165 return this.__ioService;
169 __ellipsis : null,
170 get _ellipsis() {
171 if (!this.__ellipsis) {
172 this.__ellipsis = "\u2026";
173 try {
174 var prefSvc = Cc["@mozilla.org/preferences-service;1"].
175 getService(Ci.nsIPrefBranch);
176 this.__ellipsis = prefSvc.getComplexValue("intl.ellipsis",
177 Ci.nsIPrefLocalizedString).data;
178 } catch (e) { }
180 return this.__ellipsis;
184 // Whether we are in private browsing mode
185 get _inPrivateBrowsing() {
186 // The Private Browsing service might not be available.
187 try {
188 var pbs = Cc["@mozilla.org/privatebrowsing;1"].
189 getService(Ci.nsIPrivateBrowsingService);
190 return pbs.privateBrowsingEnabled;
191 } catch (e) {
192 return false;
198 * log
200 * Internal function for logging debug messages to the Error Console window.
202 log : function (message) {
203 if (!this._debug)
204 return;
206 dump("Pwmgr Prompter: " + message + "\n");
207 this._logService.logStringMessage("Pwmgr Prompter: " + message);
213 /* ---------- nsIAuthPrompt prompts ---------- */
217 * prompt
219 * Wrapper around the prompt service prompt. Saving random fields here
220 * doesn't really make sense and therefore isn't implemented.
222 prompt : function (aDialogTitle, aText, aPasswordRealm,
223 aSavePassword, aDefaultText, aResult) {
224 if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER)
225 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
227 this.log("===== prompt() called =====");
229 if (aDefaultText) {
230 aResult.value = aDefaultText;
233 return this._promptService.prompt(this._window,
234 aDialogTitle, aText, aResult, null, {});
239 * promptUsernameAndPassword
241 * Looks up a username and password in the database. Will prompt the user
242 * with a dialog, even if a username and password are found.
244 promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm,
245 aSavePassword, aUsername, aPassword) {
246 this.log("===== promptUsernameAndPassword() called =====");
248 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
249 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
251 var selectedLogin = null;
252 var checkBox = { value : false };
253 var checkBoxLabel = null;
254 var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm);
256 // If hostname is null, we can't save this login.
257 if (hostname) {
258 var canRememberLogin;
259 if (this._inPrivateBrowsing)
260 canRememberLogin = false;
261 else
262 canRememberLogin = (aSavePassword ==
263 Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
264 this._pwmgr.getLoginSavingEnabled(hostname);
266 // if checkBoxLabel is null, the checkbox won't be shown at all.
267 if (canRememberLogin)
268 checkBoxLabel = this._getLocalizedString("rememberPassword");
270 // Look for existing logins.
271 var foundLogins = this._pwmgr.findLogins({}, hostname, null,
272 realm);
274 // XXX Like the original code, we can't deal with multiple
275 // account selection. (bug 227632)
276 if (foundLogins.length > 0) {
277 selectedLogin = foundLogins[0];
279 // If the caller provided a username, try to use it. If they
280 // provided only a password, this will try to find a password-only
281 // login (or return null if none exists).
282 if (aUsername.value)
283 selectedLogin = this._repickSelectedLogin(foundLogins,
284 aUsername.value);
286 if (selectedLogin) {
287 checkBox.value = true;
288 aUsername.value = selectedLogin.username;
289 // If the caller provided a password, prefer it.
290 if (!aPassword.value)
291 aPassword.value = selectedLogin.password;
296 var ok = this._promptService.promptUsernameAndPassword(this._window,
297 aDialogTitle, aText, aUsername, aPassword,
298 checkBoxLabel, checkBox);
300 if (!ok || !checkBox.value || !hostname)
301 return ok;
303 if (!aPassword.value) {
304 this.log("No password entered, so won't offer to save.");
305 return ok;
308 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
309 createInstance(Ci.nsILoginInfo);
310 newLogin.init(hostname, null, realm, aUsername.value, aPassword.value,
311 "", "");
313 // XXX We can't prompt with multiple logins yet (bug 227632), so
314 // the entered login might correspond to an existing login
315 // other than the one we originally selected.
316 selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value);
318 // If we didn't find an existing login, or if the username
319 // changed, save as a new login.
320 if (!selectedLogin) {
321 // add as new
322 this.log("New login seen for " + realm);
323 this._pwmgr.addLogin(newLogin);
324 } else if (aPassword.value != selectedLogin.password) {
325 // update password
326 this.log("Updating password for " + realm);
327 this._pwmgr.modifyLogin(selectedLogin, newLogin);
328 } else {
329 this.log("Login unchanged, no further action needed.");
332 return ok;
337 * promptPassword
339 * If a password is found in the database for the password realm, it is
340 * returned straight away without displaying a dialog.
342 * If a password is not found in the database, the user will be prompted
343 * with a dialog with a text field and ok/cancel buttons. If the user
344 * allows it, then the password will be saved in the database.
346 promptPassword : function (aDialogTitle, aText, aPasswordRealm,
347 aSavePassword, aPassword) {
348 this.log("===== promptPassword called() =====");
350 if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION)
351 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
353 var checkBox = { value : false };
354 var checkBoxLabel = null;
355 var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm);
357 // If hostname is null, we can't save this login.
358 if (hostname && !this._inPrivateBrowsing) {
359 var canRememberLogin = (aSavePassword ==
360 Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) &&
361 this._pwmgr.getLoginSavingEnabled(hostname);
363 // if checkBoxLabel is null, the checkbox won't be shown at all.
364 if (canRememberLogin)
365 checkBoxLabel = this._getLocalizedString("rememberPassword");
367 if (!aPassword.value) {
368 // Look for existing logins.
369 var foundLogins = this._pwmgr.findLogins({}, hostname, null,
370 realm);
372 // XXX Like the original code, we can't deal with multiple
373 // account selection (bug 227632). We can deal with finding the
374 // account based on the supplied username - but in this case we'll
375 // just return the first match.
376 for (var i = 0; i < foundLogins.length; ++i) {
377 if (foundLogins[i].username == username) {
378 aPassword.value = foundLogins[i].password;
379 // wallet returned straight away, so this mimics that code
380 return true;
386 var ok = this._promptService.promptPassword(this._window, aDialogTitle,
387 aText, aPassword,
388 checkBoxLabel, checkBox);
390 if (ok && checkBox.value && hostname && aPassword.value) {
391 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
392 createInstance(Ci.nsILoginInfo);
393 newLogin.init(hostname, null, realm, username,
394 aPassword.value, "", "");
396 this.log("New login seen for " + realm);
398 this._pwmgr.addLogin(newLogin);
401 return ok;
404 /* ---------- nsIAuthPrompt helpers ---------- */
408 * Given aRealmString, such as "http://user@example.com/foo", returns an
409 * array of:
410 * - the formatted hostname
411 * - the realm (hostname + path)
412 * - the username, if present
414 * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S]
415 * channels, e.g. "example.com:80 (httprealm)", null is returned for all
416 * arguments to let callers know the login can't be saved because we don't
417 * know whether it's http or https.
419 _getRealmInfo : function (aRealmString) {
420 var httpRealm = /^.+ \(.+\)$/;
421 if (httpRealm.test(aRealmString))
422 return [null, null, null];
424 var uri = this._ioService.newURI(aRealmString, null, null);
425 var pathname = "";
427 if (uri.path != "/")
428 pathname = uri.path;
430 var formattedHostname = this._getFormattedHostname(uri);
432 return [formattedHostname, formattedHostname + pathname, uri.username];
435 /* ---------- nsIAuthPrompt2 prompts ---------- */
441 * promptAuth
443 * Implementation of nsIAuthPrompt2.
445 * nsIChannel aChannel
446 * int aLevel
447 * nsIAuthInformation aAuthInfo
449 promptAuth : function (aChannel, aLevel, aAuthInfo) {
450 var selectedLogin = null;
451 var checkbox = { value : false };
452 var checkboxLabel = null;
453 var epicfail = false;
455 try {
457 this.log("===== promptAuth called =====");
459 // If the user submits a login but it fails, we need to remove the
460 // notification bar that was displayed. Conveniently, the user will
461 // be prompted for authentication again, which brings us here.
462 var notifyBox = this._getNotifyBox();
463 if (notifyBox)
464 this._removeLoginNotifications(notifyBox);
466 var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
469 // Looks for existing logins to prefill the prompt with.
470 var foundLogins = this._pwmgr.findLogins({},
471 hostname, null, httpRealm);
472 this.log("found " + foundLogins.length + " matching logins.");
474 // XXX Can't select from multiple accounts yet. (bug 227632)
475 if (foundLogins.length > 0) {
476 selectedLogin = foundLogins[0];
477 this._SetAuthInfo(aAuthInfo, selectedLogin.username,
478 selectedLogin.password);
479 checkbox.value = true;
482 var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname);
483 if (this._inPrivateBrowsing)
484 canRememberLogin = false;
486 // if checkboxLabel is null, the checkbox won't be shown at all.
487 if (canRememberLogin && !notifyBox)
488 checkboxLabel = this._getLocalizedString("rememberPassword");
489 } catch (e) {
490 // Ignore any errors and display the prompt anyway.
491 epicfail = true;
492 Components.utils.reportError("LoginManagerPrompter: " +
493 "Epic fail in promptAuth: " + e + "\n");
496 var ok = this._promptService.promptAuth(this._window, aChannel,
497 aLevel, aAuthInfo, checkboxLabel, checkbox);
499 // If there's a notification box, use it to allow the user to
500 // determine if the login should be saved. If there isn't a
501 // notification box, only save the login if the user set the
502 // checkbox to do so.
503 var rememberLogin = notifyBox ? canRememberLogin : checkbox.value;
504 if (!ok || !rememberLogin || epicfail)
505 return ok;
507 try {
508 var [username, password] = this._GetAuthInfo(aAuthInfo);
510 if (!password) {
511 this.log("No password entered, so won't offer to save.");
512 return ok;
515 var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
516 createInstance(Ci.nsILoginInfo);
517 newLogin.init(hostname, null, httpRealm,
518 username, password, "", "");
520 // XXX We can't prompt with multiple logins yet (bug 227632), so
521 // the entered login might correspond to an existing login
522 // other than the one we originally selected.
523 selectedLogin = this._repickSelectedLogin(foundLogins, username);
525 // If we didn't find an existing login, or if the username
526 // changed, save as a new login.
527 if (!selectedLogin) {
528 // add as new
529 this.log("New login seen for " + username +
530 " @ " + hostname + " (" + httpRealm + ")");
531 if (notifyBox)
532 this._showSaveLoginNotification(notifyBox, newLogin);
533 else
534 this._pwmgr.addLogin(newLogin);
536 } else if (password != selectedLogin.password) {
538 this.log("Updating password for " + username +
539 " @ " + hostname + " (" + httpRealm + ")");
540 if (notifyBox)
541 this._showChangeLoginNotification(notifyBox,
542 selectedLogin, newLogin);
543 else
544 this._pwmgr.modifyLogin(selectedLogin, newLogin);
546 } else {
547 this.log("Login unchanged, no further action needed.");
549 } catch (e) {
550 Components.utils.reportError("LoginManagerPrompter: " +
551 "Fail2 in promptAuth: " + e + "\n");
554 return ok;
557 asyncPromptAuth : function () {
558 return NS_ERROR_NOT_IMPLEMENTED;
564 /* ---------- nsILoginManagerPrompter prompts ---------- */
570 * init
573 init : function (aWindow) {
574 this._window = aWindow;
576 var prefBranch = Cc["@mozilla.org/preferences-service;1"].
577 getService(Ci.nsIPrefService).getBranch("signon.");
578 this._debug = prefBranch.getBoolPref("debug");
579 this.log("===== initialized =====");
584 * promptToSavePassword
587 promptToSavePassword : function (aLogin) {
588 var notifyBox = this._getNotifyBox();
590 if (notifyBox)
591 this._showSaveLoginNotification(notifyBox, aLogin);
592 else
593 this._showSaveLoginDialog(aLogin);
598 * _showLoginNotification
600 * Displays a notification bar.
603 _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
604 var oldBar = aNotifyBox.getNotificationWithValue(aName);
605 const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
607 this.log("Adding new " + aName + " notification bar");
608 var newBar = aNotifyBox.appendNotification(
609 aText, aName,
610 "chrome://mozapps/skin/passwordmgr/key.png",
611 priority, aButtons);
613 // The page we're going to hasn't loaded yet, so we want to persist
614 // across the first location change.
615 newBar.persistence++;
617 // Sites like Gmail perform a funky redirect dance before you end up
618 // at the post-authentication page. I don't see a good way to
619 // heuristically determine when to ignore such location changes, so
620 // we'll try ignoring location changes based on a time interval.
621 newBar.timeout = Date.now() + 20000; // 20 seconds
623 if (oldBar) {
624 this.log("(...and removing old " + aName + " notification bar)");
625 aNotifyBox.removeNotification(oldBar);
631 * _showSaveLoginNotification
633 * Displays a notification bar (rather than a popup), to allow the user to
634 * save the specified login. This allows the user to see the results of
635 * their login, and only save a login which they know worked.
638 _showSaveLoginNotification : function (aNotifyBox, aLogin) {
640 // Ugh. We can't use the strings from the popup window, because they
641 // have the access key marked in the string (eg "Mo&zilla"), along
642 // with some weird rules for handling access keys that do not occur
643 // in the string, for L10N. See commonDialog.js's setLabelForNode().
644 var neverButtonText =
645 this._getLocalizedString("notifyBarNeverForSiteButtonText");
646 var neverButtonAccessKey =
647 this._getLocalizedString("notifyBarNeverForSiteButtonAccessKey");
648 var rememberButtonText =
649 this._getLocalizedString("notifyBarRememberButtonText");
650 var rememberButtonAccessKey =
651 this._getLocalizedString("notifyBarRememberButtonAccessKey");
652 var notNowButtonText =
653 this._getLocalizedString("notifyBarNotNowButtonText");
654 var notNowButtonAccessKey =
655 this._getLocalizedString("notifyBarNotNowButtonAccessKey");
657 var brandShortName =
658 this._brandBundle.GetStringFromName("brandShortName");
659 var displayHost = this._getShortDisplayHost(aLogin.hostname);
660 var notificationText;
661 if (aLogin.username) {
662 var displayUser = this._sanitizeUsername(aLogin.username);
663 notificationText = this._getLocalizedString(
664 "saveLoginText",
665 [brandShortName, displayUser, displayHost]);
666 } else {
667 notificationText = this._getLocalizedString(
668 "saveLoginTextNoUsername",
669 [brandShortName, displayHost]);
672 // The callbacks in |buttons| have a closure to access the variables
673 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
674 // without a getService() call.
675 var pwmgr = this._pwmgr;
678 var buttons = [
679 // "Remember" button
681 label: rememberButtonText,
682 accessKey: rememberButtonAccessKey,
683 popup: null,
684 callback: function(aNotificationBar, aButton) {
685 pwmgr.addLogin(aLogin);
689 // "Never for this site" button
691 label: neverButtonText,
692 accessKey: neverButtonAccessKey,
693 popup: null,
694 callback: function(aNotificationBar, aButton) {
695 pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
699 // "Not now" button
701 label: notNowButtonText,
702 accessKey: notNowButtonAccessKey,
703 popup: null,
704 callback: function() { /* NOP */ }
708 this._showLoginNotification(aNotifyBox, "password-save",
709 notificationText, buttons);
714 * _removeLoginNotifications
717 _removeLoginNotifications : function (aNotifyBox) {
718 var oldBar = aNotifyBox.getNotificationWithValue("password-save");
719 if (oldBar) {
720 this.log("Removing save-password notification bar.");
721 aNotifyBox.removeNotification(oldBar);
724 oldBar = aNotifyBox.getNotificationWithValue("password-change");
725 if (oldBar) {
726 this.log("Removing change-password notification bar.");
727 aNotifyBox.removeNotification(oldBar);
733 * _showSaveLoginDialog
735 * Called when we detect a new login in a form submission,
736 * asks the user what to do.
739 _showSaveLoginDialog : function (aLogin) {
740 const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
741 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
742 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
743 (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
745 var brandShortName =
746 this._brandBundle.GetStringFromName("brandShortName");
747 var displayHost = this._getShortDisplayHost(aLogin.hostname);
749 var dialogText;
750 if (aLogin.username) {
751 var displayUser = this._sanitizeUsername(aLogin.username);
752 dialogText = this._getLocalizedString(
753 "saveLoginText",
754 [brandShortName, displayUser, displayHost]);
755 } else {
756 dialogText = this._getLocalizedString(
757 "saveLoginTextNoUsername",
758 [brandShortName, displayHost]);
760 var dialogTitle = this._getLocalizedString(
761 "savePasswordTitle");
762 var neverButtonText = this._getLocalizedString(
763 "neverForSiteButtonText");
764 var rememberButtonText = this._getLocalizedString(
765 "rememberButtonText");
766 var notNowButtonText = this._getLocalizedString(
767 "notNowButtonText");
769 this.log("Prompting user to save/ignore login");
770 var userChoice = this._promptService.confirmEx(this._window,
771 dialogTitle, dialogText,
772 buttonFlags, rememberButtonText,
773 notNowButtonText, neverButtonText,
774 null, {});
775 // Returns:
776 // 0 - Save the login
777 // 1 - Ignore the login this time
778 // 2 - Never save logins for this site
779 if (userChoice == 2) {
780 this.log("Disabling " + aLogin.hostname + " logins by request.");
781 this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
782 } else if (userChoice == 0) {
783 this.log("Saving login for " + aLogin.hostname);
784 this._pwmgr.addLogin(aLogin);
785 } else {
786 // userChoice == 1 --> just ignore the login.
787 this.log("Ignoring login.");
793 * promptToChangePassword
795 * Called when we think we detect a password change for an existing
796 * login, when the form being submitted contains multiple password
797 * fields.
800 promptToChangePassword : function (aOldLogin, aNewLogin) {
801 var notifyBox = this._getNotifyBox();
803 if (notifyBox)
804 this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin);
805 else
806 this._showChangeLoginDialog(aOldLogin, aNewLogin);
811 * _showChangeLoginNotification
813 * Shows the Change Password notification bar.
816 _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewLogin) {
817 var notificationText;
818 if (aOldLogin.username)
819 notificationText = this._getLocalizedString(
820 "passwordChangeText",
821 [aOldLogin.username]);
822 else
823 notificationText = this._getLocalizedString(
824 "passwordChangeTextNoUser");
826 var changeButtonText =
827 this._getLocalizedString("notifyBarChangeButtonText");
828 var changeButtonAccessKey =
829 this._getLocalizedString("notifyBarChangeButtonAccessKey");
830 var dontChangeButtonText =
831 this._getLocalizedString("notifyBarDontChangeButtonText");
832 var dontChangeButtonAccessKey =
833 this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
835 // The callbacks in |buttons| have a closure to access the variables
836 // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
837 // without a getService() call.
838 var pwmgr = this._pwmgr;
840 var buttons = [
841 // "Yes" button
843 label: changeButtonText,
844 accessKey: changeButtonAccessKey,
845 popup: null,
846 callback: function(aNotificationBar, aButton) {
847 pwmgr.modifyLogin(aOldLogin, aNewLogin);
851 // "No" button
853 label: dontChangeButtonText,
854 accessKey: dontChangeButtonAccessKey,
855 popup: null,
856 callback: function(aNotificationBar, aButton) {
857 // do nothing
862 this._showLoginNotification(aNotifyBox, "password-change",
863 notificationText, buttons);
868 * _showChangeLoginDialog
870 * Shows the Change Password dialog.
873 _showChangeLoginDialog : function (aOldLogin, aNewLogin) {
874 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
876 var dialogText;
877 if (aOldLogin.username)
878 dialogText = this._getLocalizedString(
879 "passwordChangeText",
880 [aOldLogin.username]);
881 else
882 dialogText = this._getLocalizedString(
883 "passwordChangeTextNoUser");
885 var dialogTitle = this._getLocalizedString(
886 "passwordChangeTitle");
888 // returns 0 for yes, 1 for no.
889 var ok = !this._promptService.confirmEx(this._window,
890 dialogTitle, dialogText, buttonFlags,
891 null, null, null,
892 null, {});
893 if (ok) {
894 this.log("Updating password for user " + aOldLogin.username);
895 this._pwmgr.modifyLogin(aOldLogin, aNewLogin);
901 * promptToChangePasswordWithUsernames
903 * Called when we detect a password change in a form submission, but we
904 * don't know which existing login (username) it's for. Asks the user
905 * to select a username and confirm the password change.
907 * Note: The caller doesn't know the username for aNewLogin, so this
908 * function fills in .username and .usernameField with the values
909 * from the login selected by the user.
911 * Note; XPCOM stupidity: |count| is just |logins.length|.
913 promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
914 const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
916 var usernames = logins.map(function (l) l.username);
917 var dialogText = this._getLocalizedString("userSelectText");
918 var dialogTitle = this._getLocalizedString("passwordChangeTitle");
919 var selectedIndex = { value: null };
921 // If user selects ok, outparam.value is set to the index
922 // of the selected username.
923 var ok = this._promptService.select(this._window,
924 dialogTitle, dialogText,
925 usernames.length, usernames,
926 selectedIndex);
927 if (ok) {
928 // Now that we know which login to change the password for,
929 // update the missing username info in the aNewLogin.
931 var selectedLogin = logins[selectedIndex.value];
933 this.log("Updating password for user " + selectedLogin.username);
935 aNewLogin.username = selectedLogin.username;
936 aNewLogin.usernameField = selectedLogin.usernameField;
938 this._pwmgr.modifyLogin(selectedLogin, aNewLogin);
945 /* ---------- Internal Methods ---------- */
951 * _getNotifyBox
953 * Returns the notification box to this prompter, or null if there isn't
954 * a notification box available.
956 _getNotifyBox : function () {
957 var notifyBox = null;
959 // Given a content DOM window, returns the chrome window it's in.
960 function getChromeWindow(aWindow) {
961 var chromeWin = aWindow
962 .QueryInterface(Ci.nsIInterfaceRequestor)
963 .getInterface(Ci.nsIWebNavigation)
964 .QueryInterface(Ci.nsIDocShellTreeItem)
965 .rootTreeItem
966 .QueryInterface(Ci.nsIInterfaceRequestor)
967 .getInterface(Ci.nsIDOMWindow)
968 .QueryInterface(Ci.nsIDOMChromeWindow);
969 return chromeWin;
972 try {
973 // Get topmost window, in case we're in a frame.
974 var notifyWindow = this._window.top
976 // Some sites pop up a temporary login window, when disappears
977 // upon submission of credentials. We want to put the notification
978 // bar in the opener window if this seems to be happening.
979 if (notifyWindow.opener) {
980 var chromeDoc = getChromeWindow(notifyWindow)
981 .document.documentElement;
982 var webnav = notifyWindow
983 .QueryInterface(Ci.nsIInterfaceRequestor)
984 .getInterface(Ci.nsIWebNavigation);
986 // Check to see if the current window was opened with chrome
987 // disabled, and if so use the opener window. But if the window
988 // has been used to visit other pages (ie, has a history),
989 // assume it'll stick around and *don't* use the opener.
990 if (chromeDoc.getAttribute("chromehidden") &&
991 webnav.sessionHistory.count == 1) {
992 this.log("Using opener window for notification bar.");
993 notifyWindow = notifyWindow.opener;
998 // Get the chrome window for the content window we're using.
999 // .wrappedJSObject needed here -- see bug 422974 comment 5.
1000 var chromeWin = getChromeWindow(notifyWindow).wrappedJSObject;
1002 if (chromeWin.getNotificationBox)
1003 notifyBox = chromeWin.getNotificationBox(notifyWindow);
1004 else
1005 this.log("getNotificationBox() not available on window");
1007 } catch (e) {
1008 // If any errors happen, just assume no notification box.
1009 this.log("No notification box available: " + e)
1012 return notifyBox;
1017 * _repickSelectedLogin
1019 * The user might enter a login that isn't the one we prefilled, but
1020 * is the same as some other existing login. So, pick a login with a
1021 * matching username, or return null.
1023 _repickSelectedLogin : function (foundLogins, username) {
1024 for (var i = 0; i < foundLogins.length; i++)
1025 if (foundLogins[i].username == username)
1026 return foundLogins[i];
1027 return null;
1032 * _getLocalizedString
1034 * Can be called as:
1035 * _getLocalizedString("key1");
1036 * _getLocalizedString("key2", ["arg1"]);
1037 * _getLocalizedString("key3", ["arg1", "arg2"]);
1038 * (etc)
1040 * Returns the localized string for the specified key,
1041 * formatted if required.
1044 _getLocalizedString : function (key, formatArgs) {
1045 if (formatArgs)
1046 return this._strBundle.formatStringFromName(
1047 key, formatArgs, formatArgs.length);
1048 else
1049 return this._strBundle.GetStringFromName(key);
1054 * _sanitizeUsername
1056 * Sanitizes the specified username, by stripping quotes and truncating if
1057 * it's too long. This helps prevent an evil site from messing with the
1058 * "save password?" prompt too much.
1060 _sanitizeUsername : function (username) {
1061 if (username.length > 30) {
1062 username = username.substring(0, 30);
1063 username += this._ellipsis;
1065 return username.replace(/['"]/g, "");
1070 * _getFormattedHostname
1072 * The aURI parameter may either be a string uri, or an nsIURI instance.
1074 * Returns the hostname to use in a nsILoginInfo object (for example,
1075 * "http://example.com").
1077 _getFormattedHostname : function (aURI) {
1078 var uri;
1079 if (aURI instanceof Ci.nsIURI) {
1080 uri = aURI;
1081 } else {
1082 uri = this._ioService.newURI(aURI, null, null);
1084 var scheme = uri.scheme;
1086 var hostname = scheme + "://" + uri.host;
1088 // If the URI explicitly specified a port, only include it when
1089 // it's not the default. (We never want "http://foo.com:80")
1090 port = uri.port;
1091 if (port != -1) {
1092 var handler = this._ioService.getProtocolHandler(scheme);
1093 if (port != handler.defaultPort)
1094 hostname += ":" + port;
1097 return hostname;
1102 * _getShortDisplayHost
1104 * Converts a login's hostname field (a URL) to a short string for
1105 * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
1106 * "ftp://www.site.co.uk" --> "site.co.uk".
1108 _getShortDisplayHost: function (aURIString) {
1109 var displayHost;
1111 var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
1112 getService(Ci.nsIEffectiveTLDService);
1113 var idnService = Cc["@mozilla.org/network/idn-service;1"].
1114 getService(Ci.nsIIDNService);
1115 try {
1116 var uri = this._ioService.newURI(aURIString, null, null);
1117 var baseDomain = eTLDService.getBaseDomain(uri);
1118 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
1119 } catch (e) {
1120 this.log("_getShortDisplayHost couldn't process " + aURIString);
1123 if (!displayHost)
1124 displayHost = aURIString;
1126 return displayHost;
1131 * _getAuthTarget
1133 * Returns the hostname and realm for which authentication is being
1134 * requested, in the format expected to be used with nsILoginInfo.
1136 _getAuthTarget : function (aChannel, aAuthInfo) {
1137 var hostname, realm;
1139 // If our proxy is demanding authentication, don't use the
1140 // channel's actual destination.
1141 if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
1142 this.log("getAuthTarget is for proxy auth");
1143 if (!(aChannel instanceof Ci.nsIProxiedChannel))
1144 throw "proxy auth needs nsIProxiedChannel";
1146 var info = aChannel.proxyInfo;
1147 if (!info)
1148 throw "proxy auth needs nsIProxyInfo";
1150 // Proxies don't have a scheme, but we'll use "moz-proxy://"
1151 // so that it's more obvious what the login is for.
1152 var idnService = Cc["@mozilla.org/network/idn-service;1"].
1153 getService(Ci.nsIIDNService);
1154 hostname = "moz-proxy://" +
1155 idnService.convertUTF8toACE(info.host) +
1156 ":" + info.port;
1157 realm = aAuthInfo.realm;
1158 if (!realm)
1159 realm = hostname;
1161 return [hostname, realm];
1164 hostname = this._getFormattedHostname(aChannel.URI);
1166 // If a HTTP WWW-Authenticate header specified a realm, that value
1167 // will be available here. If it wasn't set or wasn't HTTP, we'll use
1168 // the formatted hostname instead.
1169 realm = aAuthInfo.realm;
1170 if (!realm)
1171 realm = hostname;
1173 return [hostname, realm];
1178 * Returns [username, password] as extracted from aAuthInfo (which
1179 * holds this info after having prompted the user).
1181 * If the authentication was for a Windows domain, we'll prepend the
1182 * return username with the domain. (eg, "domain\user")
1184 _GetAuthInfo : function (aAuthInfo) {
1185 var username, password;
1187 var flags = aAuthInfo.flags;
1188 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
1189 username = aAuthInfo.domain + "\\" + aAuthInfo.username;
1190 else
1191 username = aAuthInfo.username;
1193 password = aAuthInfo.password;
1195 return [username, password];
1200 * Given a username (possibly in DOMAIN\user form) and password, parses the
1201 * domain out of the username if necessary and sets domain, username and
1202 * password on the auth information object.
1204 _SetAuthInfo : function (aAuthInfo, username, password) {
1205 var flags = aAuthInfo.flags;
1206 if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
1207 // Domain is separated from username by a backslash
1208 var idx = username.indexOf("\\");
1209 if (idx == -1) {
1210 aAuthInfo.username = username;
1211 } else {
1212 aAuthInfo.domain = username.substring(0, idx);
1213 aAuthInfo.username = username.substring(idx+1);
1215 } else {
1216 aAuthInfo.username = username;
1218 aAuthInfo.password = password;
1221 }; // end of LoginManagerPrompter implementation
1224 var component = [LoginManagerPromptFactory, LoginManagerPrompter];
1225 function NSGetModule(compMgr, fileSpec) {
1226 return XPCOMUtils.generateModule(component);