From 20f1a99356e0198924c50c69421e67cab05a008a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Sun, 26 Oct 2025 15:14:11 +0200 Subject: [PATCH] Check overlays before fill (#2733) Update and check overlays before fill --- keepassxc-browser/content/fields.js | 35 ++++++--- keepassxc-browser/content/fill.js | 74 +++++++++++++++++-- .../content/keepassxc-browser.js | 54 -------------- 3 files changed, 93 insertions(+), 70 deletions(-) diff --git a/keepassxc-browser/content/fields.js b/keepassxc-browser/content/fields.js index 844a4bd..7059960 100644 --- a/keepassxc-browser/content/fields.js +++ b/keepassxc-browser/content/fields.js @@ -466,6 +466,28 @@ kpxcFields.checkExistingFields = function() { } }; +// Check for popup overlays +kpxcFields.isOverlayOnTop = function(rect) { + for (const overlay of kpxcFields.overlays ?? []) { + if (kpxcSites.overlayExceptionFound(overlay)) { + continue; + } + + const overlayRect = overlay?.getBoundingClientRect(); + if (overlayRect && elementsOverlap(rect, overlayRect)) { + return true; + } + } + + return false; +}; + +/** + * Check if element is the topmost element + * @param {HTMLElement} elem Element to be checked + * @param {DOMRect} rect Precalculated DOMRect of the element + * @returns {boolean} True if element is the topmost + */ kpxcFields.isTopElement = function(elem, rect) { if (!elem || !rect) { return false; @@ -493,16 +515,9 @@ kpxcFields.isTopElement = function(elem, rect) { return false; } - // Check for popup overlays - for (const overlay of kpxcFields.overlays ?? []) { - if (kpxcSites.overlayExceptionFound(overlay)) { - continue; - } - - const overlayRect = overlay?.getBoundingClientRect(); - if (overlayRect && elementsOverlap(rect, overlayRect)) { - return false; - } + // Check if element has an overlay + if (kpxcFields.isOverlayOnTop(rect)) { + return false; } return true; diff --git a/keepassxc-browser/content/fill.js b/keepassxc-browser/content/fill.js index ddfdd1d..7378386 100644 --- a/keepassxc-browser/content/fill.js +++ b/keepassxc-browser/content/fill.js @@ -18,7 +18,7 @@ kpxcFill.fillAttributeToActiveElementWith = async function(attr) { return; } - kpxc.setValue(el, value[0]); + kpxcFill.setValue(el, value[0]); }; // Fill requested from the context menu. Active element is used for combination detection @@ -178,7 +178,7 @@ kpxcFill.setTOTPValue = function(elem, val) { } } - kpxc.setValue(elem, val); + kpxcFill.setValue(elem, val); }; // Fill TOTP in parts @@ -188,7 +188,7 @@ kpxcFill.fillSegmentedTotp = function(elem, val, totpInputs) { } for (let i = 0; i < totpInputs.length; ++i) { - kpxc.setValue(totpInputs[i], val[i]); + kpxcFill.setValue(totpInputs[i], val[i]); } }; @@ -260,7 +260,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui return; } - kpxc.setValueWithChange(combination.password, selectedCredentials.password); + kpxcFill.setValueWithChange(combination.password, selectedCredentials.password); await kpxc.setPasswordFilled(true); } @@ -268,7 +268,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui if (combination.username && usernameValue && combination.username !== combination.password && (!combination.username.value || combination.username.value !== usernameValue)) { if (!passOnly) { - kpxc.setValueWithChange(combination.username, usernameValue); + kpxcFill.setValueWithChange(combination.username, usernameValue); } } @@ -307,7 +307,7 @@ kpxcFill.fillInStringFields = function(fields, stringFields) { const currentField = fields[i]; if (currentField && stringFieldValue[0]) { - kpxc.setValue(currentField, stringFieldValue[0], true); + kpxcFill.setValue(currentField, stringFieldValue[0], true); filledInFields.push(currentField); } } @@ -340,6 +340,68 @@ kpxcFill.performAutoSubmit = async function(combination, skipAutoSubmit) { } }; +// Special handling for setting value to select and checkbox elements +kpxcFill.setValue = function(field, value, forced = false) { + if (field.matches('select')) { + value = value.toLowerCase().trim(); + const options = field.querySelectorAll('option'); + + for (const o of options) { + if (o.textContent.toLowerCase().trim() === value) { + kpxcFill.setValueWithChange(field, o.value); + return false; + } + } + + return; + } else if (field.getLowerCaseAttribute('type') === 'checkbox' && value?.toLowerCase() === 'true') { + field.checked = true; + } + + // Make sure the input is not wrapped inside another element (custom INPUT element) + if (field?.nodeName !== 'INPUT' && field?.nodeName?.includes('INPUT')) { + const childInput = field?.querySelector('input'); + const fieldsFromShadowDOM = kpxcObserverHelper.findInputsFromShadowDOM(field); + field = childInput ?? fieldsFromShadowDOM[0]; + } + + + kpxcFill.setValueWithChange(field, value, forced); +}; + +// Sets a new value to input field and triggers necessary events +kpxcFill.setValueWithChange = function(field, value, forced = false) { + if (!field || (field?.readOnly && !forced)) { + return; + } + + // Check for overlays before fill + kpxcFields.discoverOverlays(); + const rect = field.getBoundingClientRect(); + if (kpxcFields.isOverlayOnTop(rect)) { + return; + } + + const dispatchLegacyEvent = function(elem, eventName) { + const legacyEvent = elem.ownerDocument.createEvent('Event'); + legacyEvent.initEvent(eventName, true, false); + elem.dispatchEvent(legacyEvent); + }; + + field.focus(); + field.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: false })); + field.dispatchEvent(new KeyboardEvent('keypress', { bubbles: true, cancelable: false })); + field.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: false })); + field.dispatchEvent(new Event('input', { bubbles: true, cancelable: false })); + field.dispatchEvent(new Event('change', { bubbles: true, cancelable: false })); + field.value = value; + + // Some pages will not accept the value change without dispatching events directly to the document + dispatchLegacyEvent(field, 'input'); + dispatchLegacyEvent(field, 'change'); +}; + + // Check if password fill is done to a plain text field const passwordFillIsAllowed = function(elem) { const elementIsPasswordField = diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 19f0031..0af94ee 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -719,60 +719,6 @@ kpxc.setPasswordFilled = async function(state) { await sendMessage('password_set_filled', state); }; -// Special handling for setting value to select and checkbox elements -kpxc.setValue = function(field, value, forced = false) { - if (field.matches('select')) { - value = value.toLowerCase().trim(); - const options = field.querySelectorAll('option'); - - for (const o of options) { - if (o.textContent.toLowerCase().trim() === value) { - kpxc.setValueWithChange(field, o.value); - return false; - } - } - - return; - } else if (field.getLowerCaseAttribute('type') === 'checkbox' && value?.toLowerCase() === 'true') { - field.checked = true; - } - - // Make sure the input is not wrapped inside another element (custom INPUT element) - if (field?.nodeName !== 'INPUT' && field?.nodeName?.includes('INPUT')) { - const childInput = field?.querySelector('input'); - const fieldsFromShadowDOM = kpxcObserverHelper.findInputsFromShadowDOM(field); - field = childInput ?? fieldsFromShadowDOM[0]; - } - - - kpxc.setValueWithChange(field, value, forced); -}; - -// Sets a new value to input field and triggers necessary events -kpxc.setValueWithChange = function(field, value, forced = false) { - if (!field || (field?.readOnly && !forced)) { - return; - } - - const dispatchLegacyEvent = function(elem, eventName) { - const legacyEvent = elem.ownerDocument.createEvent('Event'); - legacyEvent.initEvent(eventName, true, false); - elem.dispatchEvent(legacyEvent); - }; - - field.focus(); - field.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: false })); - field.dispatchEvent(new KeyboardEvent('keypress', { bubbles: true, cancelable: false })); - field.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: false })); - field.dispatchEvent(new Event('input', { bubbles: true, cancelable: false })); - field.dispatchEvent(new Event('change', { bubbles: true, cancelable: false })); - field.value = value; - - // Some pages will not accept the value change without dispatching events directly to the document - dispatchLegacyEvent(field, 'input'); - dispatchLegacyEvent(field, 'change'); -}; - kpxc.showGroupNameInAutocomplete = function() { return !kpxc.settings.useCompactMode || (kpxc.settings.showGroupNameInAutocomplete && kpxc.getUniqueGroupCount(kpxc.credentials) > 1);