mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Refactoring for 1.8.0
This commit is contained in:
parent
a2576e19eb
commit
abd6364c7d
17 changed files with 1328 additions and 1298 deletions
12
.eslintrc
12
.eslintrc
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
430
keepassxc-browser/content/fields.js
Normal file
430
keepassxc-browser/content/fields.js
Normal 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;
|
||||
};
|
||||
289
keepassxc-browser/content/fill.js
Normal file
289
keepassxc-browser/content/fill.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
183
keepassxc-browser/content/form.js
Normal file
183
keepassxc-browser/content/form.js
Normal 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')
|
||||
});
|
||||
};
|
||||
115
keepassxc-browser/content/icons.js
Normal file
115
keepassxc-browser/content/icons.js
Normal 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
271
keepassxc-browser/content/observer-helper.js
Normal file
271
keepassxc-browser/content/observer-helper.js
Normal 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;
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "KeePassXC-Browser",
|
||||
"version": "1.7.11",
|
||||
"version": "1.8.0",
|
||||
"description": "KeePassXC-Browser",
|
||||
"main": "build.js",
|
||||
"devDependencies": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue