mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Better identification for Shadow DOM in Improved Input Field Detection
This commit is contained in:
parent
5d692ecc34
commit
82b823ff4e
1 changed files with 85 additions and 41 deletions
|
|
@ -11,7 +11,19 @@ MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
|
||||||
* MutationObserver handler for dynamically added input fields.
|
* MutationObserver handler for dynamically added input fields.
|
||||||
*/
|
*/
|
||||||
const kpxcObserverHelper = {};
|
const kpxcObserverHelper = {};
|
||||||
kpxcObserverHelper.ignoredNodeNames = [ 'g', 'path', 'svg', 'A', 'HEAD', 'HTML', 'LABEL', 'LINK', 'SCRIPT', 'SPAN', 'VIDEO' ];
|
kpxcObserverHelper.ignoredNodeNames = [
|
||||||
|
'g',
|
||||||
|
'path',
|
||||||
|
'svg',
|
||||||
|
'A',
|
||||||
|
'HEAD',
|
||||||
|
'HTML',
|
||||||
|
'LABEL',
|
||||||
|
'LINK',
|
||||||
|
'SCRIPT',
|
||||||
|
'SPAN',
|
||||||
|
'VIDEO',
|
||||||
|
];
|
||||||
|
|
||||||
kpxcObserverHelper.ignoredNodeTypes = [
|
kpxcObserverHelper.ignoredNodeTypes = [
|
||||||
Node.ATTRIBUTE_NODE,
|
Node.ATTRIBUTE_NODE,
|
||||||
|
|
@ -73,8 +85,10 @@ kpxcObserverHelper.initObserver = async function() {
|
||||||
}
|
}
|
||||||
} else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) {
|
} else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) {
|
||||||
// Only accept targets with forms
|
// Only accept targets with forms
|
||||||
const forms = matchesWithNodeName(mut.target, 'FORM') ? mut.target : mut.target.getElementsByTagName('form');
|
const forms = matchesWithNodeName(mut.target, 'FORM')
|
||||||
if (forms.length === 0 && !kpxcSites.exceptionFound(mut.target.classList, mut.target)) {
|
? mut.target
|
||||||
|
: mut.target.getElementsByTagName('form');
|
||||||
|
if (forms?.length === 0 && !kpxcSites.exceptionFound(mut.target.classList, mut.target)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +147,11 @@ kpxcObserverHelper.cacheStyle = function(mut, styleMutations, mutationCount) {
|
||||||
|
|
||||||
// Gets input fields from the target
|
// Gets input fields from the target
|
||||||
kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) {
|
kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) {
|
||||||
|
// Basic check for input element
|
||||||
|
const inputAllowed = (elem) => !elem.disabled
|
||||||
|
&& elem.getLowerCaseAttribute('type') !== 'hidden'
|
||||||
|
&& !kpxcObserverHelper.alreadyIdentified(elem);
|
||||||
|
|
||||||
// Ignores target element if it's not an element node
|
// Ignores target element if it's not an element node
|
||||||
if (kpxcObserverHelper.ignoredNode(target)) {
|
if (kpxcObserverHelper.ignoredNode(target)) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -140,34 +159,36 @@ kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) {
|
||||||
|
|
||||||
// Filter out any input fields with type 'hidden' right away
|
// Filter out any input fields with type 'hidden' right away
|
||||||
let inputFields = [];
|
let inputFields = [];
|
||||||
Array.from(target.getElementsByTagName('input')).forEach(e => {
|
target.childElementCount > 0 && target.querySelectorAll('input')?.forEach((elem) => {
|
||||||
if (e.type !== 'hidden' && !e.disabled && !kpxcObserverHelper.alreadyIdentified(e)) {
|
if (inputAllowed(elem)) {
|
||||||
inputFields.push(e);
|
inputFields.push(elem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matchesWithNodeName(target, 'INPUT')) {
|
if (matchesWithNodeName(target, 'input')) {
|
||||||
inputFields.push(target);
|
inputFields.push(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traverse children, only if Improved Field Detection is enabled for the site
|
|
||||||
if (kpxc.improvedFieldDetectionEnabledForPage) {
|
if (kpxc.improvedFieldDetectionEnabledForPage) {
|
||||||
const traversedChildren = kpxcObserverHelper.findInputsFromChildren(target);
|
const inputFieldsFromShadowDOM = kpxcObserverHelper.findInputsFromShadowDOM(target);
|
||||||
for (const child of traversedChildren) {
|
if (inputFieldsFromShadowDOM.length > 0) {
|
||||||
if (!inputFields.includes(child)) {
|
logDebug('Input fields from Shadow DOM found:', inputFieldsFromShadowDOM);
|
||||||
inputFields.push(child);
|
}
|
||||||
|
|
||||||
|
for (const inputField of inputFieldsFromShadowDOM) {
|
||||||
|
if (!inputFields.includes(inputField)) {
|
||||||
|
inputFields.push(inputField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append any input fields in Shadow DOM
|
// Append any input fields in Shadow DOM that are directly in the target
|
||||||
if (target.shadowRoot && typeof target.shadowSelectorAll === 'function') {
|
const targetShadowRoot = getShadowDOM(target);
|
||||||
target.shadowSelectorAll('input').forEach(e => {
|
targetShadowRoot?.querySelectorAll('input')?.forEach((input) => {
|
||||||
if (e.type !== 'hidden' && !e.disabled && !kpxcObserverHelper.alreadyIdentified(e)) {
|
if (inputAllowed(input)) {
|
||||||
inputFields.push(e);
|
inputFields.push(input);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (inputFields.length === 0) {
|
if (inputFields.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -201,9 +222,10 @@ kpxcObserverHelper.alreadyIdentified = function(target) {
|
||||||
return kpxc.inputs.some(e => e === target);
|
return kpxc.inputs.some(e => e === target);
|
||||||
};
|
};
|
||||||
|
|
||||||
kpxcObserverHelper.findInputsFromChildren = function(target) {
|
kpxcObserverHelper.findInputsFromShadowDOM = function(target) {
|
||||||
const inputFields = [];
|
const inputFields = [];
|
||||||
traverseChildren(target, inputFields);
|
traverseShadowDOM(target, inputFields);
|
||||||
|
|
||||||
return inputFields;
|
return inputFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -290,27 +312,49 @@ kpxcObserverHelper.ignoredNode = function(target) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Traverses all children, including Shadow DOM elements
|
// Gets Shadow DOM from the element
|
||||||
const traverseChildren = function(target, inputFields, depth = 1) {
|
const getShadowDOM = function(elem) {
|
||||||
depth++;
|
if (!elem || kpxcObserverHelper.ignoredNode(elem)) {
|
||||||
|
|
||||||
// Children can be scripts etc. so ignoredNode() is needed here
|
|
||||||
if (depth >= MAX_CHILDREN || kpxcObserverHelper.ignoredNode(target)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of target.childNodes) {
|
try {
|
||||||
if (child.type === 'hidden' || child.disabled || kpxcObserverHelper.ignoredNode(child)) {
|
return elem.openOrClosedShadowRoot ? elem.openOrClosedShadowRoot : browser.dom.openOrClosedShadowRoot(elem);
|
||||||
continue;
|
} catch (e) {
|
||||||
}
|
return elem.shadowRoot;
|
||||||
|
}
|
||||||
if (matchesWithNodeName(child, 'INPUT')) {
|
};
|
||||||
inputFields.push(child);
|
|
||||||
}
|
// Filter for TreeWalker
|
||||||
|
const treeWalkerFilter = function(node) {
|
||||||
traverseChildren(child, inputFields, depth);
|
return !node || node.disabled || node.getLowerCaseAttribute('type') === 'hidden'
|
||||||
if (child.shadowRoot) {
|
? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
||||||
traverseChildren(child.shadowRoot, inputFields, depth);
|
};
|
||||||
}
|
|
||||||
|
// Traverses all child elements, looking for input fields inside Shadow DOM
|
||||||
|
const traverseShadowDOM = function(target, inputFields) {
|
||||||
|
const treeWalker = document?.createTreeWalker(target, NodeFilter.SHOW_ELEMENT, treeWalkerFilter);
|
||||||
|
let currentNode = treeWalker?.currentNode;
|
||||||
|
|
||||||
|
while (currentNode) {
|
||||||
|
if (!kpxcObserverHelper.ignoredNode(currentNode)
|
||||||
|
&& matchesWithNodeName(currentNode, 'input')
|
||||||
|
&& !kpxcObserverHelper.alreadyIdentified(currentNode)) {
|
||||||
|
inputFields.push(currentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeShadowRoot = getShadowDOM(currentNode);
|
||||||
|
if (nodeShadowRoot?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && nodeShadowRoot?.childElementCount > 0) {
|
||||||
|
// Document Fragments need a special handling. It can contain children with Shadow DOM.
|
||||||
|
for (const child of nodeShadowRoot.children) {
|
||||||
|
if (!kpxcObserverHelper.ignoredNode(child)) {
|
||||||
|
traverseShadowDOM(child, inputFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (nodeShadowRoot) {
|
||||||
|
traverseShadowDOM(nodeShadowRoot, inputFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = treeWalker?.nextNode();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue