/* 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"); 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; /* * LoginManagerPromptFactory * * 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 && prompter._pwmgr.uiBusy) { this.log("_doAsyncPrompt:run bypassed, master password UI busy"); return; } // Set up a counter for ensuring that the basic auth prompt can not // be abused for DOS-style attacks. With this counter, each eTLD+1 // per browser will get a limited number of times a user can // cancel the prompt until we stop showing it. let browser = prompter._browser; let baseDomain = null; if (browser) { try { baseDomain = Services.eTLD.getBaseDomainFromHost(hostname); } catch (e) { baseDomain = hostname; } if (!browser.canceledAuthenticationPromptCounter) { browser.canceledAuthenticationPromptCounter = {}; } if (!browser.canceledAuthenticationPromptCounter[baseDomain]) { browser.canceledAuthenticationPromptCounter[baseDomain] = 0; } } 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 (browser) { // Reset the counter state if the user replied to a prompt and actually // tried to login (vs. simply clicking any button to get out). if (ok && (prompt.authInfo.username || prompt.authInfo.password)) { browser.canceledAuthenticationPromptCounter[baseDomain] = 0; } else { browser.canceledAuthenticationPromptCounter[baseDomain] += 1; } } } 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"); if (browser) { let cancelationCounter = browser.canceledAuthenticationPromptCounter[baseDomain]; this.log("cancelationCounter ="+ cancelationCounter); if (cancelDialogLimit && cancelationCounter >= 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; } } 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 ==================== */ /* * 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, _window : 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._window) { return PrivateBrowsingUtils.isContentWindowPrivate(this._window); } else { // 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. return true; } }, /* ---------- 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 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._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 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._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; } // 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; }, /* * 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 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._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 = 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 ---------- */ /* * 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; var canAutologin = 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. this._removeLoginNotifications(); 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); // 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. var notifyBox = this._getNotifyBox(); 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 = canAutologin; if (!ok) { if (this._window) PromptUtils.fireDialogEvent(this._window, "DOMWillOpenModalDialog", this._browser); 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; } // 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 + ")"); var notifyObj = this._getPopupNote() || notifyBox; if (notifyObj) this._showSaveLoginNotification(notifyObj, newLogin); else this._pwmgr.addLogin(newLogin); } else if (password != selectedLogin.password) { this.log("Updating password for " + username + " @ " + hostname + " (" + httpRealm + ")"); var notifyObj = this._getPopupNote() || notifyBox; 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 * */ init : function (aWindow, aFactory) { this._window = aWindow; this._factory = aFactory || null; this._browser = null; this._opener = null; this.log("===== initialized ====="); }, setE10sData : function (aBrowser, aOpener) { if (!(this._window instanceof Ci.nsIDOMChromeWindow)) throw new Error("Unexpected call"); this._browser = aBrowser; this._opener = aOpener; }, /* * promptToSavePassword * */ promptToSavePassword : function (aLogin) { var notifyObj = this._getPopupNote() || this._getNotifyBox(); if (notifyObj) this._showSaveLoginNotification(notifyObj, 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); } }, /** * 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 = Services.logins.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm); 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 binding may not be attached yet. chromeDoc.getElementById("password-notification-username") .setAttribute("placeholder", usernamePlaceholder); chromeDoc.getElementById("password-notification-username") .setAttribute("value", login.username); 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); if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) { passwordField.setAttribute("show-content", showPasswordPlaceholder); } else { passwordField.setAttribute("show-content", ""); } updateButtonLabel(); }; let readDataFromUI = () => { login.username = chromeDoc.getElementById("password-notification-username").value; login.password = chromeDoc.getElementById("password-notification-password").value; }; let onInput = () => { readDataFromUI(); updateButtonLabel(); }; let onPasswordFocus = (focusEvent) => { 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; if (focusEvent.rangeParent != null) { // Check for a click over the SHOW placeholder selectionStart = passwordField.value.length; selectionEnd = passwordField.value.length; } passwordField.setAttribute("type", ""); passwordField.selectionStart = selectionStart; passwordField.selectionEnd = selectionEnd; }; let onPasswordBlur = () => { // Use setAttribute in case the binding isn't applied. chromeDoc.getElementById("password-notification-password").setAttribute("type", "password"); }; let onNotificationClick = (clickEvent) => { // Removes focus from textboxes when we click elsewhere on the doorhanger. let focusedElement = Services.focus.focusedElement; if (!focusedElement || focusedElement.nodeName != "html:input") { // No input is focused so we don't need to blur return; } let focusedBindingParent = chromeDoc.getBindingParent(focusedElement); if (!focusedBindingParent || focusedBindingParent.nodeName != "textbox" || clickEvent.explicitOriginalTarget == focusedBindingParent) { // The focus wasn't in a textbox or the click was in the focused textbox. return; } focusedBindingParent.blur(); }; let persistData = () => { let foundLogins = Services.logins.findLogins({}, login.hostname, login.formSubmitURL, login.httpRealm); 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 showPasswordPlaceholder = this._getLocalizedString("showPasswordPlaceholder"); let displayHost = this._getShortDisplayHost(login.hostname); 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-username") .addEventListener("input", onInput); chromeDoc.getElementById("password-notification-password") .addEventListener("input", onInput); if (Services.prefs.getBoolPref("signon.rememberSignons.visibilityToggle")) { chromeDoc.getElementById("password-notification-password") .addEventListener("focus", onPasswordFocus); } chromeDoc.getElementById("password-notification-password") .addEventListener("blur", onPasswordBlur); break; case "shown": chromeDoc.getElementById("notification-popup") .addEventListener("click", onNotificationClick); writeDataToUI(); break; case "dismissed": readDataFromUI(); // Fall through. case "removed": currentNotification = null; chromeDoc.getElementById("notification-popup") .removeEventListener("click", onNotificationClick); chromeDoc.getElementById("password-notification-username") .removeEventListener("input", onInput); chromeDoc.getElementById("password-notification-password") .removeEventListener("input", onInput); chromeDoc.getElementById("password-notification-password") .removeEventListener("focus", onPasswordFocus); chromeDoc.getElementById("password-notification-password") .removeEventListener("blur", onPasswordBlur); break; } return false; }, } ); }, /* * _showSaveLoginNotification * * 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. */ _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); } }, /* * _removeLoginNotifications * */ _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); } } }, /* * _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 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._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."); } }, /** * 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) { let notifyObj = this._getPopupNote() || this._getNotifyBox(); if (notifyObj) { this._showChangeLoginNotification(notifyObj, aOldLogin, aNewLogin); } else { this._showChangeLoginDialog(aOldLogin, aNewLogin); } }, /* * _showChangeLoginNotification * * 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.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); } }, /* * _showChangeLoginDialog * * 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._window, dialogTitle, dialogText, buttonFlags, null, null, null, null, {}); if (ok) { this.log("Updating password for user " + aOldLogin.username); this._updateLogin(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(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 use, modify its password. var selectedLogin = logins[selectedIndex.value]; this.log("Updating password for user " + selectedLogin.username); this._updateLogin(selectedLogin, aNewLogin); } }, /* ---------- 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("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); }, /* * _getChromeWindow * * Given a content DOM window, returns the chrome window it's in. */ _getChromeWindow: function (aWindow) { // In e10s, aWindow may already be a chrome window. if (aWindow instanceof Ci.nsIDOMChromeWindow) return aWindow; var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .chromeEventHandler.ownerDocument.defaultView; return chromeWin; }, /* * _getNotifyWindow */ _getNotifyWindow: function () { try { // Get topmost window, in case we're in a frame. var notifyWin = this._window.top; var isE10s = (notifyWin instanceof Ci.nsIDOMChromeWindow); var useOpener = false; // 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 (notifyWin.opener) { var chromeDoc = this._getChromeWindow(notifyWin). document.documentElement; var hasHistory; if (isE10s) { if (!this._browser) throw new Error("Expected a browser in e10s"); hasHistory = this._browser.canGoBack; } else { var webnav = notifyWin. QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIWebNavigation); hasHistory = webnav.sessionHistory.count > 1; } // 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") && !hasHistory) { this.log("Using opener window for notification bar."); notifyWin = notifyWin.opener; useOpener = true; } } let browser; if (useOpener && this._opener && isE10s) { // In e10s, we have to reconstruct the opener browser from // the CPOW passed in the message (and then passed to us in // setE10sData). // NB: notifyWin is now the chrome window for the opening // window. browser = notifyWin.gBrowser.getBrowserForContentWindow(this._opener); } else if (isE10s) { browser = this._browser; } else { var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; browser = chromeWin.gBrowser .getBrowserForDocument(notifyWin.top.document); } return { notifyWin: notifyWin, browser: browser }; } catch (e) { // If any errors happen, just assume no notification box. this.log("Unable to get notify window: " + e.fileName + ":" + e.lineNumber + ": " + e.message); return null; } }, /* * _getPopupNote * * Returns the popup notification to this prompter, * or null if there isn't one available. */ _getPopupNote : function () { let popupNote = null; try { let { notifyWin } = this._getNotifyWindow(); // Get the chrome window for the content window we're using. // .wrappedJSObject needed here -- see bug 422974 comment 5. let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; popupNote = chromeWin.PopupNotifications; } catch (e) { this.log("Popup notifications not available on window"); } return popupNote; }, /* * _getNotifyBox * * Returns the notification box to this prompter, or null if there isn't * a notification box available. */ _getNotifyBox : function () { let notifyBox = null; try { let { notifyWin } = this._getNotifyWindow(); // Get the chrome window for the content window we're using. // .wrappedJSObject needed here -- see bug 422974 comment 5. let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; notifyBox = chromeWin.getNotificationBox(notifyWin); } catch (e) { this.log("Notification bars not available on window"); } 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, ""); }, /** * 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; }, /* * _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 = 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; }, /* * _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 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);