Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Form] Always scan shadow elements when categorizing the form inputs #703

Merged
merged 13 commits into from
Nov 22, 2024
Merged
64 changes: 34 additions & 30 deletions dist/autofill-debug.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 34 additions & 30 deletions dist/autofill.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 6 additions & 22 deletions src/Form/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import {
shouldLog,
safeRegexTest,
getActiveElement,
queryFormElements,
queryElementsWithShadow,
findElementsInShadowTree,
getFormControlElements,
} from '../autofill-utils.js';

import { getInputSubtype, getInputMainType, createMatching, getInputVariant } from './matching.js';
Expand Down Expand Up @@ -387,36 +388,19 @@ class Form {
}
}

/**
* The function looks for form's control elements, and returns them if they're iterable.
* @param {*} selector
*/
getFormControlElements(selector) {
// Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
if (this.form instanceof HTMLFormElement && this.form.elements != null && Symbol.iterator in Object(this.form.elements)) {
// For form elements we use .elements to catch fields outside the form itself using the form attribute.
// It also catches all elements when the markup is broken.
// We use .filter to avoid specific types of elements.
const formControls = [...this.form.elements].filter((el) => el.matches(selector));
return [...formControls];
} else {
return null;
}
}

categorizeInputs() {
const selector = this.matching.cssSelector('formInputsSelector');
// If there's no form container and it's just a lonely input field (this.form is an input field)
if (this.form.matches(selector)) {
this.addInput(this.form);
} else {
// Attempt to get form's control elements first as it can catch elements when markup is broke, or if the fields are outside the form.
// Other wise use queryFormElements, that can scan for shadow tree.
const formControlElements = this.getFormControlElements(selector);
// Other wise use queryElementsWithShadow, that can scan for shadow tree.
const formControlElements = getFormControlElements(this.form, selector);
const foundInputs =
formControlElements != null
? [...formControlElements, ...findElementsInShadowTree(this.form, selector)]
: queryFormElements(this.form, selector, true);
: queryElementsWithShadow(this.form, selector, true);

if (foundInputs.length < MAX_INPUTS_PER_FORM) {
foundInputs.forEach((input) => this.addInput(input));
Expand Down Expand Up @@ -486,7 +470,7 @@ class Form {

get submitButtons() {
const selector = this.matching.cssSelector('submitButtonSelector');
const allButtons = /** @type {HTMLElement[]} */ (queryFormElements(this.form, selector));
const allButtons = /** @type {HTMLElement[]} */ (queryElementsWithShadow(this.form, selector));
return allButtons.filter(
(btn) => isPotentiallyViewable(btn) && isLikelyASubmitButton(btn, this.matching) && buttonMatchesFormType(btn, this),
);
Expand Down
10 changes: 8 additions & 2 deletions src/Form/FormAnalyzer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { removeExcessWhitespace, Matching } from './matching.js';
import { constants } from '../constants.js';
import { matchingConfiguration } from './matching-config/__generated__/compiled-matching-config.js';
import { findElementsInShadowTree, queryFormElements, getTextShallow, isLikelyASubmitButton, safeRegexTest } from '../autofill-utils.js';
import {
findElementsInShadowTree,
queryElementsWithShadow,
getTextShallow,
isLikelyASubmitButton,
safeRegexTest,
} from '../autofill-utils.js';

class FormAnalyzer {
/** @type HTMLElement */
Expand Down Expand Up @@ -311,7 +317,7 @@ class FormAnalyzer {

// Check form contents (noisy elements are skipped with the safeUniversalSelector)
const selector = this.matching.cssSelector('safeUniversalSelector');
const formElements = queryFormElements(this.form, selector);
const formElements = queryElementsWithShadow(this.form, selector);
for (let i = 0; i < formElements.length; i++) {
// Safety cutoff to avoid huge DOMs freezing the browser
if (i >= 200) break;
Expand Down
34 changes: 27 additions & 7 deletions src/autofill-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -587,22 +587,41 @@ function findElementsInShadowTree(root, selector) {
return shadowElements;
}

/**
* The function looks for form's control elements, and returns them if they're iterable.
* @param {HTMLElement} form
* @param {string} selector
* @returns {Element[]|null}
*/
function getFormControlElements(form, selector) {
// Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
if (form instanceof HTMLFormElement && form.elements != null && Symbol.iterator in Object(form.elements)) {
// For form elements we use .elements to catch fields outside the form itself using the form attribute.
// It also catches all elements when the markup is broken.
// We use .filter to avoid specific types of elements.
const formControls = [...form.elements].filter((el) => el.matches(selector));
return [...formControls];
} else {
return null;
}
}

/**
* Default operation: finds elements using querySelectorAll.
* Optionally, can be forced to scan the shadow tree.
* @param {HTMLElement} element
* @param {string} selector
* @param {boolean} forceScanShadowTree
* @returns {Element[]}
*/
function queryFormElements(form, selector, forceScanShadowTree = false) {
// Some sites seem to be overriding `form.elements`, so we need to check if it's still iterable.
function queryElementsWithShadow(element, selector, forceScanShadowTree = false) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Much better name!

/** @type {Element[]|NodeListOf<Element>} element */
const formElements = form.querySelectorAll(selector);
const elements = element.querySelectorAll(selector);

if (forceScanShadowTree || formElements.length === 0) {
return [...formElements, ...findElementsInShadowTree(form, selector)];
if (forceScanShadowTree || elements.length === 0) {
return [...elements, ...findElementsInShadowTree(element, selector)];
}
return [...formElements];
return [...elements];
}

export {
Expand Down Expand Up @@ -638,5 +657,6 @@ export {
pierceShadowTree,
getActiveElement,
findElementsInShadowTree,
queryFormElements,
queryElementsWithShadow,
getFormControlElements,
};
Loading
Loading