TOTP Autocomplete Menu and support for filling TOTP from another database.

This commit is contained in:
varjolintu 2021-01-10 11:16:03 +02:00
parent 0705bce62f
commit b852185bea
7 changed files with 414 additions and 269 deletions

View file

@ -17,6 +17,7 @@
"block-scoped-var": "off",
"brace-style": ["warn", "1tbs"],
"camelcase": "warn",
"class-methods-use-this": "off",
"comma-dangle": "off",
"computed-property-spacing": ["error", "never"],
"consistent-return": "off",
@ -91,7 +92,6 @@
"jQuery": true,
"keepass": true,
"kpxc": true,
"kpxcAutocomplete": true,
"kpxcBanner": true,
"kpxcDefine": true,
"kpxcFields": true,
@ -99,7 +99,9 @@
"kpxcPasswordDialog": true,
"kpxcPasswordIcons": true,
"kpxcSites": true,
"kpxcTOTPAutocomplete": true,
"kpxcTOTPIcons": true,
"kpxcUserAutocomplete": true,
"kpxcUsernameIcons": true,
"kpxcUI": true,
"kpxcUsernameField": true,

View file

@ -2,245 +2,261 @@
const MAX_AUTOCOMPLETE_NAME_LEN = 50;
const kpxcAutocomplete = {};
kpxcAutocomplete.autoSubmit = false;
kpxcAutocomplete.elements = [];
kpxcAutocomplete.started = false;
kpxcAutocomplete.index = -1;
kpxcAutocomplete.input = undefined;
kpxcAutocomplete.shadowRoot = undefined;
kpxcAutocomplete.wrapper = undefined;
kpxcAutocomplete.create = async function(input, showListInstantly = false, autoSubmit = false) {
if (input.readOnly) {
return;
class Autocomplete {
constructor() {
this.autoSubmit = false;
this.elements = [];
this.started = false;
this.index = -1;
this.input = undefined;
this.shadowRoot = undefined;
this.wrapper = undefined;
}
kpxcAutocomplete.autoSubmit = autoSubmit;
kpxcAutocomplete.input = input;
kpxcAutocomplete.started = true;
clear() {
this.elements = [];
}
input.addEventListener('click', function(e) {
async click(e, input) {
}
async itemClick(e, item, input, uuid) {
}
async itemEnter(index, elements) {
}
async create(input, showListInstantly = false, autoSubmit = false) {
if (input.readOnly) {
return;
}
this.autoSubmit = autoSubmit;
this.input = input;
this.started = true;
input.addEventListener('click', ev => this.click(ev, this.input));
input.addEventListener('keydown', ev => this.keyPress(ev));
input.setAttribute('autocomplete', 'off');
if (showListInstantly) {
this.showList(input);
}
}
mouseOver(e, div, item) {
this.removeItem(this.getAllItems());
item.classList.add('kpxcAutocomplete-active');
this.index = Array.from(div.childNodes).indexOf(item);
}
mouseOut(e, item) {
item.classList.remove('kpxcAutocomplete-active');
}
async showList(inputField) {
this.closeList();
this.input = inputField;
this.input.select();
const div = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' });
initColorTheme(div);
this.updatePosition(inputField, div);
div.style.zIndex = '2147483646';
const styleSheet = createStylesheet('css/autocomplete.css');
const colorStyleSheet = createStylesheet('css/colors.css');
const wrapper = kpxcUI.createElement('div');
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
this.shadowRoot.append(colorStyleSheet);
this.shadowRoot.append(styleSheet);
this.shadowRoot.append(div);
this.wrapper = wrapper;
document.body.append(wrapper);
await kpxc.updateTOTPList();
for (const c of this.elements) {
const item = document.createElement('div');
item.textContent += c.label;
const itemInput = kpxcUI.createElement('input', '', { 'type': 'hidden', 'value': c.value });
item.append(itemInput);
item.addEventListener('click', ev => this.itemClick(ev, item, inputField, c.uuid));
// These events prevent the double hover effect if both keyboard and mouse are used
item.addEventListener('mouseover', ev => this.mouseOver(ev, div, item));
item.addEventListener('mouseout', ev => this.mouseOut(ev, item));
div.appendChild(item);
}
// Add a footer message for auto-submit
if (this.autoSubmit) {
const footer = kpxcUI.createElement('footer', '', {}, tr('autocompleteSubmitMessage'));
div.appendChild(footer);
}
// Activate the first item automatically
const items = this.getAllItems();
this.index = 0;
this.activateItem(items);
}
activateItem(item) {
if (!item || item.length === 0) {
return;
}
this.removeItem(item);
if (this.index >= item.length) {
this.index = 0;
}
if (this.index < 0) {
this.index = item.length - 1;
}
if (item[this.index] !== undefined) {
item[this.index].classList.add('kpxcAutocomplete-active');
}
}
removeItem(items) {
for (const item of items) {
item.classList.remove('kpxcAutocomplete-active');
}
}
closeList(elem) {
if (!this.shadowRoot) {
return;
}
const items = this.shadowSelectorAll('.kpxcAutocomplete-items');
if (!items) {
return;
}
for (const item of items) {
if (elem !== item && this.input) {
item.parentNode.removeChild(item);
}
}
}
getAllItems() {
const list = this.shadowSelector('#kpxcAutocomplete-list');
if (!list) {
return [];
}
return list.getElementsByTagName('div');
}
/**
* Keyboard shortcuts for autocomplete menu:
* - ArrowDown shows the list or selects item below, or the first item (last is active)
* - ArrowUp selects item above, or the last item (first is active)
* - Enter or Tab selects the item
* - Backspace and Delete shows the list if input field is empty. First item is activated
*/
keyPress(e) {
if (!e.isTrusted) {
return;
}
if (input.value !== '') {
input.select();
}
kpxcAutocomplete.showList(input);
});
const items = this.getAllItems();
if (e.key === 'ArrowDown') {
// If the list is not visible, show it
if (items.length === 0) {
this.index = -1;
this.showList(this.input);
} else {
// Activate next item
++this.index;
this.activateItem(items);
}
} else if (e.key === 'ArrowUp') {
--this.index;
this.activateItem(items);
} else if (e.key === 'Enter') {
if (this.input.value === '') {
e.preventDefault();
}
input.addEventListener('keydown', kpxcAutocomplete.keyPress);
input.setAttribute('autocomplete', 'off');
if (this.index >= 0 && items && items[this.index] !== undefined) {
e.preventDefault();
if (showListInstantly) {
kpxcAutocomplete.showList(input);
}
};
kpxcAutocomplete.showList = function(inputField) {
kpxcAutocomplete.closeList();
kpxcAutocomplete.input = inputField;
const div = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' });
initColorTheme(div);
kpxcAutocomplete.updatePosition(inputField, div);
div.style.zIndex = '2147483646';
const styleSheet = createStylesheet('css/autocomplete.css');
const colorStyleSheet = createStylesheet('css/colors.css');
const wrapper = kpxcUI.createElement('div');
kpxcAutocomplete.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
kpxcAutocomplete.shadowRoot.append(colorStyleSheet);
kpxcAutocomplete.shadowRoot.append(styleSheet);
kpxcAutocomplete.shadowRoot.append(div);
kpxcAutocomplete.wrapper = wrapper;
document.body.append(wrapper);
for (const c of kpxcAutocomplete.elements) {
const item = document.createElement('div');
item.textContent += c.label;
const itemInput = kpxcUI.createElement('input', '', { 'type': 'hidden', 'value': c.value });
item.append(itemInput);
item.addEventListener('click', async function(e) {
if (!e.isTrusted) {
this.itemEnter(this.index, this.elements);
this.closeList();
}
} else if (e.key === 'Tab') {
// Return if value is not in the list
if (this.input.value !== '' && !this.elements.some(c => c.value === this.input.value)) {
this.closeList();
return;
}
// Save index for combination.loginId
const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget);
await sendMessage('page_set_login_id', index);
this.index = this.elements.findIndex(c => c.value === this.input.value);
if (this.index >= 0) {
this.fillPassword(this.input.value, this.index, this.elements[this.index].uuid);
}
const usernameValue = this.getElementsByTagName('input')[0].value;
await kpxcAutocomplete.fillPassword(usernameValue, index, c.uuid);
kpxcAutocomplete.closeList();
inputField.focus();
});
// These events prevent the double hover effect if both keyboard and mouse are used
item.addEventListener('mouseover', function(e) {
kpxcAutocomplete.removeItem(kpxcAutocomplete.getAllItems());
item.classList.add('kpxcAutocomplete-active');
kpxcAutocomplete.index = Array.from(div.childNodes).indexOf(item);
});
item.addEventListener('mouseout', function(e) {
item.classList.remove('kpxcAutocomplete-active');
});
div.appendChild(item);
}
// Add a footer message for auto-submit
if (kpxcAutocomplete.autoSubmit) {
const footer = kpxcUI.createElement('footer', '', {}, tr('autocompleteSubmitMessage'));
div.appendChild(footer);
}
// Activate the first item automatically
const items = kpxcAutocomplete.getAllItems();
kpxcAutocomplete.index = 0;
kpxcAutocomplete.activateItem(items);
};
kpxcAutocomplete.activateItem = function(item) {
if (!item || item.length === 0) {
return;
}
kpxcAutocomplete.removeItem(item);
if (kpxcAutocomplete.index >= item.length) {
kpxcAutocomplete.index = 0;
}
if (kpxcAutocomplete.index < 0) {
kpxcAutocomplete.index = item.length - 1;
}
if (item[kpxcAutocomplete.index] !== undefined) {
item[kpxcAutocomplete.index].classList.add('kpxcAutocomplete-active');
}
};
kpxcAutocomplete.removeItem = function(items) {
for (const item of items) {
item.classList.remove('kpxcAutocomplete-active');
}
};
kpxcAutocomplete.closeList = function(elem) {
if (!kpxcAutocomplete.shadowRoot) {
return;
}
const items = kpxcAutocomplete.shadowSelectorAll('.kpxcAutocomplete-items');
if (!items) {
return;
}
for (const item of items) {
if (elem !== item && kpxcAutocomplete.input) {
item.parentNode.removeChild(item);
this.closeList();
} else if (e.key === 'Escape') {
this.closeList();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && this.input.value === '') {
// Show menu when input field has no value and backspace is pressed
this.index = -1;
this.showList(this.input);
}
}
};
kpxcAutocomplete.getAllItems = function() {
const list = kpxcAutocomplete.shadowSelector('#kpxcAutocomplete-list');
if (!list) {
return [];
}
return list.getElementsByTagName('div');
};
/**
* Keyboard shortcuts for autocomplete menu:
* - ArrowDown shows the list or selects item below, or the first item (last is active)
* - ArrowUp selects item above, or the last item (first is active)
* - Enter or Tab selects the item
* - Backspace and Delete shows the list if input field is empty. First item is activated
*/
kpxcAutocomplete.keyPress = function(e) {
if (!e.isTrusted) {
return;
}
const items = kpxcAutocomplete.getAllItems();
if (e.key === 'ArrowDown') {
// If the list is not visible, show it
if (items.length === 0) {
kpxcAutocomplete.index = -1;
kpxcAutocomplete.showList(kpxcAutocomplete.input);
} else {
// Activate next item
++kpxcAutocomplete.index;
kpxcAutocomplete.activateItem(items);
}
} else if (e.key === 'ArrowUp') {
--kpxcAutocomplete.index;
kpxcAutocomplete.activateItem(items);
} else if (e.key === 'Enter') {
if (kpxcAutocomplete.input.value === '') {
e.preventDefault();
}
if (kpxcAutocomplete.index >= 0 && items && items[kpxcAutocomplete.index] !== undefined) {
e.preventDefault();
const usernameValue = kpxcAutocomplete.elements[kpxcAutocomplete.index].value;
kpxcAutocomplete.fillPassword(usernameValue, kpxcAutocomplete.index, kpxcAutocomplete.elements[kpxcAutocomplete.index].uuid);
kpxcAutocomplete.closeList();
}
} else if (e.key === 'Tab') {
// Return if value is not in the list
if (kpxcAutocomplete.input.value !== '' && !kpxcAutocomplete.elements.some(c => c.value === kpxcAutocomplete.input.value)) {
kpxcAutocomplete.closeList();
updatePosition(inputField, elem) {
const div = elem || this.shadowSelector('.kpxcAutocomplete-items');
if (!div) {
return;
}
kpxcAutocomplete.index = kpxcAutocomplete.elements.findIndex(c => c.value === kpxcAutocomplete.input.value);
if (kpxcAutocomplete.index >= 0) {
kpxcAutocomplete.fillPassword(kpxcAutocomplete.input.value, kpxcAutocomplete.index, kpxcAutocomplete.elements[kpxcAutocomplete.index].uuid);
const rect = inputField.getBoundingClientRect();
div.style.minWidth = Pixels(inputField.offsetWidth);
if (kpxcUI.bodyStyle.position.toLowerCase() === 'relative') {
div.style.top = Pixels(rect.top - kpxcUI.bodyRect.top + document.scrollingElement.scrollTop + inputField.offsetHeight);
div.style.left = Pixels(rect.left - kpxcUI.bodyRect.left + document.scrollingElement.scrollLeft);
} else {
div.style.top = Pixels(rect.top + document.scrollingElement.scrollTop + inputField.offsetHeight);
div.style.left = Pixels(rect.left + document.scrollingElement.scrollLeft);
}
kpxcAutocomplete.closeList();
} else if (e.key === 'Escape') {
kpxcAutocomplete.closeList();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && kpxcAutocomplete.input.value === '') {
// Show menu when input field has no value and backspace is pressed
kpxcAutocomplete.index = -1;
kpxcAutocomplete.showList(kpxcAutocomplete.input);
}
};
}
kpxcAutocomplete.fillPassword = async function(value, index, uuid) {
const combination = await kpxcFields.getCombination(kpxcAutocomplete.input);
combination.loginId = index;
const manualFill = await sendMessage('page_get_manual_fill');
await kpxc.fillInCredentials(combination, value, uuid, manualFill === ManualFill.PASSWORD);
kpxcAutocomplete.input.setAttribute('fetched', true);
};
kpxcAutocomplete.updatePosition = function(inputField, elem) {
const div = elem || kpxcAutocomplete.shadowSelector('.kpxcAutocomplete-items');
if (!div) {
// Global handlers
const handleOutsideClick = function(e, autocompleteMenu) {
const list = autocompleteMenu.shadowRoot ? autocompleteMenu.shadowSelector('#kpxcAutocomplete-list') : undefined;
if (!list) {
return;
}
const rect = inputField.getBoundingClientRect();
div.style.minWidth = Pixels(inputField.offsetWidth);
if (e.target !== autocompleteMenu.input
&& !e.target.classList.contains('kpxc-username-icon')
&& e.target.nodeName !== autocompleteMenu.input.nodeName) {
autocompleteMenu.closeList(e.target);
if (kpxcUI.bodyStyle.position.toLowerCase() === 'relative') {
div.style.top = Pixels(rect.top - kpxcUI.bodyRect.top + document.scrollingElement.scrollTop + inputField.offsetHeight);
div.style.left = Pixels(rect.left - kpxcUI.bodyRect.left + document.scrollingElement.scrollLeft);
} else {
div.style.top = Pixels(rect.top + document.scrollingElement.scrollTop + inputField.offsetHeight);
div.style.left = Pixels(rect.left + document.scrollingElement.scrollLeft);
if (autocompleteMenu.wrapper) {
document.body.removeChild(autocompleteMenu.wrapper);
}
}
};
const updatePosition = function(autocompleteMenu) {
if (autocompleteMenu.input) {
autocompleteMenu.updatePosition(autocompleteMenu.input);
}
};
@ -250,20 +266,8 @@ document.addEventListener('click', function(e) {
return;
}
const list = kpxcAutocomplete.shadowRoot ? kpxcAutocomplete.shadowSelector('#kpxcAutocomplete-list') : undefined;
if (!list) {
return;
}
if (e.target !== kpxcAutocomplete.input
&& !e.target.classList.contains('kpxc-username-icon')
&& e.target.nodeName !== kpxcAutocomplete.input.nodeName) {
kpxcAutocomplete.closeList(e.target);
if (kpxcAutocomplete.wrapper) {
document.body.removeChild(kpxcAutocomplete.wrapper);
}
}
handleOutsideClick(e, kpxcUserAutocomplete);
handleOutsideClick(e, kpxcTOTPAutocomplete);
});
// Handle autocomplete position on window resize
@ -272,9 +276,8 @@ window.addEventListener('resize', function() {
return;
}
if (kpxcAutocomplete.input) {
kpxcAutocomplete.updatePosition(kpxcAutocomplete.input);
}
updatePosition(kpxcUserAutocomplete);
updatePosition(kpxcTOTPAutocomplete);
});
// Handle autocomplete position on scroll
@ -283,7 +286,6 @@ window.addEventListener('scroll', function() {
return;
}
if (kpxcAutocomplete.input) {
kpxcAutocomplete.updatePosition(kpxcAutocomplete.input);
}
updatePosition(kpxcUserAutocomplete);
updatePosition(kpxcTOTPAutocomplete);
});

View file

@ -0,0 +1,45 @@
'use strict';
class CredentialAutocomplete extends Autocomplete {}
CredentialAutocomplete.prototype.click = async function(e, input) {
if (!e.isTrusted) {
return;
}
if (input.value !== '') {
input.select();
}
this.showList(input);
};
CredentialAutocomplete.prototype.itemClick = async function(e, item, input, uuid) {
if (!e.isTrusted) {
return;
}
const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget);
const usernameValue = item.getElementsByTagName('input')[0].value;
await this.fillPassword(usernameValue, index, uuid);
this.closeList();
input.focus();
};
CredentialAutocomplete.prototype.itemEnter = async function(index, elements) {
const usernameValue = elements[index].value;
this.fillPassword(usernameValue, index, elements[index].uuid);
};
CredentialAutocomplete.prototype.fillPassword = async function(value, index, uuid) {
const combination = await kpxcFields.getCombination(this.input);
combination.loginId = index;
await sendMessage('page_set_login_id', index);
const manualFill = await sendMessage('page_get_manual_fill');
await kpxc.fillInCredentials(combination, value, uuid, manualFill === ManualFill.PASSWORD);
this.input.setAttribute('fetched', true);
};
const kpxcUserAutocomplete = new CredentialAutocomplete();

View file

@ -155,7 +155,7 @@ kpxcForm.getCredentialFieldsFromForm = function(form) {
// Get the form submit button instead if action URL is same as the page itself
kpxcForm.getFormSubmitButton = function(form) {
if (!form.action || typeof form.action !== 'string') {
if (!form || !form.action || typeof form.action !== 'string') {
return;
}
@ -675,11 +675,11 @@ kpxc.addToSitePreferences = async function() {
kpxc.clearAllFromPage = function() {
kpxc.credentials = [];
kpxc.inputs = [];
kpxcAutocomplete.elements = [];
kpxcUserAutocomplete.clear();
_called.retrieveCredentials = false;
if (kpxc.settings.autoCompleteUsernames) {
kpxcAutocomplete.closeList();
kpxcUserAutocomplete.closeList();
}
// Switch back to default popup
@ -773,7 +773,7 @@ kpxc.fillInFromActiveElement = async function(passOnly = false) {
if (kpxc.credentials.length > 1) {
// More than one credential -> show autocomplete list
kpxcAutocomplete.showList(field);
kpxcUserAutocomplete.showList(field);
return;
} else {
// Just one credential -> fill the first combination found
@ -825,37 +825,53 @@ kpxc.fillFromPopup = async function(id, uuid) {
await sendMessage('page_set_login_id', id);
kpxc.fillInCredentials(kpxc.combinations[0], kpxc.credentials[id].login, uuid);
kpxcAutocomplete.closeList();
kpxcUserAutocomplete.closeList();
};
// Fill requested from TOTP icon
kpxc.fillFromTOTP = async function(target) {
const el = target || document.activeElement;
let index = await sendMessage('page_get_login_id');
const credentialList = await kpxc.updateTOTPList();
// Use the first credential available if not set
if (index === undefined) {
index = 0;
if (credentialList.length === 0) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
return;
}
if (index >= 0 && kpxc.credentials[index]) {
// Check the value from StringFields
if (kpxc.credentials[index].totp && kpxc.credentials[index].totp.length > 0) {
// Retrieve a new TOTP value
const totp = await sendMessage('get_totp', [ kpxc.credentials[index].uuid, kpxc.credentials[index].totp ]);
if (!totp) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
return;
}
if (credentialList.length === 1) {
kpxc.fillTOTPFromUuid(el, credentialList[0].uuid);
return;
}
kpxc.setValue(el, totp);
} else if (kpxc.credentials[index].stringFields && kpxc.credentials[index].stringFields.length > 0) {
const stringFields = kpxc.credentials[index].stringFields;
for (const s of stringFields) {
const val = s['KPH: {TOTP}'];
if (val) {
kpxc.setValue(el, val);
}
kpxcTOTPAutocomplete.showList(el, true);
};
// Fill TOTP with matching uuid
kpxc.fillTOTPFromUuid = async function(el, uuid) {
if (!el || !uuid) {
return;
}
const user = kpxc.credentials.find(c => c.uuid === uuid);
if (!user) {
return;
}
if (user.totp && user.totp.length > 0) {
// Retrieve a new TOTP value
const totp = await sendMessage('get_totp', [ user.uuid, user.totp ]);
if (!totp) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
return;
}
kpxc.setValue(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);
}
}
}
@ -867,7 +883,7 @@ kpxc.fillFromUsernameIcon = async function(combination) {
if (kpxc.credentials.length === 0) {
return;
} else if (kpxc.credentials.length > 1 && kpxc.settings.autoCompleteUsernames) {
kpxcAutocomplete.showList(combination.username || combination.password);
kpxcUserAutocomplete.showList(combination.username || combination.password);
return;
}
@ -930,7 +946,7 @@ kpxc.fillInCredentials = async function(combination, predefinedUsername, uuid, p
}
// Close autocomplete menu after fill
kpxcAutocomplete.closeList();
kpxcUserAutocomplete.closeList();
// Reset ManualFill
await sendMessage('page_set_manual_fill', ManualFill.NONE);
@ -940,7 +956,7 @@ kpxc.fillInCredentials = async function(combination, predefinedUsername, uuid, p
const submitButton = kpxcForm.getFormSubmitButton(combination.form);
if (submitButton !== undefined) {
submitButton.click();
} else {
} else if (combination.form) {
combination.form.submit();
}
}
@ -994,7 +1010,9 @@ kpxc.getFormActionUrl = function(combination) {
}
if (typeof(action) !== 'string' || action === '') {
action = document.location.origin + document.location.pathname;
// Firefox can report location.origin as 'null' with localHost files
const origin = document.location.origin === 'null' ? 'file://' : document.location.origin;
action = origin + document.location.pathname;
}
return action;
@ -1076,10 +1094,14 @@ kpxc.initAutocomplete = function() {
for (const c of kpxc.combinations) {
if (c.username) {
kpxcAutocomplete.create(c.username, false, kpxc.settings.autoSubmit);
kpxcUserAutocomplete.create(c.username, false, kpxc.settings.autoSubmit);
} else if (!c.username && c.password) {
// Single password field
kpxcAutocomplete.create(c.password, false, kpxc.settings.autoSubmit);
kpxcUserAutocomplete.create(c.password, false, kpxc.settings.autoSubmit);
}
if (c.totp) {
kpxcTOTPAutocomplete.create(c.totp, false, kpxc.settings.autoSubmit);
}
}
};
@ -1185,14 +1207,14 @@ kpxc.initLoginPopup = function() {
// Add usernames + descriptions to autocomplete-list and popup-list
const usernames = [];
kpxcAutocomplete.elements = [];
kpxcUserAutocomplete.clear();
const showGroupNameInAutocomplete = kpxc.settings.showGroupNameInAutocomplete && (getUniqueGroupCount(kpxc.credentials) > 1);
for (let i = 0; i < kpxc.credentials.length; i++) {
const loginText = getLoginText(kpxc.credentials[i], showGroupNameInAutocomplete);
usernames.push({ text: loginText, uuid: kpxc.credentials[i].uuid });
kpxcAutocomplete.elements.push({
kpxcUserAutocomplete.elements.push({
label: loginText,
value: kpxc.credentials[i].login,
uuid: kpxc.credentials[i].uuid,
@ -1462,6 +1484,35 @@ kpxc.updateDatabaseState = async function() {
kpxc.databaseState = res.databaseClosed ? DatabaseState.LOCKED : DatabaseState.UNLOCKED;
};
// Updates the TOTP Autocomplete Menu
kpxc.updateTOTPList = async function() {
let index = await sendMessage('page_get_login_id');
if (index < 0) {
// Credential haven't been selected
return;
}
// Use the first credential available if not set
if (index === undefined) {
index = 0;
}
if (index >= 0 && kpxc.credentials[index]) {
const username = kpxc.credentials[index].login;
const password = kpxc.credentials[index].password;
// If no username is set, compare with a password
const credentialList = kpxc.credentials.filter(c => (c.totp || c.stringFields.some(s => s['KPH: {TOTP}']))
&& (c.login === username || (!username && c.password === password)));
// Filter TOTP Autocomplete Menu with matching 2FA credentials
kpxcTOTPAutocomplete.elements = kpxcUserAutocomplete.elements.filter(e => credentialList.some(u => u.uuid === e.uuid));
return credentialList;
}
return [];
};
/**
* @Object kpxcObserverHelper

View file

@ -0,0 +1,35 @@
'use strict';
class TOTPAutocomplete extends Autocomplete {}
TOTPAutocomplete.prototype.click = async function(e, input) {
if (!e.isTrusted) {
return;
}
await kpxc.updateTOTPList();
this.showList(input, true);
};
TOTPAutocomplete.prototype.itemClick = async function(e, item, input, uuid) {
if (!e.isTrusted) {
return;
}
const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget);
this.fillTotp(index, uuid);
this.closeList();
input.focus();
};
TOTPAutocomplete.prototype.itemEnter = async function(index, elements) {
this.fillTotp(index, elements[index].uuid);
};
TOTPAutocomplete.prototype.fillTotp = async function(index, uuid) {
const combination = await kpxcFields.getCombination(this.input);
combination.loginId = index;
kpxc.fillTOTPFromUuid(this.input, uuid);
};
const kpxcTOTPAutocomplete = new TOTPAutocomplete();

View file

@ -268,11 +268,19 @@ document.addEventListener('mousemove', function(e) {
}
});
document.addEventListener('mousedown', function() {
document.addEventListener('mousedown', function(e) {
if (!e.isTrusted) {
return;
}
kpxcUI.mouseDown = true;
});
document.addEventListener('mouseup', function() {
document.addEventListener('mouseup', function(e) {
if (!e.isTrusted) {
return;
}
kpxcPasswordDialog.selected = null;
kpxcDefine.selected = null;
kpxcUI.mouseDown = false;

View file

@ -56,9 +56,11 @@
"content/ui.js",
"content/banner.js",
"content/autocomplete.js",
"content/credential-autocomplete.js",
"content/define.js",
"content/keepassxc-browser.js",
"content/pwgen.js",
"content/totp-autocomplete.js",
"content/totp-field.js",
"content/username-field.js"
],