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.
|
||||
*/
|
||||
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 = [
|
||||
Node.ATTRIBUTE_NODE,
|
||||
|
|
@ -73,8 +85,10 @@ kpxcObserverHelper.initObserver = async function() {
|
|||
}
|
||||
} else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) {
|
||||
// Only accept targets with forms
|
||||
const forms = matchesWithNodeName(mut.target, 'FORM') ? mut.target : mut.target.getElementsByTagName('form');
|
||||
if (forms.length === 0 && !kpxcSites.exceptionFound(mut.target.classList, mut.target)) {
|
||||
const forms = matchesWithNodeName(mut.target, 'FORM')
|
||||
? mut.target
|
||||
: mut.target.getElementsByTagName('form');
|
||||
if (forms?.length === 0 && !kpxcSites.exceptionFound(mut.target.classList, mut.target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +147,11 @@ kpxcObserverHelper.cacheStyle = function(mut, styleMutations, mutationCount) {
|
|||
|
||||
// Gets input fields from the target
|
||||
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
|
||||
if (kpxcObserverHelper.ignoredNode(target)) {
|
||||
return [];
|
||||
|
|
@ -140,34 +159,36 @@ kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) {
|
|||
|
||||
// Filter out any input fields with type 'hidden' right away
|
||||
let inputFields = [];
|
||||
Array.from(target.getElementsByTagName('input')).forEach(e => {
|
||||
if (e.type !== 'hidden' && !e.disabled && !kpxcObserverHelper.alreadyIdentified(e)) {
|
||||
inputFields.push(e);
|
||||
target.childElementCount > 0 && target.querySelectorAll('input')?.forEach((elem) => {
|
||||
if (inputAllowed(elem)) {
|
||||
inputFields.push(elem);
|
||||
}
|
||||
});
|
||||
|
||||
if (matchesWithNodeName(target, 'INPUT')) {
|
||||
if (matchesWithNodeName(target, 'input')) {
|
||||
inputFields.push(target);
|
||||
}
|
||||
|
||||
// Traverse children, only if Improved Field Detection is enabled for the site
|
||||
if (kpxc.improvedFieldDetectionEnabledForPage) {
|
||||
const traversedChildren = kpxcObserverHelper.findInputsFromChildren(target);
|
||||
for (const child of traversedChildren) {
|
||||
if (!inputFields.includes(child)) {
|
||||
inputFields.push(child);
|
||||
const inputFieldsFromShadowDOM = kpxcObserverHelper.findInputsFromShadowDOM(target);
|
||||
if (inputFieldsFromShadowDOM.length > 0) {
|
||||
logDebug('Input fields from Shadow DOM found:', inputFieldsFromShadowDOM);
|
||||
}
|
||||
|
||||
for (const inputField of inputFieldsFromShadowDOM) {
|
||||
if (!inputFields.includes(inputField)) {
|
||||
inputFields.push(inputField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Append any input fields in Shadow DOM that are directly in the target
|
||||
const targetShadowRoot = getShadowDOM(target);
|
||||
targetShadowRoot?.querySelectorAll('input')?.forEach((input) => {
|
||||
if (inputAllowed(input)) {
|
||||
inputFields.push(input);
|
||||
}
|
||||
});
|
||||
|
||||
if (inputFields.length === 0) {
|
||||
return [];
|
||||
|
|
@ -201,9 +222,10 @@ kpxcObserverHelper.alreadyIdentified = function(target) {
|
|||
return kpxc.inputs.some(e => e === target);
|
||||
};
|
||||
|
||||
kpxcObserverHelper.findInputsFromChildren = function(target) {
|
||||
kpxcObserverHelper.findInputsFromShadowDOM = function(target) {
|
||||
const inputFields = [];
|
||||
traverseChildren(target, inputFields);
|
||||
traverseShadowDOM(target, inputFields);
|
||||
|
||||
return inputFields;
|
||||
};
|
||||
|
||||
|
|
@ -290,27 +312,49 @@ kpxcObserverHelper.ignoredNode = function(target) {
|
|||
return false;
|
||||
};
|
||||
|
||||
// Traverses all children, including Shadow DOM elements
|
||||
const traverseChildren = function(target, inputFields, depth = 1) {
|
||||
depth++;
|
||||
|
||||
// Children can be scripts etc. so ignoredNode() is needed here
|
||||
if (depth >= MAX_CHILDREN || kpxcObserverHelper.ignoredNode(target)) {
|
||||
// Gets Shadow DOM from the element
|
||||
const getShadowDOM = function(elem) {
|
||||
if (!elem || kpxcObserverHelper.ignoredNode(elem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of target.childNodes) {
|
||||
if (child.type === 'hidden' || child.disabled || kpxcObserverHelper.ignoredNode(child)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchesWithNodeName(child, 'INPUT')) {
|
||||
inputFields.push(child);
|
||||
}
|
||||
|
||||
traverseChildren(child, inputFields, depth);
|
||||
if (child.shadowRoot) {
|
||||
traverseChildren(child.shadowRoot, inputFields, depth);
|
||||
}
|
||||
try {
|
||||
return elem.openOrClosedShadowRoot ? elem.openOrClosedShadowRoot : browser.dom.openOrClosedShadowRoot(elem);
|
||||
} catch (e) {
|
||||
return elem.shadowRoot;
|
||||
}
|
||||
};
|
||||
|
||||
// Filter for TreeWalker
|
||||
const treeWalkerFilter = function(node) {
|
||||
return !node || node.disabled || node.getLowerCaseAttribute('type') === 'hidden'
|
||||
? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
||||
};
|
||||
|
||||
// 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