Refactoring for 1.8.0

This commit is contained in:
varjolintu 2022-01-01 14:57:36 +02:00
parent a2576e19eb
commit abd6364c7d
17 changed files with 1328 additions and 1298 deletions

View file

@ -76,7 +76,9 @@
"globals": {
"_called": true,
"_f": true,
"acceptedOTPFields": true,
"AssociatedAction": true,
"Autocomplete": true,
"browser": true,
"browserAction": true,
"createStylesheet": true,
@ -96,7 +98,11 @@
"kpxcBanner": true,
"kpxcDefine": true,
"kpxcFields": true,
"kpxcFill": true,
"kpxcForm": true,
"kpxcEvent": true,
"kpxcIcons": true,
"kpxcObserverHelper": true,
"kpxcPasswordDialog": true,
"kpxcPasswordIcons": true,
"kpxcSites": true,
@ -107,9 +113,15 @@
"kpxcUI": true,
"kpxcUsernameField": true,
"ManualFill": true,
"MAX_OPACITY": true,
"MIN_INPUT_FIELD_OFFSET_WIDTH": true,
"MIN_INPUT_FIELD_WIDTH_PX": true,
"MIN_OPACITY": true,
"MIN_TOTP_INPUT_LENGTH": true,
"nacl": true,
"page": true,
"Pixels": true,
"sendMessage": true,
"showNotification": true,
"siteMatch": true,
"slashNeededForUrl": true,

View file

@ -135,7 +135,7 @@ kpxcEvent.onCheckUpdateKeePassXC = async function() {
};
kpxcEvent.onUpdateAvailableKeePassXC = async function() {
return (page.settings.checkUpdateKeePassXC != CHECK_UPDATE_NEVER) ? keepass.keePassXCUpdateAvailable() : false;
return (page.settings.checkUpdateKeePassXC !== CHECK_UPDATE_NEVER) ? keepass.keePassXCUpdateAvailable() : false;
};
kpxcEvent.onRemoveCredentialsFromTabInformation = async function(tab) {

View file

@ -147,7 +147,7 @@ browser.commands.onCommand.addListener(async (command) => {
|| command === 'redetect_fields'
|| command === 'choose_credential_fields'
|| command === 'retrive_credentials_forced'
|| command === 'reload_extension') {
|| command === 'reload_extension') {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
if (tabs.length) {
browser.tabs.sendMessage(tabs[0].id, { action: command });

View file

@ -42,7 +42,7 @@ CredentialAutocomplete.prototype.fillPassword = async function(value, index, uui
await sendMessage('page_set_login_id', uuid);
const manualFill = await sendMessage('page_get_manual_fill');
await kpxc.fillInCredentials(combination, value, uuid, manualFill === ManualFill.PASSWORD);
await kpxcFill.fillInCredentials(combination, value, uuid, manualFill === ManualFill.PASSWORD);
};
const kpxcUserAutocomplete = new CredentialAutocomplete();

View file

@ -0,0 +1,430 @@
'use strict';
/**
* @Object kpxcFields
* Provides methods for input field handling.
*/
const kpxcFields = {};
// Returns all username & password combinations detected from the inputs.
// After username field is detected, first password field found after that will be saved as a combination.
kpxcFields.getAllCombinations = async function(inputs) {
const combinations = [];
let usernameField = null;
for (const input of inputs) {
if (!input) {
continue;
}
if (input.getLowerCaseAttribute('type') === 'password') {
const combination = {
username: (!usernameField || usernameField.size < 1) ? null : usernameField,
password: input,
passwordInputs: [ input ],
form: input.form
};
combinations.push(combination);
usernameField = null;
} else if (kpxcTOTPIcons.isValid(input)) {
// Dynamically added TOTP field
const combination = {
username: null,
password: null,
passwordInputs: [],
totp: input,
form: null
};
combinations.push(combination);
} else {
usernameField = input;
}
}
if (kpxc.singleInputEnabledForPage && combinations.length === 0 && usernameField) {
const combination = {
username: usernameField,
password: null,
passwordInputs: [],
form: usernameField.form
};
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,
username: null,
password: null,
passwordInputs: []
};
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 && form.className.includes(f))
|| (form.id && typeof(form.id) === 'string' && form.id.includes(f))
|| (form.name && typeof(form.name) === 'string' && 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'))
|| (i.type === 'text' && i.maxLength === 1)
|| i.type === 'tel')) {
// No form is found, but input fields are possibly segmented TOTP fields
addTotpFieldsToCombination(inputs);
}
return combinations;
};
// Return all input fields on the page, but ignore previously detected
kpxcFields.getAllPageInputs = async function(previousInputs = []) {
const fields = [];
const inputs = kpxcObserverHelper.getInputs(document.body);
for (const input of inputs) {
// Ignore fields that are already detected
if (previousInputs.some(e => e === input)) {
continue;
}
if (kpxcFields.isVisible(input) && kpxcFields.isAutocompleteAppropriate(input)) {
fields.push(input);
}
}
kpxc.detectedFields = previousInputs.length + fields.length;
// Show add username-only option for the site in popup
if (!kpxc.singleInputEnabledForPage
&& ((fields.length === 1 && fields[0].getLowerCaseAttribute('type') !== 'password')
|| (previousInputs.length === 1 && previousInputs[0].getLowerCaseAttribute('type') !== 'password'))) {
sendMessage('username_field_detected', true);
} else {
sendMessage('username_field_detected', false);
}
await kpxc.initCombinations(inputs);
return fields;
};
/**
* Returns the combination where input field is used
* @param {HTMLElement} field Input field
* @param {String} givenType 'username' or 'password'
*/
kpxcFields.getCombination = async function(field, givenType) {
// If givenType is not set, return the combination that uses the selected field
for (const combination of kpxc.combinations) {
if (!givenType && Object.values(combination).find(c => c === field)) {
return combination;
} else if (givenType && combination[givenType]) {
if (combination[givenType] === field || combination[givenType].includes(field)) {
return combination;
}
}
}
return undefined;
};
// Sets and returns unique ID's for the element
kpxcFields.setId = function(target) {
return [ kpxcFields.getIdFromXPath(target), kpxcFields.getIdFromProperties(target) ];
};
// Returns generated unique ID's for the element. If XPath ID fails, return the fallback one.
kpxcFields.getId = function(idArray, inputField) {
if (!idArray) {
return '';
}
// Legacy ID is used. Convert it to the new one if possible
if (!Array.isArray(idArray) && idArray.length > 0) {
if (idArray === kpxcFields.getLegacyId(inputField)) {
idArray = kpxcFields.setId(inputField);
}
}
const elementFromXPath = kpxcFields.getElementFromXPathId(idArray[0]);
const fallbackId = kpxcFields.getIdFromProperties(inputField);
return elementFromXPath || (fallbackId === idArray[1] ? inputField : '');
};
// Returns element XPath
kpxcFields.getIdFromXPath = function(target) {
let xpath = '';
let pos;
let temp;
while (target !== document.documentElement) {
pos = 0;
temp = target;
while (temp) {
if (temp.nodeType === 1 && temp.nodeName === target.nodeName) {
pos += 1;
}
temp = temp.previousSibling;
}
xpath = `${target.nodeName.toLowerCase()}${(pos > 1 ? `[${pos}]/` : '/')}${xpath}`;
target = target.parentNode;
}
xpath = `/${document.documentElement.nodeName.toLowerCase()}/${xpath}`;
xpath = xpath.replace(/\/$/, '');
return xpath;
};
// Generate uniqe ID from properties (new method)
kpxcFields.getIdFromProperties = function(target) {
if (target.name) {
return `${target.nodeName} ${target.type} ${target.name} ${target.placeholder}`;
}
if (target.classList && target.classList.length > 0) {
return `${target.nodeName} ${target.type} ${target.classList.value} ${target.placeholder}`;
}
if (target.id && target.id !== '') {
return `${target.nodeName} ${target.type} ${kpxcFields.prepareId(target.id)} ${target.placeholder}`;
}
return `kpxc ${target.type} ${target.clientTop}${target.clientLeft}${target.clientWidth}${target.clientHeight}${target.offsetTop}${target.offsetLeft}`;
};
// Legacy unique ID generation for converting
kpxcFields.getLegacyId = function(target) {
if (target.classList.length > 0) {
return `${target.nodeName} ${target.type} ${target.classList.value} ${target.name} ${target.placeholder}`;
}
if (target.id && target.id !== '') {
return `${target.nodeName} ${target.type} ${kpxcFields.prepareId(target.id)} ${target.name} ${target.placeholder}`;
}
return `kpxc ${target.type} ${target.clientTop}${target.clientLeft}${target.clientWidth}${target.clientHeight}${target.offsetTop}${target.offsetLeft}`;
};
kpxcFields.getElementFromXPathId = function(xpath) {
return (new XPathEvaluator()).evaluate(xpath, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
};
// Check for new password via autocomplete attribute
kpxcFields.isAutocompleteAppropriate = function(field) {
const autocomplete = field.getLowerCaseAttribute('autocomplete');
return autocomplete !== 'new-password';
};
// Checks if Custom Login Fields are used for the site
kpxcFields.isCustomLoginFieldsUsed = function() {
const location = kpxc.getDocumentLocation();
return kpxc.settings['defined-custom-fields'] !== undefined && kpxc.settings['defined-custom-fields'][location] !== undefined;
};
// Returns true if form is a search form
kpxcFields.isSearchForm = function(form) {
// Check form action
const formAction = form.getLowerCaseAttribute('action');
if (formAction && (formAction.includes('search') && !formAction.includes('research'))) {
return true;
}
// Ignore form with search classes
const formId = form.getLowerCaseAttribute('id');
if (form.className && (form.className.includes('search')
|| (formId && formId.includes('search') && !formId.includes('research')))) {
return true;
}
return false;
};
// Checks if input field is a search field. Attributes or form action containing 'search', or parent element holding
// role="search" will be identified as a search field.
kpxcFields.isSearchField = function(target) {
// Check element attributes
for (const attr of target.attributes) {
if ((attr.value && (attr.value.toLowerCase().includes('search')) || attr.value === 'q')) {
return true;
}
}
// Check closest form
const closestForm = kpxc.getForm(target);
if (closestForm && kpxcFields.isSearchForm(closestForm)) {
return true;
}
// Check parent elements for role='search'
if (target.closest('[role~=\'search\']')) {
return true;
}
return false;
};
// Returns true if element is visible on the page
kpxcFields.isVisible = function(elem) {
// Check element position and size
const rect = elem.getBoundingClientRect();
if (rect.x < 0
|| rect.y < 0
|| rect.width < MIN_INPUT_FIELD_WIDTH_PX
|| rect.x > Math.max(document.body.scrollWidth, document.body.offsetWidth, document.documentElement.clientWidth)
|| rect.y > Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight)
|| rect.height < MIN_INPUT_FIELD_WIDTH_PX) {
return false;
}
// Check CSS visibility
const elemStyle = getComputedStyle(elem);
const opacity = Number(elemStyle.opacity);
if (elemStyle.visibility && (elemStyle.visibility === 'hidden' || elemStyle.visibility === 'collapse')
|| (opacity < MIN_OPACITY || opacity > MAX_OPACITY)
|| parseInt(elemStyle.width, 10) <= MIN_INPUT_FIELD_WIDTH_PX
|| parseInt(elemStyle.height, 10) <= MIN_INPUT_FIELD_WIDTH_PX) {
return false;
}
// Check for parent opacity
if (kpxcFields.traverseParents(elem, f => f.style.opacity === '0')) {
return false;
}
return true;
};
kpxcFields.prepareId = function(id) {
return (id + '').replace(kpxcFields.rcssescape, kpxcFields.fcssescape);
};
/**
* Returns the first parent element satifying the {@code predicate} mapped by {@code resultFn} or else {@code defaultVal}.
* @param {HTMLElement} element The start element (excluded, starting with the parents)
* @param {function} predicate Matcher for the element to find, type (HTMLElement) => boolean
* @param {function} resultFn Callback function of type (HTMLElement) => {*} called for the first matching element
* @param {fun} defaultValFn Fallback return value supplier, if no element matching the predicate can be found
*/
kpxcFields.traverseParents = function(element, predicate, resultFn = () => true, defaultValFn = () => false) {
for (let f = element.parentElement; f !== null; f = f.parentElement) {
if (predicate(f)) {
return resultFn(f);
}
}
return defaultValFn();
};
// Use Custom Fields instead of detected combinations
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) {
return;
}
// Finds the input field based on the stored ID
const findInputField = async function(inputFields, idArray) {
if (idArray) {
const input = inputFields.find(e => e === kpxcFields.getId(idArray, e));
if (input) {
return input;
}
}
return null;
};
// Get all input fields from the page without any extra filters
const inputFields = [];
document.body.querySelectorAll('input, select, textarea').forEach(e => {
if (e.type !== 'hidden' && !e.disabled) {
inputFields.push(e);
}
});
const [ username, password, totp ] = await Promise.all([
await findInputField(inputFields, creds.username),
await findInputField(inputFields, creds.password),
await findInputField(inputFields, creds.totp)
]);
// Handle StringFields
const stringFields = [];
for (const sf of creds.fields) {
const field = await findInputField(inputFields, sf);
if (field) {
stringFields.push(field);
}
}
// Handle custom TOTP field
if (totp) {
totp.setAttribute('kpxc-defined', 'totp');
kpxcTOTPIcons.newIcon(totp, kpxc.databaseState);
}
// If not all expected fields are identified, return an empty combination
if ((creds.username && !username) || (creds.password && !password) || (creds.totp && !totp)
|| (creds.fields.length !== stringFields.length)) {
return [];
}
const combinations = [];
combinations.push({
username: username,
password: password,
passwordInputs: [ password ],
totp: totp,
fields: stringFields
});
return combinations;
};
// Copied from Sizzle.js
kpxcFields.rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g;
kpxcFields.fcssescape = function(ch, asCodePoint) {
if (asCodePoint) {
// U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
if (ch === '\0') {
return '\uFFFD';
}
// Control characters and (dependent upon position) numbers get escaped as code points
return ch.slice(0, -1) + '\\' + ch.charCodeAt(ch.length - 1).toString(16) + ' ';
}
// Other potentially-special ASCII characters get backslash-escaped
return '\\' + ch;
};

View file

@ -0,0 +1,289 @@
'use strict';
/**
* @Object kpxcFill
* The class for filling credentials.
*/
const kpxcFill = {};
// Fill selected attribute from the context menu
kpxcFill.fillAttributeToActiveElementWith = async function(attr) {
const el = document.activeElement;
if (el.nodeName !== 'INPUT' || kpxc.credentials.length === 0) {
return;
}
const value = Object.values(attr);
if (!value || value.length === 0) {
return;
}
kpxc.setValue(el, value[0]);
};
// Fill requested from the context menu. Active element is used for combination detection
kpxcFill.fillInFromActiveElement = async function(passOnly = false) {
if (kpxc.credentials.length === 0) {
return;
}
if (kpxc.combinations.length > 0 && kpxc.settings.autoCompleteUsernames) {
const combination = passOnly
? kpxc.combinations.find(c => c.password)
: kpxc.combinations.find(c => c.username);
if (!combination) {
return;
}
const field = passOnly ? combination.password : combination.username;
if (!field) {
return;
}
// Set focus to the input field
field.focus();
if (kpxc.credentials.length > 1) {
// More than one credential -> show autocomplete list
kpxcUserAutocomplete.showList(field);
return;
} else {
// Just one credential -> fill the first combination found
await sendMessage('page_set_login_id', kpxc.credentials[0].uuid);
kpxcFill.fillInCredentials(combination, kpxc.credentials[0].login, kpxc.credentials[0].uuid, passOnly);
return;
}
}
// No previous combinations detected. Create a new one from active element
const el = document.activeElement;
let combination;
if (kpxc.combinations.length === 0) {
combination = await kpxc.createCombination(el);
} else {
combination = el.type === 'password'
? await kpxcFields.getCombination(el, 'password')
: await kpxcFields.getCombination(el, 'username');
}
// Do not allow filling password to a non-password field
if (passOnly && combination && !combination.password) {
kpxcUI.createNotification('warning', tr('fieldsNoPasswordField'));
return;
}
await sendMessage('page_set_login_id', kpxc.credentials[0].uuid);
kpxcFill.fillInCredentials(combination, kpxc.credentials[0].login, kpxc.credentials[0].uuid, passOnly);
};
// Fill requested by Auto-Fill
kpxcFill.fillFromAutofill = async function() {
if (kpxc.credentials.length !== 1 || kpxc.combinations.length === 0) {
return;
}
const index = kpxc.combinations.length - 1;
await sendMessage('page_set_login_id', kpxc.credentials[0].uuid);
kpxcFill.fillInCredentials(kpxc.combinations[index], kpxc.credentials[0].login, kpxc.credentials[0].uuid);
// Generate popup-list of usernames + descriptions
sendMessage('popup_login', [ { text: `${kpxc.credentials[0].login} (${kpxc.credentials[0].name})`, uuid: kpxc.credentials[0].uuid } ]);
};
// Fill requested by selecting credentials from the popup
kpxcFill.fillFromPopup = async function(id, uuid) {
if (!kpxc.credentials.length === 0 || !kpxc.credentials[id] || kpxc.combinations.length === 0) {
return;
}
await sendMessage('page_set_login_id', uuid);
const selectedCredentials = kpxc.credentials.find(c => c.uuid === uuid);
if (!selectedCredentials) {
console.log('Error: Uuid not found: ', uuid);
return;
}
kpxcFill.fillInCredentials(kpxc.combinations[0], selectedCredentials.login, uuid);
kpxcUserAutocomplete.closeList();
};
// Fill requested from TOTP icon
kpxcFill.fillFromTOTP = async function(target) {
const el = target || document.activeElement;
const credentialList = await kpxc.updateTOTPList();
if (credentialList && credentialList.length === 0) {
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
return;
}
if (credentialList && credentialList.length === 1) {
kpxcFill.fillTOTPFromUuid(el, credentialList[0].uuid);
return;
}
kpxcTOTPAutocomplete.showList(el, true);
};
// Fill TOTP with matching uuid
kpxcFill.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;
}
kpxcFill.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) {
kpxcFill.setTOTPValue(el, val);
}
}
}
};
// Set normal or segmented TOTP value
kpxcFill.setTOTPValue = function(elem, val) {
if (kpxc.combinations.length === 0) {
return;
}
for (const comb of kpxc.combinations) {
if (comb.totpInputs && comb.totpInputs.length === 6) {
kpxcFill.fillSegmentedTotp(elem, val, comb.totpInputs);
return;
}
}
kpxc.setValue(elem, val);
};
// Fill TOTP in parts
kpxcFill.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
kpxcFill.fillFromUsernameIcon = async function(combination) {
await kpxc.receiveCredentialsIfNecessary();
if (kpxc.credentials.length === 0) {
return;
} else if (kpxc.credentials.length > 1 && kpxc.settings.autoCompleteUsernames) {
kpxcUserAutocomplete.showList(combination.username || combination.password);
return;
}
await sendMessage('page_set_login_id', kpxc.credentials[0].uuid);
kpxcFill.fillInCredentials(combination, kpxc.credentials[0].login, kpxc.credentials[0].uuid);
};
/**
* The main function for filling any credentials
* @param {Array} combination Combination to be used
* @param {String} predefinedUsername Predefined username. If set, there's no need to find it from combinations
* @param {Boolean} passOnly If only password is filled
* @param {String} uuid Identifier for the entry. There can be identical usernames with different password
*/
kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uuid, passOnly = false) {
if (kpxc.credentials.length === 0) {
kpxcUI.createNotification('error', tr('credentialsNoLoginsFound'));
return;
}
if (!combination || (!combination.username && !combination.password)) {
return;
}
// Use predefined username as default
let usernameValue = predefinedUsername;
if (!usernameValue) {
// With single password field the combination.password is used instead
usernameValue = combination.username ? combination.username.value : combination.password.value;
}
// Find the correct credentials
const selectedCredentials = kpxc.credentials.find(c => c.uuid === uuid);
if (!selectedCredentials) {
console.log('Error: Uuid not found: ', uuid);
return;
}
// Handle auto-submit
let skipAutoSubmit = false;
if (selectedCredentials.skipAutoSubmit !== undefined) {
skipAutoSubmit = selectedCredentials.skipAutoSubmit === 'true';
}
// Fill password
if (combination.password) {
kpxc.setValueWithChange(combination.password, selectedCredentials.password);
await kpxc.setPasswordFilled(true);
}
// Fill username
if (combination.username && (!combination.username.value || combination.username.value !== usernameValue)) {
if (!passOnly) {
kpxc.setValueWithChange(combination.username, usernameValue);
}
}
// Fill StringFields
if (selectedCredentials.stringFields && selectedCredentials.stringFields.length > 0) {
kpxcFill.fillInStringFields(combination.fields, selectedCredentials.stringFields);
}
// Close autocomplete menu after fill
kpxcUserAutocomplete.closeList();
// Reset ManualFill
await sendMessage('page_set_manual_fill', ManualFill.NONE);
// Auto-submit
const autoSubmitIgnoredForSite = await kpxc.siteIgnored(IGNORE_AUTOSUBMIT);
if (kpxc.settings.autoSubmit && !skipAutoSubmit && !autoSubmitIgnoredForSite) {
const submitButton = kpxcForm.getFormSubmitButton(combination.form);
if (submitButton !== undefined) {
submitButton.click();
} else if (combination.form) {
combination.form.submit();
}
} else {
(combination.username || combination.password).focus();
}
};
// Fills StringFields defined in Custom Fields
kpxcFill.fillInStringFields = function(fields, stringFields) {
const filledInFields = [];
if (fields && stringFields && fields.length > 0 && stringFields.length > 0) {
for (let i = 0; i < fields.length; i++) {
const currentField = fields[i];
const stringFieldValue = Object.values(stringFields[i]);
if (currentField && stringFieldValue[0]) {
kpxc.setValue(currentField, stringFieldValue[0]);
filledInFields.push(currentField);
}
}
}
};

View file

@ -0,0 +1,183 @@
'use strict';
/**
* @Object kpxcForm
* Identifies form submits and password changes.
*/
const kpxcForm = {};
kpxcForm.formButtonQuery = 'button[type=button], button[type=submit], input[type=button], input[type=submit], button:not([type]), div[role=button]';
kpxcForm.savedForms = [];
// Returns true if form has been already saved
kpxcForm.formIdentified = function(form) {
return kpxcForm.savedForms.some(f => f.form === form);
};
// Return input fields from our Object array
kpxcForm.getCredentialFieldsFromForm = function(form) {
for (const savedForm of kpxcForm.savedForms) {
if (savedForm.form === form) {
return [ savedForm.username, savedForm.password, savedForm.passwordInputs, savedForm.totp ];
}
}
return [];
};
// Get the form submit button instead if action URL is same as the page itself
kpxcForm.getFormSubmitButton = function(form) {
if (!form || !form.action || typeof form.action !== 'string') {
return;
}
const action = kpxc.submitUrl || form.action;
// Check if the site needs a special handling for retrieving the form submit button
const exceptionButton = kpxcSites.formSubmitButtonExceptionFound(form);
if (exceptionButton) {
return exceptionButton;
}
if (action.includes(document.location.origin + document.location.pathname)) {
for (const i of form.elements) {
if (i.type === 'submit') {
return i;
}
}
}
// Try to find another button. Select the last one.
// If any formaction overriding the default action is set, ignore those buttons.
const buttons = Array.from(form.querySelectorAll(kpxcForm.formButtonQuery)).filter(b => !b.getAttribute('formAction'));
if (buttons.length > 0) {
return buttons[buttons.length - 1];
}
// Try to find similar buttons outside the form which are added via 'form' property
for (const e of form.elements) {
if ((e.nodeName === 'BUTTON' && (e.type === 'button' || e.type === 'submit' || e.type === ''))
|| (e.nodeName === 'INPUT' && (e.type === 'button' || e.type === 'submit'))) {
return e;
}
}
return undefined;
};
// Retrieve new password from a form with three elements: Current, New, Repeat New
kpxcForm.getNewPassword = function(passwordInputs = []) {
if (passwordInputs.length < 2) {
return '';
}
// Just two password fields, current and new
if (passwordInputs.length === 2 && passwordInputs[0] !== passwordInputs[1]) {
return passwordInputs[1].value;
}
// Choose the last three password fields. The first ones are almost always for something else
const current = passwordInputs[passwordInputs.length - 3].value;
const newPass = passwordInputs[passwordInputs.length - 2].value;
const repeatNew = passwordInputs[passwordInputs.length - 1].value;
if ((newPass === repeatNew && current !== newPass && current !== repeatNew)
|| (current === newPass && repeatNew !== newPass && repeatNew !== current)) {
return newPass;
}
return '';
};
// Initializes form and attaches the submit button to our own callback
kpxcForm.init = function(form, credentialFields) {
if (!form.action || typeof form.action !== 'string') {
return;
}
if (!kpxcForm.formIdentified(form) && (credentialFields.password || credentialFields.username)
|| form.action.startsWith(kpxcSites.googlePasswordFormUrl)) {
kpxcForm.saveForm(form, credentialFields);
form.addEventListener('submit', kpxcForm.onSubmit);
const submitButton = kpxcForm.getFormSubmitButton(form);
if (submitButton !== undefined) {
submitButton.addEventListener('click', kpxcForm.onSubmit);
}
}
};
// Triggers when form is submitted. Shows the credential banner
kpxcForm.onSubmit = async function(e) {
if (!e.isTrusted) {
return;
}
const searchForm = f => {
if (f.nodeName === 'FORM') {
return f;
}
};
// Traverse parents if the form is not found.
let form = this.nodeName === 'FORM' ? this : kpxcFields.traverseParents(this, searchForm, searchForm, () => null);
// Check for extra forms from sites.js
if (!form) {
form = kpxcSites.savedForm;
}
// Still not found? Try using the first one from kpxcForm.savedForms
if (!form && kpxcForm.savedForms.length > 0) {
form = kpxcForm.savedForms[0].form;
}
if (!form) {
return;
}
const [ usernameField, passwordField, passwordInputs ] = kpxcForm.getCredentialFieldsFromForm(form);
let usernameValue = '';
let passwordValue = '';
if (usernameField) {
usernameValue = usernameField.value || usernameField.placeholder;
} else if (kpxc.credentials.length === 1) {
// Single entry found for the page, use the username of it instead of an empty one
usernameValue = kpxc.credentials[0].login;
}
// Check if the form has three password fields -> a possible password change form
if (passwordInputs && passwordInputs.length >= 2) {
passwordValue = kpxcForm.getNewPassword(passwordInputs);
} else if (passwordField) {
// Use the combination password field instead
passwordValue = passwordField.value;
}
// Return if credentials are already found
if (kpxc.credentials.some(c => c.login === usernameValue && c.password === passwordValue)) {
return;
}
if (passwordField) {
await kpxc.setPasswordFilled(true);
}
const url = trimURL(kpxc.settings.saveDomainOnlyNewCreds ? window.top.location.origin : window.top.location.href);
await sendMessage('page_set_submitted', [ true, usernameValue, passwordValue, url, kpxc.credentials ]);
// Show the banner if the page does not reload
kpxc.rememberCredentials(usernameValue, passwordValue);
};
// Save form to Object array
kpxcForm.saveForm = function(form, combination) {
kpxcForm.savedForms.push({
form: form,
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')
});
};

View file

@ -0,0 +1,115 @@
'use strict';
/**
* @Object kpxcIcons
* Icon handling.
*/
const kpxcIcons = {};
kpxcIcons.icons = [];
kpxcIcons.iconTypes = { USERNAME: 0, PASSWORD: 1, TOTP: 2 };
// Adds an icon to input field
kpxcIcons.addIcon = async function(field, iconType) {
if (!field || iconType < 0 || iconType > 2) {
return;
}
let iconSet = false;
if (iconType === kpxcIcons.iconTypes.USERNAME && kpxcUsernameIcons.isValid(field)) {
kpxcUsernameIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
} else if (iconType === kpxcIcons.iconTypes.PASSWORD && kpxcPasswordIcons.isValid(field)) {
kpxcPasswordIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
} else if (iconType === kpxcIcons.iconTypes.TOTP && kpxcTOTPIcons.isValid(field)) {
kpxcTOTPIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
}
if (iconSet) {
kpxcIcons.icons.push({
field: field,
iconType: iconType
});
}
};
// Adds all necessary icons to a saved form
kpxcIcons.addIconsFromForm = async function(form) {
const addUsernameIcons = async function(c) {
if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilledWithExceptions(c) === false) {
// Special case where everything else has been hidden, but a single password field is now displayed.
// For example PayPal and Amazon is handled like this.
if (c.username && !c.password && c.passwordInputs.length === 1) {
kpxcIcons.addIcon(c.passwordInputs[0], kpxcIcons.iconTypes.USERNAME);
}
if (c.username && !c.username.readOnly) {
kpxcIcons.addIcon(c.username, kpxcIcons.iconTypes.USERNAME);
} else if (c.password && (!c.username || (c.username && c.username.readOnly))) {
// Single password field
kpxcIcons.addIcon(c.password, kpxcIcons.iconTypes.USERNAME);
}
}
};
const addPasswordIcons = async function(c) {
// Show password icons also with forms without any username field
if (kpxc.settings.usePasswordGeneratorIcons
&& ((c.username && c.password) || (!c.username && c.passwordInputs.length > 0))) {
for (const input of c.passwordInputs) {
kpxcIcons.addIcon(input, kpxcIcons.iconTypes.PASSWORD);
}
}
};
const addTOTPIcons = async function(c) {
if (c.totp && kpxc.settings.showOTPIcon) {
kpxcIcons.addIcon(c.totp, kpxcIcons.iconTypes.TOTP);
}
};
await Promise.all([
await addUsernameIcons(form),
await addPasswordIcons(form),
await addTOTPIcons(form)
]);
};
// Delete all icons that have been hidden from the page view
kpxcIcons.deleteHiddenIcons = function() {
kpxcUsernameIcons.deleteHiddenIcons();
kpxcPasswordIcons.deleteHiddenIcons();
kpxcTOTPIcons.deleteHiddenIcons();
};
// Initializes all icons needed to be shown
kpxcIcons.initIcons = async function(combinations = []) {
if (combinations.length === 0) {
return;
}
for (const form of kpxcForm.savedForms) {
await kpxcIcons.addIconsFromForm(form);
}
// Check for other combinations that are not in any form
for (const c of combinations) {
if (c.form) {
continue;
}
await kpxcIcons.addIconsFromForm(c);
}
};
kpxcIcons.hasIcon = function(field) {
return !field ? false : kpxcIcons.icons.some(i => i.field === field);
};
// Sets the icons to corresponding database lock status
kpxcIcons.switchIcons = function() {
kpxcUsernameIcons.switchIcon(kpxc.databaseState);
kpxcPasswordIcons.switchIcon(kpxc.databaseState);
kpxcTOTPIcons.switchIcon(kpxc.databaseState);
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,271 @@
'use strict';
const MAX_INPUTS = 100;
const MAX_MUTATIONS = 200;
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
/**
* @Object kpxcObserverHelper
* MutationObserver handler for dynamically added input fields.
*/
const kpxcObserverHelper = {};
kpxcObserverHelper.ignoredNodeNames = [ 'g', 'path', 'svg', 'A', 'HEAD', 'HTML', 'LABEL', 'LINK', 'SCRIPT', 'SPAN', 'VIDEO' ];
kpxcObserverHelper.ignoredNodeTypes = [
Node.ATTRIBUTE_NODE,
Node.TEXT_NODE,
Node.CDATA_SECTION_NODE,
Node.PROCESSING_INSTRUCTION_NODE,
Node.COMMENT_NODE,
Node.DOCUMENT_TYPE_NODE,
Node.NOTATION_NODE
];
kpxcObserverHelper.inputTypes = [
'text',
'email',
'password',
'tel',
'number',
'username', // Note: Not a standard
undefined, // Input field can be without any type. Include this and null to the list.
null
];
// Define what element should be observed by the observer
// and what types of mutations trigger the callback
kpxcObserverHelper.observerConfig = {
subtree: true,
attributes: true,
childList: true,
characterData: true,
attributeFilter: [ 'style', 'class' ]
};
// Initializes MutationObserver
kpxcObserverHelper.initObserver = async function() {
kpxc.observer = new MutationObserver(function(mutations, obs) {
if (document.visibilityState === 'hidden' || kpxcUI.mouseDown) {
return;
}
// Limit the maximum number of mutations
if (mutations.length > MAX_MUTATIONS) {
mutations = mutations.slice(0, MAX_MUTATIONS);
}
const styleMutations = [];
for (const mut of mutations) {
if (kpxcObserverHelper.ignoredNode(mut.target)) {
continue;
}
// Cache style mutations. We only need the last style mutation of the target.
kpxcObserverHelper.cacheStyle(mut, styleMutations, mutations.length);
if (mut.type === 'childList') {
if (mut.addedNodes.length > 0) {
kpxcObserverHelper.handleObserverAdd(mut.addedNodes[0]);
} else if (mut.removedNodes.length > 0) {
kpxcObserverHelper.handleObserverRemove(mut.removedNodes[0]);
}
} else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) {
// Only accept targets with forms
const forms = mut.target.nodeName === 'FORM' ? mut.target : mut.target.getElementsByTagName('form');
if (forms.length === 0 && !kpxcSites.exceptionFound(mut.target.classList)) {
continue;
}
// There's an issue here. We cannot know for sure if the class attribute if added or removed.
kpxcObserverHelper.handleObserverAdd(mut.target);
}
}
// Handle cached style mutations
for (const styleMut of styleMutations) {
if (styleMut.display !== 'none' && styleMut.display !== '') {
kpxcObserverHelper.handleObserverAdd(styleMut.target);
} else {
kpxcObserverHelper.handleObserverRemove(styleMut.target);
}
}
});
if (document.body) {
kpxc.observer.observe(document.body, kpxcObserverHelper.observerConfig);
}
};
// Stores mutation style to an cache array
// If there's a single style mutation, it's safe to calculate it
kpxcObserverHelper.cacheStyle = function(mut, styleMutations, mutationCount) {
if (mut.attributeName !== 'style') {
return;
}
// If the target is inside a form we are monitoring, calculate the CSS style for better compatibility.
// getComputedStyle() is very slow, so we cannot do that for every style target.
let style = mut.target.style;
if (kpxcForm.formIdentified(mut.target.parentNode) || mutationCount === 1) {
style = getComputedStyle(mut.target);
}
if (style.display || style.zIndex) {
if (!styleMutations.some(m => m.target === mut.target)) {
styleMutations.push({
target: mut.target,
display: style.display,
zIndex: style.zIndex
});
} else {
const currentStyle = styleMutations.find(m => m.target === mut.target);
if (currentStyle
&& (currentStyle.display !== style.display
|| currentStyle.zIndex !== style.zIndex)) {
currentStyle.display = style.display;
currentStyle.zIndex = style.zIndex;
}
}
}
};
// Gets input fields from the target
kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) {
// Ignores target element if it's not an element node
if (kpxcObserverHelper.ignoredNode(target)) {
return [];
}
// Filter out any input fields with type 'hidden' right away
const inputFields = [];
Array.from(target.getElementsByTagName('input')).forEach(e => {
if (e.type !== 'hidden' && !e.disabled && !kpxcObserverHelper.alreadyIdentified(e)) {
inputFields.push(e);
}
});
if (target.nodeName === 'INPUT') {
inputFields.push(target);
}
// Append any input fields in Shadow DOM
if (target.shadowRoot && typeof target.shadowSelectorAll === 'function') {
target.shadowSelectorAll('input').forEach(e => {
if (e.type !== 'hidden' && !e.disabled && !kpxcObserverHelper.alreadyIdentified(e)) {
inputFields.push(e);
}
});
}
if (inputFields.length === 0) {
return [];
}
// Do not allow more visible inputs than _maximumInputs (default value: 100) -> return the first 100
if (inputFields.length > MAX_INPUTS) {
return inputFields.slice(0, MAX_INPUTS);
}
// Only include input fields that match with kpxcObserverHelper.inputTypes
const inputs = [];
for (const field of inputFields) {
if ((!ignoreVisibility && !kpxcFields.isVisible(field))
|| kpxcFields.isSearchField(field)) {
continue;
}
const type = field.getLowerCaseAttribute('type');
if (kpxcObserverHelper.inputTypes.includes(type)) {
inputs.push(field);
}
}
return inputs;
};
// Checks if the input field has already identified at page load
kpxcObserverHelper.alreadyIdentified = function(target) {
return kpxc.inputs.some(e => e === target);
};
// Adds elements to a monitor array. Identifies the input fields.
kpxcObserverHelper.handleObserverAdd = async function(target) {
if (kpxcObserverHelper.ignoredElement(target)) {
return;
}
// Sometimes the settings haven't been loaded before new input fields are detected
if (Object.keys(kpxc.settings).length === 0) {
kpxc.init();
return;
}
const inputs = kpxcObserverHelper.getInputs(target);
if (inputs.length === 0) {
return;
}
await kpxc.initCombinations(inputs);
await kpxcIcons.initIcons(kpxc.combinations);
if (kpxc.databaseState === DatabaseState.UNLOCKED) {
if (_called.retrieveCredentials === false) {
await kpxc.retrieveCredentials();
return;
}
kpxc.prepareCredentials();
}
};
// Removes monitored elements
kpxcObserverHelper.handleObserverRemove = function(target) {
if (kpxcObserverHelper.ignoredElement(target)) {
return;
}
const inputs = kpxcObserverHelper.getInputs(target, true);
if (inputs.length === 0) {
return;
}
kpxcIcons.deleteHiddenIcons();
};
// Handles CSS transitionend event
kpxcObserverHelper.handleTransitionEnd = function(e) {
if (!e.isTrusted) {
return;
}
kpxcObserverHelper.handleObserverAdd(e.currentTarget);
};
// Returns true if element should be ignored
kpxcObserverHelper.ignoredElement = function(target) {
if (kpxcObserverHelper.ignoredNode(target)) {
return true;
}
// Ignore elements that do not have a className (including SVG)
if (typeof target.className !== 'string') {
return true;
}
return false;
};
// Ignores all nodes that doesn't contain elements
// Also ignore few Youtube-specific custom nodeNames
kpxcObserverHelper.ignoredNode = function(target) {
if (!target
|| kpxcObserverHelper.ignoredNodeTypes.some(e => e === target.nodeType)
|| kpxcObserverHelper.ignoredNodeNames.some(e => e === target.nodeName)
|| target.nodeName.startsWith('YTMUSIC')
|| target.nodeName.startsWith('YT-')) {
return true;
}
return false;
};

View file

@ -100,7 +100,11 @@ PasswordIcon.prototype.createIcon = function(field) {
document.body.append(wrapper);
};
/**
* @Object kpxcPasswordDialog
* Provides a password dialog for content scripts.
* TODO: To be removed when KeePassXC 2.8.0 is released. 2.7.0 already uses KeePassXC's own password generator instead.
*/
const kpxcPasswordDialog = {};
kpxcPasswordDialog.created = false;
kpxcPasswordDialog.icon = null;

View file

@ -30,7 +30,7 @@ TOTPAutocomplete.prototype.fillTotp = async function(index, uuid) {
const combination = await kpxcFields.getCombination(this.input, 'totp')
|| await kpxcFields.getCombination(this.input, 'totpInputs');
combination.loginId = index;
kpxc.fillTOTPFromUuid(this.input, uuid);
kpxcFill.fillTOTPFromUuid(this.input, uuid);
};
const kpxcTOTPAutocomplete = new TOTPAutocomplete();

View file

@ -134,7 +134,7 @@ TOTPFieldIcon.prototype.createIcon = function(field, segmented = false) {
e.stopPropagation();
await kpxc.receiveCredentialsIfNecessary();
kpxc.fillFromTOTP(field);
kpxcFill.fillFromTOTP(field);
});
icon.addEventListener('mousedown', ev => ev.stopPropagation());

