/* 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;