mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Merge pull request #1216 from keepassxreboot/feature/multiple_totp_fields
Fill multiple single TOTP fields
This commit is contained in:
commit
07d59efebc
4 changed files with 98 additions and 22 deletions
|
|
@ -317,6 +317,7 @@ kpxcForm.saveForm = function(form, combination) {
|
|||
username: combination.username,
|
||||
password: combination.password,
|
||||
totp: combination.totp,
|
||||
totpInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && kpxcTOTPIcons.isValid(e)),
|
||||
passwordInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type === 'password')
|
||||
});
|
||||
};
|
||||
|
|
@ -376,6 +377,46 @@ kpxcFields.getAllCombinations = async function(inputs) {
|
|||
combinations.push(combination);
|
||||
}
|
||||
|
||||
// Check for multiple segmented TOTP fields
|
||||
if (combinations.length === 0) {
|
||||
kpxcFields.getSegmentedTOTPFields(inputs, combinations);
|
||||
}
|
||||
|
||||
return combinations;
|
||||
};
|
||||
|
||||
// Adds segmented TOTP fields to the combination if found
|
||||
kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) {
|
||||
const addTotpFieldsToCombination = function(inputFields) {
|
||||
const totpInputs = Array.from(inputFields).filter(e => e.nodeName === 'INPUT' && e.type !== 'password');
|
||||
if (totpInputs.length === 6) {
|
||||
const combination = {
|
||||
form: form,
|
||||
totpInputs: totpInputs
|
||||
};
|
||||
|
||||
combinations.push(combination);
|
||||
|
||||
// Create an icon to the right side of the segmented fields
|
||||
kpxcTOTPIcons.newIcon(totpInputs[totpInputs.length - 1], kpxc.databaseState, true);
|
||||
kpxcIcons.icons.push({
|
||||
field: totpInputs[totpInputs.length - 1],
|
||||
iconType: kpxcIcons.iconTypes.TOTP,
|
||||
segmented: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const form = inputs.length > 0 ? inputs[0].form : undefined;
|
||||
if (form && (acceptedOTPFields.some(f => form.className.includes(f) || form.id.includes(f) || form.name.includes(f))
|
||||
|| form.length === 6)) {
|
||||
// Use the form's elements
|
||||
addTotpFieldsToCombination(form.elements);
|
||||
} else if (inputs.length === 6 && inputs.every(i => i.inputMode === 'numeric' && i.pattern.includes('0-9'))) {
|
||||
// No form is found, but input fields are possibly segmented TOTP fields
|
||||
addTotpFieldsToCombination(inputs);
|
||||
}
|
||||
|
||||
return combinations;
|
||||
};
|
||||
|
||||
|
|
@ -420,8 +461,8 @@ kpxcFields.getCombination = async function(field, givenType) {
|
|||
for (const combination of kpxc.combinations) {
|
||||
if (!givenType && Object.values(combination).find(c => c === field)) {
|
||||
return combination;
|
||||
} else if (givenType) {
|
||||
if (combination[givenType] === field) {
|
||||
} else if (givenType && combination[givenType]) {
|
||||
if (combination[givenType] === field || combination[givenType].includes(field)) {
|
||||
return combination;
|
||||
}
|
||||
}
|
||||
|
|
@ -664,7 +705,7 @@ kpxcFields.useCustomLoginFields = async function() {
|
|||
// Handle custom TOTP field
|
||||
if (totp) {
|
||||
totp.setAttribute('kpxc-defined', 'totp');
|
||||
kpxcTOTPIcons.newIcon(totp, kpxc.databaseState, true);
|
||||
kpxcTOTPIcons.newIcon(totp, kpxc.databaseState);
|
||||
}
|
||||
|
||||
// If not all expected fields are identified, return an empty combination
|
||||
|
|
@ -910,12 +951,12 @@ kpxc.fillFromTOTP = async function(target) {
|
|||
const el = target || document.activeElement;
|
||||
const credentialList = await kpxc.updateTOTPList();
|
||||
|
||||
if (credentialList.length === 0) {
|
||||
if (credentialList && credentialList.length === 0) {
|
||||
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (credentialList.length === 1) {
|
||||
if (credentialList && credentialList.length === 1) {
|
||||
kpxc.fillTOTPFromUuid(el, credentialList[0].uuid);
|
||||
return;
|
||||
}
|
||||
|
|
@ -942,18 +983,45 @@ kpxc.fillTOTPFromUuid = async function(el, uuid) {
|
|||
return;
|
||||
}
|
||||
|
||||
kpxc.setValue(el, totp);
|
||||
kpxc.setTOTPValue(el, totp);
|
||||
} else if (user.stringFields && user.stringFields.length > 0) {
|
||||
const stringFields = user.stringFields;
|
||||
for (const s of stringFields) {
|
||||
const val = s['KPH: {TOTP}'];
|
||||
if (val) {
|
||||
kpxc.setValue(el, val);
|
||||
kpxc.setTOTPValue(el, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set normal or segmented TOTP value
|
||||
kpxc.setTOTPValue = function(elem, val) {
|
||||
if (kpxc.combinations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const comb of kpxc.combinations) {
|
||||
if (comb.totpInputs && comb.totpInputs.length === 6) {
|
||||
kpxc.fillSegmentedTotp(elem, val, comb.totpInputs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
kpxc.setValue(elem, val);
|
||||
};
|
||||
|
||||
// Fill TOTP in parts
|
||||
kpxc.fillSegmentedTotp = function(elem, val, totpInputs) {
|
||||
if (!totpInputs.includes(elem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 6; ++i) {
|
||||
kpxc.setValue(totpInputs[i], val[i]);
|
||||
}
|
||||
};
|
||||
|
||||
// Fill requested from username icon
|
||||
kpxc.fillFromUsernameIcon = async function(combination) {
|
||||
await kpxc.receiveCredentialsIfNecessary();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ TOTPAutocomplete.prototype.itemEnter = async function(index, elements) {
|
|||
};
|
||||
|
||||
TOTPAutocomplete.prototype.fillTotp = async function(index, uuid) {
|
||||
const combination = await kpxcFields.getCombination(this.input);
|
||||
const combination = await kpxcFields.getCombination(this.input, 'totp')
|
||||
|| await kpxcFields.getCombination(this.input, 'totpInputs');
|
||||
combination.loginId = index;
|
||||
kpxc.fillTOTPFromUuid(this.input, uuid);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,14 +13,15 @@ const acceptedOTPFields = [
|
|||
'otp',
|
||||
'token',
|
||||
'twofa',
|
||||
'two-factor',
|
||||
'twofactor'
|
||||
];
|
||||
|
||||
var kpxcTOTPIcons = {};
|
||||
kpxcTOTPIcons.icons = [];
|
||||
|
||||
kpxcTOTPIcons.newIcon = function(field, databaseState = DatabaseState.DISCONNECTED, forced = false) {
|
||||
kpxcTOTPIcons.icons.push(new TOTPFieldIcon(field, databaseState, forced));
|
||||
kpxcTOTPIcons.newIcon = function(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) {
|
||||
kpxcTOTPIcons.icons.push(new TOTPFieldIcon(field, databaseState, segmented));
|
||||
};
|
||||
|
||||
kpxcTOTPIcons.switchIcon = function(state) {
|
||||
|
|
@ -75,25 +76,25 @@ kpxcTOTPIcons.isValid = function(field, forced) {
|
|||
};
|
||||
|
||||
class TOTPFieldIcon extends Icon {
|
||||
constructor(field, databaseState = DatabaseState.DISCONNECTED, forced = false) {
|
||||
super(field, databaseState);
|
||||
constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) {
|
||||
super(field, databaseState, segmented);
|
||||
|
||||
this.initField(field, forced);
|
||||
this.initField(field, segmented);
|
||||
kpxcUI.monitorIconPosition(this);
|
||||
}
|
||||
}
|
||||
|
||||
TOTPFieldIcon.prototype.initField = function(field, forced) {
|
||||
TOTPFieldIcon.prototype.initField = function(field, segmented) {
|
||||
// Observer the visibility
|
||||
if (this.observer) {
|
||||
this.observer.observe(field);
|
||||
}
|
||||
|
||||
this.createIcon(field);
|
||||
this.createIcon(field, segmented);
|
||||
this.inputField = field;
|
||||
};
|
||||
|
||||
TOTPFieldIcon.prototype.createIcon = function(field) {
|
||||
TOTPFieldIcon.prototype.createIcon = function(field, segmented = false) {
|
||||
const className = (isFirefox() ? 'moz' : 'default');
|
||||
|
||||
// Size the icon dynamically, but not greater than 24 or smaller than 14
|
||||
|
|
@ -125,7 +126,7 @@ TOTPFieldIcon.prototype.createIcon = function(field) {
|
|||
kpxc.fillFromTOTP(field);
|
||||
});
|
||||
|
||||
kpxcUI.setIconPosition(icon, field, this.rtl);
|
||||
kpxcUI.setIconPosition(icon, field, this.rtl, segmented);
|
||||
this.icon = icon;
|
||||
|
||||
const styleSheet = document.createElement('link');
|
||||
|
|
|
|||
|
|
@ -23,11 +23,12 @@ const Pixels = function(value) {
|
|||
|
||||
// Basic icon class
|
||||
class Icon {
|
||||
constructor(field, databaseState = DatabaseState.DISCONNECTED) {
|
||||
constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) {
|
||||
this.databaseState = databaseState;
|
||||
this.icon = null;
|
||||
this.inputField = null;
|
||||
this.rtl = kpxcUI.isRTL(field);
|
||||
this.segmented = segmented;
|
||||
|
||||
try {
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
|
|
@ -103,7 +104,7 @@ kpxcUI.monitorIconPosition = function(iconClass) {
|
|||
|
||||
kpxcUI.updateIconPosition = function(iconClass) {
|
||||
if (iconClass.inputField && iconClass.icon) {
|
||||
kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField, iconClass.rtl);
|
||||
kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField, iconClass.rtl, iconClass.segmented);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -112,13 +113,18 @@ kpxcUI.calculateIconOffset = function(field, size) {
|
|||
return (offset < 0) ? 0 : offset;
|
||||
};
|
||||
|
||||
kpxcUI.setIconPosition = function(icon, field, rtl = false) {
|
||||
kpxcUI.setIconPosition = function(icon, field, rtl = false, segmented = false) {
|
||||
const rect = field.getBoundingClientRect();
|
||||
const size = Number(icon.getAttribute('size'));
|
||||
const offset = kpxcUI.calculateIconOffset(field, size);
|
||||
const left = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left;
|
||||
let left = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left;
|
||||
const top = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top;
|
||||
|
||||
// Add more space for the icon to show it at the right side of the field if TOTP fields are segmented
|
||||
if (segmented) {
|
||||
left += size + 10;
|
||||
}
|
||||
|
||||
icon.style.top = Pixels(top + document.scrollingElement.scrollTop + offset + 1);
|
||||
icon.style.left = rtl
|
||||
? Pixels((left + document.scrollingElement.scrollLeft) + offset)
|
||||
|
|
@ -181,7 +187,7 @@ kpxcUI.updateFromIntersectionObserver = function(iconClass, entries) {
|
|||
|
||||
// Wait for possible DOM animations
|
||||
setTimeout(() => {
|
||||
kpxcUI.setIconPosition(iconClass.icon, entry.target, iconClass.rtl);
|
||||
kpxcUI.setIconPosition(iconClass.icon, entry.target, iconClass.rtl, iconClass.segmented);
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue