mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	Depends on D255122 Differential Revision: https://phabricator.services.mozilla.com/D255123
		
			
				
	
	
		
			1536 lines
		
	
	
	
		
			51 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1536 lines
		
	
	
	
		
			51 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/. */
 | 
						|
 | 
						|
import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
 | 
						|
import { HeuristicsRegExp } from "resource://gre/modules/shared/HeuristicsRegExp.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
 | 
						|
  CreditCardRulesets: "resource://gre/modules/shared/CreditCardRuleset.sys.mjs",
 | 
						|
  FieldDetail: "resource://gre/modules/shared/FieldScanner.sys.mjs",
 | 
						|
  FieldScanner: "resource://gre/modules/shared/FieldScanner.sys.mjs",
 | 
						|
  FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
 | 
						|
  LabelUtils: "resource://gre/modules/shared/LabelUtils.sys.mjs",
 | 
						|
  MLAutofill: "resource://autofill/MLAutofill.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * To help us classify sections that can appear only N times in a row.
 | 
						|
 * For example, the only time multiple cc-number fields are valid is when
 | 
						|
 * there are four of these fields in a row.
 | 
						|
 * Otherwise, multiple cc-number fields should be in separate sections.
 | 
						|
 */
 | 
						|
const MULTI_N_FIELD_NAMES = {
 | 
						|
  "cc-number": 4,
 | 
						|
};
 | 
						|
 | 
						|
const CC_TYPE = 1;
 | 
						|
const ADDR_TYPE = 2;
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns the autocomplete information of fields according to heuristics.
 | 
						|
 */
 | 
						|
