From 82b823ff4eab8aec6f78554eef997dcdacbc2842 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Wed, 9 Oct 2024 22:14:53 +0300 Subject: [PATCH] Better identification for Shadow DOM in Improved Input Field Detection --- keepassxc-browser/content/observer-helper.js | 126 +++++++++++++------ 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/keepassxc-browser/content/observer-helper.js b/keepassxc-browser/content/observer-helper.js index 6e1a1c5..187b38e 100644 --- a/keepassxc-browser/content/observer-helper.js +++ b/keepassxc-browser/content/observer-helper.js @@ -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(); } };