mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
682 lines
24 KiB
JavaScript
682 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
const DEFAULT_SEGMENTED_TOTP_FIELDS = 6;
|
|
const MIN_SEGMENTED_TOTP_FIELDS = 4;
|
|
const MAX_SEGMENTED_FIELD_LENGTH = 100;
|
|
|
|
/**
|
|
* @Object kpxcFields
|
|
* Provides methods for input field handling.
|
|
*/
|
|
const kpxcFields = {};
|
|
kpxcFields.popoverSupported = true;
|
|
|
|
// 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(kpxcFields.getExistingCombination(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;
|
|
}
|
|
}
|
|
|
|
// Allow single input if autocomplete="username" is present, or Username-Only Detection is enabled for the page
|
|
const autoComplete = usernameField?.getLowerCaseAttribute('autocomplete');
|
|
if (combinations.length === 0 && usernameField &&
|
|
(kpxc.singleInputEnabledForPage || autoComplete?.includes('username') || autoComplete?.includes('email'))
|
|
) {
|
|
const combination = {
|
|
username: usernameField,
|
|
password: null,
|
|
passwordInputs: [],
|
|
form: usernameField.form
|
|
};
|
|
|
|
combinations.push(combination);
|
|
}
|
|
|
|
// Check for segmented TOTP fields
|
|
kpxcFields.handleSegmentedTOTPFields(inputs, combinations);
|
|
|
|
return combinations;
|
|
};
|
|
|
|
// If there are multiple combinations, return the first one where input field can be found inside the document.
|
|
// Used with Custom Login Fields where selected input fields might not be visible on the page yet,
|
|
// and there's an extra combination for those. Only used from popup fill.
|
|
kpxcFields.getCombinationFromAllInputs = function() {
|
|
const inputs = kpxcObserverHelper.getInputs(document.body);
|
|
|
|
for (const combination of kpxc.combinations) {
|
|
for (const value of Object.values(combination)) {
|
|
if (Array.isArray(value)) {
|
|
for (const v of value) {
|
|
if (inputs.some(i => i === v)) {
|
|
return combination;
|
|
}
|
|
}
|
|
} else {
|
|
if (inputs.some(i => i === value)) {
|
|
return combination;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return kpxc.combinations[0];
|
|
};
|
|
|
|
// Checks if existing combination is found and recognized fields are added to it
|
|
kpxcFields.getExistingCombination = function(combination) {
|
|
// Lookup existing combinations that use the same form
|
|
const existingCombination = kpxc.combinations?.find(c => c.form === combination?.form);
|
|
if (existingCombination) {
|
|
// Replace values to the existing combination
|
|
existingCombination.username ??= combination.username;
|
|
existingCombination.password ??= combination.password;
|
|
if (existingCombination.passwordInputs?.length === 0) {
|
|
existingCombination.passwordInputs = combination.passwordInputs;
|
|
} else {
|
|
existingCombination.passwordInputs.push(combination.password);
|
|
}
|
|
|
|
// Remove username field from combination with certain sites (replaced by password input)
|
|
if (kpxcSites.combinationExceptionFound(existingCombination)) {
|
|
existingCombination.username = null;
|
|
}
|
|
|
|
return existingCombination;
|
|
}
|
|
|
|
return combination;
|
|
};
|
|
|
|
// Adds segmented TOTP fields to the combination if found
|
|
kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) {
|
|
if (!kpxc.settings.showOTPIcon) {
|
|
return;
|
|
}
|
|
|
|
let exceptionFound = false;
|
|
|
|
// Returns true if all 6 inputs are numeric, tel or text/number with maxLength of 1, or maxLength not defined.
|
|
// If ignoreFieldCount is true, number of TOTP fields are allowed to differ from the default (6),
|
|
// but 4 at minimum must exist.
|
|
const areFieldsSegmentedTotp = function(totpInputs, ignoreFieldCount = false) {
|
|
const fieldCountMatches = (ignoreFieldCount && totpInputs.length >= MIN_SEGMENTED_TOTP_FIELDS) ||
|
|
totpInputs.length === DEFAULT_SEGMENTED_TOTP_FIELDS;
|
|
|
|
const inputIsNumeric = (input) =>
|
|
(input.inputMode === 'numeric' || input.inputMode === '') && input.pattern.includes('0-9');
|
|
|
|
const inputIsText = (input) => input.offsetWidth < MAX_SEGMENTED_FIELD_LENGTH &&
|
|
(input.type === 'text' || input.type === 'number') && (input.maxLength === 1 || input.maxLength === -1);
|
|
|
|
return (
|
|
fieldCountMatches &&
|
|
totpInputs.every(input => inputIsNumeric(input) || inputIsText(input) || input.type === 'tel')
|
|
);
|
|
};
|
|
|
|
const addTotpFieldsToCombination = function(inputFields, ignoreFieldCount = false) {
|
|
const totpInputs = Array.from(inputFields).filter(
|
|
(field) =>
|
|
matchesWithNodeName(field, 'INPUT') &&
|
|
[ 'hidden', 'password', 'submit' ].includes(field.type) === false &&
|
|
field.offsetWidth < MAX_SEGMENTED_FIELD_LENGTH
|
|
);
|
|
|
|
if (areFieldsSegmentedTotp(totpInputs, ignoreFieldCount)) {
|
|
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.at(-1), kpxc.databaseState, true);
|
|
kpxcIcons.icons.push({
|
|
field: totpInputs.at(-1),
|
|
iconType: kpxcIcons.iconTypes.TOTP,
|
|
segmented: true
|
|
});
|
|
}
|
|
};
|
|
|
|
const formLengthMatches = function(currentForm) {
|
|
if (!currentForm) {
|
|
return false;
|
|
}
|
|
|
|
// Accept 6 inputs directly
|
|
if (currentForm.length === DEFAULT_SEGMENTED_TOTP_FIELDS) {
|
|
return true;
|
|
}
|
|
|
|
// 7 inputs with a button as the last one (e.g. PayPal uses this)
|
|
if (
|
|
currentForm.length === 7 &&
|
|
(matchesWithNodeName(currentForm.lastChild, 'BUTTON') ||
|
|
(matchesWithNodeName(currentForm.lastChild, 'INPUT') &&
|
|
currentForm.lastChild.type === 'button'))
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// Accept any other site-specific exceptions
|
|
if (kpxcSites.segmentedTotpExceptionFound(currentForm)) {
|
|
exceptionFound = true;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Returns true of form's className, id or name points to a possible TOTP form
|
|
const isSegmentedTotpForm = (form) =>
|
|
acceptedOTPFields.some(
|
|
field =>
|
|
(form.className && form.className.includes(field)) ||
|
|
(form.id && typeof form.id === 'string' && form.id.includes(field)) ||
|
|
(form.name && typeof form.name === 'string' && form.name.includes(field))
|
|
);
|
|
|
|
// Use the form if it has segmented TOTP fields, otherwise try checking the input fields directly
|
|
const form = inputs.length > 0 ? inputs[0].form : undefined;
|
|
if (form && (formLengthMatches(form) || isSegmentedTotpForm(form))) {
|
|
// Use the form's elements
|
|
addTotpFieldsToCombination(form.elements, exceptionFound);
|
|
} else if (areFieldsSegmentedTotp(inputs)) {
|
|
// 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 For example: 'username', 'password', 'totp', 'totpInputs'
|
|
*/
|
|
kpxcFields.getCombination = async function(field, givenType) {
|
|
for (const combination of kpxc.combinations) {
|
|
if (givenType) {
|
|
// Strictly search a given type
|
|
const c = combination[givenType];
|
|
if (c && (c === field || (Array.isArray(c) && c.includes(field)))) {
|
|
return combination;
|
|
}
|
|
} else if (Object.values(combination).find(c => c === 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?.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;
|
|
};
|
|
|
|
// Checks if inputs or combinations contain segmented TOTP fields
|
|
kpxcFields.handleSegmentedTOTPFields = function(inputs, combinations) {
|
|
// Check for multiple segmented TOTP fields when there are no inputs, or combination contains the segemented fields
|
|
const segmentedFields = combinations.filter(c => c.totp);
|
|
if (combinations.length === 0
|
|
|| segmentedFields.length === DEFAULT_SEGMENTED_TOTP_FIELDS
|
|
|| inputs?.length === DEFAULT_SEGMENTED_TOTP_FIELDS) {
|
|
kpxcFields.getSegmentedTOTPFields(inputs, combinations);
|
|
}
|
|
|
|
// Remove previously detected segmented TOTP fields from the combination.
|
|
// This prevents adding icons to each single field when segmented fields are used.
|
|
if (segmentedFields.length === DEFAULT_SEGMENTED_TOTP_FIELDS) {
|
|
const firstTotpField = combinations.findIndex(c => c.totp);
|
|
if (firstTotpField >= 0) {
|
|
combinations.splice(firstTotpField, DEFAULT_SEGMENTED_TOTP_FIELDS);
|
|
}
|
|
}
|
|
|
|
return combinations;
|
|
};
|
|
|
|
// 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?.includes('search') && !formAction?.includes('research')) {
|
|
return true;
|
|
}
|
|
|
|
// Ignore form with search classes
|
|
const formId = form.getLowerCaseAttribute('id');
|
|
if (form.className?.includes('search') || (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;
|
|
};
|
|
|
|
// :popover-open selector is supported only with Firefox >= 125 and Chrome >= 114
|
|
kpxcFields.discoverOverlays = function() {
|
|
try {
|
|
kpxcFields.overlays = document.querySelectorAll(':popover-open, [popover]');
|
|
} catch (e) {
|
|
// Ignore SyntaxError (e.g., unsupported selector)
|
|
if (!(e instanceof SyntaxError)) {
|
|
kpxcFields.popoverSupported = false;
|
|
logError(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Checks if element has an overlay
|
|
kpxcFields.hasOverlay = function(elem) {
|
|
try {
|
|
return elem?.hasAttribute('popover') || elem?.matches(':popover-open');
|
|
} catch (e) {
|
|
// Ignore SyntaxError (e.g., unsupported selector)
|
|
if (!(e instanceof SyntaxError)) {
|
|
kpxcFields.popoverSupported = false;
|
|
logError(e);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Check the visibility of existing fields
|
|
kpxcFields.checkExistingFields = function() {
|
|
if (kpxc.inputs?.some(input => !kpxcFields.isVisible(input))) {
|
|
kpxc.clearAllFromPage();
|
|
kpxc.combinations = [];
|
|
}
|
|
};
|
|
|
|
// Check for popup overlays
|
|
kpxcFields.isOverlayOnTop = function(rect) {
|
|
for (const overlay of kpxcFields.overlays ?? []) {
|
|
if (kpxcSites.overlayExceptionFound(overlay)) {
|
|
continue;
|
|
}
|
|
|
|
const overlayRect = overlay?.getBoundingClientRect();
|
|
if (overlayRect && elementsOverlap(rect, overlayRect)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Check if element is the topmost element
|
|
* @param {HTMLElement} elem Element to be checked
|
|
* @param {DOMRect} rect Precalculated DOMRect of the element
|
|
* @returns {boolean} True if element is the topmost
|
|
*/
|
|
kpxcFields.isTopElement = function(elem, rect) {
|
|
if (!elem || !rect) {
|
|
return false;
|
|
}
|
|
|
|
const rootNode = elem.getRootNode() ?? document;
|
|
|
|
// Returns the topmost element from point x, height/2
|
|
// If the input has a label as the top element and it's inside the input, allow it.
|
|
const getTopmostElement = (element, x, elementRect) => {
|
|
const topElement = rootNode.elementFromPoint(x, elementRect.top + (elementRect.height / 2));
|
|
return element?.labels &&
|
|
element.labels[0] === topElement &&
|
|
elementsOverlap(elementRect, topElement.getBoundingClientRect())
|
|
? element
|
|
: topElement;
|
|
};
|
|
|
|
// Check topmost element from three points inside the input
|
|
if (matchesWithNodeName(elem, 'INPUT') && [
|
|
getTopmostElement(elem, rect.left + (rect.width / 4), rect), // First third
|
|
getTopmostElement(elem, rect.left + (rect.width / 2), rect), // Middle
|
|
getTopmostElement(elem, rect.left + (rect.width / 1.33), rect), // Last third
|
|
].some((e) => e !== elem)) {
|
|
return false;
|
|
}
|
|
|
|
// Check if element has an overlay
|
|
if (kpxcFields.isOverlayOnTop(rect)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Returns true if element is visible on the page
|
|
kpxcFields.isVisible = function(elem) {
|
|
// Returns true if opacity is not set, otherwise check the limits
|
|
const isOpacityAllowed = (opacity) => {
|
|
const opac = Number(opacity);
|
|
return opacity === '' || (opac >= MIN_OPACITY && opac <= MAX_OPACITY);
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (!kpxcFields.isTopElement(elem, rect)) {
|
|
return false;
|
|
}
|
|
|
|
// Check CSS visibility
|
|
const elemStyle = getComputedStyle(elem);
|
|
if (elemStyle.visibility && (elemStyle.visibility === 'hidden' || elemStyle.visibility === 'collapse')
|
|
|| !isOpacityAllowed(elemStyle.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 => !isOpacityAllowed(f.style.opacity))) {
|
|
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 && !creds.submitButton) {
|
|
return;
|
|
}
|
|
|
|
// Finds the element based on the stored ID
|
|
const findElement = async function(fields, idArray) {
|
|
if (idArray) {
|
|
const input = fields.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 buttons = [];
|
|
document.body.querySelectorAll('button, input[type=submit]').forEach(e => {
|
|
if (e.type !== 'hidden' && !e.disabled) {
|
|
buttons.push(e);
|
|
}
|
|
});
|
|
|
|
const [ username, password, totp, submitButton ] = await Promise.all([
|
|
await findElement(inputFields, creds.username),
|
|
await findElement(inputFields, creds.password),
|
|
await findElement(inputFields, creds.totp),
|
|
await findElement(buttons, creds.submitButton),
|
|
]);
|
|
|
|
// Handle StringFields
|
|
const stringFields = [];
|
|
for (const sf of creds.fields) {
|
|
const field = await findElement(inputFields, sf);
|
|
if (field) {
|
|
stringFields.push(field);
|
|
}
|
|
}
|
|
|
|
// Handle custom TOTP field
|
|
if (totp) {
|
|
totp.setAttribute('kpxc-defined', 'totp');
|
|
kpxcTOTPIcons.newIcon(totp, kpxc.databaseState);
|
|
}
|
|
|
|
// No values found
|
|
if (!username && !password && !totp && !submitButton && stringFields?.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const combinations = [];
|
|
combinations.push({
|
|
username: username,
|
|
password: password,
|
|
passwordInputs: [ password ],
|
|
totp: totp,
|
|
fields: stringFields,
|
|
submitButton: submitButton
|
|
});
|
|
|
|
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;
|
|
};
|