320 lines
9.8 KiB
JavaScript
320 lines
9.8 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, {
|
|
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
|
|
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
|
|
MLAutofill: "resource://autofill/MLAutofill.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;
|
|
|
|
// The identifier generated via ContentDOMReference for the associated DOM element
|
|
// of this field
|
|
elementId = null;
|
|
|
|
// The identifier generated via ContentDOMReference for the root element of
|
|
// this field
|
|
rootElementId = null;
|
|
|
|
// If the element is an iframe, it is the id of the BrowsingContext of the iframe,
|
|
// Otherwise, it is the id of the BrowsingContext the element is in
|
|
browsingContextId = null;
|
|
|
|
// string with `${element.id}/{element.name}`. This is only used for debugging.
|
|
identifier = "";
|
|
|
|
// tag name attribute of the element
|
|
localName = null;
|
|
|
|
// 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;
|
|
|
|
// This field could be a lookup field, for example, one that could be used to
|
|
// search for an address or postal code and fill in other fields.
|
|
isLookup = false;
|
|
|
|
/*
|
|
* 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) {
|
|
this.elementWeakRef = new WeakRef(element);
|
|
}
|
|
|
|
get element() {
|
|
return this.elementWeakRef.deref();
|
|
}
|
|
|
|
/**
|
|
* Convert FieldDetail class to an object that is suitable for
|
|
* sending over IPC. Avoid using this in other case.
|
|
*/
|
|
toVanillaObject() {
|
|
const json = { ...this };
|
|
delete json.elementWeakRef;
|
|
return json;
|
|
}
|
|
|
|
static fromVanillaObject(obj) {
|
|
const element = lazy.FormAutofillUtils.getElementByIdentifier(
|
|
obj.elementId
|
|
);
|
|
return element ? Object.assign(new FieldDetail(element), obj) : null;
|
|
}
|
|
|
|
static create(
|
|
element,
|
|
form,
|
|
fieldName = null,
|
|
{
|
|
autocompleteInfo = null,
|
|
fathomLabel = null,
|
|
fathomConfidence = null,
|
|
isVisible = true,
|
|
mlHeaderInput = null,
|
|
mlButtonInput = null,
|
|
isLookup = false,
|
|
} = {}
|
|
) {
|
|
const fieldDetail = new FieldDetail(element);
|
|
|
|
fieldDetail.elementId =
|
|
lazy.FormAutofillUtils.getElementIdentifier(element);
|
|
fieldDetail.rootElementId = lazy.FormAutofillUtils.getElementIdentifier(
|
|
form.rootElement
|
|
);
|
|
fieldDetail.identifier = `${element.id}/${element.name}`;
|
|
fieldDetail.localName = element.localName;
|
|
|
|
if (Array.isArray(fieldName)) {
|
|
fieldDetail.fieldName = fieldName[0] ?? "";
|
|
fieldDetail.alternativeFieldName = fieldName[1] ?? "";
|
|
} else {
|
|
fieldDetail.fieldName = fieldName;
|
|
}
|
|
|
|
if (!fieldDetail.fieldName) {
|
|
fieldDetail.reason = "unknown";
|
|
} else if (autocompleteInfo) {
|
|
fieldDetail.reason = "autocomplete";
|
|
fieldDetail.section = autocompleteInfo.section;
|
|
fieldDetail.addressType = autocompleteInfo.addressType;
|
|
fieldDetail.contactType = autocompleteInfo.contactType;
|
|
fieldDetail.credentialType = autocompleteInfo.credentialType;
|
|
fieldDetail.sectionName =
|
|
autocompleteInfo.section || autocompleteInfo.addressType;
|
|
} else if (fathomConfidence) {
|
|
fieldDetail.reason = "fathom";
|
|
fieldDetail.confidence = fathomConfidence;
|
|
|
|
// TODO: This should be removed once we support reference field info across iframe.
|
|
// Temporarily add an addtional "the field is the only visible input" constraint
|
|
// when determining whether a form has only a high-confidence cc-* field a valid
|
|
// credit card section. We can remove this restriction once we are confident
|
|
// about only using fathom.
|
|
fieldDetail.isOnlyVisibleFieldWithHighConfidence = false;
|
|
if (
|
|
fieldDetail.confidence >
|
|
lazy.FormAutofillUtils.ccFathomHighConfidenceThreshold
|
|
) {
|
|
const root = element.form || element.ownerDocument;
|
|
const inputs = root.querySelectorAll("input:not([type=hidden])");
|
|
if (inputs.length == 1 && inputs[0] == element) {
|
|
fieldDetail.isOnlyVisibleFieldWithHighConfidence = true;
|
|
}
|
|
}
|
|
} else {
|
|
fieldDetail.reason = "regex-heuristic";
|
|
}
|
|
|
|
try {
|
|
fieldDetail.browsingContextId =
|
|
element.localName == "iframe"
|
|
? element.browsingContext.id
|
|
: BrowsingContext.getFromWindow(element.ownerGlobal).id;
|
|
} catch {
|
|
/* unit test doesn't have ownerGlobal */
|
|
}
|
|
|
|
fieldDetail.isVisible = isVisible;
|
|
|
|
// Info required by heuristics
|
|
fieldDetail.maxLength = element.maxLength;
|
|
|
|
if (
|
|
lazy.FormAutofill.isMLExperimentEnabled &&
|
|
["input", "select"].includes(element.localName)
|
|
) {
|
|
fieldDetail.mlinput = lazy.MLAutofill.getMLMarkup(fieldDetail.element);
|
|
fieldDetail.mlHeaderInput = mlHeaderInput;
|
|
fieldDetail.mlButtonInput = mlButtonInput;
|
|
fieldDetail.fathomLabel = fathomLabel;
|
|
fieldDetail.fathomConfidence = fathomConfidence;
|
|
}
|
|
|
|
fieldDetail.isLookup = isLookup;
|
|
|
|
return fieldDetail;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
#parsingIndex = 0;
|
|
|
|
#fieldDetails = [];
|
|
|
|
/**
|
|
* Create a FieldScanner based on form elements with the existing
|
|
* fieldDetails.
|
|
*
|
|
* @param {Array<FieldDetails>} fieldDetails
|
|
* An array of fieldDetail object to be scanned.
|
|
*/
|
|
constructor(fieldDetails) {
|
|
this.#fieldDetails = fieldDetails;
|
|
}
|
|
|
|
/**
|
|
* 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.#fieldDetails.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.#fieldDetails.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.#fieldDetails.length) {
|
|
return null;
|
|
}
|
|
|
|
return this.#fieldDetails[index];
|
|
}
|
|
|
|
/**
|
|
* Return the index of the first visible field found with the given name.
|
|
*
|
|
* @param {string} fieldName
|
|
* The field name to find.
|
|
* @param {string} includeInvisible
|
|
* Whether to find non-visible fields.
|
|
* @returns {number}
|
|
* The index of the element or -1 if not found.
|
|
*/
|
|
getFieldIndexByName(fieldName, includeInvisible = false) {
|
|
for (let idx = 0; this.elementExisting(idx); idx++) {
|
|
let field = this.#fieldDetails[idx];
|
|
if (
|
|
field.fieldName == fieldName &&
|
|
(includeInvisible || field.isVisible)
|
|
) {
|
|
return idx;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
fieldDetail.fieldName = fieldName;
|
|
fieldDetail.reason = "update-heuristic";
|
|
}
|
|
|
|
elementExisting(index) {
|
|
return index < this.#fieldDetails.length;
|
|
}
|
|
}
|
|
|
|
export default FieldScanner;
|