mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Fix TOTP fill missing digits on some sites (fixes #2215)
Fixes Authelia and Epic Games OTP fill by addressing two underlying problems: 1. Incorrect DOM event ordering and data - So far only empty key events were sent, which may confuse OTP input implementations that rely on the text from those events instead of input field value. - The "beforeinput" event was missing altogether. - Reorder the events to comply to the UIEvents spec (https://w3c.github.io/uievents/#keypress-event-order) and change field.value at the expected moment in that order. 2. Focus events were not sent As described in https://github.com/keepassxreboot/keepassxc-browser/issues/2215#issuecomment-3629070227, the extension focuses fields sequentially when filling in the OTP digits, but this does not trigger focus/blur events as this is performed within the same window event loop cycle, throwing off certain implementations of multi-textfield OTP input; in the cause of Authelia filling an internal digit buffer starting from nonexistent index -1. Sending those events manually was attempted but did not yield satisfactory results as some frameworks like React use Promise objects to schedule their state update, resulting in the problem still being present. Instead, makes kpxcFill.setValueWithChange async so it can await on an empty Promise of its own, giving time for the user agent to trigger the appropriate focus events as well as for pages' scripts to settle.
This commit is contained in:
parent
fdbd077852
commit
d3cc7ee925
1 changed files with 34 additions and 23 deletions
|
|
@ -18,7 +18,7 @@ kpxcFill.fillAttributeToActiveElementWith = async function(attr) {
|
|||
return;
|
||||
}
|
||||
|
||||
kpxcFill.setValue(el, value[0]);
|
||||
await kpxcFill.setValue(el, value[0]);
|
||||
};
|
||||
|
||||
// Fill requested from the context menu. Active element is used for combination detection
|
||||
|
|
@ -152,20 +152,20 @@ kpxcFill.fillTOTPFromUuid = async function(el, uuid) {
|
|||
return;
|
||||
}
|
||||
|
||||
kpxcFill.setTOTPValue(el, totp);
|
||||
await kpxcFill.setTOTPValue(el, totp);
|
||||
} else if (user.stringFields?.length > 0) {
|
||||
const stringFields = user.stringFields;
|
||||
for (const s of stringFields) {
|
||||
const val = s['KPH: {TOTP}'];
|
||||
if (val) {
|
||||
kpxcFill.setTOTPValue(el, val);
|
||||
await kpxcFill.setTOTPValue(el, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set normal or segmented TOTP value
|
||||
kpxcFill.setTOTPValue = function(elem, val) {
|
||||
kpxcFill.setTOTPValue = async function(elem, val) {
|
||||
if (kpxc.credentials.length === 0) {
|
||||
logDebug('Error: Credential list is empty.');
|
||||
return;
|
||||
|
|
@ -173,22 +173,22 @@ kpxcFill.setTOTPValue = function(elem, val) {
|
|||
|
||||
for (const comb of kpxc.combinations) {
|
||||
if (comb.totpInputs?.length > 0) {
|
||||
kpxcFill.fillSegmentedTotp(elem, val, comb.totpInputs);
|
||||
await kpxcFill.fillSegmentedTotp(elem, val, comb.totpInputs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
kpxcFill.setValue(elem, val);
|
||||
await kpxcFill.setValue(elem, val);
|
||||
};
|
||||
|
||||
// Fill TOTP in parts
|
||||
kpxcFill.fillSegmentedTotp = function(elem, val, totpInputs) {
|
||||
kpxcFill.fillSegmentedTotp = async function(elem, val, totpInputs) {
|
||||
if (!totpInputs.includes(elem) || val.length < totpInputs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < totpInputs.length; ++i) {
|
||||
kpxcFill.setValue(totpInputs[i], val[i]);
|
||||
await kpxcFill.setValue(totpInputs[i], val[i]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
|
|||
return;
|
||||
}
|
||||
|
||||
kpxcFill.setValueWithChange(combination.password, selectedCredentials.password);
|
||||
await kpxcFill.setValueWithChange(combination.password, selectedCredentials.password);
|
||||
await kpxc.setPasswordFilled(true);
|
||||
}
|
||||
|
||||
|
|
@ -279,20 +279,20 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
|
|||
if (combination.username && usernameValue && combination.username !== combination.password &&
|
||||
(!combination.username.value || combination.username.value !== usernameValue)) {
|
||||
if (!passOnly) {
|
||||
kpxcFill.setValueWithChange(combination.username, usernameValue);
|
||||
await kpxcFill.setValueWithChange(combination.username, usernameValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill StringFields
|
||||
if (selectedCredentials.stringFields?.length > 0) {
|
||||
kpxcFill.fillInStringFields(combination.fields, selectedCredentials.stringFields);
|
||||
await kpxcFill.fillInStringFields(combination.fields, selectedCredentials.stringFields);
|
||||
}
|
||||
|
||||
// Fill TOTP
|
||||
if (kpxc.settings.autoFillSingleTotp && kpxc.entryHasTotp(selectedCredentials)) {
|
||||
const totpCombination = combination?.totp || kpxc.combinations?.find(c => c.totp);
|
||||
if (totpCombination?.totp) {
|
||||
kpxcFill.fillTOTPFromUuid(totpCombination.totp, selectedCredentials.uuid);
|
||||
await kpxcFill.fillTOTPFromUuid(totpCombination.totp, selectedCredentials.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +306,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
|
|||
};
|
||||
|
||||
// Fills StringFields defined in Custom Fields
|
||||
kpxcFill.fillInStringFields = function(fields, stringFields) {
|
||||
kpxcFill.fillInStringFields = async function(fields, stringFields) {
|
||||
const filledInFields = [];
|
||||
if (fields && stringFields && fields?.length > 0 && stringFields?.length > 0) {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
|
|
@ -318,7 +318,7 @@ kpxcFill.fillInStringFields = function(fields, stringFields) {
|
|||
const currentField = fields[i];
|
||||
|
||||
if (currentField && stringFieldValue[0]) {
|
||||
kpxcFill.setValue(currentField, stringFieldValue[0], true);
|
||||
await kpxcFill.setValue(currentField, stringFieldValue[0], true);
|
||||
filledInFields.push(currentField);
|
||||
}
|
||||
}
|
||||
|
|
@ -353,14 +353,14 @@ kpxcFill.performAutoSubmit = async function(combination, skipAutoSubmit) {
|
|||
};
|
||||
|
||||
// Special handling for setting value to select and checkbox elements
|
||||
kpxcFill.setValue = function(field, value, forced = false) {
|
||||
kpxcFill.setValue = async 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);
|
||||
await kpxcFill.setValueWithChange(field, o.value);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -378,11 +378,11 @@ kpxcFill.setValue = function(field, value, forced = false) {
|
|||
}
|
||||
|
||||
|
||||
kpxcFill.setValueWithChange(field, value, forced);
|
||||
await kpxcFill.setValueWithChange(field, value, forced);
|
||||
};
|
||||
|
||||
// Sets a new value to input field and triggers necessary events
|
||||
kpxcFill.setValueWithChange = function(field, value, forced = false) {
|
||||
kpxcFill.setValueWithChange = async function(field, value, forced = false) {
|
||||
if (!field || (field?.readOnly && !forced)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -401,12 +401,23 @@ kpxcFill.setValueWithChange = function(field, value, forced = false) {
|
|||
};
|
||||
|
||||
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 }));
|
||||
|
||||
// Use a delay to allow focus events to trigger and to give some
|
||||
// breathing room to frameworks that rely on promises to update their
|
||||
// state, like React. Not doing so can break OTP input, see issue #2215.
|
||||
await Promise.resolve();
|
||||
|
||||
field.dispatchEvent(new FocusEvent('focus', { bubbles: false, cancelable: false }));
|
||||
field.dispatchEvent(new FocusEvent('focusin', { bubbles: true, cancelable: false }));
|
||||
|
||||
// https://w3c.github.io/uievents/#keypress-event-order
|
||||
field.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: false, key: value }));
|
||||
field.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: false, inputType: 'insertText', data: value }));
|
||||
field.dispatchEvent(new KeyboardEvent('keypress', { bubbles: true, cancelable: false, key: value }));
|
||||
field.value = value;
|
||||
field.dispatchEvent(new Event('input', { bubbles: true, cancelable: false }));
|
||||
field.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: false, key: value }));
|
||||
field.dispatchEvent(new Event('change', { bubbles: true, cancelable: false }));
|
||||
|
||||
// Some pages will not accept the value change without dispatching events directly to the document
|
||||
dispatchLegacyEvent(field, 'input');
|
||||
|
|
|
|||
Loading…
Reference in a new issue