From 8bc00cf194ba65ac958ffb63d7e50dbae44f68ea Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sun, 9 Nov 2025 14:14:28 +0200 Subject: [PATCH] Add the new selector for submit button --- keepassxc-browser/_locales/en/messages.json | 18 +++- .../content/custom-fields-banner.js | 99 +++++++++++++++---- keepassxc-browser/content/fields.js | 29 ++++-- keepassxc-browser/content/fill.js | 3 +- 4 files changed, 114 insertions(+), 35 deletions(-) diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index b58595d..41a9d52 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -319,9 +319,13 @@ "description": "Clear save data button text when choosing Custom Login Fields." }, "defineStringField": { - "message": "String field", + "message": "String Field", "description": "Text for string field." }, + "defineSubmitButton": { + "message": "Submit Button", + "description": "Text for Submit Button." + }, "defineChooseUsername": { "message": "Choose a username field", "description": "Choosing a username field text when choosing Custom Login Fields." @@ -338,9 +342,13 @@ "message": "Choose String Fields", "description": "Choose String Fields a selection text when choosing Custom Login Fields." }, + "defineChooseSubmitButton": { + "message": "Choose submit button", + "description": "Choose submit button selection text when choosing Custom Login Fields." + }, "defineHelpText": { - "message": "Please confirm your selection or choose more fields as String fields.", - "description": "Confirm a selection text when choosing Custom Login Fields which contains string fields." + "message": "Please confirm your selection or choose more fields as String Fields.", + "description": "Confirm a selection text when choosing Custom Login Fields which contains String Fields." }, "defineKeyboardText": { "message": "You can also use the numbers to choose the input fields from keyboard.", @@ -362,6 +370,10 @@ "message": "String Fields", "description": "General text for a String Fields." }, + "submitButton": { + "message": "Submit Button", + "description": "Custom Submit Button text." + }, "credentialsNoUsername": { "message": "- no username -", "description": "Shown when no username is set in the credentials." diff --git a/keepassxc-browser/content/custom-fields-banner.js b/keepassxc-browser/content/custom-fields-banner.js index c1a2001..91707d9 100644 --- a/keepassxc-browser/content/custom-fields-banner.js +++ b/keepassxc-browser/content/custom-fields-banner.js @@ -5,6 +5,7 @@ const STEP_SELECT_USERNAME = 1; const STEP_SELECT_PASSWORD = 2; const STEP_SELECT_TOTP = 3; const STEP_SELECT_STRING_FIELDS = 4; +const STEP_SELECT_SUBMIT_BUTTON = 5; const CHECKBOX_OVERLAY_SIZE = 20; @@ -18,9 +19,10 @@ const PASSWORD_FIELD_CLASS = 'kpxcDefine-fixed-password-field'; const TOTP_FIELD_CLASS = 'kpxcDefine-fixed-totp-field'; const STRING_FIELD_CLASS = 'kpxcDefine-fixed-string-field'; -const inputQueryPatternStart = 'input'; -const inputQueryPatternNotCheckbox = ':not([type=checkbox])'; -const inputQueryPattern = ':not([disabled]):not([type=button]):not([type=radio]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=file]):not([type=hidden]):not([type=image]):not([type=month]):not([type=range]):not([type=reset]):not([type=submit]):not([type=time]):not([type=week]), select, textarea'; +const INPUT_BUTTON_QUERY_PATTERN = 'button, input[type=submit]'; +const INPUT_QUERY_PATTERNS_START = 'input'; +const INPUT_QUERY_PATTERN_NOT_CHECKBOX = ':not([type=checkbox])'; +const INPUT_QUERY_PATTERN = ':not([disabled]):not([type=button]):not([type=radio]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=file]):not([type=hidden]):not([type=image]):not([type=month]):not([type=range]):not([type=reset]):not([type=submit]):not([type=time]):not([type=week]), select, textarea'; const kpxcCustomLoginFieldsBanner = {}; kpxcCustomLoginFieldsBanner.banner = undefined; @@ -29,8 +31,9 @@ kpxcCustomLoginFieldsBanner.created = false; kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE; kpxcCustomLoginFieldsBanner.infoText = undefined; kpxcCustomLoginFieldsBanner.wrapper = undefined; -kpxcCustomLoginFieldsBanner.inputQueryPatternNormal = inputQueryPatternStart + inputQueryPatternNotCheckbox + inputQueryPattern; -kpxcCustomLoginFieldsBanner.inputQueryPatternStringFields = inputQueryPatternStart + inputQueryPattern; +kpxcCustomLoginFieldsBanner.inputQueryPatternNormal = + INPUT_QUERY_PATTERNS_START + INPUT_QUERY_PATTERN_NOT_CHECKBOX + INPUT_QUERY_PATTERN; +kpxcCustomLoginFieldsBanner.inputQueryPatternStringFields = INPUT_QUERY_PATTERNS_START + INPUT_QUERY_PATTERN; kpxcCustomLoginFieldsBanner.markedFields = []; kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern = `div.${FIXED_FIELD_CLASS}:not(.${USERNAME_FIELD_CLASS}):not(.${PASSWORD_FIELD_CLASS}):not(.${TOTP_FIELD_CLASS}):not(.${STRING_FIELD_CLASS})`; @@ -43,6 +46,7 @@ kpxcCustomLoginFieldsBanner.selection = { totpElement: undefined, fields: [], fieldElements: [], + submitButton: undefined, }; kpxcCustomLoginFieldsBanner.buttons = { @@ -100,6 +104,7 @@ kpxcCustomLoginFieldsBanner.create = async function() { const passwordButton = kpxcUI.createButton(RED_BUTTON, tr('password'), kpxcCustomLoginFieldsBanner.passwordButtonClicked); const totpButton = kpxcUI.createButton(GREEN_BUTTON, 'TOTP', kpxcCustomLoginFieldsBanner.totpButtonClicked); const stringFieldsButton = kpxcUI.createButton(BLUE_BUTTON, tr('stringFields'), kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked); + const submitButton = kpxcUI.createButton(BLUE_BUTTON, tr('submitButton'), kpxcCustomLoginFieldsBanner.submitButtonClicked); const clearDataButton = kpxcUI.createButton(RED_BUTTON, tr('defineClearData'), kpxcCustomLoginFieldsBanner.clearData); const confirmButton = kpxcUI.createButton(GREEN_BUTTON, tr('defineConfirm'), kpxcCustomLoginFieldsBanner.confirm); const closeButton = kpxcUI.createButton(RED_BUTTON, tr('defineClose'), kpxcCustomLoginFieldsBanner.closeButtonClicked); @@ -116,10 +121,11 @@ kpxcCustomLoginFieldsBanner.create = async function() { kpxcCustomLoginFieldsBanner.buttons.password = passwordButton; kpxcCustomLoginFieldsBanner.buttons.totp = totpButton; kpxcCustomLoginFieldsBanner.buttons.stringFields = stringFieldsButton; + kpxcCustomLoginFieldsBanner.buttons.submitButton = submitButton; bannerInfo.appendMultiple(icon, infoText); - bannerButtons.appendMultiple(resetButton, separator, usernameButton, - passwordButton, totpButton, stringFieldsButton, secondSeparator, clearDataButton, confirmButton, closeButton); + bannerButtons.appendMultiple(resetButton, separator, usernameButton, passwordButton, totpButton, + stringFieldsButton, submitButton, secondSeparator, clearDataButton, confirmButton, closeButton); banner.appendMultiple(bannerInfo, bannerButtons); kpxcUI.makeBannerDraggable(banner); @@ -189,7 +195,8 @@ kpxcCustomLoginFieldsBanner.usernameButtonClicked = function(e) { // Reset username field selection if already set if (kpxcCustomLoginFieldsBanner.selection.username) { - kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.username, `div.${USERNAME_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.username, + `div.${USERNAME_FIELD_CLASS}`); kpxcCustomLoginFieldsBanner.selection.username = undefined; } @@ -207,7 +214,8 @@ kpxcCustomLoginFieldsBanner.passwordButtonClicked = function(e) { // Reset password field selection if already set if (kpxcCustomLoginFieldsBanner.selection.password) { - kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.password, `div.${PASSWORD_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.password, + `div.${PASSWORD_FIELD_CLASS}`); kpxcCustomLoginFieldsBanner.selection.password = undefined; } @@ -225,7 +233,8 @@ kpxcCustomLoginFieldsBanner.totpButtonClicked = function(e) { // Reset TOTP field selection if already set if (kpxcCustomLoginFieldsBanner.selection.totp) { - kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.totp, `div.${TOTP_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.totp, + `div.${TOTP_FIELD_CLASS}`); kpxcCustomLoginFieldsBanner.selection.totp = undefined; } @@ -256,6 +265,25 @@ kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked = function(e) { sendMessageToFrames(e, 'string_field_button_clicked'); }; +kpxcCustomLoginFieldsBanner.submitButtonClicked = function(e) { + if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_SUBMIT_BUTTON) { + kpxcCustomLoginFieldsBanner.backToStart(); + return; + } + + // Reset TOTP field selection if already set + if (kpxcCustomLoginFieldsBanner.selection.submitButton) { + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.submitButton, + `div.${STRING_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.selection.submitButton = undefined; + } + + kpxcCustomLoginFieldsBanner.prepareSubmitButtonSelection(); + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true; + + sendMessageToFrames(e, 'submit_button_clicked'); +}; + kpxcCustomLoginFieldsBanner.closeButtonClicked = function(e) { if (!e.isTrusted) { return; @@ -282,6 +310,8 @@ kpxcCustomLoginFieldsBanner.updateFieldSelections = function() { kpxcCustomLoginFieldsBanner.prepareTOTPSelection(); } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) { kpxcCustomLoginFieldsBanner.prepareStringFieldSelection(); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_SUBMIT_BUTTON) { + kpxcCustomLoginFieldsBanner.prepareSubmitButtonSelection(); } }; @@ -314,6 +344,8 @@ kpxcCustomLoginFieldsBanner.confirm = async function(e) { kpxc.settings[DEFINED_CUSTOM_FIELDS][location].password = undefined; } else if (currentSite.totp?.[0] === path[0]) { kpxc.settings[DEFINED_CUSTOM_FIELDS][location].totp = undefined; + } else if (currentSite.submitButton?.[0] === path[0]) { + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].submitButton = undefined; } }; @@ -321,10 +353,11 @@ kpxcCustomLoginFieldsBanner.confirm = async function(e) { const passwordPath = kpxcCustomLoginFieldsBanner.selection.password; const totpPath = kpxcCustomLoginFieldsBanner.selection.totp; const stringFieldsPaths = kpxcCustomLoginFieldsBanner.selection.fields; + const submitButtonPath = kpxcCustomLoginFieldsBanner.selection.submitButton; const location = kpxc.getDocumentLocation(); const currentSettings = kpxc.settings[DEFINED_CUSTOM_FIELDS][location]; - if (usernamePath || passwordPath || totpPath || stringFieldsPaths.length > 0) { + if (usernamePath || passwordPath || totpPath || stringFieldsPaths.length > 0 || submitButtonPath) { if (currentSettings) { // Update the single selection to current settings if (usernamePath) { @@ -345,13 +378,19 @@ kpxcCustomLoginFieldsBanner.confirm = async function(e) { if (stringFieldsPaths.length > 0) { kpxc.settings[DEFINED_CUSTOM_FIELDS][location].fields = stringFieldsPaths; } + + if (submitButtonPath) { + clearIdenticalField(submitButtonPath, location); + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].submitButton = submitButtonPath; + } } else { // Override all fields (default, because there's no currentSettings available) kpxc.settings[DEFINED_CUSTOM_FIELDS][location] = { username: usernamePath, password: passwordPath, totp: totpPath, - fields: stringFieldsPaths + fields: stringFieldsPaths, + submitButton: submitButtonPath, }; } @@ -381,7 +420,8 @@ kpxcCustomLoginFieldsBanner.resetSelection = function() { username: undefined, password: undefined, totp: undefined, - fields: [] + fields: [], + submitButton: undefined, }; kpxcCustomLoginFieldsBanner.removeMarkedFields(); @@ -423,15 +463,24 @@ kpxcCustomLoginFieldsBanner.prepareStringFieldSelection = function() { kpxcCustomLoginFieldsBanner.selectStringFields(); }; +kpxcCustomLoginFieldsBanner.prepareSubmitButtonSelection = function() { + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseSubmitButton'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_SUBMIT_BUTTON; + kpxcCustomLoginFieldsBanner.buttons.submitButton.classList.remove(GRAY_BUTTON_CLASS); + kpxcCustomLoginFieldsBanner.selectField('submitButton'); +}; + kpxcCustomLoginFieldsBanner.isFieldSelected = function(field) { const currentFieldId = kpxcFields.setId(field); + const selection = kpxcCustomLoginFieldsBanner.selection; if (kpxcCustomLoginFieldsBanner.markedFields.some(f => f === field)) { return ( - (kpxcCustomLoginFieldsBanner.selection.username && kpxcCustomLoginFieldsBanner.selection.usernameElement === field) - || (kpxcCustomLoginFieldsBanner.selection.password && kpxcCustomLoginFieldsBanner.selection.passwordElement === field) - || (kpxcCustomLoginFieldsBanner.selection.totp && kpxcCustomLoginFieldsBanner.selection.totpElement === field) - || kpxcCustomLoginFieldsBanner.selection.fields.some(f => f[0] === currentFieldId[0]) + (selection.username && selection.usernameElement === field) + || (selection.password && selection.passwordElement === field) + || (selection.totp && selection.totpElement === field) + || (selection.submitButton && selection.submitButton === field) + || selection.fields.some(f => f[0] === currentFieldId[0]) ); } @@ -460,7 +509,7 @@ kpxcCustomLoginFieldsBanner.setSelectedField = function(elem) { kpxcCustomLoginFieldsBanner.buttons.close.textContent = tr('optionsButtonCancel'); }; -// Expects 'username', 'password' or 'totp' +// Expects 'username', 'password', 'totp' or 'submitButton' kpxcCustomLoginFieldsBanner.selectField = function(fieldType) { kpxcCustomLoginFieldsBanner.eventFieldClick = function(e) { const field = kpxcCustomLoginFieldsBanner.getSelectedField(e); @@ -516,9 +565,10 @@ kpxcCustomLoginFieldsBanner.selectStringFields = function() { kpxcCustomLoginFieldsBanner.markFields = function() { let firstInput; const inputs = document.querySelectorAll( - kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS - ? kpxcCustomLoginFieldsBanner.inputQueryPatternStringFields - : kpxcCustomLoginFieldsBanner.inputQueryPatternNormal); + kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_SUBMIT_BUTTON ? INPUT_BUTTON_QUERY_PATTERN : + (STEP_SELECT_STRING_FIELDS + ? kpxcCustomLoginFieldsBanner.inputQueryPatternStringFields + : kpxcCustomLoginFieldsBanner.inputQueryPatternNormal)); const zoom = kpxcUI.bodyStyle.zoom || 1; for (const i of inputs) { @@ -676,6 +726,9 @@ kpxcCustomLoginFieldsBanner.handleTopWindowMessage = function(args) { } else if (message === 'string_field_selected') { kpxcCustomLoginFieldsBanner.selection.fields = selection; kpxcCustomLoginFieldsBanner.setSelectedField(); + } else if (message === 'submitButton_selected') { + kpxcCustomLoginFieldsBanner.selection.submitButton = selection; + kpxcCustomLoginFieldsBanner.setSelectedField(); } else if (message === 'enable_clear_data_button') { kpxcCustomLoginFieldsBanner.buttons.clearData.style.display = 'inline-block'; } @@ -699,6 +752,8 @@ kpxcCustomLoginFieldsBanner.handleParentWindowMessage = function(args) { kpxcCustomLoginFieldsBanner.totpButtonClicked(e); } else if (message === 'string_field_button_clicked') { kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked(e); + } else if (message === 'submit_button_clicked') { + kpxcCustomLoginFieldsBanner.submitButtonClicked(e); } else if (message === 'reset_button_clicked') { kpxcCustomLoginFieldsBanner.reset(); } else if (message === 'close_button_clicked') { @@ -766,5 +821,7 @@ const dataStepToString = function() { return tr('totp'); } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) { return tr('defineStringField'); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_SUBMIT_BUTTON) { + return tr('defineSubmitButton'); } }; diff --git a/keepassxc-browser/content/fields.js b/keepassxc-browser/content/fields.js index 7059960..ff6099f 100644 --- a/keepassxc-browser/content/fields.js +++ b/keepassxc-browser/content/fields.js @@ -588,14 +588,14 @@ kpxcFields.traverseParents = function(element, predicate, resultFn = () => true, kpxcFields.useCustomLoginFields = async function() { const location = kpxc.getDocumentLocation(); const creds = kpxc.settings['defined-custom-fields'][location]; - if (!creds.username && !creds.password && !creds.totp && creds.fields.length === 0) { + if (!creds.username && !creds.password && !creds.totp && creds.fields.length === 0 && !creds.submitButton) { return; } - // Finds the input field based on the stored ID - const findInputField = async function(inputFields, idArray) { + // Finds the element based on the stored ID + const findElement = async function(fields, idArray) { if (idArray) { - const input = inputFields.find(e => e === kpxcFields.getId(idArray, e)); + const input = fields.find(e => e === kpxcFields.getId(idArray, e)); if (input) { return input; } @@ -612,16 +612,24 @@ kpxcFields.useCustomLoginFields = async function() { } }); - const [ username, password, totp ] = await Promise.all([ - await findInputField(inputFields, creds.username), - await findInputField(inputFields, creds.password), - await findInputField(inputFields, creds.totp) + const buttons = []; + document.body.querySelectorAll('button, input[type=submit]').forEach(e => { + if (e.type !== 'hidden' && !e.disabled) { + buttons.push(e); + } + }); + + const [ username, password, totp, submitButton ] = await Promise.all([ + await findElement(inputFields, creds.username), + await findElement(inputFields, creds.password), + await findElement(inputFields, creds.totp), + await findElement(buttons, creds.submitButton), ]); // Handle StringFields const stringFields = []; for (const sf of creds.fields) { - const field = await findInputField(inputFields, sf); + const field = await findElement(inputFields, sf); if (field) { stringFields.push(field); } @@ -639,7 +647,8 @@ kpxcFields.useCustomLoginFields = async function() { password: password, passwordInputs: [ password ], totp: totp, - fields: stringFields + fields: stringFields, + submitButton: submitButton }); return combinations; diff --git a/keepassxc-browser/content/fill.js b/keepassxc-browser/content/fill.js index 59e2c60..b6b9571 100644 --- a/keepassxc-browser/content/fill.js +++ b/keepassxc-browser/content/fill.js @@ -340,7 +340,8 @@ kpxcFill.performAutoSubmit = async function(combination, skipAutoSubmit) { if (!skipAutoSubmit && !autoSubmitIgnoredForSite) { await sendMessage('page_set_autosubmit_performed'); - const submitButton = kpxcForm.getFormSubmitButton(combination.form); + // Use submit button from Custom Login Fields or detect it from the form + const submitButton = combination?.submitButton ?? kpxcForm.getFormSubmitButton(combination.form); if (submitButton !== undefined) { submitButton.click(); } else if (combination.form) {