View file

@ -157,5 +157,5 @@ const getIconText = function(state) {
const fillCredentials = async function(field) {
const combination = await kpxcFields.getCombination(field);
kpxc.fillFromUsernameIcon(combination);
kpxcFill.fillFromUsernameIcon(combination);
};

View file

@ -1,8 +1,8 @@
{
"manifest_version": 2,
"name": "KeePassXC-Browser",
"version": "1.7.11",
"version_name": "1.7.11",
"version": "1.8.0",
"version_name": "1.8.0",
"description": "__MSG_extensionDescription__",
"author": "KeePassXC Team",
"icons": {
@ -58,7 +58,12 @@
"content/autocomplete.js",
"content/credential-autocomplete.js",
"content/define.js",
"content/fields.js",
"content/fill.js",
"content/form.js",
"content/icons.js",
"content/keepassxc-browser.js",
"content/observer-helper.js",
"content/pwgen.js",
"content/totp-autocomplete.js",
"content/totp-field.js",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "KeePassXC-Browser",
"version": "1.7.10.1",
"version": "1.8.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "KeePassXC-Browser",
"version": "1.7.10.1",
"version": "1.8.0",
"license": "GPL-3.0",
"dependencies": {
"file-url": "^3.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "KeePassXC-Browser",
"version": "1.7.11",
"version": "1.8.0",
"description": "KeePassXC-Browser",
"main": "build.js",
"devDependencies": {