export const FormAutofillHeuristics = {
 | 
						|
  RULES: HeuristicsRegExp.getRules(),
 | 
						|
  LABEL_RULES: HeuristicsRegExp.getLabelRules(),
 | 
						|
 | 
						|
  CREDIT_CARD_FIELDNAMES: [],
 | 
						|
  ADDRESS_FIELDNAMES: [],
 | 
						|
  /**
 | 
						|
   * Try to find a contiguous sub-array within an array.
 | 
						|
   *
 | 
						|
   * @param {Array} array
 | 
						|
   * @param {Array} subArray
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return whether subArray was found within the array or not.
 | 
						|
   */
 | 
						|
  _matchContiguousSubArray(array, subArray) {
 | 
						|
    return array.some((elm, i) =>
 | 
						|
      subArray.every((sElem, j) => sElem == array[i + j])
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Try to find the field that is look like a month select.
 | 
						|
   *
 | 
						|
   * @param {DOMElement} element
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if we observe the trait of month select in
 | 
						|
   *          the current element.
 | 
						|
   */
 | 
						|
  _isExpirationMonthLikely(element) {
 | 
						|
    if (!HTMLSelectElement.isInstance(element)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const options = [...element.options];
 | 
						|
    const desiredValues = Array(12)
 | 
						|
      .fill(1)
 | 
						|
      .map((v, i) => v + i);
 | 
						|
 | 
						|
    // The number of month options shouldn't be less than 12 or larger than 13
 | 
						|
    // including the default option.
 | 
						|
    if (options.length < 12 || options.length > 13) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    return (
 | 
						|
      this._matchContiguousSubArray(
 | 
						|
        options.map(e => +e.value),
 | 
						|
        desiredValues
 | 
						|
      ) ||
 | 
						|
      this._matchContiguousSubArray(
 | 
						|
        options.map(e => +e.label),
 | 
						|
        desiredValues
 | 
						|
      )
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Try to find the field that is look like a year select.
 | 
						|
   *
 | 
						|
   * @param {DOMElement} element
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if we observe the trait of year select in
 | 
						|
   *          the current element.
 | 
						|
   */
 | 
						|
  _isExpirationYearLikely(element) {
 | 
						|
    if (!HTMLSelectElement.isInstance(element)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const options = [...element.options];
 | 
						|
    // A normal expiration year select should contain at least the last three years
 | 
						|
    // in the list.
 | 
						|
    const curYear = new Date().getFullYear();
 | 
						|
    const desiredValues = Array(3)
 | 
						|
      .fill(0)
 | 
						|
      .map((v, i) => v + curYear + i);
 | 
						|
 | 
						|
    return (
 | 
						|
      this._matchContiguousSubArray(
 | 
						|
        options.map(e => +e.value),
 | 
						|
        desiredValues
 | 
						|
      ) ||
 | 
						|
      this._matchContiguousSubArray(
 | 
						|
        options.map(e => +e.label),
 | 
						|
        desiredValues
 | 
						|
      )
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return a set of additonal attributes related to a field.
 | 
						|
   *
 | 
						|
   * @param {Element} element
 | 
						|
   *        Form element to examine.
 | 
						|
   * @param {list} fieldNames
 | 
						|
   *        String or list of field names for the element.
 | 
						|
   * @returns {map}
 | 
						|
   *        Returns a map of extra attributes.
 | 
						|
   */
 | 
						|
  parseAdditionalAttributes(element, fieldNames) {
 | 
						|
    let attributes = { isLookup: false };
 | 
						|
    const INTERESTED_FIELDS = [
 | 
						|
      "street-address",
 | 
						|
      "address-line1",
 | 
						|
      "address-line2",
 | 
						|
      "address-line3",
 | 
						|
      "postal-code",
 | 
						|
    ];
 | 
						|
 | 
						|
    if (typeof fieldNames == "string") {
 | 
						|
      fieldNames = [fieldNames];
 | 
						|
    }
 | 
						|
 | 
						|
    if (fieldNames?.some(fieldName => INTERESTED_FIELDS.includes(fieldName))) {
 | 
						|
      const regExpLookup = HeuristicsRegExp.getExtraRules("lookup");
 | 
						|
      if (this._matchRegexp(element, regExpLookup)) {
 | 
						|
        attributes.isLookup = true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return attributes;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * This function handles the case when two adjacent fields are incorrectly
 | 
						|
   * identified with the same field name. Currently, only given-name and
 | 
						|
   * family-name are handled as possible errors.
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *        Return true if any field is recognized and updated, otherwise false.
 | 
						|
   */
 | 
						|
  _parseNameFieldsContent(scanner, fieldDetail) {
 | 
						|
    const TARGET_FIELDS = ["given-name", "family-name"];
 | 
						|
    if (!TARGET_FIELDS.includes(fieldDetail.fieldName)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    let idx = scanner.parsingIndex;
 | 
						|
    const detailBefore = scanner.getFieldDetailByIndex(idx - 1);
 | 
						|
    if (fieldDetail.fieldName == detailBefore?.fieldName) {
 | 
						|
      let otherFieldName =
 | 
						|
        fieldDetail.fieldName == TARGET_FIELDS[0]
 | 
						|
          ? TARGET_FIELDS[1]
 | 
						|
          : TARGET_FIELDS[0];
 | 
						|
 | 
						|
      // If the second field matches both field names, or both fields match
 | 
						|
      // both field names, then we change the second field, since the author
 | 
						|
      // was more likely to miscopy the second field from the first. However,
 | 
						|
      // if the earlier field only matches, then we change the first field.
 | 
						|
      if (
 | 
						|
        this._findMatchedFieldNames(fieldDetail.element, [otherFieldName])
 | 
						|
          .length
 | 
						|
      ) {
 | 
						|
        scanner.updateFieldName(idx, otherFieldName);
 | 
						|
      } else if (
 | 
						|
        this._findMatchedFieldNames(detailBefore.element, [otherFieldName])
 | 
						|
          .length
 | 
						|
      ) {
 | 
						|
        scanner.updateFieldName(idx - 1, otherFieldName);
 | 
						|
      }
 | 
						|
 | 
						|
      scanner.parsingIndex++;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * In some languages such French (nom) and German (Name), name can mean either family name or
 | 
						|
   * full name in a form, depending on the context. We want to be sure that if "name" is
 | 
						|
   * detected in the context of "family-name" or "given-name", it is updated accordingly.
 | 
						|
   *
 | 
						|
   * Look for "given-name", "family-name", and "name" fields. If any two of those fields are detected
 | 
						|
   * and one of them is "name", then replace "name" with "family-name" if "name" is accompanied by
 | 
						|
   * "given-name" or vise-versa.
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *        Return true if any field is recognized and updated, otherwise false.
 | 
						|
   */
 | 
						|
  _parseNameFields(scanner, fieldDetail) {
 | 
						|
    const TARGET_FIELDS = ["name", "given-name", "family-name"];
 | 
						|
 | 
						|
    if (!TARGET_FIELDS.includes(fieldDetail.fieldName)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const fields = [];
 | 
						|
    let nameIndex = -1;
 | 
						|
 | 
						|
    for (let idx = scanner.parsingIndex; ; idx++) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (!TARGET_FIELDS.includes(detail?.fieldName)) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      if (detail.fieldName === "name") {
 | 
						|
        nameIndex = idx;
 | 
						|
      }
 | 
						|
      fields.push(detail);
 | 
						|
    }
 | 
						|
 | 
						|
    if (nameIndex != -1 && fields.length == 2) {
 | 
						|
      //if name is detected and the other of the two fields detected is 'given-name'
 | 
						|
      //then update name to 'name' to 'family-name'
 | 
						|
      if (
 | 
						|
        fields[0].fieldName == "given-name" ||
 | 
						|
        fields[1].fieldName == "given-name"
 | 
						|
      ) {
 | 
						|
        scanner.updateFieldName(nameIndex, "family-name");
 | 
						|
        //if name is detected and the other of the two fields detected is 'family-name'
 | 
						|
        //then update name to 'name' to 'given-name'
 | 
						|
      } else if (
 | 
						|
        fields[0].fieldName == "family-name" ||
 | 
						|
        fields[1].fieldName == "family-name"
 | 
						|
      ) {
 | 
						|
        scanner.updateFieldName(nameIndex, "given-name");
 | 
						|
      } else {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
 | 
						|
      scanner.parsingIndex += fields.length;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Try to match the telephone related fields to the grammar
 | 
						|
   * list to see if there is any valid telephone set and correct their
 | 
						|
   * field names.
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if there is any field can be recognized in the parser,
 | 
						|
   *          otherwise false.
 | 
						|
   */
 | 
						|
  _parsePhoneFields(scanner, _fieldDetail) {
 | 
						|
    let matchingResult;
 | 
						|
    const GRAMMARS = this.PHONE_FIELD_GRAMMARS;
 | 
						|
 | 
						|
    function isGrammarSeparator(index) {
 | 
						|
      return !GRAMMARS[index][0];
 | 
						|
    }
 | 
						|
 | 
						|
    const savedIndex = scanner.parsingIndex;
 | 
						|
    for (let ruleFrom = 0; ruleFrom < GRAMMARS.length; ) {
 | 
						|
      const detailStart = scanner.parsingIndex;
 | 
						|
      let ruleTo = ruleFrom;
 | 
						|
      for (let count = 0; ruleTo < GRAMMARS.length; ruleTo++, count++) {
 | 
						|
        // Bail out when reaching the end of the current set of grammars
 | 
						|
        // or there are no more elements to parse
 | 
						|
        if (
 | 
						|
          isGrammarSeparator(ruleTo) ||
 | 
						|
          !scanner.elementExisting(detailStart + count)
 | 
						|
        ) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
 | 
						|
        const [category, , length] = GRAMMARS[ruleTo];
 | 
						|
        const detail = scanner.getFieldDetailByIndex(detailStart + count);
 | 
						|
 | 
						|
        // If the field is not what this grammar rule is interested in, skip processing.
 | 
						|
        if (
 | 
						|
          !detail ||
 | 
						|
          detail.fieldName != category ||
 | 
						|
          detail.reason == "autocomplete"
 | 
						|
        ) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
 | 
						|
        const element = detail.element;
 | 
						|
        if (length && (!element.maxLength || length < element.maxLength)) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      // if we reach the grammar separator, that means all the previous rules are matched.
 | 
						|
      // Set the matchingResult so we update field names accordingly.
 | 
						|
      if (isGrammarSeparator(ruleTo)) {
 | 
						|
        matchingResult = { ruleFrom, ruleTo };
 | 
						|
        break;
 | 
						|
      }
 | 
						|
 | 
						|
      // Fast forward to the next rule set.
 | 
						|
      for (; ruleFrom < GRAMMARS.length; ) {
 | 
						|
        if (isGrammarSeparator(ruleFrom++)) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (matchingResult) {
 | 
						|
      const { ruleFrom, ruleTo } = matchingResult;
 | 
						|
      for (let i = ruleFrom; i < ruleTo; i++) {
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex, GRAMMARS[i][1]);
 | 
						|
        scanner.parsingIndex++;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // If the previous parsed field is a "tel" field, run heuristic to see
 | 
						|
    // if the current field is a "tel-extension" field
 | 
						|
    const field = scanner.getFieldDetailByIndex(scanner.parsingIndex);
 | 
						|
    if (field && field.reason != "autocomplete") {
 | 
						|
      const prev = scanner.getFieldDetailByIndex(scanner.parsingIndex - 1);
 | 
						|
      if (
 | 
						|
        prev &&
 | 
						|
        lazy.FormAutofillUtils.getCategoryFromFieldName(prev.fieldName) == "tel"
 | 
						|
      ) {
 | 
						|
        const regExpTelExtension = new RegExp(
 | 
						|
          "\\bext|ext\\b|extension|ramal", // pt-BR, pt-PT
 | 
						|
          "iug"
 | 
						|
        );
 | 
						|
        if (this._matchRegexp(field.element, regExpTelExtension)) {
 | 
						|
          scanner.updateFieldName(scanner.parsingIndex, "tel-extension");
 | 
						|
          scanner.parsingIndex++;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return savedIndex != scanner.parsingIndex;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * If this is a house number field and there is no address-line1 or
 | 
						|
   * street-address field, change the house number field to address-line1.
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if there is any field can be recognized in the parser,
 | 
						|
   *          otherwise false.
 | 
						|
   */
 | 
						|
  _parseHouseNumberFields(scanner, fieldDetail) {
 | 
						|
    if (fieldDetail?.fieldName == "address-housenumber") {
 | 
						|
      const savedIndex = scanner.parsingIndex;
 | 
						|
      for (let idx = 0; !scanner.parsingFinished; idx++) {
 | 
						|
        const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
        if (!detail) {
 | 
						|
          break;
 | 
						|
        }
 | 
						|
 | 
						|
        if (["address-line1", "street-address"].includes(detail?.fieldName)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      // Return false so additional address handling still gets performed.
 | 
						|
      scanner.updateFieldName(savedIndex, "street-address");
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Try to find the correct address-line[1-3] sequence and correct their field
 | 
						|
   * names.
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if there is any field can be recognized in the parser,
 | 
						|
   *          otherwise false.
 | 
						|
   */
 | 
						|
  _parseStreetAddressFields(scanner, _fieldDetail) {
 | 
						|
    const INTERESTED_FIELDS = [
 | 
						|
      "street-address",
 | 
						|
      "address-line1",
 | 
						|
      "address-line2",
 | 
						|
      "address-line3",
 | 
						|
    ];
 | 
						|
 | 
						|
    // Store the index of fields that are recognized as 'address-housenumber'
 | 
						|
    let houseNumberFields = [];
 | 
						|
 | 
						|
    // The number of address-related lookup fields found.
 | 
						|
    let lookupFieldsCount = 0;
 | 
						|
 | 
						|
    // We need to build a list of the address fields. A list of the indicies
 | 
						|
    // is also needed as the fields with a given name can change positions
 | 
						|
    // during the update.
 | 
						|
    const fields = [];
 | 
						|
    const fieldIndicies = [];
 | 
						|
    for (let idx = scanner.parsingIndex; !scanner.parsingFinished; idx++) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
 | 
						|
      // Skip over any house number fields. There should only be zero or one,
 | 
						|
      // but we'll skip over them all anyway. Only check the alternate field
 | 
						|
      // name if it wasn't already changed by an earlier step.
 | 
						|
      if (
 | 
						|
        detail?.fieldName == "address-housenumber" ||
 | 
						|
        (detail?.reason == "regex-heuristic" &&
 | 
						|
          detail?.alternativeFieldName == "address-housenumber")
 | 
						|
      ) {
 | 
						|
        houseNumberFields.push(idx);
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      if (!INTERESTED_FIELDS.includes(detail?.fieldName)) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
 | 
						|
      if (detail?.isLookup) {
 | 
						|
        lookupFieldsCount++;
 | 
						|
        continue; // Skip address lookup fields
 | 
						|
      }
 | 
						|
 | 
						|
      fields.push(detail);
 | 
						|
      fieldIndicies.push(idx);
 | 
						|
    }
 | 
						|
 | 
						|
    if (!fields.length) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    switch (fields.length) {
 | 
						|
      case 1:
 | 
						|
        if (
 | 
						|
          fields[0].reason != "autocomplete" &&
 | 
						|
          ["address-line2", "address-line3"].includes(fields[0].fieldName)
 | 
						|
        ) {
 | 
						|
          // If an earlier address field was already found, ignore any
 | 
						|
          // address-related fields from the OTHER_ADDRESS_FIELDS
 | 
						|
          // list since those can appear in-between the address-level1
 | 
						|
          // and additional address info fields. If no address field
 | 
						|
          // exists, update the field to be address-line1.
 | 
						|
          const OTHER_ADDRESS_FIELDS = [
 | 
						|
            "address-level1",
 | 
						|
            "address-level2",
 | 
						|
            "address-level3",
 | 
						|
            "postal-code",
 | 
						|
            "organization",
 | 
						|
          ];
 | 
						|
          let canUpdate = true;
 | 
						|
 | 
						|
          for (let idx = scanner.parsingIndex - 1; idx >= 0; idx--) {
 | 
						|
            const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
            if (
 | 
						|
              detail?.fieldName == "street-address" ||
 | 
						|
              detail?.fieldName == "address-line1" ||
 | 
						|
              detail?.fieldName == "address-housenumber"
 | 
						|
            ) {
 | 
						|
              canUpdate = false;
 | 
						|
              break;
 | 
						|
            }
 | 
						|
 | 
						|
            if (!OTHER_ADDRESS_FIELDS.includes(detail?.fieldName)) {
 | 
						|
              break;
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          if (canUpdate) {
 | 
						|
            scanner.updateFieldName(fieldIndicies[0], "address-line1");
 | 
						|
          }
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case 2:
 | 
						|
        if (fields[0].reason == "autocomplete") {
 | 
						|
          if (
 | 
						|
            fields[0].fieldName == "street-address" &&
 | 
						|
            (fields[1].fieldName == "address-line2" ||
 | 
						|
              fields[1].reason != "autocomplete")
 | 
						|
          ) {
 | 
						|
            scanner.updateFieldName(fieldIndicies[0], "address-line1", true);
 | 
						|
          }
 | 
						|
        } else {
 | 
						|
          scanner.updateFieldName(fieldIndicies[0], "address-line1");
 | 
						|
        }
 | 
						|
        scanner.updateFieldName(fieldIndicies[1], "address-line2");
 | 
						|
        break;
 | 
						|
      case 3:
 | 
						|
      default:
 | 
						|
        scanner.updateFieldName(fieldIndicies[0], "address-line1");
 | 
						|
        scanner.updateFieldName(fieldIndicies[1], "address-line2");
 | 
						|
        scanner.updateFieldName(fieldIndicies[2], "address-line3");
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    // 'address-housenumber' might be recognized alongside another field type
 | 
						|
    // (see `alternativeFieldName`). In this case, we should update the field
 | 
						|
    // name before advancing the parsing index.
 | 
						|
    for (const idx of houseNumberFields) {
 | 
						|
      scanner.updateFieldName(idx, "address-housenumber");
 | 
						|
    }
 | 
						|
    scanner.parsingIndex +=
 | 
						|
      fields.length + houseNumberFields.length + lookupFieldsCount;
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
 | 
						|
  _parseAddressFields(scanner, fieldDetail) {
 | 
						|
    let fieldFound = false;
 | 
						|
 | 
						|
    // If there is an address-level3 field but no address-level2 field,
 | 
						|
    // modify to be address-level2.
 | 
						|
    if (
 | 
						|
      fieldDetail.fieldName == "address-level3" &&
 | 
						|
      scanner.getFieldIndexByName("address-level2") == -1
 | 
						|
    ) {
 | 
						|
      scanner.updateFieldName(scanner.parsingIndex, "address-level2");
 | 
						|
      fieldFound = true;
 | 
						|
    }
 | 
						|
 | 
						|
    // State & City(address-level2)
 | 
						|
    if (
 | 
						|
      fieldDetail.fieldName == "address-level2" &&
 | 
						|
      scanner.getFieldIndexByName("address-level1") == -1
 | 
						|
    ) {
 | 
						|
      const prev = scanner.getFieldDetailByIndex(scanner.parsingIndex - 1);
 | 
						|
      if (prev && !prev.fieldName && prev.localName == "select") {
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex - 1, "address-level1");
 | 
						|
        scanner.parsingIndex += 1;
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
      const next = scanner.getFieldDetailByIndex(scanner.parsingIndex + 1);
 | 
						|
      if (next && !next.fieldName && next.localName == "select") {
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex + 1, "address-level1");
 | 
						|
        scanner.parsingIndex += 2;
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
 | 
						|
      fieldFound = true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (fieldFound) {
 | 
						|
      scanner.parsingIndex++;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Try to look for expiration date fields and revise the field names if needed.
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if there is any field can be recognized in the parser,
 | 
						|
   *          otherwise false.
 | 
						|
   */
 | 
						|
  _parseCreditCardExpiryFields(scanner, fieldDetail) {
 | 
						|
    const INTERESTED_FIELDS = ["cc-exp", "cc-exp-month", "cc-exp-year"];
 | 
						|
 | 
						|
    if (!INTERESTED_FIELDS.includes(fieldDetail.fieldName)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const fields = [];
 | 
						|
    for (let idx = scanner.parsingIndex; ; idx++) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (!INTERESTED_FIELDS.includes(detail?.fieldName)) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      fields.push(detail);
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't process the fields if expiration month and expiration year are already
 | 
						|
    // matched by regex in correct order.
 | 
						|
    if (
 | 
						|
      (fields.length == 1 && fields[0].fieldName == "cc-exp") ||
 | 
						|
      (fields.length == 2 &&
 | 
						|
        fields[0].fieldName == "cc-exp-month" &&
 | 
						|
        fields[1].fieldName == "cc-exp-year")
 | 
						|
    ) {
 | 
						|
      scanner.parsingIndex += fields.length;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    const prevCCFields = new Set();
 | 
						|
    for (let idx = scanner.parsingIndex - 1; ; idx--) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (
 | 
						|
        lazy.FormAutofillUtils.getCategoryFromFieldName(detail?.fieldName) !=
 | 
						|
        "creditCard"
 | 
						|
      ) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      prevCCFields.add(detail.fieldName);
 | 
						|
    }
 | 
						|
    // We update the "cc-exp-*" fields to correct "cc-ex-*" fields order when
 | 
						|
    // the following conditions are met:
 | 
						|
    // 1. The previous elements are identified as credit card fields and
 | 
						|
    //    cc-number is in it
 | 
						|
    // 2. There is no "cc-exp-*" fields in the previous credit card elements
 | 
						|
    if (
 | 
						|
      ["cc-number", "cc-name"].some(f => prevCCFields.has(f)) &&
 | 
						|
      !["cc-exp", "cc-exp-month", "cc-exp-year"].some(f => prevCCFields.has(f))
 | 
						|
    ) {
 | 
						|
      if (fields.length == 1) {
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex, "cc-exp");
 | 
						|
      } else if (fields.length == 2) {
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex, "cc-exp-month");
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex + 1, "cc-exp-year");
 | 
						|
      }
 | 
						|
      scanner.parsingIndex += fields.length;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // Set field name to null as it failed to match any patterns.
 | 
						|
    for (let idx = 0; idx < fields.length; idx++) {
 | 
						|
      scanner.updateFieldName(scanner.parsingIndex + idx, null);
 | 
						|
    }
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  _parseCreditCardNumberFields(scanner, fieldDetail) {
 | 
						|
    const INTERESTED_FIELDS = ["cc-number"];
 | 
						|
 | 
						|
    if (!INTERESTED_FIELDS.includes(fieldDetail.fieldName)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const fieldDetails = [];
 | 
						|
    for (let idx = scanner.parsingIndex; ; idx++) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (!INTERESTED_FIELDS.includes(detail?.fieldName)) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      fieldDetails.push(detail);
 | 
						|
    }
 | 
						|
 | 
						|
    // This rule only applies when all the fields are visible
 | 
						|
    if (fieldDetails.some(field => !field.isVisible)) {
 | 
						|
      scanner.parsingIndex += fieldDetails.length;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // This is the heuristic to handle special cases where we can have multiple
 | 
						|
    // fields in one section, but only if the field has appeared N times in a row.
 | 
						|
    // For example, websites can use 4 consecutive 4-digit `cc-number` fields
 | 
						|
    // instead of one 16-digit `cc-number` field.
 | 
						|
    const N = MULTI_N_FIELD_NAMES["cc-number"];
 | 
						|
    if (fieldDetails.length == N) {
 | 
						|
      fieldDetails.forEach((fd, index) => {
 | 
						|
        // part starts with 1
 | 
						|
        fd.part = index + 1;
 | 
						|
      });
 | 
						|
      scanner.parsingIndex += fieldDetails.length;
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
  /**
 | 
						|
   * Look for cc-*-name fields when *-name field is present
 | 
						|
   *
 | 
						|
   * @param {FieldScanner} scanner
 | 
						|
   *        The current parsing status for all elements
 | 
						|
   * @returns {boolean}
 | 
						|
   *          Return true if there is any field can be recognized in the parser,
 | 
						|
   *          otherwise false.
 | 
						|
   */
 | 
						|
  _parseCreditCardNameFields(scanner, fieldDetail) {
 | 
						|
    const INTERESTED_FIELDS = [
 | 
						|
      "name",
 | 
						|
      "given-name",
 | 
						|
      "additional-name",
 | 
						|
      "family-name",
 | 
						|
    ];
 | 
						|
 | 
						|
    if (!INTERESTED_FIELDS.includes(fieldDetail.fieldName)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const fields = [];
 | 
						|
    for (let idx = scanner.parsingIndex; ; idx++) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (!INTERESTED_FIELDS.includes(detail?.fieldName)) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      fields.push(detail);
 | 
						|
    }
 | 
						|
 | 
						|
    const prevCCFields = new Set();
 | 
						|
    for (let idx = scanner.parsingIndex - 1; ; idx--) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (
 | 
						|
        lazy.FormAutofillUtils.getCategoryFromFieldName(detail?.fieldName) !=
 | 
						|
        "creditCard"
 | 
						|
      ) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      prevCCFields.add(detail.fieldName);
 | 
						|
    }
 | 
						|
 | 
						|
    const subsequentCCFields = new Set();
 | 
						|
 | 
						|
    for (let idx = scanner.parsingIndex + fields.length; ; idx++) {
 | 
						|
      const detail = scanner.getFieldDetailByIndex(idx);
 | 
						|
      if (
 | 
						|
        // For updates we only check subsequent fields that are not of type address or do not have an
 | 
						|
        // alternative field name that is of type address, to avoid falsely updating address
 | 
						|
        // form name fields to cc-*-name.
 | 
						|
        lazy.FormAutofillUtils.getCategoryFromFieldName(detail?.fieldName) !=
 | 
						|
          "creditCard" ||
 | 
						|
        (detail?.alternativeFieldName !== undefined &&
 | 
						|
          lazy.FormAutofillUtils.getCategoryFromFieldName(
 | 
						|
            detail?.alternativeFieldName
 | 
						|
          ) != "creditCard")
 | 
						|
      ) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      subsequentCCFields.add(detail.fieldName);
 | 
						|
    }
 | 
						|
 | 
						|
    const isLastField =
 | 
						|
      scanner.getFieldDetailByIndex(scanner.parsingIndex + 1) === null;
 | 
						|
 | 
						|
    // We update the "name" fields to "cc-name" fields when the following
 | 
						|
    // conditions are met:
 | 
						|
    // 1. The preceding fields are identified as credit card fields and
 | 
						|
    //    contain the "cc-number" field.
 | 
						|
    // 2. No "cc-name-*" field is found among the preceding credit card fields.
 | 
						|
    // 3. The "cc-csc" field is either not present among the preceding credit card fields,
 | 
						|
    //    or the current field is the last field in the form. This condition is in place
 | 
						|
    //    because "cc-csc" is often the last field in a credit card form, and we want to
 | 
						|
    //    avoid mistakenly updating fields in subsequent address forms.
 | 
						|
    if (
 | 
						|
      (["cc-number"].some(f => prevCCFields.has(f)) &&
 | 
						|
        !["cc-name", "cc-given-name", "cc-family-name"].some(f =>
 | 
						|
          prevCCFields.has(f)
 | 
						|
        ) &&
 | 
						|
        (isLastField || !prevCCFields.has("cc-csc"))) || // 4. Or we update when current name field is followed by
 | 
						|
      //    creditcard form fields that contain cc-number
 | 
						|
      //    and no cc-*-name field is detected
 | 
						|
      (["cc-number"].some(f => subsequentCCFields.has(f)) &&
 | 
						|
        !["cc-name", "cc-given-name", "cc-family-name"].some(f =>
 | 
						|
          subsequentCCFields.has(f)
 | 
						|
        ))
 | 
						|
    ) {
 | 
						|
      // If there is only one field, assume the name field a `cc-name` field
 | 
						|
      if (fields.length == 1) {
 | 
						|
        scanner.updateFieldName(scanner.parsingIndex, `cc-name`);
 | 
						|
        scanner.parsingIndex += 1;
 | 
						|
      } else {
 | 
						|
        // update *-name to cc-*-name
 | 
						|
        for (const field of fields) {
 | 
						|
          scanner.updateFieldName(
 | 
						|
            scanner.parsingIndex,
 | 
						|
            `cc-${field.fieldName}`
 | 
						|
          );
 | 
						|
          scanner.parsingIndex += 1;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * If the given field is of a different type than the previous
 | 
						|
   * field, use the alternate field name instead.
 | 
						|
   */
 | 
						|
  _checkForAlternateField(scanner, fieldDetail) {
 | 
						|
    if (fieldDetail.alternativeFieldName) {
 | 
						|
      const previousField = scanner.getFieldDetailByIndex(
 | 
						|
        scanner.parsingIndex - 1
 | 
						|
      );
 | 
						|
      if (previousField) {
 | 
						|
        const preIsCC = lazy.FormAutofillUtils.isCreditCardField(
 | 
						|
          previousField.fieldName
 | 
						|
        );
 | 
						|
        const curIsCC = lazy.FormAutofillUtils.isCreditCardField(
 | 
						|
          fieldDetail.fieldName
 | 
						|
        );
 | 
						|
 | 
						|
        // If the current type is different from the previous element's type, use
 | 
						|
        // the alternative fieldname instead.
 | 
						|
        if (preIsCC != curIsCC) {
 | 
						|
          fieldDetail.fieldName = fieldDetail.alternativeFieldName;
 | 
						|
          fieldDetail.reason = "update-heuristic-alternate";
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * This function should provide all field details of a form which are placed
 | 
						|
   * in the belonging section. The details contain the autocomplete info
 | 
						|
   * (e.g. fieldName, section, etc).
 | 
						|
   *
 | 
						|
   * @param {formLike} formLike
 | 
						|
   *        the elements in this form to be predicted the field info.
 | 
						|
   * @param {boolean} ignoreInvisibleInput
 | 
						|
   *        True to NOT run heuristics on invisible <input> fields.
 | 
						|
   * @returns {Array<FormSection>}
 | 
						|
   *        all sections within its field details in the form.
 | 
						|
   */
 | 
						|
  getFormInfo(formLike, ignoreInvisibleInput) {
 | 
						|
    const elements = Array.from(formLike.elements).filter(element =>
 | 
						|
      lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)
 | 
						|
    );
 | 
						|
 | 
						|
    let closestHeaders;
 | 
						|
    let closestButtons;
 | 
						|
    if (FormAutofill.isMLExperimentEnabled && elements.length) {
 | 
						|
      closestHeaders = lazy.MLAutofill.closestHeaderAbove(elements);
 | 
						|
      closestButtons = lazy.MLAutofill.closestButtonBelow(elements);
 | 
						|
    }
 | 
						|
 | 
						|
    const fieldDetails = [];
 | 
						|
    for (let idx = 0; idx < elements.length; idx++) {
 | 
						|
      const element = elements[idx];
 | 
						|
      // Ignore invisible <input>, we still keep invisible <select> since
 | 
						|
      // some websites implements their custom dropdown and use invisible <select>
 | 
						|
      // to store the value.
 | 
						|
      const isVisible = lazy.FormAutofillUtils.isFieldVisible(element);
 | 
						|
      if (
 | 
						|
        !HTMLSelectElement.isInstance(element) &&
 | 
						|
        !isVisible &&
 | 
						|
        ignoreInvisibleInput &&
 | 
						|
        // Include invisible but previously autocompleted inputs in order
 | 
						|
        // keep track of them. This way they will also be cleared on a form clearing action.
 | 
						|
        element.autofillState != lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED
 | 
						|
      ) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      const [fieldName, inferInfo] = this.inferFieldInfo(element, elements);
 | 
						|
      const attributes = this.parseAdditionalAttributes(element, fieldName);
 | 
						|
 | 
						|
      // For cases where the heuristic has determined the field name without
 | 
						|
      // running Fathom, still run Fathom so we can compare the results between
 | 
						|
      // Fathom and the ML model. Note that this is only enabled when the ML experiment
 | 
						|
      // is enabled.
 | 
						|
      if (
 | 
						|
        FormAutofill.isMLExperimentEnabled &&
 | 
						|
        inferInfo.fathomConfidence == undefined
 | 
						|
      ) {
 | 
						|
        let fields = this._getPossibleFieldNames(element);
 | 
						|
        fields = fields.filter(r => lazy.CreditCardRulesets.types.includes(r));
 | 
						|
        const [label, score] = this.getFathomField(element, fields, elements);
 | 
						|
        inferInfo.fathomLabel = label;
 | 
						|
        inferInfo.fathomConfidence = score;
 | 
						|
      }
 | 
						|
 | 
						|
      fieldDetails.push(
 | 
						|
        lazy.FieldDetail.create(element, formLike, fieldName, {
 | 
						|
          autocompleteInfo: inferInfo.autocompleteInfo,
 | 
						|
          fathomLabel: inferInfo.fathomLabel,
 | 
						|
          fathomConfidence: inferInfo.fathomConfidence,
 | 
						|
          isVisible,
 | 
						|
          mlHeaderInput: closestHeaders?.[idx] ?? null,
 | 
						|
          mlButtonInput: closestButtons?.[idx] ?? null,
 | 
						|
          isLookup: attributes.isLookup,
 | 
						|
        })
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    this.parseAndUpdateFieldNamesContent(fieldDetails);
 | 
						|
 | 
						|
    lazy.LabelUtils.clearLabelMap();
 | 
						|
 | 
						|
    return fieldDetails;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Similar to `parseAndUpdateFieldNamesParent`. The difference is that
 | 
						|
   * the parsing heuristics used in this function are based on information
 | 
						|
   * not currently passed to the parent process. For example,
 | 
						|
   * text strings from associated labels.
 | 
						|
   *
 | 
						|
   * Note that the heuristics run in this function will not be able
 | 
						|
   * to reference field information across frames.
 | 
						|
   *
 | 
						|
   * @param {Array<FieldDetail>} fieldDetails
 | 
						|
   *        An array of the identified fields.
 | 
						|
   */
 | 
						|
  parseAndUpdateFieldNamesContent(fieldDetails) {
 | 
						|
    const scanner = new lazy.FieldScanner(fieldDetails);
 | 
						|
 | 
						|
    while (!scanner.parsingFinished) {
 | 
						|
      const savedIndex = scanner.parsingIndex;
 | 
						|
 | 
						|
      // First, we get the inferred field info
 | 
						|
      const fieldDetail = scanner.getFieldDetailByIndex(scanner.parsingIndex);
 | 
						|
 | 
						|
      if (
 | 
						|
        this._parseNameFieldsContent(scanner, fieldDetail) ||
 | 
						|
        this._parsePhoneFields(scanner, fieldDetail)
 | 
						|
      ) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      if (savedIndex == scanner.parsingIndex) {
 | 
						|
        scanner.parsingIndex++;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Iterates through the field details and updates the field names
 | 
						|
   * based on surrounding field information, using various parsing functions.
 | 
						|
   *
 | 
						|
   * @param {Array<FieldDetail>} fieldDetails
 | 
						|
   *        An array of the identified fields.
 | 
						|
   */
 | 
						|
  parseAndUpdateFieldNamesParent(fieldDetails) {
 | 
						|
    const scanner = new lazy.FieldScanner(fieldDetails);
 | 
						|
 | 
						|
    while (!scanner.parsingFinished) {
 | 
						|
      const savedIndex = scanner.parsingIndex;
 | 
						|
 | 
						|
      const fieldDetail = scanner.getFieldDetailByIndex(scanner.parsingIndex);
 | 
						|
 | 
						|
      this._checkForAlternateField(scanner, fieldDetail);
 | 
						|
 | 
						|
      // Attempt to parse the field using different parsers.
 | 
						|
      if (
 | 
						|
        this._parseNameFields(scanner, fieldDetail) ||
 | 
						|
        this._parseHouseNumberFields(scanner, fieldDetail) ||
 | 
						|
        this._parseStreetAddressFields(scanner, fieldDetail) ||
 | 
						|
        this._parseAddressFields(scanner, fieldDetail) ||
 | 
						|
        this._parseCreditCardExpiryFields(scanner, fieldDetail) ||
 | 
						|
        this._parseCreditCardNameFields(scanner, fieldDetail) ||
 | 
						|
        this._parseCreditCardNumberFields(scanner, fieldDetail)
 | 
						|
      ) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      // Move the parsing cursor forward if no parser was applied.
 | 
						|
      if (savedIndex == scanner.parsingIndex) {
 | 
						|
        scanner.parsingIndex++;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _getPossibleFieldNames(element) {
 | 
						|
    let fieldNames = [];
 | 
						|
    const isAutoCompleteOff =
 | 
						|
      element.autocomplete == "off" || element.form?.autocomplete == "off";
 | 
						|
    if (!isAutoCompleteOff || FormAutofill.creditCardsAutocompleteOff) {
 | 
						|
      fieldNames.push(...this.CREDIT_CARD_FIELDNAMES);
 | 
						|
    }
 | 
						|
    if (!isAutoCompleteOff || FormAutofill.addressesAutocompleteOff) {
 | 
						|
      fieldNames.push(...this.ADDRESS_FIELDNAMES);
 | 
						|
    }
 | 
						|
 | 
						|
    if (HTMLInputElement.isInstance(element) && element.type == "search") {
 | 
						|
      const FIELDNAMES_FOR_SEARCH_TYPE = [
 | 
						|
        "address-level1",
 | 
						|
        "address-level2",
 | 
						|
        "address-line1",
 | 
						|
        "address-line2",
 | 
						|
        "address-line3",
 | 
						|
        "street-address",
 | 
						|
        "postal-code",
 | 
						|
      ];
 | 
						|
      fieldNames = fieldNames.filter(name =>
 | 
						|
        FIELDNAMES_FOR_SEARCH_TYPE.includes(name)
 | 
						|
      );
 | 
						|
    } else if (HTMLSelectElement.isInstance(element)) {
 | 
						|
      const FIELDNAMES_FOR_SELECT_ELEMENT = [
 | 
						|
        "address-level1",
 | 
						|
        "address-level2",
 | 
						|
        "country",
 | 
						|
        "cc-exp-month",
 | 
						|
        "cc-exp-year",
 | 
						|
        "cc-exp",
 | 
						|
        "cc-type",
 | 
						|
        "tel-country-code",
 | 
						|
      ];
 | 
						|
      fieldNames = fieldNames.filter(name =>
 | 
						|
        FIELDNAMES_FOR_SELECT_ELEMENT.includes(name)
 | 
						|
      );
 | 
						|
    } else if (HTMLTextAreaElement.isInstance(element)) {
 | 
						|
      const FIELDNAMES_FOR_TEXT_AREA_ELEMENT = ["street-address"];
 | 
						|
      fieldNames = fieldNames.filter(name =>
 | 
						|
        FIELDNAMES_FOR_TEXT_AREA_ELEMENT.includes(name)
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return fieldNames;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get inferred information about an input element using autocomplete info, fathom and regex-based heuristics.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} element - The input element to infer information about.
 | 
						|
   * @param {Array<HTMLElement>} elements - See `getFathomField` for details
 | 
						|
   * @returns {Array} - An array containing:
 | 
						|
   *                    [0]the inferred field name
 | 
						|
   *                    [1]information collected during the inference process. The possible values includes:
 | 
						|
   *                       'autocompleteInfo', 'fathomLabel', and 'fathomConfidence'.
 | 
						|
   */
 | 
						|
  inferFieldInfo(element, elements = []) {
 | 
						|
    const inferredInfo = {};
 | 
						|
    const autocompleteInfo = element.getAutocompleteInfo();
 | 
						|
 | 
						|
    // An input[autocomplete="on"] will not be early return here since it stll
 | 
						|
    // needs to find the field name.
 | 
						|
    if (
 | 
						|
      autocompleteInfo?.fieldName &&
 | 
						|
      !["on", "off"].includes(autocompleteInfo.fieldName)
 | 
						|
    ) {
 | 
						|
      inferredInfo.autocompleteInfo = autocompleteInfo;
 | 
						|
      return [autocompleteInfo.fieldName, inferredInfo];
 | 
						|
    }
 | 
						|
 | 
						|
    const fields = this._getPossibleFieldNames(element);
 | 
						|
 | 
						|
    // "email" type of input is accurate for heuristics to determine its Email
 | 
						|
    // field or not. However, "tel" type is used for ZIP code for some web site
 | 
						|
    // (e.g. HomeDepot, BestBuy), so "tel" type should be not used for "tel"
 | 
						|
    // prediction.
 | 
						|
    if (element.type == "email" && fields.includes("email")) {
 | 
						|
      return ["email", inferredInfo];
 | 
						|
    }
 | 
						|
 | 
						|
    if (lazy.FormAutofillUtils.isFathomCreditCardsEnabled()) {
 | 
						|
      // We don't care fields that are not supported by fathom
 | 
						|
      const fathomFields = fields.filter(r =>
 | 
						|
        lazy.CreditCardRulesets.types.includes(r)
 | 
						|
      );
 | 
						|
      const [matchedFieldName, confidence] = this.getFathomField(
 | 
						|
        element,
 | 
						|
        fathomFields,
 | 
						|
        elements
 | 
						|
      );
 | 
						|
      if (confidence != null) {
 | 
						|
        inferredInfo.fathomLabel = matchedFieldName;
 | 
						|
        inferredInfo.fathomConfidence = confidence;
 | 
						|
      }
 | 
						|
      // At this point, use fathom's recommendation if it has one
 | 
						|
      if (matchedFieldName) {
 | 
						|
        return [matchedFieldName, inferredInfo];
 | 
						|
      }
 | 
						|
 | 
						|
      // Continue to run regex-based heuristics even when fathom doesn't recognize
 | 
						|
      // the field. Since the regex-based heuristic has good search coverage but
 | 
						|
      // has a worse precision. We use it in conjunction with fathom to maximize
 | 
						|
      // our search coverage. For example, when a <input> is not considered cc-name
 | 
						|
      // by fathom but is considered cc-name by regex-based heuristic, if the form
 | 
						|
      // also contains a cc-number identified by fathom, we will treat the form as a
 | 
						|
      // valid cc form; hence both cc-number & cc-name are identified.
 | 
						|
    }
 | 
						|
 | 
						|
    // Check every select for options that
 | 
						|
    // match credit card network names in value or label.
 | 
						|
    if (HTMLSelectElement.isInstance(element)) {
 | 
						|
      if (this._isExpirationMonthLikely(element)) {
 | 
						|
        return ["cc-exp-month", inferredInfo];
 | 
						|
      } else if (this._isExpirationYearLikely(element)) {
 | 
						|
        return ["cc-exp-year", inferredInfo];
 | 
						|
      }
 | 
						|
 | 
						|
      const options = Array.from(element.querySelectorAll("option"));
 | 
						|
      if (
 | 
						|
        options.find(
 | 
						|
          option =>
 | 
						|
            lazy.CreditCard.getNetworkFromName(option.value) ||
 | 
						|
            lazy.CreditCard.getNetworkFromName(option.text)
 | 
						|
        )
 | 
						|
      ) {
 | 
						|
        return ["cc-type", inferredInfo];
 | 
						|
      }
 | 
						|
 | 
						|
      // At least two options match the country name, otherwise some state name might
 | 
						|
      // also match a country name, ex, Georgia. We check the last two
 | 
						|
      // options rather than the first, as selects often start with a non-country display option.
 | 
						|
      const countryDisplayNames = Array.from(FormAutofill.countries.values());
 | 
						|
      if (
 | 
						|
        options.length >= 2 &&
 | 
						|
        options
 | 
						|
          .slice(-2)
 | 
						|
          .every(
 | 
						|
            option =>
 | 
						|
              countryDisplayNames.includes(option.value) ||
 | 
						|
              countryDisplayNames.includes(option.text)
 | 
						|
          )
 | 
						|
      ) {
 | 
						|
        return ["country", inferredInfo];
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Find a matched field name using regexp-based heuristics
 | 
						|
    const matchedFieldNames = this._findMatchedFieldNames(element, fields);
 | 
						|
 | 
						|
    return [matchedFieldNames, inferredInfo];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Using Fathom, say what kind of CC field an element is most likely to be.
 | 
						|
   * This function deoesn't only run fathom on the passed elements. It also
 | 
						|
   * runs fathom for all elements in the FieldScanner for optimization purpose.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} element
 | 
						|
   * @param {Array} fields
 | 
						|
   * @param {Array<HTMLElement>} elements - All other eligible elements in the same form. This is mainly used as an
 | 
						|
   *                                        optimization approach to run fathom model on all eligible elements
 | 
						|
   *                                        once instead of one by one
 | 
						|
   * @returns {Array} A tuple of [field name, probability] describing the
 | 
						|
   *   highest-confidence classification
 | 
						|
   */
 | 
						|
  getFathomField(element, fields, elements = []) {
 | 
						|
    if (!fields.length) {
 | 
						|
      return [null, null];
 | 
						|
    }
 | 
						|
 | 
						|
    if (!this._fathomConfidences?.get(element)) {
 | 
						|
      this._fathomConfidences = new Map();
 | 
						|
 | 
						|
      // This should not throw unless we run into an OOM situation, at which
 | 
						|
      // point we have worse problems and this failing is not a big deal.
 | 
						|
      elements = elements.includes(element) ? elements : [element];
 | 
						|
      const confidences = this.getFormAutofillConfidences(elements);
 | 
						|
 | 
						|
      for (let i = 0; i < elements.length; i++) {
 | 
						|
        this._fathomConfidences.set(elements[i], confidences[i]);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const elementConfidences = this._fathomConfidences.get(element);
 | 
						|
    if (!elementConfidences) {
 | 
						|
      return [null, null];
 | 
						|
    }
 | 
						|
 | 
						|
    let highestField = null;
 | 
						|
    let highestConfidence = lazy.FormAutofillUtils.ccFathomConfidenceThreshold; // Start with a threshold of 0.5
 | 
						|
    for (let [key, value] of Object.entries(elementConfidences)) {
 | 
						|
      if (!fields.includes(key)) {
 | 
						|
        // ignore field that we don't care
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      if (value > highestConfidence) {
 | 
						|
        highestConfidence = value;
 | 
						|
        highestField = key;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!highestField) {
 | 
						|
      return [null, null];
 | 
						|
    }
 | 
						|
 | 
						|
    // Used by test ONLY! This ensure testcases always get the same confidence
 | 
						|
    if (lazy.FormAutofillUtils.ccFathomTestConfidence > 0) {
 | 
						|
      highestConfidence = lazy.FormAutofillUtils.ccFathomTestConfidence;
 | 
						|
    }
 | 
						|
 | 
						|
    return [highestField, highestConfidence];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {Array} elements Array of elements that we want to get result from fathom cc rules
 | 
						|
   * @returns {object} Fathom confidence keyed by field-type.
 | 
						|
   */
 | 
						|
  getFormAutofillConfidences(elements) {
 | 
						|
    if (
 | 
						|
      lazy.FormAutofillUtils.ccHeuristicsMode ==
 | 
						|
      lazy.FormAutofillUtils.CC_FATHOM_NATIVE
 | 
						|
    ) {
 | 
						|
      const confidences = ChromeUtils.getFormAutofillConfidences(elements);
 | 
						|
      return confidences.map(c => {
 | 
						|
        let result = {};
 | 
						|
        for (let [fieldName, confidence] of Object.entries(c)) {
 | 
						|
          let type =
 | 
						|
            lazy.FormAutofillUtils.formAutofillConfidencesKeyToCCFieldType(
 | 
						|
              fieldName
 | 
						|
            );
 | 
						|
          result[type] = confidence;
 | 
						|
        }
 | 
						|
        return result;
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    return elements.map(element => {
 | 
						|
      /**
 | 
						|
       * Return how confident our ML model is that `element` is a field of the
 | 
						|
       * given type.
 | 
						|
       *
 | 
						|
       * @param {string} fieldName The Fathom type to check against. This is
 | 
						|
       *   conveniently the same as the autocomplete attribute value that means
 | 
						|
       *   the same thing.
 | 
						|
       * @returns {number} Confidence in range [0, 1]
 | 
						|
       */
 | 
						|
      function confidence(fieldName) {
 | 
						|
        const ruleset = lazy.CreditCardRulesets[fieldName];
 | 
						|
        const fnodes = ruleset.against(element).get(fieldName);
 | 
						|
 | 
						|
        // fnodes is either 0 or 1 item long, since we ran the ruleset
 | 
						|
        // against a single element:
 | 
						|
        return fnodes.length ? fnodes[0].scoreFor(fieldName) : 0;
 | 
						|
      }
 | 
						|
 | 
						|
      // Bang the element against the ruleset for every type of field:
 | 
						|
      const confidences = {};
 | 
						|
      lazy.CreditCardRulesets.types.map(fieldName => {
 | 
						|
        confidences[fieldName] = confidence(fieldName);
 | 
						|
      });
 | 
						|
 | 
						|
      return confidences;
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * @typedef ElementStrings
 | 
						|
   * @type {object}
 | 
						|
   * @yields {string} id - element id.
 | 
						|
   * @yields {string} name - element name.
 | 
						|
   * @yields {Array<string>} labels - extracted labels.
 | 
						|
   */
 | 
						|
 | 
						|
  /**
 | 
						|
   * Extract all the signature strings of an element.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} element
 | 
						|
   * @returns {Array<string>}
 | 
						|
   */
 | 
						|
  _getElementStrings(element) {
 | 
						|
    return [element.id, element.name, element.placeholder?.trim()];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Extract all the label strings associated with an element.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} element
 | 
						|
   * @returns {ElementStrings}
 | 
						|
   */
 | 
						|
  _getElementLabelStrings(element) {
 | 
						|
    return {
 | 
						|
      *[Symbol.iterator]() {
 | 
						|
        const labels = lazy.LabelUtils.findLabelElements(element);
 | 
						|
        for (let label of labels) {
 | 
						|
          yield* lazy.LabelUtils.extractLabelStrings(label);
 | 
						|
        }
 | 
						|
 | 
						|
        const ariaLabels = element.getAttribute("aria-label");
 | 
						|
        if (ariaLabels) {
 | 
						|
          yield* [ariaLabels];
 | 
						|
        }
 | 
						|
      },
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  // In order to support webkit we need to avoid usage of negative lookbehind due to low support
 | 
						|
  // First safari version with support is 16.4 (Release Date: 27th March 2023)
 | 
						|
  // https://caniuse.com/js-regexp-lookbehind
 | 
						|
  // We can mimic the behaviour of negative lookbehinds by using a named capture group
 | 
						|
  // (?<!not)word -> (?<neg>notword)|word
 | 
						|
  // TODO: Bug 1829583
 | 
						|
  testRegex(regex, string) {
 | 
						|
    const matches = string?.matchAll(regex);
 | 
						|
    if (!matches) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    const excludeNegativeCaptureGroups = [];
 | 
						|
 | 
						|
    for (const match of matches) {
 | 
						|
      excludeNegativeCaptureGroups.push(
 | 
						|
        ...match.filter(m => m !== match?.groups?.neg).filter(Boolean)
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return excludeNegativeCaptureGroups?.length > 0;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Find matching field names from a given list of field names
 | 
						|
   * that matches an HTML element.
 | 
						|
   *
 | 
						|
   * The function first tries to match the element against a set of
 | 
						|
   * pre-defined regular expression rules. If no match is found, it
 | 
						|
   * then checks for label-specific rules, if they exist.
 | 
						|
   *
 | 
						|
   * The return value can contain a maximum of two field names, the
 | 
						|
   * first item the first match found, and the second an alternate field
 | 
						|
   * name always of a different type, where the two type are credit card
 | 
						|
   * and address.
 | 
						|
   *
 | 
						|
   * Note: For label rules, the keyword is often more general
 | 
						|
   * (e.g., "^\\W*address"), hence they are only searched within labels
 | 
						|
   * to reduce the occurrence of false positives.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} element The element to match.
 | 
						|
   * @param {Array<string>} fieldNames An array of field names to compare against.
 | 
						|
   * @returns {Array} An array of the matching field names.
 | 
						|
   */
 | 
						|
  _findMatchedFieldNames(element, fieldNames) {
 | 
						|
    if (!fieldNames.length) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    // The first element is the field name, and the second element is the type.
 | 
						|
    let fields = fieldNames.map(name => [
 | 
						|
      name,
 | 
						|
      lazy.FormAutofillUtils.isCreditCardField(name) ? CC_TYPE : ADDR_TYPE,
 | 
						|
    ]);
 | 
						|
 | 
						|
    let foundType;
 | 
						|
    let attribute = true;
 | 
						|
    let matchedFieldNames = [];
 | 
						|
 | 
						|
    // Check RULES first, and only check LABEL_RULES if no match is found.
 | 
						|
    for (let rules of [this.RULES, this.LABEL_RULES]) {
 | 
						|
      // Attempt to match the element against the default set of rules.
 | 
						|
      if (
 | 
						|
        fields.find(field => {
 | 
						|
          const [fieldName, type] = field;
 | 
						|
 | 
						|
          // The same type has been found already, so skip.
 | 
						|
          if (foundType == type) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
 | 
						|
          if (!this._matchRegexp(element, rules[fieldName], { attribute })) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
 | 
						|
          foundType = type;
 | 
						|
          matchedFieldNames.push(fieldName);
 | 
						|
 | 
						|
          return matchedFieldNames.length == 2;
 | 
						|
        })
 | 
						|
      ) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
 | 
						|
      // Don't match attributes for label rules.
 | 
						|
      attribute = false;
 | 
						|
    }
 | 
						|
 | 
						|
    return matchedFieldNames;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Determine whether the regexp can match any of element strings.
 | 
						|
   *
 | 
						|
   * @param {HTMLElement} element The HTML element to match.
 | 
						|
   * @param {RegExp} regexp       The regular expression to match against.
 | 
						|
   * @param {object} [options]    Optional parameters for matching.
 | 
						|
   * @param {boolean} [options.attribute=true]
 | 
						|
   *                              Whether to match against the element's attributes.
 | 
						|
   * @param {boolean} [options.label=true]
 | 
						|
   *                              Whether to match against the element's labels.
 | 
						|
   * @returns {boolean} True if a match is found, otherwise false.
 | 
						|
   */
 | 
						|
  _matchRegexp(element, regexp, { attribute = true, label = true } = {}) {
 | 
						|
    if (!regexp) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (attribute) {
 | 
						|
      const elemStrings = this._getElementStrings(element);
 | 
						|
      if (elemStrings.find(s => this.testRegex(regexp, s?.toLowerCase()))) {
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (label) {
 | 
						|
      const elementLabelStrings = this._getElementLabelStrings(element);
 | 
						|
      for (const s of elementLabelStrings) {
 | 
						|
        if (this.testRegex(regexp, s?.toLowerCase())) {
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Phone field grammars - first matched grammar will be parsed. Grammars are
 | 
						|
   * separated by { REGEX_SEPARATOR, FIELD_NONE, 0 }. Suffix and extension are
 | 
						|
   * parsed separately unless they are necessary parts of the match.
 | 
						|
   * The following notation is used to describe the patterns:
 | 
						|
   * <cc> - country code field.
 | 
						|
   * <ac> - area code field.
 | 
						|
   * <phone> - phone or prefix.
 | 
						|
   * <suffix> - suffix.
 | 
						|
   * <ext> - extension.
 | 
						|
   * :N means field is limited to N characters, otherwise it is unlimited.
 | 
						|
   * (pattern <field>)? means pattern is optional and matched separately.
 | 
						|
   *
 | 
						|
   * This grammar list from Chromium will be enabled partially once we need to
 | 
						|
   * support more cases of Telephone fields.
 | 
						|
   */
 | 
						|
  PHONE_FIELD_GRAMMARS: [
 | 
						|
    // Country code: <cc> Area Code: <ac> Phone: <phone> (- <suffix>
 | 
						|
 | 
						|
    // (Ext: <ext>)?)?
 | 
						|
    // {REGEX_COUNTRY, FIELD_COUNTRY_CODE, 0},
 | 
						|
    // {REGEX_AREA, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PHONE, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // \( <ac> \) <phone>:3 <suffix>:4 (Ext: <ext>)?
 | 
						|
    // {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 3},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 3},
 | 
						|
    // {REGEX_PHONE, FIELD_SUFFIX, 4},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <cc> <ac>:3 - <phone>:3 - <suffix>:4 (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
 | 
						|
    // {REGEX_PHONE, FIELD_AREA_CODE, 3},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 3},
 | 
						|
    // {REGEX_SUFFIX_SEPARATOR, FIELD_SUFFIX, 4},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <cc>:3 <ac>:3 <phone>:3 <suffix>:4 (Ext: <ext>)?
 | 
						|
    ["tel", "tel-country-code", 3],
 | 
						|
    ["tel", "tel-area-code", 3],
 | 
						|
    ["tel", "tel-local-prefix", 3],
 | 
						|
    ["tel", "tel-local-suffix", 4],
 | 
						|
    [null, null, 0],
 | 
						|
 | 
						|
    // Area Code: <ac> Phone: <phone> (- <suffix> (Ext: <ext>)?)?
 | 
						|
    // {REGEX_AREA, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PHONE, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <ac> <phone>:3 <suffix>:4 (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PHONE, FIELD_PHONE, 3},
 | 
						|
    // {REGEX_PHONE, FIELD_SUFFIX, 4},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <cc> \( <ac> \) <phone> (- <suffix> (Ext: <ext>)?)?
 | 
						|
    // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
 | 
						|
    // {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: \( <ac> \) <phone> (- <suffix> (Ext: <ext>)?)?
 | 
						|
    // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
 | 
						|
    // {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <cc> - <ac> - <phone> - <suffix> (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SUFFIX_SEPARATOR, FIELD_SUFFIX, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Area code: <ac>:3 Prefix: <prefix>:3 Suffix: <suffix>:4 (Ext: <ext>)?
 | 
						|
    // {REGEX_AREA, FIELD_AREA_CODE, 3},
 | 
						|
    // {REGEX_PREFIX, FIELD_PHONE, 3},
 | 
						|
    // {REGEX_SUFFIX, FIELD_SUFFIX, 4},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <ac> Prefix: <phone> Suffix: <suffix> (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PREFIX, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SUFFIX, FIELD_SUFFIX, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <ac> - <phone>:3 - <suffix>:4 (Ext: <ext>)?
 | 
						|
    ["tel", "tel-area-code", 0],
 | 
						|
    ["tel", "tel-local-prefix", 3],
 | 
						|
    ["tel", "tel-local-suffix", 4],
 | 
						|
    [null, null, 0],
 | 
						|
 | 
						|
    // Phone: <cc> - <ac> - <phone> (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
 | 
						|
    // {REGEX_PREFIX_SEPARATOR, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_SUFFIX_SEPARATOR, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <ac> - <phone> (Ext: <ext>)?
 | 
						|
    // {REGEX_AREA, FIELD_AREA_CODE, 0},
 | 
						|
    // {REGEX_PHONE, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <cc>:3 - <phone>:10 (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_COUNTRY_CODE, 3},
 | 
						|
    // {REGEX_PHONE, FIELD_PHONE, 10},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Ext: <ext>
 | 
						|
    // {REGEX_EXTENSION, FIELD_EXTENSION, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
 | 
						|
    // Phone: <phone> (Ext: <ext>)?
 | 
						|
    // {REGEX_PHONE, FIELD_PHONE, 0},
 | 
						|
    // {REGEX_SEPARATOR, FIELD_NONE, 0},
 | 
						|
  ],
 | 
						|
};
 | 
						|
 | 
						|
ChromeUtils.defineLazyGetter(
 | 
						|
  FormAutofillHeuristics,
 | 
						|
  "CREDIT_CARD_FIELDNAMES",
 | 
						|
  () =>
 | 
						|
    Object.keys(FormAutofillHeuristics.RULES).filter(name =>
 | 
						|
      lazy.FormAutofillUtils.isCreditCardField(name)
 | 
						|
    )
 | 
						|
);
 | 
						|
 | 
						|
ChromeUtils.defineLazyGetter(FormAutofillHeuristics, "ADDRESS_FIELDNAMES", () =>
 | 
						|
  Object.keys(FormAutofillHeuristics.RULES).filter(name =>
 | 
						|
    lazy.FormAutofillUtils.isAddressField(name)
 | 
						|
  )
 | 
						|
);
 | 
						|
 | 
						|
export default FormAutofillHeuristics;
 |