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