diff --git a/keepassxc-browser/common/sites.js b/keepassxc-browser/common/sites.js index ad337aa..fff9090 100644 --- a/keepassxc-browser/common/sites.js +++ b/keepassxc-browser/common/sites.js @@ -149,11 +149,11 @@ kpxcSites.expectedTOTPMaxLength = function() { /** * Handles a few exceptions for certain sites where form submit button is not regognized properly. - * @param {object} form Form element + * @param {object} form Form element (optional) * @returns {object} Button element */ kpxcSites.formSubmitButtonExceptionFound = function(form) { - if (form.action.startsWith(googleUrl)) { + if (form?.action?.startsWith(googleUrl)) { const findDiv = $('#identifierNext, #passwordNext'); if (!findDiv) { return undefined; @@ -162,14 +162,14 @@ kpxcSites.formSubmitButtonExceptionFound = function(form) { const buttons = findDiv.getElementsByTagName('button'); kpxcSites.savedForm = form; return buttons.length > 0 ? buttons[0] : undefined; - } else if (form.action.startsWith('https://www.ebay.')) { + } else if (form?.action?.startsWith('https://www.ebay.')) { // For eBay we must return the first button. for (const i of form.elements) { if (i.type === 'button') { return i; } } - } else if (form.action.includes('signin.aws.amazon.com')) { + } else if (form?.action?.includes('signin.aws.amazon.com')) { // For Amazon AWS the button is outside the form. const button = $('#signin_button'); if (button) { @@ -182,14 +182,17 @@ kpxcSites.formSubmitButtonExceptionFound = function(form) { 'odc.officeapps.live.com', 'login.microsoftonline.com', 'login.microsoftonline.us', - ].some(u => form.action.includes(u))) { + ].some(u => form?.action?.includes(u))) { const buttons = Array.from(form.querySelectorAll(kpxcForm.formButtonQuery)); if (buttons?.length > 1) { return buttons[1]; } - } else if (form.action.startsWith('https://barmerid.id.bconnect.barmer.de')) { + } else if (form?.action?.startsWith('https://barmerid.id.bconnect.barmer.de')) { const loginButton = $('#btn-login'); return loginButton?.shadowRoot?.children?.[0]; + } else if (!form && document.location.href.includes('reddit.com/settings')) { + // Reddit change password popup + return $('.button[slot=primary-button]'); } return undefined; diff --git a/keepassxc-browser/content/form.js b/keepassxc-browser/content/form.js index e9157e8..95372be 100644 --- a/keepassxc-browser/content/form.js +++ b/keepassxc-browser/content/form.js @@ -6,9 +6,39 @@ */ const kpxcForm = {}; kpxcForm.formButtonQuery = 'button[type=button], button[type=submit], input[type=button], input[type=submit], button:not([type]), div[role=button]'; +kpxcForm.savedCustomInputs = []; kpxcForm.savedForms = []; kpxcForm.submitTriggered = false; +// Activate the Credential Banner if existing credentials are not found +kpxcForm.activateCredentialBanner = async function(usernameValue, passwordInputs, passwordField) { + let passwordValue = ''; + // Check if the form has three password fields -> a possible password change form + if (passwordInputs && passwordInputs.length >= 2) { + passwordValue = kpxcForm.getNewPassword(passwordInputs); + } else if (passwordField) { + // Use the combination password field instead + passwordValue = passwordField.value; + } + + // Return if credentials are already found + if (kpxc.credentials.some(c => c.login === usernameValue && c.password === passwordValue)) { + kpxcForm.submitTriggered = false; + return; + } + + if (passwordField) { + await kpxc.setPasswordFilled(true); + } + + const url = trimURL(kpxc.settings.saveDomainOnlyNewCreds ? window.top.location.origin : window.top.location.href); + await sendMessage('page_set_submitted', [ true, usernameValue, passwordValue, url, kpxc.credentials ]); + + // Show the banner if the page does not reload + kpxc.rememberCredentials(usernameValue, passwordValue); + kpxcForm.submitTriggered = false; +}; + // Returns true if form has been already saved kpxcForm.formIdentified = function(form) { return kpxcForm.savedForms.some(f => f.form === form); @@ -94,8 +124,29 @@ kpxcForm.getNewPassword = function(passwordInputs = []) { return ''; }; +// Returns the username value from an input field or selected login +kpxcForm.getUsernameValue = async function(usernameField) { + if (usernameField) { + return usernameField.value || usernameField.placeholder; + } else if (kpxc.credentials.length === 1) { + // Single entry found for the page, use the username of it instead of an empty one + return kpxc.credentials[0].login; + } else { + // Multiple entries found for the page, try to find out which one might have been used + const pageUuid = await sendMessage('page_get_login_id'); + if (pageUuid) { + const credential = kpxc.credentials.find(c => c.uuid === pageUuid); + if (credential) { + return credential.login; + } + } + } + + return ''; +}; + // Initializes form and attaches the submit button to our own callback -kpxcForm.init = function(form, credentialFields) { +kpxcForm.initForm = function(form, credentialFields) { if (!form.action || typeof form.action !== 'string') { logDebug('Error: Form action is not found.'); return; @@ -113,6 +164,30 @@ kpxcForm.init = function(form, credentialFields) { } }; +// Initialize a "form" where three different password input fields are in a combination +kpxcForm.initCustomForm = function(combinations) { + if (combinations?.length >= 2 && combinations?.every(c => !c?.form && !c.username && c.password)) { + kpxcForm.savedCustomInputs = []; + const submitButton = kpxcSites.formSubmitButtonExceptionFound(); + if (submitButton) { + kpxcForm.savedCustomInputs = combinations?.map(c => c.password); + submitButton.addEventListener('click', kpxcForm.onCustomFormSubmit); + } + } +}; + +// Triggers when a custom form has been identified with a specific form submit button +kpxcForm.onCustomFormSubmit = async function(e) { + if (!e.isTrusted || kpxcForm.savedCustomInputs?.length === 0) { + return; + } + + kpxcForm.submitTriggered = true; + + const usernameValue = await kpxcForm.getUsernameValue(); + await kpxcForm.activateCredentialBanner(usernameValue, kpxcForm.savedCustomInputs); +}; + // Triggers when form is submitted. Shows the credential banner kpxcForm.onSubmit = async function(e) { if (!e.isTrusted) { @@ -154,49 +229,8 @@ kpxcForm.onSubmit = async function(e) { } const [ usernameField, passwordField, passwordInputs ] = kpxcForm.getCredentialFieldsFromForm(form); - let usernameValue = ''; - let passwordValue = ''; - - if (usernameField) { - usernameValue = usernameField.value || usernameField.placeholder; - } else if (kpxc.credentials.length === 1) { - // Single entry found for the page, use the username of it instead of an empty one - usernameValue = kpxc.credentials[0].login; - } else { - // Multiple entries found for the page, try to find out which one might have been used - const pageUuid = await sendMessage('page_get_login_id'); - if (pageUuid) { - const credential = kpxc.credentials.find(c => c.uuid === pageUuid); - if (credential) { - usernameValue = credential.login; - } - } - } - - // Check if the form has three password fields -> a possible password change form - if (passwordInputs && passwordInputs.length >= 2) { - passwordValue = kpxcForm.getNewPassword(passwordInputs); - } else if (passwordField) { - // Use the combination password field instead - passwordValue = passwordField.value; - } - - // Return if credentials are already found - if (kpxc.credentials.some(c => c.login === usernameValue && c.password === passwordValue)) { - kpxcForm.submitTriggered = false; - return; - } - - if (passwordField) { - await kpxc.setPasswordFilled(true); - } - - const url = trimURL(kpxc.settings.saveDomainOnlyNewCreds ? window.top.location.origin : window.top.location.href); - await sendMessage('page_set_submitted', [ true, usernameValue, passwordValue, url, kpxc.credentials ]); - - // Show the banner if the page does not reload - kpxc.rememberCredentials(usernameValue, passwordValue); - kpxcForm.submitTriggered = false; + const usernameValue = await kpxcForm.getUsernameValue(usernameField); + await kpxcForm.activateCredentialBanner(usernameValue, passwordInputs, passwordField); }; // Save form to Object array diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 69e469e..41a051e 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -327,15 +327,20 @@ kpxc.initCombinations = async function(inputs = []) { const field = c.username || c.password; if (field && c.form) { // Initialize form-submit for remembering credentials - kpxcForm.init(c.form, c); + kpxcForm.initForm(c.form, c); } // Don't allow duplicates - if (!kpxc.combinations.some(f => f.username === c.username && f.password === c.password && f.totp === c.totp && f.form === c.form)) { + if (!kpxc.combinations.some(f => + f.username === c.username && f.password === c.password && f.totp === c.totp && f.form === c.form + )) { kpxc.combinations.push(c); } } + // Identify a custom password change form + kpxcForm.initCustomForm(combinations); + // Update the fields in Custom Login Fields banner if it's open if (kpxcCustomLoginFieldsBanner.created) { kpxcCustomLoginFieldsBanner.updateFieldSelections();