diff options
Diffstat (limited to 'tools/xo_bundle/components/nsLoginManagerPrompter.js')
-rwxr-xr-x | tools/xo_bundle/components/nsLoginManagerPrompter.js | 1229 |
1 files changed, 1229 insertions, 0 deletions
diff --git a/tools/xo_bundle/components/nsLoginManagerPrompter.js b/tools/xo_bundle/components/nsLoginManagerPrompter.js new file mode 100755 index 0000000..4af1738 --- /dev/null +++ b/tools/xo_bundle/components/nsLoginManagerPrompter.js @@ -0,0 +1,1229 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske <dolske@mozilla.com> (original author) + * Ehsan Akhgari <ehsan.akhgari@gmail.com> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +/* + * LoginManagerPromptFactory + * + * Implements nsIPromptFactory + * + * Invoked by NS_NewAuthPrompter2() + * [embedding/components/windowwatcher/src/nsPrompt.cpp] + */ +function LoginManagerPromptFactory() {} + +LoginManagerPromptFactory.prototype = { + + classDescription : "LoginManagerPromptFactory", + contractID : "@mozilla.org/passwordmanager/authpromptfactory;1", + classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory]), + + getPrompt : function (aWindow, aIID) { + var prompt = new LoginManagerPrompter().QueryInterface(aIID); + prompt.init(aWindow); + return prompt; + } +}; // end of LoginManagerPromptFactory implementation + + + + +/* ==================== LoginManagerPrompter ==================== */ + + + + +/* + * LoginManagerPrompter + * + * Implements interfaces for prompting the user to enter/save/change auth info. + * + * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox. + * + * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication + * (eg HTTP Authenticate, FTP login). + * + * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins + * found in HTML forms. + */ +function LoginManagerPrompter() {} + +LoginManagerPrompter.prototype = { + + classDescription : "LoginManagerPrompter", + contractID : "@mozilla.org/login-manager/prompter;1", + classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt, + Ci.nsIAuthPrompt2, + Ci.nsILoginManagerPrompter]), + + _window : null, + _debug : false, // mirrors signon.debug + + __pwmgr : null, // Password Manager service + get _pwmgr() { + if (!this.__pwmgr) + this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__pwmgr; + }, + + __logService : null, // Console logging service, used for debugging. + get _logService() { + if (!this.__logService) + this.__logService = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__logService; + }, + + __promptService : null, // Prompt service for user interaction + get _promptService() { + if (!this.__promptService) + this.__promptService = + Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService2); + return this.__promptService; + }, + + + __strBundle : null, // String bundle for L10N + get _strBundle() { + if (!this.__strBundle) { + var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + this.__strBundle = bunService.createBundle( + "chrome://passwordmgr/locale/passwordmgr.properties"); + if (!this.__strBundle) + throw "String bundle for Login Manager not present!"; + } + + return this.__strBundle; + }, + + + __brandBundle : null, // String bundle for L10N + get _brandBundle() { + if (!this.__brandBundle) { + var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + this.__brandBundle = bunService.createBundle( + "chrome://branding/locale/brand.properties"); + if (!this.__brandBundle) + throw "Branding string bundle not present!"; + } + + return this.__brandBundle; + }, + + + __ioService: null, // IO service for string -> nsIURI conversion + get _ioService() { + if (!this.__ioService) + this.__ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return this.__ioService; + }, + + + __ellipsis : null, + get _ellipsis() { + if (!this.__ellipsis) { + this.__ellipsis = "\u2026"; + try { + var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + this.__ellipsis = prefSvc.getComplexValue("intl.ellipsis", + Ci.nsIPrefLocalizedString).data; + } catch (e) { } + } + return this.__ellipsis; + }, + + + // Whether we are in private browsing mode + get _inPrivateBrowsing() { + // The Private Browsing service might not be available. + try { + var pbs = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); + return pbs.privateBrowsingEnabled; + } catch (e) { + return false; + } + }, + + + /* + * log + * + * Internal function for logging debug messages to the Error Console window. + */ + log : function (message) { + if (!this._debug) + return; + + dump("Pwmgr Prompter: " + message + "\n"); + this._logService.logStringMessage("Pwmgr Prompter: " + message); + }, + + + + + /* ---------- nsIAuthPrompt prompts ---------- */ + + + /* + * prompt + * + * Wrapper around the prompt service prompt. Saving random fields here + * doesn't really make sense and therefore isn't implemented. + */ + prompt : function (aDialogTitle, aText, aPasswordRealm, + aSavePassword, aDefaultText, aResult) { + if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + this.log("===== prompt() called ====="); + + if (aDefaultText) { + aResult.value = aDefaultText; + } + + return this._promptService.prompt(this._window, + aDialogTitle, aText, aResult, null, {}); + }, + + + /* + * promptUsernameAndPassword + * + * Looks up a username and password in the database. Will prompt the user + * with a dialog, even if a username and password are found. + */ + promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm, + aSavePassword, aUsername, aPassword) { + this.log("===== promptUsernameAndPassword() called ====="); + + if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + var selectedLogin = null; + var checkBox = { value : false }; + var checkBoxLabel = null; + var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm); + + // If hostname is null, we can't save this login. + if (hostname) { + var canRememberLogin; + if (this._inPrivateBrowsing) + canRememberLogin = false; + else + canRememberLogin = (aSavePassword == + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) && + this._pwmgr.getLoginSavingEnabled(hostname); + + // if checkBoxLabel is null, the checkbox won't be shown at all. + if (canRememberLogin) + checkBoxLabel = this._getLocalizedString("rememberPassword"); + + // Look for existing logins. + var foundLogins = this._pwmgr.findLogins({}, hostname, null, + realm); + + // XXX Like the original code, we can't deal with multiple + // account selection. (bug 227632) + if (foundLogins.length > 0) { + selectedLogin = foundLogins[0]; + + // If the caller provided a username, try to use it. If they + // provided only a password, this will try to find a password-only + // login (or return null if none exists). + if (aUsername.value) + selectedLogin = this._repickSelectedLogin(foundLogins, + aUsername.value); + + if (selectedLogin) { + checkBox.value = true; + aUsername.value = selectedLogin.username; + // If the caller provided a password, prefer it. + if (!aPassword.value) + aPassword.value = selectedLogin.password; + } + } + } + + var ok = this._promptService.promptUsernameAndPassword(this._window, + aDialogTitle, aText, aUsername, aPassword, + checkBoxLabel, checkBox); + + if (!ok || !checkBox.value || !hostname) + return ok; + + if (!aPassword.value) { + this.log("No password entered, so won't offer to save."); + return ok; + } + + var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + newLogin.init(hostname, null, realm, aUsername.value, aPassword.value, + "", ""); + + // XXX We can't prompt with multiple logins yet (bug 227632), so + // the entered login might correspond to an existing login + // other than the one we originally selected. + selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value); + + // If we didn't find an existing login, or if the username + // changed, save as a new login. + if (!selectedLogin) { + // add as new + this.log("New login seen for " + realm); + this._pwmgr.addLogin(newLogin); + } else if (aPassword.value != selectedLogin.password) { + // update password + this.log("Updating password for " + realm); + this._pwmgr.modifyLogin(selectedLogin, newLogin); + } else { + this.log("Login unchanged, no further action needed."); + } + + return ok; + }, + + + /* + * promptPassword + * + * If a password is found in the database for the password realm, it is + * returned straight away without displaying a dialog. + * + * If a password is not found in the database, the user will be prompted + * with a dialog with a text field and ok/cancel buttons. If the user + * allows it, then the password will be saved in the database. + */ + promptPassword : function (aDialogTitle, aText, aPasswordRealm, + aSavePassword, aPassword) { + this.log("===== promptPassword called() ====="); + + if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + var checkBox = { value : false }; + var checkBoxLabel = null; + var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm); + + username = decodeURIComponent(username); + + // If hostname is null, we can't save this login. + if (hostname && !this._inPrivateBrowsing) { + var canRememberLogin = (aSavePassword == + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) && + this._pwmgr.getLoginSavingEnabled(hostname); + + // if checkBoxLabel is null, the checkbox won't be shown at all. + if (canRememberLogin) + checkBoxLabel = this._getLocalizedString("rememberPassword"); + + if (!aPassword.value) { + // Look for existing logins. + var foundLogins = this._pwmgr.findLogins({}, hostname, null, + realm); + + // XXX Like the original code, we can't deal with multiple + // account selection (bug 227632). We can deal with finding the + // account based on the supplied username - but in this case we'll + // just return the first match. + for (var i = 0; i < foundLogins.length; ++i) { + if (foundLogins[i].username == username) { + aPassword.value = foundLogins[i].password; + // wallet returned straight away, so this mimics that code + return true; + } + } + } + } + + var ok = this._promptService.promptPassword(this._window, aDialogTitle, + aText, aPassword, + checkBoxLabel, checkBox); + + if (ok && checkBox.value && hostname && aPassword.value) { + var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + newLogin.init(hostname, null, realm, username, + aPassword.value, "", ""); + + this.log("New login seen for " + realm); + + this._pwmgr.addLogin(newLogin); + } + + return ok; + }, + + /* ---------- nsIAuthPrompt helpers ---------- */ + + + /** + * Given aRealmString, such as "http://user@example.com/foo", returns an + * array of: + * - the formatted hostname + * - the realm (hostname + path) + * - the username, if present + * + * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S] + * channels, e.g. "example.com:80 (httprealm)", null is returned for all + * arguments to let callers know the login can't be saved because we don't + * know whether it's http or https. + */ + _getRealmInfo : function (aRealmString) { + var httpRealm = /^.+ \(.+\)$/; + if (httpRealm.test(aRealmString)) + return [null, null, null]; + + var uri = this._ioService.newURI(aRealmString, null, null); + var pathname = ""; + + if (uri.path != "/") + pathname = uri.path; + + var formattedHostname = this._getFormattedHostname(uri); + + return [formattedHostname, formattedHostname + pathname, uri.username]; + }, + + /* ---------- nsIAuthPrompt2 prompts ---------- */ + + + + + /* + * promptAuth + * + * Implementation of nsIAuthPrompt2. + * + * nsIChannel aChannel + * int aLevel + * nsIAuthInformation aAuthInfo + */ + promptAuth : function (aChannel, aLevel, aAuthInfo) { + var selectedLogin = null; + var checkbox = { value : false }; + var checkboxLabel = null; + var epicfail = false; + + try { + + this.log("===== promptAuth called ====="); + + // If the user submits a login but it fails, we need to remove the + // notification bar that was displayed. Conveniently, the user will + // be prompted for authentication again, which brings us here. + var notifyBox = this._getNotifyBox(); + if (notifyBox) + this._removeLoginNotifications(notifyBox); + + var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); + + + // Looks for existing logins to prefill the prompt with. + var foundLogins = this._pwmgr.findLogins({}, + hostname, null, httpRealm); + this.log("found " + foundLogins.length + " matching logins."); + + // XXX Can't select from multiple accounts yet. (bug 227632) + if (foundLogins.length > 0) { + selectedLogin = foundLogins[0]; + this._SetAuthInfo(aAuthInfo, selectedLogin.username, + selectedLogin.password); + checkbox.value = true; + } + + var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname); + if (this._inPrivateBrowsing) + canRememberLogin = false; + + // if checkboxLabel is null, the checkbox won't be shown at all. + if (canRememberLogin && !notifyBox) + checkboxLabel = this._getLocalizedString("rememberPassword"); + } catch (e) { + // Ignore any errors and display the prompt anyway. + epicfail = true; + Components.utils.reportError("LoginManagerPrompter: " + + "Epic fail in promptAuth: " + e + "\n"); + } + + var ok = this._promptService.promptAuth(this._window, aChannel, + aLevel, aAuthInfo, checkboxLabel, checkbox); + + // If there's a notification box, use it to allow the user to + // determine if the login should be saved. If there isn't a + // notification box, only save the login if the user set the + // checkbox to do so. + var rememberLogin = notifyBox ? canRememberLogin : checkbox.value; + if (!ok || !rememberLogin || epicfail) + return ok; + + try { + var [username, password] = this._GetAuthInfo(aAuthInfo); + + if (!password) { + this.log("No password entered, so won't offer to save."); + return ok; + } + + var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + newLogin.init(hostname, null, httpRealm, + username, password, "", ""); + + // XXX We can't prompt with multiple logins yet (bug 227632), so + // the entered login might correspond to an existing login + // other than the one we originally selected. + selectedLogin = this._repickSelectedLogin(foundLogins, username); + + // If we didn't find an existing login, or if the username + // changed, save as a new login. + if (!selectedLogin) { + // add as new + this.log("New login seen for " + username + + " @ " + hostname + " (" + httpRealm + ")"); + if (notifyBox) + this._showSaveLoginNotification(notifyBox, newLogin); + else + this._pwmgr.addLogin(newLogin); + + } else if (password != selectedLogin.password) { + + this.log("Updating password for " + username + + " @ " + hostname + " (" + httpRealm + ")"); + if (notifyBox) + this._showChangeLoginNotification(notifyBox, + selectedLogin, newLogin); + else + this._pwmgr.modifyLogin(selectedLogin, newLogin); + + } else { + this.log("Login unchanged, no further action needed."); + } + } catch (e) { + Components.utils.reportError("LoginManagerPrompter: " + + "Fail2 in promptAuth: " + e + "\n"); + } + + return ok; + }, + + asyncPromptAuth : function () { + return NS_ERROR_NOT_IMPLEMENTED; + }, + + + + + /* ---------- nsILoginManagerPrompter prompts ---------- */ + + + + + /* + * init + * + */ + init : function (aWindow) { + this._window = aWindow; + + var prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch("signon."); + this._debug = prefBranch.getBoolPref("debug"); + this.log("===== initialized ====="); + }, + + + /* + * promptToSavePassword + * + */ + promptToSavePassword : function (aLogin) { + var notifyBox = this._getNotifyBox(); + + if (notifyBox) + this._showSaveLoginNotification(notifyBox, aLogin); + else + this._showSaveLoginDialog(aLogin); + }, + + + /* + * _showLoginNotification + * + * Displays a notification bar. + * + */ + _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) { + var oldBar = aNotifyBox.getNotificationWithValue(aName); + const priority = aNotifyBox.PRIORITY_INFO_MEDIUM; + + this.log("Adding new " + aName + " notification bar"); + var newBar = aNotifyBox.appendNotification( + aText, aName, + "chrome://mozapps/skin/passwordmgr/key.png", + priority, aButtons); + + // The page we're going to hasn't loaded yet, so we want to persist + // across the first location change. + newBar.persistence++; + + // Sites like Gmail perform a funky redirect dance before you end up + // at the post-authentication page. I don't see a good way to + // heuristically determine when to ignore such location changes, so + // we'll try ignoring location changes based on a time interval. + newBar.timeout = Date.now() + 20000; // 20 seconds + + if (oldBar) { + this.log("(...and removing old " + aName + " notification bar)"); + aNotifyBox.removeNotification(oldBar); + } + }, + + + /* + * _showSaveLoginNotification + * + * Displays a notification bar (rather than a popup), to allow the user to + * save the specified login. This allows the user to see the results of + * their login, and only save a login which they know worked. + * + */ + _showSaveLoginNotification : function (aNotifyBox, aLogin) { + + // Ugh. We can't use the strings from the popup window, because they + // have the access key marked in the string (eg "Mo&zilla"), along + // with some weird rules for handling access keys that do not occur + // in the string, for L10N. See commonDialog.js's setLabelForNode(). + var neverButtonText = + this._getLocalizedString("notifyBarNeverForSiteButtonText"); + var neverButtonAccessKey = + this._getLocalizedString("notifyBarNeverForSiteButtonAccessKey"); + var rememberButtonText = + this._getLocalizedString("notifyBarRememberButtonText"); + var rememberButtonAccessKey = + this._getLocalizedString("notifyBarRememberButtonAccessKey"); + var notNowButtonText = + this._getLocalizedString("notifyBarNotNowButtonText"); + var notNowButtonAccessKey = + this._getLocalizedString("notifyBarNotNowButtonAccessKey"); + + var brandShortName = + this._brandBundle.GetStringFromName("brandShortName"); + var displayHost = this._getShortDisplayHost(aLogin.hostname); + var notificationText; + if (aLogin.username) { + var displayUser = this._sanitizeUsername(aLogin.username); + notificationText = this._getLocalizedString( + "saveLoginText", + [brandShortName, displayUser, displayHost]); + } else { + notificationText = this._getLocalizedString( + "saveLoginTextNoUsername", + [brandShortName, displayHost]); + } + + // The callbacks in |buttons| have a closure to access the variables + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr + // without a getService() call. + var pwmgr = this._pwmgr; + + + var buttons = [ + // "Remember" button + { + label: rememberButtonText, + accessKey: rememberButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + pwmgr.addLogin(aLogin); + } + }, + + // "Never for this site" button + { + label: neverButtonText, + accessKey: neverButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + pwmgr.setLoginSavingEnabled(aLogin.hostname, false); + } + }, + + // "Not now" button + { + label: notNowButtonText, + accessKey: notNowButtonAccessKey, + popup: null, + callback: function() { /* NOP */ } + } + ]; + + this._showLoginNotification(aNotifyBox, "password-save", + notificationText, buttons); + }, + + + /* + * _removeLoginNotifications + * + */ + _removeLoginNotifications : function (aNotifyBox) { + var oldBar = aNotifyBox.getNotificationWithValue("password-save"); + if (oldBar) { + this.log("Removing save-password notification bar."); + aNotifyBox.removeNotification(oldBar); + } + + oldBar = aNotifyBox.getNotificationWithValue("password-change"); + if (oldBar) { + this.log("Removing change-password notification bar."); + aNotifyBox.removeNotification(oldBar); + } + }, + + + /* + * _showSaveLoginDialog + * + * Called when we detect a new login in a form submission, + * asks the user what to do. + * + */ + _showSaveLoginDialog : function (aLogin) { + const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + + (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2); + + var brandShortName = + this._brandBundle.GetStringFromName("brandShortName"); + var displayHost = this._getShortDisplayHost(aLogin.hostname); + + var dialogText; + if (aLogin.username) { + var displayUser = this._sanitizeUsername(aLogin.username); + dialogText = this._getLocalizedString( + "saveLoginText", + [brandShortName, displayUser, displayHost]); + } else { + dialogText = this._getLocalizedString( + "saveLoginTextNoUsername", + [brandShortName, displayHost]); + } + var dialogTitle = this._getLocalizedString( + "savePasswordTitle"); + var neverButtonText = this._getLocalizedString( + "neverForSiteButtonText"); + var rememberButtonText = this._getLocalizedString( + "rememberButtonText"); + var notNowButtonText = this._getLocalizedString( + "notNowButtonText"); + + this.log("Prompting user to save/ignore login"); + var userChoice = this._promptService.confirmEx(this._window, + dialogTitle, dialogText, + buttonFlags, rememberButtonText, + notNowButtonText, neverButtonText, + null, {}); + // Returns: + // 0 - Save the login + // 1 - Ignore the login this time + // 2 - Never save logins for this site + if (userChoice == 2) { + this.log("Disabling " + aLogin.hostname + " logins by request."); + this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false); + } else if (userChoice == 0) { + this.log("Saving login for " + aLogin.hostname); + this._pwmgr.addLogin(aLogin); + } else { + // userChoice == 1 --> just ignore the login. + this.log("Ignoring login."); + } + }, + + + /* + * promptToChangePassword + * + * Called when we think we detect a password change for an existing + * login, when the form being submitted contains multiple password + * fields. + * + */ + promptToChangePassword : function (aOldLogin, aNewLogin) { + var notifyBox = this._getNotifyBox(); + + if (notifyBox) + this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin); + else + this._showChangeLoginDialog(aOldLogin, aNewLogin); + }, + + + /* + * _showChangeLoginNotification + * + * Shows the Change Password notification bar. + * + */ + _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewLogin) { + var notificationText; + if (aOldLogin.username) + notificationText = this._getLocalizedString( + "passwordChangeText", + [aOldLogin.username]); + else + notificationText = this._getLocalizedString( + "passwordChangeTextNoUser"); + + var changeButtonText = + this._getLocalizedString("notifyBarChangeButtonText"); + var changeButtonAccessKey = + this._getLocalizedString("notifyBarChangeButtonAccessKey"); + var dontChangeButtonText = + this._getLocalizedString("notifyBarDontChangeButtonText"); + var dontChangeButtonAccessKey = + this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); + + // The callbacks in |buttons| have a closure to access the variables + // in scope here; set one to |this._pwmgr| so we can get back to pwmgr + // without a getService() call. + var pwmgr = this._pwmgr; + + var buttons = [ + // "Yes" button + { + label: changeButtonText, + accessKey: changeButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + pwmgr.modifyLogin(aOldLogin, aNewLogin); + } + }, + + // "No" button + { + label: dontChangeButtonText, + accessKey: dontChangeButtonAccessKey, + popup: null, + callback: function(aNotificationBar, aButton) { + // do nothing + } + } + ]; + + this._showLoginNotification(aNotifyBox, "password-change", + notificationText, buttons); + }, + + + /* + * _showChangeLoginDialog + * + * Shows the Change Password dialog. + * + */ + _showChangeLoginDialog : function (aOldLogin, aNewLogin) { + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; + + var dialogText; + if (aOldLogin.username) + dialogText = this._getLocalizedString( + "passwordChangeText", + [aOldLogin.username]); + else + dialogText = this._getLocalizedString( + "passwordChangeTextNoUser"); + + var dialogTitle = this._getLocalizedString( + "passwordChangeTitle"); + + // returns 0 for yes, 1 for no. + var ok = !this._promptService.confirmEx(this._window, + dialogTitle, dialogText, buttonFlags, + null, null, null, + null, {}); + if (ok) { + this.log("Updating password for user " + aOldLogin.username); + this._pwmgr.modifyLogin(aOldLogin, aNewLogin); + } + }, + + + /* + * promptToChangePasswordWithUsernames + * + * Called when we detect a password change in a form submission, but we + * don't know which existing login (username) it's for. Asks the user + * to select a username and confirm the password change. + * + * Note: The caller doesn't know the username for aNewLogin, so this + * function fills in .username and .usernameField with the values + * from the login selected by the user. + * + * Note; XPCOM stupidity: |count| is just |logins.length|. + */ + promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; + + var usernames = logins.map(function (l) l.username); + var dialogText = this._getLocalizedString("userSelectText"); + var dialogTitle = this._getLocalizedString("passwordChangeTitle"); + var selectedIndex = { value: null }; + + // If user selects ok, outparam.value is set to the index + // of the selected username. + var ok = this._promptService.select(this._window, + dialogTitle, dialogText, + usernames.length, usernames, + selectedIndex); + if (ok) { + // Now that we know which login to change the password for, + // update the missing username info in the aNewLogin. + + var selectedLogin = logins[selectedIndex.value]; + + this.log("Updating password for user " + selectedLogin.username); + + aNewLogin.username = selectedLogin.username; + aNewLogin.usernameField = selectedLogin.usernameField; + + this._pwmgr.modifyLogin(selectedLogin, aNewLogin); + } + }, + + + + + /* ---------- Internal Methods ---------- */ + + + + + /* + * _getNotifyBox + * + * Returns the notification box to this prompter, or null if there isn't + * a notification box available. + */ + _getNotifyBox : function () { + var notifyBox = null; + + // Given a content DOM window, returns the chrome window it's in. + function getChromeWindow(aWindow) { + var chromeWin = aWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + return chromeWin; + } + + try { + // Get topmost window, in case we're in a frame. + var notifyWindow = this._window.top + + // Some sites pop up a temporary login window, when disappears + // upon submission of credentials. We want to put the notification + // bar in the opener window if this seems to be happening. + if (notifyWindow.opener) { + var chromeDoc = getChromeWindow(notifyWindow) + .document.documentElement; + var webnav = notifyWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + + // Check to see if the current window was opened with chrome + // disabled, and if so use the opener window. But if the window + // has been used to visit other pages (ie, has a history), + // assume it'll stick around and *don't* use the opener. + if (chromeDoc.getAttribute("chromehidden") && + webnav.sessionHistory.count == 1) { + this.log("Using opener window for notification bar."); + notifyWindow = notifyWindow.opener; + } + } + + + // Get the chrome window for the content window we're using. + // .wrappedJSObject needed here -- see bug 422974 comment 5. + var chromeWin = getChromeWindow(notifyWindow).wrappedJSObject; + + if (chromeWin.getNotificationBox) + notifyBox = chromeWin.getNotificationBox(notifyWindow); + else + this.log("getNotificationBox() not available on window"); + + } catch (e) { + // If any errors happen, just assume no notification box. + this.log("No notification box available: " + e) + } + + return notifyBox; + }, + + + /* + * _repickSelectedLogin + * + * The user might enter a login that isn't the one we prefilled, but + * is the same as some other existing login. So, pick a login with a + * matching username, or return null. + */ + _repickSelectedLogin : function (foundLogins, username) { + for (var i = 0; i < foundLogins.length; i++) + if (foundLogins[i].username == username) + return foundLogins[i]; + return null; + }, + + + /* + * _getLocalizedString + * + * Can be called as: + * _getLocalizedString("key1"); + * _getLocalizedString("key2", ["arg1"]); + * _getLocalizedString("key3", ["arg1", "arg2"]); + * (etc) + * + * Returns the localized string for the specified key, + * formatted if required. + * + */ + _getLocalizedString : function (key, formatArgs) { + if (formatArgs) + return this._strBundle.formatStringFromName( + key, formatArgs, formatArgs.length); + else + return this._strBundle.GetStringFromName(key); + }, + + + /* + * _sanitizeUsername + * + * Sanitizes the specified username, by stripping quotes and truncating if + * it's too long. This helps prevent an evil site from messing with the + * "save password?" prompt too much. + */ + _sanitizeUsername : function (username) { + if (username.length > 30) { + username = username.substring(0, 30); + username += this._ellipsis; + } + return username.replace(/['"]/g, ""); + }, + + + /* + * _getFormattedHostname + * + * The aURI parameter may either be a string uri, or an nsIURI instance. + * + * Returns the hostname to use in a nsILoginInfo object (for example, + * "http://example.com"). + */ + _getFormattedHostname : function (aURI) { + var uri; + if (aURI instanceof Ci.nsIURI) { + uri = aURI; + } else { + uri = this._ioService.newURI(aURI, null, null); + } + var scheme = uri.scheme; + + var hostname = scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + port = uri.port; + if (port != -1) { + var handler = this._ioService.getProtocolHandler(scheme); + if (port != handler.defaultPort) + hostname += ":" + port; + } + + return hostname; + }, + + + /* + * _getShortDisplayHost + * + * Converts a login's hostname field (a URL) to a short string for + * prompting purposes. Eg, "http://foo.com" --> "foo.com", or + * "ftp://www.site.co.uk" --> "site.co.uk". + */ + _getShortDisplayHost: function (aURIString) { + var displayHost; + + var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. + getService(Ci.nsIEffectiveTLDService); + var idnService = Cc["@mozilla.org/network/idn-service;1"]. + getService(Ci.nsIIDNService); + try { + var uri = this._ioService.newURI(aURIString, null, null); + var baseDomain = eTLDService.getBaseDomain(uri); + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); + } catch (e) { + this.log("_getShortDisplayHost couldn't process " + aURIString); + } + + if (!displayHost) + displayHost = aURIString; + + return displayHost; + }, + + + /* + * _getAuthTarget + * + * Returns the hostname and realm for which authentication is being + * requested, in the format expected to be used with nsILoginInfo. + */ + _getAuthTarget : function (aChannel, aAuthInfo) { + var hostname, realm; + + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + this.log("getAuthTarget is for proxy auth"); + if (!(aChannel instanceof Ci.nsIProxiedChannel)) + throw "proxy auth needs nsIProxiedChannel"; + + var info = aChannel.proxyInfo; + if (!info) + throw "proxy auth needs nsIProxyInfo"; + + // Proxies don't have a scheme, but we'll use "moz-proxy://" + // so that it's more obvious what the login is for. + var idnService = Cc["@mozilla.org/network/idn-service;1"]. + getService(Ci.nsIIDNService); + hostname = "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + info.port; + realm = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + } + + hostname = this._getFormattedHostname(aChannel.URI); + + // If a HTTP WWW-Authenticate header specified a realm, that value + // will be available here. If it wasn't set or wasn't HTTP, we'll use + // the formatted hostname instead. + realm = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + }, + + + /** + * Returns [username, password] as extracted from aAuthInfo (which + * holds this info after having prompted the user). + * + * If the authentication was for a Windows domain, we'll prepend the + * return username with the domain. (eg, "domain\user") + */ + _GetAuthInfo : function (aAuthInfo) { + var username, password; + + var flags = aAuthInfo.flags; + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) + username = aAuthInfo.domain + "\\" + aAuthInfo.username; + else + username = aAuthInfo.username; + + password = aAuthInfo.password; + + return [username, password]; + }, + + + /** + * Given a username (possibly in DOMAIN\user form) and password, parses the + * domain out of the username if necessary and sets domain, username and + * password on the auth information object. + */ + _SetAuthInfo : function (aAuthInfo, username, password) { + var flags = aAuthInfo.flags; + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + var idx = username.indexOf("\\"); + if (idx == -1) { + aAuthInfo.username = username; + } else { + aAuthInfo.domain = username.substring(0, idx); + aAuthInfo.username = username.substring(idx+1); + } + } else { + aAuthInfo.username = username; + } + aAuthInfo.password = password; + } + +}; // end of LoginManagerPrompter implementation + + +var component = [LoginManagerPromptFactory, LoginManagerPrompter]; +function NSGetModule(compMgr, fileSpec) { + return XPCOMUtils.generateModule(component); +} |