mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-06 19:29:24 +02:00
We introduced visibility/focusability check to filter out invisible or unfocusable elements to address the following issues: 1. Bug 1688209: The website's credit card form contains hidden credit card fields beneath the visible ones. Example: <input id="cc-number" autocomplete="cc-number"> <input id="hidden-cc-number" autocomplete="cc-number" hidden>. <input id="cc-name" autocomplete="cc-name"> The issue occurs because when our heuristic encounters consecutive fields that should not appear multiple times in a row, we divide them and treat them as separate sections. In this example, the visible cc-number and visible cc-name are put in different sections so we don't autofill both of the fields at the same time. 2. Bug 1822494: There is one hidden cc-exp-month field and one visible cc-exp field. Example: <input id="cc-exp" autocomplete="cc-exp"> <input id="hidden-cc-exp" autocomplete="cc-exp" hidden>. When two cc-exp-* fields appear consecutively, our heuristic adjusts the first one to cc-exp-month and the second one to cc-exp-year. However, in this bug, we should just honor the autocomplete attribute and do not change the field name Bug 1753669: An invisible country field is located between tel-* fields. Example: <input id="country" autocomplete="country"> <input id="tel-area-code" autocomplete="tel-area-code"> <input id="hidden-country" autocomplete="country" hidden> <input id="tel-local" autocomplete="tel-local"> When the heuristic sees the hidden country field, since it has already identified another country field previously, our heuristic creates a new section upon encountering the invisible country field. This results that we don't put tel-local field in the same section as the rest of the address fields. --- However, introducing visibility and focusability checks also brings issues. Some websites implement their own dropdowns for certain fields, like province, and include an invisible or unfocusable field to store the value, as seen in Bug 1873202 and Bug 1836036. We also see, in some cases, websites prefill certain address fields for users, and those fields are unfocusable. For example, websites can use known-address data to determine the "state/province" field so users don't have to fill it. But in these cases, we still want to identify this type of field so we can capture the data after users submit the form. So, given the information collected so far, I think we should not filter out unfocusable or invisible elements before running heuristics. Instead, we should adjust our heuristic to consider invisible elements in some cases. For example, we should not create a new section upon encountering an invisible field, recognizing that it's common for websites to place an invisible field near a visible field of the same type for various reasons. Differential Revision: https://phabricator.services.mozilla.com/D202297
237 lines
7 KiB
JavaScript
237 lines
7 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* Represents the detailed information about a form field, including
|
|
* the inferred field name, the approach used for inferring, and additional metadata.
|
|
*/
|
|
export class FieldDetail {
|
|
// Reference to the elemenet
|
|
elementWeakRef = null;
|
|
|
|
// id/name. This is only used for debugging
|
|
identifier = "";
|
|
|
|
// The inferred field name for this element
|
|
fieldName = null;
|
|
|
|
// The approach we use to infer the information for this element
|
|
// The possible values are "autocomplete", "fathom", and "regex-heuristic"
|
|
reason = null;
|
|
|
|
/*
|
|
* The "section", "addressType", and "contactType" values are
|
|
* used to identify the exact field when the serializable data is received
|
|
* from the backend. There cannot be multiple fields which have
|
|
* the same exact combination of these values.
|
|
*/
|
|
|
|
// Which section the field belongs to. The value comes from autocomplete attribute.
|
|
// See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-detail-tokens for more details
|
|
section = "";
|
|
addressType = "";
|
|
contactType = "";
|
|
credentialType = "";
|
|
|
|
// When a field is split into N fields, we use part to record which field it is
|
|
// For example, a credit card number field is split into 4 fields, the value of
|
|
// "part" for the first cc-number field is 1, for the last one is 4.
|
|
// If the field is not split, the value is null
|
|
part = null;
|
|
|
|
// Confidence value when the field name is inferred by "fathom"
|
|
confidence = null;
|
|
|
|
constructor(
|
|
element,
|
|
fieldName = null,
|
|
{ autocompleteInfo = {}, confidence = null } = {}
|
|
) {
|
|
this.elementWeakRef = new WeakRef(element);
|
|
this.identifier = `${element.id}/${element.name}`;
|
|
this.fieldName = fieldName;
|
|
|
|
if (autocompleteInfo) {
|
|
this.reason = "autocomplete";
|
|
this.section = autocompleteInfo.section;
|
|
this.addressType = autocompleteInfo.addressType;
|
|
this.contactType = autocompleteInfo.contactType;
|
|
this.credentialType = autocompleteInfo.credentialType;
|
|
} else if (confidence) {
|
|
this.reason = "fathom";
|
|
this.confidence = confidence;
|
|
} else {
|
|
this.reason = "regex-heuristic";
|
|
}
|
|
}
|
|
|
|
get element() {
|
|
return this.elementWeakRef.deref();
|
|
}
|
|
|
|
get sectionName() {
|
|
return this.section || this.addressType;
|
|
}
|
|
|
|
#isVisible = null;
|
|
get isVisible() {
|
|
if (this.#isVisible == null) {
|
|
this.#isVisible = lazy.FormAutofillUtils.isFieldVisible(this.element);
|
|
}
|
|
return this.#isVisible;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A scanner for traversing all elements in a form. It also provides a
|
|
* cursor (parsingIndex) to indicate which element is waiting for parsing.
|
|
*
|
|
* The scanner retrives the field detail by calling heuristics handlers
|
|
* `inferFieldInfo` function.
|
|
*/
|
|
export class FieldScanner {
|
|
#elementsWeakRef = null;
|
|
#inferFieldInfoFn = null;
|
|
|
|
#parsingIndex = 0;
|
|
|
|
fieldDetails = [];
|
|
|
|
/**
|
|
* Create a FieldScanner based on form elements with the existing
|
|
* fieldDetails.
|
|
*
|
|
* @param {Array.DOMElement} elements
|
|
* The elements from a form for each parser.
|
|
* @param {Funcion} inferFieldInfoFn
|
|
* The callback function that is used to infer the field info of a given element
|
|
*/
|
|
constructor(elements, inferFieldInfoFn) {
|
|
this.#elementsWeakRef = new WeakRef(elements);
|
|
this.#inferFieldInfoFn = inferFieldInfoFn;
|
|
}
|
|
|
|
get #elements() {
|
|
return this.#elementsWeakRef.deref();
|
|
}
|
|
|
|
/**
|
|
* This cursor means the index of the element which is waiting for parsing.
|
|
*
|
|
* @returns {number}
|
|
* The index of the element which is waiting for parsing.
|
|
*/
|
|
get parsingIndex() {
|
|
return this.#parsingIndex;
|
|
}
|
|
|
|
get parsingFinished() {
|
|
return this.parsingIndex >= this.#elements.length;
|
|
}
|
|
|
|
/**
|
|
* Move the parsingIndex to the next elements. Any elements behind this index
|
|
* means the parsing tasks are finished.
|
|
*
|
|
* @param {number} index
|
|
* The latest index of elements waiting for parsing.
|
|
*/
|
|
set parsingIndex(index) {
|
|
if (index > this.#elements.length) {
|
|
throw new Error("The parsing index is out of range.");
|
|
}
|
|
this.#parsingIndex = index;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the field detail by the index. If the field detail is not ready,
|
|
* the elements will be traversed until matching the index.
|
|
*
|
|
* @param {number} index
|
|
* The index of the element that you want to retrieve.
|
|
* @returns {object}
|
|
* The field detail at the specific index.
|
|
*/
|
|
getFieldDetailByIndex(index) {
|
|
if (index >= this.#elements.length) {
|
|
return null;
|
|
}
|
|
|
|
if (index < this.fieldDetails.length) {
|
|
return this.fieldDetails[index];
|
|
}
|
|
|
|
for (let i = this.fieldDetails.length; i < index + 1; i++) {
|
|
this.pushDetail();
|
|
}
|
|
|
|
return this.fieldDetails[index];
|
|
}
|
|
|
|
/**
|
|
* This function retrieves the first unparsed element and obtains its
|
|
* information by invoking the `inferFieldInfoFn` callback function.
|
|
* The field information is then stored in a FieldDetail object and
|
|
* appended to the `fieldDetails` array.
|
|
*
|
|
* Any element without the related detail will be used for adding the detail
|
|
* to the end of field details.
|
|
*/
|
|
pushDetail() {
|
|
const elementIndex = this.fieldDetails.length;
|
|
if (elementIndex >= this.#elements.length) {
|
|
throw new Error("Try to push the non-existing element info.");
|
|
}
|
|
const element = this.#elements[elementIndex];
|
|
const [fieldName, autocompleteInfo, confidence] =
|
|
this.#inferFieldInfoFn(element);
|
|
const fieldDetail = new FieldDetail(element, fieldName, {
|
|
autocompleteInfo,
|
|
confidence,
|
|
});
|
|
|
|
this.fieldDetails.push(fieldDetail);
|
|
}
|
|
|
|
/**
|
|
* When a field detail should be changed its fieldName after parsing, use
|
|
* this function to update the fieldName which is at a specific index.
|
|
*
|
|
* @param {number} index
|
|
* The index indicates a field detail to be updated.
|
|
* @param {string} fieldName
|
|
* The new name of the field
|
|
* @param {boolean} [ignoreAutocomplete=false]
|
|
* Whether to change the field name when the field name is determined by
|
|
* autocomplete attribute
|
|
*/
|
|
updateFieldName(index, fieldName, ignoreAutocomplete = false) {
|
|
if (index >= this.fieldDetails.length) {
|
|
throw new Error("Try to update the non-existing field detail.");
|
|
}
|
|
|
|
const fieldDetail = this.fieldDetails[index];
|
|
if (fieldDetail.fieldName == fieldName) {
|
|
return;
|
|
}
|
|
|
|
if (!ignoreAutocomplete && fieldDetail.reason == "autocomplete") {
|
|
return;
|
|
}
|
|
|
|
this.fieldDetails[index].fieldName = fieldName;
|
|
this.fieldDetails[index].reason = "update-heuristic";
|
|
}
|
|
|
|
elementExisting(index) {
|
|
return index < this.#elements.length;
|
|
}
|
|
}
|
|
|
|
export default FieldScanner;
|