/* 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/. */ import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { showConfirmation } from "resource://gre/modules/FillHelpers.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", }); XPCOMUtils.defineLazyServiceGetter( lazy, "usernameAutocompleteSearch", "@mozilla.org/autocomplete/search;1?name=login-doorhanger-username", "nsIAutoCompleteSimpleSearch" ); ChromeUtils.defineLazyGetter(lazy, "l10n", () => { return new Localization(["toolkit/passwordmgr/passwordmgr.ftl"], true); }); const LoginInfo = Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init" ); /** * The maximum age of the password in ms (using `timePasswordChanged`) whereby * a user can toggle the password visibility in a doorhanger to add a username to * a saved login. */ const VISIBILITY_TOGGLE_MAX_PW_AGE_MS = 2 * 60 * 1000; // 2 minutes /** * Constants for password prompt telemetry. */ const PROMPT_DISPLAYED = 0; const PROMPT_ADD_OR_UPDATE = 1; const PROMPT_NOTNOW_OR_DONTUPDATE = 2; const PROMPT_NEVER = 3; const PROMPT_DELETE = 3; /** * The minimum age of a doorhanger in ms before it will get removed after a locationchange */ const NOTIFICATION_TIMEOUT_MS = 10 * 1000; // 10 seconds /** * The minimum age of an attention-requiring dismissed doorhanger in ms * before it will get removed after a locationchange */ const ATTENTION_NOTIFICATION_TIMEOUT_MS = 60 * 1000; // 1 minute function autocompleteSelected(popup) { const doc = popup.ownerDocument; const nameField = doc.getElementById("password-notification-username"); const passwordField = doc.getElementById("password-notification-password"); const activeElement = nameField.ownerDocument.activeElement; if (activeElement == nameField) { popup.onUsernameSelect(); } else if (activeElement == passwordField) { popup.onPasswordSelect(); } } const observer = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), // nsIObserver observe(subject, topic, _data) { switch (topic) { case "autocomplete-did-enter-text": { const input = subject.QueryInterface(Ci.nsIAutoCompleteInput); autocompleteSelected(input.popupElement); break; } } }, }; /** * Implements interfaces for prompting the user to enter/save/change login info * found in HTML forms. */ export class LoginManagerPrompter { get classID() { return Components.ID("{c47ff942-9678-44a5-bc9b-05e0d676c79c}"); } get QueryInterface() { return ChromeUtils.generateQI(["nsILoginManagerPrompter"]); } /** * Called when we detect a password or username that is not yet saved as * an existing login. * * @param {Element} aBrowser * The browser element that the request came from. * @param {nsILoginInfo} aLogin * The new login from the page form. * @param {boolean} [dismissed = false] * If the prompt should be automatically dismissed on being shown. * @param {boolean} [notifySaved = false] * Whether the notification should indicate that a login has been saved * @param {string} [autoSavedLoginGuid = ""] * A guid value for the old login to be removed if the changes match it * to a different login * @param {object?} possibleValues * Contains values from anything that we think, but are not sure, might be * a username or password. Has two properties, 'usernames' and 'passwords'. * @param {Set} possibleValues.usernames * @param {Set} possibleValues.passwords */ promptToSavePassword( aBrowser, aLogin, dismissed = false, notifySaved = false, autoFilledLoginGuid = "", possibleValues = undefined ) { lazy.log.debug("Prompting user to save login."); const inPrivateBrowsing = PrivateBrowsingUtils.isBrowserPrivate(aBrowser); const notification = LoginManagerPrompter._showLoginCaptureDoorhanger( aBrowser, aLogin, "password-save", { dismissed: inPrivateBrowsing || dismissed, extraAttr: notifySaved ? "attention" : "", }, possibleValues, { notifySaved, autoFilledLoginGuid, } ); Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save"); return { dismiss() { const { PopupNotifications } = aBrowser.ownerGlobal.wrappedJSObject; PopupNotifications.remove(notification); }, }; } /** * Displays the PopupNotifications.sys.mjs doorhanger for password save or change. * * @param {Element} browser * The browser to show the doorhanger on. * @param {nsILoginInfo} login * Login to save or change. For changes, this login should contain the * new password and/or username * @param {string} type * This is "password-save" or "password-change" depending on the * original notification type. This is used for telemetry and tests. * @param {object} showOptions * Options to pass along to PopupNotifications.show(). * @param {bool} [options.notifySaved = false] * Whether to indicate to the user that the login was already saved. * @param {string} [options.messageStringID = undefined] * An optional string ID to override the default message. * @param {string} [options.autoSavedLoginGuid = ""] * A string guid value for the auto-saved login to be removed if the changes * match it to a different login * @param {string} [options.autoFilledLoginGuid = ""] * A string guid value for the autofilled login * @param {object?} possibleValues * Contains values from anything that we think, but are not sure, might be * a username or password. Has two properties, 'usernames' and 'passwords'. * @param {Set} possibleValues.usernames * @param {Set} possibleValues.passwords */ static _showLoginCaptureDoorhanger( browser, login, type, showOptions = {}, possibleValues = undefined, { notifySaved = false, messageStringID, autoSavedLoginGuid = "", autoFilledLoginGuid = "", } = {} ) { lazy.log.debug( `Got autoSavedLoginGuid: ${autoSavedLoginGuid} and autoFilledLoginGuid ${autoFilledLoginGuid}.` ); const saveMessageIds = { prompt: "password-manager-save-password-message", mainButton: "password-manager-save-password-button-allow", secondaryButton: "password-manager-save-password-button-deny", }; const changeMessageIds = { prompt: messageStringID ?? "password-manager-update-password-message", mainButton: "password-manager-password-password-button-allow", secondaryButton: "password-manager-update-password-button-deny", }; const initialMessageIds = type == "password-save" ? saveMessageIds : changeMessageIds; const promptId = initialMessageIds.prompt; const host = this._getShortDisplayHost(login.origin); const promptMessage = lazy.l10n.formatValueSync(promptId, { host }); const histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION" : "PWMGR_PROMPT_UPDATE_ACTION"; const histogram = Services.telemetry.getHistogramById(histogramName); const chromeDoc = browser.ownerDocument; let currentNotification; const wasModifiedEvent = { // Values are mutated did_edit_un: "false", did_select_un: "false", did_edit_pw: "false", did_select_pw: "false", }; const updateButtonStatus = element => { const mainActionButton = element.button; // Disable the main button inside the menu-button if the password field is empty. if (!login.password.length) { 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"); } }; const updateButtonLabel = () => { if (!currentNotification) { console.error("updateButtonLabel, no currentNotification"); } const foundLogins = lazy.LoginHelper.searchLoginsWithObject({ formActionOrigin: login.formActionOrigin, origin: login.origin, httpRealm: login.httpRealm, schemeUpgrades: lazy.LoginHelper.schemeUpgrades, }); const logins = this._filterUpdatableLogins( login, foundLogins, autoSavedLoginGuid ); const messageIds = !logins.length ? saveMessageIds : changeMessageIds; // Update the label based on whether this will be a new login or not. const mainButton = this.getLabelAndAccessKey(messageIds.mainButton); // Update the labels for the next time the panel is opened. currentNotification.mainAction.label = mainButton.label; currentNotification.mainAction.accessKey = mainButton.accessKey; // Update the labels in real time if the notification is displayed. const element = [...currentNotification.owner.panel.childNodes].find( n => n.notification == currentNotification ); if (element) { element.setAttribute("buttonlabel", mainButton.label); element.setAttribute("buttonaccesskey", mainButton.accessKey); updateButtonStatus(element); } }; const writeDataToUI = () => { const nameField = chromeDoc.getElementById( "password-notification-username" ); nameField.placeholder = usernamePlaceholder; nameField.value = login.username; const toggleCheckbox = chromeDoc.getElementById( "password-notification-visibilityToggle" ); toggleCheckbox.removeAttribute("checked"); const passwordField = chromeDoc.getElementById( "password-notification-password" ); // Ensure the type is reset so the field is masked. passwordField.type = "password"; passwordField.value = login.password; updateButtonLabel(); }; const readDataFromUI = () => { login.username = chromeDoc.getElementById( "password-notification-username" ).value; login.password = chromeDoc.getElementById( "password-notification-password" ).value; }; const onInput = () => { readDataFromUI(); updateButtonLabel(); }; const onUsernameInput = () => { wasModifiedEvent.did_edit_un = "true"; wasModifiedEvent.did_select_un = "false"; onInput(); }; const onUsernameSelect = () => { wasModifiedEvent.did_edit_un = "false"; wasModifiedEvent.did_select_un = "true"; }; const onPasswordInput = () => { wasModifiedEvent.did_edit_pw = "true"; wasModifiedEvent.did_select_pw = "false"; onInput(); }; const onPasswordSelect = () => { wasModifiedEvent.did_edit_pw = "false"; wasModifiedEvent.did_select_pw = "true"; }; const onKeyUp = e => { if (e.key == "Enter") { e.target.closest("popupnotification").button.doCommand(); } }; const onVisibilityToggle = commandEvent => { const passwordField = chromeDoc.getElementById( "password-notification-password" ); // Gets the caret position before changing the type of the textbox const selectionStart = passwordField.selectionStart; const selectionEnd = passwordField.selectionEnd; passwordField.setAttribute( "type", commandEvent.target.checked ? "" : "password" ); if (!passwordField.hasAttribute("focused")) { return; } passwordField.selectionStart = selectionStart; passwordField.selectionEnd = selectionEnd; }; const togglePopup = event => { event.target.parentElement .getElementsByClassName("ac-has-end-icon")[0] .toggleHistoryPopup(); }; const persistData = async () => { const foundLogins = lazy.LoginHelper.searchLoginsWithObject({ formActionOrigin: login.formActionOrigin, origin: login.origin, httpRealm: login.httpRealm, schemeUpgrades: lazy.LoginHelper.schemeUpgrades, }); let logins = this._filterUpdatableLogins( login, foundLogins, autoSavedLoginGuid ); const resolveBy = ["scheme", "timePasswordChanged"]; logins = lazy.LoginHelper.dedupeLogins( logins, ["username"], resolveBy, login.origin ); // sort exact username matches to the top logins.sort(l => (l.username == login.username ? -1 : 1)); lazy.log.debug(`Matched ${logins.length} logins.`); let loginToRemove; const loginToUpdate = logins.shift(); if (logins.length && logins[0].guid == autoSavedLoginGuid) { loginToRemove = logins.shift(); } if (logins.length) { lazy.log.warn( "persistData:", logins.length, "other updatable logins!", logins.map(l => l.guid), "loginToUpdate:", loginToUpdate && loginToUpdate.guid, "loginToRemove:", loginToRemove && loginToRemove.guid ); // Proceed with updating the login with the best username match rather // than returning and losing the edit. } if (!loginToUpdate) { // Create a new login, don't update an original. // 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. await Services.logins.addLoginAsync( new LoginInfo( login.origin, login.formActionOrigin, login.httpRealm, login.username, login.password, login.usernameField, login.passwordField ) ); } else if ( loginToUpdate.password == login.password && loginToUpdate.username == login.username ) { // We only want to touch the login's use count and last used time. lazy.log.debug(`Touch matched login: ${loginToUpdate.guid}.`); Services.logins.recordPasswordUse( loginToUpdate, PrivateBrowsingUtils.isBrowserPrivate(browser), loginToUpdate.username ? "form_password" : "form_login", !!autoFilledLoginGuid ); } else { lazy.log.debug(`Update matched login: ${loginToUpdate.guid}.`); this._updateLogin(loginToUpdate, login); // notify that this auto-saved login has been merged if (loginToRemove && loginToRemove.guid == autoSavedLoginGuid) { Services.obs.notifyObservers( loginToRemove, "passwordmgr-autosaved-login-merged" ); } } if (loginToRemove) { lazy.log.debug(`Removing login ${loginToRemove.guid}.`); Services.logins.removeLogin(loginToRemove); } }; const supportedHistogramNames = { PWMGR_PROMPT_REMEMBER_ACTION: true, PWMGR_PROMPT_UPDATE_ACTION: true, }; const mainButton = this.getLabelAndAccessKey(initialMessageIds.mainButton); // The main action is the "Save" or "Update" button. const mainAction = { label: mainButton.label, accessKey: mainButton.accessKey, callback: async () => { const eventTypeMapping = { "password-save": { eventObject: "save", confirmationHintFtlId: "confirmation-hint-password-created", }, "password-change": { eventObject: "update", confirmationHintFtlId: "confirmation-hint-password-updated", }, }; if (!eventTypeMapping[type]) { throw new Error(`Unexpected doorhanger type: '${type}'`); } readDataFromUI(); if ( type == "password-save" && !Services.policies.isAllowed("removeMasterPassword") ) { if (!lazy.LoginHelper.isPrimaryPasswordSet()) { browser.ownerGlobal.openDialog( "chrome://mozapps/content/preferences/changemp.xhtml", "", "centerscreen,chrome,modal,titlebar" ); if (!lazy.LoginHelper.isPrimaryPasswordSet()) { return; } } } histogram.add(PROMPT_ADD_OR_UPDATE); if (!supportedHistogramNames[histogramName]) { throw new Error("Unknown histogram"); } showConfirmation(browser, eventTypeMapping[type].confirmationHintFtlId); // The popup does not wait until this promise is resolved, but is // closed immediately when the function is returned. Therefore, we set // the focus before awaiting the asynchronous operation. browser.focus(); await persistData(); Services.telemetry.recordEvent( "pwmgr", "doorhanger_submitted", eventTypeMapping[type].eventObject, null, wasModifiedEvent ); if (histogramName == "PWMGR_PROMPT_REMEMBER_ACTION") { Services.obs.notifyObservers(browser, "LoginStats:NewSavedPassword"); } else if (histogramName == "PWMGR_PROMPT_UPDATE_ACTION") { Services.obs.notifyObservers(browser, "LoginStats:LoginUpdateSaved"); } Services.obs.notifyObservers( null, "weave:telemetry:histogram", histogramName ); }, }; const secondaryButton = this.getLabelAndAccessKey( initialMessageIds.secondaryButton ); const secondaryActions = [ { label: secondaryButton.label, accessKey: secondaryButton.accessKey, callback: () => { histogram.add(PROMPT_NOTNOW_OR_DONTUPDATE); Services.obs.notifyObservers( null, "weave:telemetry:histogram", histogramName ); browser.focus(); }, }, ]; // Include a "Never for this site" button when saving a new password. if (type == "password-save") { const neverSaveButton = this.getLabelAndAccessKey( "password-manager-save-password-button-never" ); secondaryActions.push({ label: neverSaveButton.label, accessKey: neverSaveButton.accessKey, callback: () => { histogram.add(PROMPT_NEVER); Services.obs.notifyObservers( null, "weave:telemetry:histogram", histogramName ); Services.logins.setLoginSavingEnabled(login.origin, false); browser.focus(); }, }); } const updatePasswordButtonDelete = this.getLabelAndAccessKey( "password-manager-update-password-button-delete" ); // Include a "Delete this login" button when updating an existing password if (type == "password-change") { secondaryActions.push({ label: updatePasswordButtonDelete.label, accessKey: updatePasswordButtonDelete.accessKey, callback: async () => { histogram.add(PROMPT_DELETE); Services.obs.notifyObservers( null, "weave:telemetry:histogram", histogramName ); const matchingLogins = await Services.logins.searchLoginsAsync({ guid: login.guid, origin: login.origin, }); Services.logins.removeLogin(matchingLogins[0]); browser.focus(); lazy.log.debug("Showing the ConfirmationHint"); showConfirmation(browser, "confirmation-hint-password-removed"); }, }); } const usernamePlaceholder = lazy.l10n.formatValueSync( "password-manager-no-username-placeholder" ); const togglePassword = this.getLabelAndAccessKey( "password-manager-toggle-password" ); // .wrappedJSObject needed here -- see bug 422974 comment 5. const { PopupNotifications } = browser.ownerGlobal.wrappedJSObject; const notificationID = "password"; // keep attention notifications around for longer after a locationchange const timeoutMs = showOptions.dismissed && showOptions.extraAttr == "attention" ? ATTENTION_NOTIFICATION_TIMEOUT_MS : NOTIFICATION_TIMEOUT_MS; const options = Object.assign( { timeout: Date.now() + timeoutMs, persistWhileVisible: true, passwordNotificationType: type, hideClose: true, eventCallback(topic) { switch (topic) { case "showing": { lazy.log.debug("showing"); currentNotification = this; // Record the first time this instance of the doorhanger is shown. if (!this.timeShown) { histogram.add(PROMPT_DISPLAYED); Services.obs.notifyObservers( null, "weave:telemetry:histogram", histogramName ); } chromeDoc .getElementById("password-notification-password") .removeAttribute("focused"); chromeDoc .getElementById("password-notification-username") .removeAttribute("focused"); chromeDoc .getElementById("password-notification-username") .addEventListener("input", onUsernameInput); chromeDoc .getElementById("password-notification-username") .addEventListener("keyup", onKeyUp); chromeDoc .getElementById("password-notification-password") .addEventListener("keyup", onKeyUp); chromeDoc .getElementById("password-notification-password") .addEventListener("input", onPasswordInput); chromeDoc .getElementById("password-notification-username-dropmarker") .addEventListener("click", togglePopup); LoginManagerPrompter._getUsernameSuggestions( login, possibleValues?.usernames ).then(usernameSuggestions => { const dropmarker = chromeDoc?.getElementById( "password-notification-username-dropmarker" ); if (dropmarker) { dropmarker.hidden = !usernameSuggestions.length; } const usernameField = chromeDoc?.getElementById( "password-notification-username" ); if (usernameField) { usernameField.classList.toggle( "ac-has-end-icon", !!usernameSuggestions.length ); } }); const toggleBtn = chromeDoc.getElementById( "password-notification-visibilityToggle" ); if ( Services.prefs.getBoolPref( "signon.rememberSignons.visibilityToggle" ) ) { toggleBtn.addEventListener("command", onVisibilityToggle); toggleBtn.setAttribute("label", togglePassword.label); toggleBtn.setAttribute("accesskey", togglePassword.accessKey); const hideToggle = lazy.LoginHelper.isPrimaryPasswordSet() || // Don't show the toggle when the login was autofilled !!autoFilledLoginGuid || // Dismissed-by-default prompts should still show the toggle. (this.timeShown && this.wasDismissed) || // If we are only adding a username then the password is // one that is already saved and we don't want to reveal // it as the submitter of this form may not be the account // owner, they may just be using the saved password. (messageStringID == "password-manager-update-login-add-username" && login.timePasswordChanged < Date.now() - VISIBILITY_TOGGLE_MAX_PW_AGE_MS); toggleBtn.hidden = hideToggle; } let popup = chromeDoc.getElementById("PopupAutoComplete"); popup.onUsernameSelect = onUsernameSelect; popup.onPasswordSelect = onPasswordSelect; LoginManagerPrompter._setUsernameAutocomplete( login, possibleValues?.usernames ); } break; case "shown": { lazy.log.debug("shown"); writeDataToUI(); const anchorIcon = this.anchorElement; if (anchorIcon && this.options.extraAttr == "attention") { anchorIcon.removeAttribute("extraAttr"); delete this.options.extraAttr; } break; } case "dismissed": // Note that this can run after `showing` but before `shown` upon tab switch. this.wasDismissed = true; // Fall through. case "removed": { // Note that this can run after `showing` and `shown` for the // notification it's replacing. lazy.log.debug(topic); currentNotification = null; const usernameField = chromeDoc.getElementById( "password-notification-username" ); usernameField.removeEventListener("input", onUsernameInput); usernameField.removeEventListener("keyup", onKeyUp); const passwordField = chromeDoc.getElementById( "password-notification-password" ); passwordField.removeEventListener("input", onPasswordInput); passwordField.removeEventListener("keyup", onKeyUp); passwordField.removeEventListener("command", onVisibilityToggle); chromeDoc .getElementById("password-notification-username-dropmarker") .removeEventListener("click", togglePopup); break; } } return false; }, }, showOptions ); const notification = PopupNotifications.show( browser, notificationID, promptMessage, "password-notification-icon", mainAction, secondaryActions, options ); if (notifySaved) { showConfirmation( browser, "confirmation-hint-password-created", "password-notification-icon" ); } return notification; } /** * 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 {Element} aBrowser * The browser element that the request came from. * @param {nsILoginInfo} aOldLogin * The old login we may want to update. * @param {nsILoginInfo} aNewLogin * The new login from the page form. * @param {boolean} [dismissed = false] * If the prompt should be automatically dismissed on being shown. * @param {boolean} [notifySaved = false] * Whether the notification should indicate that a login has been saved * @param {string} [autoSavedLoginGuid = ""] * A guid value for the old login to be removed if the changes match it * to a different login * @param {object?} possibleValues * Contains values from anything that we think, but are not sure, might be * a username or password. Has two properties, 'usernames' and 'passwords'. * @param {Set} possibleValues.usernames * @param {Set} possibleValues.passwords */ promptToChangePassword( aBrowser, aOldLogin, aNewLogin, dismissed = false, notifySaved = false, autoSavedLoginGuid = "", autoFilledLoginGuid = "", possibleValues = undefined ) { const login = aOldLogin.clone(); login.origin = aNewLogin.origin; login.formActionOrigin = aNewLogin.formActionOrigin; login.password = aNewLogin.password; login.username = aNewLogin.username; let messageStringID; if ( aOldLogin.username === "" && login.username !== "" && login.password == aOldLogin.password ) { // If the saved password matches the password we're prompting with then we // are only prompting to let the user add a username since there was one in // the form. Change the message so the purpose of the prompt is clearer. messageStringID = "password-manager-update-login-add-username"; } const notification = LoginManagerPrompter._showLoginCaptureDoorhanger( aBrowser, login, "password-change", { dismissed, extraAttr: notifySaved ? "attention" : "", }, possibleValues, { notifySaved, messageStringID, autoSavedLoginGuid, autoFilledLoginGuid, } ); const oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid; Services.obs.notifyObservers( aNewLogin, "passwordmgr-prompt-change", oldGUID ); return { dismiss() { const { PopupNotifications } = aBrowser.ownerGlobal.wrappedJSObject; PopupNotifications.remove(notification); }, }; } /** * 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. */ promptToChangePasswordWithUsernames(browser, logins, aNewLogin) { lazy.log.debug( `Prompting user to change passowrd for username with count: ${logins.length}.` ); const noUsernamePlaceholder = lazy.l10n.formatValueSync( "password-manager-no-username-placeholder" ); const usernames = logins.map(l => l.username || noUsernamePlaceholder); const dialogText = lazy.l10n.formatValueSync( "password-manager-select-username" ); const dialogTitle = lazy.l10n.formatValueSync( "password-manager-confirm-password-change" ); const selectedIndex = { value: null }; // If user selects ok, outparam.value is set to the index // of the selected username. const ok = Services.prompt.select( browser.ownerGlobal, dialogTitle, dialogText, usernames, selectedIndex ); if (ok) { // Now that we know which login to use, modify its password. const selectedLogin = logins[selectedIndex.value]; lazy.log.debug(`Updating password for origin: ${aNewLogin.origin}.`); const newLoginWithUsername = Cc[ "@mozilla.org/login-manager/loginInfo;1" ].createInstance(Ci.nsILoginInfo); newLoginWithUsername.init( aNewLogin.origin, aNewLogin.formActionOrigin, aNewLogin.httpRealm, selectedLogin.username, aNewLogin.password, selectedLogin.usernameField, aNewLogin.passwordField ); LoginManagerPrompter._updateLogin(selectedLogin, newLoginWithUsername); } } /* ---------- Internal Methods ---------- */ /** * Helper method to update and persist an existing nsILoginInfo object with new property values. */ static _updateLogin(login, aNewLogin) { const now = Date.now(); const propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance( Ci.nsIWritablePropertyBag ); propBag.setProperty("formActionOrigin", aNewLogin.formActionOrigin); propBag.setProperty("origin", aNewLogin.origin); 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); // Note that we don't call `recordPasswordUse` so telemetry won't record a // use in this case though that is normally correct since we would instead // record the save/update in a separate probe and recording it in both would // be wrong. Services.logins.modifyLogin(login, propBag); } /** * Retrieves the message of the given id from fluent * and extracts the label and accesskey * * @param {String} id message id * @returns label and accesskey */ static getLabelAndAccessKey(id) { const msg = lazy.l10n.formatMessagesSync([id])[0]; return { label: msg.attributes.find(x => x.name == "label").value, accessKey: msg.attributes.find(x => x.name == "accesskey").value, }; } /** * Converts a login's origin field to a short string for * prompting purposes. Eg, "http://foo.com" --> "foo.com", or * "ftp://www.site.co.uk" --> "site.co.uk". */ static _getShortDisplayHost(aURIString) { let displayHost; const idnService = Cc["@mozilla.org/network/idn-service;1"].getService( Ci.nsIIDNService ); try { const uri = Services.io.newURI(aURIString); const baseDomain = Services.eTLD.getBaseDomain(uri); displayHost = idnService.convertToDisplayIDN(baseDomain, {}); } catch (e) { lazy.log.warn(`Couldn't process supplied URIString: ${aURIString}`); } if (!displayHost) { displayHost = aURIString; } return displayHost; } /** * 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 * - the same password as aLogin and an empty username * so the user can add a username. * - the same guid as the given login when it has an empty username * * @param {nsILoginInfo} aLogin * login to use as filter. * @param {nsILoginInfo[]} aLoginList * Array of logins to filter. * @param {String} includeGUID * guid value for login that not be filtered out * @returns {nsILoginInfo[]} the filtered array of logins. */ static _filterUpdatableLogins(aLogin, aLoginList, includeGUID) { return aLoginList.filter( l => l.username == aLogin.username || (l.password == aLogin.password && !l.username) || (includeGUID && includeGUID == l.guid) ); } /** * Set the values that will be used the next time the username autocomplete popup is opened. * * @param {nsILoginInfo} login - used only for its information about the current domain. * @param {Set?} possibleUsernames - values that we believe may be new/changed login usernames. */ static async _setUsernameAutocomplete(login, possibleUsernames = new Set()) { const result = Cc[ "@mozilla.org/autocomplete/simple-result;1" ].createInstance(Ci.nsIAutoCompleteSimpleResult); result.setDefaultIndex(0); const usernames = await this._getUsernameSuggestions( login, possibleUsernames ); for (const { text, style } of usernames) { const value = text; const comment = ""; const image = ""; const _style = style; result.appendMatch(value, comment, image, _style); } result.setSearchResult( usernames.length ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS : Ci.nsIAutoCompleteResult.RESULT_NOMATCH ); lazy.usernameAutocompleteSearch.overrideNextResult(result); } /** * @param {nsILoginInfo} login - used only for its information about the current domain. * @param {Set?} possibleUsernames - values that we believe may be new/changed login usernames. * * @returns {object[]} an ordered list of usernames to be used the next time the username autocomplete popup is opened. */ static async _getUsernameSuggestions(login, possibleUsernames = new Set()) { if (!Services.prefs.getBoolPref("signon.capture.inputChanges.enabled")) { return []; } // Don't reprompt for Primary Password, as we already prompted at least once // to show the doorhanger if it is locked if (!Services.logins.isLoggedIn) { return []; } const baseDomainLogins = await Services.logins.searchLoginsAsync({ origin: login.origin, schemeUpgrades: lazy.LoginHelper.schemeUpgrades, acceptDifferentSubdomains: true, }); const saved = baseDomainLogins.map(login => { return { text: login.username, style: "login" }; }); const possible = [...possibleUsernames].map(username => { return { text: username, style: "possible-username" }; }); return possible .concat(saved) .reduce((acc, next) => { const alreadyInAcc = acc.findIndex(entry => entry.text == next.text) != -1; if (!alreadyInAcc) { acc.push(next); } else if (next.style == "possible-username") { const existingIndex = acc.findIndex(entry => entry.text == next.text); acc[existingIndex] = next; } return acc; }, []) .filter(suggestion => !!suggestion.text); } } // Add this observer once for the process. Services.obs.addObserver(observer, "autocomplete-did-enter-text"); ChromeUtils.defineLazyGetter(lazy, "log", () => { return lazy.LoginHelper.createLogger("LoginManagerPrompter"); });