diff options
Diffstat (limited to 'toolkit/components/passwordmgr/nsLoginManagerPrompter.js')
-rw-r--r-- | toolkit/components/passwordmgr/nsLoginManagerPrompter.js | 1701 |
1 files changed, 1701 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js new file mode 100644 index 000000000..b66489234 --- /dev/null +++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js @@ -0,0 +1,1701 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +const { PromptUtils } = Cu.import("resource://gre/modules/SharedPromptUtils.jsm", {}); + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); + +const LoginInfo = + Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", "init"); + +const BRAND_BUNDLE = "chrome://branding/locale/brand.properties"; + +/** + * Constants for password prompt telemetry. + * Mirrored in mobile/android/components/LoginManagerPrompter.js */ +const PROMPT_DISPLAYED = 0; + +const PROMPT_ADD_OR_UPDATE = 1; +const PROMPT_NOTNOW = 2; +const PROMPT_NEVER = 3; + +/** + * Implements nsIPromptFactory + * + * Invoked by [toolkit/components/prompts/src/nsPrompter.js] + */ +function LoginManagerPromptFactory() { + Services.obs.addObserver(this, "quit-application-granted", true); + Services.obs.addObserver(this, "passwordmgr-crypto-login", true); + Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true); +} + +LoginManagerPromptFactory.prototype = { + + classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + _asyncPrompts : {}, + _asyncPromptInProgress : false, + + observe : function (subject, topic, data) { + this.log("Observed: " + topic); + if (topic == "quit-application-granted") { + this._cancelPendingPrompts(); + } else if (topic == "passwordmgr-crypto-login") { + // Start processing the deferred prompters. + this._doAsyncPrompt(); + } else if (topic == "passwordmgr-crypto-loginCanceled") { + // User canceled a Master Password prompt, so go ahead and cancel + // all pending auth prompts to avoid nagging over and over. + this._cancelPendingPrompts(); + } + }, + + getPrompt : function (aWindow, aIID) { + var prompt = new LoginManagerPrompter().QueryInterface(aIID); + prompt.init(aWindow, this); + return prompt; + }, + + _doAsyncPrompt : function() { + if (this._asyncPromptInProgress) { + this.log("_doAsyncPrompt bypassed, already in progress"); + return; + } + + // Find the first prompt key we have in the queue + var hashKey = null; + for (hashKey in this._asyncPrompts) + break; + + if (!hashKey) { + this.log("_doAsyncPrompt:run bypassed, no prompts in the queue"); + return; + } + + // If login manger has logins for this host, defer prompting if we're + // already waiting on a master password entry. + var prompt = this._asyncPrompts[hashKey]; + var prompter = prompt.prompter; + var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo); + var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0); + if (!hasLogins && LoginHelper.schemeUpgrades && hostname.startsWith("https://")) { + let httpHostname = hostname.replace(/^https:\/\//, "http://"); + hasLogins = (prompter._pwmgr.countLogins(httpHostname, null, httpRealm) > 0); + } + if (hasLogins && prompter._pwmgr.uiBusy) { + this.log("_doAsyncPrompt:run bypassed, master password UI busy"); + return; + } + + // Allow only a limited number of authentication dialogs when they are all + // canceled by the user. + var cancelationCounter = (prompter._browser && prompter._browser.canceledAuthenticationPromptCounter) || { count: 0, id: 0 }; + if (prompt.channel) { + var httpChannel = prompt.channel.QueryInterface(Ci.nsIHttpChannel); + if (httpChannel) { + var windowId = httpChannel.topLevelContentWindowId; + if (windowId != cancelationCounter.id) { + // window has been reloaded or navigated, reset the counter + cancelationCounter = { count: 0, id: windowId }; + } + } + } + + var self = this; + + var runnable = { + cancel: false, + run : function() { + var ok = false; + if (!this.cancel) { + try { + self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'"); + ok = prompter.promptAuth(prompt.channel, + prompt.level, + prompt.authInfo); + } catch (e) { + if (e instanceof Components.Exception && + e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + self.log("_doAsyncPrompt:run bypassed, UI is not available in this context"); + } else { + Components.utils.reportError("LoginManagerPrompter: " + + "_doAsyncPrompt:run: " + e + "\n"); + } + } + + delete self._asyncPrompts[hashKey]; + prompt.inProgress = false; + self._asyncPromptInProgress = false; + + if (ok) { + cancelationCounter.count = 0; + } else { + cancelationCounter.count++; + } + if (prompter._browser) { + prompter._browser.canceledAuthenticationPromptCounter = cancelationCounter; + } + } + + for (var consumer of prompt.consumers) { + if (!consumer.callback) + // Not having a callback means that consumer didn't provide it + // or canceled the notification + continue; + + self.log("Calling back to " + consumer.callback + " ok=" + ok); + try { + if (ok) { + consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo); + } else { + consumer.callback.onAuthCancelled(consumer.context, !this.cancel); + } + } catch (e) { /* Throw away exceptions caused by callback */ } + } + self._doAsyncPrompt(); + } + }; + + var cancelDialogLimit = Services.prefs.getIntPref("prompts.authentication_dialog_abuse_limit"); + + this.log("cancelationCounter =", cancelationCounter); + if (cancelDialogLimit && cancelationCounter.count >= cancelDialogLimit) { + this.log("Blocking auth dialog, due to exceeding dialog bloat limit"); + delete this._asyncPrompts[hashKey]; + + // just make the runnable cancel all consumers + runnable.cancel = true; + } else { + this._asyncPromptInProgress = true; + prompt.inProgress = true; + } + + Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); + this.log("_doAsyncPrompt:run dispatched"); + }, + + + _cancelPendingPrompts : function() { + this.log("Canceling all pending prompts..."); + var asyncPrompts = this._asyncPrompts; + this.__proto__._asyncPrompts = {}; + + for (var hashKey in asyncPrompts) { + let prompt = asyncPrompts[hashKey]; + // Watch out! If this prompt is currently prompting, let it handle + // notifying the callbacks of success/failure, since it's already + // asking the user for input. Reusing a callback can be crashy. + if (prompt.inProgress) { + this.log("skipping a prompt in progress"); + continue; + } + + for (var consumer of prompt.consumers) { + if (!consumer.callback) + continue; + + this.log("Canceling async auth prompt callback " + consumer.callback); + try { + consumer.callback.onAuthCancelled(consumer.context, true); + } catch (e) { /* Just ignore exceptions from the callback */ } + } + } + }, +}; // end of LoginManagerPromptFactory implementation + +XPCOMUtils.defineLazyGetter(this.LoginManagerPromptFactory.prototype, "log", () => { + let logger = LoginHelper.createLogger("Login PromptFactory"); + return logger.log.bind(logger); +}); + + + + +/* ==================== 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 = { + + classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt, + Ci.nsIAuthPrompt2, + Ci.nsILoginManagerPrompter]), + + _factory : null, + _chromeWindow : null, + _browser : null, + _opener : null, + + __pwmgr : null, // Password Manager service + get _pwmgr() { + if (!this.__pwmgr) + this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + return this.__pwmgr; + }, + + __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 new Error("String bundle for Login Manager not present!"); + } + + return this.__strBundle; + }, + + + __ellipsis : null, + get _ellipsis() { + if (!this.__ellipsis) { + this.__ellipsis = "\u2026"; + try { + this.__ellipsis = Services.prefs.getComplexValue( + "intl.ellipsis", Ci.nsIPrefLocalizedString).data; + } catch (e) { } + } + return this.__ellipsis; + }, + + + // Whether we are in private browsing mode + get _inPrivateBrowsing() { + if (this._chromeWindow) { + return PrivateBrowsingUtils.isWindowPrivate(this._chromeWindow); + } + // If we don't that we're in private browsing mode if the caller did + // not provide a window. The callers which really care about this + // will indeed pass down a window to us, and for those who don't, + // we can just assume that we don't want to save the entered login + // information. + this.log("We have no chromeWindow so assume we're in a private context"); + return true; + }, + + + + + /* ---------- nsIAuthPrompt prompts ---------- */ + + + /** + * 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 new Components.Exception("prompt only supports SAVE_PASSWORD_NEVER", + Cr.NS_ERROR_NOT_IMPLEMENTED); + + this.log("===== prompt() called ====="); + + if (aDefaultText) { + aResult.value = aDefaultText; + } + + return this._promptService.prompt(this._chromeWindow, + aDialogTitle, aText, aResult, null, {}); + }, + + + /** + * 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 new Components.Exception("promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION", + Cr.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._chromeWindow, + 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; + } + + // 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. + let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + newLogin.init(hostname, null, realm, + aUsername.value, aPassword.value, "", ""); + 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._updateLogin(selectedLogin, newLogin); + } else { + this.log("Login unchanged, no further action needed."); + this._updateLogin(selectedLogin); + } + + return ok; + }, + + + /** + * 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 new Components.Exception("promptPassword doesn't support SAVE_PASSWORD_FOR_SESSION", + Cr.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._chromeWindow, 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 = Services.io.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 ---------- */ + + + + + /** + * Implementation of nsIAuthPrompt2. + * + * @param {nsIChannel} aChannel + * @param {int} aLevel + * @param {nsIAuthInformation} aAuthInfo + */ + promptAuth : function (aChannel, aLevel, aAuthInfo) { + var selectedLogin = null; + var checkbox = { value : false }; + var checkboxLabel = null; + var epicfail = false; + var canAutologin = false; + var notifyObj; + var foundLogins; + + 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. + this._removeLoginNotifications(); + + var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); + + // Looks for existing logins to prefill the prompt with. + foundLogins = LoginHelper.searchLoginsWithObject({ + hostname, + httpRealm, + schemeUpgrades: LoginHelper.schemeUpgrades, + }); + this.log("found", foundLogins.length, "matching logins."); + let resolveBy = [ + "scheme", + "timePasswordChanged", + ]; + foundLogins = LoginHelper.dedupeLogins(foundLogins, ["username"], resolveBy, hostname); + this.log(foundLogins.length, "matching logins remain after deduping"); + + // XXX Can't select from multiple accounts yet. (bug 227632) + if (foundLogins.length > 0) { + selectedLogin = foundLogins[0]; + this._SetAuthInfo(aAuthInfo, selectedLogin.username, + selectedLogin.password); + + // Allow automatic proxy login + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY && + !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) && + Services.prefs.getBoolPref("signon.autologin.proxy") && + !this._inPrivateBrowsing) { + + this.log("Autologin enabled, skipping auth prompt."); + canAutologin = true; + } + + 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. + notifyObj = this._getPopupNote() || this._getNotifyBox(); + if (canRememberLogin && !notifyObj) + 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 = canAutologin; + if (!ok) { + if (this._chromeWindow) + PromptUtils.fireDialogEvent(this._chromeWindow, "DOMWillOpenModalDialog", this._browser); + ok = this._promptService.promptAuth(this._chromeWindow, + 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 = notifyObj ? 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; + } + + // 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. + let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + newLogin.init(hostname, null, httpRealm, + username, password, "", ""); + if (!selectedLogin) { + this.log("New login seen for " + username + + " @ " + hostname + " (" + httpRealm + ")"); + + if (notifyObj) + this._showSaveLoginNotification(notifyObj, newLogin); + else + this._pwmgr.addLogin(newLogin); + } else if (password != selectedLogin.password) { + this.log("Updating password for " + username + + " @ " + hostname + " (" + httpRealm + ")"); + if (notifyObj) + this._showChangeLoginNotification(notifyObj, + selectedLogin, newLogin); + else + this._updateLogin(selectedLogin, newLogin); + } else { + this.log("Login unchanged, no further action needed."); + this._updateLogin(selectedLogin); + } + } catch (e) { + Components.utils.reportError("LoginManagerPrompter: " + + "Fail2 in promptAuth: " + e + "\n"); + } + + return ok; + }, + + asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) { + var cancelable = null; + + try { + this.log("===== asyncPromptAuth 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. + this._removeLoginNotifications(); + + cancelable = this._newAsyncPromptConsumer(aCallback, aContext); + + var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); + + var hashKey = aLevel + "|" + hostname + "|" + httpRealm; + this.log("Async prompt key = " + hashKey); + var asyncPrompt = this._factory._asyncPrompts[hashKey]; + if (asyncPrompt) { + this.log("Prompt bound to an existing one in the queue, callback = " + aCallback); + asyncPrompt.consumers.push(cancelable); + return cancelable; + } + + this.log("Adding new prompt to the queue, callback = " + aCallback); + asyncPrompt = { + consumers: [cancelable], + channel: aChannel, + authInfo: aAuthInfo, + level: aLevel, + inProgress : false, + prompter: this + }; + + this._factory._asyncPrompts[hashKey] = asyncPrompt; + this._factory._doAsyncPrompt(); + } catch (e) { + Components.utils.reportError("LoginManagerPrompter: " + + "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n"); + // Fail the prompt operation to let the consumer fall back + // to synchronous promptAuth method + throw e; + } + + return cancelable; + }, + + + + + /* ---------- nsILoginManagerPrompter prompts ---------- */ + + + init : function (aWindow = null, aFactory = null) { + if (!aWindow) { + // There may be no applicable window e.g. in a Sandbox or JSM. + this._chromeWindow = null; + this._browser = null; + } else if (aWindow instanceof Ci.nsIDOMChromeWindow) { + this._chromeWindow = aWindow; + // needs to be set explicitly using setBrowser + this._browser = null; + } else { + let {win, browser} = this._getChromeWindow(aWindow); + this._chromeWindow = win; + this._browser = browser; + } + this._opener = null; + this._factory = aFactory || null; + + this.log("===== initialized ====="); + }, + + set browser(aBrowser) { + this._browser = aBrowser; + }, + + set opener(aOpener) { + this._opener = aOpener; + }, + + promptToSavePassword : function (aLogin) { + this.log("promptToSavePassword"); + var notifyObj = this._getPopupNote() || this._getNotifyBox(); + if (notifyObj) + this._showSaveLoginNotification(notifyObj, aLogin); + else + this._showSaveLoginDialog(aLogin); + }, + + /** + * 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, "", + 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); + } + }, + + /** + * Displays the PopupNotifications.jsm doorhanger for password save or change. + * + * @param {nsILoginInfo} login + * Login to save or change. For changes, this login should contain the + * new password. + * @param {string} type + * This is "password-save" or "password-change" depending on the + * original notification type. This is used for telemetry and tests. + */ + _showLoginCaptureDoorhanger(login, type) { + let { browser } = this._getNotifyWindow(); + + let saveMsgNames = { + prompt: login.username === "" ? "rememberLoginMsgNoUser" + : "rememberLoginMsg", + buttonLabel: "rememberLoginButtonText", + buttonAccessKey: "rememberLoginButtonAccessKey", + }; + + let changeMsgNames = { + prompt: login.username === "" ? "updateLoginMsgNoUser" + : "updateLoginMsg", + buttonLabel: "updateLoginButtonText", + buttonAccessKey: "updateLoginButtonAccessKey", + }; + + let initialMsgNames = type == "password-save" ? saveMsgNames + : changeMsgNames; + + let brandBundle = Services.strings.createBundle(BRAND_BUNDLE); + let brandShortName = brandBundle.GetStringFromName("brandShortName"); + let promptMsg = type == "password-save" ? this._getLocalizedString(saveMsgNames.prompt, [brandShortName]) + : this._getLocalizedString(changeMsgNames.prompt); + + let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION" + : "PWMGR_PROMPT_UPDATE_ACTION"; + let histogram = Services.telemetry.getHistogramById(histogramName); + histogram.add(PROMPT_DISPLAYED); + + let chromeDoc = browser.ownerDocument; + + let currentNotification; + + let updateButtonStatus = (element) => { + let mainActionButton = chromeDoc.getAnonymousElementByAttribute(element.button, "anonid", "button"); + // Disable the main button inside the menu-button if the password field is empty. + if (login.password.length == 0) { + mainActionButton.setAttribute("disabled", true); + chromeDoc.getElementById("password-notification-password") + .classList.add("popup-notification-invalid-input"); + } else { + mainActionButton.removeAttribute("disabled"); + chromeDoc.getElementById("password-notification-password") + .classList.remove("popup-notification-invalid-input"); + } + }; + + let updateButtonLabel = () => { + let foundLogins = LoginHelper.searchLoginsWithObject({ + formSubmitURL: login.formSubmitURL, + hostname: login.hostname, + httpRealm: login.httpRealm, + schemeUpgrades: LoginHelper.schemeUpgrades, + }); + + let logins = this._filterUpdatableLogins(login, foundLogins); + let msgNames = (logins.length == 0) ? saveMsgNames : changeMsgNames; + + // Update the label based on whether this will be a new login or not. + let label = this._getLocalizedString(msgNames.buttonLabel); + let accessKey = this._getLocalizedString(msgNames.buttonAccessKey); + + // Update the labels for the next time the panel is opened. + currentNotification.mainAction.label = label; + currentNotification.mainAction.accessKey = accessKey; + + // Update the labels in real time if the notification is displayed. + let element = [...currentNotification.owner.panel.childNodes] + .find(n => n.notification == currentNotification); + if (element) { + element.setAttribute("buttonlabel", label); + element.setAttribute("buttonaccesskey", accessKey); + updateButtonStatus(element); + } + }; + + let writeDataToUI = () => { + // setAttribute is used since the <textbox> binding may not be attached yet. + chromeDoc.getElementById("password-notification-username") + .setAttribute("placeholder", usernamePlaceholder); + chromeDoc.getElementById("password-notification-username") + .setAttribute("value", login.username); + + let toggleCheckbox = chromeDoc.getElementById("password-notification-visibilityToggle"); + toggleCheckbox.removeAttribute("checked"); + let passwordField = chromeDoc.getElementById("password-notification-password"); + // Ensure the type is reset so the field is masked. + passwordField.setAttribute("type", "password"); + passwordField.setAttribute("value", login.password); + updateButtonLabel(); + }; + + let readDataFromUI = () => { + login.username = + chromeDoc.getElementById("password-notification-username").value; + login.password = + chromeDoc.getElementById("password-notification-password").value; + }; + + let onInput = () => { + readDataFromUI(); + updateButtonLabel(); + }; + + let onVisibilityToggle = (commandEvent) => { + let passwordField = chromeDoc.getElementById("password-notification-password"); + // Gets the caret position before changing the type of the textbox + let selectionStart = passwordField.selectionStart; + let selectionEnd = passwordField.selectionEnd; + passwordField.setAttribute("type", commandEvent.target.checked ? "" : "password"); + if (!passwordField.hasAttribute("focused")) { + return; + } + passwordField.selectionStart = selectionStart; + passwordField.selectionEnd = selectionEnd; + }; + + let persistData = () => { + let foundLogins = LoginHelper.searchLoginsWithObject({ + formSubmitURL: login.formSubmitURL, + hostname: login.hostname, + httpRealm: login.httpRealm, + schemeUpgrades: LoginHelper.schemeUpgrades, + }); + + let logins = this._filterUpdatableLogins(login, foundLogins); + + if (logins.length == 0) { + // The original login we have been provided with might have its own + // metadata, but we don't want it propagated to the newly created one. + Services.logins.addLogin(new LoginInfo(login.hostname, + login.formSubmitURL, + login.httpRealm, + login.username, + login.password, + login.usernameField, + login.passwordField)); + } else if (logins.length == 1) { + if (logins[0].password == login.password && + logins[0].username == login.username) { + // We only want to touch the login's use count and last used time. + this._updateLogin(logins[0]); + } else { + this._updateLogin(logins[0], login); + } + } else { + Cu.reportError("Unexpected match of multiple logins."); + } + }; + + // The main action is the "Remember" or "Update" button. + let mainAction = { + label: this._getLocalizedString(initialMsgNames.buttonLabel), + accessKey: this._getLocalizedString(initialMsgNames.buttonAccessKey), + callback: () => { + histogram.add(PROMPT_ADD_OR_UPDATE); + if (histogramName == "PWMGR_PROMPT_REMEMBER_ACTION") { + Services.obs.notifyObservers(null, 'LoginStats:NewSavedPassword', null); + } + readDataFromUI(); + persistData(); + browser.focus(); + } + }; + + // Include a "Never for this site" button when saving a new password. + let secondaryActions = type == "password-save" ? [{ + label: this._getLocalizedString("notifyBarNeverRememberButtonText"), + accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"), + callback: () => { + histogram.add(PROMPT_NEVER); + Services.logins.setLoginSavingEnabled(login.hostname, false); + browser.focus(); + } + }] : null; + + let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder"); + let togglePasswordLabel = this._getLocalizedString("togglePasswordLabel"); + let togglePasswordAccessKey = this._getLocalizedString("togglePasswordAccessKey"); + + this._getPopupNote().show( + browser, + "password", + promptMsg, + "password-notification-icon", + mainAction, + secondaryActions, + { + timeout: Date.now() + 10000, + displayURI: Services.io.newURI(login.hostname, null, null), + persistWhileVisible: true, + passwordNotificationType: type, + eventCallback: function (topic) { + switch (topic) { + case "showing": + currentNotification = this; + chromeDoc.getElementById("password-notification-password") + .removeAttribute("focused"); + chromeDoc.getElementById("password-notification-username") + .removeAttribute("focused"); + chromeDoc.getElementById("password-notification-username") + .addEventListener("input", onInput); + chromeDoc.getElementById("password-notification-password") + .addEventListener("input", onInput); + let toggleBtn = chromeDoc.getElementById("password-notification-visibilityToggle"); + + if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) { + toggleBtn.addEventListener("command", onVisibilityToggle); + toggleBtn.setAttribute("label", togglePasswordLabel); + toggleBtn.setAttribute("accesskey", togglePasswordAccessKey); + toggleBtn.setAttribute("hidden", LoginHelper.isMasterPasswordSet()); + } + if (this.wasDismissed) { + chromeDoc.getElementById("password-notification-visibilityToggle") + .setAttribute("hidden", true); + } + break; + case "shown": + writeDataToUI(); + break; + case "dismissed": + this.wasDismissed = true; + readDataFromUI(); + // Fall through. + case "removed": + currentNotification = null; + chromeDoc.getElementById("password-notification-username") + .removeEventListener("input", onInput); + chromeDoc.getElementById("password-notification-password") + .removeEventListener("input", onInput); + chromeDoc.getElementById("password-notification-visibilityToggle") + .removeEventListener("command", onVisibilityToggle); + break; + } + return false; + }, + } + ); + }, + + /** + * Displays a notification bar or a popup notification, 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. + * + * @param aNotifyObj + * A notification box or a popup notification. + * @param aLogin + * The login captured from the form. + */ + _showSaveLoginNotification : function (aNotifyObj, 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("notifyBarNeverRememberButtonText"); + var neverButtonAccessKey = + this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"); + var rememberButtonText = + this._getLocalizedString("notifyBarRememberPasswordButtonText"); + var rememberButtonAccessKey = + this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); + + var displayHost = this._getShortDisplayHost(aLogin.hostname); + var notificationText = this._getLocalizedString( + "rememberPasswordMsgNoUsername", + [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; + + // Notification is a PopupNotification + if (aNotifyObj == this._getPopupNote()) { + this._showLoginCaptureDoorhanger(aLogin, "password-save"); + } else { + var notNowButtonText = + this._getLocalizedString("notifyBarNotNowButtonText"); + var notNowButtonAccessKey = + this._getLocalizedString("notifyBarNotNowButtonAccessKey"); + var buttons = [ + // "Remember" button + { + label: rememberButtonText, + accessKey: rememberButtonAccessKey, + popup: null, + callback: function(aNotifyObj, aButton) { + pwmgr.addLogin(aLogin); + } + }, + + // "Never for this site" button + { + label: neverButtonText, + accessKey: neverButtonAccessKey, + popup: null, + callback: function(aNotifyObj, aButton) { + pwmgr.setLoginSavingEnabled(aLogin.hostname, false); + } + }, + + // "Not now" button + { + label: notNowButtonText, + accessKey: notNowButtonAccessKey, + popup: null, + callback: function() { /* NOP */ } + } + ]; + + this._showLoginNotification(aNotifyObj, "password-save", + notificationText, buttons); + } + + Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null); + }, + + _removeLoginNotifications : function () { + var popupNote = this._getPopupNote(); + if (popupNote) + popupNote = popupNote.getNotification("password"); + if (popupNote) + popupNote.remove(); + + var notifyBox = this._getNotifyBox(); + if (notifyBox) { + var oldBar = notifyBox.getNotificationWithValue("password-save"); + if (oldBar) { + this.log("Removing save-password notification bar."); + notifyBox.removeNotification(oldBar); + } + + oldBar = notifyBox.getNotificationWithValue("password-change"); + if (oldBar) { + this.log("Removing change-password notification bar."); + notifyBox.removeNotification(oldBar); + } + } + }, + + + /** + * 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 displayHost = this._getShortDisplayHost(aLogin.hostname); + + var dialogText; + if (aLogin.username) { + var displayUser = this._sanitizeUsername(aLogin.username); + dialogText = this._getLocalizedString( + "rememberPasswordMsg", + [displayUser, displayHost]); + } else { + dialogText = this._getLocalizedString( + "rememberPasswordMsgNoUsername", + [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._chromeWindow, + 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."); + } + + Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null); + }, + + + /** + * Called when we think we detect a password or username change for + * an existing login, when the form being submitted contains multiple + * password fields. + * + * @param {nsILoginInfo} aOldLogin + * The old login we may want to update. + * @param {nsILoginInfo} aNewLogin + * The new login from the page form. + */ + promptToChangePassword(aOldLogin, aNewLogin) { + this.log("promptToChangePassword"); + let notifyObj = this._getPopupNote() || this._getNotifyBox(); + + if (notifyObj) { + this._showChangeLoginNotification(notifyObj, aOldLogin, + aNewLogin); + } else { + this._showChangeLoginDialog(aOldLogin, aNewLogin); + } + }, + + /** + * Shows the Change Password notification bar or popup notification. + * + * @param aNotifyObj + * A notification box or a popup notification. + * + * @param aOldLogin + * The stored login we want to update. + * + * @param aNewLogin + * The login object with the changes we want to make. + */ + _showChangeLoginNotification(aNotifyObj, aOldLogin, aNewLogin) { + var changeButtonText = + this._getLocalizedString("notifyBarUpdateButtonText"); + var changeButtonAccessKey = + this._getLocalizedString("notifyBarUpdateButtonAccessKey"); + + // We reuse the existing message, even if it expects a username, until we + // switch to the final terminology in bug 1144856. + var displayHost = this._getShortDisplayHost(aOldLogin.hostname); + var notificationText = this._getLocalizedString("updatePasswordMsg", + [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 self = this; + + // Notification is a PopupNotification + if (aNotifyObj == this._getPopupNote()) { + aOldLogin.hostname = aNewLogin.hostname; + aOldLogin.formSubmitURL = aNewLogin.formSubmitURL; + aOldLogin.password = aNewLogin.password; + aOldLogin.username = aNewLogin.username; + this._showLoginCaptureDoorhanger(aOldLogin, "password-change"); + } else { + var dontChangeButtonText = + this._getLocalizedString("notifyBarDontChangeButtonText"); + var dontChangeButtonAccessKey = + this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); + var buttons = [ + // "Yes" button + { + label: changeButtonText, + accessKey: changeButtonAccessKey, + popup: null, + callback: function(aNotifyObj, aButton) { + self._updateLogin(aOldLogin, aNewLogin); + } + }, + + // "No" button + { + label: dontChangeButtonText, + accessKey: dontChangeButtonAccessKey, + popup: null, + callback: function(aNotifyObj, aButton) { + // do nothing + } + } + ]; + + this._showLoginNotification(aNotifyObj, "password-change", + notificationText, buttons); + } + + let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid; + Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID); + }, + + + /** + * Shows the Change Password dialog. + */ + _showChangeLoginDialog(aOldLogin, aNewLogin) { + const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; + + var dialogText; + if (aOldLogin.username) + dialogText = this._getLocalizedString( + "updatePasswordMsg", + [aOldLogin.username]); + else + dialogText = this._getLocalizedString( + "updatePasswordMsgNoUser"); + + var dialogTitle = this._getLocalizedString( + "passwordChangeTitle"); + + // returns 0 for yes, 1 for no. + var ok = !this._promptService.confirmEx(this._chromeWindow, + dialogTitle, dialogText, buttonFlags, + null, null, null, + null, {}); + if (ok) { + this.log("Updating password for user " + aOldLogin.username); + this._updateLogin(aOldLogin, aNewLogin); + } + + let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid; + Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID); + }, + + + /** + * 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) { + this.log("promptToChangePasswordWithUsernames with count:", count); + + var usernames = logins.map(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._chromeWindow, + dialogTitle, dialogText, + usernames.length, usernames, + selectedIndex); + if (ok) { + // Now that we know which login to use, modify its password. + var selectedLogin = logins[selectedIndex.value]; + this.log("Updating password for user " + selectedLogin.username); + var newLoginWithUsername = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + newLoginWithUsername.init(aNewLogin.hostname, + aNewLogin.formSubmitURL, aNewLogin.httpRealm, + selectedLogin.username, aNewLogin.password, + selectedLogin.userNameField, aNewLogin.passwordField); + this._updateLogin(selectedLogin, newLoginWithUsername); + } + }, + + + + + /* ---------- Internal Methods ---------- */ + + + + + _updateLogin(login, aNewLogin = null) { + var now = Date.now(); + var propBag = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + if (aNewLogin) { + propBag.setProperty("formSubmitURL", aNewLogin.formSubmitURL); + propBag.setProperty("hostname", aNewLogin.hostname); + propBag.setProperty("password", aNewLogin.password); + propBag.setProperty("username", aNewLogin.username); + // Explicitly set the password change time here (even though it would + // be changed automatically), to ensure that it's exactly the same + // value as timeLastUsed. + propBag.setProperty("timePasswordChanged", now); + } + propBag.setProperty("timeLastUsed", now); + propBag.setProperty("timesUsedIncrement", 1); + this._pwmgr.modifyLogin(login, propBag); + }, + + /** + * Given a content DOM window, returns the chrome window and browser it's in. + */ + _getChromeWindow: function (aWindow) { + let windows = Services.wm.getEnumerator(null); + while (windows.hasMoreElements()) { + let win = windows.getNext(); + let browser = win.gBrowser.getBrowserForContentWindow(aWindow); + if (browser) { + return { win, browser }; + } + } + return null; + }, + + _getNotifyWindow: function () { + // Some sites pop up a temporary login window, which disappears + // upon submission of credentials. We want to put the notification + // bar in the opener window if this seems to be happening. + if (this._opener) { + let chromeDoc = this._chromeWindow.document.documentElement; + + // 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") && !this._browser.canGoBack) { + this.log("Using opener window for notification bar."); + return this._getChromeWindow(this._opener); + } + } + + return { win: this._chromeWindow, browser: this._browser }; + }, + + /** + * Returns the popup notification to this prompter, + * or null if there isn't one available. + */ + _getPopupNote : function () { + let popupNote = null; + + try { + let { win: notifyWin } = this._getNotifyWindow(); + + // .wrappedJSObject needed here -- see bug 422974 comment 5. + popupNote = notifyWin.wrappedJSObject.PopupNotifications; + } catch (e) { + this.log("Popup notifications not available on window"); + } + + return popupNote; + }, + + + /** + * Returns the notification box to this prompter, or null if there isn't + * a notification box available. + */ + _getNotifyBox : function () { + let notifyBox = null; + + try { + let { win: notifyWin } = this._getNotifyWindow(); + + // .wrappedJSObject needed here -- see bug 422974 comment 5. + notifyBox = notifyWin.wrappedJSObject.getNotificationBox(notifyWin); + } catch (e) { + this.log("Notification bars not available on window"); + } + + return notifyBox; + }, + + + /** + * 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; + }, + + + /** + * 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); + return this._strBundle.GetStringFromName(key); + }, + + + /** + * 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, ""); + }, + + + /** + * 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) { + let uri; + if (aURI instanceof Ci.nsIURI) { + uri = aURI; + } else { + uri = Services.io.newURI(aURI, null, null); + } + + return uri.scheme + "://" + uri.hostPort; + }, + + + /** + * 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 = Services.io.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; + }, + + + /** + * 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 new Error("proxy auth needs nsIProxiedChannel"); + + var info = aChannel.proxyInfo; + if (!info) + throw new Error("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; + }, + + _newAsyncPromptConsumer : function(aCallback, aContext) { + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]), + callback: aCallback, + context: aContext, + cancel: function() { + this.callback.onAuthCancelled(this.context, false); + this.callback = null; + this.context = null; + } + }; + }, + + /** + * This function looks for existing logins that can be updated + * to match a submitted login, instead of creating a new one. + * + * Given a login and a loginList, it filters the login list + * to find every login with either the same username as aLogin + * or with the same password as aLogin and an empty username + * so the user can add a username. + * + * @param {nsILoginInfo} aLogin + * login to use as filter. + * @param {nsILoginInfo[]} aLoginList + * Array of logins to filter. + * @returns {nsILoginInfo[]} the filtered array of logins. + */ + _filterUpdatableLogins(aLogin, aLoginList) { + return aLoginList.filter(l => l.username == aLogin.username || + (l.password == aLogin.password && + !l.username)); + }, + +}; // end of LoginManagerPrompter implementation + +XPCOMUtils.defineLazyGetter(this.LoginManagerPrompter.prototype, "log", () => { + let logger = LoginHelper.createLogger("LoginManagerPrompter"); + return logger.log.bind(logger); +}); + +var component = [LoginManagerPromptFactory, LoginManagerPrompter]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); |