Better identification for Shadow DOM in Improved Input Field Detection

This commit is contained in:
varjolintu 2024-10-09 22:14:53 +03:00
parent 5d692ecc34
commit 82b823ff4e

View file

@ -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();
}
};