summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/shared
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/formautofill/shared
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill/shared')
-rw-r--r--toolkit/components/formautofill/shared/AddressComponent.sys.mjs1120
-rw-r--r--toolkit/components/formautofill/shared/AddressMetaData.sys.mjs2451
-rw-r--r--toolkit/components/formautofill/shared/AddressMetaDataExtension.sys.mjs765
-rw-r--r--toolkit/components/formautofill/shared/AddressMetaDataLoader.sys.mjs168
-rw-r--r--toolkit/components/formautofill/shared/AddressParser.sys.mjs285
-rw-r--r--toolkit/components/formautofill/shared/CreditCardRecord.sys.mjs66
-rw-r--r--toolkit/components/formautofill/shared/CreditCardRuleset.sys.mjs1221
-rw-r--r--toolkit/components/formautofill/shared/FieldScanner.sys.mjs224
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillHandler.sys.mjs411
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs1213
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillNameUtils.sys.mjs406
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs1292
-rw-r--r--toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs1129
-rw-r--r--toolkit/components/formautofill/shared/FormStateManager.sys.mjs157
-rw-r--r--toolkit/components/formautofill/shared/HeuristicsRegExp.sys.mjs687
-rw-r--r--toolkit/components/formautofill/shared/LabelUtils.sys.mjs120
16 files changed, 11715 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/shared/AddressComponent.sys.mjs b/toolkit/components/formautofill/shared/AddressComponent.sys.mjs
new file mode 100644
index 0000000000..a849e889b2
--- /dev/null
+++ b/toolkit/components/formautofill/shared/AddressComponent.sys.mjs
@@ -0,0 +1,1120 @@
+/* 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";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddressParser: "resource://gre/modules/shared/AddressParser.sys.mjs",
+ FormAutofillNameUtils:
+ "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ PhoneNumber: "resource://autofill/phonenumberutils/PhoneNumber.sys.mjs",
+ PhoneNumberNormalizer:
+ "resource://autofill/phonenumberutils/PhoneNumberNormalizer.sys.mjs",
+});
+
+/**
+ * Class representing a collection of tokens extracted from a string.
+ */
+class Tokens {
+ #tokens = null;
+
+ // By default we split passed string with whitespace.
+ constructor(value, sep = /\s+/) {
+ this.#tokens = value.split(sep);
+ }
+
+ get tokens() {
+ return this.#tokens;
+ }
+
+ /**
+ * Checks if all the tokens in the current object can be found in another
+ * token object.
+ *
+ * @param {Tokens} other The other Tokens instance to compare with.
+ * @param {Function} compare An optional custom comparison function.
+ * @returns {boolean} True if the current Token object is a subset of the
+ * other Token object, false otherwise.
+ */
+ isSubset(other, compare = (a, b) => a == b) {
+ return this.tokens.every(tokenSelf => {
+ for (const tokenOther of other.tokens) {
+ if (compare(tokenSelf, tokenOther)) {
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+
+ /**
+ * Checks if all the tokens in the current object can be found in another
+ * Token object's tokens (in order).
+ * For example, ["John", "Doe"] is a subset of ["John", "Michael", "Doe"]
+ * in order but not a subset of ["Doe", "Michael", "John"] in order.
+ *
+ * @param {Tokens} other The other Tokens instance to compare with.
+ * @param {Function} compare An optional custom comparison function.
+ * @returns {boolean} True if the current Token object is a subset of the
+ * other Token object, false otherwise.
+ */
+ isSubsetInOrder(other, compare = (a, b) => a == b) {
+ if (this.tokens.length > other.tokens.length) {
+ return false;
+ }
+
+ let idx = 0;
+ return this.tokens.every(tokenSelf => {
+ for (; idx < other.tokens.length; idx++) {
+ if (compare(tokenSelf, other.tokens[idx])) {
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+}
+
+/**
+ * The AddressField class is a base class representing a single address field.
+ */
+class AddressField {
+ #userValue = null;
+
+ #region = null;
+
+ /**
+ * Create a representation of a single address field.
+ *
+ * @param {string} value
+ * The unnormalized value of an address field.
+ *
+ * @param {string} region
+ * The region of a single address field. Used to determine what collator should be
+ * for string comparisons of the address's field value.
+ */
+ constructor(value, region) {
+ this.#userValue = value?.trim();
+ this.#region = region;
+ }
+
+ /**
+ * Get the unnormalized value of the address field.
+ *
+ * @returns {string} The unnormalized field value.
+ */
+ get userValue() {
+ return this.#userValue;
+ }
+
+ /**
+ * Get the collator used for string comparisons.
+ *
+ * @returns {Intl.Collator} The collator.
+ */
+ get collator() {
+ return lazy.FormAutofillUtils.getSearchCollators(this.#region, {
+ ignorePunctuation: false,
+ });
+ }
+
+ get region() {
+ return this.#region;
+ }
+
+ /**
+ * Compares two strings using the collator.
+ *
+ * @param {string} a The first string to compare.
+ * @param {string} b The second string to compare.
+ * @returns {number} A negative, zero, or positive value, depending on the comparison result.
+ */
+ localeCompare(a, b) {
+ return lazy.FormAutofillUtils.strCompare(a, b, this.collator);
+ }
+
+ /**
+ * Checks if the field value is empty.
+ *
+ * @returns {boolean} True if the field value is empty, false otherwise.
+ */
+ isEmpty() {
+ return !this.#userValue;
+ }
+
+ /**
+ * Normalizes the unnormalized field value using the provided options.
+ *
+ * @param {object} options - Options for normalization.
+ * @returns {string} The normalized field value.
+ */
+ normalizeUserValue(options) {
+ return lazy.AddressParser.normalizeString(this.#userValue, options);
+ }
+
+ /**
+ * Returns a string representation of the address field.
+ * Ex. "Country: US", "PostalCode: 55123", etc.
+ */
+ toString() {
+ return `${this.constructor.name}: ${this.#userValue}\n`;
+ }
+
+ /**
+ * Checks if the field value is valid.
+ *
+ * @returns {boolean} True if the field value is valid, false otherwise.
+ */
+ isValid() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /**
+ * Compares the current field value with another field value for equality.
+ */
+ equals() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /**
+ * Checks if the current field value contains another field value.
+ */
+ contains() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+}
+
+/**
+ * A street address.
+ * See autocomplete="street-address".
+ */
+class StreetAddress extends AddressField {
+ static ac = "street-address";
+
+ #structuredStreetAddress = null;
+
+ constructor(value, region) {
+ super(value, region);
+
+ this.#structuredStreetAddress = lazy.AddressParser.parseStreetAddress(
+ lazy.AddressParser.replaceControlCharacters(this.userValue, " ")
+ );
+ }
+
+ get structuredStreetAddress() {
+ return this.#structuredStreetAddress;
+ }
+ get street_number() {
+ return this.#structuredStreetAddress?.street_number;
+ }
+ get street_name() {
+ return this.#structuredStreetAddress?.street_name;
+ }
+ get floor_number() {
+ return this.#structuredStreetAddress?.floor_number;
+ }
+ get apartment_number() {
+ return this.#structuredStreetAddress?.apartment_number;
+ }
+
+ isValid() {
+ return this.userValue ? !!/[\p{Letter}]/u.exec(this.userValue) : true;
+ }
+
+ equals(other) {
+ if (this.structuredStreetAddress && other.structuredStreetAddress) {
+ return (
+ this.street_number?.toLowerCase() ==
+ other.street_number?.toLowerCase() &&
+ this.street_name?.toLowerCase() == other.street_name?.toLowerCase() &&
+ this.apartment_number?.toLowerCase() ==
+ other.apartment_number?.toLowerCase() &&
+ this.floor_number?.toLowerCase() == other.floor_number?.toLowerCase()
+ );
+ }
+
+ const options = {
+ ignore_case: true,
+ };
+
+ return (
+ this.normalizeUserValue(options) == other.normalizeUserValue(options)
+ );
+ }
+
+ contains(other) {
+ let selfStreetName = this.userValue;
+ let otherStreetName = other.userValue;
+
+ // Compare street number, apartment number and floor number if
+ // both addresses are parsed successfully.
+ if (this.structuredStreetAddress && other.structuredStreetAddress) {
+ if (
+ (other.street_number && this.street_number != other.street_number) ||
+ (other.apartment_number &&
+ this.apartment_number != other.apartment_number) ||
+ (other.floor_number && this.floor_number != other.floor_number)
+ ) {
+ return false;
+ }
+
+ // Use parsed street name to compare
+ selfStreetName = this.street_name;
+ otherStreetName = other.street_name;
+ }
+
+ // Check if one street name contains the other
+ const options = {
+ ignore_case: true,
+ replace_punctuation: " ",
+ };
+ const selfTokens = new Tokens(
+ lazy.AddressParser.normalizeString(selfStreetName, options),
+ /[\s\n\r]+/
+ );
+ const otherTokens = new Tokens(
+ lazy.AddressParser.normalizeString(otherStreetName, options),
+ /[\s\n\r]+/
+ );
+
+ return otherTokens.isSubsetInOrder(selfTokens, (a, b) =>
+ this.localeCompare(a, b)
+ );
+ }
+
+ static fromRecord(record, region) {
+ return new StreetAddress(record[StreetAddress.ac], region);
+ }
+}
+
+/**
+ * A postal code / zip code
+ * See autocomplete="postal-code"
+ */
+class PostalCode extends AddressField {
+ static ac = "postal-code";
+
+ constructor(value, region) {
+ super(value, region);
+ }
+
+ isValid() {
+ const { postalCodePattern } = lazy.FormAutofillUtils.getFormFormat(
+ this.region
+ );
+ const regexp = new RegExp(`^${postalCodePattern}$`);
+ return regexp.test(this.userValue);
+ }
+
+ equals(other) {
+ const options = {
+ ignore_case: true,
+ remove_whitespace: true,
+ remove_punctuation: true,
+ };
+
+ return (
+ this.normalizeUserValue(options) == other.normalizeUserValue(options)
+ );
+ }
+
+ contains(other) {
+ const options = {
+ ignore_case: true,
+ remove_whitespace: true,
+ remove_punctuation: true,
+ };
+
+ const self_normalized_value = this.normalizeUserValue(options);
+ const other_normalized_value = other.normalizeUserValue(options);
+
+ return (
+ self_normalized_value.endsWith(other_normalized_value) ||
+ self_normalized_value.startsWith(other_normalized_value)
+ );
+ }
+
+ static fromRecord(record, region) {
+ return new PostalCode(record[PostalCode.ac], region);
+ }
+}
+
+/**
+ * City name.
+ * See autocomplete="address-level2"
+ */
+class City extends AddressField {
+ static ac = "address-level2";
+
+ #city = null;
+
+ constructor(value, region) {
+ super(value, region);
+
+ const options = {
+ ignore_case: true,
+ };
+ this.#city = this.normalizeUserValue(options);
+ }
+
+ get city() {
+ return this.#city;
+ }
+
+ isValid() {
+ return this.userValue ? !!/[\p{Letter}]/u.exec(this.userValue) : true;
+ }
+
+ equals(other) {
+ return this.city == other.city;
+ }
+
+ contains(other) {
+ const options = {
+ ignore_case: true,
+ replace_punctuation: " ",
+ merge_whitespace: true,
+ };
+
+ const selfTokens = new Tokens(this.normalizeUserValue(options));
+ const otherTokens = new Tokens(other.normalizeUserValue(options));
+
+ return otherTokens.isSubsetInOrder(selfTokens, (a, b) =>
+ this.localeCompare(a, b)
+ );
+ }
+
+ static fromRecord(record, region) {
+ return new City(record[City.ac], region);
+ }
+}
+
+/**
+ * State.
+ * See autocomplete="address-level1"
+ */
+class State extends AddressField {
+ static ac = "address-level1";
+
+ // The abbreviated region name. For example, California is abbreviated as CA
+ #state = null;
+
+ constructor(value, region) {
+ super(value, region);
+
+ if (!this.userValue) {
+ return;
+ }
+
+ const options = {
+ merge_whitespace: true,
+ remove_punctuation: true,
+ };
+ this.#state = lazy.FormAutofillUtils.getAbbreviatedSubregionName(
+ this.normalizeUserValue(options),
+ region
+ );
+ }
+
+ get state() {
+ return this.#state;
+ }
+
+ isValid() {
+ // If we can't get the abbreviated name, assume this is an invalid state name
+ return !!this.#state;
+ }
+
+ equals(other) {
+ // If we have an abbreviated name, compare with it.
+ if (this.state) {
+ return this.state == other.state;
+ }
+
+ // If we don't have an abbreviated name, just compare the userValue
+ return this.userValue == other.userValue;
+ }
+
+ contains(other) {
+ return this.equals(other);
+ }
+
+ static fromRecord(record, region) {
+ return new State(record[State.ac], region);
+ }
+}
+
+/**
+ * A country or territory code.
+ * See autocomplete="country"
+ */
+class Country extends AddressField {
+ static ac = "country";
+
+ // iso 3166 2-alpha code
+ #country_code = null;
+
+ constructor(value, region) {
+ super(value, region);
+
+ if (this.isEmpty()) {
+ return;
+ }
+
+ const options = {
+ merge_whitespace: true,
+ remove_punctuation: true,
+ };
+
+ const country = this.normalizeUserValue(options);
+ this.#country_code = lazy.FormAutofillUtils.identifyCountryCode(country);
+
+ // When the country name is not a valid one, we use the current region instead
+ if (!this.#country_code) {
+ this.#country_code = lazy.FormAutofillUtils.identifyCountryCode(region);
+ }
+ }
+
+ get country_code() {
+ return this.#country_code;
+ }
+
+ isValid() {
+ return !!this.#country_code;
+ }
+
+ equals(other) {
+ return this.country_code == other.country_code;
+ }
+
+ contains(other) {
+ return false;
+ }
+
+ static fromRecord(record, region) {
+ return new Country(record[Country.ac], region);
+ }
+}
+
+/**
+ * The field expects the value to be a person's full name.
+ * See autocomplete="name"
+ */
+class Name extends AddressField {
+ static ac = "name";
+
+ constructor(value, region) {
+ super(value, region);
+ }
+
+ // Reference:
+ // https://source.chromium.org/chromium/chromium/src/+/main:components/autofill/core/browser/data_model/autofill_profile_comparator.cc;drc=566369da19275cc306eeb51a3d3451885299dabb;bpv=1;bpt=1;l=935
+ static createNameVariants(name) {
+ let tokens = name.trim().split(" ");
+
+ let variants = [""];
+ if (!tokens[0]) {
+ return variants;
+ }
+
+ for (const token of tokens) {
+ let tmp = [];
+ for (const variant of variants) {
+ tmp.push(variant + " " + token);
+ tmp.push(variant + " " + token[0]);
+ }
+ variants = variants.concat(tmp);
+ }
+
+ const options = {
+ merge_whitespace: true,
+ };
+ return variants.map(v => lazy.AddressParser.normalizeString(v, options));
+ }
+
+ isValid() {
+ return this.userValue ? !!/[\p{Letter}]/u.exec(this.userValue) : true;
+ }
+
+ equals(other) {
+ const options = {
+ ignore_case: true,
+ };
+ return (
+ this.normalizeUserValue(options) == other.normalizeUserValue(options)
+ );
+ }
+
+ contains(other) {
+ // Unify puncutation while comparing so users can choose the right one
+ // if the only different part is puncutation
+ // Ex. John O'Brian is similar to John O`Brian
+ let options = {
+ ignore_case: true,
+ replace_punctuation: " ",
+ merge_whitespace: true,
+ };
+ let selfName = this.normalizeUserValue(options);
+ let otherName = other.normalizeUserValue(options);
+ let selfTokens = new Tokens(selfName);
+ let otherTokens = new Tokens(otherName);
+
+ if (
+ otherTokens.isSubsetInOrder(selfTokens, (a, b) =>
+ this.localeCompare(a, b)
+ )
+ ) {
+ return true;
+ }
+
+ // Remove puncutation from self and test whether current contains other
+ // Ex. John O'Brian is similar to John OBrian
+ selfName = this.normalizeUserValue({
+ ignore_case: true,
+ remove_punctuation: true,
+ merge_whitespace: true,
+ });
+ otherName = other.normalizeUserValue({
+ ignore_case: true,
+ remove_punctuation: true,
+ merge_whitespace: true,
+ });
+
+ selfTokens = new Tokens(selfName);
+ otherTokens = new Tokens(otherName);
+ if (
+ otherTokens.isSubsetInOrder(selfTokens, (a, b) =>
+ this.localeCompare(a, b)
+ )
+ ) {
+ return true;
+ }
+
+ // Create variants of the names by generating initials for given and middle names.
+
+ selfName = lazy.FormAutofillNameUtils.splitName(selfName);
+ otherName = lazy.FormAutofillNameUtils.splitName(otherName);
+ // In the following we compare cases when people abbreviate first name
+ // and middle name with initials. So if family name is different,
+ // we can just skip and assume the two names are different
+ if (!this.localeCompare(selfName.family, otherName.family)) {
+ return false;
+ }
+
+ const otherNameWithoutFamily = lazy.FormAutofillNameUtils.joinNameParts({
+ given: otherName.given,
+ middle: otherName.middle,
+ });
+ let givenVariants = Name.createNameVariants(selfName.given);
+ let middleVariants = Name.createNameVariants(selfName.middle);
+
+ for (const given of givenVariants) {
+ for (const middle of middleVariants) {
+ const nameVariant = lazy.FormAutofillNameUtils.joinNameParts({
+ given,
+ middle,
+ });
+
+ if (this.localeCompare(nameVariant, otherNameWithoutFamily)) {
+ return true;
+ }
+ }
+ }
+
+ // Check cases when given name and middle name are abbreviated with initial
+ // and the initials are put together. ex. John Michael Doe to JM. Doe
+ if (selfName.given && selfName.middle) {
+ const nameVariant = [
+ ...selfName.given.split(" "),
+ ...selfName.middle.split(" "),
+ ].reduce((initials, name) => {
+ initials += name[0];
+ return initials;
+ }, "");
+
+ if (this.localeCompare(nameVariant, otherNameWithoutFamily)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static fromRecord(record, region) {
+ return new Name(record[Name.ac], region);
+ }
+}
+
+/**
+ * A full telephone number, including the country code.
+ * See autocomplete="tel"
+ */
+class Tel extends AddressField {
+ static ac = "tel";
+
+ #valid = false;
+
+ // The country code part of a telphone number, such as "1" for the United States
+ #country_code = null;
+
+ // The national part of a telphone number. For example, the phone number "+1 520-248-6621"
+ // national part is "520-248-6621".
+ #national_number = null;
+
+ constructor(value, region) {
+ super(value, region);
+
+ if (!this.userValue) {
+ return;
+ }
+
+ // TODO: Support parse telephone extension
+ // We compress all tel-related fields into a single tel field when an an form
+ // is submitted, so we need to decompress it here.
+ const parsed_tel = lazy.PhoneNumber.Parse(this.userValue, region);
+ if (parsed_tel) {
+ this.#national_number = parsed_tel?.nationalNumber;
+ this.#country_code = parsed_tel?.countryCode;
+
+ this.#valid = true;
+ } else {
+ this.#national_number = lazy.PhoneNumberNormalizer.Normalize(
+ this.userValue
+ );
+
+ const md = lazy.PhoneNumber.FindMetaDataForRegion(region);
+ this.#country_code = md ? "+" + md.nationalPrefix : null;
+
+ this.#valid = lazy.PhoneNumber.IsValid(this.#national_number, md);
+ }
+ }
+
+ get country_code() {
+ return this.#country_code;
+ }
+
+ get national_number() {
+ return this.#national_number;
+ }
+
+ isValid() {
+ return this.#valid;
+ }
+
+ equals(other) {
+ return (
+ this.national_number == other.national_number &&
+ this.country_code == other.country_code
+ );
+ }
+
+ contains(other) {
+ if (!this.country_code || this.country_code != other.country_code) {
+ return false;
+ }
+
+ return this.national_number.endsWith(other.national_number);
+ }
+
+ toString() {
+ return `${this.constructor.name}: ${this.country_code} ${this.national_number}\n`;
+ }
+
+ static fromRecord(record, region) {
+ return new Tel(record[Tel.ac], region);
+ }
+}
+
+/**
+ * A company or organization name.
+ * See autocomplete="organization".
+ */
+class Organization extends AddressField {
+ static ac = "organization";
+
+ constructor(value, region) {
+ super(value, region);
+ }
+
+ isValid() {
+ return this.userValue
+ ? !!/[\p{Letter}\p{Number}]/u.exec(this.userValue)
+ : true;
+ }
+
+ /**
+ * Two company names are considered equal only when everything is the same.
+ */
+ equals(other) {
+ return this.userValue == other.userValue;
+ }
+
+ // Mergeable use locale compare
+ contains(other) {
+ const options = {
+ replace_punctuation: " ", // mozilla org vs mozilla-org
+ merge_whitespace: true,
+ ignore_case: true, // mozilla vs Mozilla
+ };
+
+ // If every token in B can be found in A without considering order
+ // Example, 'Food & Pharmacy' contains 'Pharmacy & Food'
+ const selfTokens = new Tokens(this.normalizeUserValue(options));
+ const otherTokens = new Tokens(other.normalizeUserValue(options));
+
+ return otherTokens.isSubset(selfTokens, (a, b) => this.localeCompare(a, b));
+ }
+
+ static fromRecord(record, region) {
+ return new Organization(record[Organization.ac], region);
+ }
+}
+
+/**
+ * An email address
+ * See autocomplete="email".
+ */
+class Email extends AddressField {
+ static ac = "email";
+
+ constructor(value, region) {
+ super(value, region);
+ }
+
+ // Since we are using the valid check to determine whether we capture the email field when users submitting a forma,
+ // use a less restrict email verification method so we capture an email for most of the cases.
+ // The current algorithm is based on the regular expression defined in
+ // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ //
+ // We might also change this to something similar to the algorithm used in
+ // EmailInputType::IsValidEmailAddress if we want a more strict email validation algorithm.
+ isValid() {
+ const regex =
+ /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+ const match = this.userValue.match(regex);
+ if (!match) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /*
+ // JS version of EmailInputType::IsValidEmailAddress
+ isValid() {
+ const regex = /^([a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+)@([a-zA-Z0-9-]+\.[a-zA-Z]{2,})$/;
+ const match = this.userValue.match(regex);
+ if (!match) {
+ return false;
+ }
+ const local = match[1];
+ const domain = match[2];
+
+ // The domain name can't begin with a dot or a dash.
+ if (['-', '.'].includes(domain[0])) {
+ return false;
+ }
+
+ // A dot can't follow a dot or a dash.
+ // A dash can't follow a dot.
+ const pattern = /(\.\.)|(\.-)|(-\.)/;
+ if (pattern.test(domain)) {
+ return false;
+ }
+
+ return true;
+ }
+*/
+
+ equals(other) {
+ const options = {
+ ignore_case: true,
+ };
+
+ // email is case-insenstive
+ return (
+ this.normalizeUserValue(options) == other.normalizeUserValue(options)
+ );
+ }
+
+ contains(other) {
+ return false;
+ }
+
+ static fromRecord(record, region) {
+ return new Email(record[Email.ac], region);
+ }
+}
+
+/**
+ * The AddressComparison class compares two AddressComponent instances and
+ * provides information about the differences or similarities between them.
+ *
+ * The comparison result is stored and the object and can be retrieved by calling
+ * 'result' getter.
+ */
+export class AddressComparison {
+ // Const to define the comparison result for two address fields
+ static BOTH_EMPTY = 0;
+ static A_IS_EMPTY = 1;
+ static B_IS_EMPTY = 2;
+ static A_CONTAINS_B = 3;
+ static B_CONTAINS_A = 4;
+ // When A contains B and B contains A Ex. "Pizza & Food vs Food & Pizza"
+ static SIMILAR = 5;
+ static SAME = 6;
+ static DIFFERENT = 7;
+
+ // The comparion result, keyed by field name.
+ #result = {};
+
+ /**
+ * Constructs AddressComparison by comparing two AddressComponent objects.
+ *
+ * @class
+ * @param {AddressComponent} addressA - The first address to compare.
+ * @param {AddressComponent} addressB - The second address to compare.
+ */
+ constructor(addressA, addressB) {
+ for (const fieldA of addressA.getAllFields()) {
+ const fieldName = fieldA.constructor.ac;
+ const fieldB = addressB.getField(fieldName);
+ if (fieldB) {
+ this.#result[fieldName] = AddressComparison.compare(fieldA, fieldB);
+ } else {
+ this.#result[fieldName] = AddressComparison.B_IS_EMPTY;
+ }
+ }
+
+ for (const fieldB of addressB.getAllFields()) {
+ const fieldName = fieldB.constructor.ac;
+ if (!addressB.getField(fieldName)) {
+ this.#result[fieldName] = AddressComparison.A_IS_EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Retrieves the result object containing the comparison results.
+ *
+ * @returns {object} The result object with keys corresponding to field names
+ * and values being comparison constants.
+ */
+ get result() {
+ return this.#result;
+ }
+
+ /**
+ * Compares two address fields and returns the comparison result.
+ *
+ * @param {AddressField} fieldA The first field to compare.
+ * @param {AddressField} fieldB The second field to compare.
+ * @returns {number} A constant representing the comparison result.
+ */
+ static compare(fieldA, fieldB) {
+ if (fieldA.isEmpty()) {
+ return fieldB.isEmpty()
+ ? AddressComparison.BOTH_EMPTY
+ : AddressComparison.A_IS_EMPTY;
+ } else if (fieldB.isEmpty()) {
+ return AddressComparison.B_IS_EMPTY;
+ }
+
+ if (fieldA.equals(fieldB)) {
+ return AddressComparison.SAME;
+ }
+
+ if (fieldB.contains(fieldA)) {
+ if (fieldA.contains(fieldB)) {
+ return AddressComparison.SIMILAR;
+ }
+ return AddressComparison.B_CONTAINS_A;
+ } else if (fieldA.contains(fieldB)) {
+ return AddressComparison.A_CONTAINS_B;
+ }
+
+ return AddressComparison.DIFFERENT;
+ }
+
+ /**
+ * Converts a comparison result constant to a readable string.
+ *
+ * @param {number} result The comparison result constant.
+ * @returns {string} A readable string representing the comparison result.
+ */
+ static resultToString(result) {
+ switch (result) {
+ case AddressComparison.BOTH_EMPTY:
+ return "both fields are empty";
+ case AddressComparison.A_IS_EMPTY:
+ return "field A is empty";
+ case AddressComparison.B_IS_EMPTY:
+ return "field B is empty";
+ case AddressComparison.A_CONTAINS_B:
+ return "field A contains field B";
+ case AddressComparison.B_CONTAINS_B:
+ return "field B contains field A";
+ case AddressComparison.SIMILAR:
+ return "field A and field B are similar";
+ case AddressComparison.SAME:
+ return "two fields are the same";
+ case AddressComparison.DIFFERENT:
+ return "two fields are different";
+ }
+ return "";
+ }
+
+ /**
+ * Returns a formatted string representing the comparison results for each field.
+ *
+ * @returns {string} A formatted string with field names and their respective
+ * comparison results.
+ */
+ toString() {
+ let string = "Comparison Result:\n";
+ for (const [name, result] of Object.entries(this.#result)) {
+ string += `${name}: ${AddressComparison.resultToString(result)}\n`;
+ }
+ return string;
+ }
+}
+
+/**
+ * The AddressComponent class represents a structured address that is transformed
+ * from address record created in FormAutofillHandler 'createRecord' function.
+ *
+ * An AddressComponent object consisting of various fields such as state, city,
+ * country, postal code, etc. The class provides a compare methods
+ * to compare another AddressComponent against the current instance.
+ *
+ * Note. This class assumes records that pass to it have already been normalized.
+ */
+export class AddressComponent {
+ /**
+ * An object that stores individual address field instances
+ * (e.g., class State, class City, class Country, etc.), keyed by the
+ * field's clas name.
+ */
+ #fields = {};
+
+ /**
+ * Constructs an AddressComponent object by converting passed address record object.
+ *
+ * @class
+ * @param {object} record The address record object containing address data.
+ * @param {object} [options = {}] a list of options for this method
+ * @param {boolean} [options.ignoreInvalid = true] Whether to ignore invalid address
+ * fields in the AddressComponent object. If set to true,
+ * invalid fields will be ignored.
+ */
+ constructor(record, { ignoreInvalid = true } = {}) {
+ this.record = {};
+
+ // Get country code first so we can use it to parse other fields
+ const country = new Country(
+ record[Country.ac],
+ FormAutofill.DEFAULT_REGION
+ );
+ const region =
+ country.country_code ||
+ lazy.FormAutofillUtils.identifyCountryCode(FormAutofill.DEFAULT_REGION);
+
+ // Build an mapping that the key is field name and the value is the AddressField object
+ [
+ country,
+ new StreetAddress(record[StreetAddress.ac], region),
+ new PostalCode(record[PostalCode.ac], region),
+ new State(record[State.ac], region),
+ new City(record[City.ac], region),
+ new Name(record[Name.ac], region),
+ new Tel(record[Tel.ac], region),
+ new Organization(record[Organization.ac], region),
+ new Email(record[Email.ac], region),
+ ].forEach(addressField => {
+ if (
+ !addressField.isEmpty() &&
+ (!ignoreInvalid || addressField.isValid())
+ ) {
+ const fieldName = addressField.constructor.ac;
+ this.#fields[fieldName] = addressField;
+ this.record[fieldName] = record[fieldName];
+ }
+ });
+ }
+
+ /**
+ * Retrieves all the address fields.
+ *
+ * @returns {Array} An array of address field objects.
+ */
+ getAllFields() {
+ return Object.values(this.#fields);
+ }
+
+ /**
+ * Retrieves the field object with the specified name.
+ *
+ * @param {string} name The name of the field to retrieve.
+ * @returns {object} The address field object with the specified name,
+ * or undefined if the field is not found.
+ */
+ getField(name) {
+ return this.#fields[name];
+ }
+
+ /**
+ * Compares the current AddressComponent with another AddressComponent.
+ *
+ * @param {AddressComponent} address The AddressComponent object to compare
+ * against the current one.
+ * @returns {object} An object containing comparison results. The keys of the object represent
+ * individual address field, and the values are strings indicating the comparison result:
+ * - "same" if both components are either empty or the same,
+ * - "superset" if the current contains the input or the input is empty,
+ * - "subset" if the input contains the current or the current is empty,
+ * - "similar" if the two address components are similar,
+ * - "different" if the two address components are different.
+ */
+ compare(address) {
+ let result = {};
+
+ const comparison = new AddressComparison(this, address);
+ for (const [k, v] of Object.entries(comparison.result)) {
+ if ([AddressComparison.BOTH_EMPTY, AddressComparison.SAME].includes(v)) {
+ result[k] = "same";
+ } else if (
+ [AddressComparison.B_IS_EMPTY, AddressComparison.A_CONTAINS_B].includes(
+ v
+ )
+ ) {
+ result[k] = "superset";
+ } else if (
+ [AddressComparison.A_IS_EMPTY, AddressComparison.B_CONTAINS_A].includes(
+ v
+ )
+ ) {
+ result[k] = "subset";
+ } else if ([AddressComparison.SIMILAR].includes(v)) {
+ result[k] = "similar";
+ } else {
+ result[k] = "different";
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Print all the fields in this AddressComponent object.
+ */
+ toString() {
+ let string = "";
+ for (const field of Object.values(this.#fields)) {
+ string += field.toString();
+ }
+ return string;
+ }
+}
diff --git a/toolkit/components/formautofill/shared/AddressMetaData.sys.mjs b/toolkit/components/formautofill/shared/AddressMetaData.sys.mjs
new file mode 100644
index 0000000000..7f80a220af
--- /dev/null
+++ b/toolkit/components/formautofill/shared/AddressMetaData.sys.mjs
@@ -0,0 +1,2451 @@
+/* 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/. */
+
+// The data below is initially copied from
+// https://chromium-i18n.appspot.com/ssl-aggregate-address
+
+// See https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata for
+// documentation on how to use the data.
+
+// WARNING: DO NOT change any value or add additional properties in addressData.
+// We only accept the metadata of the supported countries that is copied from libaddressinput directly.
+// Please edit AddressMetaDataExtension.sys.mjs instead if you want to add new property as complement
+// or overwrite the existing properties.
+
+export const AddressMetaData = {
+ "data/AD": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/AD",
+ key: "AD",
+ lang: "ca",
+ languages: "ca",
+ name: "ANDORRA",
+ posturl:
+ "http://www.correos.es/comun/CodigosPostales/1010_s-CodPostal.asp?Provincia=",
+ sub_isoids: "07~02~03~08~04~05~06",
+ sub_keys:
+ "Parròquia d'Andorra la Vella~Canillo~Encamp~Escaldes-Engordany~La Massana~Ordino~Sant Julià de Lòria",
+ sub_names:
+ "Andorra la Vella~Canillo~Encamp~Escaldes-Engordany~La Massana~Ordino~Sant Julià de Lòria",
+ sub_zipexs: "AD500~AD100~AD200~AD700~AD400~AD300~AD600",
+ sub_zips: "AD50[01]~AD10[01]~AD20[01]~AD70[01]~AD40[01]~AD30[01]~AD60[01]",
+ zip: "AD[1-7]0\\d",
+ zipex: "AD100,AD501,AD700",
+ },
+ "data/AE": {
+ fmt: "%N%n%O%n%A%n%S",
+ id: "data/AE",
+ key: "AE",
+ lang: "ar",
+ languages: "ar",
+ lfmt: "%N%n%O%n%A%n%S",
+ name: "UNITED ARAB EMIRATES",
+ require: "AS",
+ state_name_type: "emirate",
+ sub_isoids: "AZ~SH~FU~UQ~DU~RK~AJ",
+ sub_keys:
+ "أبو ظبي~إمارة الشارقةّ~الفجيرة~ام القيوين~إمارة دبيّ~إمارة رأس الخيمة~عجمان",
+ sub_lnames:
+ "Abu Dhabi~Sharjah~Fujairah~Umm Al Quwain~Dubai~Ras al Khaimah~Ajman",
+ sub_names: "أبو ظبي~الشارقة~الفجيرة~ام القيوين~دبي~رأس الخيمة~عجمان",
+ },
+ "data/AF": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/AF",
+ key: "AF",
+ name: "AFGHANISTAN",
+ zip: "\\d{4}",
+ zipex: "1001,2601,3801",
+ },
+ "data/AG": {
+ id: "data/AG",
+ key: "AG",
+ name: "ANTIGUA AND BARBUDA",
+ require: "A",
+ },
+ "data/AI": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/AI",
+ key: "AI",
+ name: "ANGUILLA",
+ zip: "(?:AI-)?2640",
+ zipex: "2640",
+ },
+ "data/AL": {
+ fmt: "%N%n%O%n%A%n%Z%n%C",
+ id: "data/AL",
+ key: "AL",
+ name: "ALBANIA",
+ zip: "\\d{4}",
+ zipex: "1001,1017,3501",
+ },
+ "data/AM": {
+ fmt: "%N%n%O%n%A%n%Z%n%C%n%S",
+ id: "data/AM",
+ key: "AM",
+ lang: "hy",
+ languages: "hy",
+ lfmt: "%N%n%O%n%A%n%Z%n%C%n%S",
+ name: "ARMENIA",
+ sub_isoids: "AG~AR~AV~GR~ER~LO~KT~SH~SU~VD~TV",
+ sub_keys:
+ "Արագածոտն~Արարատ~Արմավիր~Գեղարքունիք~Երևան~Լոռի~Կոտայք~Շիրակ~Սյունիք~Վայոց ձոր~Տավուշ",
+ sub_lnames:
+ "Aragatsotn~Ararat~Armavir~Gegharkunik~Yerevan~Lori~Kotayk~Shirak~Syunik~Vayots Dzor~Tavush",
+ sub_zipexs:
+ "0201,0514~0601,0823~0901,1149~1201,1626~0000,0099~1701,2117~2201,2506~2601,3126~3201,3519~3601,3810~3901,4216",
+ sub_zips:
+ "0[2-5]~0[6-8]~09|1[01]~1[2-6]~00~1[7-9]|2[01]~2[2-5]~2[6-9]|3[01]~3[2-5]~3[6-8]~39|4[0-2]",
+ zip: "(?:37)?\\d{4}",
+ zipex: "375010,0002,0010",
+ },
+ "data/AO": { id: "data/AO", key: "AO", name: "ANGOLA" },
+ "data/AQ": { id: "data/AQ", key: "AQ", name: "ANTARCTICA" },
+ "data/AR": {
+ fmt: "%N%n%O%n%A%n%Z %C%n%S",
+ id: "data/AR",
+ key: "AR",
+ lang: "es",
+ languages: "es",
+ name: "ARGENTINA",
+ posturl: "http://www.correoargentino.com.ar/formularios/cpa",
+ sub_isoids: "B~K~H~U~C~X~W~E~P~Y~L~F~M~N~Q~R~A~J~D~Z~S~G~V~T",
+ sub_keys:
+ "Buenos Aires~Catamarca~Chaco~Chubut~Ciudad Autónoma de Buenos Aires~Córdoba~Corrientes~Entre Ríos~Formosa~Jujuy~La Pampa~La Rioja~Mendoza~Misiones~Neuquén~Río Negro~Salta~San Juan~San Luis~Santa Cruz~Santa Fe~Santiago del Estero~Tierra del Fuego~Tucumán",
+ sub_names:
+ "Buenos Aires~Catamarca~Chaco~Chubut~Ciudad Autónoma de Buenos Aires~Córdoba~Corrientes~Entre Ríos~Formosa~Jujuy~La Pampa~La Rioja~Mendoza~Misiones~Neuquén~Río Negro~Salta~San Juan~San Luis~Santa Cruz~Santa Fe~Santiago del Estero~Tierra del Fuego~Tucumán",
+ sub_zips:
+ "B?[1-36-8]~K?[45]~H?3~U?[89]~C?1~X?[235-8]~W?3~E?[1-3]~P?[37]~Y?4~L?[3568]~F?5~M?[56]~N?3~Q?[38]~R?[89]~A?[34]~J?5~D?[4-6]~Z?[89]~S?[2368]~G?[2-5]~V?9~T?[45]",
+ upper: "ACZ",
+ zip: "((?:[A-HJ-NP-Z])?\\d{4})([A-Z]{3})?",
+ zipex: "C1070AAM,C1000WAM,B1000TBU,X5187XAB",
+ },
+ "data/AS": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/AS",
+ key: "AS",
+ name: "AMERICAN SAMOA",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACSZ",
+ state_name_type: "state",
+ upper: "ACNOS",
+ zip: "(96799)(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "96799",
+ },
+ "data/AT": {
+ fmt: "%O%n%N%n%A%n%Z %C",
+ id: "data/AT",
+ key: "AT",
+ name: "AUSTRIA",
+ posturl: "http://www.post.at/post_subsite_postleitzahlfinder.php",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "1010,3741",
+ },
+ "data/AU": {
+ fmt: "%O%n%N%n%A%n%C %S %Z",
+ id: "data/AU",
+ key: "AU",
+ lang: "en",
+ languages: "en",
+ locality_name_type: "suburb",
+ name: "AUSTRALIA",
+ posturl: "http://www1.auspost.com.au/postcodes/",
+ require: "ACSZ",
+ state_name_type: "state",
+ sub_isoids: "ACT~NSW~NT~QLD~SA~TAS~VIC~WA",
+ sub_keys: "ACT~NSW~NT~QLD~SA~TAS~VIC~WA",
+ sub_names:
+ "Australian Capital Territory~New South Wales~Northern Territory~Queensland~South Australia~Tasmania~Victoria~Western Australia",
+ sub_zipexs:
+ "0200,2540,2618,2999~1000,2888,3585,3707~0800,0999~4000,9999~5000~7000,7999~3000,8000~6000,0872",
+ sub_zips:
+ "29|2540|260|261[0-8]|02|2620~1|2[0-57-8]|26[2-9]|261[189]|3500|358[56]|3644|3707~0[89]~[49]~5|0872~7~[38]~6|0872",
+ upper: "CS",
+ zip: "\\d{4}",
+ zipex: "2060,3171,6430,4000,4006,3001",
+ },
+ "data/AW": { id: "data/AW", key: "AW", name: "ARUBA" },
+ "data/AZ": {
+ fmt: "%N%n%O%n%A%nAZ %Z %C",
+ id: "data/AZ",
+ key: "AZ",
+ name: "AZERBAIJAN",
+ postprefix: "AZ ",
+ zip: "\\d{4}",
+ zipex: "1000",
+ },
+ "data/BA": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/BA",
+ key: "BA",
+ name: "BOSNIA AND HERZEGOVINA",
+ zip: "\\d{5}",
+ zipex: "71000",
+ },
+ "data/BB": {
+ fmt: "%N%n%O%n%A%n%C, %S %Z",
+ id: "data/BB",
+ key: "BB",
+ name: "BARBADOS",
+ state_name_type: "parish",
+ zip: "BB\\d{5}",
+ zipex: "BB23026,BB22025",
+ },
+ "data/BD": {
+ fmt: "%N%n%O%n%A%n%C - %Z",
+ id: "data/BD",
+ key: "BD",
+ name: "BANGLADESH",
+ posturl: "http://www.bangladeshpost.gov.bd/PostCode.asp",
+ zip: "\\d{4}",
+ zipex: "1340,1000",
+ },
+ "data/BE": {
+ fmt: "%O%n%N%n%A%n%Z %C",
+ id: "data/BE",
+ key: "BE",
+ name: "BELGIUM",
+ posturl:
+ "http://www.post.be/site/nl/residential/customerservice/search/postal_codes.html",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "4000,1000",
+ },
+ "data/BF": {
+ fmt: "%N%n%O%n%A%n%C %X",
+ id: "data/BF",
+ key: "BF",
+ name: "BURKINA FASO",
+ },
+ "data/BG": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/BG",
+ key: "BG",
+ name: "BULGARIA (REP.)",
+ posturl: "http://www.bgpost.bg/?cid=5",
+ zip: "\\d{4}",
+ zipex: "1000,1700",
+ },
+ "data/BH": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/BH",
+ key: "BH",
+ name: "BAHRAIN",
+ zip: "(?:\\d|1[0-2])\\d{2}",
+ zipex: "317",
+ },
+ "data/BI": { id: "data/BI", key: "BI", name: "BURUNDI" },
+ "data/BJ": { id: "data/BJ", key: "BJ", name: "BENIN", upper: "AC" },
+ "data/BL": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/BL",
+ key: "BL",
+ name: "SAINT BARTHELEMY",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78][01]\\d{2}",
+ zipex: "97100",
+ },
+ "data/BM": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/BM",
+ key: "BM",
+ name: "BERMUDA",
+ posturl: "http://www.landvaluation.bm/",
+ zip: "[A-Z]{2} ?[A-Z0-9]{2}",
+ zipex: "FL 07,HM GX,HM 12",
+ },
+ "data/BN": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/BN",
+ key: "BN",
+ name: "BRUNEI DARUSSALAM",
+ posturl: "http://www.post.gov.bn/SitePages/postcodes.aspx",
+ zip: "[A-Z]{2} ?\\d{4}",
+ zipex: "BT2328,KA1131,BA1511",
+ },
+ "data/BO": { id: "data/BO", key: "BO", name: "BOLIVIA", upper: "AC" },
+ "data/BQ": {
+ id: "data/BQ",
+ key: "BQ",
+ name: "BONAIRE, SINT EUSTATIUS, AND SABA",
+ },
+ "data/BR": {
+ fmt: "%O%n%N%n%A%n%D%n%C-%S%n%Z",
+ id: "data/BR",
+ key: "BR",
+ lang: "pt",
+ languages: "pt",
+ name: "BRAZIL",
+ posturl: "http://www.buscacep.correios.com.br/",
+ require: "ASCZ",
+ state_name_type: "state",
+ sub_isoids:
+ "AC~AL~AP~AM~BA~CE~DF~ES~GO~MA~MT~MS~MG~PA~PB~PR~PE~PI~RJ~RN~RS~RO~RR~SC~SP~SE~TO",
+ sub_keys:
+ "AC~AL~AP~AM~BA~CE~DF~ES~GO~MA~MT~MS~MG~PA~PB~PR~PE~PI~RJ~RN~RS~RO~RR~SC~SP~SE~TO",
+ sub_mores:
+ "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true",
+ sub_names:
+ "Acre~Alagoas~Amapá~Amazonas~Bahia~Ceará~Distrito Federal~Espírito Santo~Goiás~Maranhão~Mato Grosso~Mato Grosso do Sul~Minas Gerais~Pará~Paraíba~Paraná~Pernambuco~Piauí~Rio de Janeiro~Rio Grande do Norte~Rio Grande do Sul~Rondônia~Roraima~Santa Catarina~São Paulo~Sergipe~Tocantins",
+ sub_zipexs:
+ "69900-000,69999-999~57000-000,57999-999~68900-000,68999-999~69000-000,69400-123~40000-000,48999-999~60000-000,63999-999~70000-000,73500-123~29000-000,29999-999~72800-000,73700-123~65000-000,65999-999~78000-000,78899-999~79000-000,79999-999~30000-000,39999-999~66000-000,68899-999~58000-000,58999-999~80000-000,87999-999~50000-000,56999-999~64000-000,64999-999~20000-000,28999-999~59000-000,59999-999~90000-000,99999-999~76800-000,78900-000,78999-999~69300-000,69399-999~88000-000,89999-999~01000-000,13000-123~49000-000,49999-999~77000-000,77999-999",
+ sub_zips:
+ "699~57~689~69[0-24-8]~4[0-8]~6[0-3]~7[0-1]|72[0-7]|73[0-6]~29~72[89]|73[7-9]|7[4-6]~65~78[0-8]~79~3~6[6-7]|68[0-8]~58~8[0-7]~5[0-6]~64~2[0-8]~59~9~76[89]|789~693~8[89]~[01][1-9]~49~77",
+ sublocality_name_type: "neighborhood",
+ upper: "CS",
+ zip: "\\d{5}-?\\d{3}",
+ zipex: "40301-110,70002-900",
+ },
+ "data/BS": {
+ fmt: "%N%n%O%n%A%n%C, %S",
+ id: "data/BS",
+ key: "BS",
+ lang: "en",
+ languages: "en",
+ name: "BAHAMAS",
+ state_name_type: "island",
+ sub_isoids: "~AK~~BY~BI~CI~~~EX~~HI~IN~LI~MG~~RI~RC~SS~SW",
+ sub_keys:
+ "Abaco~Acklins~Andros~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~N.P.~Ragged Island~Rum Cay~San Salvador~Spanish Wells",
+ sub_names:
+ "Abaco Islands~Acklins~Andros Island~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma and Cays~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~New Providence~Ragged Island~Rum Cay~San Salvador~Spanish Wells",
+ },
+ "data/BT": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/BT",
+ key: "BT",
+ name: "BHUTAN",
+ posturl: "http://www.bhutanpost.bt/postcodes/",
+ zip: "\\d{5}",
+ zipex: "11001,31101,35003",
+ },
+ "data/BV": { id: "data/BV", key: "BV", name: "BOUVET ISLAND" },
+ "data/BW": { id: "data/BW", key: "BW", name: "BOTSWANA" },
+ "data/BY": {
+ fmt: "%S%n%Z %C%n%A%n%O%n%N",
+ id: "data/BY",
+ key: "BY",
+ name: "BELARUS",
+ posturl: "http://ex.belpost.by/addressbook/",
+ zip: "\\d{6}",
+ zipex: "223016,225860,220050",
+ },
+ "data/BZ": { id: "data/BZ", key: "BZ", name: "BELIZE" },
+ "data/CA": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/CA",
+ key: "CA",
+ lang: "en",
+ languages: "en~fr",
+ name: "CANADA",
+ posturl: "https://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf",
+ require: "ACSZ",
+ sub_isoids: "AB~BC~MB~NB~NL~NT~NS~NU~ON~PE~QC~SK~YT",
+ sub_keys: "AB~BC~MB~NB~NL~NT~NS~NU~ON~PE~QC~SK~YT",
+ sub_names:
+ "Alberta~British Columbia~Manitoba~New Brunswick~Newfoundland and Labrador~Northwest Territories~Nova Scotia~Nunavut~Ontario~Prince Edward Island~Quebec~Saskatchewan~Yukon",
+ sub_zips:
+ "T~V~R~E~A~X0E|X0G|X1A~B~X0A|X0B|X0C~K|L|M|N|P~C~G|H|J|K1A~S|R8A~Y",
+ upper: "ACNOSZ",
+ zip: "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
+ zipex: "H3Z 2Y7,V8X 3X4,T0L 1K0,T0H 1A0,K1A 0B1",
+ },
+ "data/CA--fr": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/CA--fr",
+ key: "CA",
+ lang: "fr",
+ name: "CANADA",
+ posturl: "https://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf",
+ require: "ACSZ",
+ sub_isoids: "AB~BC~PE~MB~NB~NS~NU~ON~QC~SK~NL~NT~YT",
+ sub_keys: "AB~BC~PE~MB~NB~NS~NU~ON~QC~SK~NL~NT~YT",
+ sub_names:
+ "Alberta~Colombie-Britannique~Île-du-Prince-Édouard~Manitoba~Nouveau-Brunswick~Nouvelle-Écosse~Nunavut~Ontario~Québec~Saskatchewan~Terre-Neuve-et-Labrador~Territoires du Nord-Ouest~Yukon",
+ sub_zips:
+ "T~V~C~R~E~B~X0A|X0B|X0C~K|L|M|N|P~G|H|J|K1A~S|R8A~A~X0E|X0G|X1A~Y",
+ upper: "ACNOSZ",
+ zip: "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
+ zipex: "H3Z 2Y7,V8X 3X4,T0L 1K0,T0H 1A0,K1A 0B1",
+ },
+ "data/CC": {
+ fmt: "%O%n%N%n%A%n%C %S %Z",
+ id: "data/CC",
+ key: "CC",
+ name: "COCOS (KEELING) ISLANDS",
+ upper: "CS",
+ zip: "6799",
+ zipex: "6799",
+ },
+ "data/CD": { id: "data/CD", key: "CD", name: "CONGO (DEM. REP.)" },
+ "data/CF": { id: "data/CF", key: "CF", name: "CENTRAL AFRICAN REPUBLIC" },
+ "data/CG": { id: "data/CG", key: "CG", name: "CONGO (REP.)" },
+ "data/CH": {
+ fmt: "%O%n%N%n%A%nCH-%Z %C",
+ id: "data/CH",
+ key: "CH",
+ name: "SWITZERLAND",
+ postprefix: "CH-",
+ posturl: "http://www.post.ch/db/owa/pv_plz_pack/pr_main",
+ require: "ACZ",
+ upper: "",
+ zip: "\\d{4}",
+ zipex: "2544,1211,1556,3030",
+ },
+ "data/CI": {
+ fmt: "%N%n%O%n%X %A %C %X",
+ id: "data/CI",
+ key: "CI",
+ name: "COTE D'IVOIRE",
+ },
+ "data/CK": { id: "data/CK", key: "CK", name: "COOK ISLANDS" },
+ "data/CL": {
+ fmt: "%N%n%O%n%A%n%Z %C%n%S",
+ id: "data/CL",
+ key: "CL",
+ lang: "es",
+ languages: "es",
+ name: "CHILE",
+ posturl: "http://www.correos.cl/SitePages/home.aspx",
+ sub_isoids: "AN~AR~AP~AT~AI~BI~CO~LI~LL~LR~MA~ML~RM~TA~VS",
+ sub_keys:
+ "Antofagasta~Araucanía~Arica y Parinacota~Atacama~Aysén~Biobío~Coquimbo~O'Higgins~Los Lagos~Los Ríos~Magallanes~Maule~Región Metropolitana~Tarapacá~Valparaíso",
+ sub_mores:
+ "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true",
+ sub_names:
+ "Antofagasta~Araucanía~Arica y Parinacota~Atacama~Aysén del General Carlos Ibáñez del Campo~Biobío~Coquimbo~Libertador General Bernardo O'Higgins~Los Lagos~Los Ríos~Magallanes y de la Antártica Chilena~Maule~Metropolitana de Santiago~Tarapacá~Valparaíso",
+ zip: "\\d{7}",
+ zipex: "8340457,8720019,1230000,8329100",
+ },
+ "data/CM": { id: "data/CM", key: "CM", name: "CAMEROON" },
+ "data/CN": {
+ fmt: "%Z%n%S%C%D%n%A%n%O%n%N",
+ id: "data/CN",
+ key: "CN",
+ lang: "zh",
+ languages: "zh",
+ lfmt: "%N%n%O%n%A%n%D%n%C%n%S, %Z",
+ name: "CHINA",
+ posturl: "http://www.ems.com.cn/serviceguide/you_bian_cha_xun.html",
+ require: "ACSZ",
+ sub_isoids:
+ "34~92~11~50~35~62~44~45~52~46~13~41~23~42~43~22~32~36~21~15~64~63~37~14~61~31~51~71~12~54~91~65~53~33",
+ sub_keys:
+ "安徽省~澳门~北京市~重庆市~福建省~甘肃省~广东省~广西壮族自治区~贵州省~海南省~河北省~河南省~黑龙江省~湖北省~湖南省~吉林省~江苏省~江西省~辽宁省~内蒙古自治区~宁夏回族自治区~青海省~山东省~山西省~陕西省~上海市~四川省~台湾~天津市~西藏自治区~香港~新疆维吾尔自治区~云南省~浙江省",
+ sub_lnames:
+ "Anhui Sheng~Macau~Beijing Shi~Chongqing Shi~Fujian Sheng~Gansu Sheng~Guangdong Sheng~Guangxi Zhuangzuzizhiqu~Guizhou Sheng~Hainan Sheng~Hebei Sheng~Henan Sheng~Heilongjiang Sheng~Hubei Sheng~Hunan Sheng~Jilin Sheng~Jiangsu Sheng~Jiangxi Sheng~Liaoning Sheng~Neimenggu Zizhiqu~Ningxia Huizuzizhiqu~Qinghai Sheng~Shandong Sheng~Shanxi Sheng~Shaanxi Sheng~Shanghai Shi~Sichuan Sheng~Taiwan~Tianjin Shi~Xizang Zizhiqu~Hong Kong~Xinjiang Weiwuerzizhiqu~Yunnan Sheng~Zhejiang Sheng",
+ sub_mores:
+ "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true",
+ sub_names:
+ "安徽省~澳门~北京市~重庆市~福建省~甘肃省~广东省~广西~贵州省~海南省~河北省~河南省~黑龙江省~湖北省~湖南省~吉林省~江苏省~江西省~辽宁省~内蒙古~宁夏~青海省~山东省~山西省~陕西省~上海市~四川省~台湾~天津市~西藏~香港~新疆~云南省~浙江省",
+ sub_xrequires: "~A~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ACS~~~",
+ sub_xzips: "~999078~~~~~~~~~~~~~~~~~~~~~~~~~~\\d{3}(\\d{2})?~~~999077~~~",
+ sublocality_name_type: "district",
+ upper: "S",
+ zip: "\\d{6}",
+ zipex: "266033,317204,100096,100808",
+ },
+ "data/CO": {
+ fmt: "%N%n%O%n%A%n%C, %S, %Z",
+ id: "data/CO",
+ key: "CO",
+ name: "COLOMBIA",
+ posturl: "http://www.codigopostal.gov.co/",
+ require: "AS",
+ state_name_type: "department",
+ zip: "\\d{6}",
+ zipex: "111221,130001,760011",
+ },
+ "data/CR": {
+ fmt: "%N%n%O%n%A%n%S, %C%n%Z",
+ id: "data/CR",
+ key: "CR",
+ name: "COSTA RICA",
+ posturl: "https://www.correos.go.cr/nosotros/codigopostal/busqueda.html",
+ require: "ACS",
+ zip: "\\d{4,5}|\\d{3}-\\d{4}",
+ zipex: "1000,2010,1001",
+ },
+ "data/CU": {
+ fmt: "%N%n%O%n%A%n%C %S%n%Z",
+ id: "data/CU",
+ key: "CU",
+ lang: "es",
+ languages: "es",
+ name: "CUBA",
+ sub_isoids: "15~09~08~06~12~14~11~99~03~10~04~16~01~07~13~05",
+ sub_keys:
+ "Artemisa~Camagüey~Ciego de Ávila~Cienfuegos~Granma~Guantánamo~Holguín~Isla de la Juventud~La Habana~Las Tunas~Matanzas~Mayabeque~Pinar del Río~Sancti Spíritus~Santiago de Cuba~Villa Clara",
+ zip: "\\d{5}",
+ zipex: "10700",
+ },
+ "data/CV": {
+ fmt: "%N%n%O%n%A%n%Z %C%n%S",
+ id: "data/CV",
+ key: "CV",
+ lang: "pt",
+ languages: "pt",
+ name: "CAPE VERDE",
+ state_name_type: "island",
+ sub_isoids: "BV~BR~~MA~SL~~~~SV",
+ sub_keys:
+ "Boa Vista~Brava~Fogo~Maio~Sal~Santiago~Santo Antão~São Nicolau~São Vicente",
+ zip: "\\d{4}",
+ zipex: "7600",
+ },
+ "data/CW": { id: "data/CW", key: "CW", name: "CURACAO" },
+ "data/CX": {
+ fmt: "%O%n%N%n%A%n%C %S %Z",
+ id: "data/CX",
+ key: "CX",
+ name: "CHRISTMAS ISLAND",
+ upper: "CS",
+ zip: "6798",
+ zipex: "6798",
+ },
+ "data/CY": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/CY",
+ key: "CY",
+ name: "CYPRUS",
+ zip: "\\d{4}",
+ zipex: "2008,3304,1900",
+ },
+ "data/CZ": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/CZ",
+ key: "CZ",
+ name: "CZECH REP.",
+ posturl: "http://psc.ceskaposta.cz/CleanForm.action",
+ require: "ACZ",
+ zip: "\\d{3} ?\\d{2}",
+ zipex: "100 00,251 66,530 87,110 00,225 99",
+ },
+ "data/DE": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/DE",
+ key: "DE",
+ name: "GERMANY",
+ posturl: "http://www.postdirekt.de/plzserver/",
+ require: "ACZ",
+ zip: "\\d{5}",
+ zipex: "26133,53225",
+ },
+ "data/DJ": { id: "data/DJ", key: "DJ", name: "DJIBOUTI" },
+ "data/DK": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/DK",
+ key: "DK",
+ name: "DENMARK",
+ posturl:
+ "http://www.postdanmark.dk/da/Privat/Kundeservice/postnummerkort/Sider/Find-postnummer.aspx",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "8660,1566",
+ },
+ "data/DM": { id: "data/DM", key: "DM", name: "DOMINICA" },
+ "data/DO": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/DO",
+ key: "DO",
+ name: "DOMINICAN REP.",
+ posturl: "http://inposdom.gob.do/codigo-postal/",
+ zip: "\\d{5}",
+ zipex: "11903,10101",
+ },
+ "data/DZ": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/DZ",
+ key: "DZ",
+ name: "ALGERIA",
+ zip: "\\d{5}",
+ zipex: "40304,16027",
+ },
+ "data/EC": {
+ fmt: "%N%n%O%n%A%n%Z%n%C",
+ id: "data/EC",
+ key: "EC",
+ name: "ECUADOR",
+ posturl: "http://www.codigopostal.gob.ec/",
+ upper: "CZ",
+ zip: "\\d{6}",
+ zipex: "090105,092301",
+ },
+ "data/EE": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/EE",
+ key: "EE",
+ name: "ESTONIA",
+ posturl: "https://www.omniva.ee/era/sihtnumbrite_otsing",
+ zip: "\\d{5}",
+ zipex: "69501,11212",
+ },
+ "data/EG": {
+ fmt: "%N%n%O%n%A%n%C%n%S%n%Z",
+ id: "data/EG",
+ key: "EG",
+ lang: "ar",
+ languages: "ar",
+ lfmt: "%N%n%O%n%A%n%C%n%S%n%Z",
+ name: "EGYPT",
+ sub_isoids:
+ "ASN~AST~ALX~IS~LX~BA~BH~GZ~DK~SUZ~SHR~GH~FYM~C~KB~MNF~MN~WAD~BNS~PTS~JS~DT~SHG~SIN~KN~KFS~MT",
+ sub_keys:
+ "أسوان~أسيوط~الإسكندرية~الإسماعيلية~الأقصر~البحر الأحمر~البحيرة~الجيزة~الدقهلية~السويس~الشرقية~الغربية~الفيوم~القاهرة~القليوبية~المنوفية~المنيا~الوادي الجديد~بني سويف~بورسعيد~جنوب سيناء~دمياط~سوهاج~شمال سيناء~قنا~كفر الشيخ~مطروح",
+ sub_lnames:
+ "Aswan Governorate~Asyut Governorate~Alexandria Governorate~Ismailia Governorate~Luxor Governorate~Red Sea Governorate~El Beheira Governorate~Giza Governorate~Dakahlia Governorate~Suez Governorate~Ash Sharqia Governorate~Gharbia Governorate~Faiyum Governorate~Cairo Governorate~Qalyubia Governorate~Menofia Governorate~Menia Governorate~New Valley Governorate~Beni Suef Governorate~Port Said Governorate~South Sinai Governorate~Damietta Governorate~Sohag Governorate~North Sinai Governorate~Qena Governorate~Kafr El Sheikh Governorate~Matrouh Governorate",
+ sub_zipexs:
+ "81000~71000~21000,23000~41000~85000~84000~22000~12000~35000~43000~44000~31000~63000~11000~13000~32000~61000~72000~62000~42000~46000~34000~82000~45000~83000~33000~51000",
+ sub_zips:
+ "81~71~2[13]~41~85~84~22~12~35~43~44~31~63~11~13~32~61~72~62~42~46~34~82~45~83~33~51",
+ zip: "\\d{5}",
+ zipex: "12411,11599",
+ },
+ "data/EH": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/EH",
+ key: "EH",
+ name: "WESTERN SAHARA",
+ zip: "\\d{5}",
+ zipex: "70000,72000",
+ },
+ "data/ER": { id: "data/ER", key: "ER", name: "ERITREA" },
+ "data/ES": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/ES",
+ key: "ES",
+ lang: "es",
+ languages: "es~ca~gl~eu",
+ name: "SPAIN",
+ posturl:
+ "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp",
+ require: "ACSZ",
+ sub_keys:
+ "VI~AB~A~AL~O~AV~BA~B~BU~CC~CA~S~CS~CE~CR~CO~CU~GI~GR~GU~SS~H~HU~PM~J~C~LO~GC~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~BI~ZA~Z",
+ sub_names:
+ "Álava~Albacete~Alicante~Almería~Asturias~Ávila~Badajoz~Barcelona~Burgos~Cáceres~Cádiz~Cantabria~Castellón~Ceuta~Ciudad Real~Córdoba~Cuenca~Girona~Granada~Guadalajara~Guipúzcoa~Huelva~Huesca~Islas Baleares~Jaén~La Coruña~La Rioja~Las Palmas~León~Lérida~Lugo~Madrid~Málaga~Melilla~Murcia~Navarra~Ourense~Palencia~Pontevedra~Salamanca~Santa Cruz de Tenerife~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~Valencia~Valladolid~Vizcaya~Zamora~Zaragoza",
+ sub_zips:
+ "01~02~03~04~33~05~06~08~09~10~11~39~12~51~13~14~16~17~18~19~20~21~22~07~23~15~26~35~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~48~49~50",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "28039,28300,28070",
+ },
+ "data/ES--ca": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/ES--ca",
+ key: "ES",
+ lang: "ca",
+ name: "SPAIN",
+ posturl:
+ "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp",
+ require: "ACSZ",
+ sub_keys:
+ "A~AB~AL~VI~O~AV~BA~B~BI~BU~CC~CA~S~CS~CE~CR~CO~CU~GI~GR~GU~SS~H~HU~PM~J~C~LO~GC~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~ZA~Z",
+ sub_names:
+ "Alacant~Albacete~Almeria~Araba~Asturias~Àvila~Badajoz~Barcelona~Bizkaia~Burgos~Cáceres~Cadis~Cantabria~Castelló~Ceuta~Ciudad Real~Córdoba~Cuenca~Girona~Granada~Guadalajara~Guipúscoa~Huelva~Huesca~Illes Balears~Jaén~La Corunya~La Rioja~Las Palmas~León~Lleida~Lugo~Madrid~Málaga~Melilla~Murcia~Navarra~Ourense~Palencia~Pontevedra~Salamanca~Santa Cruz de Tenerife~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~València~Valladolid~Zamora~Zaragoza",
+ sub_zips:
+ "03~02~04~01~33~05~06~08~48~09~10~11~39~12~51~13~14~16~17~18~19~20~21~22~07~23~15~26~35~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~49~50",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "28039,28300,28070",
+ },
+ "data/ES--eu": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/ES--eu",
+ key: "ES",
+ lang: "eu",
+ name: "SPAIN",
+ posturl:
+ "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp",
+ require: "ACSZ",
+ sub_keys:
+ "A~AB~AL~VI~O~AV~BA~B~BI~BU~CC~CA~S~CS~CE~CR~C~CU~SS~GI~GR~GU~H~HU~PM~J~CO~LO~GC~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~ZA~Z",
+ sub_names:
+ "Alacant~Albacete~Almería~Araba~Asturias~Ávila~Badajoz~Barcelona~Bizkaia~Burgos~Cáceres~Cádiz~Cantabria~Castelló~Ceuta~Ciudad Real~Coruña~Cuenca~Gipuzkoa~Girona~Granada~Guadalajara~Huelva~Huesca~Illes Balears~Jaén~Kordoba~La Rioja~Las Palmas~León~Lleida~Lugo~Madrid~Málaga~Melilla~Murtzia~Nafarroa~Ourense~Palentzia~Pontevedra~Salamanca~Santa Cruz Tenerifekoa~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~Valentzia~Valladolid~Zamora~Zaragoza",
+ sub_zips:
+ "03~02~04~01~33~05~06~08~48~09~10~11~39~12~51~13~15~16~20~17~18~19~21~22~07~23~14~26~35~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~49~50",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "28039,28300,28070",
+ },
+ "data/ES--gl": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/ES--gl",
+ key: "ES",
+ lang: "gl",
+ name: "SPAIN",
+ posturl:
+ "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp",
+ require: "ACSZ",
+ sub_keys:
+ "C~A~VI~AB~AL~GC~O~AV~BA~B~BI~BU~CC~CA~S~CS~CE~CR~CO~CU~GR~GU~SS~H~HU~PM~LO~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~J~GI~ZA~Z",
+ sub_names:
+ "A Coruña~Alacant~Álava~Albacete~Almería~As Palmas~Asturias~Ávila~Badaxoz~Barcelona~Biscaia~Burgos~Cáceres~Cádiz~Cantabria~Castelló~Ceuta~Cidade Real~Córdoba~Cuenca~Granada~Guadalajara~Guipúscoa~Huelva~Huesca~Illas Baleares~La Rioja~León~Lleida~Lugo~Madrid~Málaga~Melilla~Murcia~Navarra~Ourense~Palencia~Pontevedra~Salamanca~Santa Cruz de Tenerife~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~Valencia~Valladolid~Xaén~Xirona~Zamora~Zaragoza",
+ sub_zips:
+ "15~03~01~02~04~35~33~05~06~08~48~09~10~11~39~12~51~13~14~16~18~19~20~21~22~07~26~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~23~17~49~50",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "28039,28300,28070",
+ },
+ "data/ET": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/ET",
+ key: "ET",
+ name: "ETHIOPIA",
+ zip: "\\d{4}",
+ zipex: "1000",
+ },
+ "data/FI": {
+ fmt: "%O%n%N%n%A%nFI-%Z %C",
+ id: "data/FI",
+ key: "FI",
+ name: "FINLAND",
+ postprefix: "FI-",
+ posturl: "http://www.verkkoposti.com/e3/postinumeroluettelo",
+ require: "ACZ",
+ zip: "\\d{5}",
+ zipex: "00550,00011",
+ },
+ "data/FJ": { id: "data/FJ", key: "FJ", name: "FIJI" },
+ "data/FK": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/FK",
+ key: "FK",
+ name: "FALKLAND ISLANDS (MALVINAS)",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "FIQQ 1ZZ",
+ zipex: "FIQQ 1ZZ",
+ },
+ "data/FM": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/FM",
+ key: "FM",
+ name: "MICRONESIA (Federated State of)",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACSZ",
+ state_name_type: "state",
+ upper: "ACNOS",
+ zip: "(9694[1-4])(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "96941,96944",
+ },
+ "data/FO": {
+ fmt: "%N%n%O%n%A%nFO%Z %C",
+ id: "data/FO",
+ key: "FO",
+ name: "FAROE ISLANDS",
+ postprefix: "FO",
+ posturl: "http://www.postur.fo/",
+ zip: "\\d{3}",
+ zipex: "100",
+ },
+ "data/FR": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/FR",
+ key: "FR",
+ name: "FRANCE",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "CX",
+ zip: "\\d{2} ?\\d{3}",
+ zipex: "33380,34092,33506",
+ },
+ "data/GA": { id: "data/GA", key: "GA", name: "GABON" },
+ "data/GB": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/GB",
+ key: "GB",
+ locality_name_type: "post_town",
+ name: "UNITED KINGDOM",
+ posturl: "http://www.royalmail.com/postcode-finder",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "GIR ?0AA|(?:(?:AB|AL|B|BA|BB|BD|BF|BH|BL|BN|BR|BS|BT|BX|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(?:\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}))|BFPO ?\\d{1,4}",
+ zipex:
+ "EC1Y 8SY,GIR 0AA,M2 5BQ,M34 4AB,CR0 2YR,DN16 9AA,W1A 4ZZ,EC1A 1HQ,OX14 4PG,BS18 8HF,NR25 7HG,RH6 0NP,BH23 6AA,B6 5BA,SO23 9AP,PO1 3AX,BFPO 61",
+ },
+ "data/GD": { id: "data/GD", key: "GD", name: "GRENADA (WEST INDIES)" },
+ "data/GE": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/GE",
+ key: "GE",
+ name: "GEORGIA",
+ posturl: "http://www.georgianpost.ge/index.php?page=10",
+ zip: "\\d{4}",
+ zipex: "0101",
+ },
+ "data/GF": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/GF",
+ key: "GF",
+ name: "FRENCH GUIANA",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78]3\\d{2}",
+ zipex: "97300",
+ },
+ "data/GG": {
+ fmt: "%N%n%O%n%A%n%C%nGUERNSEY%n%Z",
+ id: "data/GG",
+ key: "GG",
+ name: "CHANNEL ISLANDS",
+ posturl: "http://www.guernseypost.com/postcode_finder/",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "GY\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}",
+ zipex: "GY1 1AA,GY2 2BT",
+ },
+ "data/GH": { id: "data/GH", key: "GH", name: "GHANA" },
+ "data/GI": {
+ fmt: "%N%n%O%n%A%nGIBRALTAR%n%Z",
+ id: "data/GI",
+ key: "GI",
+ name: "GIBRALTAR",
+ require: "A",
+ zip: "GX11 1AA",
+ zipex: "GX11 1AA",
+ },
+ "data/GL": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/GL",
+ key: "GL",
+ name: "GREENLAND",
+ require: "ACZ",
+ zip: "39\\d{2}",
+ zipex: "3900,3950,3911",
+ },
+ "data/GM": { id: "data/GM", key: "GM", name: "GAMBIA" },
+ "data/GN": {
+ fmt: "%N%n%O%n%Z %A %C",
+ id: "data/GN",
+ key: "GN",
+ name: "GUINEA",
+ zip: "\\d{3}",
+ zipex: "001,200,100",
+ },
+ "data/GP": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/GP",
+ key: "GP",
+ name: "GUADELOUPE",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78][01]\\d{2}",
+ zipex: "97100",
+ },
+ "data/GQ": { id: "data/GQ", key: "GQ", name: "EQUATORIAL GUINEA" },
+ "data/GR": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/GR",
+ key: "GR",
+ name: "GREECE",
+ posturl: "http://www.elta.gr/findapostcode.aspx",
+ require: "ACZ",
+ zip: "\\d{3} ?\\d{2}",
+ zipex: "151 24,151 10,101 88",
+ },
+ "data/GS": {
+ fmt: "%N%n%O%n%A%n%n%C%n%Z",
+ id: "data/GS",
+ key: "GS",
+ name: "SOUTH GEORGIA",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "SIQQ 1ZZ",
+ zipex: "SIQQ 1ZZ",
+ },
+ "data/GT": {
+ fmt: "%N%n%O%n%A%n%Z- %C",
+ id: "data/GT",
+ key: "GT",
+ name: "GUATEMALA",
+ zip: "\\d{5}",
+ zipex: "09001,01501",
+ },
+ "data/GU": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/GU",
+ key: "GU",
+ name: "GUAM",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACZ",
+ upper: "ACNO",
+ zip: "(969(?:[12]\\d|3[12]))(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "96910,96931",
+ },
+ "data/GW": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/GW",
+ key: "GW",
+ name: "GUINEA-BISSAU",
+ zip: "\\d{4}",
+ zipex: "1000,1011",
+ },
+ "data/GY": { id: "data/GY", key: "GY", name: "GUYANA" },
+ "data/HK": {
+ fmt: "%S%n%C%n%A%n%O%n%N",
+ id: "data/HK",
+ key: "HK",
+ lang: "zh-Hant",
+ languages: "zh-Hant~en",
+ lfmt: "%N%n%O%n%A%n%C%n%S",
+ locality_name_type: "district",
+ name: "HONG KONG",
+ require: "AS",
+ state_name_type: "area",
+ sub_keys: "Kowloon~Hong Kong Island~New Territories",
+ sub_mores: "true~true~true",
+ sub_names: "九龍~香港島~新界",
+ upper: "S",
+ },
+ "data/HK--en": {
+ fmt: "%S%n%C%n%A%n%O%n%N",
+ id: "data/HK--en",
+ key: "HK",
+ lang: "en",
+ lfmt: "%N%n%O%n%A%n%C%n%S",
+ locality_name_type: "district",
+ name: "HONG KONG",
+ require: "AS",
+ state_name_type: "area",
+ sub_keys: "Hong Kong Island~Kowloon~New Territories",
+ sub_lnames: "Hong Kong Island~Kowloon~New Territories",
+ sub_mores: "true~true~true",
+ upper: "S",
+ },
+ "data/HM": {
+ fmt: "%O%n%N%n%A%n%C %S %Z",
+ id: "data/HM",
+ key: "HM",
+ name: "HEARD AND MCDONALD ISLANDS",
+ upper: "CS",
+ zip: "\\d{4}",
+ zipex: "7050",
+ },
+ "data/HN": {
+ fmt: "%N%n%O%n%A%n%C, %S%n%Z",
+ id: "data/HN",
+ key: "HN",
+ name: "HONDURAS",
+ require: "ACS",
+ zip: "\\d{5}",
+ zipex: "31301",
+ },
+ "data/HR": {
+ fmt: "%N%n%O%n%A%nHR-%Z %C",
+ id: "data/HR",
+ key: "HR",
+ name: "CROATIA",
+ postprefix: "HR-",
+ posturl: "http://www.posta.hr/default.aspx?pretpum",
+ zip: "\\d{5}",
+ zipex: "10000,21001,10002",
+ },
+ "data/HT": {
+ fmt: "%N%n%O%n%A%nHT%Z %C",
+ id: "data/HT",
+ key: "HT",
+ name: "HAITI",
+ postprefix: "HT",
+ zip: "\\d{4}",
+ zipex: "6120,5310,6110,8510",
+ },
+ "data/HU": {
+ fmt: "%N%n%O%n%C%n%A%n%Z",
+ id: "data/HU",
+ key: "HU",
+ name: "HUNGARY (Rep.)",
+ posturl: "http://posta.hu/ugyfelszolgalat/iranyitoszam_kereso",
+ require: "ACZ",
+ upper: "ACNO",
+ zip: "\\d{4}",
+ zipex: "1037,2380,1540",
+ },
+ "data/ID": {
+ fmt: "%N%n%O%n%A%n%C%n%S %Z",
+ id: "data/ID",
+ key: "ID",
+ lang: "id",
+ languages: "id",
+ name: "INDONESIA",
+ require: "AS",
+ sub_isoids:
+ "AC~BA~BT~BE~YO~JK~GO~JA~JB~JT~JI~KB~KS~KT~KI~KU~BB~KR~LA~MA~MU~NB~NT~PA~PB~RI~SR~SN~ST~SG~SA~SB~SS~SU",
+ sub_keys:
+ "Aceh~Bali~Banten~Bengkulu~Daerah Istimewa Yogyakarta~DKI Jakarta~Gorontalo~Jambi~Jawa Barat~Jawa Tengah~Jawa Timur~Kalimantan Barat~Kalimantan Selatan~Kalimantan Tengah~Kalimantan Timur~Kalimantan Utara~Kepulauan Bangka Belitung~Kepulauan Riau~Lampung~Maluku~Maluku Utara~Nusa Tenggara Barat~Nusa Tenggara Timur~Papua~Papua Barat~Riau~Sulawesi Barat~Sulawesi Selatan~Sulawesi Tengah~Sulawesi Tenggara~Sulawesi Utara~Sumatera Barat~Sumatera Selatan~Sumatera Utara",
+ zip: "\\d{5}",
+ zipex: "40115",
+ },
+ "data/IE": {
+ fmt: "%N%n%O%n%A%n%D%n%C%n%S %Z",
+ id: "data/IE",
+ key: "IE",
+ lang: "en",
+ languages: "en",
+ name: "IRELAND",
+ posturl: "https://finder.eircode.ie",
+ state_name_type: "county",
+ sub_isoids:
+ "CW~CN~CE~C~DL~D~G~KY~KE~KK~LS~LM~LK~LD~LH~MO~MH~MN~OY~RN~SO~TA~WD~WH~WX~WW",
+ sub_keys:
+ "Co. Carlow~Co. Cavan~Co. Clare~Co. Cork~Co. Donegal~Co. Dublin~Co. Galway~Co. Kerry~Co. Kildare~Co. Kilkenny~Co. Laois~Co. Leitrim~Co. Limerick~Co. Longford~Co. Louth~Co. Mayo~Co. Meath~Co. Monaghan~Co. Offaly~Co. Roscommon~Co. Sligo~Co. Tipperary~Co. Waterford~Co. Westmeath~Co. Wexford~Co. Wicklow",
+ sublocality_name_type: "townland",
+ zip: "[\\dA-Z]{3} ?[\\dA-Z]{4}",
+ zip_name_type: "eircode",
+ zipex: "A65 F4E2",
+ },
+ "data/IL": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/IL",
+ key: "IL",
+ name: "ISRAEL",
+ posturl: "http://www.israelpost.co.il/zipcode.nsf/demozip?openform",
+ zip: "\\d{5}(?:\\d{2})?",
+ zipex: "9614303",
+ },
+ "data/IM": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/IM",
+ key: "IM",
+ name: "ISLE OF MAN",
+ posturl: "https://www.iompost.com/tools-forms/postcode-finder/",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "IM\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}",
+ zipex: "IM2 1AA,IM99 1PS",
+ },
+ "data/IN": {
+ fmt: "%N%n%O%n%A%n%C %Z%n%S",
+ id: "data/IN",
+ key: "IN",
+ lang: "en",
+ languages: "en~hi",
+ name: "INDIA",
+ posturl: "https://www.indiapost.gov.in/vas/pages/FindPinCode.aspx",
+ require: "ACSZ",
+ state_name_type: "state",
+ sub_isoids:
+ "AN~AP~AR~AS~BR~CH~CT~DN~DD~DL~GA~GJ~HR~HP~JK~JH~KA~KL~LD~MP~MH~MN~ML~MZ~NL~OR~PY~PB~RJ~SK~TN~TG~TR~UP~UT~WB",
+ sub_keys:
+ "Andaman and Nicobar Islands~Andhra Pradesh~Arunachal Pradesh~Assam~Bihar~Chandigarh~Chhattisgarh~Dadra and Nagar Haveli~Daman and Diu~Delhi~Goa~Gujarat~Haryana~Himachal Pradesh~Jammu and Kashmir~Jharkhand~Karnataka~Kerala~Lakshadweep~Madhya Pradesh~Maharashtra~Manipur~Meghalaya~Mizoram~Nagaland~Odisha~Puducherry~Punjab~Rajasthan~Sikkim~Tamil Nadu~Telangana~Tripura~Uttar Pradesh~Uttarakhand~West Bengal",
+ sub_names:
+ "Andaman & Nicobar~Andhra Pradesh~Arunachal Pradesh~Assam~Bihar~Chandigarh~Chhattisgarh~Dadra & Nagar Haveli~Daman & Diu~Delhi~Goa~Gujarat~Haryana~Himachal Pradesh~Jammu & Kashmir~Jharkhand~Karnataka~Kerala~Lakshadweep~Madhya Pradesh~Maharashtra~Manipur~Meghalaya~Mizoram~Nagaland~Odisha~Puducherry~Punjab~Rajasthan~Sikkim~Tamil Nadu~Telangana~Tripura~Uttar Pradesh~Uttarakhand~West Bengal",
+ sub_zips:
+ "744~5[0-3]~79[0-2]~78~8[0-5]~16|1440[3-9]~49~396~396~11~403~3[6-9]~1[23]~17~1[89]~81[4-9]|82|83[0-5]~5[4-9]|53[7-9]~6[7-9]|6010|607008|777~682~4[5-8]|490~4[0-4]~79[56]~79[34]~796~79[78]~7[5-7]~60[579]~1[456]~3[0-4]~737|750~6[0-6]|536~5[0-3]~799~2[0-35-8]|24[0-7]|26[12]~24[46-9]|254|26[23]~7[0-4]",
+ zip: "\\d{6}",
+ zip_name_type: "pin",
+ zipex: "110034,110001",
+ },
+ "data/IN--hi": {
+ fmt: "%N%n%O%n%A%n%C %Z%n%S",
+ id: "data/IN--hi",
+ key: "IN",
+ lang: "hi",
+ name: "INDIA",
+ posturl: "https://www.indiapost.gov.in/vas/pages/FindPinCode.aspx",
+ require: "ACSZ",
+ state_name_type: "state",
+ sub_isoids:
+ "AN~AR~AS~AP~UP~UT~OR~KA~KL~GJ~GA~CH~CT~JK~JH~TN~TG~TR~DD~DN~DL~NL~PB~WB~PY~BR~MN~MP~MH~MZ~ML~RJ~LD~SK~HR~HP",
+ sub_keys:
+ "Andaman & Nicobar~Arunachal Pradesh~Assam~Andhra Pradesh~Uttar Pradesh~Uttarakhand~Odisha~Karnataka~Kerala~Gujarat~Goa~Chandigarh~Chhattisgarh~Jammu & Kashmir~Jharkhand~Tamil Nadu~Telangana~Tripura~Daman & Diu~Dadra & Nagar Haveli~Delhi~Nagaland~Punjab~West Bengal~Puducherry~Bihar~Manipur~Madhya Pradesh~Maharashtra~Mizoram~Meghalaya~Rajasthan~Lakshadweep~Sikkim~Haryana~Himachal Pradesh",
+ sub_names:
+ "अंडमान और निकोबार द्वीपसमूह~अरुणाचल प्रदेश~असम~आंध्र प्रदेश~उत्तर प्रदेश~उत्तराखण्ड~ओड़िशा~कर्नाटक~केरल~गुजरात~गोआ~चंडीगढ़~छत्तीसगढ़~जम्मू और कश्मीर~झारखण्ड~तमिल नाडु~तेलंगाना~त्रिपुरा~दमन और दीव~दादरा और नगर हवेली~दिल्ली~नागालैंड~पंजाब~पश्चिम बंगाल~पांडिचेरी~बिहार~मणिपुर~मध्य प्रदेश~महाराष्ट्र~मिजोरम~मेघालय~राजस्थान~लक्षद्वीप~सिक्किम~हरियाणा~हिमाचल प्रदेश",
+ sub_zips:
+ "744~79[0-2]~78~5[0-3]~2[0-35-8]|24[0-7]|26[12]~24[46-9]|254|26[23]~7[5-7]~5[4-9]|53[7-9]~6[7-9]|6010|607008|777~3[6-9]~403~16|1440[3-9]~49~1[89]~81[4-9]|82|83[0-5]~6[0-6]|536~5[0-3]~799~396~396~11~79[78]~1[456]~7[0-4]~60[579]~8[0-5]~79[56]~4[5-8]|490~4[0-4]~796~79[34]~3[0-4]~682~737|750~1[23]~17",
+ zip: "\\d{6}",
+ zip_name_type: "pin",
+ zipex: "110034,110001",
+ },
+ "data/IO": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/IO",
+ key: "IO",
+ name: "BRITISH INDIAN OCEAN TERRITORY",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "BBND 1ZZ",
+ zipex: "BBND 1ZZ",
+ },
+ "data/IQ": {
+ fmt: "%O%n%N%n%A%n%C, %S%n%Z",
+ id: "data/IQ",
+ key: "IQ",
+ name: "IRAQ",
+ require: "ACS",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "31001",
+ },
+ "data/IR": {
+ fmt: "%O%n%N%n%S%n%C, %D%n%A%n%Z",
+ id: "data/IR",
+ key: "IR",
+ lang: "fa",
+ languages: "fa",
+ name: "IRAN",
+ sub_isoids:
+ "01~02~03~04~32~05~06~07~08~29~30~31~10~11~12~13~14~28~26~16~15~17~18~27~19~20~21~22~23~24~25",
+ sub_keys:
+ "استان آذربایجان شرقی~استان آذربایجان غربی~استان اردبیل~استان اصفهان~استان البرز~استان ایلام~استان بوشهر~استان تهران~استان چهارمحال و بختیاری~استان خراسان جنوبی~استان خراسان رضوی~استان خراسان شمالی~استان خوزستان~استان زنجان~استان سمنان~استان سیستان و بلوچستان~استان فارس~استان قزوین~استان قم~استان کردستان~استان کرمان~استان کرمانشاه~استان کهگیلویه و بویراحمد~استان گلستان~استان گیلان~استان لرستان~استان مازندران~استان مرکزی~استان هرمزگان~استان همدان~استان یزد",
+ sub_lnames:
+ "East Azerbaijan Province~West Azerbaijan Province~Ardabil Province~Isfahan Province~Alborz Province~Ilam Province~Bushehr Province~Tehran Province~Chaharmahal and Bakhtiari Province~South Khorasan Province~Razavi Khorasan Province~North Khorasan Province~Khuzestan Province~Zanjan Province~Semnan Province~Sistan and Baluchestan Province~Fars Province~Qazvin Province~Qom Province~Kurdistan Province~Kerman Province~Kermanshah Province~Kohgiluyeh and Boyer-Ahmad Province~Golestan Province~Gilan Province~Lorestan Province~Mazandaran Province~Markazi Province~Hormozgan Province~Hamadan Province~Yazd Province",
+ sublocality_name_type: "neighborhood",
+ zip: "\\d{5}-?\\d{5}",
+ zipex: "11936-12345",
+ },
+ "data/IS": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/IS",
+ key: "IS",
+ name: "ICELAND",
+ posturl: "http://www.postur.is/einstaklingar/posthus/postnumer/",
+ zip: "\\d{3}",
+ zipex: "320,121,220,110",
+ },
+ "data/IT": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/IT",
+ key: "IT",
+ lang: "it",
+ languages: "it",
+ name: "ITALY",
+ posturl: "http://www.poste.it/online/cercacap/",
+ require: "ACSZ",
+ sub_isoids:
+ "AG~AL~AN~AO~AR~AP~AT~AV~BA~BT~BL~BN~BG~BI~BO~BZ~BS~BR~CA~CL~CB~CI~CE~CT~CZ~CH~CO~CS~CR~KR~CN~EN~FM~FE~FI~FG~FC~FR~GE~GO~GR~IM~IS~AQ~SP~LT~LE~LC~LI~LO~LU~MC~MN~MS~MT~VS~ME~MI~MO~MB~NA~NO~NU~OG~OT~OR~PD~PA~PR~PV~PG~PU~PE~PC~PI~PT~PN~PZ~PO~RG~RA~RC~RE~RI~RN~RM~RO~SA~SS~SV~SI~SR~SO~TA~TE~TR~TO~TP~TN~TV~TS~UD~VA~VE~VB~VC~VR~VV~VI~VT",
+ sub_keys:
+ "AG~AL~AN~AO~AR~AP~AT~AV~BA~BT~BL~BN~BG~BI~BO~BZ~BS~BR~CA~CL~CB~CI~CE~CT~CZ~CH~CO~CS~CR~KR~CN~EN~FM~FE~FI~FG~FC~FR~GE~GO~GR~IM~IS~AQ~SP~LT~LE~LC~LI~LO~LU~MC~MN~MS~MT~VS~ME~MI~MO~MB~NA~NO~NU~OG~OT~OR~PD~PA~PR~PV~PG~PU~PE~PC~PI~PT~PN~PZ~PO~RG~RA~RC~RE~RI~RN~RM~RO~SA~SS~SV~SI~SR~SO~TA~TE~TR~TO~TP~TN~TV~TS~UD~VA~VE~VB~VC~VR~VV~VI~VT",
+ sub_names:
+ "Agrigento~Alessandria~Ancona~Aosta~Arezzo~Ascoli Piceno~Asti~Avellino~Bari~Barletta-Andria-Trani~Belluno~Benevento~Bergamo~Biella~Bologna~Bolzano~Brescia~Brindisi~Cagliari~Caltanissetta~Campobasso~Carbonia-Iglesias~Caserta~Catania~Catanzaro~Chieti~Como~Cosenza~Cremona~Crotone~Cuneo~Enna~Fermo~Ferrara~Firenze~Foggia~Forlì-Cesena~Frosinone~Genova~Gorizia~Grosseto~Imperia~Isernia~L'Aquila~La Spezia~Latina~Lecce~Lecco~Livorno~Lodi~Lucca~Macerata~Mantova~Massa-Carrara~Matera~Medio Campidano~Messina~Milano~Modena~Monza e Brianza~Napoli~Novara~Nuoro~Ogliastra~Olbia-Tempio~Oristano~Padova~Palermo~Parma~Pavia~Perugia~Pesaro e Urbino~Pescara~Piacenza~Pisa~Pistoia~Pordenone~Potenza~Prato~Ragusa~Ravenna~Reggio Calabria~Reggio Emilia~Rieti~Rimini~Roma~Rovigo~Salerno~Sassari~Savona~Siena~Siracusa~Sondrio~Taranto~Teramo~Terni~Torino~Trapani~Trento~Treviso~Trieste~Udine~Varese~Venezia~Verbano-Cusio-Ossola~Vercelli~Verona~Vibo Valentia~Vicenza~Viterbo",
+ sub_zips:
+ "92~15~60~11~52~63~14~83~70~76[01]~32~82~24~13[89]~40~39~25~72~0912[1-9]|0913[0-4]|0901[0289]|0902[03468]|0903[0234]|0904|0803[035]|08043~93~860[1-4]|86100~0901[013-7]~81~95~88[01]~66~22~87~26[01]~88[89]~12|18025~94~638|63900~44~50~71~47[015]~03~16~34[01]7~58~18~860[7-9]|86170~67~19~04~73~23[89]~57~26[89]~55~62~46~54~75~0902[012579]|0903[015-9]|09040~98~20~41~208|20900~80~28[01]~080[1-3]|08100~08037|0804[024-9]~08020|0702|0703[08]~090[7-9]|09170|0801[039]|0803[04]~35~90~43~27~06~61~65~29~56~51~330[7-9]|33170~85~59~97~48~89[01]~42~02~47[89]~00~45~84~070[14]|0703[0-79]|07100~17|12071~53~96~23[01]~74~64~05~10~91~38~31~3401|341[0-689]|34062~330[1-5]|33100~21~30~28[89]~13[01]~37~89[89]~36~01",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "00144,47037,39049",
+ },
+ "data/JE": {
+ fmt: "%N%n%O%n%A%n%C%nJERSEY%n%Z",
+ id: "data/JE",
+ key: "JE",
+ name: "CHANNEL ISLANDS",
+ posturl: "http://www.jerseypost.com/tools/postcode-address-finder/",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "JE\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}",
+ zipex: "JE1 1AA,JE2 2BT",
+ },
+ "data/JM": {
+ fmt: "%N%n%O%n%A%n%C%n%S %X",
+ id: "data/JM",
+ key: "JM",
+ lang: "en",
+ languages: "en",
+ name: "JAMAICA",
+ require: "ACS",
+ state_name_type: "parish",
+ sub_isoids: "13~09~01~12~04~02~06~14~11~08~05~03~07~10",
+ sub_keys:
+ "Clarendon~Hanover~Kingston~Manchester~Portland~St. Andrew~St. Ann~St. Catherine~St. Elizabeth~St. James~St. Mary~St. Thomas~Trelawny~Westmoreland",
+ },
+ "data/JO": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/JO",
+ key: "JO",
+ name: "JORDAN",
+ zip: "\\d{5}",
+ zipex: "11937,11190",
+ },
+ "data/JP": {
+ fmt: "〒%Z%n%S%n%A%n%O%n%N",
+ id: "data/JP",
+ key: "JP",
+ lang: "ja",
+ languages: "ja",
+ lfmt: "%N%n%O%n%A, %S%n%Z",
+ name: "JAPAN",
+ posturl: "http://www.post.japanpost.jp/zipcode/",
+ require: "ASZ",
+ state_name_type: "prefecture",
+ sub_isoids:
+ "01~02~03~04~05~06~07~08~09~10~11~12~13~14~15~16~17~18~19~20~21~22~23~24~25~26~27~28~29~30~31~32~33~34~35~36~37~38~39~40~41~42~43~44~45~46~47",
+ sub_keys:
+ "北海道~青森県~岩手県~宮城県~秋田県~山形県~福島県~茨城県~栃木県~群馬県~埼玉県~千葉県~東京都~神奈川県~新潟県~富山県~石川県~福井県~山梨県~長野県~岐阜県~静岡県~愛知県~三重県~滋賀県~京都府~大阪府~兵庫県~奈良県~和歌山県~鳥取県~島根県~岡山県~広島県~山口県~徳島県~香川県~愛媛県~高知県~福岡県~佐賀県~長崎県~熊本県~大分県~宮崎県~鹿児島県~沖縄県",
+ sub_lnames:
+ "Hokkaido~Aomori~Iwate~Miyagi~Akita~Yamagata~Fukushima~Ibaraki~Tochigi~Gunma~Saitama~Chiba~Tokyo~Kanagawa~Niigata~Toyama~Ishikawa~Fukui~Yamanashi~Nagano~Gifu~Shizuoka~Aichi~Mie~Shiga~Kyoto~Osaka~Hyogo~Nara~Wakayama~Tottori~Shimane~Okayama~Hiroshima~Yamaguchi~Tokushima~Kagawa~Ehime~Kochi~Fukuoka~Saga~Nagasaki~Kumamoto~Oita~Miyazaki~Kagoshima~Okinawa",
+ sub_zips:
+ "0[4-9]|00[1-7]~03|018~02~98~01~99~9[67]~3[01]~32|311|349~37|38[49]~3[3-6]~2[6-9]~1[0-8]|19[0-8]|20~2[1-5]|199~9[45]|389~93~92|939~91|922~40~3[89]|949~50~4[1-9]~4[4-9]|431~51|498|647~52~6[0-2]|520~5[3-9]|618|630~6[5-7]|563~63|64[78]~64|519~68~69|68[45]~7[01]~7[23]~7[45]~77~76~79~78~8[0-3]|871~84~85|81[17]|848~86~87|839~88~89~90",
+ upper: "S",
+ zip: "\\d{3}-?\\d{4}",
+ zipex: "154-0023,350-1106,951-8073,112-0001,208-0032,231-0012",
+ },
+ "data/KE": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/KE",
+ key: "KE",
+ name: "KENYA",
+ zip: "\\d{5}",
+ zipex: "20100,00100",
+ },
+ "data/KG": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/KG",
+ key: "KG",
+ name: "KYRGYZSTAN",
+ zip: "\\d{6}",
+ zipex: "720001",
+ },
+ "data/KH": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/KH",
+ key: "KH",
+ name: "CAMBODIA",
+ zip: "\\d{5}",
+ zipex: "12203,14206,12000",
+ },
+ "data/KI": {
+ fmt: "%N%n%O%n%A%n%S%n%C",
+ id: "data/KI",
+ key: "KI",
+ name: "KIRIBATI",
+ state_name_type: "island",
+ upper: "ACNOS",
+ },
+ "data/KM": { id: "data/KM", key: "KM", name: "COMOROS", upper: "AC" },
+ "data/KN": {
+ fmt: "%N%n%O%n%A%n%C, %S",
+ id: "data/KN",
+ key: "KN",
+ lang: "en",
+ languages: "en",
+ name: "SAINT KITTS AND NEVIS",
+ require: "ACS",
+ state_name_type: "island",
+ sub_isoids: "N~K",
+ sub_keys: "Nevis~St. Kitts",
+ },
+ "data/KP": {
+ fmt: "%Z%n%S%n%C%n%A%n%O%n%N",
+ id: "data/KP",
+ key: "KP",
+ lang: "ko",
+ languages: "ko",
+ lfmt: "%N%n%O%n%A%n%C%n%S, %Z",
+ name: "NORTH KOREA",
+ sub_isoids: "07~13~10~04~02~03~01~08~09~05~06",
+ sub_keys:
+ "강원도~라선 특별시~량강도~자강도~평안 남도~평안 북도~평양 직할시~함경 남도~함경 북도~황해남도~황해북도",
+ sub_lnames:
+ "Kangwon~Rason~Ryanggang~Chagang~South Pyongan~North Pyongan~Pyongyang~South Hamgyong~North Hamgyong~South Hwanghae~North Hwanghae",
+ },
+ "data/KR": {
+ fmt: "%S %C%D%n%A%n%O%n%N%n%Z",
+ id: "data/KR",
+ key: "KR",
+ lang: "ko",
+ languages: "ko",
+ lfmt: "%N%n%O%n%A%n%D%n%C%n%S%n%Z",
+ name: "SOUTH KOREA",
+ posturl: "http://www.epost.go.kr/search/zipcode/search5.jsp",
+ require: "ACSZ",
+ state_name_type: "do_si",
+ sub_isoids: "42~41~48~47~29~27~30~26~11~50~31~28~46~45~49~44~43",
+ sub_keys:
+ "강원도~경기도~경상남도~경상북도~광주광역시~대구광역시~대전광역시~부산광역시~서울특별시~세종특별자치시~울산광역시~인천광역시~전라남도~전라북도~제주특별자치도~충청남도~충청북도",
+ sub_lnames:
+ "Gangwon-do~Gyeonggi-do~Gyeongsangnam-do~Gyeongsangbuk-do~Gwangju~Daegu~Daejeon~Busan~Seoul~Sejong~Ulsan~Incheon~Jeollanam-do~Jeollabuk-do~Jeju-do~Chungcheongnam-do~Chungcheongbuk-do",
+ sub_mores:
+ "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true",
+ sub_names:
+ "강원~경기~경남~경북~광주~대구~대전~부산~서울~세종~울산~인천~전남~전북~제주~충남~충북",
+ sub_zipexs:
+ "25627~12410~53286~38540~62394~42456~34316~46706~06321~30065~44782~23024~59222~56445~63563~32832~28006",
+ sub_zips:
+ "2[456]\\d{2}~1[0-8]\\d{2}~5[0-3]\\d{2}~(?:3[6-9]|40)\\d{2}~6[12]\\d{2}~4[12]\\d{2}~3[45]\\d{2}~4[6-9]\\d{2}~0[1-8]\\d{2}~30[01]\\d~4[45]\\d{2}~2[1-3]\\d{2}~5[7-9]\\d{2}~5[4-6]\\d{2}~63[0-356]\\d~3[1-3]\\d{2}~2[789]\\d{2}",
+ sublocality_name_type: "district",
+ upper: "Z",
+ zip: "\\d{5}",
+ zipex: "03051",
+ },
+ "data/KW": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/KW",
+ key: "KW",
+ name: "KUWAIT",
+ zip: "\\d{5}",
+ zipex: "54541,54551,54404,13009",
+ },
+ "data/KY": {
+ fmt: "%N%n%O%n%A%n%S %Z",
+ id: "data/KY",
+ key: "KY",
+ lang: "en",
+ languages: "en",
+ name: "CAYMAN ISLANDS",
+ posturl: "http://www.caymanpost.gov.ky/",
+ require: "AS",
+ state_name_type: "island",
+ sub_keys: "Cayman Brac~Grand Cayman~Little Cayman",
+ zip: "KY\\d-\\d{4}",
+ zipex: "KY1-1100,KY1-1702,KY2-2101",
+ },
+ "data/KZ": {
+ fmt: "%Z%n%S%n%C%n%A%n%O%n%N",
+ id: "data/KZ",
+ key: "KZ",
+ name: "KAZAKHSTAN",
+ zip: "\\d{6}",
+ zipex: "040900,050012",
+ },
+ "data/LA": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/LA",
+ key: "LA",
+ name: "LAO (PEOPLE'S DEM. REP.)",
+ zip: "\\d{5}",
+ zipex: "01160,01000",
+ },
+ "data/LB": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/LB",
+ key: "LB",
+ name: "LEBANON",
+ zip: "(?:\\d{4})(?: ?(?:\\d{4}))?",
+ zipex: "2038 3054,1107 2810,1000",
+ },
+ "data/LC": { id: "data/LC", key: "LC", name: "SAINT LUCIA" },
+ "data/LI": {
+ fmt: "%O%n%N%n%A%nFL-%Z %C",
+ id: "data/LI",
+ key: "LI",
+ name: "LIECHTENSTEIN",
+ postprefix: "FL-",
+ posturl: "http://www.post.ch/db/owa/pv_plz_pack/pr_main",
+ require: "ACZ",
+ zip: "948[5-9]|949[0-8]",
+ zipex: "9496,9491,9490,9485",
+ },
+ "data/LK": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/LK",
+ key: "LK",
+ name: "SRI LANKA",
+ posturl: "http://www.slpost.gov.lk/",
+ zip: "\\d{5}",
+ zipex: "20000,00100",
+ },
+ "data/LR": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/LR",
+ key: "LR",
+ name: "LIBERIA",
+ zip: "\\d{4}",
+ zipex: "1000",
+ },
+ "data/LS": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/LS",
+ key: "LS",
+ name: "LESOTHO",
+ zip: "\\d{3}",
+ zipex: "100",
+ },
+ "data/LT": {
+ fmt: "%O%n%N%n%A%nLT-%Z %C",
+ id: "data/LT",
+ key: "LT",
+ name: "LITHUANIA",
+ postprefix: "LT-",
+ posturl: "http://www.post.lt/lt/?id=316",
+ zip: "\\d{5}",
+ zipex: "04340,03500",
+ },
+ "data/LU": {
+ fmt: "%O%n%N%n%A%nL-%Z %C",
+ id: "data/LU",
+ key: "LU",
+ name: "LUXEMBOURG",
+ postprefix: "L-",
+ posturl:
+ "https://www.post.lu/fr/grandes-entreprises/solutions-postales/rechercher-un-code-postal",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "4750,2998",
+ },
+ "data/LV": {
+ fmt: "%N%n%O%n%A%n%C, %Z",
+ id: "data/LV",
+ key: "LV",
+ name: "LATVIA",
+ posturl: "http://www.pasts.lv/lv/uzzinas/nodalas/",
+ zip: "LV-\\d{4}",
+ zipex: "LV-1073,LV-1000",
+ },
+ "data/LY": { id: "data/LY", key: "LY", name: "LIBYA" },
+ "data/MA": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/MA",
+ key: "MA",
+ name: "MOROCCO",
+ zip: "\\d{5}",
+ zipex: "53000,10000,20050,16052",
+ },
+ "data/MC": {
+ fmt: "%N%n%O%n%A%nMC-%Z %C %X",
+ id: "data/MC",
+ key: "MC",
+ name: "MONACO",
+ postprefix: "MC-",
+ zip: "980\\d{2}",
+ zipex: "98000,98020,98011,98001",
+ },
+ "data/MD": {
+ fmt: "%N%n%O%n%A%nMD-%Z %C",
+ id: "data/MD",
+ key: "MD",
+ name: "Rep. MOLDOVA",
+ postprefix: "MD-",
+ zip: "\\d{4}",
+ zipex: "2012,2019",
+ },
+ "data/ME": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/ME",
+ key: "ME",
+ name: "MONTENEGRO",
+ zip: "8\\d{4}",
+ zipex: "81257,81258,81217,84314,85366",
+ },
+ "data/MF": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/MF",
+ key: "MF",
+ name: "SAINT MARTIN",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78][01]\\d{2}",
+ zipex: "97100",
+ },
+ "data/MG": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/MG",
+ key: "MG",
+ name: "MADAGASCAR",
+ zip: "\\d{3}",
+ zipex: "501,101",
+ },
+ "data/MH": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/MH",
+ key: "MH",
+ name: "MARSHALL ISLANDS",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACSZ",
+ state_name_type: "state",
+ upper: "ACNOS",
+ zip: "(969[67]\\d)(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "96960,96970",
+ },
+ "data/MK": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/MK",
+ key: "MK",
+ name: "MACEDONIA",
+ zip: "\\d{4}",
+ zipex: "1314,1321,1443,1062",
+ },
+ "data/ML": { id: "data/ML", key: "ML", name: "MALI" },
+ "data/MM": {
+ fmt: "%N%n%O%n%A%n%C, %Z",
+ id: "data/MM",
+ key: "MM",
+ name: "MYANMAR",
+ zip: "\\d{5}",
+ zipex: "11181",
+ },
+ "data/MN": {
+ fmt: "%N%n%O%n%A%n%C%n%S %Z",
+ id: "data/MN",
+ key: "MN",
+ name: "MONGOLIA",
+ posturl: "http://www.zipcode.mn/",
+ zip: "\\d{5}",
+ zipex: "65030,65270",
+ },
+ "data/MO": {
+ fmt: "%A%n%O%n%N",
+ id: "data/MO",
+ key: "MO",
+ lfmt: "%N%n%O%n%A",
+ name: "MACAO",
+ require: "A",
+ },
+ "data/MP": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/MP",
+ key: "MP",
+ name: "NORTHERN MARIANA ISLANDS",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACSZ",
+ state_name_type: "state",
+ upper: "ACNOS",
+ zip: "(9695[012])(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "96950,96951,96952",
+ },
+ "data/MQ": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/MQ",
+ key: "MQ",
+ name: "MARTINIQUE",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78]2\\d{2}",
+ zipex: "97220",
+ },
+ "data/MR": { id: "data/MR", key: "MR", name: "MAURITANIA", upper: "AC" },
+ "data/MS": { id: "data/MS", key: "MS", name: "MONTSERRAT" },
+ "data/MT": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/MT",
+ key: "MT",
+ name: "MALTA",
+ posturl: "http://postcodes.maltapost.com/",
+ upper: "CZ",
+ zip: "[A-Z]{3} ?\\d{2,4}",
+ zipex: "NXR 01,ZTN 05,GPO 01,BZN 1130,SPB 6031,VCT 1753",
+ },
+ "data/MU": {
+ fmt: "%N%n%O%n%A%n%Z%n%C",
+ id: "data/MU",
+ key: "MU",
+ name: "MAURITIUS",
+ upper: "CZ",
+ zip: "\\d{3}(?:\\d{2}|[A-Z]{2}\\d{3})",
+ zipex: "42602",
+ },
+ "data/MV": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/MV",
+ key: "MV",
+ name: "MALDIVES",
+ posturl: "http://www.maldivespost.com/?lid=10",
+ zip: "\\d{5}",
+ zipex: "20026",
+ },
+ "data/MW": {
+ fmt: "%N%n%O%n%A%n%C %X",
+ id: "data/MW",
+ key: "MW",
+ name: "MALAWI",
+ },
+ "data/MX": {
+ fmt: "%N%n%O%n%A%n%D%n%Z %C, %S",
+ id: "data/MX",
+ key: "MX",
+ lang: "es",
+ languages: "es",
+ name: "MEXICO",
+ posturl:
+ "http://www.correosdemexico.gob.mx/ServiciosLinea/Paginas/ccpostales.aspx",
+ require: "ACZ",
+ state_name_type: "state",
+ sub_isoids:
+ "AGU~BCN~BCS~CAM~CHP~CHH~CMX~COA~COL~DUR~MEX~GUA~GRO~HID~JAL~MIC~MOR~NAY~NLE~OAX~PUE~QUE~ROO~SLP~SIN~SON~TAB~TAM~TLA~VER~YUC~ZAC",
+ sub_keys:
+ "Ags.~B.C.~B.C.S.~Camp.~Chis.~Chih.~CDMX~Coah.~Col.~Dgo.~Méx.~Gto.~Gro.~Hgo.~Jal.~Mich.~Mor.~Nay.~N.L.~Oax.~Pue.~Qro.~Q.R.~S.L.P.~Sin.~Son.~Tab.~Tamps.~Tlax.~Ver.~Yuc.~Zac.",
+ sub_names:
+ "Aguascalientes~Baja California~Baja California Sur~Campeche~Chiapas~Chihuahua~Ciudad de México~Coahuila de Zaragoza~Colima~Durango~Estado de México~Guanajuato~Guerrero~Hidalgo~Jalisco~Michoacán~Morelos~Nayarit~Nuevo León~Oaxaca~Puebla~Querétaro~Quintana Roo~San Luis Potosí~Sinaloa~Sonora~Tabasco~Tamaulipas~Tlaxcala~Veracruz~Yucatán~Zacatecas",
+ sub_zipexs:
+ "20000,20999~21000,22999~23000,23999~24000,24999~29000,30999~31000,33999~00000,16999~25000,27999~28000,28999~34000,35999~50000,57999~36000,38999~39000,41999~42000,43999~44000,49999~58000,61999~62000,62999~63000,63999~64000,67999~68000,71999~72000,75999~76000,76999~77000,77999~78000,79999~80000,82999~83000,85999~86000,86999~87000,89999~90000,90999~91000,96999~97000,97999~98000,99999",
+ sub_zips:
+ "20~2[12]~23~24~29|30~3[1-3]~0|1[0-6]~2[5-7]~28~3[45]~5[0-7]~3[6-8]~39|4[01]~4[23]~4[4-9]~5[89]|6[01]~62~63~6[4-7]~6[89]|7[01]~7[2-5]~76~77~7[89]~8[0-2]~8[3-5]~86~8[7-9]~90~9[1-6]~97~9[89]",
+ sublocality_name_type: "neighborhood",
+ upper: "CSZ",
+ zip: "\\d{5}",
+ zipex: "02860,77520,06082",
+ },
+ "data/MY": {
+ fmt: "%N%n%O%n%A%n%D%n%Z %C%n%S",
+ id: "data/MY",
+ key: "MY",
+ lang: "ms",
+ languages: "ms",
+ name: "MALAYSIA",
+ posturl: "http://www.pos.com.my",
+ require: "ACZ",
+ state_name_type: "state",
+ sub_isoids: "01~02~03~14~15~04~05~06~08~09~07~16~12~13~10~11",
+ sub_keys:
+ "Johor~Kedah~Kelantan~Kuala Lumpur~Labuan~Melaka~Negeri Sembilan~Pahang~Perak~Perlis~Pulau Pinang~Putrajaya~Sabah~Sarawak~Selangor~Terengganu",
+ sub_zipexs:
+ "79000,86999~05000,09999,34950~15000,18599~50000,60000~87000,87999~75000,78399~70000,73599~25000,28999,39000,49000,69000~30000,36899,39000~01000,02799~10000,14999~62000,62999~88000,91999~93000,98999~40000,48999,63000,68199~20000,24999",
+ sub_zips:
+ "79|8[0-6]~0[5-9]|34950~1[5-9]~5|60~87~7[5-8]~7[0-4]~2[5-8]|[346]9~3[0-6]|39000~0[12]~1[0-4]~62~8[89]|9[01]~9[3-8]~4[0-8]|6[3-8]~2[0-4]",
+ sublocality_name_type: "village_township",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "43000,50754,88990,50670",
+ },
+ "data/MZ": {
+ fmt: "%N%n%O%n%A%n%Z %C%S",
+ id: "data/MZ",
+ key: "MZ",
+ lang: "pt",
+ languages: "pt",
+ name: "MOZAMBIQUE",
+ sub_isoids: "P~MPM~G~I~B~L~N~A~S~T~Q",
+ sub_keys:
+ "Cabo Delgado~Cidade de Maputo~Gaza~Inhambane~Manica~Maputo~Nampula~Niassa~Sofala~Tete~Zambezia",
+ zip: "\\d{4}",
+ zipex: "1102,1119,3212",
+ },
+ "data/NA": { id: "data/NA", key: "NA", name: "NAMIBIA" },
+ "data/NC": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/NC",
+ key: "NC",
+ name: "NEW CALEDONIA",
+ posturl:
+ "http://poste.opt.nc/index.php?option=com_content&view=article&id=80&Itemid=131",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "988\\d{2}",
+ zipex: "98814,98800,98810",
+ },
+ "data/NE": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/NE",
+ key: "NE",
+ name: "NIGER",
+ zip: "\\d{4}",
+ zipex: "8001",
+ },
+ "data/NF": {
+ fmt: "%O%n%N%n%A%n%C %S %Z",
+ id: "data/NF",
+ key: "NF",
+ name: "NORFOLK ISLAND",
+ upper: "CS",
+ zip: "2899",
+ zipex: "2899",
+ },
+ "data/NG": {
+ fmt: "%N%n%O%n%A%n%D%n%C %Z%n%S",
+ id: "data/NG",
+ key: "NG",
+ lang: "en",
+ languages: "en",
+ name: "NIGERIA",
+ posturl: "http://www.nigeriapostcodes.com/",
+ state_name_type: "state",
+ sub_isoids:
+ "AB~AD~AK~AN~BA~BY~BE~BO~CR~DE~EB~ED~EK~EN~FC~GO~IM~JI~KD~KN~KT~KE~KO~KW~LA~NA~NI~OG~ON~OS~OY~PL~RI~SO~TA~YO~ZA",
+ sub_keys:
+ "Abia~Adamawa~Akwa Ibom~Anambra~Bauchi~Bayelsa~Benue~Borno~Cross River~Delta~Ebonyi~Edo~Ekiti~Enugu~Federal Capital Territory~Gombe~Imo~Jigawa~Kaduna~Kano~Katsina~Kebbi~Kogi~Kwara~Lagos~Nasarawa~Niger~Ogun State~Ondo~Osun~Oyo~Plateau~Rivers~Sokoto~Taraba~Yobe~Zamfara",
+ upper: "CS",
+ zip: "\\d{6}",
+ zipex: "930283,300001,931104",
+ },
+ "data/NI": {
+ fmt: "%N%n%O%n%A%n%Z%n%C, %S",
+ id: "data/NI",
+ key: "NI",
+ lang: "es",
+ languages: "es",
+ name: "NICARAGUA",
+ posturl: "http://www.correos.gob.ni/index.php/codigo-postal-2",
+ state_name_type: "department",
+ sub_isoids: "BO~CA~CI~CO~ES~GR~JI~LE~MD~MN~MS~MT~NS~AN~AS~SJ~RI",
+ sub_keys:
+ "Boaco~Carazo~Chinandega~Chontales~Esteli~Granada~Jinotega~Leon~Madriz~Managua~Masaya~Matagalpa~Nueva Segovia~Raan~Raas~Rio San Juan~Rivas",
+ sub_zips:
+ "5[12]~4[56]~2[5-7]~5[56]~3[12]~4[34]~6[56]~2[12]~3[45]~1[0-6]~4[12]~6[1-3]~3[7-9]~7[12]~8[1-3]~9[12]~4[78]",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "52000",
+ },
+ "data/NL": {
+ fmt: "%O%n%N%n%A%n%Z %C",
+ id: "data/NL",
+ key: "NL",
+ name: "NETHERLANDS",
+ posturl: "http://www.postnl.nl/voorthuis/",
+ require: "ACZ",
+ zip: "\\d{4} ?[A-Z]{2}",
+ zipex: "1234 AB,2490 AA",
+ },
+ "data/NO": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/NO",
+ key: "NO",
+ locality_name_type: "post_town",
+ name: "NORWAY",
+ posturl: "http://adressesok.posten.no/nb/postal_codes/search",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "0025,0107,6631",
+ },
+ "data/NP": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/NP",
+ key: "NP",
+ name: "NEPAL",
+ posturl: "http://www.gpo.gov.np/Home/Postalcode",
+ zip: "\\d{5}",
+ zipex: "44601",
+ },
+ "data/NR": {
+ fmt: "%N%n%O%n%A%n%S",
+ id: "data/NR",
+ key: "NR",
+ lang: "en",
+ languages: "en",
+ name: "NAURU CENTRAL PACIFIC",
+ require: "AS",
+ state_name_type: "district",
+ sub_isoids: "01~02~03~04~05~06~07~08~09~10~11~12~13~14",
+ sub_keys:
+ "Aiwo District~Anabar District~Anetan District~Anibare District~Baiti District~Boe District~Buada District~Denigomodu District~Ewa District~Ijuw District~Meneng District~Nibok District~Uaboe District~Yaren District",
+ },
+ "data/NU": { id: "data/NU", key: "NU", name: "NIUE" },
+ "data/NZ": {
+ fmt: "%N%n%O%n%A%n%D%n%C %Z",
+ id: "data/NZ",
+ key: "NZ",
+ name: "NEW ZEALAND",
+ posturl:
+ "http://www.nzpost.co.nz/Cultures/en-NZ/OnlineTools/PostCodeFinder/",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "6001,6015,6332,8252,1030",
+ },
+ "data/OM": {
+ fmt: "%N%n%O%n%A%n%Z%n%C",
+ id: "data/OM",
+ key: "OM",
+ name: "OMAN",
+ zip: "(?:PC )?\\d{3}",
+ zipex: "133,112,111",
+ },
+ "data/PA": {
+ fmt: "%N%n%O%n%A%n%C%n%S",
+ id: "data/PA",
+ key: "PA",
+ name: "PANAMA (REP.)",
+ upper: "CS",
+ },
+ "data/PE": {
+ fmt: "%N%n%O%n%A%n%C %Z%n%S",
+ id: "data/PE",
+ key: "PE",
+ lang: "es",
+ languages: "es",
+ locality_name_type: "district",
+ name: "PERU",
+ posturl: "http://www.serpost.com.pe/cpostal/codigo",
+ sub_isoids:
+ "AMA~ANC~APU~ARE~AYA~CAJ~CAL~CUS~LIM~HUV~HUC~ICA~JUN~LAL~LAM~LOR~MDD~MOQ~LMA~PAS~PIU~PUN~SAM~TAC~TUM~UCA",
+ sub_keys:
+ "Amazonas~Áncash~Apurímac~Arequipa~Ayacucho~Cajamarca~Callao~Cuzco~Gobierno Regional de Lima~Huancavelica~Huánuco~Ica~Junín~La Libertad~Lambayeque~Loreto~Madre de Dios~Moquegua~Municipalidad Metropolitana de Lima~Pasco~Piura~Puno~San Martín~Tacna~Tumbes~Ucayali",
+ zip: "(?:LIMA \\d{1,2}|CALLAO 0?\\d)|[0-2]\\d{4}",
+ zipex: "LIMA 23,LIMA 42,CALLAO 2,02001",
+ },
+ "data/PF": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/PF",
+ key: "PF",
+ name: "FRENCH POLYNESIA",
+ require: "ACSZ",
+ state_name_type: "island",
+ upper: "CS",
+ zip: "987\\d{2}",
+ zipex: "98709",
+ },
+ "data/PG": {
+ fmt: "%N%n%O%n%A%n%C %Z %S",
+ id: "data/PG",
+ key: "PG",
+ name: "PAPUA NEW GUINEA",
+ require: "ACS",
+ zip: "\\d{3}",
+ zipex: "111",
+ },
+ "data/PH": {
+ fmt: "%N%n%O%n%A%n%D, %C%n%Z %S",
+ id: "data/PH",
+ key: "PH",
+ lang: "en",
+ languages: "en",
+ name: "PHILIPPINES",
+ posturl: "http://www.philpost.gov.ph/",
+ sub_isoids:
+ "ABR~AGN~AGS~AKL~ALB~ANT~APA~AUR~BAS~BAN~BTN~BTG~BEN~BIL~BOH~BUK~BUL~CAG~CAN~CAS~CAM~CAP~CAT~CAV~CEB~COM~NCO~DAV~DAS~DVO~DAO~DIN~EAS~GUI~IFU~ILN~ILS~ILI~ISA~KAL~LUN~LAG~LAN~LAS~LEY~MAG~MAD~MAS~00~MDC~MDR~MSC~MSR~MOU~NEC~NER~NSA~NUE~NUV~PLW~PAM~PAN~QUE~QUI~RIZ~ROM~WSA~SAR~SIG~SOR~SCO~SLE~SUK~SLU~SUN~SUR~TAR~TAW~ZMB~ZAN~ZAS~ZSI",
+ sub_keys:
+ "Abra~Agusan del Norte~Agusan del Sur~Aklan~Albay~Antique~Apayao~Aurora~Basilan~Bataan~Batanes~Batangas~Benguet~Biliran~Bohol~Bukidnon~Bulacan~Cagayan~Camarines Norte~Camarines Sur~Camiguin~Capiz~Catanduanes~Cavite~Cebu~Compostela Valley~Cotabato~Davao del Norte~Davao del Sur~Davao Occidental~Davao Oriental~Dinagat Islands~Eastern Samar~Guimaras~Ifugao~Ilocos Norte~Ilocos Sur~Iloilo~Isabela~Kalinga~La Union~Laguna~Lanao del Norte~Lanao del Sur~Leyte~Maguindanao~Marinduque~Masbate~Metro Manila~Mindoro Occidental~Mindoro Oriental~Misamis Occidental~Misamis Oriental~Mountain Province~Negros Occidental~Negros Oriental~Northern Samar~Nueva Ecija~Nueva Vizcaya~Palawan~Pampanga~Pangasinan~Quezon Province~Quirino~Rizal~Romblon~Samar~Sarangani~Siquijor~Sorsogon~South Cotabato~Southern Leyte~Sultan Kudarat~Sulu~Surigao del Norte~Surigao del Sur~Tarlac~Tawi-Tawi~Zambales~Zamboanga del Norte~Zamboanga del Sur~Zamboanga Sibuguey",
+ sub_zipexs:
+ "2800,2826~8600,8611~8500,8513~5600,5616~4500,4517~5700,5717~3800,3806,3808~3200,3207~7300,7306~2100,2114~3900,3905~4200,4234~2600,2615~6543,6550~6300,6337~8700,8723~3000,3024~3500,3528~4600,4612~4400,4436~9100,9104~5800,5816~4800,4810~4100,4126~6000,6053~8800,8810~9400,9417~8100,8120~8000,8010~8015,8013~8200,8210~8426,8412~6800,6822~5044,5046~3600,3610~2900,2922~2700,2733~5000,5043~3300,3336~3807,3809,3814~2500,2520~4000,4033~9200,9223~9300,9321,9700,9716~6500,6542~9600,9619~4900,4905~5400,5421~~5100,5111~5200,5214~7200,7215~9000,9025~2616,2625~6100,6132~6200,6224~6400,6423~3100,3133~3700,3714~5300,5322~2000,2022~2400,2447~4300,4342~3400,3405~1850,1990~5500,5516~6700,6725~8015~6225,6230~4700,4715~9500,9513~6600,6613~9800,9811~7400,7416~8400,8425~8300,8319~2300,2318~7500,7509~2200,2213~7100,7124~7000,7043~7000,7043",
+ sub_zips:
+ "28[0-2]~86[01]~85[01]~56[01]~45[01]~57[01]~380[0-68]~320~730~21[01]~390~42[0-3]~26(0|1[0-5])~65(4[3-9]|5)~63[0-3]~87[0-2]~30[0-2]~35[0-2]~46[01]~44[0-3]~910~58[01]~48[01]~41[0-2]~60[0-5]~88[01]~94[01]~81[0-2]~80[01]~801[1-5]~82[01]~84[12]~68[0-2]~504[4-6]~36[01]~29[0-2]~27[0-3]~50([0-3]|4[0-3])~33[0-3]~38(0[79]|1[0-4])~25[0-2]~40[0-3]~92[0-2]~9(3[0-2]|7[01])~65([0-3]|4[0-2])~96[01]~490~54[0-2]~~51[01]~52[01]~72[01]~90[0-2]~26(1[6-9]|2[0-5])~61[0-3]~62[0-2]~64[0-2]~31[0-3]~37[01]~53[0-2]~20[0-2]~24[0-4]~43[0-4]~340~1[89]~55[01]~67[0-2]~8015~62(2[5-9]|30)~47[01]~95[01]~66[10]~98[01]~74[01]~84[0-2]~83[01]~23[01]~750~22[01]~71[0-2]~70[0-4]~70[0-4]",
+ zip: "\\d{4}",
+ zipex: "1008,1050,1135,1207,2000,1000",
+ },
+ "data/PK": {
+ fmt: "%N%n%O%n%A%n%C-%Z",
+ id: "data/PK",
+ key: "PK",
+ name: "PAKISTAN",
+ posturl: "http://www.pakpost.gov.pk/postcode.php",
+ zip: "\\d{5}",
+ zipex: "44000",
+ },
+ "data/PL": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/PL",
+ key: "PL",
+ name: "POLAND",
+ posturl: "http://kody.poczta-polska.pl/",
+ require: "ACZ",
+ zip: "\\d{2}-\\d{3}",
+ zipex: "00-950,05-470,48-300,32-015,00-940",
+ },
+ "data/PM": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/PM",
+ key: "PM",
+ name: "ST. PIERRE AND MIQUELON",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78]5\\d{2}",
+ zipex: "97500",
+ },
+ "data/PN": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/PN",
+ key: "PN",
+ name: "PITCAIRN",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "PCRN 1ZZ",
+ zipex: "PCRN 1ZZ",
+ },
+ "data/PR": {
+ fmt: "%N%n%O%n%A%n%C PR %Z",
+ id: "data/PR",
+ key: "PR",
+ name: "PUERTO RICO",
+ postprefix: "PR ",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACZ",
+ upper: "ACNO",
+ zip: "(00[679]\\d{2})(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "00930",
+ },
+ "data/PT": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/PT",
+ key: "PT",
+ name: "PORTUGAL",
+ posturl: "http://www.ctt.pt/feapl_2/app/open/tools.jspx?tool=1",
+ require: "ACZ",
+ zip: "\\d{4}-\\d{3}",
+ zipex: "2725-079,1250-096,1201-950,2860-571,1208-148",
+ },
+ "data/PW": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/PW",
+ key: "PW",
+ name: "PALAU",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACSZ",
+ state_name_type: "state",
+ upper: "ACNOS",
+ zip: "(969(?:39|40))(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "96940",
+ },
+ "data/PY": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/PY",
+ key: "PY",
+ name: "PARAGUAY",
+ zip: "\\d{4}",
+ zipex: "1536,1538,1209",
+ },
+ "data/QA": { id: "data/QA", key: "QA", name: "QATAR", upper: "AC" },
+ "data/RE": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/RE",
+ key: "RE",
+ name: "REUNION",
+ posturl:
+ "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "9[78]4\\d{2}",
+ zipex: "97400",
+ },
+ "data/RO": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/RO",
+ key: "RO",
+ name: "ROMANIA",
+ posturl: "http://www.posta-romana.ro/zip_codes",
+ upper: "AC",
+ zip: "\\d{6}",
+ zipex: "060274,061357,200716",
+ },
+ "data/RS": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/RS",
+ key: "RS",
+ name: "REPUBLIC OF SERBIA",
+ posturl:
+ "http://www.posta.rs/struktura/lat/aplikacije/pronadji/nadji-postu.asp",
+ zip: "\\d{5,6}",
+ zipex: "106314",
+ },
+ "data/RU": {
+ fmt: "%N%n%O%n%A%n%C%n%S%n%Z",
+ id: "data/RU",
+ key: "RU",
+ lang: "ru",
+ languages: "ru",
+ lfmt: "%N%n%O%n%A%n%C%n%S%n%Z",
+ name: "RUSSIAN FEDERATION",
+ posturl: "http://info.russianpost.ru/servlet/department",
+ require: "ACSZ",
+ state_name_type: "oblast",
+ sub_isoids:
+ "ALT~AMU~ARK~AST~BEL~BRY~VLA~VGG~VLG~VOR~YEV~ZAB~IVA~IRK~KB~KGD~KLU~KAM~KC~KEM~KIR~KOS~KDA~KYA~KGN~KRS~LEN~LIP~MAG~MOW~MOS~MUR~NEN~NIZ~NGR~NVS~OMS~ORE~ORL~PNZ~PER~PRI~PSK~AD~AL~BA~BU~DA~IN~KL~KR~KO~~ME~MO~SA~SE~TA~TY~UD~KK~ROS~RYA~SAM~SPE~SAR~SAK~SVE~~SMO~STA~TAM~TVE~TOM~TUL~TYU~ULY~KHA~KHM~CHE~CE~CU~CHU~YAN~YAR",
+ sub_keys:
+ "Алтайский край~Амурская область~Архангельская область~Астраханская область~Белгородская область~Брянская область~Владимирская область~Волгоградская область~Вологодская область~Воронежская область~Еврейская автономная область~Забайкальский край~Ивановская область~Иркутская область~Кабардино-Балкарская Республика~Калининградская область~Калужская область~Камчатский край~Карачаево-Черкесская Республика~Кемеровская область~Кировская область~Костромская область~Краснодарский край~Красноярский край~Курганская область~Курская область~Ленинградская область~Липецкая область~Магаданская область~Москва~Московская область~Мурманская область~Ненецкий автономный округ~Нижегородская область~Новгородская область~Новосибирская область~Омская область~Оренбургская область~Орловская область~Пензенская область~Пермский край~Приморский край~Псковская область~Республика Адыгея~Республика Алтай~Республика Башкортостан~Республика Бурятия~Республика Дагестан~Республика Ингушетия~Республика Калмыкия~Республика Карелия~Республика Коми~Автономна Республіка Крим~Республика Марий Эл~Республика Мордовия~Республика Саха (Якутия)~Республика Северная Осетия-Алания~Республика Татарстан~Республика Тыва~Республика Удмуртия~Республика Хакасия~Ростовская область~Рязанская область~Самарская область~Санкт-Петербург~Саратовская область~Сахалинская область~Свердловская область~Севастополь~Смоленская область~Ставропольский край~Тамбовская область~Тверская область~Томская область~Тульская область~Тюменская область~Ульяновская область~Хабаровский край~Ханты-Мансийский автономный округ~Челябинская область~Чеченская Республика~Чувашская Республика~Чукотский автономный округ~Ямало-Ненецкий автономный округ~Ярославская область",
+ sub_lnames:
+ "Altayskiy kray~Amurskaya oblast'~Arkhangelskaya oblast'~Astrakhanskaya oblast'~Belgorodskaya oblast'~Bryanskaya oblast'~Vladimirskaya oblast'~Volgogradskaya oblast'~Vologodskaya oblast'~Voronezhskaya oblast'~Evreyskaya avtonomnaya oblast'~Zabaykalskiy kray~Ivanovskaya oblast'~Irkutskaya oblast'~Kabardino-Balkarskaya Republits~Kaliningradskaya oblast'~Kaluzhskaya oblast'~Kamchatskiy kray~Karachaevo-Cherkesskaya Republits~Kemerovskaya oblast'~Kirovskaya oblast'~Kostromskaya oblast'~Krasnodarskiy kray~Krasnoyarskiy kray~Kurganskaya oblast'~Kurskaya oblast'~Leningradskaya oblast'~Lipetskaya oblast'~Magadanskaya oblast'~Moskva~Moskovskaya oblast'~Murmanskaya oblast'~Nenetskiy~Nizhegorodskaya oblast'~Novgorodskaya oblast'~Novosibirskaya oblast'~Omskaya oblast'~Orenburgskaya oblast'~Orlovskaya oblast'~Penzenskaya oblast'~Permskiy kray~Primorskiy kray~Pskovskaya oblast'~Respublika Adygeya~Altay Republits~Bashkortostan Republits~Buryatiya Republits~Dagestan Republits~Ingushetiya Republits~Respublika Kalmykiya~Kareliya Republits~Komi Republits~Respublika Krym~Respublika Mariy El~Respublika Mordoviya~Sakha (Yakutiya) Republits~Respublika Severnaya Osetiya-Alaniya~Respublika Tatarstan~Tyva Republits~Respublika Udmurtiya~Khakasiya Republits~Rostovskaya oblast'~Ryazanskaya oblast'~Samarskaya oblast'~Sankt-Peterburg~Saratovskaya oblast'~Sakhalinskaya oblast'~Sverdlovskaya oblast'~Sevastopol'~Smolenskaya oblast'~Stavropolskiy kray~Tambovskaya oblast'~Tverskaya oblast'~Tomskaya oblast'~Tulskaya oblast'~Tyumenskaya oblast'~Ulyanovskaya oblast'~Khabarovskiy kray~Khanty-Mansiyskiy avtonomnyy okrug~Chelyabinskaya oblast'~Chechenskaya Republits~Chuvashia~Chukotskiy~Yamalo-Nenetskiy~Yaroslavskaya oblast'",
+ sub_names:
+ "Алтайский край~Амурская область~Архангельская область~Астраханская область~Белгородская область~Брянская область~Владимирская область~Волгоградская область~Вологодская область~Воронежская область~Еврейская автономная область~Забайкальский край~Ивановская область~Иркутская область~Кабардино-Балкарская Республика~Калининградская область~Калужская область~Камчатский край~Карачаево-Черкесская Республика~Кемеровская область~Кировская область~Костромская область~Краснодарский край~Красноярский край~Курганская область~Курская область~Ленинградская область~Липецкая область~Магаданская область~Москва~Московская область~Мурманская область~Ненецкий автономный округ~Нижегородская область~Новгородская область~Новосибирская область~Омская область~Оренбургская область~Орловская область~Пензенская область~Пермский край~Приморский край~Псковская область~Республика Адыгея~Республика Алтай~Республика Башкортостан~Республика Бурятия~Республика Дагестан~Республика Ингушетия~Республика Калмыкия~Республика Карелия~Республика Коми~Республика Крым~Республика Марий Эл~Республика Мордовия~Республика Саха (Якутия)~Республика Северная Осетия-Алания~Республика Татарстан~Республика Тыва~Республика Удмуртия~Республика Хакасия~Ростовская область~Рязанская область~Самарская область~Санкт-Петербург~Саратовская область~Сахалинская область~Свердловская область~Севастополь~Смоленская область~Ставропольский край~Тамбовская область~Тверская область~Томская область~Тульская область~Тюменская область~Ульяновская область~Хабаровский край~Ханты-Мансийский автономный округ~Челябинская область~Чеченская Республика~Чувашская Республика~Чукотский автономный округ~Ямало-Ненецкий автономный округ~Ярославская область",
+ sub_zips:
+ "65[6-9]~67[56]~16[3-5]~41[4-6]~30[89]~24[1-3]~60[0-2]~40[0-4]~16[0-2]~39[4-7]~679~6(?:7[2-4]|87)~15[3-5]~66[4-9]~36[01]~23[6-8]~24[89]~68[348]~369~65[0-4]~61[0-3]~15[67]~35[0-4]~6(?:6[0-3]|4[78])~64[01]~30[5-7]~18[78]~39[89]~68[56]~1(?:0[1-9]|1|2|3[0-5]|4[0-4])~14[0-4]~18[34]~166~60[3-7]~17[3-5]~63[0-3]~64[4-6]~46[0-2]~30[23]~44[0-2]~61[4-9]~69[0-2]~18[0-2]~385~649~45[0-3]~67[01]~36[78]~386~35[89]~18[56]~16[7-9]~29[5-8]~42[45]~43[01]~67[78]~36[23]~42[0-3]~66[78]~42[67]~655~34[4-7]~39[01]~44[3-6]~19~41[0-3]~69[34]~62[0-4]~299~21[4-6]~35[5-7]~39[23]~17[0-2]~63[4-6]~30[01]~62[5-7]~43[23]~68[0-2]~628~45[4-7]~36[4-6]~42[89]~689~629~15[0-2]",
+ upper: "AC",
+ zip: "\\d{6}",
+ zipex: "247112,103375,188300",
+ },
+ "data/RW": { id: "data/RW", key: "RW", name: "RWANDA", upper: "AC" },
+ "data/SA": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/SA",
+ key: "SA",
+ name: "SAUDI ARABIA",
+ zip: "\\d{5}",
+ zipex: "11564,11187,11142",
+ },
+ "data/SB": { id: "data/SB", key: "SB", name: "SOLOMON ISLANDS" },
+ "data/SC": {
+ fmt: "%N%n%O%n%A%n%C%n%S",
+ id: "data/SC",
+ key: "SC",
+ name: "SEYCHELLES",
+ state_name_type: "island",
+ upper: "S",
+ },
+ "data/SD": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/SD",
+ key: "SD",
+ locality_name_type: "district",
+ name: "SUDAN",
+ zip: "\\d{5}",
+ zipex: "11042,11113",
+ },
+ "data/SE": {
+ fmt: "%O%n%N%n%A%nSE-%Z %C",
+ id: "data/SE",
+ key: "SE",
+ locality_name_type: "post_town",
+ name: "SWEDEN",
+ postprefix: "SE-",
+ posturl:
+ "http://www.posten.se/sv/Kundservice/Sidor/Sok-postnummer-resultat.aspx",
+ require: "ACZ",
+ zip: "\\d{3} ?\\d{2}",
+ zipex: "11455,12345,10500",
+ },
+ "data/SG": {
+ fmt: "%N%n%O%n%A%nSINGAPORE %Z",
+ id: "data/SG",
+ key: "SG",
+ name: "REP. OF SINGAPORE",
+ posturl: "https://www.singpost.com/find-postal-code",
+ require: "AZ",
+ zip: "\\d{6}",
+ zipex: "546080,308125,408600",
+ },
+ "data/SH": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/SH",
+ key: "SH",
+ name: "SAINT HELENA",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "(?:ASCN|STHL) 1ZZ",
+ zipex: "STHL 1ZZ",
+ },
+ "data/SI": {
+ fmt: "%N%n%O%n%A%nSI-%Z %C",
+ id: "data/SI",
+ key: "SI",
+ name: "SLOVENIA",
+ postprefix: "SI-",
+ zip: "\\d{4}",
+ zipex: "4000,1001,2500",
+ },
+ "data/SK": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/SK",
+ key: "SK",
+ name: "SLOVAKIA",
+ posturl: "http://psc.posta.sk",
+ require: "ACZ",
+ zip: "\\d{3} ?\\d{2}",
+ zipex: "010 01,023 14,972 48,921 01,975 99",
+ },
+ "data/SL": { id: "data/SL", key: "SL", name: "SIERRA LEONE" },
+ "data/SM": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/SM",
+ key: "SM",
+ name: "SAN MARINO",
+ posturl: "http://www.poste.it/online/cercacap/",
+ require: "AZ",
+ zip: "4789\\d",
+ zipex: "47890,47891,47895,47899",
+ },
+ "data/SN": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/SN",
+ key: "SN",
+ name: "SENEGAL",
+ zip: "\\d{5}",
+ zipex: "12500,46024,16556,10000",
+ },
+ "data/SO": {
+ fmt: "%N%n%O%n%A%n%C, %S %Z",
+ id: "data/SO",
+ key: "SO",
+ lang: "so",
+ languages: "so",
+ name: "SOMALIA",
+ require: "ACS",
+ sub_isoids: "AW~BK~BN~BR~BY~GA~GE~HI~JD~JH~MU~NU~SA~SD~SH~SO~TO~WO",
+ sub_keys: "AD~BK~BN~BR~BY~GG~GD~HR~JD~JH~MD~NG~SG~SD~SH~SL~TG~WG",
+ sub_names:
+ "Awdal~Bakool~Banaadir~Bari~Bay~Galguduud~Gedo~Hiiraan~Jubbada Dhexe~Jubbada Hoose~Mudug~Nugaal~Sanaag~Shabeellaha Dhexe~Shabeellaha Hoose~Sool~Togdheer~Woqooyi Galbeed",
+ upper: "ACS",
+ zip: "[A-Z]{2} ?\\d{5}",
+ zipex: "JH 09010,AD 11010",
+ },
+ "data/SR": {
+ fmt: "%N%n%O%n%A%n%C%n%S",
+ id: "data/SR",
+ key: "SR",
+ lang: "nl",
+ languages: "nl",
+ name: "SURINAME",
+ sub_isoids: "BR~CM~CR~MA~NI~PR~PM~SA~SI~WA",
+ sub_keys:
+ "Brokopondo~Commewijne~Coronie~Marowijne~Nickerie~Para~Paramaribo~Saramacca~Sipaliwini~Wanica",
+ upper: "AS",
+ },
+ "data/SS": { id: "data/SS", key: "SS", name: "SOUTH SUDAN" },
+ "data/ST": { id: "data/ST", key: "ST", name: "SAO TOME AND PRINCIPE" },
+ "data/SV": {
+ fmt: "%N%n%O%n%A%n%Z-%C%n%S",
+ id: "data/SV",
+ key: "SV",
+ lang: "es",
+ languages: "es",
+ name: "EL SALVADOR",
+ require: "ACS",
+ sub_isoids: "AH~CA~CH~CU~LI~PA~UN~MO~SM~SS~SV~SA~SO~US",
+ sub_keys:
+ "Ahuachapan~Cabanas~Calatenango~Cuscatlan~La Libertad~La Paz~La Union~Morazan~San Miguel~San Salvador~San Vicente~Santa Ana~Sonsonate~Usulutan",
+ sub_names:
+ "Ahuachapán~Cabañas~Chalatenango~Cuscatlán~La Libertad~La Paz~La Unión~Morazán~San Miguel~San Salvador~San Vicente~Santa Ana~Sonsonate~Usulután",
+ sub_zipexs:
+ "CP 2101~CP 1201~CP 1301~CP 1401~CP 1501~CP 1601~CP 3101~CP 3201~CP 3301~CP 1101~CP 1701~CP 2201~CP 2301~CP 3401",
+ sub_zips:
+ "CP 21~CP 12~CP 13~CP 14~CP 15~CP 16~CP 31~CP 32~CP 33~CP 11~CP 17~CP 22~CP 23~CP 34",
+ upper: "CSZ",
+ zip: "CP [1-3][1-7][0-2]\\d",
+ zipex: "CP 1101",
+ },
+ "data/SX": { id: "data/SX", key: "SX", name: "SINT MAARTEN" },
+ "data/SY": {
+ id: "data/SY",
+ key: "SY",
+ locality_name_type: "district",
+ name: "SYRIA",
+ },
+ "data/SZ": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/SZ",
+ key: "SZ",
+ name: "SWAZILAND",
+ posturl: "http://www.sptc.co.sz/swazipost/codes/index.php",
+ upper: "ACZ",
+ zip: "[HLMS]\\d{3}",
+ zipex: "H100",
+ },
+ "data/TC": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/TC",
+ key: "TC",
+ name: "TURKS AND CAICOS ISLANDS",
+ require: "ACZ",
+ upper: "CZ",
+ zip: "TKCA 1ZZ",
+ zipex: "TKCA 1ZZ",
+ },
+ "data/TD": { id: "data/TD", key: "TD", name: "CHAD" },
+ "data/TF": { id: "data/TF", key: "TF", name: "FRENCH SOUTHERN TERRITORIES" },
+ "data/TG": { id: "data/TG", key: "TG", name: "TOGO" },
+ "data/TH": {
+ fmt: "%N%n%O%n%A%n%D %C%n%S %Z",
+ id: "data/TH",
+ key: "TH",
+ lang: "th",
+ languages: "th",
+ lfmt: "%N%n%O%n%A%n%D, %C%n%S %Z",
+ name: "THAILAND",
+ sub_isoids:
+ "81~10~71~46~62~40~38~22~24~20~18~36~86~57~50~92~23~63~26~73~48~30~80~60~12~96~55~31~13~77~25~94~14~56~82~93~66~65~76~67~54~83~44~49~58~35~95~45~85~21~70~16~52~51~42~33~47~90~91~11~75~74~27~19~17~64~72~84~32~43~39~15~37~41~53~61~34",
+ sub_keys:
+ "กระบี่~กรุงเทพมหานคร~กาญจนบุรี~กาฬสินธุ์~กำแพงเพชร~ขอนแก่น~จังหวัด บึงกาฬ~จันทบุรี~ฉะเชิงเทรา~ชลบุรี~ชัยนาท~ชัยภูมิ~ชุมพร~เชียงราย~เชียงใหม่~ตรัง~ตราด~ตาก~นครนายก~นครปฐม~นครพนม~นครราชสีมา~นครศรีธรรมราช~นครสวรรค์~นนทบุรี~นราธิวาส~น่าน~บุรีรัมย์~ปทุมธานี~ประจวบคีรีขันธ์~ปราจีนบุรี~ปัตตานี~พระนครศรีอยุธยา~พะเยา~พังงา~พัทลุง~พิจิตร~พิษณุโลก~เพชรบุรี~เพชรบูรณ์~แพร่~ภูเก็ต~มหาสารคาม~มุกดาหาร~แม่ฮ่องสอน~ยโสธร~ยะลา~ร้อยเอ็ด~ระนอง~ระยอง~ราชบุรี~ลพบุรี~ลำปาง~ลำพูน~เลย~ศรีสะเกษ~สกลนคร~สงขลา~สตูล~สมุทรปราการ~สมุทรสงคราม~สมุทรสาคร~สระแก้ว~สระบุรี~สิงห์บุรี~สุโขทัย~สุพรรณบุรี~สุราษฎร์ธานี~สุรินทร์~หนองคาย~หนองบัวลำภู~อ่างทอง~อำนาจเจริญ~อุดรธานี~อุตรดิตถ์~อุทัยธานี~อุบลราชธานี",
+ sub_lnames:
+ "Krabi~Bangkok~Kanchanaburi~Kalasin~Kamphaeng Phet~Khon Kaen~Bueng Kan~Chanthaburi~Chachoengsao~Chon Buri~Chai Nat~Chaiyaphum~Chumpon~Chiang Rai~Chiang Mai~Trang~Trat~Tak~Nakhon Nayok~Nakhon Pathom~Nakhon Phanom~Nakhon Ratchasima~Nakhon Si Thammarat~Nakhon Sawan~Nonthaburi~Narathiwat~Nan~Buri Ram~Pathum Thani~Prachuap Khiri Khan~Prachin Buri~Pattani~Phra Nakhon Si Ayutthaya~Phayao~Phang Nga~Phattalung~Phichit~Phitsanulok~Phetchaburi~Phetchabun~Phrae~Phuket~Maha Sarakham~Mukdahan~Mae Hong Son~Yasothon~Yala~Roi Et~Ranong~Rayong~Ratchaburi~Lop Buri~Lampang~Lamphun~Loei~Si Sa Ket~Sakon Nakhon~Songkhla~Satun~Samut Prakan~Samut Songkhram~Samut Sakhon~Sa Kaeo~Saraburi~Sing Buri~Sukhothai~Suphanburi~Surat Thani~Surin~Nong Khai~Nong Bua Lam Phu~Ang Thong~Amnat Charoen~Udon Thani~Uttaradit~Uthai Thani~Ubon Ratchathani",
+ sub_zips:
+ "81~10~71~46~62~40~~22~24~20~17~36~86~57~50~92~23~63~26~73~48~30~80~60~11~96~55~31~12~77~25~94~13~56~82~93~66~65~76~67~54~83~44~49~58~35~95~45~85~21~70~15~52~51~42~33~47~90~91~10~75~74~27~18~16~64~72~84~32~43~39~14~37~41~53~61~34",
+ upper: "S",
+ zip: "\\d{5}",
+ zipex: "10150,10210",
+ },
+ "data/TJ": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/TJ",
+ key: "TJ",
+ name: "TAJIKISTAN",
+ zip: "\\d{6}",
+ zipex: "735450,734025",
+ },
+ "data/TK": { id: "data/TK", key: "TK", name: "TOKELAU" },
+ "data/TL": { id: "data/TL", key: "TL", name: "TIMOR-LESTE" },
+ "data/TM": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/TM",
+ key: "TM",
+ name: "TURKMENISTAN",
+ zip: "\\d{6}",
+ zipex: "744000",
+ },
+ "data/TN": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/TN",
+ key: "TN",
+ name: "TUNISIA",
+ posturl: "http://www.poste.tn/codes.php",
+ zip: "\\d{4}",
+ zipex: "1002,8129,3100,1030",
+ },
+ "data/TO": { id: "data/TO", key: "TO", name: "TONGA" },
+ "data/TR": {
+ fmt: "%N%n%O%n%A%n%Z %C/%S",
+ id: "data/TR",
+ key: "TR",
+ lang: "tr",
+ languages: "tr",
+ locality_name_type: "district",
+ name: "TURKEY",
+ posturl: "http://postakodu.ptt.gov.tr/",
+ require: "ACZ",
+ sub_isoids:
+ "01~02~03~04~68~05~06~07~75~08~09~10~74~72~69~11~12~13~14~15~16~17~18~19~20~21~81~22~23~24~25~26~27~28~29~30~31~76~32~34~35~46~78~70~36~37~38~71~39~40~79~41~42~43~44~45~47~33~48~49~50~51~52~80~53~54~55~56~57~58~63~73~59~60~61~62~64~65~77~66~67",
+ sub_keys:
+ "Adana~Adıyaman~Afyon~Ağrı~Aksaray~Amasya~Ankara~Antalya~Ardahan~Artvin~Aydın~Balıkesir~Bartın~Batman~Bayburt~Bilecik~Bingöl~Bitlis~Bolu~Burdur~Bursa~Çanakkale~Çankırı~Çorum~Denizli~Diyarbakır~Düzce~Edirne~Elazığ~Erzincan~Erzurum~Eskişehir~Gaziantep~Giresun~Gümüşhane~Hakkari~Hatay~Iğdır~Isparta~İstanbul~İzmir~Kahramanmaraş~Karabük~Karaman~Kars~Kastamonu~Kayseri~Kırıkkale~Kırklareli~Kırşehir~Kilis~Kocaeli~Konya~Kütahya~Malatya~Manisa~Mardin~Mersin~Muğla~Muş~Nevşehir~Niğde~Ordu~Osmaniye~Rize~Sakarya~Samsun~Siirt~Sinop~Sivas~Şanlıurfa~Şırnak~Tekirdağ~Tokat~Trabzon~Tunceli~Uşak~Van~Yalova~Yozgat~Zonguldak",
+ sub_zips:
+ "01~02~03~04~68~05~06~07~75~08~09~10~74~72~69~11~12~13~14~15~16~17~18~19~20~21~81~22~23~24~25~26~27~28~29~30~31~76~32~34~35~46~78~70~36~37~38~71~39~40~79~41~42~43~44~45~47~33~48~49~50~51~52~80~53~54~55~56~57~58~63~73~59~60~61~62~64~65~77~66~67",
+ zip: "\\d{5}",
+ zipex: "01960,06101",
+ },
+ "data/TT": { id: "data/TT", key: "TT", name: "TRINIDAD AND TOBAGO" },
+ "data/TV": {
+ fmt: "%N%n%O%n%A%n%C%n%S",
+ id: "data/TV",
+ key: "TV",
+ lang: "tyv",
+ languages: "tyv",
+ name: "TUVALU",
+ state_name_type: "island",
+ sub_isoids: "FUN~NMG~NMA~~NIT~NUI~NKF~NKL~VAI",
+ sub_keys:
+ "Funafuti~Nanumanga~Nanumea~Niulakita~Niutao~Nui~Nukufetau~Nukulaelae~Vaitupu",
+ upper: "ACS",
+ },
+ "data/TW": {
+ fmt: "%Z%n%S%C%n%A%n%O%n%N",
+ id: "data/TW",
+ key: "TW",
+ lang: "zh-Hant",
+ languages: "zh-Hant",
+ lfmt: "%N%n%O%n%A%n%C, %S %Z",
+ name: "TAIWAN",
+ posturl:
+ "http://www.post.gov.tw/post/internet/f_searchzone/index.jsp?ID=190102",
+ require: "ACSZ",
+ state_name_type: "county",
+ sub_isoids:
+ "TXG~TPE~TTT~TNN~ILA~HUA~~NAN~PIF~MIA~TAO~KHH~KEE~~YUN~NWT~HSZ~HSQ~CYI~CYQ~CHA~PEN",
+ sub_keys:
+ "台中市~台北市~台東縣~台南市~宜蘭縣~花蓮縣~金門縣~南投縣~屏東縣~苗栗縣~桃園市~高雄市~基隆市~連江縣~雲林縣~新北市~新竹市~新竹縣~嘉義市~嘉義縣~彰化縣~澎湖縣",
+ sub_lnames:
+ "Taichung City~Taipei City~Taitung County~Tainan City~Yilan County~Hualien County~Kinmen County~Nantou County~Pingtung County~Miaoli County~Taoyuan City~Kaohsiung City~Keelung City~Lienchiang County~Yunlin County~New Taipei City~Hsinchu City~Hsinchu County~Chiayi City~Chiayi County~Changhua County~Penghu County",
+ sub_mores:
+ "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true",
+ sub_zipexs:
+ "400,408,411,439~100,119~950,966~700,745~260,272~970,983~890,896~540,558~900,947~350,369~320,338~800,815,817,852~200,206~209,212~630,655~207,208,220,253~~302,315~~602,625~500,530~880,885",
+ sub_zips:
+ "4[0-3]~1[01]~9[56]~7[0-4]~2[67]~9[78]~89~5[45]~9[0-4]~3[56]~3[23]~8[02-5]|81[1-579]~20[0-6]~209|21[012]~6[3-5]~20[78]|2[2345]~300~30[2-8]|31~600~60[1-9]|6[12]~5[0123]~88",
+ zip: "\\d{3}(?:\\d{2})?",
+ zipex: "104,106,10603,40867",
+ },
+ "data/TZ": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/TZ",
+ key: "TZ",
+ name: "TANZANIA (UNITED REP.)",
+ zip: "\\d{4,5}",
+ zipex: "6090,34413",
+ },
+ "data/UA": {
+ fmt: "%N%n%O%n%A%n%C%n%S%n%Z",
+ id: "data/UA",
+ key: "UA",
+ lang: "uk",
+ languages: "uk",
+ lfmt: "%N%n%O%n%A%n%C%n%S%n%Z",
+ name: "UKRAINE",
+ posturl: "http://services.ukrposhta.com/postindex_new/",
+ require: "ACSZ",
+ state_name_type: "oblast",
+ sub_isoids:
+ "43~05~07~12~14~18~21~23~26~30~32~35~09~46~48~51~53~56~40~59~61~63~65~68~71~77~74",
+ sub_keys:
+ "Автономна Республіка Крим~Вінницька область~Волинська область~Дніпропетровська область~Донецька область~Житомирська область~Закарпатська область~Запорізька область~Івано-Франківська область~місто Київ~Київська область~Кіровоградська область~Луганська область~Львівська область~Миколаївська область~Одеська область~Полтавська область~Рівненська область~місто Севастополь~Сумська область~Тернопільська область~Харківська область~Херсонська область~Хмельницька область~Черкаська область~Чернівецька область~Чернігівська область",
+ sub_lnames:
+ "Crimea~Vinnyts'ka oblast~Volyns'ka oblast~Dnipropetrovsk oblast~Donetsk oblast~Zhytomyrs'ka oblast~Zakarpats'ka oblast~Zaporiz'ka oblast~Ivano-Frankivs'ka oblast~Kyiv city~Kiev oblast~Kirovohrads'ka oblast~Luhans'ka oblast~Lviv oblast~Mykolaivs'ka oblast~Odessa oblast~Poltavs'ka oblast~Rivnens'ka oblast~Sevastopol' city~Sums'ka oblast~Ternopil's'ka oblast~Kharkiv oblast~Khersons'ka oblast~Khmel'nyts'ka oblast~Cherkas'ka oblast~Chernivets'ka oblast~Chernihivs'ka oblast",
+ sub_names:
+ "Автономна Республіка Крим~Вінницька область~Волинська область~Дніпропетровська область~Донецька область~Житомирська область~Закарпатська область~Запорізька область~Івано-Франківська область~Київ~Київська область~Кіровоградська область~Луганська область~Львівська область~Миколаївська область~Одеська область~Полтавська область~Рівненська область~Севастополь~Сумська область~Тернопільська область~Харківська область~Херсонська область~Хмельницька область~Черкаська область~Чернівецька область~Чернігівська область",
+ sub_zips:
+ "9[5-8]~2[1-4]~4[3-5]~49|5[0-3]~8[3-7]~1[0-3]~8[89]|90~69|7[0-2]~7[6-8]~0[1-6]~0[7-9]~2[5-8]~9[1-4]~79|8[0-2]~5[4-7]~6[5-8]~3[6-9]~3[3-5]~99~4[0-2]~4[6-8]~6[1-4]~7[3-5]~29|3[0-2]~1[89]|20~5[89]|60~1[4-7]",
+ zip: "\\d{5}",
+ zipex: "15432,01055,01001",
+ },
+ "data/UG": { id: "data/UG", key: "UG", name: "UGANDA" },
+ "data/US": {
+ fmt: "%N%n%O%n%A%n%C, %S %Z",
+ id: "data/US",
+ key: "US",
+ lang: "en",
+ languages: "en",
+ name: "UNITED STATES",
+ posturl: "https://tools.usps.com/go/ZipLookupAction!input.action",
+ require: "ACSZ",
+ state_name_type: "state",
+ sub_isoids:
+ "AL~AK~~AZ~AR~~~~CA~CO~CT~DE~DC~FL~GA~~HI~ID~IL~IN~IA~KS~KY~LA~ME~~MD~MA~MI~~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~~OH~OK~OR~~PA~~RI~SC~SD~TN~TX~UT~VT~~VA~WA~WV~WI~WY",
+ sub_keys:
+ "AL~AK~AS~AZ~AR~AA~AE~AP~CA~CO~CT~DE~DC~FL~GA~GU~HI~ID~IL~IN~IA~KS~KY~LA~ME~MH~MD~MA~MI~FM~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~MP~OH~OK~OR~PW~PA~PR~RI~SC~SD~TN~TX~UT~VT~VI~VA~WA~WV~WI~WY",
+ sub_names:
+ "Alabama~Alaska~American Samoa~Arizona~Arkansas~Armed Forces (AA)~Armed Forces (AE)~Armed Forces (AP)~California~Colorado~Connecticut~Delaware~District of Columbia~Florida~Georgia~Guam~Hawaii~Idaho~Illinois~Indiana~Iowa~Kansas~Kentucky~Louisiana~Maine~Marshall Islands~Maryland~Massachusetts~Michigan~Micronesia~Minnesota~Mississippi~Missouri~Montana~Nebraska~Nevada~New Hampshire~New Jersey~New Mexico~New York~North Carolina~North Dakota~Northern Mariana Islands~Ohio~Oklahoma~Oregon~Palau~Pennsylvania~Puerto Rico~Rhode Island~South Carolina~South Dakota~Tennessee~Texas~Utah~Vermont~Virgin Islands~Virginia~Washington~West Virginia~Wisconsin~Wyoming",
+ sub_zipexs:
+ "35000,36999~99500,99999~96799~85000,86999~71600,72999~34000,34099~09000,09999~96200,96699~90000,96199~80000,81999~06000,06999~19700,19999~20000,56999~32000,34999~30000,39901~96910,96932~96700,96899~83200,83999~60000,62999~46000,47999~50000,52999~66000,67999~40000,42799~70000,71599~03900,04999~96960,96979~20600,21999~01000,05544~48000,49999~96941,96944~55000,56799~38600,39799~63000,65999~59000,59999~68000,69999~88900,89999~03000,03899~07000,08999~87000,88499~10000,00544~27000,28999~58000,58999~96950,96952~43000,45999~73000,74999~97000,97999~96940~15000,19699~00600,00999~02800,02999~29000,29999~57000,57999~37000,38599~75000,73344~84000,84999~05000,05999~00800,00899~20100,24699~98000,99499~24700,26999~53000,54999~82000,83414",
+ sub_zips:
+ "3[56]~99[5-9]~96799~8[56]~71[6-9]|72~340~09~96[2-6]~9[0-5]|96[01]~8[01]~06~19[7-9]~20[02-5]|569~3[23]|34[1-9]~3[01]|398|39901~969([1-2]\\d|3[12])~967[0-8]|9679[0-8]|968~83[2-9]~6[0-2]~4[67]~5[0-2]~6[67]~4[01]|42[0-7]~70|71[0-5]~039|04~969[67]~20[6-9]|21~01|02[0-7]|05501|05544~4[89]~9694[1-4]~55|56[0-7]~38[6-9]|39[0-7]~6[3-5]~59~6[89]~889|89~03[0-8]~0[78]~87|88[0-4]~1[0-4]|06390|00501|00544~2[78]~58~9695[0-2]~4[3-5]~7[34]~97~969(39|40)~1[5-8]|19[0-6]~00[679]~02[89]~29~57~37|38[0-5]~7[5-9]|885|73301|73344~84~05~008~201|2[23]|24[0-6]~98|99[0-4]~24[7-9]|2[56]~5[34]~82|83[01]|83414",
+ upper: "CS",
+ zip: "(\\d{5})(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "95014,22162-1010",
+ },
+ "data/UY": {
+ fmt: "%N%n%O%n%A%n%Z %C %S",
+ id: "data/UY",
+ key: "UY",
+ lang: "es",
+ languages: "es",
+ name: "URUGUAY",
+ posturl:
+ "http://www.correo.com.uy/index.asp?codPag=codPost&switchMapa=codPost",
+ sub_isoids: "AR~CA~CL~CO~DU~FS~FD~LA~MA~MO~PA~RN~RV~RO~SA~SJ~SO~TA~TT",
+ sub_keys:
+ "Artigas~Canelones~Cerro Largo~Colonia~Durazno~Flores~Florida~Lavalleja~Maldonado~Montevideo~Paysandú~Río Negro~Rivera~Rocha~Salto~San José~Soriano~Tacuarembó~Treinta y Tres",
+ sub_zips:
+ "55~9[01]|1[456]~37~70|75204~97~85~94|9060|97005~30~20~1|91600~60~65|60002~40~27~50~80~75|70003~45~33|30203|30204|30302|37007",
+ upper: "CS",
+ zip: "\\d{5}",
+ zipex: "11600",
+ },
+ "data/UZ": {
+ fmt: "%N%n%O%n%A%n%Z %C%n%S",
+ id: "data/UZ",
+ key: "UZ",
+ name: "UZBEKISTAN",
+ posturl: "http://www.pochta.uz/ru/uslugi/indexsearch.html",
+ upper: "CS",
+ zip: "\\d{6}",
+ zipex: "702100,700000",
+ },
+ "data/VA": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/VA",
+ key: "VA",
+ name: "VATICAN",
+ zip: "00120",
+ zipex: "00120",
+ },
+ "data/VC": {
+ fmt: "%N%n%O%n%A%n%C %Z",
+ id: "data/VC",
+ key: "VC",
+ name: "SAINT VINCENT AND THE GRENADINES (ANTILLES)",
+ posturl:
+ "http://www.svgpost.gov.vc/?option=com_content&view=article&id=3&Itemid=16",
+ zip: "VC\\d{4}",
+ zipex: "VC0100,VC0110,VC0400",
+ },
+ "data/VE": {
+ fmt: "%N%n%O%n%A%n%C %Z, %S",
+ id: "data/VE",
+ key: "VE",
+ lang: "es",
+ languages: "es",
+ name: "VENEZUELA",
+ posturl: "http://www.ipostel.gob.ve/index.php/oficinas-postales",
+ require: "ACS",
+ state_name_type: "state",
+ sub_isoids: "Z~B~C~D~E~F~G~H~Y~W~A~I~J~K~L~M~N~O~P~R~S~T~X~U~V",
+ sub_keys:
+ "Amazonas~Anzoátegui~Apure~Aragua~Barinas~Bolívar~Carabobo~Cojedes~Delta Amacuro~Dependencias Federales~Distrito Federal~Falcón~Guárico~Lara~Mérida~Miranda~Monagas~Nueva Esparta~Portuguesa~Sucre~Táchira~Trujillo~Vargas~Yaracuy~Zulia",
+ upper: "CS",
+ zip: "\\d{4}",
+ zipex: "1010,3001,8011,1020",
+ },
+ "data/VG": {
+ fmt: "%N%n%O%n%A%n%C%n%Z",
+ id: "data/VG",
+ key: "VG",
+ name: "VIRGIN ISLANDS (BRITISH)",
+ require: "A",
+ zip: "VG\\d{4}",
+ zipex: "VG1110,VG1150,VG1160",
+ },
+ "data/VI": {
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ id: "data/VI",
+ key: "VI",
+ name: "VIRGIN ISLANDS (U.S.)",
+ posturl: "http://zip4.usps.com/zip4/welcome.jsp",
+ require: "ACSZ",
+ state_name_type: "state",
+ upper: "ACNOS",
+ zip: "(008(?:(?:[0-4]\\d)|(?:5[01])))(?:[ \\-](\\d{4}))?",
+ zip_name_type: "zip",
+ zipex: "00802-1222,00850-9802",
+ },
+ "data/VN": {
+ fmt: "%N%n%O%n%A%n%C%n%S %Z",
+ id: "data/VN",
+ key: "VN",
+ lang: "vi",
+ languages: "vi",
+ lfmt: "%N%n%O%n%A%n%C%n%S %Z",
+ name: "VIET NAM",
+ posturl: "http://postcode.vnpost.vn/services/search.aspx",
+ sub_isoids:
+ "44~43~55~54~53~56~50~57~31~58~40~59~04~CT~DN~33~72~71~39~45~30~03~63~HN~23~61~HP~73~14~66~34~47~28~01~09~02~35~41~67~22~18~36~68~32~24~27~29~13~25~52~05~37~20~69~21~SG~26~46~51~07~49~70~06",
+ sub_keys:
+ "An Giang~Bà Rịa–Vũng Tàu~Bạc Liêu~Bắc Giang~Bắc Kạn~Bắc Ninh~Bến Tre~Bình Dương~Bình Định~Bình Phước~Bình Thuận~Cà Mau~Cao Bằng~Cần Thơ~Đà Nẵng~Đắk Lắk~Đăk Nông~Điện Biên~Đồng Nai~Đồng Tháp~Gia Lai~Hà Giang~Hà Nam~Hà Nội~Hà Tĩnh~Hải Dương~Hải Phòng~Hậu Giang~Hòa Bình~Hưng Yên~Khánh Hòa~Kiên Giang~Kon Tum~Lai Châu~Lạng Sơn~Lào Cai~Lâm Đồng~Long An~Nam Định~Nghệ An~Ninh Bình~Ninh Thuận~Phú Thọ~Phú Yên~Quảng Bình~Quảng Nam~Quảng Ngãi~Quảng Ninh~Quảng Trị~Sóc Trăng~Sơn La~Tây Ninh~Thái Bình~Thái Nguyên~Thanh Hóa~Thành phố Hồ Chí Minh~Thừa Thiên–Huế~Tiền Giang~Trà Vinh~Tuyên Quang~Vĩnh Long~Vĩnh Phúc~Yên Bái",
+ sub_lnames:
+ "An Giang Province~Ba Ria-Vung Tau Province~Bac Lieu Province~Bac Giang Province~Bac Kan Province~Bac Ninh Province~Ben Tre Province~Binh Duong Province~Binh Dinh Province~Binh Phuoc Province~Binh Thuan Province~Ca Mau Province~Cao Bang Province~Can Tho City~Da Nang City~Dak Lak Province~Dak Nong Province~Dien Bien Province~Dong Nai Province~Dong Thap Province~Gia Lai Province~Ha Giang Province~Ha Nam Province~Hanoi City~Ha Tinh Province~Hai Duong Province~Haiphong City~Hau Giang Province~Hoa Binh Province~Hung Yen Province~Khanh Hoa Province~Kien Giang Province~Kon Tum Province~Lai Chau Province~Lang Song Province~Lao Cai Province~Lam Dong Province~Long An Province~Nam Dinh Province~Nghe An Province~Ninh Binh Province~Ninh Thuan Province~Phu Tho Province~Phu Yen Province~Quang Binh Province~Quang Nam Province~Quang Ngai Province~Quang Ninh Province~Quang Tri Province~Soc Trang Province~Son La Province~Tay Ninh Province~Thai Binh Province~Thai Nguyen Province~Thanh Hoa Province~Ho Chi Minh City~Thua Thien-Hue Province~Tien Giang Province~Tra Vinh Province~Tuyen Quang Province~Vinh Long Province~Vinh Phuc Province~Yen Bai Province",
+ zip: "\\d{5}\\d?",
+ zipex: "70010,55999",
+ },
+ "data/VU": { id: "data/VU", key: "VU", name: "VANUATU" },
+ "data/WF": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/WF",
+ key: "WF",
+ name: "WALLIS AND FUTUNA ISLANDS",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "986\\d{2}",
+ zipex: "98600",
+ },
+ "data/WS": { id: "data/WS", key: "WS", name: "SAMOA" },
+ "data/XK": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/XK",
+ key: "XK",
+ name: "KOSOVO",
+ zip: "[1-7]\\d{4}",
+ zipex: "10000",
+ },
+ "data/YE": { id: "data/YE", key: "YE", name: "YEMEN" },
+ "data/YT": {
+ fmt: "%O%n%N%n%A%n%Z %C %X",
+ id: "data/YT",
+ key: "YT",
+ name: "MAYOTTE",
+ require: "ACZ",
+ upper: "ACX",
+ zip: "976\\d{2}",
+ zipex: "97600",
+ },
+ "data/ZA": {
+ fmt: "%N%n%O%n%A%n%D%n%C%n%Z",
+ id: "data/ZA",
+ key: "ZA",
+ name: "SOUTH AFRICA",
+ posturl: "https://www.postoffice.co.za/Questions/postalcode.html",
+ require: "ACZ",
+ zip: "\\d{4}",
+ zipex: "0083,1451,0001",
+ },
+ "data/ZM": {
+ fmt: "%N%n%O%n%A%n%Z %C",
+ id: "data/ZM",
+ key: "ZM",
+ name: "ZAMBIA",
+ zip: "\\d{5}",
+ zipex: "50100,50101",
+ },
+ "data/ZW": { id: "data/ZW", key: "ZW", name: "ZIMBABWE" },
+};
+export default AddressMetaData;
diff --git a/toolkit/components/formautofill/shared/AddressMetaDataExtension.sys.mjs b/toolkit/components/formautofill/shared/AddressMetaDataExtension.sys.mjs
new file mode 100644
index 0000000000..da13b66784
--- /dev/null
+++ b/toolkit/components/formautofill/shared/AddressMetaDataExtension.sys.mjs
@@ -0,0 +1,765 @@
+/* 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/. */
+
+export const AddressMetaDataExtension = {
+ "data/AF": {
+ alpha_3_code: "AFG",
+ },
+ "data/AX": {
+ alpha_3_code: "ALA",
+ },
+ "data/AL": {
+ alpha_3_code: "ALB",
+ },
+ "data/DZ": {
+ alpha_3_code: "DZA",
+ },
+ "data/AS": {
+ alpha_3_code: "ASM",
+ },
+ "data/AD": {
+ alpha_3_code: "AND",
+ },
+ "data/AO": {
+ alpha_3_code: "AGO",
+ },
+ "data/AI": {
+ alpha_3_code: "AIA",
+ },
+ "data/AQ": {
+ alpha_3_code: "ATA",
+ },
+ "data/AG": {
+ alpha_3_code: "ATG",
+ },
+ "data/AR": {
+ alpha_3_code: "ARG",
+ },
+ "data/AM": {
+ alpha_3_code: "ARM",
+ },
+ "data/AW": {
+ alpha_3_code: "ABW",
+ },
+ "data/AU": {
+ alpha_3_code: "AUS",
+ },
+ "data/AT": {
+ alpha_3_code: "AUT",
+ },
+ "data/AZ": {
+ alpha_3_code: "AZE",
+ },
+ "data/BS": {
+ alpha_3_code: "BHS",
+ },
+ "data/BH": {
+ alpha_3_code: "BHR",
+ },
+ "data/BD": {
+ alpha_3_code: "BGD",
+ },
+ "data/BB": {
+ alpha_3_code: "BRB",
+ },
+ "data/BY": {
+ alpha_3_code: "BLR",
+ },
+ "data/BE": {
+ alpha_3_code: "BEL",
+ },
+ "data/BZ": {
+ alpha_3_code: "BLZ",
+ },
+ "data/BJ": {
+ alpha_3_code: "BEN",
+ },
+ "data/BM": {
+ alpha_3_code: "BMU",
+ },
+ "data/BT": {
+ alpha_3_code: "BTN",
+ },
+ "data/BO": {
+ alpha_3_code: "BOL",
+ },
+ "data/BQ": {
+ alpha_3_code: "BES",
+ },
+ "data/BA": {
+ alpha_3_code: "BIH",
+ },
+ "data/BW": {
+ alpha_3_code: "BWA",
+ },
+ "data/BV": {
+ alpha_3_code: "BVT",
+ },
+ "data/BR": {
+ alpha_3_code: "BRA",
+ },
+ "data/IO": {
+ alpha_3_code: "IOT",
+ },
+ "data/BN": {
+ alpha_3_code: "BRN",
+ },
+ "data/BG": {
+ alpha_3_code: "BGR",
+ },
+ "data/BF": {
+ alpha_3_code: "BFA",
+ },
+ "data/BI": {
+ alpha_3_code: "BDI",
+ },
+ "data/CV": {
+ alpha_3_code: "CPV",
+ },
+ "data/KH": {
+ alpha_3_code: "KHM",
+ },
+ "data/CM": {
+ alpha_3_code: "CMR",
+ },
+ "data/CA": {
+ alpha_3_code: "CAN",
+ },
+ "data/KY": {
+ alpha_3_code: "CYM",
+ },
+ "data/CF": {
+ alpha_3_code: "CAF",
+ },
+ "data/TD": {
+ alpha_3_code: "TCD",
+ },
+ "data/CL": {
+ alpha_3_code: "CHL",
+ },
+ "data/CN": {
+ alpha_3_code: "CHN",
+ },
+ "data/CX": {
+ alpha_3_code: "CXR",
+ },
+ "data/CC": {
+ alpha_3_code: "CCK",
+ },
+ "data/CO": {
+ alpha_3_code: "COL",
+ },
+ "data/KM": {
+ alpha_3_code: "COM",
+ },
+ "data/CG": {
+ alpha_3_code: "COG",
+ },
+ "data/CD": {
+ alpha_3_code: "COD",
+ },
+ "data/CK": {
+ alpha_3_code: "COK",
+ },
+ "data/CR": {
+ alpha_3_code: "CRI",
+ },
+ "data/CI": {
+ alpha_3_code: "CIV",
+ },
+ "data/HR": {
+ alpha_3_code: "HRV",
+ },
+ "data/CU": {
+ alpha_3_code: "CUB",
+ },
+ "data/CW": {
+ alpha_3_code: "CUW",
+ },
+ "data/CY": {
+ alpha_3_code: "CYP",
+ },
+ "data/CZ": {
+ alpha_3_code: "CZE",
+ },
+ "data/DK": {
+ alpha_3_code: "DNK",
+ },
+ "data/DJ": {
+ alpha_3_code: "DJI",
+ },
+ "data/DM": {
+ alpha_3_code: "DMA",
+ },
+ "data/DO": {
+ alpha_3_code: "DOM",
+ },
+ "data/EC": {
+ alpha_3_code: "ECU",
+ },
+ "data/EG": {
+ alpha_3_code: "EGY",
+ },
+ "data/SV": {
+ alpha_3_code: "SLV",
+ },
+ "data/GQ": {
+ alpha_3_code: "GNQ",
+ },
+ "data/ER": {
+ alpha_3_code: "ERI",
+ },
+ "data/EE": {
+ alpha_3_code: "EST",
+ },
+ "data/SZ": {
+ alpha_3_code: "SWZ",
+ },
+ "data/ET": {
+ alpha_3_code: "ETH",
+ },
+ "data/FK": {
+ alpha_3_code: "FLK",
+ },
+ "data/FO": {
+ alpha_3_code: "FRO",
+ },
+ "data/FJ": {
+ alpha_3_code: "FJI",
+ },
+ "data/FI": {
+ alpha_3_code: "FIN",
+ },
+ "data/FR": {
+ alpha_3_code: "FRA",
+ },
+ "data/GF": {
+ alpha_3_code: "GUF",
+ },
+ "data/PF": {
+ alpha_3_code: "PYF",
+ },
+ "data/TF": {
+ alpha_3_code: "ATF",
+ },
+ "data/GA": {
+ alpha_3_code: "GAB",
+ },
+ "data/GM": {
+ alpha_3_code: "GMB",
+ },
+ "data/GE": {
+ alpha_3_code: "GEO",
+ },
+ "data/DE": {
+ alpha_3_code: "DEU",
+ },
+ "data/GH": {
+ alpha_3_code: "GHA",
+ },
+ "data/GI": {
+ alpha_3_code: "GIB",
+ },
+ "data/GR": {
+ alpha_3_code: "GRC",
+ },
+ "data/GL": {
+ alpha_3_code: "GRL",
+ },
+ "data/GD": {
+ alpha_3_code: "GRD",
+ },
+ "data/GP": {
+ alpha_3_code: "GLP",
+ },
+ "data/GU": {
+ alpha_3_code: "GUM",
+ },
+ "data/GT": {
+ alpha_3_code: "GTM",
+ },
+ "data/GG": {
+ alpha_3_code: "GGY",
+ },
+ "data/GN": {
+ alpha_3_code: "GIN",
+ },
+ "data/GW": {
+ alpha_3_code: "GNB",
+ },
+ "data/GY": {
+ alpha_3_code: "GUY",
+ },
+ "data/HT": {
+ alpha_3_code: "HTI",
+ },
+ "data/HM": {
+ alpha_3_code: "HMD",
+ },
+ "data/VA": {
+ alpha_3_code: "VAT",
+ },
+ "data/HN": {
+ alpha_3_code: "HND",
+ },
+ "data/HK": {
+ alpha_3_code: "HKG",
+ },
+ "data/HU": {
+ alpha_3_code: "HUN",
+ },
+ "data/IS": {
+ alpha_3_code: "ISL",
+ },
+ "data/IN": {
+ alpha_3_code: "IND",
+ },
+ "data/ID": {
+ alpha_3_code: "IDN",
+ },
+ "data/IR": {
+ alpha_3_code: "IRN",
+ },
+ "data/IQ": {
+ alpha_3_code: "IRQ",
+ },
+ "data/IE": {
+ alpha_3_code: "IRL",
+ },
+ "data/IM": {
+ alpha_3_code: "IMN",
+ },
+ "data/IL": {
+ alpha_3_code: "ISR",
+ },
+ "data/IT": {
+ alpha_3_code: "ITA",
+ },
+ "data/JM": {
+ alpha_3_code: "JAM",
+ },
+ "data/JP": {
+ alpha_3_code: "JPN",
+ },
+ "data/JE": {
+ alpha_3_code: "JEY",
+ },
+ "data/JO": {
+ alpha_3_code: "JOR",
+ },
+ "data/KZ": {
+ alpha_3_code: "KAZ",
+ },
+ "data/KE": {
+ alpha_3_code: "KEN",
+ },
+ "data/KI": {
+ alpha_3_code: "KIR",
+ },
+ "data/KP": {
+ alpha_3_code: "PRK",
+ },
+ "data/KR": {
+ alpha_3_code: "KOR",
+ },
+ "data/KW": {
+ alpha_3_code: "KWT",
+ },
+ "data/KG": {
+ alpha_3_code: "KGZ",
+ },
+ "data/LA": {
+ alpha_3_code: "LAO",
+ },
+ "data/LV": {
+ alpha_3_code: "LVA",
+ },
+ "data/LB": {
+ alpha_3_code: "LBN",
+ },
+ "data/LS": {
+ alpha_3_code: "LSO",
+ },
+ "data/LR": {
+ alpha_3_code: "LBR",
+ },
+ "data/LY": {
+ alpha_3_code: "LBY",
+ },
+ "data/LI": {
+ alpha_3_code: "LIE",
+ },
+ "data/LT": {
+ alpha_3_code: "LTU",
+ },
+ "data/LU": {
+ alpha_3_code: "LUX",
+ },
+ "data/MO": {
+ alpha_3_code: "MAC",
+ },
+ "data/MG": {
+ alpha_3_code: "MDG",
+ },
+ "data/MW": {
+ alpha_3_code: "MWI",
+ },
+ "data/MY": {
+ alpha_3_code: "MYS",
+ },
+ "data/MV": {
+ alpha_3_code: "MDV",
+ },
+ "data/ML": {
+ alpha_3_code: "MLI",
+ },
+ "data/MT": {
+ alpha_3_code: "MLT",
+ },
+ "data/MH": {
+ alpha_3_code: "MHL",
+ },
+ "data/MQ": {
+ alpha_3_code: "MTQ",
+ },
+ "data/MR": {
+ alpha_3_code: "MRT",
+ },
+ "data/MU": {
+ alpha_3_code: "MUS",
+ },
+ "data/YT": {
+ alpha_3_code: "MYT",
+ },
+ "data/MX": {
+ alpha_3_code: "MEX",
+ },
+ "data/FM": {
+ alpha_3_code: "FSM",
+ },
+ "data/MD": {
+ alpha_3_code: "MDA",
+ },
+ "data/MC": {
+ alpha_3_code: "MCO",
+ },
+ "data/MN": {
+ alpha_3_code: "MNG",
+ },
+ "data/ME": {
+ alpha_3_code: "MNE",
+ },
+ "data/MS": {
+ alpha_3_code: "MSR",
+ },
+ "data/MA": {
+ alpha_3_code: "MAR",
+ },
+ "data/MZ": {
+ alpha_3_code: "MOZ",
+ },
+ "data/MM": {
+ alpha_3_code: "MMR",
+ },
+ "data/NA": {
+ alpha_3_code: "NAM",
+ },
+ "data/NR": {
+ alpha_3_code: "NRU",
+ },
+ "data/NP": {
+ alpha_3_code: "NPL",
+ },
+ "data/NL": {
+ alpha_3_code: "NLD",
+ },
+ "data/NC": {
+ alpha_3_code: "NCL",
+ },
+ "data/NZ": {
+ alpha_3_code: "NZL",
+ },
+ "data/NI": {
+ alpha_3_code: "NIC",
+ },
+ "data/NE": {
+ alpha_3_code: "NER",
+ },
+ "data/NG": {
+ alpha_3_code: "NGA",
+ },
+ "data/NU": {
+ alpha_3_code: "NIU",
+ },
+ "data/NF": {
+ alpha_3_code: "NFK",
+ },
+ "data/MK": {
+ alpha_3_code: "MKD",
+ },
+ "data/MP": {
+ alpha_3_code: "MNP",
+ },
+ "data/NO": {
+ alpha_3_code: "NOR",
+ },
+ "data/OM": {
+ alpha_3_code: "OMN",
+ },
+ "data/PK": {
+ alpha_3_code: "PAK",
+ },
+ "data/PW": {
+ alpha_3_code: "PLW",
+ },
+ "data/PS": {
+ alpha_3_code: "PSE",
+ },
+ "data/PA": {
+ alpha_3_code: "PAN",
+ },
+ "data/PG": {
+ alpha_3_code: "PNG",
+ },
+ "data/PY": {
+ alpha_3_code: "PRY",
+ },
+ "data/PE": {
+ alpha_3_code: "PER",
+ },
+ "data/PH": {
+ alpha_3_code: "PHL",
+ },
+ "data/PN": {
+ alpha_3_code: "PCN",
+ },
+ "data/PL": {
+ alpha_3_code: "POL",
+ },
+ "data/PT": {
+ alpha_3_code: "PRT",
+ },
+ "data/PR": {
+ alpha_3_code: "PRI",
+ },
+ "data/QA": {
+ alpha_3_code: "QAT",
+ },
+ "data/RE": {
+ alpha_3_code: "REU",
+ },
+ "data/RO": {
+ alpha_3_code: "ROU",
+ },
+ "data/RU": {
+ alpha_3_code: "RUS",
+ },
+ "data/RW": {
+ alpha_3_code: "RWA",
+ },
+ "data/BL": {
+ alpha_3_code: "BLM",
+ },
+ "data/SH": {
+ alpha_3_code: "SHN",
+ },
+ "data/KN": {
+ alpha_3_code: "KNA",
+ },
+ "data/LC": {
+ alpha_3_code: "LCA",
+ },
+ "data/MF": {
+ alpha_3_code: "MAF",
+ },
+ "data/PM": {
+ alpha_3_code: "SPM",
+ },
+ "data/VC": {
+ alpha_3_code: "VCT",
+ },
+ "data/WS": {
+ alpha_3_code: "WSM",
+ },
+ "data/SM": {
+ alpha_3_code: "SMR",
+ },
+ "data/ST": {
+ alpha_3_code: "STP",
+ },
+ "data/SA": {
+ alpha_3_code: "SAU",
+ },
+ "data/SN": {
+ alpha_3_code: "SEN",
+ },
+ "data/RS": {
+ alpha_3_code: "SRB",
+ },
+ "data/SC": {
+ alpha_3_code: "SYC",
+ },
+ "data/SL": {
+ alpha_3_code: "SLE",
+ },
+ "data/SG": {
+ alpha_3_code: "SGP",
+ },
+ "data/SX": {
+ alpha_3_code: "SXM",
+ },
+ "data/SK": {
+ alpha_3_code: "SVK",
+ },
+ "data/SI": {
+ alpha_3_code: "SVN",
+ },
+ "data/SB": {
+ alpha_3_code: "SLB",
+ },
+ "data/SO": {
+ alpha_3_code: "SOM",
+ },
+ "data/ZA": {
+ alpha_3_code: "ZAF",
+ },
+ "data/GS": {
+ alpha_3_code: "SGS",
+ },
+ "data/SS": {
+ alpha_3_code: "SSD",
+ },
+ "data/ES": {
+ alpha_3_code: "ESP",
+ },
+ "data/LK": {
+ alpha_3_code: "LKA",
+ },
+ "data/SD": {
+ alpha_3_code: "SDN",
+ },
+ "data/SR": {
+ alpha_3_code: "SUR",
+ },
+ "data/SJ": {
+ alpha_3_code: "SJM",
+ },
+ "data/SE": {
+ alpha_3_code: "SWE",
+ },
+ "data/CH": {
+ alpha_3_code: "CHE",
+ },
+ "data/SY": {
+ alpha_3_code: "SYR",
+ },
+ "data/TW": {
+ alpha_3_code: "TWN",
+ },
+ "data/TJ": {
+ alpha_3_code: "TJK",
+ },
+ "data/TZ": {
+ alpha_3_code: "TZA",
+ },
+ "data/TH": {
+ alpha_3_code: "THA",
+ },
+ "data/TL": {
+ alpha_3_code: "TLS",
+ },
+ "data/TG": {
+ alpha_3_code: "TGO",
+ },
+ "data/TK": {
+ alpha_3_code: "TKL",
+ },
+ "data/TO": {
+ alpha_3_code: "TON",
+ },
+ "data/TT": {
+ alpha_3_code: "TTO",
+ },
+ "data/TN": {
+ alpha_3_code: "TUN",
+ },
+ "data/TR": {
+ alpha_3_code: "TUR",
+ },
+ "data/TM": {
+ alpha_3_code: "TKM",
+ },
+ "data/TC": {
+ alpha_3_code: "TCA",
+ },
+ "data/TV": {
+ alpha_3_code: "TUV",
+ },
+ "data/UG": {
+ alpha_3_code: "UGA",
+ },
+ "data/UA": {
+ alpha_3_code: "UKR",
+ },
+ "data/AE": {
+ alpha_3_code: "ARE",
+ },
+ "data/GB": {
+ alpha_3_code: "GBR",
+ },
+ "data/US": {
+ alternative_names: [
+ "US",
+ "United States of America",
+ "United States",
+ "America",
+ "U.S.",
+ "USA",
+ "U.S.A.",
+ "U.S.A",
+ ],
+ alpha_3_code: "USA",
+ },
+ "data/UM": {
+ alpha_3_code: "UMI",
+ },
+ "data/UY": {
+ alpha_3_code: "URY",
+ },
+ "data/UZ": {
+ alpha_3_code: "UZB",
+ },
+ "data/VU": {
+ alpha_3_code: "VUT",
+ },
+ "data/VE": {
+ alpha_3_code: "VEN",
+ },
+ "data/VN": {
+ alpha_3_code: "VNM",
+ },
+ "data/VG": {
+ alpha_3_code: "VGB",
+ },
+ "data/VI": {
+ alpha_3_code: "VIR",
+ },
+ "data/WF": {
+ alpha_3_code: "WLF",
+ },
+ "data/EH": {
+ alpha_3_code: "ESH",
+ },
+ "data/YE": {
+ alpha_3_code: "YEM",
+ },
+ "data/ZM": {
+ alpha_3_code: "ZMB",
+ },
+ "data/ZW": {
+ alpha_3_code: "ZWE",
+ },
+};
+
+export default AddressMetaDataExtension;
diff --git a/toolkit/components/formautofill/shared/AddressMetaDataLoader.sys.mjs b/toolkit/components/formautofill/shared/AddressMetaDataLoader.sys.mjs
new file mode 100644
index 0000000000..a7be227921
--- /dev/null
+++ b/toolkit/components/formautofill/shared/AddressMetaDataLoader.sys.mjs
@@ -0,0 +1,168 @@
+/* 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, {
+ AddressMetaData: "resource://gre/modules/shared/AddressMetaData.sys.mjs",
+ AddressMetaDataExtension:
+ "resource://gre/modules/shared/AddressMetaDataExtension.sys.mjs",
+});
+
+export class AddressMetaDataLoader {
+ // Status of address data loading. We'll load all the countries with basic level 1
+ // information while requesting conutry information, and set country to true.
+ // Level 1 Set is for recording which country's level 1/level 2 data is loaded,
+ // since we only load this when getCountryAddressData called with level 1 parameter.
+ static dataLoaded = {
+ country: false,
+ level1: new Set(),
+ };
+
+ static addressData = {};
+
+ static DATA_PREFIX = "data/";
+
+ /**
+ * Load address meta data and extension into one object.
+ *
+ * @returns {object}
+ * An object containing address data object with properties from extension.
+ */
+ static loadAddressMetaData() {
+ const addressMetaData = lazy.AddressMetaData;
+
+ for (const key in lazy.AddressMetaDataExtension) {
+ let addressDataForKey = addressMetaData[key];
+ if (!addressDataForKey) {
+ addressDataForKey = addressMetaData[key] = {};
+ }
+
+ Object.assign(addressDataForKey, lazy.AddressMetaDataExtension[key]);
+ }
+ return addressMetaData;
+ }
+
+ /**
+ * Convert certain properties' string value into array. We should make sure
+ * the cached data is parsed.
+ *
+ * @param {object} data Original metadata from addressReferences.
+ * @returns {object} parsed metadata with property value that converts to array.
+ */
+ static #parse(data) {
+ if (!data) {
+ return null;
+ }
+
+ const properties = [
+ "languages",
+ "sub_keys",
+ "sub_isoids",
+ "sub_names",
+ "sub_lnames",
+ ];
+ for (const key of properties) {
+ if (!data[key]) {
+ continue;
+ }
+ // No need to normalize data if the value is array already.
+ if (Array.isArray(data[key])) {
+ return data;
+ }
+
+ data[key] = data[key].split("~");
+ }
+ return data;
+ }
+
+ /**
+ * We'll cache addressData in the loader once the data loaded from scripts.
+ * It'll become the example below after loading addressReferences with extension:
+ * addressData: {
+ * "data/US": {"lang": ["en"], ...// Data defined in libaddressinput metadata
+ * "alternative_names": ... // Data defined in extension }
+ * "data/CA": {} // Other supported country metadata
+ * "data/TW": {} // Other supported country metadata
+ * "data/TW/台北市": {} // Other supported country level 1 metadata
+ * }
+ *
+ * @param {string} country
+ * @param {string?} level1
+ * @returns {object} Default locale metadata
+ */
+ static #loadData(country, level1 = null) {
+ // Load the addressData if needed
+ if (!this.dataLoaded.country) {
+ this.addressData = this.loadAddressMetaData();
+ this.dataLoaded.country = true;
+ }
+ if (!level1) {
+ return this.#parse(this.addressData[`${this.DATA_PREFIX}${country}`]);
+ }
+ // If level1 is set, load addressReferences under country folder with specific
+ // country/level 1 for level 2 information.
+ if (!this.dataLoaded.level1.has(country)) {
+ Object.assign(this.addressData, this.loadAddressMetaData());
+ this.dataLoaded.level1.add(country);
+ }
+ return this.#parse(
+ this.addressData[`${this.DATA_PREFIX}${country}/${level1}`]
+ );
+ }
+
+ /**
+ * Return the region metadata with default locale and other locales (if exists).
+ *
+ * @param {string} country
+ * @param {string?} level1
+ * @returns {object} Return default locale and other locales metadata.
+ */
+ static getData(country, level1 = null) {
+ const defaultLocale = this.#loadData(country, level1);
+ if (!defaultLocale) {
+ return null;
+ }
+
+ const countryData = this.#parse(
+ this.addressData[`${this.DATA_PREFIX}${country}`]
+ );
+ let locales = [];
+ // TODO: Should be able to support multi-locale level 1/ level 2 metadata query
+ // in Bug 1421886
+ if (countryData.languages) {
+ const list = countryData.languages.filter(
+ key => key !== countryData.lang
+ );
+ locales = list.map(key =>
+ this.#parse(this.addressData[`${defaultLocale.id}--${key}`])
+ );
+ }
+ return { defaultLocale, locales };
+ }
+
+ /**
+ * Return an array containing countries alpha2 codes.
+ *
+ * @returns {Array} Return an array containing countries alpha2 codes.
+ */
+ static get #countryCodes() {
+ return Object.keys(lazy.AddressMetaDataExtension).map(dataKey =>
+ dataKey.replace(this.DATA_PREFIX, "")
+ );
+ }
+
+ static getCountries(locales = []) {
+ const displayNames = new Intl.DisplayNames(locales, {
+ type: "region",
+ fallback: "none",
+ });
+ const countriesMap = new Map();
+ for (const countryCode of this.#countryCodes) {
+ countriesMap.set(countryCode, displayNames.of(countryCode));
+ }
+ return countriesMap;
+ }
+}
+
+export default AddressMetaDataLoader;
diff --git a/toolkit/components/formautofill/shared/AddressParser.sys.mjs b/toolkit/components/formautofill/shared/AddressParser.sys.mjs
new file mode 100644
index 0000000000..5cb76934c1
--- /dev/null
+++ b/toolkit/components/formautofill/shared/AddressParser.sys.mjs
@@ -0,0 +1,285 @@
+/* eslint-disable no-useless-concat */
+/* 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/. */
+
+// NamedCaptureGroup class represents a named capturing group in a regular expression
+class NamedCaptureGroup {
+ // The named of this capturing group
+ #name = null;
+
+ // The capturing group
+ #capture = null;
+
+ // The matched result
+ #match = null;
+
+ constructor(name, capture) {
+ this.#name = name;
+ this.#capture = capture;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ get capture() {
+ return this.#capture;
+ }
+
+ get match() {
+ return this.#match;
+ }
+
+ // Setter for the matched result based on the match groups
+ setMatch(matchGroups) {
+ this.#match = matchGroups[this.#name];
+ }
+}
+
+// Base class for different part of a street address regular expression.
+// The regular expression is constructed with prefix, pattern, suffix
+// and separator to extract "value" part.
+// For examplem, when we write "apt 4." to for floor number, its prefix is `apt`,
+// suffix is `.` and value to represent apartment number is `4`.
+class StreetAddressPartRegExp extends NamedCaptureGroup {
+ constructor(name, prefix, pattern, suffix, sep, optional = false) {
+ prefix = prefix ?? "";
+ suffix = suffix ?? "";
+ super(
+ name,
+ `((?:${prefix})(?<${name}>${pattern})(?:${suffix})(?:${sep})+)${
+ optional ? "?" : ""
+ }`
+ );
+ }
+}
+
+// A regular expression to match the street number portion of a street address,
+class StreetNumberRegExp extends StreetAddressPartRegExp {
+ static PREFIX = "((no|°|º|number)(\\.|-|\\s)*)?"; // From chromium source
+
+ static PATTERN = "\\d+\\w?";
+
+ // TODO: possible suffix : (th\\.|\\.)?
+ static SUFFIX = null;
+
+ constructor(sep, optional) {
+ super(
+ StreetNumberRegExp.name,
+ StreetNumberRegExp.PREFIX,
+ StreetNumberRegExp.PATTERN,
+ StreetNumberRegExp.SUFFIX,
+ sep,
+ optional
+ );
+ }
+}
+
+// A regular expression to match the street name portion of a street address,
+class StreetNameRegExp extends StreetAddressPartRegExp {
+ static PREFIX = null;
+
+ static PATTERN = "(?:[^\\s,]+(?:[^\\S\\r\\n]+[^\\s,]+)*?)"; // From chromium source
+
+ // TODO: Should we consider suffix like (ave|st)?
+ static SUFFIX = null;
+
+ constructor(sep, optional) {
+ super(
+ StreetNameRegExp.name,
+ StreetNameRegExp.PREFIX,
+ StreetNameRegExp.PATTERN,
+ StreetNameRegExp.SUFFIX,
+ sep,
+ optional
+ );
+ }
+}
+
+// A regular expression to match the apartment number portion of a street address,
+class ApartmentNumberRegExp extends StreetAddressPartRegExp {
+ static keyword = "apt|apartment|wohnung|apto|-" + "|unit|suite|ste|#|room"; // From chromium source // Firefox specific
+ static PREFIX = `(${ApartmentNumberRegExp.keyword})(\\.|\\s|-)*`;
+
+ static PATTERN = "\\w*([-|\\/]\\w*)?";
+
+ static SUFFIX = "(\\.|\\s|-)*(ª)?"; // From chromium source
+
+ constructor(sep, optional) {
+ super(
+ ApartmentNumberRegExp.name,
+ ApartmentNumberRegExp.PREFIX,
+ ApartmentNumberRegExp.PATTERN,
+ ApartmentNumberRegExp.SUFFIX,
+ sep,
+ optional
+ );
+ }
+}
+
+// A regular expression to match the floor number portion of a street address,
+class FloorNumberRegExp extends StreetAddressPartRegExp {
+ static keyword =
+ "floor|flur|fl|og|obergeschoss|ug|untergeschoss|geschoss|andar|piso|º" + // From chromium source
+ "|level|lvl"; // Firefox specific
+ static PREFIX = `(${FloorNumberRegExp.keyword})?(\\.|\\s|-)*`; // TODO
+ static PATTERN = "\\d{1,3}\\w?";
+ static SUFFIX = `(st|nd|rd|th)?(\\.|\\s|-)*(${FloorNumberRegExp.keyword})?`; // TODO
+
+ constructor(sep, optional) {
+ super(
+ FloorNumberRegExp.name,
+ FloorNumberRegExp.PREFIX,
+ FloorNumberRegExp.PATTERN,
+ FloorNumberRegExp.SUFFIX,
+ sep,
+ optional
+ );
+ }
+}
+
+/**
+ * Class represents a street address with the following fields:
+ * - street number
+ * - street name
+ * - apartment number
+ * - floor number
+ */
+export class StructuredStreetAddress {
+ #street_number = null;
+ #street_name = null;
+ #apartment_number = null;
+ #floor_number = null;
+
+ constructor(street_number, street_name, apartment_number, floor_number) {
+ this.#street_number = street_number?.toString();
+ this.#street_name = street_name?.toString();
+ this.#apartment_number = apartment_number?.toString();
+ this.#floor_number = floor_number?.toString();
+ }
+
+ get street_number() {
+ return this.#street_number;
+ }
+
+ get street_name() {
+ return this.#street_name;
+ }
+
+ get apartment_number() {
+ return this.#apartment_number;
+ }
+
+ get floor_number() {
+ return this.#floor_number;
+ }
+
+ toString() {
+ return `
+ street number: ${this.#street_number}\n
+ street name: ${this.#street_name}\n
+ apartment number: ${this.#apartment_number}\n
+ floor number: ${this.#floor_number}\n
+ `;
+ }
+}
+
+export class AddressParser {
+ /**
+ * Parse street address with the following pattern.
+ * street number, street name, apartment number(optional), floor number(optional)
+ * For example, 2 Harrison St #175 floor 2
+ *
+ * @param {string} address The street address to be parsed.
+ * @returns {StructuredStreetAddress}
+ */
+ static parseStreetAddress(address) {
+ if (!address) {
+ return null;
+ }
+
+ const separator = "(\\s|,|$)";
+
+ const regexpes = [
+ new StreetNumberRegExp(separator),
+ new StreetNameRegExp(separator),
+ new ApartmentNumberRegExp(separator, true),
+ new FloorNumberRegExp(separator, true),
+ ];
+
+ return AddressParser.parse(address, regexpes)
+ ? new StructuredStreetAddress(...regexpes.map(regexp => regexp.match))
+ : null;
+ }
+
+ static parse(address, regexpes) {
+ const options = {
+ trim: true,
+ merge_whitespace: true,
+ ignore_case: true,
+ };
+ address = AddressParser.normalizeString(address, options);
+
+ const match = address.match(
+ new RegExp(`^(${regexpes.map(regexp => regexp.capture).join("")})$`)
+ );
+ if (!match) {
+ return null;
+ }
+
+ regexpes.forEach(regexp => regexp.setMatch(match.groups));
+ return regexpes.reduce((acc, current) => {
+ return { ...acc, [current.name]: current.match };
+ }, {});
+ }
+
+ static normalizeString(s, options) {
+ if (typeof s != "string") {
+ return s;
+ }
+
+ if (options.ignore_case) {
+ s = s.toLowerCase();
+ }
+
+ // process punctuation before whitespace because if a punctuation
+ // is replaced with whitespace, we might want to merge it later
+ if (options.remove_punctuation) {
+ s = AddressParser.replacePunctuation(s, "");
+ } else if ("replace_punctuation" in options) {
+ const replace = options.replace_punctuation;
+ s = AddressParser.replacePunctuation(s, replace);
+ }
+
+ // process whitespace
+ if (options.merge_whitespace) {
+ s = AddressParser.mergeWhitespace(s);
+ } else if (options.remove_whitespace) {
+ s = AddressParser.removeWhitespace(s);
+ }
+
+ return s.trim();
+ }
+
+ static replacePunctuation(s, replace) {
+ const regex = /\p{Punctuation}/gu;
+ return s?.replace(regex, replace);
+ }
+
+ static removePunctuation(s) {
+ return s?.replace(/[.,\/#!$%\^&\*;:{}=\-_~()]/g, "");
+ }
+
+ static replaceControlCharacters(s, replace) {
+ return s?.replace(/[\t\n\r]/g, " ");
+ }
+
+ static removeWhitespace(s) {
+ return s?.replace(/[\s]/g, "");
+ }
+
+ static mergeWhitespace(s) {
+ return s?.replace(/\s{2,}/g, " ");
+ }
+}
diff --git a/toolkit/components/formautofill/shared/CreditCardRecord.sys.mjs b/toolkit/components/formautofill/shared/CreditCardRecord.sys.mjs
new file mode 100644
index 0000000000..97235e8cdd
--- /dev/null
+++ b/toolkit/components/formautofill/shared/CreditCardRecord.sys.mjs
@@ -0,0 +1,66 @@
+/* eslint-disable no-useless-concat */
+/* 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 { CreditCard } from "resource://gre/modules/CreditCard.sys.mjs";
+import { FormAutofillNameUtils } from "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs";
+
+/**
+ * The CreditCardRecord class serves to handle and normalize internal credit card records.
+ * Unlike the CreditCard class, which represents actual card data, CreditCardRecord is used
+ * for processing and consistent data representation.
+ */
+export class CreditCardRecord {
+ static normalizeFields(creditCard) {
+ this.#normalizeCCNameFields(creditCard);
+ this.#normalizeCCNumberFields(creditCard);
+ this.#normalizeCCExpirationDateFields(creditCard);
+ this.#normalizeCCTypeFields(creditCard);
+ }
+
+ static #normalizeCCNameFields(creditCard) {
+ if (!creditCard["cc-name"]) {
+ creditCard["cc-name"] = FormAutofillNameUtils.joinNameParts({
+ given: creditCard["cc-given-name"] ?? "",
+ middle: creditCard["cc-additional-name"] ?? "",
+ family: creditCard["cc-family-name"] ?? "",
+ });
+ }
+
+ delete creditCard["cc-given-name"];
+ delete creditCard["cc-additional-name"];
+ delete creditCard["cc-family-name"];
+ }
+
+ static #normalizeCCNumberFields(creditCard) {
+ if (!("cc-number" in creditCard)) {
+ return;
+ }
+
+ if (!CreditCard.isValidNumber(creditCard["cc-number"])) {
+ delete creditCard["cc-number"];
+ return;
+ }
+
+ const card = new CreditCard({ number: creditCard["cc-number"] });
+ creditCard["cc-number"] = card.number;
+ }
+
+ static #normalizeCCExpirationDateFields(creditCard) {
+ let normalizedExpiration = CreditCard.normalizeExpiration({
+ expirationMonth: creditCard["cc-exp-month"],
+ expirationYear: creditCard["cc-exp-year"],
+ expirationString: creditCard["cc-exp"],
+ });
+
+ creditCard["cc-exp-month"] = normalizedExpiration.month ?? "";
+ creditCard["cc-exp-year"] = normalizedExpiration.year ?? "";
+ delete creditCard["cc-exp"];
+ }
+
+ static #normalizeCCTypeFields(creditCard) {
+ // Let's overwrite the credit card type with auto-detect algorithm
+ creditCard["cc-type"] = CreditCard.getType(creditCard["cc-number"]) ?? "";
+ }
+}
diff --git a/toolkit/components/formautofill/shared/CreditCardRuleset.sys.mjs b/toolkit/components/formautofill/shared/CreditCardRuleset.sys.mjs
new file mode 100644
index 0000000000..26651fe65a
--- /dev/null
+++ b/toolkit/components/formautofill/shared/CreditCardRuleset.sys.mjs
@@ -0,0 +1,1221 @@
+/* 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/. */
+
+/**
+ * Fathom ML model for identifying the fields of credit-card forms
+ *
+ * This is developed out-of-tree at https://github.com/mozilla-services/fathom-
+ * form-autofill, where there is also over a GB of training, validation, and
+ * testing data. To make changes, do your edits there (whether adding new
+ * training pages, adding new rules, or both), retrain and evaluate as
+ * documented at https://mozilla.github.io/fathom/training.html, paste the
+ * coefficients emitted by the trainer into the ruleset, and finally copy the
+ * ruleset's "CODE TO COPY INTO PRODUCTION" section to this file's "CODE FROM
+ * TRAINING REPOSITORY" section.
+ */
+
+/**
+ * CODE UNIQUE TO PRODUCTION--NOT IN THE TRAINING REPOSITORY:
+ */
+
+import {
+ element as clickedElement,
+ out,
+ rule,
+ ruleset,
+ score,
+ type,
+} from "resource://gre/modules/third_party/fathom/fathom.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
+import {
+ CreditCard,
+ NETWORK_NAMES,
+} from "resource://gre/modules/CreditCard.sys.mjs";
+
+import { FormLikeFactory } from "resource://gre/modules/FormLikeFactory.sys.mjs";
+import { LabelUtils } from "resource://gre/modules/shared/LabelUtils.sys.mjs";
+
+/**
+ * Callthrough abstraction to allow .getAutocompleteInfo() to be mocked out
+ * during training
+ *
+ * @param {Element} element DOM element to get info about
+ * @returns {object} Page-author-provided autocomplete metadata
+ */
+function getAutocompleteInfo(element) {
+ return element.getAutocompleteInfo();
+}
+
+/**
+ * @param {string} selector A CSS selector that prunes away ineligible elements
+ * @returns {Lhs} An LHS yielding the element the user has clicked or, if
+ * pruned, none
+ */
+function queriedOrClickedElements(selector) {
+ return clickedElement(selector);
+}
+
+/**
+ * START OF CODE PASTED FROM TRAINING REPOSITORY
+ */
+
+var FathomHeuristicsRegExp = {
+ RULES: {
+ "cc-name": undefined,
+ "cc-number": undefined,
+ "cc-exp-month": undefined,
+ "cc-exp-year": undefined,
+ "cc-exp": undefined,
+ "cc-type": undefined,
+ },
+
+ RULE_SETS: [
+ {
+ /* eslint-disable */
+ // Let us keep our consistent wrapping.
+ "cc-name":
+ // Firefox-specific rules
+ "account.*holder.*name" +
+ "|^(credit[-\\s]?card|card).*name" +
+ // de-DE
+ "|^(kredit)?(karten|konto)inhaber" +
+ "|^(name).*karte" +
+ // fr-FR
+ "|nom.*(titulaire|détenteur)" +
+ "|(titulaire|détenteur).*(carte)" +
+ // it-IT
+ "|titolare.*carta" +
+ // pl-PL
+ "|posiadacz.*karty" +
+ // es-ES
+ "|nombre.*(titular|tarjeta)" +
+ // nl-NL
+ "|naam.*op.*kaart" +
+ // Rules from Bitwarden
+ "|cc-?name" +
+ "|card-?name" +
+ "|cardholder-?name" +
+ "|(^nom$)" +
+ // Rules are from Chromium source codes
+ "|card.?(?:holder|owner)|name.*(\\b)?on(\\b)?.*card" +
+ "|(?:card|cc).?name|cc.?full.?name" +
+ "|(?:card|cc).?owner" +
+ "|nom.*carte" + // fr-FR
+ "|nome.*cart" + // it-IT
+ "|名前" + // ja-JP
+ "|Имя.*карты" + // ru
+ "|信用卡开户名|开户名|持卡人姓名" + // zh-CN
+ "|持卡人姓名", // zh-TW
+
+ "cc-number":
+ // Firefox-specific rules
+ // de-DE
+ "(cc|kk)nr" +
+ "|(kredit)?(karten)(nummer|nr)" +
+ // it-IT
+ "|numero.*carta" +
+ // fr-FR
+ "|(numero|número|numéro).*(carte)" +
+ // pl-PL
+ "|numer.*karty" +
+ // es-ES
+ "|(número|numero).*tarjeta" +
+ // nl-NL
+ "|kaartnummer" +
+ // Rules from Bitwarden
+ "|cc-?number" +
+ "|cc-?num" +
+ "|card-?number" +
+ "|card-?num" +
+ "|cc-?no" +
+ "|card-?no" +
+ "|numero-?carte" +
+ "|num-?carte" +
+ "|cb-?num" +
+ // Rules are from Chromium source codes
+ "|(add)?(?:card|cc|acct).?(?:number|#|no|num)" +
+ "|カード番号" + // ja-JP
+ "|Номер.*карты" + // ru
+ "|信用卡号|信用卡号码" + // zh-CN
+ "|信用卡卡號" + // zh-TW
+ "|카드", // ko-KR
+
+ "cc-exp":
+ // Firefox-specific rules
+ "mm\\s*(\/|\\|-)\\s*(yy|jj|aa)" +
+ "|(month|mois)\\s*(\/|\\|-|et)\\s*(year|année)" +
+ // de-DE
+ // fr-FR
+ // Rules from Bitwarden
+ "|(^cc-?exp$)" +
+ "|(^card-?exp$)" +
+ "|(^cc-?expiration$)" +
+ "|(^card-?expiration$)" +
+ "|(^cc-?ex$)" +
+ "|(^card-?ex$)" +
+ "|(^card-?expire$)" +
+ "|(^card-?expiry$)" +
+ "|(^validite$)" +
+ "|(^expiration$)" +
+ "|(^expiry$)" +
+ "|mm-?yy" +
+ "|mm-?yyyy" +
+ "|yy-?mm" +
+ "|yyyy-?mm" +
+ "|expiration-?date" +
+ "|payment-?card-?expiration" +
+ "|(^payment-?cc-?date$)" +
+ // Rules are from Chromium source codes
+ "|expir|exp.*date|^expfield$" +
+ "|ablaufdatum|gueltig|gültig" + // de-DE
+ "|fecha" + // es
+ "|date.*exp" + // fr-FR
+ "|scadenza" + // it-IT
+ "|有効期限" + // ja-JP
+ "|validade" + // pt-BR, pt-PT
+ "|Срок действия карты", // ru
+
+ "cc-exp-month":
+ // Firefox-specific rules
+ "(cc|kk)month" + // de-DE
+ // Rules from Bitwarden
+ "|(^exp-?month$)" +
+ "|(^cc-?exp-?month$)" +
+ "|(^cc-?month$)" +
+ "|(^card-?month$)" +
+ "|(^cc-?mo$)" +
+ "|(^card-?mo$)" +
+ "|(^exp-?mo$)" +
+ "|(^card-?exp-?mo$)" +
+ "|(^cc-?exp-?mo$)" +
+ "|(^card-?expiration-?month$)" +
+ "|(^expiration-?month$)" +
+ "|(^cc-?mm$)" +
+ "|(^cc-?m$)" +
+ "|(^card-?mm$)" +
+ "|(^card-?m$)" +
+ "|(^card-?exp-?mm$)" +
+ "|(^cc-?exp-?mm$)" +
+ "|(^exp-?mm$)" +
+ "|(^exp-?m$)" +
+ "|(^expire-?month$)" +
+ "|(^expire-?mo$)" +
+ "|(^expiry-?month$)" +
+ "|(^expiry-?mo$)" +
+ "|(^card-?expire-?month$)" +
+ "|(^card-?expire-?mo$)" +
+ "|(^card-?expiry-?month$)" +
+ "|(^card-?expiry-?mo$)" +
+ "|(^mois-?validite$)" +
+ "|(^mois-?expiration$)" +
+ "|(^m-?validite$)" +
+ "|(^m-?expiration$)" +
+ "|(^expiry-?date-?field-?month$)" +
+ "|(^expiration-?date-?month$)" +
+ "|(^expiration-?date-?mm$)" +
+ "|(^exp-?mon$)" +
+ "|(^validity-?mo$)" +
+ "|(^exp-?date-?mo$)" +
+ "|(^cb-?date-?mois$)" +
+ "|(^date-?m$)" +
+ // Rules are from Chromium source codes
+ "|exp.*mo|ccmonth|cardmonth|addmonth" +
+ "|monat" + // de-DE
+ // "|fecha" + // es
+ // "|date.*exp" + // fr-FR
+ // "|scadenza" + // it-IT
+ // "|有効期限" + // ja-JP
+ // "|validade" + // pt-BR, pt-PT
+ // "|Срок действия карты" + // ru
+ "|月", // zh-CN
+
+ "cc-exp-year":
+ // Firefox-specific rules
+ "(cc|kk)year" + // de-DE
+ // Rules from Bitwarden
+ "|(^exp-?year$)" +
+ "|(^cc-?exp-?year$)" +
+ "|(^cc-?year$)" +
+ "|(^card-?year$)" +
+ "|(^cc-?yr$)" +
+ "|(^card-?yr$)" +
+ "|(^exp-?yr$)" +
+ "|(^card-?exp-?yr$)" +
+ "|(^cc-?exp-?yr$)" +
+ "|(^card-?expiration-?year$)" +
+ "|(^expiration-?year$)" +
+ "|(^cc-?yy$)" +
+ "|(^cc-?y$)" +
+ "|(^card-?yy$)" +
+ "|(^card-?y$)" +
+ "|(^card-?exp-?yy$)" +
+ "|(^cc-?exp-?yy$)" +
+ "|(^exp-?yy$)" +
+ "|(^exp-?y$)" +
+ "|(^cc-?yyyy$)" +
+ "|(^card-?yyyy$)" +
+ "|(^card-?exp-?yyyy$)" +
+ "|(^cc-?exp-?yyyy$)" +
+ "|(^expire-?year$)" +
+ "|(^expire-?yr$)" +
+ "|(^expiry-?year$)" +
+ "|(^expiry-?yr$)" +
+ "|(^card-?expire-?year$)" +
+ "|(^card-?expire-?yr$)" +
+ "|(^card-?expiry-?year$)" +
+ "|(^card-?expiry-?yr$)" +
+ "|(^an-?validite$)" +
+ "|(^an-?expiration$)" +
+ "|(^annee-?validite$)" +
+ "|(^annee-?expiration$)" +
+ "|(^expiry-?date-?field-?year$)" +
+ "|(^expiration-?date-?year$)" +
+ "|(^cb-?date-?ann$)" +
+ "|(^expiration-?date-?yy$)" +
+ "|(^expiration-?date-?yyyy$)" +
+ "|(^validity-?year$)" +
+ "|(^exp-?date-?year$)" +
+ "|(^date-?y$)" +
+ // Rules are from Chromium source codes
+ "|(add)?year" +
+ "|jahr" + // de-DE
+ // "|fecha" + // es
+ // "|scadenza" + // it-IT
+ // "|有効期限" + // ja-JP
+ // "|validade" + // pt-BR, pt-PT
+ // "|Срок действия карты" + // ru
+ "|年|有效期", // zh-CN
+
+ "cc-type":
+ // Firefox-specific rules
+ "type" +
+ // de-DE
+ "|Kartenmarke" +
+ // Rules from Bitwarden
+ "|(^cc-?type$)" +
+ "|(^card-?type$)" +
+ "|(^card-?brand$)" +
+ "|(^cc-?brand$)" +
+ "|(^cb-?type$)",
+ // Rules are from Chromium source codes
+ },
+ ],
+
+ _getRule(name) {
+ let rules = [];
+ this.RULE_SETS.forEach(set => {
+ if (set[name]) {
+ rules.push(`(${set[name]})`.normalize("NFKC"));
+ }
+ });
+
+ const value = new RegExp(rules.join("|"), "iu");
+ Object.defineProperty(this.RULES, name, { get: undefined });
+ Object.defineProperty(this.RULES, name, { value });
+ return value;
+ },
+
+ init() {
+ Object.keys(this.RULES).forEach(field =>
+ Object.defineProperty(this.RULES, field, {
+ get() {
+ return FathomHeuristicsRegExp._getRule(field);
+ },
+ })
+ );
+ },
+};
+
+FathomHeuristicsRegExp.init();
+
+const MMRegExp = /^mm$|\(mm\)/i;
+const YYorYYYYRegExp = /^(yy|yyyy)$|\(yy\)|\(yyyy\)/i;
+const monthRegExp = /month/i;
+const yearRegExp = /year/i;
+const MMYYRegExp = /mm\s*(\/|\\)\s*yy/i;
+const VisaCheckoutRegExp = /visa(-|\s)checkout/i;
+const CREDIT_CARD_NETWORK_REGEXP = new RegExp(
+ CreditCard.getSupportedNetworks()
+ .concat(Object.keys(NETWORK_NAMES))
+ .join("|"),
+ "gui"
+ );
+const TwoDigitYearRegExp = /(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yy(?:[^y]|$)/i;
+const FourDigitYearRegExp = /(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yyyy(?:[^y]|$)/i;
+const dwfrmRegExp = /^dwfrm/i;
+const bmlRegExp = /bml/i;
+const templatedValue = /^\{\{.*\}\}$/;
+const firstRegExp = /first/i;
+const lastRegExp = /last/i;
+const giftRegExp = /gift/i;
+const subscriptionRegExp = /subscription/i;
+
+function autocompleteStringMatches(element, ccString) {
+ const info = getAutocompleteInfo(element);
+ return info.fieldName === ccString;
+}
+
+function getFillableFormElements(element) {
+ const formLike = FormLikeFactory.createFromField(element);
+ return Array.from(formLike.elements).filter(el =>
+ FormAutofillUtils.isCreditCardOrAddressFieldType(el)
+ );
+}
+
+function nextFillableFormField(element) {
+ const fillableFormElements = getFillableFormElements(element);
+ const elementIndex = fillableFormElements.indexOf(element);
+ return fillableFormElements[elementIndex + 1];
+}
+
+function previousFillableFormField(element) {
+ const fillableFormElements = getFillableFormElements(element);
+ const elementIndex = fillableFormElements.indexOf(element);
+ return fillableFormElements[elementIndex - 1];
+}
+
+function nextFieldPredicateIsTrue(element, predicate) {
+ const nextField = nextFillableFormField(element);
+ return !!nextField && predicate(nextField);
+}
+
+function previousFieldPredicateIsTrue(element, predicate) {
+ const previousField = previousFillableFormField(element);
+ return !!previousField && predicate(previousField);
+}
+
+function nextFieldMatchesExpYearAutocomplete(fnode) {
+ return nextFieldPredicateIsTrue(fnode.element, nextField =>
+ autocompleteStringMatches(nextField, "cc-exp-year")
+ );
+}
+
+function previousFieldMatchesExpMonthAutocomplete(fnode) {
+ return previousFieldPredicateIsTrue(fnode.element, previousField =>
+ autocompleteStringMatches(previousField, "cc-exp-month")
+ );
+}
+
+//////////////////////////////////////////////
+// Attribute Regular Expression Rules
+function idOrNameMatchRegExp(element, regExp) {
+ for (const str of [element.id, element.name]) {
+ if (regExp.test(str)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function getElementLabels(element) {
+ return {
+ *[Symbol.iterator]() {
+ const labels = LabelUtils.findLabelElements(element);
+ for (let label of labels) {
+ yield* LabelUtils.extractLabelStrings(label);
+ }
+ },
+ };
+}
+
+function labelsMatchRegExp(element, regExp) {
+ const elemStrings = getElementLabels(element);
+ for (const str of elemStrings) {
+ if (regExp.test(str)) {
+ return true;
+ }
+ }
+
+ const parentElement = element.parentElement;
+ // Bug 1634819: element.parentElement is null if element.parentNode is a ShadowRoot
+ if (!parentElement) {
+ return false;
+ }
+ // Check if the input is in a <td>, and, if so, check the textContent of the containing <tr>
+ if (parentElement.tagName === "TD" && parentElement.parentElement) {
+ // TODO: How bad is the assumption that the <tr> won't be the parent of the <td>?
+ return regExp.test(parentElement.parentElement.textContent);
+ }
+
+ // Check if the input is in a <dd>, and, if so, check the textContent of the preceding <dt>
+ if (
+ parentElement.tagName === "DD" &&
+ // previousElementSibling can be null
+ parentElement.previousElementSibling
+ ) {
+ return regExp.test(parentElement.previousElementSibling.textContent);
+ }
+ return false;
+}
+
+function closestLabelMatchesRegExp(element, regExp) {
+ const previousElementSibling = element.previousElementSibling;
+ if (
+ previousElementSibling !== null &&
+ previousElementSibling.tagName === "LABEL"
+ ) {
+ return regExp.test(previousElementSibling.textContent);
+ }
+
+ const nextElementSibling = element.nextElementSibling;
+ if (nextElementSibling !== null && nextElementSibling.tagName === "LABEL") {
+ return regExp.test(nextElementSibling.textContent);
+ }
+
+ return false;
+}
+
+function ariaLabelMatchesRegExp(element, regExp) {
+ const ariaLabel = element.getAttribute("aria-label");
+ return !!ariaLabel && regExp.test(ariaLabel);
+}
+
+function placeholderMatchesRegExp(element, regExp) {
+ const placeholder = element.getAttribute("placeholder");
+ return !!placeholder && regExp.test(placeholder);
+}
+
+function nextFieldIdOrNameMatchRegExp(element, regExp) {
+ return nextFieldPredicateIsTrue(element, nextField =>
+ idOrNameMatchRegExp(nextField, regExp)
+ );
+}
+
+function nextFieldLabelsMatchRegExp(element, regExp) {
+ return nextFieldPredicateIsTrue(element, nextField =>
+ labelsMatchRegExp(nextField, regExp)
+ );
+}
+
+function nextFieldPlaceholderMatchesRegExp(element, regExp) {
+ return nextFieldPredicateIsTrue(element, nextField =>
+ placeholderMatchesRegExp(nextField, regExp)
+ );
+}
+
+function nextFieldAriaLabelMatchesRegExp(element, regExp) {
+ return nextFieldPredicateIsTrue(element, nextField =>
+ ariaLabelMatchesRegExp(nextField, regExp)
+ );
+}
+
+function previousFieldIdOrNameMatchRegExp(element, regExp) {
+ return previousFieldPredicateIsTrue(element, previousField =>
+ idOrNameMatchRegExp(previousField, regExp)
+ );
+}
+
+function previousFieldLabelsMatchRegExp(element, regExp) {
+ return previousFieldPredicateIsTrue(element, previousField =>
+ labelsMatchRegExp(previousField, regExp)
+ );
+}
+
+function previousFieldPlaceholderMatchesRegExp(element, regExp) {
+ return previousFieldPredicateIsTrue(element, previousField =>
+ placeholderMatchesRegExp(previousField, regExp)
+ );
+}
+
+function previousFieldAriaLabelMatchesRegExp(element, regExp) {
+ return previousFieldPredicateIsTrue(element, previousField =>
+ ariaLabelMatchesRegExp(previousField, regExp)
+ );
+}
+//////////////////////////////////////////////
+
+function isSelectWithCreditCardOptions(fnode) {
+ // Check every select for options that match credit card network names in
+ // value or label.
+ const element = fnode.element;
+ if (element.tagName === "SELECT") {
+ for (let option of element.querySelectorAll("option")) {
+ if (
+ CreditCard.getNetworkFromName(option.value) ||
+ CreditCard.getNetworkFromName(option.text)
+ ) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * If any of the regular expressions match multiple times, we assume the tested
+ * string belongs to a radio button for payment type instead of card type.
+ *
+ * @param {Fnode} fnode
+ * @returns {boolean}
+ */
+function isRadioWithCreditCardText(fnode) {
+ const element = fnode.element;
+ const inputType = element.type;
+ if (!!inputType && inputType === "radio") {
+ const valueMatches = element.value.match(CREDIT_CARD_NETWORK_REGEXP);
+ if (valueMatches) {
+ return valueMatches.length === 1;
+ }
+
+ // Here we are checking that only one label matches only one entry in the regular expression.
+ const labels = getElementLabels(element);
+ let labelsMatched = 0;
+ for (const label of labels) {
+ const labelMatches = label.match(CREDIT_CARD_NETWORK_REGEXP);
+ if (labelMatches) {
+ if (labelMatches.length > 1) {
+ return false;
+ }
+ labelsMatched++;
+ }
+ }
+ if (labelsMatched > 0) {
+ return labelsMatched === 1;
+ }
+
+ const textContentMatches = element.textContent.match(
+ CREDIT_CARD_NETWORK_REGEXP
+ );
+ if (textContentMatches) {
+ return textContentMatches.length === 1;
+ }
+ }
+ return false;
+}
+
+function matchContiguousSubArray(array, subArray) {
+ return array.some((elm, i) =>
+ subArray.every((sElem, j) => sElem === array[i + j])
+ );
+}
+
+function isExpirationMonthLikely(element) {
+ if (element.tagName !== "SELECT") {
+ 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 (
+ matchContiguousSubArray(
+ options.map(e => +e.value),
+ desiredValues
+ ) ||
+ matchContiguousSubArray(
+ options.map(e => +e.label),
+ desiredValues
+ )
+ );
+}
+
+function isExpirationYearLikely(element) {
+ if (element.tagName !== "SELECT") {
+ 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 (
+ matchContiguousSubArray(
+ options.map(e => +e.value),
+ desiredValues
+ ) ||
+ matchContiguousSubArray(
+ options.map(e => +e.label),
+ desiredValues
+ )
+ );
+}
+
+function nextFieldIsExpirationYearLikely(fnode) {
+ return nextFieldPredicateIsTrue(fnode.element, isExpirationYearLikely);
+}
+
+function previousFieldIsExpirationMonthLikely(fnode) {
+ return previousFieldPredicateIsTrue(fnode.element, isExpirationMonthLikely);
+}
+
+function attrsMatchExpWith2Or4DigitYear(fnode, regExpMatchingFunction) {
+ const element = fnode.element;
+ return (
+ regExpMatchingFunction(element, TwoDigitYearRegExp) ||
+ regExpMatchingFunction(element, FourDigitYearRegExp)
+ );
+}
+
+function maxLengthIs(fnode, maxLengthValue) {
+ return fnode.element.maxLength === maxLengthValue;
+}
+
+function roleIsMenu(fnode) {
+ const role = fnode.element.getAttribute("role");
+ return !!role && role === "menu";
+}
+
+function idOrNameMatchDwfrmAndBml(fnode) {
+ return (
+ idOrNameMatchRegExp(fnode.element, dwfrmRegExp) &&
+ idOrNameMatchRegExp(fnode.element, bmlRegExp)
+ );
+}
+
+function hasTemplatedValue(fnode) {
+ const value = fnode.element.getAttribute("value");
+ return !!value && templatedValue.test(value);
+}
+
+function inputTypeNotNumbery(fnode) {
+ const inputType = fnode.element.type;
+ if (inputType) {
+ return !["text", "tel", "number"].includes(inputType);
+ }
+ return false;
+}
+
+function idOrNameMatchFirstAndLast(fnode) {
+ return (
+ idOrNameMatchRegExp(fnode.element, firstRegExp) &&
+ idOrNameMatchRegExp(fnode.element, lastRegExp)
+ );
+}
+
+/**
+ * Compactly generate a series of rules that all take a single LHS type with no
+ * .when() clause and have only a score() call on the right- hand side.
+ *
+ * @param {Lhs} inType The incoming fnode type that all rules take
+ * @param {object} ruleMap A simple object used as a map with rule names
+ * pointing to scoring callbacks
+ * @yields {Rule}
+ */
+function* simpleScoringRules(inType, ruleMap) {
+ for (const [name, scoringCallback] of Object.entries(ruleMap)) {
+ yield rule(type(inType), score(scoringCallback), { name });
+ }
+}
+
+function makeRuleset(coeffs, biases) {
+ return ruleset(
+ [
+ /**
+ * Factor out the page scan just for a little more speed during training.
+ * This selector is good for most fields. cardType is an exception: it
+ * cannot be type=month.
+ */
+ rule(
+ queriedOrClickedElements(
+ "input:not([type]), input[type=text], input[type=textbox], input[type=email], input[type=tel], input[type=number], input[type=month], select, button"
+ ),
+ type("typicalCandidates")
+ ),
+
+ /**
+ * number rules
+ */
+ rule(type("typicalCandidates"), type("cc-number")),
+ ...simpleScoringRules("cc-number", {
+ idOrNameMatchNumberRegExp: fnode =>
+ idOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-number"]
+ ),
+ labelsMatchNumberRegExp: fnode =>
+ labelsMatchRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-number"]),
+ closestLabelMatchesNumberRegExp: fnode =>
+ closestLabelMatchesRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-number"]),
+ placeholderMatchesNumberRegExp: fnode =>
+ placeholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-number"]
+ ),
+ ariaLabelMatchesNumberRegExp: fnode =>
+ ariaLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-number"]
+ ),
+ idOrNameMatchGift: fnode =>
+ idOrNameMatchRegExp(fnode.element, giftRegExp),
+ labelsMatchGift: fnode => labelsMatchRegExp(fnode.element, giftRegExp),
+ placeholderMatchesGift: fnode =>
+ placeholderMatchesRegExp(fnode.element, giftRegExp),
+ ariaLabelMatchesGift: fnode =>
+ ariaLabelMatchesRegExp(fnode.element, giftRegExp),
+ idOrNameMatchSubscription: fnode =>
+ idOrNameMatchRegExp(fnode.element, subscriptionRegExp),
+ idOrNameMatchDwfrmAndBml,
+ hasTemplatedValue,
+ inputTypeNotNumbery,
+ }),
+ rule(type("cc-number"), out("cc-number")),
+
+ /**
+ * name rules
+ */
+ rule(type("typicalCandidates"), type("cc-name")),
+ ...simpleScoringRules("cc-name", {
+ idOrNameMatchNameRegExp: fnode =>
+ idOrNameMatchRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-name"]),
+ labelsMatchNameRegExp: fnode =>
+ labelsMatchRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-name"]),
+ closestLabelMatchesNameRegExp: fnode =>
+ closestLabelMatchesRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-name"]),
+ placeholderMatchesNameRegExp: fnode =>
+ placeholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-name"]
+ ),
+ ariaLabelMatchesNameRegExp: fnode =>
+ ariaLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-name"]
+ ),
+ idOrNameMatchFirst: fnode =>
+ idOrNameMatchRegExp(fnode.element, firstRegExp),
+ labelsMatchFirst: fnode =>
+ labelsMatchRegExp(fnode.element, firstRegExp),
+ placeholderMatchesFirst: fnode =>
+ placeholderMatchesRegExp(fnode.element, firstRegExp),
+ ariaLabelMatchesFirst: fnode =>
+ ariaLabelMatchesRegExp(fnode.element, firstRegExp),
+ idOrNameMatchLast: fnode =>
+ idOrNameMatchRegExp(fnode.element, lastRegExp),
+ labelsMatchLast: fnode => labelsMatchRegExp(fnode.element, lastRegExp),
+ placeholderMatchesLast: fnode =>
+ placeholderMatchesRegExp(fnode.element, lastRegExp),
+ ariaLabelMatchesLast: fnode =>
+ ariaLabelMatchesRegExp(fnode.element, lastRegExp),
+ idOrNameMatchSubscription: fnode =>
+ idOrNameMatchRegExp(fnode.element, subscriptionRegExp),
+ idOrNameMatchFirstAndLast,
+ idOrNameMatchDwfrmAndBml,
+ hasTemplatedValue,
+ }),
+ rule(type("cc-name"), out("cc-name")),
+
+ /**
+ * cardType rules
+ */
+ rule(
+ queriedOrClickedElements(
+ "input:not([type]), input[type=text], input[type=textbox], input[type=email], input[type=tel], input[type=number], input[type=radio], select, button"
+ ),
+ type("cc-type")
+ ),
+ ...simpleScoringRules("cc-type", {
+ idOrNameMatchTypeRegExp: fnode =>
+ idOrNameMatchRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-type"]),
+ labelsMatchTypeRegExp: fnode =>
+ labelsMatchRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-type"]),
+ closestLabelMatchesTypeRegExp: fnode =>
+ closestLabelMatchesRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-type"]),
+ idOrNameMatchVisaCheckout: fnode =>
+ idOrNameMatchRegExp(fnode.element, VisaCheckoutRegExp),
+ ariaLabelMatchesVisaCheckout: fnode =>
+ ariaLabelMatchesRegExp(fnode.element, VisaCheckoutRegExp),
+ isSelectWithCreditCardOptions,
+ isRadioWithCreditCardText,
+ idOrNameMatchSubscription: fnode =>
+ idOrNameMatchRegExp(fnode.element, subscriptionRegExp),
+ idOrNameMatchDwfrmAndBml,
+ hasTemplatedValue,
+ }),
+ rule(type("cc-type"), out("cc-type")),
+
+ /**
+ * expiration rules
+ */
+ rule(type("typicalCandidates"), type("cc-exp")),
+ ...simpleScoringRules("cc-exp", {
+ labelsMatchExpRegExp: fnode =>
+ labelsMatchRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-exp"]),
+ closestLabelMatchesExpRegExp: fnode =>
+ closestLabelMatchesRegExp(fnode.element, FathomHeuristicsRegExp.RULES["cc-exp"]),
+ placeholderMatchesExpRegExp: fnode =>
+ placeholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp"]
+ ),
+ labelsMatchExpWith2Or4DigitYear: fnode =>
+ attrsMatchExpWith2Or4DigitYear(fnode, labelsMatchRegExp),
+ placeholderMatchesExpWith2Or4DigitYear: fnode =>
+ attrsMatchExpWith2Or4DigitYear(fnode, placeholderMatchesRegExp),
+ labelsMatchMMYY: fnode => labelsMatchRegExp(fnode.element, MMYYRegExp),
+ placeholderMatchesMMYY: fnode =>
+ placeholderMatchesRegExp(fnode.element, MMYYRegExp),
+ maxLengthIs7: fnode => maxLengthIs(fnode, 7),
+ idOrNameMatchSubscription: fnode =>
+ idOrNameMatchRegExp(fnode.element, subscriptionRegExp),
+ idOrNameMatchDwfrmAndBml,
+ hasTemplatedValue,
+ isExpirationMonthLikely: fnode =>
+ isExpirationMonthLikely(fnode.element),
+ isExpirationYearLikely: fnode => isExpirationYearLikely(fnode.element),
+ idOrNameMatchMonth: fnode =>
+ idOrNameMatchRegExp(fnode.element, monthRegExp),
+ idOrNameMatchYear: fnode =>
+ idOrNameMatchRegExp(fnode.element, yearRegExp),
+ idOrNameMatchExpMonthRegExp: fnode =>
+ idOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ idOrNameMatchExpYearRegExp: fnode =>
+ idOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ idOrNameMatchValidation: fnode =>
+ idOrNameMatchRegExp(fnode.element, /validate|validation/i),
+ }),
+ rule(type("cc-exp"), out("cc-exp")),
+
+ /**
+ * expirationMonth rules
+ */
+ rule(type("typicalCandidates"), type("cc-exp-month")),
+ ...simpleScoringRules("cc-exp-month", {
+ idOrNameMatchExpMonthRegExp: fnode =>
+ idOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ labelsMatchExpMonthRegExp: fnode =>
+ labelsMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ closestLabelMatchesExpMonthRegExp: fnode =>
+ closestLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ placeholderMatchesExpMonthRegExp: fnode =>
+ placeholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ ariaLabelMatchesExpMonthRegExp: fnode =>
+ ariaLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ idOrNameMatchMonth: fnode =>
+ idOrNameMatchRegExp(fnode.element, monthRegExp),
+ labelsMatchMonth: fnode =>
+ labelsMatchRegExp(fnode.element, monthRegExp),
+ placeholderMatchesMonth: fnode =>
+ placeholderMatchesRegExp(fnode.element, monthRegExp),
+ ariaLabelMatchesMonth: fnode =>
+ ariaLabelMatchesRegExp(fnode.element, monthRegExp),
+ nextFieldIdOrNameMatchExpYearRegExp: fnode =>
+ nextFieldIdOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ nextFieldLabelsMatchExpYearRegExp: fnode =>
+ nextFieldLabelsMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ nextFieldPlaceholderMatchExpYearRegExp: fnode =>
+ nextFieldPlaceholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ nextFieldAriaLabelMatchExpYearRegExp: fnode =>
+ nextFieldAriaLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ nextFieldIdOrNameMatchYear: fnode =>
+ nextFieldIdOrNameMatchRegExp(fnode.element, yearRegExp),
+ nextFieldLabelsMatchYear: fnode =>
+ nextFieldLabelsMatchRegExp(fnode.element, yearRegExp),
+ nextFieldPlaceholderMatchesYear: fnode =>
+ nextFieldPlaceholderMatchesRegExp(fnode.element, yearRegExp),
+ nextFieldAriaLabelMatchesYear: fnode =>
+ nextFieldAriaLabelMatchesRegExp(fnode.element, yearRegExp),
+ nextFieldMatchesExpYearAutocomplete,
+ isExpirationMonthLikely: fnode =>
+ isExpirationMonthLikely(fnode.element),
+ nextFieldIsExpirationYearLikely,
+ maxLengthIs2: fnode => maxLengthIs(fnode, 2),
+ placeholderMatchesMM: fnode =>
+ placeholderMatchesRegExp(fnode.element, MMRegExp),
+ roleIsMenu,
+ idOrNameMatchSubscription: fnode =>
+ idOrNameMatchRegExp(fnode.element, subscriptionRegExp),
+ idOrNameMatchDwfrmAndBml,
+ hasTemplatedValue,
+ }),
+ rule(type("cc-exp-month"), out("cc-exp-month")),
+
+ /**
+ * expirationYear rules
+ */
+ rule(type("typicalCandidates"), type("cc-exp-year")),
+ ...simpleScoringRules("cc-exp-year", {
+ idOrNameMatchExpYearRegExp: fnode =>
+ idOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ labelsMatchExpYearRegExp: fnode =>
+ labelsMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ closestLabelMatchesExpYearRegExp: fnode =>
+ closestLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ placeholderMatchesExpYearRegExp: fnode =>
+ placeholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ ariaLabelMatchesExpYearRegExp: fnode =>
+ ariaLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-year"]
+ ),
+ idOrNameMatchYear: fnode =>
+ idOrNameMatchRegExp(fnode.element, yearRegExp),
+ labelsMatchYear: fnode => labelsMatchRegExp(fnode.element, yearRegExp),
+ placeholderMatchesYear: fnode =>
+ placeholderMatchesRegExp(fnode.element, yearRegExp),
+ ariaLabelMatchesYear: fnode =>
+ ariaLabelMatchesRegExp(fnode.element, yearRegExp),
+ previousFieldIdOrNameMatchExpMonthRegExp: fnode =>
+ previousFieldIdOrNameMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ previousFieldLabelsMatchExpMonthRegExp: fnode =>
+ previousFieldLabelsMatchRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ previousFieldPlaceholderMatchExpMonthRegExp: fnode =>
+ previousFieldPlaceholderMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ previousFieldAriaLabelMatchExpMonthRegExp: fnode =>
+ previousFieldAriaLabelMatchesRegExp(
+ fnode.element,
+ FathomHeuristicsRegExp.RULES["cc-exp-month"]
+ ),
+ previousFieldIdOrNameMatchMonth: fnode =>
+ previousFieldIdOrNameMatchRegExp(fnode.element, monthRegExp),
+ previousFieldLabelsMatchMonth: fnode =>
+ previousFieldLabelsMatchRegExp(fnode.element, monthRegExp),
+ previousFieldPlaceholderMatchesMonth: fnode =>
+ previousFieldPlaceholderMatchesRegExp(fnode.element, monthRegExp),
+ previousFieldAriaLabelMatchesMonth: fnode =>
+ previousFieldAriaLabelMatchesRegExp(fnode.element, monthRegExp),
+ previousFieldMatchesExpMonthAutocomplete,
+ isExpirationYearLikely: fnode => isExpirationYearLikely(fnode.element),
+ previousFieldIsExpirationMonthLikely,
+ placeholderMatchesYYOrYYYY: fnode =>
+ placeholderMatchesRegExp(fnode.element, YYorYYYYRegExp),
+ roleIsMenu,
+ idOrNameMatchSubscription: fnode =>
+ idOrNameMatchRegExp(fnode.element, subscriptionRegExp),
+ idOrNameMatchDwfrmAndBml,
+ hasTemplatedValue,
+ }),
+ rule(type("cc-exp-year"), out("cc-exp-year")),
+ ],
+ coeffs,
+ biases
+ );
+}
+
+const coefficients = {
+ "cc-number": [
+ ["idOrNameMatchNumberRegExp", 7.679469585418701],
+ ["labelsMatchNumberRegExp", 5.122580051422119],
+ ["closestLabelMatchesNumberRegExp", 2.1256935596466064],
+ ["placeholderMatchesNumberRegExp", 9.471800804138184],
+ ["ariaLabelMatchesNumberRegExp", 6.067715644836426],
+ ["idOrNameMatchGift", -22.946273803710938],
+ ["labelsMatchGift", -7.852959632873535],
+ ["placeholderMatchesGift", -2.355496406555176],
+ ["ariaLabelMatchesGift", -2.940307855606079],
+ ["idOrNameMatchSubscription", 0.11255314946174622],
+ ["idOrNameMatchDwfrmAndBml", -0.0006645023822784424],
+ ["hasTemplatedValue", -0.11370040476322174],
+ ["inputTypeNotNumbery", -3.750155210494995]
+ ],
+ "cc-name": [
+ ["idOrNameMatchNameRegExp", 7.496212959289551],
+ ["labelsMatchNameRegExp", 6.081472873687744],
+ ["closestLabelMatchesNameRegExp", 2.600574254989624],
+ ["placeholderMatchesNameRegExp", 5.750874042510986],
+ ["ariaLabelMatchesNameRegExp", 5.162227153778076],
+ ["idOrNameMatchFirst", -6.742659091949463],
+ ["labelsMatchFirst", -0.5234538912773132],
+ ["placeholderMatchesFirst", -3.4615235328674316],
+ ["ariaLabelMatchesFirst", -1.3145145177841187],
+ ["idOrNameMatchLast", -12.561869621276855],
+ ["labelsMatchLast", -0.27417105436325073],
+ ["placeholderMatchesLast", -1.434966802597046],
+ ["ariaLabelMatchesLast", -2.9319725036621094],
+ ["idOrNameMatchFirstAndLast", 24.123435974121094],
+ ["idOrNameMatchSubscription", 0.08349418640136719],
+ ["idOrNameMatchDwfrmAndBml", 0.01882520318031311],
+ ["hasTemplatedValue", 0.182317852973938]
+ ],
+ "cc-type": [
+ ["idOrNameMatchTypeRegExp", 2.0581533908843994],
+ ["labelsMatchTypeRegExp", 1.0784518718719482],
+ ["closestLabelMatchesTypeRegExp", 0.6995877623558044],
+ ["idOrNameMatchVisaCheckout", -3.320356845855713],
+ ["ariaLabelMatchesVisaCheckout", -3.4196767807006836],
+ ["isSelectWithCreditCardOptions", 10.337477684020996],
+ ["isRadioWithCreditCardText", 4.530318737030029],
+ ["idOrNameMatchSubscription", -3.7206356525421143],
+ ["idOrNameMatchDwfrmAndBml", -0.08782318234443665],
+ ["hasTemplatedValue", 0.1772511601448059]
+ ],
+ "cc-exp": [
+ ["labelsMatchExpRegExp", 7.588159561157227],
+ ["closestLabelMatchesExpRegExp", 1.41484534740448],
+ ["placeholderMatchesExpRegExp", 8.759064674377441],
+ ["labelsMatchExpWith2Or4DigitYear", -3.876218795776367],
+ ["placeholderMatchesExpWith2Or4DigitYear", 2.8364884853363037],
+ ["labelsMatchMMYY", 8.836017608642578],
+ ["placeholderMatchesMMYY", -0.5231751799583435],
+ ["maxLengthIs7", 1.3565447330474854],
+ ["idOrNameMatchSubscription", 0.1779913753271103],
+ ["idOrNameMatchDwfrmAndBml", 0.21037884056568146],
+ ["hasTemplatedValue", 0.14900512993335724],
+ ["isExpirationMonthLikely", -3.223409652709961],
+ ["isExpirationYearLikely", -2.536919593811035],
+ ["idOrNameMatchMonth", -3.6893014907836914],
+ ["idOrNameMatchYear", -3.108184337615967],
+ ["idOrNameMatchExpMonthRegExp", -2.264357089996338],
+ ["idOrNameMatchExpYearRegExp", -2.7957723140716553],
+ ["idOrNameMatchValidation", -2.29402756690979]
+ ],
+ "cc-exp-month": [
+ ["idOrNameMatchExpMonthRegExp", 0.2787344455718994],
+ ["labelsMatchExpMonthRegExp", 1.298413634300232],
+ ["closestLabelMatchesExpMonthRegExp", -11.206244468688965],
+ ["placeholderMatchesExpMonthRegExp", 1.2605619430541992],
+ ["ariaLabelMatchesExpMonthRegExp", 1.1330018043518066],
+ ["idOrNameMatchMonth", 6.1464314460754395],
+ ["labelsMatchMonth", 0.7051732540130615],
+ ["placeholderMatchesMonth", 0.7463492751121521],
+ ["ariaLabelMatchesMonth", 1.8244760036468506],
+ ["nextFieldIdOrNameMatchExpYearRegExp", 0.06347066164016724],
+ ["nextFieldLabelsMatchExpYearRegExp", -0.1692247837781906],
+ ["nextFieldPlaceholderMatchExpYearRegExp", 1.0434566736221313],
+ ["nextFieldAriaLabelMatchExpYearRegExp", 1.751156210899353],
+ ["nextFieldIdOrNameMatchYear", -0.532447338104248],
+ ["nextFieldLabelsMatchYear", 1.3248541355133057],
+ ["nextFieldPlaceholderMatchesYear", 0.604235827922821],
+ ["nextFieldAriaLabelMatchesYear", 1.5364223718643188],
+ ["nextFieldMatchesExpYearAutocomplete", 6.285938262939453],
+ ["isExpirationMonthLikely", 13.117807388305664],
+ ["nextFieldIsExpirationYearLikely", 7.182341575622559],
+ ["maxLengthIs2", 4.477289199829102],
+ ["placeholderMatchesMM", 14.403288841247559],
+ ["roleIsMenu", 5.770959854125977],
+ ["idOrNameMatchSubscription", -0.043085768818855286],
+ ["idOrNameMatchDwfrmAndBml", 0.02823038399219513],
+ ["hasTemplatedValue", 0.07234494388103485]
+ ],
+ "cc-exp-year": [
+ ["idOrNameMatchExpYearRegExp", 5.426016807556152],
+ ["labelsMatchExpYearRegExp", 1.3240209817886353],
+ ["closestLabelMatchesExpYearRegExp", -8.702284812927246],
+ ["placeholderMatchesExpYearRegExp", 0.9059725999832153],
+ ["ariaLabelMatchesExpYearRegExp", 0.5550334453582764],
+ ["idOrNameMatchYear", 5.362994194030762],
+ ["labelsMatchYear", 2.7185044288635254],
+ ["placeholderMatchesYear", 0.7883157134056091],
+ ["ariaLabelMatchesYear", 0.311492383480072],
+ ["previousFieldIdOrNameMatchExpMonthRegExp", 1.8155208826065063],
+ ["previousFieldLabelsMatchExpMonthRegExp", -0.46133187413215637],
+ ["previousFieldPlaceholderMatchExpMonthRegExp", 1.0374903678894043],
+ ["previousFieldAriaLabelMatchExpMonthRegExp", -0.5901495814323425],
+ ["previousFieldIdOrNameMatchMonth", -5.960310935974121],
+ ["previousFieldLabelsMatchMonth", 0.6495584845542908],
+ ["previousFieldPlaceholderMatchesMonth", 0.7198042273521423],
+ ["previousFieldAriaLabelMatchesMonth", 3.4590985774993896],
+ ["previousFieldMatchesExpMonthAutocomplete", 2.986003875732422],
+ ["isExpirationYearLikely", 4.021566390991211],
+ ["previousFieldIsExpirationMonthLikely", 9.298635482788086],
+ ["placeholderMatchesYYOrYYYY", 10.457176208496094],
+ ["roleIsMenu", 1.1051956415176392],
+ ["idOrNameMatchSubscription", 0.000688597559928894],
+ ["idOrNameMatchDwfrmAndBml", 0.15687309205532074],
+ ["hasTemplatedValue", -0.19141331315040588]
+ ],
+};
+
+const biases = [
+ ["cc-number", -4.948795795440674],
+ ["cc-name", -5.3578081130981445],
+ ["cc-type", -5.979659557342529],
+ ["cc-exp", -5.849575996398926],
+ ["cc-exp-month", -8.844199180603027],
+ ["cc-exp-year", -6.499860763549805],
+];
+
+/**
+ * END OF CODE PASTED FROM TRAINING REPOSITORY
+ */
+
+/**
+ * MORE CODE UNIQUE TO PRODUCTION--NOT IN THE TRAINING REPOSITORY:
+ */
+// Currently there is a bug when a ruleset has multple types (ex, cc-name, cc-number)
+// and those types also has the same rules (ex. rule `hasTemplatedValue` is used in
+// all the tyoes). When the above case exists, the coefficient of the rule will be
+// overwritten, which means, we can't have different coefficient for the same rule on
+// different types. To workaround this issue, we create a new ruleset for each type.
+export var CreditCardRulesets = {
+ init() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "supportedTypes",
+ "extensions.formautofill.creditCards.heuristics.fathom.types",
+ null,
+ null,
+ val => val.split(",")
+ );
+
+ for (const type of this.types) {
+ this[type] = makeRuleset([...coefficients[type]], biases);
+ }
+ },
+
+ get types() {
+ return this.supportedTypes;
+ },
+};
+
+CreditCardRulesets.init();
+
+export default CreditCardRulesets;
diff --git a/toolkit/components/formautofill/shared/FieldScanner.sys.mjs b/toolkit/components/formautofill/shared/FieldScanner.sys.mjs
new file mode 100644
index 0000000000..22adfdabe8
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FieldScanner.sys.mjs
@@ -0,0 +1,224 @@
+/* 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/. */
+
+/**
+ * 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;
+ }
+}
+
+/**
+ * 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;
diff --git a/toolkit/components/formautofill/shared/FormAutofillHandler.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillHandler.sys.mjs
new file mode 100644
index 0000000000..49f79be77a
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FormAutofillHandler.sys.mjs
@@ -0,0 +1,411 @@
+/* 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 { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofillAddressSection:
+ "resource://gre/modules/shared/FormAutofillSection.sys.mjs",
+ FormAutofillCreditCardSection:
+ "resource://gre/modules/shared/FormAutofillSection.sys.mjs",
+ FormAutofillHeuristics:
+ "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs",
+ FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
+ FormSection: "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs",
+});
+
+const { FIELD_STATES } = FormAutofillUtils;
+
+/**
+ * Handles profile autofill for a DOM Form element.
+ */
+export class FormAutofillHandler {
+ // The window to which this form belongs
+ window = null;
+
+ // A WindowUtils reference of which Window the form belongs
+ winUtils = null;
+
+ // DOM Form element to which this object is attached
+ form = null;
+
+ // An array of section that are found in this form
+ sections = [];
+
+ // The section contains the focused input
+ #focusedSection = null;
+
+ // Caches the element to section mapping
+ #cachedSectionByElement = new WeakMap();
+
+ // Keeps track of filled state for all identified elements
+ #filledStateByElement = new WeakMap();
+ /**
+ * Array of collected data about relevant form fields. Each item is an object
+ * storing the identifying details of the field and a reference to the
+ * originally associated element from the form.
+ *
+ * The "section", "addressType", "contactType", and "fieldName" 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.
+ *
+ * A direct reference to the associated element cannot be sent to the user
+ * interface because processing may be done in the parent process.
+ */
+ fieldDetails = null;
+
+ /**
+ * Initialize the form from `FormLike` object to handle the section or form
+ * operations.
+ *
+ * @param {FormLike} form Form that need to be auto filled
+ * @param {Function} onFormSubmitted Function that can be invoked
+ * to simulate form submission. Function is passed
+ * four arguments: (1) a FormLike for the form being
+ * submitted, (2) the reason for infering the form
+ * submission (3) the corresponding Window, and (4)
+ * the responsible FormAutofillHandler.
+ * @param {Function} onAutofillCallback Function that can be invoked
+ * when we want to suggest autofill on a form.
+ */
+ constructor(form, onFormSubmitted = () => {}, onAutofillCallback = () => {}) {
+ this._updateForm(form);
+
+ this.window = this.form.rootElement.ownerGlobal;
+ this.winUtils = this.window.windowUtils;
+
+ // Enum for form autofill MANUALLY_MANAGED_STATES values
+ this.FIELD_STATE_ENUM = {
+ // not themed
+ [FIELD_STATES.NORMAL]: null,
+ // highlighted
+ [FIELD_STATES.AUTO_FILLED]: "autofill",
+ // highlighted && grey color text
+ [FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
+ };
+
+ /**
+ * This function is used if the form handler (or one of its sections)
+ * determines that it needs to act as if the form had been submitted.
+ */
+ this.onFormSubmitted = formSubmissionReason => {
+ onFormSubmitted(this.form, formSubmissionReason, this.window, this);
+ };
+
+ this.onAutofillCallback = onAutofillCallback;
+
+ ChromeUtils.defineLazyGetter(this, "log", () =>
+ FormAutofill.defineLogGetter(this, "FormAutofillHandler")
+ );
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "input": {
+ if (!event.isTrusted) {
+ return;
+ }
+ const target = event.target;
+ const targetFieldDetail = this.getFieldDetailByElement(target);
+ const isCreditCardField = FormAutofillUtils.isCreditCardField(
+ targetFieldDetail.fieldName
+ );
+
+ // If the user manually blanks a credit card field, then
+ // we want the popup to be activated.
+ if (
+ !HTMLSelectElement.isInstance(target) &&
+ isCreditCardField &&
+ target.value === ""
+ ) {
+ this.onAutofillCallback();
+ }
+
+ if (this.getFilledStateByElement(target) == FIELD_STATES.NORMAL) {
+ return;
+ }
+
+ this.changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
+ const section = this.getSectionByElement(targetFieldDetail.element);
+ section?.clearFilled(targetFieldDetail);
+ }
+ }
+ }
+
+ set focusedInput(element) {
+ const section = this.getSectionByElement(element);
+ if (!section) {
+ return;
+ }
+
+ this.#focusedSection = section;
+ this.#focusedSection.focusedInput = element;
+ }
+
+ getSectionByElement(element) {
+ const section =
+ this.#cachedSectionByElement.get(element) ??
+ this.sections.find(s => s.getFieldDetailByElement(element));
+ if (!section) {
+ return null;
+ }
+
+ this.#cachedSectionByElement.set(element, section);
+ return section;
+ }
+
+ getFieldDetailByElement(element) {
+ for (const section of this.sections) {
+ const detail = section.getFieldDetailByElement(element);
+ if (detail) {
+ return detail;
+ }
+ }
+ return null;
+ }
+
+ get activeSection() {
+ return this.#focusedSection;
+ }
+
+ /**
+ * Check the form is necessary to be updated. This function should be able to
+ * detect any changes including all control elements in the form.
+ *
+ * @param {HTMLElement} element The element supposed to be in the form.
+ * @returns {boolean} FormAutofillHandler.form is updated or not.
+ */
+ updateFormIfNeeded(element) {
+ // When the following condition happens, FormAutofillHandler.form should be
+ // updated:
+ // * The count of form controls is changed.
+ // * When the element can not be found in the current form.
+ //
+ // However, we should improve the function to detect the element changes.
+ // e.g. a tel field is changed from type="hidden" to type="tel".
+
+ let _formLike;
+ const getFormLike = () => {
+ if (!_formLike) {
+ _formLike = lazy.FormLikeFactory.createFromField(element);
+ }
+ return _formLike;
+ };
+
+ const currentForm = element.form ?? getFormLike();
+ if (currentForm.elements.length != this.form.elements.length) {
+ this.log.debug("The count of form elements is changed.");
+ this._updateForm(getFormLike());
+ return true;
+ }
+
+ if (!this.form.elements.includes(element)) {
+ this.log.debug("The element can not be found in the current form.");
+ this._updateForm(getFormLike());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the form with a new FormLike, and the related fields should be
+ * updated or clear to ensure the data consistency.
+ *
+ * @param {FormLike} form a new FormLike to replace the original one.
+ */
+ _updateForm(form) {
+ this.form = form;
+
+ this.fieldDetails = null;
+
+ this.sections = [];
+ this.#cachedSectionByElement = new WeakMap();
+ }
+
+ /**
+ * Set fieldDetails from the form about fields that can be autofilled.
+ *
+ * @returns {Array} The valid address and credit card details.
+ */
+ collectFormFields(ignoreInvalid = true) {
+ const sections = lazy.FormAutofillHeuristics.getFormInfo(this.form);
+ const allValidDetails = [];
+ for (const section of sections) {
+ // We don't support csc field, so remove csc fields from section
+ const fieldDetails = section.fieldDetails.filter(
+ f => !["cc-csc"].includes(f.fieldName)
+ );
+ if (!fieldDetails.length) {
+ continue;
+ }
+
+ let autofillableSection;
+ if (section.type == lazy.FormSection.ADDRESS) {
+ autofillableSection = new lazy.FormAutofillAddressSection(
+ fieldDetails,
+ this
+ );
+ } else {
+ autofillableSection = new lazy.FormAutofillCreditCardSection(
+ fieldDetails,
+ this
+ );
+ }
+
+ // Do not include section that is either disabled or invalid.
+ // We only include invalid section for testing purpose.
+ if (
+ !autofillableSection.isEnabled() ||
+ (ignoreInvalid && !autofillableSection.isValidSection())
+ ) {
+ continue;
+ }
+
+ this.sections.push(autofillableSection);
+ allValidDetails.push(...autofillableSection.fieldDetails);
+ }
+
+ this.fieldDetails = allValidDetails;
+ return allValidDetails;
+ }
+
+ #hasFilledSection() {
+ return this.sections.some(section => section.isFilled());
+ }
+
+ getFilledStateByElement(element) {
+ return this.#filledStateByElement.get(element);
+ }
+
+ /**
+ * Change the state of a field to correspond with different presentations.
+ *
+ * @param {object} fieldDetail
+ * A fieldDetail of which its element is about to update the state.
+ * @param {string} nextState
+ * Used to determine the next state
+ */
+ changeFieldState(fieldDetail, nextState) {
+ const element = fieldDetail.element;
+ if (!element) {
+ this.log.warn(
+ fieldDetail.fieldName,
+ "is unreachable while changing state"
+ );
+ return;
+ }
+ if (!(nextState in this.FIELD_STATE_ENUM)) {
+ this.log.warn(
+ fieldDetail.fieldName,
+ "is trying to change to an invalid state"
+ );
+ return;
+ }
+
+ if (this.#filledStateByElement.get(element) == nextState) {
+ return;
+ }
+
+ let nextStateValue = null;
+ for (const [state, mmStateValue] of Object.entries(this.FIELD_STATE_ENUM)) {
+ // The NORMAL state is simply the absence of other manually
+ // managed states so we never need to add or remove it.
+ if (!mmStateValue) {
+ continue;
+ }
+
+ if (state == nextState) {
+ nextStateValue = mmStateValue;
+ } else {
+ this.winUtils.removeManuallyManagedState(element, mmStateValue);
+ }
+ }
+
+ if (nextStateValue) {
+ this.winUtils.addManuallyManagedState(element, nextStateValue);
+ }
+
+ if (nextState == FIELD_STATES.AUTO_FILLED) {
+ element.addEventListener("input", this, { mozSystemGroup: true });
+ }
+
+ this.#filledStateByElement.set(element, nextState);
+ }
+
+ /**
+ * Processes form fields that can be autofilled, and populates them with the
+ * profile provided by backend.
+ *
+ * @param {object} profile
+ * A profile to be filled in.
+ */
+ async autofillFormFields(profile) {
+ const noFilledSectionsPreviously = !this.#hasFilledSection();
+ await this.activeSection.autofillFields(profile);
+
+ const onChangeHandler = e => {
+ if (!e.isTrusted) {
+ return;
+ }
+ if (e.type == "reset") {
+ this.sections.map(section => section.resetFieldStates());
+ }
+ // Unregister listeners once no field is in AUTO_FILLED state.
+ if (!this.#hasFilledSection()) {
+ this.form.rootElement.removeEventListener("input", onChangeHandler, {
+ mozSystemGroup: true,
+ });
+ this.form.rootElement.removeEventListener("reset", onChangeHandler, {
+ mozSystemGroup: true,
+ });
+ }
+ };
+
+ if (noFilledSectionsPreviously) {
+ // Handle the highlight style resetting caused by user's correction afterward.
+ this.log.debug("register change handler for filled form:", this.form);
+ this.form.rootElement.addEventListener("input", onChangeHandler, {
+ mozSystemGroup: true,
+ });
+ this.form.rootElement.addEventListener("reset", onChangeHandler, {
+ mozSystemGroup: true,
+ });
+ }
+ }
+
+ /**
+ * Collect the filled sections within submitted form and convert all the valid
+ * field data into multiple records.
+ *
+ * @returns {object} records
+ * {Array.<Object>} records.address
+ * {Array.<Object>} records.creditCard
+ */
+ createRecords() {
+ const records = {
+ address: [],
+ creditCard: [],
+ };
+
+ for (const section of this.sections) {
+ const secRecord = section.createRecord();
+ if (!secRecord) {
+ continue;
+ }
+ if (section instanceof lazy.FormAutofillAddressSection) {
+ records.address.push(secRecord);
+ } else if (section instanceof lazy.FormAutofillCreditCardSection) {
+ records.creditCard.push(secRecord);
+ } else {
+ throw new Error("Unknown section type");
+ }
+ }
+
+ return records;
+ }
+}
diff --git a/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs
new file mode 100644
index 0000000000..4ee1fc1fe1
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FormAutofillHeuristics.sys.mjs
@@ -0,0 +1,1213 @@
+/* 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",
+ FieldScanner: "resource://gre/modules/shared/FieldScanner.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ LabelUtils: "resource://gre/modules/shared/LabelUtils.sys.mjs",
+});
+
+/**
+ * To help us classify sections, we want to know what fields can appear
+ * multiple times in a row.
+ * Such fields, like `address-line{X}`, should not break sections.
+ */
+const MULTI_FIELD_NAMES = [
+ "address-level3",
+ "address-level2",
+ "address-level1",
+ "tel",
+ "postal-code",
+ "email",
+ "street-address",
+];
+
+/**
+ * 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,
+};
+
+export class FormSection {
+ static ADDRESS = "address";
+ static CREDIT_CARD = "creditCard";
+
+ #fieldDetails = [];
+
+ #name = "";
+
+ constructor(fieldDetails) {
+ if (!fieldDetails.length) {
+ throw new TypeError("A section should contain at least one field");
+ }
+
+ fieldDetails.forEach(field => this.addField(field));
+
+ const fieldName = fieldDetails[0].fieldName;
+ if (lazy.FormAutofillUtils.isAddressField(fieldName)) {
+ this.type = FormSection.ADDRESS;
+ } else if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
+ this.type = FormSection.CREDIT_CARD;
+ } else {
+ throw new Error("Unknown field type to create a section.");
+ }
+ }
+
+ get fieldDetails() {
+ return this.#fieldDetails;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ addField(fieldDetail) {
+ this.#name ||= fieldDetail.sectionName;
+ this.#fieldDetails.push(fieldDetail);
+ }
+}
+
+/**
+ * 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
+ )
+ );
+ },
+
+ /**
+ * 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, detail) {
+ 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;
+ },
+
+ /**
+ * 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",
+ ];
+
+ const fields = [];
+ for (let idx = scanner.parsingIndex; !scanner.parsingFinished; idx++) {
+ const detail = scanner.getFieldDetailByIndex(idx);
+ if (!INTERESTED_FIELDS.includes(detail?.fieldName)) {
+ break;
+ }
+ fields.push(detail);
+ }
+
+ if (!fields.length) {
+ return false;
+ }
+
+ switch (fields.length) {
+ case 1:
+ if (
+ fields[0].reason != "autocomplete" &&
+ ["address-line2", "address-line3"].includes(fields[0].fieldName)
+ ) {
+ scanner.updateFieldName(scanner.parsingIndex, "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(
+ scanner.parsingIndex,
+ "address-line1",
+ true
+ );
+ }
+ } else {
+ scanner.updateFieldName(scanner.parsingIndex, "address-line1");
+ }
+
+ scanner.updateFieldName(scanner.parsingIndex + 1, "address-line2");
+ break;
+ case 3:
+ default:
+ scanner.updateFieldName(scanner.parsingIndex, "address-line1");
+ scanner.updateFieldName(scanner.parsingIndex + 1, "address-line2");
+ scanner.updateFieldName(scanner.parsingIndex + 2, "address-line3");
+ break;
+ }
+
+ scanner.parsingIndex += fields.length;
+ return true;
+ },
+
+ _parseAddressFields(scanner, fieldDetail) {
+ const INTERESTED_FIELDS = ["address-level1", "address-level2"];
+
+ if (!INTERESTED_FIELDS.includes(fieldDetail.fieldName)) {
+ return false;
+ }
+
+ const fields = [];
+ for (let idx = scanner.parsingIndex; !scanner.parsingFinished; idx++) {
+ const detail = scanner.getFieldDetailByIndex(idx);
+ if (!INTERESTED_FIELDS.includes(detail?.fieldName)) {
+ break;
+ }
+ fields.push(detail);
+ }
+
+ if (!fields.length) {
+ return false;
+ }
+
+ // State & City(address-level2)
+ if (fields.length == 1) {
+ if (fields[0].fieldName == "address-level2") {
+ const prev = scanner.getFieldDetailByIndex(scanner.parsingIndex - 1);
+ if (
+ prev &&
+ !prev.fieldName &&
+ HTMLSelectElement.isInstance(prev.element)
+ ) {
+ scanner.updateFieldName(scanner.parsingIndex - 1, "address-level1");
+ scanner.parsingIndex += 1;
+ return true;
+ }
+ const next = scanner.getFieldDetailByIndex(scanner.parsingIndex + 1);
+ if (
+ next &&
+ !next.fieldName &&
+ HTMLSelectElement.isInstance(next.element)
+ ) {
+ scanner.updateFieldName(scanner.parsingIndex + 1, "address-level1");
+ scanner.parsingIndex += 2;
+ return true;
+ }
+ }
+ }
+
+ scanner.parsingIndex += fields.length;
+ return true;
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * 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);
+ }
+
+ // 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 not present among the preceding credit card fields.
+ if (
+ ["cc-number"].some(f => prevCCFields.has(f)) &&
+ !["cc-name", "cc-given-name", "cc-family-name", "cc-csc"].some(f =>
+ prevCCFields.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;
+ },
+
+ /**
+ * 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 {HTMLFormElement} form
+ * the elements in this form to be predicted the field info.
+ * @returns {Array<FormSection>}
+ * all sections within its field details in the form.
+ */
+ getFormInfo(form) {
+ let elements = this.getFormElements(form);
+
+ const scanner = new lazy.FieldScanner(elements, element =>
+ this.inferFieldInfo(element, elements)
+ );
+
+ while (!scanner.parsingFinished) {
+ const savedIndex = scanner.parsingIndex;
+
+ // First, we get the inferred field info
+ const fieldDetail = scanner.getFieldDetailByIndex(scanner.parsingIndex);
+
+ if (
+ this._parsePhoneFields(scanner, fieldDetail) ||
+ this._parseStreetAddressFields(scanner, fieldDetail) ||
+ this._parseAddressFields(scanner, fieldDetail) ||
+ this._parseCreditCardExpiryFields(scanner, fieldDetail) ||
+ this._parseCreditCardNameFields(scanner, fieldDetail)
+ ) {
+ continue;
+ }
+
+ // If there is no field parsed, the parsing cursor can be moved
+ // forward to the next one.
+ if (savedIndex == scanner.parsingIndex) {
+ scanner.parsingIndex++;
+ }
+ }
+
+ lazy.LabelUtils.clearLabelMap();
+
+ const fields = scanner.fieldDetails;
+ const sections = [
+ ...this._classifySections(
+ fields.filter(f => lazy.FormAutofillUtils.isAddressField(f.fieldName))
+ ),
+ ...this._classifySections(
+ fields.filter(f =>
+ lazy.FormAutofillUtils.isCreditCardField(f.fieldName)
+ )
+ ),
+ ];
+
+ return sections.sort(
+ (a, b) =>
+ fields.indexOf(a.fieldDetails[0]) - fields.indexOf(b.fieldDetails[0])
+ );
+ },
+
+ /**
+ * Get focusable form elements that are of credit card or address type
+ *
+ * @param {HTMLElement} form
+ * @returns {Array<HTMLElement>} focusable elements
+ */
+ getFormElements(form) {
+ let elements = Array.from(form.elements).filter(
+ element =>
+ lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) &&
+ lazy.FormAutofillUtils.isFieldFocusable(element)
+ );
+
+ return elements;
+ },
+
+ /**
+ * The result is an array contains the sections with its belonging field details.
+ *
+ * @param {Array<FieldDetails>} fieldDetails field detail array to be classified
+ * @returns {Array<FormSection>} The array with the sections.
+ */
+ _classifySections(fieldDetails) {
+ let sections = [];
+ for (let i = 0; i < fieldDetails.length; i++) {
+ const fieldName = fieldDetails[i].fieldName;
+ const sectionName = fieldDetails[i].sectionName;
+
+ const [currentSection] = sections.slice(-1);
+
+ // The section this field might belong to
+ let candidateSection = null;
+
+ // If the field doesn't have a section name, MAYBE put it to the previous
+ // section if exists. If the field has a section name, maybe put it to the
+ // nearest section that either has the same name or it doesn't has a name.
+ // Otherwise, create a new section.
+ if (!currentSection || !sectionName) {
+ candidateSection = currentSection;
+ } else if (sectionName) {
+ for (let idx = sections.length - 1; idx >= 0; idx--) {
+ if (!sections[idx].name || sections[idx].name == sectionName) {
+ candidateSection = sections[idx];
+ break;
+ }
+ }
+ }
+
+ // We got an candidate section to put the field to, check whether the section
+ // already has a field with the same field name. If yes, only add the field to when
+ // the type of the field might appear multiple times in a row.
+ if (candidateSection) {
+ let createNewSection = true;
+ if (candidateSection.fieldDetails.find(f => f.fieldName == fieldName)) {
+ const [lastFieldDetail] = candidateSection.fieldDetails.slice(-1);
+ if (lastFieldDetail.fieldName == fieldName) {
+ if (MULTI_FIELD_NAMES.includes(fieldName)) {
+ createNewSection = false;
+ } else if (fieldName in MULTI_N_FIELD_NAMES) {
+ // 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[fieldName];
+ if (lastFieldDetail.part) {
+ // If `part` is set, we have already identified this field can be
+ // merged previously
+ if (lastFieldDetail.part < N) {
+ createNewSection = false;
+ fieldDetails[i].part = lastFieldDetail.part + 1;
+ }
+ // If the next N fields are all the same field, we can merge them
+ } else if (
+ N == 2 ||
+ fieldDetails
+ .slice(i + 1, i + N - 1)
+ .every(f => f.fieldName == fieldName)
+ ) {
+ lastFieldDetail.part = 1;
+ fieldDetails[i].part = 2;
+ createNewSection = false;
+ }
+ }
+ }
+ } else {
+ // The field doesn't exist in the candidate section, add it.
+ createNewSection = false;
+ }
+
+ if (!createNewSection) {
+ candidateSection.addField(fieldDetails[i]);
+ continue;
+ }
+ }
+
+ // Create a new section
+ sections.push(new FormSection([fieldDetails[i]]));
+ }
+
+ return sections;
+ },
+
+ _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 (HTMLSelectElement.isInstance(element)) {
+ const FIELDNAMES_FOR_SELECT_ELEMENT = [
+ "address-level1",
+ "address-level2",
+ "country",
+ "cc-exp-month",
+ "cc-exp-year",
+ "cc-exp",
+ "cc-type",
+ ];
+ fieldNames = fieldNames.filter(name =>
+ FIELDNAMES_FOR_SELECT_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]autocomplete information if the element has autocompelte attribute, null otherwise.
+ * [2]fathom confidence if fathom considers it a cc field, null otherwise.
+ */
+ inferFieldInfo(element, elements = []) {
+ 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)
+ ) {
+ return [autocompleteInfo.fieldName, autocompleteInfo, null];
+ }
+
+ 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", null, null];
+ }
+
+ 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
+ );
+ // At this point, use fathom's recommendation if it has one
+ if (matchedFieldName) {
+ return [matchedFieldName, null, confidence];
+ }
+
+ // 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", null, null];
+ } else if (this._isExpirationYearLikely(element)) {
+ return ["cc-exp-year", null, null];
+ }
+
+ const options = Array.from(element.querySelectorAll("option"));
+ if (
+ options.find(
+ option =>
+ lazy.CreditCard.getNetworkFromName(option.value) ||
+ lazy.CreditCard.getNetworkFromName(option.text)
+ )
+ ) {
+ return ["cc-type", null, null];
+ }
+
+ // 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", null, null];
+ }
+ }
+
+ // Find a matched field name using regexp-based heuristics
+ const matchedFieldName = this._findMatchedFieldName(element, fields);
+ return [matchedFieldName, null, null];
+ },
+
+ /**
+ * 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 the first matching field name 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.
+ *
+ * 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 {string|null} The name of the matched field, or null if no match was found.
+ */
+ _findMatchedFieldName(element, fieldNames) {
+ if (!fieldNames.length) {
+ return null;
+ }
+
+ // Attempt to match the element against the default set of rules
+ let matchedFieldName = fieldNames.find(fieldName =>
+ this._matchRegexp(element, this.RULES[fieldName])
+ );
+
+ // If no match is found, and if a label rule exists for the field,
+ // attempt to match against the label rules
+ if (!matchedFieldName) {
+ matchedFieldName = fieldNames.find(fieldName => {
+ const regexp = this.LABEL_RULES[fieldName];
+ return this._matchRegexp(element, regexp, { attribute: false });
+ });
+ }
+ return matchedFieldName;
+ },
+
+ /**
+ * 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;
diff --git a/toolkit/components/formautofill/shared/FormAutofillNameUtils.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillNameUtils.sys.mjs
new file mode 100644
index 0000000000..8a1d5ba55e
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FormAutofillNameUtils.sys.mjs
@@ -0,0 +1,406 @@
+/* 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/. */
+
+// FormAutofillNameUtils is initially translated from
+// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util.cc?rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
+export var FormAutofillNameUtils = {
+ NAME_PREFIXES: [
+ "1lt",
+ "1st",
+ "2lt",
+ "2nd",
+ "3rd",
+ "admiral",
+ "capt",
+ "captain",
+ "col",
+ "cpt",
+ "dr",
+ "gen",
+ "general",
+ "lcdr",
+ "lt",
+ "ltc",
+ "ltg",
+ "ltjg",
+ "maj",
+ "major",
+ "mg",
+ "mr",
+ "mrs",
+ "ms",
+ "pastor",
+ "prof",
+ "rep",
+ "reverend",
+ "rev",
+ "sen",
+ "st",
+ ],
+
+ NAME_SUFFIXES: [
+ "b.a",
+ "ba",
+ "d.d.s",
+ "dds",
+ "i",
+ "ii",
+ "iii",
+ "iv",
+ "ix",
+ "jr",
+ "m.a",
+ "m.d",
+ "ma",
+ "md",
+ "ms",
+ "ph.d",
+ "phd",
+ "sr",
+ "v",
+ "vi",
+ "vii",
+ "viii",
+ "x",
+ ],
+
+ FAMILY_NAME_PREFIXES: [
+ "d'",
+ "de",
+ "del",
+ "der",
+ "di",
+ "la",
+ "le",
+ "mc",
+ "san",
+ "st",
+ "ter",
+ "van",
+ "von",
+ ],
+
+ // The common and non-ambiguous CJK surnames (last names) that have more than
+ // one character.
+ COMMON_CJK_MULTI_CHAR_SURNAMES: [
+ // Korean, taken from the list of surnames:
+ // https://ko.wikipedia.org/wiki/%ED%95%9C%EA%B5%AD%EC%9D%98_%EC%84%B1%EC%94%A8_%EB%AA%A9%EB%A1%9D
+ "남궁",
+ "사공",
+ "서문",
+ "선우",
+ "제갈",
+ "황보",
+ "독고",
+ "망절",
+
+ // Chinese, taken from the top 10 Chinese 2-character surnames:
+ // https://zh.wikipedia.org/wiki/%E8%A4%87%E5%A7%93#.E5.B8.B8.E8.A6.8B.E7.9A.84.E8.A4.87.E5.A7.93
+ // Simplified Chinese (mostly mainland China)
+ "欧阳",
+ "令狐",
+ "皇甫",
+ "上官",
+ "司徒",
+ "诸葛",
+ "司马",
+ "宇文",
+ "呼延",
+ "端木",
+ // Traditional Chinese (mostly Taiwan)
+ "張簡",
+ "歐陽",
+ "諸葛",
+ "申屠",
+ "尉遲",
+ "司馬",
+ "軒轅",
+ "夏侯",
+ ],
+
+ // All Korean surnames that have more than one character, even the
+ // rare/ambiguous ones.
+ KOREAN_MULTI_CHAR_SURNAMES: [
+ "강전",
+ "남궁",
+ "독고",
+ "동방",
+ "망절",
+ "사공",
+ "서문",
+ "선우",
+ "소봉",
+ "어금",
+ "장곡",
+ "제갈",
+ "황목",
+ "황보",
+ ],
+
+ // The whitespace definition based on
+ // https://cs.chromium.org/chromium/src/base/strings/string_util_constants.cc?l=9&rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
+ WHITESPACE: [
+ "\u0009", // CHARACTER TABULATION
+ "\u000A", // LINE FEED (LF)
+ "\u000B", // LINE TABULATION
+ "\u000C", // FORM FEED (FF)
+ "\u000D", // CARRIAGE RETURN (CR)
+ "\u0020", // SPACE
+ "\u0085", // NEXT LINE (NEL)
+ "\u00A0", // NO-BREAK SPACE
+ "\u1680", // OGHAM SPACE MARK
+ "\u2000", // EN QUAD
+ "\u2001", // EM QUAD
+ "\u2002", // EN SPACE
+ "\u2003", // EM SPACE
+ "\u2004", // THREE-PER-EM SPACE
+ "\u2005", // FOUR-PER-EM SPACE
+ "\u2006", // SIX-PER-EM SPACE
+ "\u2007", // FIGURE SPACE
+ "\u2008", // PUNCTUATION SPACE
+ "\u2009", // THIN SPACE
+ "\u200A", // HAIR SPACE
+ "\u2028", // LINE SEPARATOR
+ "\u2029", // PARAGRAPH SEPARATOR
+ "\u202F", // NARROW NO-BREAK SPACE
+ "\u205F", // MEDIUM MATHEMATICAL SPACE
+ "\u3000", // IDEOGRAPHIC SPACE
+ ],
+
+ // The middle dot is used as a separator for foreign names in Japanese.
+ MIDDLE_DOT: [
+ "\u30FB", // KATAKANA MIDDLE DOT
+ "\u00B7", // A (common?) typo for "KATAKANA MIDDLE DOT"
+ ],
+
+ // The Unicode range is based on Wiki:
+ // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs
+ // https://en.wikipedia.org/wiki/Hangul
+ // https://en.wikipedia.org/wiki/Japanese_writing_system
+ CJK_RANGE: [
+ "\u1100-\u11FF", // Hangul Jamo
+ "\u3040-\u309F", // Hiragana
+ "\u30A0-\u30FF", // Katakana
+ "\u3105-\u312C", // Bopomofo
+ "\u3130-\u318F", // Hangul Compatibility Jamo
+ "\u31F0-\u31FF", // Katakana Phonetic Extensions
+ "\u3200-\u32FF", // Enclosed CJK Letters and Months
+ "\u3400-\u4DBF", // CJK unified ideographs Extension A
+ "\u4E00-\u9FFF", // CJK Unified Ideographs
+ "\uA960-\uA97F", // Hangul Jamo Extended-A
+ "\uAC00-\uD7AF", // Hangul Syllables
+ "\uD7B0-\uD7FF", // Hangul Jamo Extended-B
+ "\uFF00-\uFFEF", // Halfwidth and Fullwidth Forms
+ ],
+
+ HANGUL_RANGE: [
+ "\u1100-\u11FF", // Hangul Jamo
+ "\u3130-\u318F", // Hangul Compatibility Jamo
+ "\uA960-\uA97F", // Hangul Jamo Extended-A
+ "\uAC00-\uD7AF", // Hangul Syllables
+ "\uD7B0-\uD7FF", // Hangul Jamo Extended-B
+ ],
+
+ _dataLoaded: false,
+
+ // Returns true if |set| contains |token|, modulo a final period.
+ _containsString(set, token) {
+ let target = token.replace(/\.$/, "").toLowerCase();
+ return set.includes(target);
+ },
+
+ // Removes common name prefixes from |name_tokens|.
+ _stripPrefixes(nameTokens) {
+ for (let i in nameTokens) {
+ if (!this._containsString(this.NAME_PREFIXES, nameTokens[i])) {
+ return nameTokens.slice(i);
+ }
+ }
+ return [];
+ },
+
+ // Removes common name suffixes from |name_tokens|.
+ _stripSuffixes(nameTokens) {
+ for (let i = nameTokens.length - 1; i >= 0; i--) {
+ if (!this._containsString(this.NAME_SUFFIXES, nameTokens[i])) {
+ return nameTokens.slice(0, i + 1);
+ }
+ }
+ return [];
+ },
+
+ _isCJKName(name) {
+ // The name is considered to be a CJK name if it is only CJK characters,
+ // spaces, and "middle dot" separators, with at least one CJK character, and
+ // no more than 2 words.
+ //
+ // Chinese and Japanese names are usually spelled out using the Han
+ // characters (logographs), which constitute the "CJK Unified Ideographs"
+ // block in Unicode, also referred to as Unihan. Korean names are usually
+ // spelled out in the Korean alphabet (Hangul), although they do have a Han
+ // equivalent as well.
+
+ if (!name) {
+ return false;
+ }
+
+ let previousWasCJK = false;
+ let wordCount = 0;
+
+ for (let c of name) {
+ let isMiddleDot = this.MIDDLE_DOT.includes(c);
+ let isCJK = !isMiddleDot && this.reCJK.test(c);
+ if (!isCJK && !isMiddleDot && !this.WHITESPACE.includes(c)) {
+ return false;
+ }
+ if (isCJK && !previousWasCJK) {
+ wordCount++;
+ }
+ previousWasCJK = isCJK;
+ }
+
+ return wordCount > 0 && wordCount < 3;
+ },
+
+ // Tries to split a Chinese, Japanese, or Korean name into its given name &
+ // surname parts. If splitting did not work for whatever reason, returns null.
+ _splitCJKName(nameTokens) {
+ // The convention for CJK languages is to put the surname (last name) first,
+ // and the given name (first name) second. In a continuous text, there is
+ // normally no space between the two parts of the name. When entering their
+ // name into a field, though, some people add a space to disambiguate. CJK
+ // names (almost) never have a middle name.
+
+ let reHangulName = new RegExp(
+ "^[" + this.HANGUL_RANGE.join("") + this.WHITESPACE.join("") + "]+$",
+ "u"
+ );
+ let nameParts = {
+ given: "",
+ middle: "",
+ family: "",
+ };
+
+ if (nameTokens.length == 1) {
+ // There is no space between the surname and given name. Try to infer
+ // where to separate between the two. Most Chinese and Korean surnames
+ // have only one character, but there are a few that have 2. If the name
+ // does not start with a surname from a known list, default to one
+ // character.
+ let name = nameTokens[0];
+ let isKorean = reHangulName.test(name);
+ let surnameLength = 0;
+
+ // 4-character Korean names are more likely to be 2/2 than 1/3, so use
+ // the full list of Korean 2-char surnames. (instead of only the common
+ // ones)
+ let multiCharSurnames =
+ isKorean && name.length > 3
+ ? this.KOREAN_MULTI_CHAR_SURNAMES
+ : this.COMMON_CJK_MULTI_CHAR_SURNAMES;
+
+ // Default to 1 character if the surname is not in the list.
+ surnameLength = multiCharSurnames.some(surname =>
+ name.startsWith(surname)
+ )
+ ? 2
+ : 1;
+
+ nameParts.family = name.substr(0, surnameLength);
+ nameParts.given = name.substr(surnameLength);
+ } else if (nameTokens.length == 2) {
+ // The user entered a space between the two name parts. This makes our job
+ // easier. Family name first, given name second.
+ nameParts.family = nameTokens[0];
+ nameParts.given = nameTokens[1];
+ } else {
+ return null;
+ }
+
+ return nameParts;
+ },
+
+ init() {
+ if (this._dataLoaded) {
+ return;
+ }
+ this._dataLoaded = true;
+
+ this.reCJK = new RegExp("[" + this.CJK_RANGE.join("") + "]", "u");
+ },
+
+ splitName(name) {
+ let nameParts = {
+ given: "",
+ middle: "",
+ family: "",
+ };
+
+ if (!name) {
+ return nameParts;
+ }
+
+ let nameTokens = name.trim().split(/[ ,\u3000\u30FB\u00B7]+/);
+ nameTokens = this._stripPrefixes(nameTokens);
+
+ if (this._isCJKName(name)) {
+ let parts = this._splitCJKName(nameTokens);
+ if (parts) {
+ return parts;
+ }
+ }
+
+ // Don't assume "Ma" is a suffix in John Ma.
+ if (nameTokens.length > 2) {
+ nameTokens = this._stripSuffixes(nameTokens);
+ }
+
+ if (!nameTokens.length) {
+ // Bad things have happened; just assume the whole thing is a given name.
+ nameParts.given = name;
+ return nameParts;
+ }
+
+ // Only one token, assume given name.
+ if (nameTokens.length == 1) {
+ nameParts.given = nameTokens[0];
+ return nameParts;
+ }
+
+ // 2 or more tokens. Grab the family, which is the last word plus any
+ // recognizable family prefixes.
+ let familyTokens = [nameTokens.pop()];
+ while (nameTokens.length) {
+ let lastToken = nameTokens[nameTokens.length - 1];
+ if (!this._containsString(this.FAMILY_NAME_PREFIXES, lastToken)) {
+ break;
+ }
+ familyTokens.unshift(lastToken);
+ nameTokens.pop();
+ }
+ nameParts.family = familyTokens.join(" ");
+
+ // Take the last remaining token as the middle name (if there are at least 2
+ // tokens).
+ if (nameTokens.length >= 2) {
+ nameParts.middle = nameTokens.pop();
+ }
+
+ // Remainder is given name.
+ nameParts.given = nameTokens.join(" ");
+
+ return nameParts;
+ },
+
+ joinNameParts({ given, middle, family }) {
+ if (this._isCJKName(given) && this._isCJKName(family) && !middle) {
+ return family + given;
+ }
+ return [given, middle, family]
+ .filter(part => part && part.length)
+ .join(" ");
+ },
+};
+
+FormAutofillNameUtils.init();
diff --git a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs
new file mode 100644
index 0000000000..1c7696432a
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs
@@ -0,0 +1,1292 @@
+/* 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 { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
+import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AutofillTelemetry: "resource://autofill/AutofillTelemetry.sys.mjs",
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+ FormAutofillNameUtils:
+ "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs",
+ LabelUtils: "resource://gre/modules/shared/LabelUtils.sys.mjs",
+});
+
+const { FIELD_STATES } = FormAutofillUtils;
+
+export class FormAutofillSection {
+ static SHOULD_FOCUS_ON_AUTOFILL = true;
+ #focusedInput = null;
+
+ #fieldDetails = [];
+
+ constructor(fieldDetails, handler) {
+ this.#fieldDetails = fieldDetails;
+
+ if (!this.isValidSection()) {
+ return;
+ }
+
+ this.handler = handler;
+ this.filledRecordGUID = null;
+
+ ChromeUtils.defineLazyGetter(this, "log", () =>
+ FormAutofill.defineLogGetter(this, "FormAutofillHandler")
+ );
+
+ this._cacheValue = {
+ allFieldNames: null,
+ matchingSelectOption: null,
+ };
+
+ // Identifier used to correlate events relating to the same form
+ this.flowId = Services.uuid.generateUUID().toString();
+ this.log.debug(
+ "Creating new credit card section with flowId =",
+ this.flowId
+ );
+ }
+
+ get fieldDetails() {
+ return this.#fieldDetails;
+ }
+
+ /*
+ * Examine the section is a valid section or not based on its fieldDetails or
+ * other information. This method must be overrided.
+ *
+ * @returns {boolean} True for a valid section, otherwise false
+ *
+ */
+ isValidSection() {
+ throw new TypeError("isValidSection method must be overrided");
+ }
+
+ /*
+ * Examine the section is an enabled section type or not based on its
+ * preferences. This method must be overrided.
+ *
+ * @returns {boolean} True for an enabled section type, otherwise false
+ *
+ */
+ isEnabled() {
+ throw new TypeError("isEnabled method must be overrided");
+ }
+
+ /*
+ * Examine the section is createable for storing the profile. This method
+ * must be overrided.
+ *
+ * @param {Object} record The record for examining createable
+ * @returns {boolean} True for the record is createable, otherwise false
+ *
+ */
+ isRecordCreatable(record) {
+ throw new TypeError("isRecordCreatable method must be overridden");
+ }
+
+ /**
+ * Override this method if the profile is needed to apply some transformers.
+ *
+ * @param {object} profile
+ * A profile should be converted based on the specific requirement.
+ */
+ applyTransformers(profile) {}
+
+ /**
+ * Override this method if the profile is needed to be customized for
+ * previewing values.
+ *
+ * @param {object} profile
+ * A profile for pre-processing before previewing values.
+ */
+ preparePreviewProfile(profile) {}
+
+ /**
+ * Override this method if the profile is needed to be customized for filling
+ * values.
+ *
+ * @param {object} profile
+ * A profile for pre-processing before filling values.
+ * @returns {boolean} Whether the profile should be filled.
+ */
+ async prepareFillingProfile(profile) {
+ return true;
+ }
+
+ /**
+ * Override this method if the profile is needed to be customized for filling
+ * values.
+ *
+ * @param {object} fieldDetail A fieldDetail of the related element.
+ * @param {object} profile The profile to fill.
+ * @returns {string} The value to fill for the given field.
+ */
+ getFilledValueFromProfile(fieldDetail, profile) {
+ return (
+ profile[`${fieldDetail.fieldName}-formatted`] ||
+ profile[fieldDetail.fieldName]
+ );
+ }
+
+ /*
+ * Override this method if there is any field value needs to compute for a
+ * specific case. Return the original value in the default case.
+ * @param {String} value
+ * The original field value.
+ * @param {Object} fieldDetail
+ * A fieldDetail of the related element.
+ * @param {HTMLElement} element
+ * A element for checking converting value.
+ *
+ * @returns {String}
+ * A string of the converted value.
+ */
+ computeFillingValue(value, fieldName, element) {
+ return value;
+ }
+
+ set focusedInput(element) {
+ this.#focusedInput = element;
+ }
+
+ getFieldDetailByElement(element) {
+ return this.fieldDetails.find(detail => detail.element == element);
+ }
+
+ getFieldDetailByName(fieldName) {
+ return this.fieldDetails.find(detail => detail.fieldName == fieldName);
+ }
+
+ get allFieldNames() {
+ if (!this._cacheValue.allFieldNames) {
+ this._cacheValue.allFieldNames = this.fieldDetails.map(
+ record => record.fieldName
+ );
+ }
+ return this._cacheValue.allFieldNames;
+ }
+
+ matchSelectOptions(profile) {
+ if (!this._cacheValue.matchingSelectOption) {
+ this._cacheValue.matchingSelectOption = new WeakMap();
+ }
+
+ for (const fieldName in profile) {
+ const fieldDetail = this.getFieldDetailByName(fieldName);
+ const element = fieldDetail?.element;
+
+ if (!HTMLSelectElement.isInstance(element)) {
+ continue;
+ }
+
+ const cache = this._cacheValue.matchingSelectOption.get(element) || {};
+ const value = profile[fieldName];
+ if (cache[value] && cache[value].deref()) {
+ continue;
+ }
+
+ const option = FormAutofillUtils.findSelectOption(
+ element,
+ profile,
+ fieldName
+ );
+
+ if (option) {
+ cache[value] = new WeakRef(option);
+ this._cacheValue.matchingSelectOption.set(element, cache);
+ } else {
+ if (cache[value]) {
+ delete cache[value];
+ this._cacheValue.matchingSelectOption.set(element, cache);
+ }
+ // Skip removing cc-type since this is needed for displaying the icon for credit card network
+ // TODO(Bug 1874339): Cleanup transformation and normalization of data to not remove any
+ // fields and be more consistent
+ if (!["cc-type"].includes(fieldName)) {
+ // Delete the field so the phishing hint won't treat it as a "also fill"
+ // field.
+ delete profile[fieldName];
+ }
+ }
+ }
+ }
+
+ adaptFieldMaxLength(profile) {
+ for (let key in profile) {
+ let detail = this.getFieldDetailByName(key);
+ if (!detail) {
+ continue;
+ }
+
+ let element = detail.element;
+ if (!element) {
+ continue;
+ }
+
+ let maxLength = element.maxLength;
+ if (
+ maxLength === undefined ||
+ maxLength < 0 ||
+ profile[key].toString().length <= maxLength
+ ) {
+ continue;
+ }
+
+ if (maxLength) {
+ switch (typeof profile[key]) {
+ case "string":
+ // If this is an expiration field and our previous
+ // adaptations haven't resulted in a string that is
+ // short enough to satisfy the field length, and the
+ // field is constrained to a length of 4 or 5, then we
+ // assume it is intended to hold an expiration of the
+ // form "MMYY" or "MM/YY".
+ if (key == "cc-exp" && (maxLength == 4 || maxLength == 5)) {
+ const month2Digits = (
+ "0" + profile["cc-exp-month"].toString()
+ ).slice(-2);
+ const year2Digits = profile["cc-exp-year"].toString().slice(-2);
+ const separator = maxLength == 5 ? "/" : "";
+ profile[key] = `${month2Digits}${separator}${year2Digits}`;
+ } else if (key == "cc-number") {
+ // We want to show the last four digits of credit card so that
+ // the masked credit card previews correctly and appears correctly
+ // in the autocomplete menu
+ profile[key] = profile[key].substr(
+ profile[key].length - maxLength
+ );
+ } else {
+ profile[key] = profile[key].substr(0, maxLength);
+ }
+ break;
+ case "number":
+ // There's no way to truncate a number smaller than a
+ // single digit.
+ if (maxLength < 1) {
+ maxLength = 1;
+ }
+ // The only numbers we store are expiration month/year,
+ // and if they truncate, we want the final digits, not
+ // the initial ones.
+ profile[key] = profile[key] % Math.pow(10, maxLength);
+ break;
+ default:
+ }
+ } else {
+ delete profile[key];
+ delete profile[`${key}-formatted`];
+ }
+ }
+ }
+
+ fillFieldValue(element, value) {
+ if (FormAutofillUtils.focusOnAutofill) {
+ element.focus({ preventScroll: true });
+ }
+ if (HTMLInputElement.isInstance(element)) {
+ element.setUserInput(value);
+ } else if (HTMLSelectElement.isInstance(element)) {
+ // Set the value of the select element so that web event handlers can react accordingly
+ element.value = value;
+ element.dispatchEvent(
+ new element.ownerGlobal.Event("input", { bubbles: true })
+ );
+ element.dispatchEvent(
+ new element.ownerGlobal.Event("change", { bubbles: true })
+ );
+ }
+ }
+
+ getAdaptedProfiles(originalProfiles) {
+ for (let profile of originalProfiles) {
+ this.applyTransformers(profile);
+ }
+ return originalProfiles;
+ }
+
+ /**
+ * Processes form fields that can be autofilled, and populates them with the
+ * profile provided by backend.
+ *
+ * @param {object} profile
+ * A profile to be filled in.
+ * @returns {boolean}
+ * True if successful, false if failed
+ */
+ async autofillFields(profile) {
+ if (!this.#focusedInput) {
+ throw new Error("No focused input.");
+ }
+
+ const focusedDetail = this.getFieldDetailByElement(this.#focusedInput);
+ if (!focusedDetail) {
+ throw new Error("No fieldDetail for the focused input.");
+ }
+
+ this.getAdaptedProfiles([profile]);
+ if (!(await this.prepareFillingProfile(profile))) {
+ this.log.debug("profile cannot be filled");
+ return false;
+ }
+
+ this.filledRecordGUID = profile.guid;
+ for (const fieldDetail of this.fieldDetails) {
+ // Avoid filling field value in the following cases:
+ // 1. a non-empty input field for an unfocused input
+ // 2. the invalid value set
+ // 3. value already chosen in select element
+
+ const element = fieldDetail.element;
+ // Skip the field if it is null or readonly or disabled
+ if (!FormAutofillUtils.isFieldAutofillable(element)) {
+ continue;
+ }
+
+ element.previewValue = "";
+ // Bug 1687679: Since profile appears to be presentation ready data, we need to utilize the "x-formatted" field
+ // that is generated when presentation ready data doesn't fit into the autofilling element.
+ // For example, autofilling expiration month into an input element will not work as expected if
+ // the month is less than 10, since the input is expected a zero-padded string.
+ // See Bug 1722941 for follow up.
+ const value = this.getFilledValueFromProfile(fieldDetail, profile);
+ if (HTMLInputElement.isInstance(element) && value) {
+ // For the focused input element, it will be filled with a valid value
+ // anyway.
+ // For the others, the fields should be only filled when their values are empty
+ // or their values are equal to the site prefill value
+ // or are the result of an earlier auto-fill.
+ if (
+ element == this.#focusedInput ||
+ (element != this.#focusedInput &&
+ (!element.value || element.value == element.defaultValue)) ||
+ this.handler.getFilledStateByElement(element) ==
+ FIELD_STATES.AUTO_FILLED
+ ) {
+ this.fillFieldValue(element, value);
+ this.handler.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
+ }
+ } else if (HTMLSelectElement.isInstance(element)) {
+ let cache = this._cacheValue.matchingSelectOption.get(element) || {};
+ let option = cache[value] && cache[value].deref();
+ if (!option) {
+ continue;
+ }
+ // Do not change value or dispatch events if the option is already selected.
+ // Use case for multiple select is not considered here.
+ if (!option.selected) {
+ option.selected = true;
+ this.fillFieldValue(element, option.value);
+ }
+ // Autofill highlight appears regardless if value is changed or not
+ this.handler.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
+ }
+ }
+ this.#focusedInput.focus({ preventScroll: true });
+
+ lazy.AutofillTelemetry.recordFormInteractionEvent("filled", this, {
+ profile,
+ });
+
+ return true;
+ }
+
+ /**
+ * Populates result to the preview layers with given profile.
+ *
+ * @param {object} profile
+ * A profile to be previewed with
+ */
+ previewFormFields(profile) {
+ this.preparePreviewProfile(profile);
+
+ for (const fieldDetail of this.fieldDetails) {
+ let element = fieldDetail.element;
+ // Skip the field if it is null or readonly or disabled
+ if (!FormAutofillUtils.isFieldAutofillable(element)) {
+ continue;
+ }
+
+ let value =
+ profile[`${fieldDetail.fieldName}-formatted`] ||
+ profile[fieldDetail.fieldName] ||
+ "";
+ if (HTMLSelectElement.isInstance(element)) {
+ // Unlike text input, select element is always previewed even if
+ // the option is already selected.
+ if (value) {
+ const cache =
+ this._cacheValue.matchingSelectOption.get(element) ?? {};
+ const option = cache[value]?.deref();
+ value = option?.text ?? "";
+ }
+ } else if (element.value && element.value != element.defaultValue) {
+ // Skip the field if the user has already entered text and that text is not the site prefilled value.
+ continue;
+ }
+ element.previewValue = value?.toString().replaceAll("*", "•");
+ this.handler.changeFieldState(
+ fieldDetail,
+ value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL
+ );
+ }
+ }
+
+ /**
+ * Clear a previously autofilled field in this section
+ */
+ clearFilled(fieldDetail) {
+ lazy.AutofillTelemetry.recordFormInteractionEvent("filled_modified", this, {
+ fieldName: fieldDetail.fieldName,
+ });
+
+ let isAutofilled = false;
+ const dimFieldDetails = [];
+ for (const fieldDetail of this.fieldDetails) {
+ const element = fieldDetail.element;
+
+ if (HTMLSelectElement.isInstance(element)) {
+ // Dim fields are those we don't attempt to revert their value
+ // when clear the target set, such as <select>.
+ dimFieldDetails.push(fieldDetail);
+ } else {
+ isAutofilled |=
+ this.handler.getFilledStateByElement(element) ==
+ FIELD_STATES.AUTO_FILLED;
+ }
+ }
+ if (!isAutofilled) {
+ // Restore the dim fields to initial state as well once we knew
+ // that user had intention to clear the filled form manually.
+ for (const fieldDetail of dimFieldDetails) {
+ // If we can't find a selected option, then we should just reset to the first option's value
+ let element = fieldDetail.element;
+ this._resetSelectElementValue(element);
+ this.handler.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
+ }
+ this.filledRecordGUID = null;
+ }
+ }
+
+ /**
+ * Clear preview text and background highlight of all fields.
+ */
+ clearPreviewedFormFields() {
+ this.log.debug("clear previewed fields");
+
+ for (const fieldDetail of this.fieldDetails) {
+ let element = fieldDetail.element;
+ if (!element) {
+ this.log.warn(fieldDetail.fieldName, "is unreachable");
+ continue;
+ }
+
+ element.previewValue = "";
+
+ // We keep the state if this field has
+ // already been auto-filled.
+ if (
+ this.handler.getFilledStateByElement(element) ==
+ FIELD_STATES.AUTO_FILLED
+ ) {
+ continue;
+ }
+
+ this.handler.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
+ }
+ }
+
+ /**
+ * Clear value and highlight style of all filled fields.
+ */
+ clearPopulatedForm() {
+ for (let fieldDetail of this.fieldDetails) {
+ let element = fieldDetail.element;
+ if (!element) {
+ this.log.warn(fieldDetail.fieldName, "is unreachable");
+ continue;
+ }
+
+ if (
+ this.handler.getFilledStateByElement(element) ==
+ FIELD_STATES.AUTO_FILLED
+ ) {
+ if (HTMLInputElement.isInstance(element)) {
+ element.setUserInput("");
+ } else if (HTMLSelectElement.isInstance(element)) {
+ // If we can't find a selected option, then we should just reset to the first option's value
+ this._resetSelectElementValue(element);
+ }
+ }
+ }
+ }
+
+ resetFieldStates() {
+ for (const fieldDetail of this.fieldDetails) {
+ const element = fieldDetail.element;
+ element.removeEventListener("input", this, { mozSystemGroup: true });
+ this.handler.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
+ }
+ this.filledRecordGUID = null;
+ }
+
+ isFilled() {
+ return !!this.filledRecordGUID;
+ }
+
+ /**
+ * Condenses multiple credit card number fields into one fieldDetail
+ * in order to submit the credit card record correctly.
+ *
+ * @param {Array.<object>} condensedDetails
+ * An array of fieldDetails
+ * @memberof FormAutofillSection
+ */
+ _condenseMultipleCCNumberFields(condensedDetails) {
+ let countOfCCNumbers = 0;
+ // We ignore the cases where there are more than or less than four credit card number
+ // fields in a form as this is not a valid case for filling the credit card number.
+ for (let i = condensedDetails.length - 1; i >= 0; i--) {
+ if (condensedDetails[i].fieldName == "cc-number") {
+ countOfCCNumbers++;
+ if (countOfCCNumbers == 4) {
+ countOfCCNumbers = 0;
+ condensedDetails[i].fieldValue =
+ condensedDetails[i].element?.value +
+ condensedDetails[i + 1].element?.value +
+ condensedDetails[i + 2].element?.value +
+ condensedDetails[i + 3].element?.value;
+ condensedDetails.splice(i + 1, 3);
+ }
+ } else {
+ countOfCCNumbers = 0;
+ }
+ }
+ }
+ /**
+ * Return the record that is converted from `fieldDetails` and only valid
+ * form record is included.
+ *
+ * @returns {object | null}
+ * A record object consists of three properties:
+ * - guid: The id of the previously-filled profile or null if omitted.
+ * - record: A valid record converted from details with trimmed result.
+ * - untouchedFields: Fields that aren't touched after autofilling.
+ * Return `null` for any uncreatable or invalid record.
+ */
+ createRecord() {
+ let details = this.fieldDetails;
+ if (!this.isEnabled() || !details || !details.length) {
+ return null;
+ }
+
+ let data = {
+ guid: this.filledRecordGUID,
+ record: {},
+ untouchedFields: [],
+ section: this,
+ };
+ if (this.flowId) {
+ data.flowId = this.flowId;
+ }
+ let condensedDetails = this.fieldDetails;
+
+ // TODO: This is credit card specific code...
+ this._condenseMultipleCCNumberFields(condensedDetails);
+
+ condensedDetails.forEach(detail => {
+ const element = detail.element;
+ // Remove the unnecessary spaces
+ let value = detail.fieldValue ?? (element && element.value.trim());
+ value = this.computeFillingValue(value, detail, element);
+
+ if (!value || value.length > FormAutofillUtils.MAX_FIELD_VALUE_LENGTH) {
+ // Keep the property and preserve more information for updating
+ data.record[detail.fieldName] = "";
+ return;
+ }
+
+ data.record[detail.fieldName] = value;
+
+ if (
+ this.handler.getFilledStateByElement(element) ==
+ FIELD_STATES.AUTO_FILLED
+ ) {
+ data.untouchedFields.push(detail.fieldName);
+ }
+ });
+
+ const telFields = this.fieldDetails.filter(
+ f => FormAutofillUtils.getCategoryFromFieldName(f.fieldName) == "tel"
+ );
+ if (
+ telFields.length &&
+ telFields.every(f => data.untouchedFields.includes(f.fieldName))
+ ) {
+ // No need to verify it if none of related fields are modified after autofilling.
+ if (!data.untouchedFields.includes("tel")) {
+ data.untouchedFields.push("tel");
+ }
+ }
+
+ if (!this.isRecordCreatable(data.record)) {
+ return null;
+ }
+
+ return data;
+ }
+
+ /**
+ * Resets a <select> element to its selected option or the first option if there is none selected.
+ *
+ * @param {HTMLElement} element
+ * @memberof FormAutofillSection
+ */
+ _resetSelectElementValue(element) {
+ if (!element.options.length) {
+ return;
+ }
+ let selected = [...element.options].find(option =>
+ option.hasAttribute("selected")
+ );
+ element.value = selected ? selected.value : element.options[0].value;
+ element.dispatchEvent(
+ new element.ownerGlobal.Event("input", { bubbles: true })
+ );
+ element.dispatchEvent(
+ new element.ownerGlobal.Event("change", { bubbles: true })
+ );
+ }
+}
+
+export class FormAutofillAddressSection extends FormAutofillSection {
+ constructor(fieldDetails, handler) {
+ super(fieldDetails, handler);
+
+ if (!this.isValidSection()) {
+ return;
+ }
+
+ this._cacheValue.oneLineStreetAddress = null;
+
+ lazy.AutofillTelemetry.recordDetectedSectionCount(this);
+ lazy.AutofillTelemetry.recordFormInteractionEvent("detected", this);
+ }
+
+ isValidSection() {
+ const fields = new Set(this.fieldDetails.map(f => f.fieldName));
+ return fields.size >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
+ }
+
+ isEnabled() {
+ return FormAutofill.isAutofillAddressesEnabled;
+ }
+
+ isRecordCreatable(record) {
+ const country = FormAutofillUtils.identifyCountryCode(
+ record.country || record["country-name"]
+ );
+ if (
+ country &&
+ !FormAutofill.isAutofillAddressesAvailableInCountry(country)
+ ) {
+ // We don't want to save data in the wrong fields due to not having proper
+ // heuristic regexes in countries we don't yet support.
+ this.log.warn(
+ "isRecordCreatable: Country not supported:",
+ record.country
+ );
+ return false;
+ }
+
+ // Multiple name or tel fields are treat as 1 field while countng whether
+ // the number of fields exceed the valid address secton threshold
+ const categories = Object.entries(record)
+ .filter(e => !!e[1])
+ .map(e => FormAutofillUtils.getCategoryFromFieldName(e[0]));
+
+ return (
+ categories.reduce(
+ (acc, category) =>
+ ["name", "tel"].includes(category) && acc.includes(category)
+ ? acc
+ : [...acc, category],
+ []
+ ).length >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
+ );
+ }
+
+ _getOneLineStreetAddress(address) {
+ if (!this._cacheValue.oneLineStreetAddress) {
+ this._cacheValue.oneLineStreetAddress = {};
+ }
+ if (!this._cacheValue.oneLineStreetAddress[address]) {
+ this._cacheValue.oneLineStreetAddress[address] =
+ FormAutofillUtils.toOneLineAddress(address);
+ }
+ return this._cacheValue.oneLineStreetAddress[address];
+ }
+
+ addressTransformer(profile) {
+ if (profile["street-address"]) {
+ // "-moz-street-address-one-line" is used by the labels in
+ // ProfileAutoCompleteResult.
+ profile["-moz-street-address-one-line"] = this._getOneLineStreetAddress(
+ profile["street-address"]
+ );
+ let streetAddressDetail = this.getFieldDetailByName("street-address");
+ if (
+ streetAddressDetail &&
+ HTMLInputElement.isInstance(streetAddressDetail.element)
+ ) {
+ profile["street-address"] = profile["-moz-street-address-one-line"];
+ }
+
+ let waitForConcat = [];
+ for (let f of ["address-line3", "address-line2", "address-line1"]) {
+ waitForConcat.unshift(profile[f]);
+ if (this.getFieldDetailByName(f)) {
+ if (waitForConcat.length > 1) {
+ profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
+ }
+ waitForConcat = [];
+ }
+ }
+ }
+ }
+
+ /**
+ * Replace tel with tel-national if tel violates the input element's
+ * restriction.
+ *
+ * @param {object} profile
+ * A profile to be converted.
+ */
+ telTransformer(profile) {
+ if (!profile.tel || !profile["tel-national"]) {
+ return;
+ }
+
+ let detail = this.getFieldDetailByName("tel");
+ if (!detail) {
+ return;
+ }
+
+ let element = detail.element;
+ let _pattern;
+ let testPattern = str => {
+ if (!_pattern) {
+ // The pattern has to match the entire value.
+ _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
+ }
+ return _pattern.test(str);
+ };
+ if (element.pattern) {
+ if (testPattern(profile.tel)) {
+ return;
+ }
+ } else if (element.maxLength) {
+ if (
+ detail.reason == "autocomplete" &&
+ profile.tel.length <= element.maxLength
+ ) {
+ return;
+ }
+ }
+
+ if (detail.reason != "autocomplete") {
+ // Since we only target people living in US and using en-US websites in
+ // MVP, it makes more sense to fill `tel-national` instead of `tel`
+ // if the field is identified by heuristics and no other clues to
+ // determine which one is better.
+ // TODO: [Bug 1407545] This should be improved once more countries are
+ // supported.
+ profile.tel = profile["tel-national"];
+ } else if (element.pattern) {
+ if (testPattern(profile["tel-national"])) {
+ profile.tel = profile["tel-national"];
+ }
+ } else if (element.maxLength) {
+ if (profile["tel-national"].length <= element.maxLength) {
+ profile.tel = profile["tel-national"];
+ }
+ }
+ }
+
+ /*
+ * Apply all address related transformers.
+ *
+ * @param {Object} profile
+ * A profile for adjusting address related value.
+ * @override
+ */
+ applyTransformers(profile) {
+ this.addressTransformer(profile);
+ this.telTransformer(profile);
+ this.matchSelectOptions(profile);
+ this.adaptFieldMaxLength(profile);
+ }
+
+ computeFillingValue(value, fieldDetail, element) {
+ // Try to abbreviate the value of select element.
+ if (
+ fieldDetail.fieldName == "address-level1" &&
+ HTMLSelectElement.isInstance(element)
+ ) {
+ // Don't save the record when the option value is empty *OR* there
+ // are multiple options being selected. The empty option is usually
+ // assumed to be default along with a meaningless text to users.
+ if (!value || element.selectedOptions.length != 1) {
+ // Keep the property and preserve more information for address updating
+ value = "";
+ } else {
+ let text = element.selectedOptions[0].text.trim();
+ value =
+ FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text;
+ }
+ }
+ return value;
+ }
+}
+
+export class FormAutofillCreditCardSection extends FormAutofillSection {
+ /**
+ * Credit Card Section Constructor
+ *
+ * @param {Array<FieldDetails>} fieldDetails
+ * The fieldDetail objects for the fields in this section
+ * @param {Object<FormAutofillHandler>} handler
+ * The handler responsible for this section
+ */
+ constructor(fieldDetails, handler) {
+ super(fieldDetails, handler);
+
+ if (!this.isValidSection()) {
+ return;
+ }
+
+ lazy.AutofillTelemetry.recordDetectedSectionCount(this);
+ lazy.AutofillTelemetry.recordFormInteractionEvent("detected", this);
+
+ // Check whether the section is in an <iframe>; and, if so,
+ // watch for the <iframe> to pagehide.
+ if (handler.window.location != handler.window.parent?.location) {
+ this.log.debug(
+ "Credit card form is in an iframe -- watching for pagehide",
+ fieldDetails
+ );
+ handler.window.addEventListener(
+ "pagehide",
+ this._handlePageHide.bind(this)
+ );
+ }
+ }
+
+ _handlePageHide(event) {
+ this.handler.window.removeEventListener(
+ "pagehide",
+ this._handlePageHide.bind(this)
+ );
+ this.log.debug("Credit card subframe is pagehideing", this.handler.form);
+
+ const formSubmissionReason =
+ FormAutofillUtils.FORM_SUBMISSION_REASON.IFRAME_PAGEHIDE;
+ this.handler.onFormSubmitted(formSubmissionReason);
+ }
+
+ /**
+ * Determine whether a set of cc fields identified by our heuristics form a
+ * valid credit card section.
+ * There are 4 different cases when a field is considered a credit card field
+ * 1. Identified by autocomplete attribute. ex <input autocomplete="cc-number">
+ * 2. Identified by fathom and fathom is pretty confident (when confidence
+ * value is higher than `highConfidenceThreshold`)
+ * 3. Identified by fathom. Confidence value is between `fathom.confidenceThreshold`
+ * and `fathom.highConfidenceThreshold`
+ * 4. Identified by regex-based heurstic. There is no confidence value in thise case.
+ *
+ * A form is considered a valid credit card form when one of the following condition
+ * is met:
+ * A. One of the cc field is identified by autocomplete (case 1)
+ * B. One of the cc field is identified by fathom (case 2 or 3), and there is also
+ * another cc field found by any of our heuristic (case 2, 3, or 4)
+ * C. Only one cc field is found in the section, but fathom is very confident (Case 2).
+ * Currently we add an extra restriction to this rule to decrease the false-positive
+ * rate. See comments below for details.
+ *
+ * @returns {boolean} True for a valid section, otherwise false
+ */
+ isValidSection() {
+ let ccNumberDetail = null;
+ let ccNameDetail = null;
+ let ccExpiryDetail = null;
+
+ for (let detail of this.fieldDetails) {
+ switch (detail.fieldName) {
+ case "cc-number":
+ ccNumberDetail = detail;
+ break;
+ case "cc-name":
+ case "cc-given-name":
+ case "cc-additional-name":
+ case "cc-family-name":
+ ccNameDetail = detail;
+ break;
+ case "cc-exp":
+ case "cc-exp-month":
+ case "cc-exp-year":
+ ccExpiryDetail = detail;
+ break;
+ }
+ }
+
+ // Condition A. Always trust autocomplete attribute. A section is considered a valid
+ // cc section as long as a field has autocomplete=cc-number, cc-name or cc-exp*
+ if (
+ ccNumberDetail?.reason == "autocomplete" ||
+ ccNameDetail?.reason == "autocomplete" ||
+ ccExpiryDetail?.reason == "autocomplete"
+ ) {
+ return true;
+ }
+
+ // Condition B. One of the field is identified by fathom, if this section also
+ // contains another cc field found by our heuristic (Case 2, 3, or 4), we consider
+ // this section a valid credit card seciton
+ if (ccNumberDetail?.reason == "fathom") {
+ if (ccNameDetail || ccExpiryDetail) {
+ return true;
+ }
+ } else if (ccNameDetail?.reason == "fathom") {
+ if (ccNumberDetail || ccExpiryDetail) {
+ return true;
+ }
+ }
+
+ // Condition C.
+ let highConfidenceThreshold =
+ FormAutofillUtils.ccFathomHighConfidenceThreshold;
+ let highConfidenceField;
+ if (ccNumberDetail?.confidence > highConfidenceThreshold) {
+ highConfidenceField = ccNumberDetail;
+ } else if (ccNameDetail?.confidence > highConfidenceThreshold) {
+ highConfidenceField = ccNameDetail;
+ }
+ if (highConfidenceField) {
+ // 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.
+ const element = highConfidenceField.element;
+ const root = element.form || element.ownerDocument;
+ const inputs = root.querySelectorAll("input:not([type=hidden])");
+ if (inputs.length == 1 && inputs[0] == element) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ isEnabled() {
+ return FormAutofill.isAutofillCreditCardsEnabled;
+ }
+
+ isRecordCreatable(record) {
+ return (
+ record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"])
+ );
+ }
+
+ /**
+ * Handles credit card expiry date transformation when
+ * the expiry date exists in a cc-exp field.
+ *
+ * @param {object} profile
+ * @memberof FormAutofillCreditCardSection
+ */
+ creditCardExpiryDateTransformer(profile) {
+ if (!profile["cc-exp"]) {
+ return;
+ }
+
+ const element = this.getFieldDetailByName("cc-exp")?.element;
+ if (!element) {
+ return;
+ }
+
+ function updateExpiry(_string, _month, _year) {
+ // Bug 1687681: This is a short term fix to other locales having
+ // different characters to represent year.
+ // - FR locales may use "A" to represent year.
+ // - DE locales may use "J" to represent year.
+ // - PL locales may use "R" to represent year.
+ // This approach will not scale well and should be investigated in a follow up bug.
+ const monthChars = "m";
+ const yearChars = "yy|aa|jj|rr";
+ const expiryDateFormatRegex = (firstChars, secondChars) =>
+ new RegExp(
+ "(?:\\b|^)((?:[" +
+ firstChars +
+ "]{2}){1,2})\\s*([\\-/])\\s*((?:[" +
+ secondChars +
+ "]{2}){1,2})(?:\\b|$)",
+ "i"
+ );
+
+ // If the month first check finds a result, where placeholder is "mm - yyyy",
+ // the result will be structured as such: ["mm - yyyy", "mm", "-", "yyyy"]
+ let result = expiryDateFormatRegex(monthChars, yearChars).exec(_string);
+ if (result) {
+ return (
+ _month.padStart(result[1].length, "0") +
+ result[2] +
+ _year.substr(-1 * result[3].length)
+ );
+ }
+
+ // If the year first check finds a result, where placeholder is "yyyy mm",
+ // the result will be structured as such: ["yyyy mm", "yyyy", " ", "mm"]
+ result = expiryDateFormatRegex(yearChars, monthChars).exec(_string);
+ if (result) {
+ return (
+ _year.substr(-1 * result[1].length) +
+ result[2] +
+ _month.padStart(result[3].length, "0")
+ );
+ }
+ return null;
+ }
+
+ let newExpiryString = null;
+ const month = profile["cc-exp-month"].toString();
+ const year = profile["cc-exp-year"].toString();
+ if (element.tagName == "INPUT") {
+ // Use the placeholder or label to determine the expiry string format.
+ const possibleExpiryStrings = [];
+ if (element.placeholder) {
+ possibleExpiryStrings.push(element.placeholder);
+ }
+ const labels = lazy.LabelUtils.findLabelElements(element);
+ if (labels) {
+ // Not consider multiple lable for now.
+ possibleExpiryStrings.push(element.labels[0]?.textContent);
+ }
+ if (element.previousElementSibling?.tagName == "LABEL") {
+ possibleExpiryStrings.push(element.previousElementSibling.textContent);
+ }
+
+ possibleExpiryStrings.some(string => {
+ newExpiryString = updateExpiry(string, month, year);
+ return !!newExpiryString;
+ });
+ }
+
+ // Bug 1688576: Change YYYY-MM to MM/YYYY since MM/YYYY is the
+ // preferred presentation format for credit card expiry dates.
+ profile["cc-exp"] = newExpiryString ?? `${month.padStart(2, "0")}/${year}`;
+ }
+
+ /**
+ * Handles credit card expiry date transformation when the expiry date exists in
+ * the separate cc-exp-month and cc-exp-year fields
+ *
+ * @param {object} profile
+ * @memberof FormAutofillCreditCardSection
+ */
+ creditCardExpMonthAndYearTransformer(profile) {
+ const getInputElementByField = (field, self) => {
+ if (!field) {
+ return null;
+ }
+ let detail = self.getFieldDetailByName(field);
+ if (!detail) {
+ return null;
+ }
+ let element = detail.element;
+ return element.tagName === "INPUT" ? element : null;
+ };
+ let month = getInputElementByField("cc-exp-month", this);
+ if (month) {
+ // Transform the expiry month to MM since this is a common format needed for filling.
+ profile["cc-exp-month-formatted"] = profile["cc-exp-month"]
+ ?.toString()
+ .padStart(2, "0");
+ }
+ let year = getInputElementByField("cc-exp-year", this);
+ // If the expiration year element is an input,
+ // then we examine any placeholder to see if we should format the expiration year
+ // as a zero padded string in order to autofill correctly.
+ if (year) {
+ let placeholder = year.placeholder;
+
+ // Checks for 'YY'|'AA'|'JJ'|'RR' placeholder and converts the year to a two digit string using the last two digits.
+ let result = /\b(yy|aa|jj|rr)\b/i.test(placeholder);
+ if (result) {
+ profile["cc-exp-year-formatted"] = profile["cc-exp-year"]
+ .toString()
+ .substring(2);
+ }
+ }
+ }
+
+ /**
+ * Handles credit card name transformation when the name exists in
+ * the separate cc-given-name, cc-middle-name, and cc-family name fields
+ *
+ * @param {object} profile
+ * @memberof FormAutofillCreditCardSection
+ */
+ creditCardNameTransformer(profile) {
+ const name = profile["cc-name"];
+ if (!name) {
+ return;
+ }
+
+ const given = this.getFieldDetailByName("cc-given-name");
+ const middle = this.getFieldDetailByName("cc-middle-name");
+ const family = this.getFieldDetailByName("cc-family-name");
+ if (given || middle || family) {
+ const nameParts = lazy.FormAutofillNameUtils.splitName(name);
+ if (given && nameParts.given) {
+ profile["cc-given-name"] = nameParts.given;
+ }
+ if (middle && nameParts.middle) {
+ profile["cc-middle-name"] = nameParts.middle;
+ }
+ if (family && nameParts.family) {
+ profile["cc-family-name"] = nameParts.family;
+ }
+ }
+ }
+
+ async _decrypt(cipherText, reauth) {
+ // Get the window for the form field.
+ let window;
+ for (let fieldDetail of this.fieldDetails) {
+ let element = fieldDetail.element;
+ if (element) {
+ window = element.ownerGlobal;
+ break;
+ }
+ }
+ if (!window) {
+ return null;
+ }
+
+ let actor = window.windowGlobalChild.getActor("FormAutofill");
+ return actor.sendQuery("FormAutofill:GetDecryptedString", {
+ cipherText,
+ reauth,
+ });
+ }
+
+ /*
+ * Apply all credit card related transformers.
+ *
+ * @param {Object} profile
+ * A profile for adjusting credit card related value.
+ * @override
+ */
+ applyTransformers(profile) {
+ // The matchSelectOptions transformer must be placed after the expiry transformers.
+ // This ensures that the expiry value that is cached in the matchSelectOptions
+ // matches the expiry value that is stored in the profile ensuring that autofill works
+ // correctly when dealing with option elements.
+ this.creditCardExpiryDateTransformer(profile);
+ this.creditCardExpMonthAndYearTransformer(profile);
+ this.creditCardNameTransformer(profile);
+ this.matchSelectOptions(profile);
+ this.adaptFieldMaxLength(profile);
+ }
+
+ getFilledValueFromProfile(fieldDetail, profile) {
+ const value = super.getFilledValueFromProfile(fieldDetail, profile);
+ if (fieldDetail.fieldName == "cc-number" && fieldDetail.part != null) {
+ const part = fieldDetail.part;
+ return value.slice((part - 1) * 4, part * 4);
+ }
+ return value;
+ }
+
+ computeFillingValue(value, fieldDetail, element) {
+ if (
+ fieldDetail.fieldName != "cc-type" ||
+ !HTMLSelectElement.isInstance(element)
+ ) {
+ return value;
+ }
+
+ if (lazy.CreditCard.isValidNetwork(value)) {
+ return value;
+ }
+
+ // Don't save the record when the option value is empty *OR* there
+ // are multiple options being selected. The empty option is usually
+ // assumed to be default along with a meaningless text to users.
+ if (value && element.selectedOptions.length == 1) {
+ let selectedOption = element.selectedOptions[0];
+ let networkType =
+ lazy.CreditCard.getNetworkFromName(selectedOption.text) ??
+ lazy.CreditCard.getNetworkFromName(selectedOption.value);
+ if (networkType) {
+ return networkType;
+ }
+ }
+ // If we couldn't match the value to any network, we'll
+ // strip this field when submitting.
+ return value;
+ }
+
+ /**
+ * Customize for previewing profile
+ *
+ * @param {object} profile
+ * A profile for pre-processing before previewing values.
+ * @override
+ */
+ preparePreviewProfile(profile) {
+ // Always show the decrypted credit card number when Master Password is
+ // disabled.
+ if (profile["cc-number-decrypted"]) {
+ profile["cc-number"] = profile["cc-number-decrypted"];
+ } else if (!profile["cc-number"].startsWith("****")) {
+ // Show the previewed credit card as "**** 4444" which is
+ // needed when a credit card number field has a maxlength of four.
+ profile["cc-number"] = "****" + profile["cc-number"];
+ }
+ }
+
+ /**
+ * Customize for filling profile
+ *
+ * @param {object} profile
+ * A profile for pre-processing before filling values.
+ * @returns {boolean} Whether the profile should be filled.
+ * @override
+ */
+ async prepareFillingProfile(profile) {
+ // Prompt the OS login dialog to get the decrypted credit card number.
+ if (profile["cc-number-encrypted"]) {
+ const promptMessage = FormAutofillUtils.reauthOSPromptMessage(
+ "autofill-use-payment-method-os-prompt-macos",
+ "autofill-use-payment-method-os-prompt-windows",
+ "autofill-use-payment-method-os-prompt-other"
+ );
+ let decrypted = await this._decrypt(
+ profile["cc-number-encrypted"],
+ promptMessage
+ );
+
+ if (!decrypted) {
+ // Early return if the decrypted is empty or undefined
+ return false;
+ }
+
+ profile["cc-number"] = decrypted;
+ }
+ return true;
+ }
+}
diff --git a/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs
new file mode 100644
index 0000000000..ce10c71ce1
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FormAutofillUtils.sys.mjs
@@ -0,0 +1,1129 @@
+/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
+ FormAutofillNameUtils:
+ "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ AddressMetaDataLoader:
+ "resource://gre/modules/shared/AddressMetaDataLoader.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () =>
+ new Localization(
+ ["toolkit/formautofill/formAutofill.ftl", "branding/brand.ftl"],
+ true
+ )
+);
+
+export let FormAutofillUtils;
+
+const ADDRESSES_COLLECTION_NAME = "addresses";
+const CREDITCARDS_COLLECTION_NAME = "creditCards";
+const MANAGE_ADDRESSES_L10N_IDS = [
+ "autofill-add-address-title",
+ "autofill-manage-addresses-title",
+];
+const EDIT_ADDRESS_L10N_IDS = [
+ "autofill-address-given-name",
+ "autofill-address-additional-name",
+ "autofill-address-family-name",
+ "autofill-address-organization",
+ "autofill-address-street",
+ "autofill-address-state",
+ "autofill-address-province",
+ "autofill-address-city",
+ "autofill-address-country",
+ "autofill-address-zip",
+ "autofill-address-postal-code",
+ "autofill-address-email",
+ "autofill-address-tel",
+];
+const MANAGE_CREDITCARDS_L10N_IDS = [
+ "autofill-add-card-title",
+ "autofill-manage-payment-methods-title",
+];
+const EDIT_CREDITCARD_L10N_IDS = [
+ "autofill-card-number",
+ "autofill-card-name-on-card",
+ "autofill-card-expires-month",
+ "autofill-card-expires-year",
+ "autofill-card-network",
+];
+const FIELD_STATES = {
+ NORMAL: "NORMAL",
+ AUTO_FILLED: "AUTO_FILLED",
+ PREVIEW: "PREVIEW",
+};
+const FORM_SUBMISSION_REASON = {
+ FORM_SUBMIT_EVENT: "form-submit-event",
+ FORM_REMOVAL_AFTER_FETCH: "form-removal-after-fetch",
+ IFRAME_PAGEHIDE: "iframe-pagehide",
+ PAGE_NAVIGATION: "page-navigation",
+};
+
+const ELIGIBLE_INPUT_TYPES = ["text", "email", "tel", "number", "month"];
+
+// The maximum length of data to be saved in a single field for preventing DoS
+// attacks that fill the user's hard drive(s).
+const MAX_FIELD_VALUE_LENGTH = 200;
+
+FormAutofillUtils = {
+ get AUTOFILL_FIELDS_THRESHOLD() {
+ return 3;
+ },
+
+ ADDRESSES_COLLECTION_NAME,
+ CREDITCARDS_COLLECTION_NAME,
+ MANAGE_ADDRESSES_L10N_IDS,
+ EDIT_ADDRESS_L10N_IDS,
+ MANAGE_CREDITCARDS_L10N_IDS,
+ EDIT_CREDITCARD_L10N_IDS,
+ MAX_FIELD_VALUE_LENGTH,
+ FIELD_STATES,
+ FORM_SUBMISSION_REASON,
+
+ _fieldNameInfo: {
+ name: "name",
+ "given-name": "name",
+ "additional-name": "name",
+ "family-name": "name",
+ organization: "organization",
+ "street-address": "address",
+ "address-line1": "address",
+ "address-line2": "address",
+ "address-line3": "address",
+ "address-level1": "address",
+ "address-level2": "address",
+ "postal-code": "address",
+ country: "address",
+ "country-name": "address",
+ tel: "tel",
+ "tel-country-code": "tel",
+ "tel-national": "tel",
+ "tel-area-code": "tel",
+ "tel-local": "tel",
+ "tel-local-prefix": "tel",
+ "tel-local-suffix": "tel",
+ "tel-extension": "tel",
+ email: "email",
+ "cc-name": "creditCard",
+ "cc-given-name": "creditCard",
+ "cc-additional-name": "creditCard",
+ "cc-family-name": "creditCard",
+ "cc-number": "creditCard",
+ "cc-exp-month": "creditCard",
+ "cc-exp-year": "creditCard",
+ "cc-exp": "creditCard",
+ "cc-type": "creditCard",
+ "cc-csc": "creditCard",
+ },
+
+ _collators: {},
+ _reAlternativeCountryNames: {},
+
+ isAddressField(fieldName) {
+ return (
+ !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName)
+ );
+ },
+
+ isCreditCardField(fieldName) {
+ return this._fieldNameInfo?.[fieldName] == "creditCard";
+ },
+
+ isCCNumber(ccNumber) {
+ return ccNumber && lazy.CreditCard.isValidNumber(ccNumber);
+ },
+
+ ensureLoggedIn(promptMessage) {
+ return lazy.OSKeyStore.ensureLoggedIn(
+ this._reauthEnabledByUser && promptMessage ? promptMessage : false
+ );
+ },
+
+ /**
+ * Get the array of credit card network ids ("types") we expect and offer as valid choices
+ *
+ * @returns {Array}
+ */
+ getCreditCardNetworks() {
+ return lazy.CreditCard.getSupportedNetworks();
+ },
+
+ getCategoryFromFieldName(fieldName) {
+ return this._fieldNameInfo[fieldName];
+ },
+
+ getCategoriesFromFieldNames(fieldNames) {
+ let categories = new Set();
+ for (let fieldName of fieldNames) {
+ let info = this.getCategoryFromFieldName(fieldName);
+ if (info) {
+ categories.add(info);
+ }
+ }
+ return Array.from(categories);
+ },
+
+ getAddressSeparator() {
+ // The separator should be based on the L10N address format, and using a
+ // white space is a temporary solution.
+ return " ";
+ },
+
+ /**
+ * Get address display label. It should display information separated
+ * by a comma.
+ *
+ * @param {object} address
+ * @returns {string}
+ */
+ getAddressLabel(address) {
+ // TODO: Implement a smarter way for deciding what to display
+ // as option text. Possibly improve the algorithm in
+ // ProfileAutoCompleteResult.jsm and reuse it here.
+ let fieldOrder = [
+ "name",
+ "-moz-street-address-one-line", // Street address
+ "address-level3", // Townland / Neighborhood / Village
+ "address-level2", // City/Town
+ "organization", // Company or organization name
+ "address-level1", // Province/State (Standardized code if possible)
+ "country", // Country name
+ "postal-code", // Postal code
+ "tel", // Phone number
+ "email", // Email address
+ ];
+
+ address = { ...address };
+ let parts = [];
+ if (address["street-address"]) {
+ address["-moz-street-address-one-line"] = this.toOneLineAddress(
+ address["street-address"]
+ );
+ }
+
+ if (!("name" in address)) {
+ address.name = lazy.FormAutofillNameUtils.joinNameParts({
+ given: address["given-name"],
+ middle: address["additional-name"],
+ family: address["family-name"],
+ });
+ }
+
+ for (const fieldName of fieldOrder) {
+ let string = address[fieldName];
+ if (string) {
+ parts.push(string);
+ }
+ }
+ return parts.join(", ");
+ },
+
+ /**
+ * Internal method to split an address to multiple parts per the provided delimiter,
+ * removing blank parts.
+ *
+ * @param {string} address The address the split
+ * @param {string} [delimiter] The separator that is used between lines in the address
+ * @returns {string[]}
+ */
+ _toStreetAddressParts(address, delimiter = "\n") {
+ let array = typeof address == "string" ? address.split(delimiter) : address;
+
+ if (!Array.isArray(array)) {
+ return [];
+ }
+ return array.map(s => (s ? s.trim() : "")).filter(s => s);
+ },
+
+ /**
+ * Converts a street address to a single line, removing linebreaks marked by the delimiter
+ *
+ * @param {string} address The address the convert
+ * @param {string} [delimiter] The separator that is used between lines in the address
+ * @returns {string}
+ */
+ toOneLineAddress(address, delimiter = "\n") {
+ let addressParts = this._toStreetAddressParts(address, delimiter);
+ return addressParts.join(this.getAddressSeparator());
+ },
+
+ /**
+ * In-place concatenate tel-related components into a single "tel" field and
+ * delete unnecessary fields.
+ *
+ * @param {object} address An address record.
+ */
+ compressTel(address) {
+ let telCountryCode = address["tel-country-code"] || "";
+ let telAreaCode = address["tel-area-code"] || "";
+
+ if (!address.tel) {
+ if (address["tel-national"]) {
+ address.tel = telCountryCode + address["tel-national"];
+ } else if (address["tel-local"]) {
+ address.tel = telCountryCode + telAreaCode + address["tel-local"];
+ } else if (address["tel-local-prefix"] && address["tel-local-suffix"]) {
+ address.tel =
+ telCountryCode +
+ telAreaCode +
+ address["tel-local-prefix"] +
+ address["tel-local-suffix"];
+ }
+ }
+
+ for (let field in address) {
+ if (field != "tel" && this.getCategoryFromFieldName(field) == "tel") {
+ delete address[field];
+ }
+ }
+ },
+
+ /**
+ * Determines if an element can be autofilled or not.
+ *
+ * @param {HTMLElement} element
+ * @returns {boolean} true if the element can be autofilled
+ */
+ isFieldAutofillable(element) {
+ return element && !element.readOnly && !element.disabled;
+ },
+
+ /**
+ * Determines if an element is focusable
+ * and accessible via keyboard navigation or not.
+ *
+ * @param {HTMLElement} element
+ *
+ * @returns {bool} true if the element is focusable and accessible
+ */
+ isFieldFocusable(element) {
+ return (
+ // The Services.focus.elementIsFocusable API considers elements with
+ // tabIndex="-1" set as focusable. But since they are not accessible
+ // via keyboard navigation we treat them as non-interactive
+ Services.focus.elementIsFocusable(element, 0) && element.tabIndex != "-1"
+ );
+ },
+
+ /**
+ * Determines if an element is eligible to be used by credit card or address autofill.
+ *
+ * @param {HTMLElement} element
+ * @returns {boolean} true if element can be used by credit card or address autofill
+ */
+ isCreditCardOrAddressFieldType(element) {
+ if (!element) {
+ return false;
+ }
+
+ if (HTMLInputElement.isInstance(element)) {
+ // `element.type` can be recognized as `text`, if it's missing or invalid.
+ return ELIGIBLE_INPUT_TYPES.includes(element.type);
+ }
+
+ return HTMLSelectElement.isInstance(element);
+ },
+
+ loadDataFromScript(url, sandbox = {}) {
+ Services.scriptloader.loadSubScript(url, sandbox);
+ return sandbox;
+ },
+
+ /**
+ * Get country address data and fallback to US if not found.
+ * See AddressMetaDataLoader.#loadData for more details of addressData structure.
+ *
+ * @param {string} [country=FormAutofill.DEFAULT_REGION]
+ * The country code for requesting specific country's metadata. It'll be
+ * default region if parameter is not set.
+ * @param {string} [level1=null]
+ * Return address level 1/level 2 metadata if parameter is set.
+ * @returns {object|null}
+ * Return metadata of specific region with default locale and other supported
+ * locales. We need to return a default country metadata for layout format
+ * and collator, but for sub-region metadata we'll just return null if not found.
+ */
+ getCountryAddressRawData(
+ country = FormAutofill.DEFAULT_REGION,
+ level1 = null
+ ) {
+ let metadata = lazy.AddressMetaDataLoader.getData(country, level1);
+ if (!metadata) {
+ if (level1) {
+ return null;
+ }
+ // Fallback to default region if we couldn't get data from given country.
+ if (country != FormAutofill.DEFAULT_REGION) {
+ metadata = lazy.AddressMetaDataLoader.getData(
+ FormAutofill.DEFAULT_REGION
+ );
+ }
+ }
+
+ // TODO: Now we fallback to US if we couldn't get data from default region,
+ // but it could be removed in bug 1423464 if it's not necessary.
+ if (!metadata) {
+ metadata = lazy.AddressMetaDataLoader.getData("US");
+ }
+ return metadata;
+ },
+
+ /**
+ * Get country address data with default locale.
+ *
+ * @param {string} country
+ * @param {string} level1
+ * @returns {object|null} Return metadata of specific region with default locale.
+ * NOTE: The returned data may be for a default region if the
+ * specified one cannot be found. Callers who only want the specific
+ * region should check the returned country code.
+ */
+ getCountryAddressData(country, level1) {
+ let metadata = this.getCountryAddressRawData(country, level1);
+ return metadata && metadata.defaultLocale;
+ },
+
+ /**
+ * Get country address data with all locales.
+ *
+ * @param {string} country
+ * @param {string} level1
+ * @returns {Array<object> | null}
+ * Return metadata of specific region with all the locales.
+ * NOTE: The returned data may be for a default region if the
+ * specified one cannot be found. Callers who only want the specific
+ * region should check the returned country code.
+ */
+ getCountryAddressDataWithLocales(country, level1) {
+ let metadata = this.getCountryAddressRawData(country, level1);
+ return metadata && [metadata.defaultLocale, ...metadata.locales];
+ },
+
+ /**
+ * Get the collators based on the specified country.
+ *
+ * @param {string} country The specified country.
+ * @param {object} [options = {}] a list of options for this method
+ * @param {boolean} [options.ignorePunctuation = true] Whether punctuation should be ignored.
+ * @param {string} [options.sensitivity = 'base'] Which differences in the strings should lead to non-zero result values
+ * @param {string} [options.usage = 'search'] Whether the comparison is for sorting or for searching for matching strings
+ * @returns {Array} An array containing several collator objects.
+ */
+ getSearchCollators(
+ country,
+ { ignorePunctuation = true, sensitivity = "base", usage = "search" } = {}
+ ) {
+ // TODO: Only one language should be used at a time per country. The locale
+ // of the page should be taken into account to do this properly.
+ // We are going to support more countries in bug 1370193 and this
+ // should be addressed when we start to implement that bug.
+
+ if (!this._collators[country]) {
+ let dataset = this.getCountryAddressData(country);
+ let languages = dataset.languages || [dataset.lang];
+ let options = {
+ ignorePunctuation,
+ sensitivity,
+ usage,
+ };
+ this._collators[country] = languages.map(
+ lang => new Intl.Collator(lang, options)
+ );
+ }
+ return this._collators[country];
+ },
+
+ // Based on the list of fields abbreviations in
+ // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata
+ FIELDS_LOOKUP: {
+ N: "name",
+ O: "organization",
+ A: "street-address",
+ S: "address-level1",
+ C: "address-level2",
+ D: "address-level3",
+ Z: "postal-code",
+ n: "newLine",
+ },
+
+ /**
+ * Parse a country address format string and outputs an array of fields.
+ * Spaces, commas, and other literals are ignored in this implementation.
+ * For example, format string "%A%n%C, %S" should return:
+ * [
+ * {fieldId: "street-address", newLine: true},
+ * {fieldId: "address-level2"},
+ * {fieldId: "address-level1"},
+ * ]
+ *
+ * @param {string} fmt Country address format string
+ * @returns {Array<object>} List of fields
+ */
+ parseAddressFormat(fmt) {
+ if (!fmt) {
+ throw new Error("fmt string is missing.");
+ }
+
+ return fmt.match(/%[^%]/g).reduce((parsed, part) => {
+ // Take the first letter of each segment and try to identify it
+ let fieldId = this.FIELDS_LOOKUP[part[1]];
+ // Early return if cannot identify part.
+ if (!fieldId) {
+ return parsed;
+ }
+ // If a new line is detected, add an attribute to the previous field.
+ if (fieldId == "newLine") {
+ let size = parsed.length;
+ if (size) {
+ parsed[size - 1].newLine = true;
+ }
+ return parsed;
+ }
+ return parsed.concat({ fieldId });
+ }, []);
+ },
+
+ /**
+ * Used to populate dropdowns in the UI (e.g. FormAutofill preferences).
+ * Use findAddressSelectOption for matching a value to a region.
+ *
+ * @param {string[]} subKeys An array of regionCode strings
+ * @param {string[]} subIsoids An array of ISO ID strings, if provided will be preferred over the key
+ * @param {string[]} subNames An array of regionName strings
+ * @param {string[]} subLnames An array of latinised regionName strings
+ * @returns {Map?} Returns null if subKeys or subNames are not truthy.
+ * Otherwise, a Map will be returned mapping keys -> names.
+ */
+ buildRegionMapIfAvailable(subKeys, subIsoids, subNames, subLnames) {
+ // Not all regions have sub_keys. e.g. DE
+ if (
+ !subKeys ||
+ !subKeys.length ||
+ (!subNames && !subLnames) ||
+ (subNames && subKeys.length != subNames.length) ||
+ (subLnames && subKeys.length != subLnames.length)
+ ) {
+ return null;
+ }
+
+ // Overwrite subKeys with subIsoids, when available
+ if (subIsoids && subIsoids.length && subIsoids.length == subKeys.length) {
+ for (let i = 0; i < subIsoids.length; i++) {
+ if (subIsoids[i]) {
+ subKeys[i] = subIsoids[i];
+ }
+ }
+ }
+
+ // Apply sub_lnames if sub_names does not exist
+ let names = subNames || subLnames;
+ return new Map(subKeys.map((key, index) => [key, names[index]]));
+ },
+
+ /**
+ * Parse a require string and outputs an array of fields.
+ * Spaces, commas, and other literals are ignored in this implementation.
+ * For example, a require string "ACS" should return:
+ * ["street-address", "address-level2", "address-level1"]
+ *
+ * @param {string} requireString Country address require string
+ * @returns {Array<string>} List of fields
+ */
+ parseRequireString(requireString) {
+ if (!requireString) {
+ throw new Error("requireString string is missing.");
+ }
+
+ return requireString.split("").map(fieldId => this.FIELDS_LOOKUP[fieldId]);
+ },
+
+ /**
+ * Use address data and alternative country name list to identify a country code from a
+ * specified country name.
+ *
+ * @param {string} countryName A country name to be identified
+ * @param {string} [countrySpecified] A country code indicating that we only
+ * search its alternative names if specified.
+ * @returns {string} The matching country code.
+ */
+ identifyCountryCode(countryName, countrySpecified) {
+ if (!countryName) {
+ return null;
+ }
+
+ if (lazy.AddressMetaDataLoader.getData(countryName)) {
+ return countryName;
+ }
+
+ const countries = countrySpecified
+ ? [countrySpecified]
+ : [...FormAutofill.countries.keys()];
+
+ for (const country of countries) {
+ let collators = this.getSearchCollators(country);
+ let metadata = this.getCountryAddressData(country);
+ if (country != metadata.key) {
+ // We hit the fallback logic in getCountryAddressRawData so ignore it as
+ // it's not related to `country` and use the name from l10n instead.
+ metadata = {
+ id: `data/${country}`,
+ key: country,
+ name: FormAutofill.countries.get(country),
+ };
+ }
+ let alternativeCountryNames = metadata.alternative_names || [
+ metadata.name,
+ ];
+ let reAlternativeCountryNames = this._reAlternativeCountryNames[country];
+ if (!reAlternativeCountryNames) {
+ reAlternativeCountryNames = this._reAlternativeCountryNames[country] =
+ [];
+ }
+
+ if (countryName.length == 3) {
+ if (this.strCompare(metadata.alpha_3_code, countryName, collators)) {
+ return country;
+ }
+ }
+
+ for (let i = 0; i < alternativeCountryNames.length; i++) {
+ let name = alternativeCountryNames[i];
+ let reName = reAlternativeCountryNames[i];
+ if (!reName) {
+ reName = reAlternativeCountryNames[i] = new RegExp(
+ "\\b" + this.escapeRegExp(name) + "\\b",
+ "i"
+ );
+ }
+
+ if (
+ this.strCompare(name, countryName, collators) ||
+ reName.test(countryName)
+ ) {
+ return country;
+ }
+ }
+ }
+
+ return null;
+ },
+
+ findSelectOption(selectEl, record, fieldName) {
+ if (this.isAddressField(fieldName)) {
+ return this.findAddressSelectOption(selectEl, record, fieldName);
+ }
+ if (this.isCreditCardField(fieldName)) {
+ return this.findCreditCardSelectOption(selectEl, record, fieldName);
+ }
+ return null;
+ },
+
+ /**
+ * Try to find the abbreviation of the given sub-region name
+ *
+ * @param {string[]} subregionValues A list of inferable sub-region values.
+ * @param {string} [country] A country name to be identified.
+ * @returns {string} The matching sub-region abbreviation.
+ */
+ getAbbreviatedSubregionName(subregionValues, country) {
+ let values = Array.isArray(subregionValues)
+ ? subregionValues
+ : [subregionValues];
+
+ let collators = this.getSearchCollators(country);
+ for (let metadata of this.getCountryAddressDataWithLocales(country)) {
+ let {
+ sub_keys: subKeys,
+ sub_names: subNames,
+ sub_lnames: subLnames,
+ } = metadata;
+ if (!subKeys) {
+ // Not all regions have sub_keys. e.g. DE
+ continue;
+ }
+ // Apply sub_lnames if sub_names does not exist
+ subNames = subNames || subLnames;
+
+ let speculatedSubIndexes = [];
+ for (const val of values) {
+ let identifiedValue = this.identifyValue(
+ subKeys,
+ subNames,
+ val,
+ collators
+ );
+ if (identifiedValue) {
+ return identifiedValue;
+ }
+
+ // Predict the possible state by partial-matching if no exact match.
+ [subKeys, subNames].forEach(sub => {
+ speculatedSubIndexes.push(
+ sub.findIndex(token => {
+ let pattern = new RegExp(
+ "\\b" + this.escapeRegExp(token) + "\\b"
+ );
+
+ return pattern.test(val);
+ })
+ );
+ });
+ }
+ let subKey = subKeys[speculatedSubIndexes.find(i => !!~i)];
+ if (subKey) {
+ return subKey;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Find the option element from select element.
+ * 1. Try to find the locale using the country from address.
+ * 2. First pass try to find exact match.
+ * 3. Second pass try to identify values from address value and options,
+ * and look for a match.
+ *
+ * @param {DOMElement} selectEl
+ * @param {object} address
+ * @param {string} fieldName
+ * @returns {DOMElement}
+ */
+ findAddressSelectOption(selectEl, address, fieldName) {
+ if (selectEl.options.length > 512) {
+ // Allow enough space for all countries (roughly 300 distinct values) and all
+ // timezones (roughly 400 distinct values), plus some extra wiggle room.
+ return null;
+ }
+ let value = address[fieldName];
+ if (!value) {
+ return null;
+ }
+
+ let collators = this.getSearchCollators(address.country);
+
+ for (let option of selectEl.options) {
+ if (
+ this.strCompare(value, option.value, collators) ||
+ this.strCompare(value, option.text, collators)
+ ) {
+ return option;
+ }
+ }
+
+ switch (fieldName) {
+ case "address-level1": {
+ let { country } = address;
+ let identifiedValue = this.getAbbreviatedSubregionName(
+ [value],
+ country
+ );
+ // No point going any further if we cannot identify value from address level 1
+ if (!identifiedValue) {
+ return null;
+ }
+ for (let dataset of this.getCountryAddressDataWithLocales(country)) {
+ let keys = dataset.sub_keys;
+ if (!keys) {
+ // Not all regions have sub_keys. e.g. DE
+ continue;
+ }
+ // Apply sub_lnames if sub_names does not exist
+ let names = dataset.sub_names || dataset.sub_lnames;
+
+ // Go through options one by one to find a match.
+ // Also check if any option contain the address-level1 key.
+ let pattern = new RegExp(
+ "\\b" + this.escapeRegExp(identifiedValue) + "\\b",
+ "i"
+ );
+ for (let option of selectEl.options) {
+ let optionValue = this.identifyValue(
+ keys,
+ names,
+ option.value,
+ collators
+ );
+ let optionText = this.identifyValue(
+ keys,
+ names,
+ option.text,
+ collators
+ );
+ if (
+ identifiedValue === optionValue ||
+ identifiedValue === optionText ||
+ pattern.test(option.value)
+ ) {
+ return option;
+ }
+ }
+ }
+ break;
+ }
+ case "country": {
+ if (this.getCountryAddressData(value)) {
+ for (let option of selectEl.options) {
+ if (
+ this.identifyCountryCode(option.text, value) ||
+ this.identifyCountryCode(option.value, value)
+ ) {
+ return option;
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ return null;
+ },
+
+ findCreditCardSelectOption(selectEl, creditCard, fieldName) {
+ let oneDigitMonth = creditCard["cc-exp-month"]
+ ? creditCard["cc-exp-month"].toString()
+ : null;
+ let twoDigitsMonth = oneDigitMonth ? oneDigitMonth.padStart(2, "0") : null;
+ let fourDigitsYear = creditCard["cc-exp-year"]
+ ? creditCard["cc-exp-year"].toString()
+ : null;
+ let twoDigitsYear = fourDigitsYear ? fourDigitsYear.substr(2, 2) : null;
+ let options = Array.from(selectEl.options);
+
+ switch (fieldName) {
+ case "cc-exp-month": {
+ if (!oneDigitMonth) {
+ return null;
+ }
+ for (let option of options) {
+ if (
+ [option.text, option.label, option.value].some(s => {
+ let result = /[1-9]\d*/.exec(s);
+ return result && result[0] == oneDigitMonth;
+ })
+ ) {
+ return option;
+ }
+ }
+ break;
+ }
+ case "cc-exp-year": {
+ if (!fourDigitsYear) {
+ return null;
+ }
+ for (let option of options) {
+ if (
+ [option.text, option.label, option.value].some(
+ s => s == twoDigitsYear || s == fourDigitsYear
+ )
+ ) {
+ return option;
+ }
+ }
+ break;
+ }
+ case "cc-exp": {
+ if (!oneDigitMonth || !fourDigitsYear) {
+ return null;
+ }
+ let patterns = [
+ oneDigitMonth + "/" + twoDigitsYear, // 8/22
+ oneDigitMonth + "/" + fourDigitsYear, // 8/2022
+ twoDigitsMonth + "/" + twoDigitsYear, // 08/22
+ twoDigitsMonth + "/" + fourDigitsYear, // 08/2022
+ oneDigitMonth + "-" + twoDigitsYear, // 8-22
+ oneDigitMonth + "-" + fourDigitsYear, // 8-2022
+ twoDigitsMonth + "-" + twoDigitsYear, // 08-22
+ twoDigitsMonth + "-" + fourDigitsYear, // 08-2022
+ twoDigitsYear + "-" + twoDigitsMonth, // 22-08
+ fourDigitsYear + "-" + twoDigitsMonth, // 2022-08
+ fourDigitsYear + "/" + oneDigitMonth, // 2022/8
+ twoDigitsMonth + twoDigitsYear, // 0822
+ twoDigitsYear + twoDigitsMonth, // 2208
+ ];
+
+ for (let option of options) {
+ if (
+ [option.text, option.label, option.value].some(str =>
+ patterns.some(pattern => str.includes(pattern))
+ )
+ ) {
+ return option;
+ }
+ }
+ break;
+ }
+ case "cc-type": {
+ let network = creditCard["cc-type"] || "";
+ for (let option of options) {
+ if (
+ [option.text, option.label, option.value].some(
+ s => lazy.CreditCard.getNetworkFromName(s) == network
+ )
+ ) {
+ return option;
+ }
+ }
+ break;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Try to match value with keys and names, but always return the key.
+ *
+ * @param {Array<string>} keys
+ * @param {Array<string>} names
+ * @param {string} value
+ * @param {Array} collators
+ * @returns {string}
+ */
+ identifyValue(keys, names, value, collators) {
+ let resultKey = keys.find(key => this.strCompare(value, key, collators));
+ if (resultKey) {
+ return resultKey;
+ }
+
+ let index = names.findIndex(name =>
+ this.strCompare(value, name, collators)
+ );
+ if (index !== -1) {
+ return keys[index];
+ }
+
+ return null;
+ },
+
+ /**
+ * Compare if two strings are the same.
+ *
+ * @param {string} a
+ * @param {string} b
+ * @param {Array} collators
+ * @returns {boolean}
+ */
+ strCompare(a = "", b = "", collators) {
+ return collators.some(collator => !collator.compare(a, b));
+ },
+
+ /**
+ * Determine whether one string(b) may be found within another string(a)
+ *
+ * @param {string} a
+ * @param {string} b
+ * @param {Array} collators
+ * @returns {boolean} True if the string is found
+ */
+ strInclude(a = "", b = "", collators) {
+ const len = a.length - b.length;
+ for (let i = 0; i <= len; i++) {
+ if (this.strCompare(a.substring(i, i + b.length), b, collators)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Escaping user input to be treated as a literal string within a regular
+ * expression.
+ *
+ * @param {string} string
+ * @returns {string}
+ */
+ escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ },
+
+ /**
+ * Get formatting information of a given country
+ *
+ * @param {string} country
+ * @returns {object}
+ * {
+ * {string} addressLevel3L10nId
+ * {string} addressLevel2L10nId
+ * {string} addressLevel1L10nId
+ * {string} postalCodeL10nId
+ * {object} fieldsOrder
+ * {string} postalCodePattern
+ * }
+ */
+ getFormFormat(country) {
+ let dataset = this.getCountryAddressData(country);
+ // We hit a country fallback in `getCountryAddressRawData` but it's not relevant here.
+ if (country != dataset.key) {
+ // Use a sparse object so the below default values take effect.
+ dataset = {
+ /**
+ * Even though data/ZZ only has address-level2, include the other levels
+ * in case they are needed for unknown countries. Users can leave the
+ * unnecessary fields blank which is better than forcing users to enter
+ * the data in incorrect fields.
+ */
+ fmt: "%N%n%O%n%A%n%C %S %Z",
+ };
+ }
+ return {
+ // When particular values are missing for a country, the
+ // data/ZZ value should be used instead:
+ // https://chromium-i18n.appspot.com/ssl-aggregate-address/data/ZZ
+ addressLevel3L10nId: this.getAddressFieldL10nId(
+ dataset.sublocality_name_type || "suburb"
+ ),
+ addressLevel2L10nId: this.getAddressFieldL10nId(
+ dataset.locality_name_type || "city"
+ ),
+ addressLevel1L10nId: this.getAddressFieldL10nId(
+ dataset.state_name_type || "province"
+ ),
+ addressLevel1Options: this.buildRegionMapIfAvailable(
+ dataset.sub_keys,
+ dataset.sub_isoids,
+ dataset.sub_names,
+ dataset.sub_lnames
+ ),
+ countryRequiredFields: this.parseRequireString(dataset.require || "AC"),
+ fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
+ postalCodeL10nId: this.getAddressFieldL10nId(
+ dataset.zip_name_type || "postal-code"
+ ),
+ postalCodePattern: dataset.zip,
+ };
+ },
+
+ getAddressFieldL10nId(type) {
+ return "autofill-address-" + type.replace(/_/g, "-");
+ },
+
+ CC_FATHOM_NONE: 0,
+ CC_FATHOM_JS: 1,
+ CC_FATHOM_NATIVE: 2,
+ isFathomCreditCardsEnabled() {
+ return this.ccHeuristicsMode != this.CC_FATHOM_NONE;
+ },
+
+ /**
+ * Transform the key in FormAutofillConfidences (defined in ChromeUtils.webidl)
+ * to fathom recognized field type.
+ *
+ * @param {string} key key from FormAutofillConfidences dictionary
+ * @returns {string} fathom field type
+ */
+ formAutofillConfidencesKeyToCCFieldType(key) {
+ const MAP = {
+ ccNumber: "cc-number",
+ ccName: "cc-name",
+ ccType: "cc-type",
+ ccExp: "cc-exp",
+ ccExpMonth: "cc-exp-month",
+ ccExpYear: "cc-exp-year",
+ };
+ return MAP[key];
+ },
+ /**
+ * Generates the localized os dialog message that
+ * prompts the user to reauthenticate
+ *
+ * @param {string} msgMac fluent message id for macos clients
+ * @param {string} msgWin fluent message id for windows clients
+ * @param {string} msgOther fluent message id for other clients
+ * @param {string} msgLin (optional) fluent message id for linux clients
+ * @returns {string} localized os prompt message
+ */
+ reauthOSPromptMessage(msgMac, msgWin, msgOther, msgLin = null) {
+ const platform = AppConstants.platform;
+ let messageID;
+
+ switch (platform) {
+ case "win":
+ messageID = msgWin;
+ break;
+ case "macosx":
+ messageID = msgMac;
+ break;
+ case "linux":
+ messageID = msgLin ?? msgOther;
+ break;
+ default:
+ messageID = msgOther;
+ }
+ return lazy.l10n.formatValueSync(messageID);
+ },
+};
+
+ChromeUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://formautofill/locale/formautofill.properties"
+ );
+});
+
+ChromeUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ FormAutofillUtils,
+ "_reauthEnabledByUser",
+ "extensions.formautofill.reauth.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ FormAutofillUtils,
+ "ccHeuristicsMode",
+ "extensions.formautofill.creditCards.heuristics.mode",
+ 0
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ FormAutofillUtils,
+ "ccFathomConfidenceThreshold",
+ "extensions.formautofill.creditCards.heuristics.fathom.confidenceThreshold",
+ null,
+ null,
+ pref => parseFloat(pref)
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ FormAutofillUtils,
+ "ccFathomHighConfidenceThreshold",
+ "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
+ null,
+ null,
+ pref => parseFloat(pref)
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ FormAutofillUtils,
+ "ccFathomTestConfidence",
+ "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
+ null,
+ null,
+ pref => parseFloat(pref)
+);
+
+// This is only used in iOS
+XPCOMUtils.defineLazyPreferenceGetter(
+ FormAutofillUtils,
+ "focusOnAutofill",
+ "extensions.formautofill.focusOnAutofill",
+ true
+);
diff --git a/toolkit/components/formautofill/shared/FormStateManager.sys.mjs b/toolkit/components/formautofill/shared/FormStateManager.sys.mjs
new file mode 100644
index 0000000000..064b4e5356
--- /dev/null
+++ b/toolkit/components/formautofill/shared/FormStateManager.sys.mjs
@@ -0,0 +1,157 @@
+/* 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, {
+ FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
+ FormAutofillHandler:
+ "resource://gre/modules/shared/FormAutofillHandler.sys.mjs",
+});
+
+export class FormStateManager {
+ constructor(onSubmit, onAutofillCallback) {
+ /**
+ * @type {WeakMap} mapping FormLike root HTML elements to FormAutofillHandler objects.
+ */
+ this._formsDetails = new WeakMap();
+ /**
+ * @type {object} The object where to store the active items, e.g. element,
+ * handler, section, and field detail.
+ */
+ this._activeItems = {};
+
+ this.onSubmit = onSubmit;
+
+ this.onAutofillCallback = onAutofillCallback;
+ }
+
+ /**
+ * Get the active input's information from cache which is created after page
+ * identified.
+ *
+ * @returns {object | null}
+ * Return the active input's information that cloned from content cache
+ * (or return null if the information is not found in the cache).
+ */
+ get activeFieldDetail() {
+ if (!this._activeItems.fieldDetail) {
+ let formDetails = this.activeFormDetails;
+ if (!formDetails) {
+ return null;
+ }
+ for (let detail of formDetails) {
+ let detailElement = detail.element;
+ if (detailElement && this.activeInput == detailElement) {
+ this._activeItems.fieldDetail = detail;
+ break;
+ }
+ }
+ }
+ return this._activeItems.fieldDetail;
+ }
+
+ /**
+ * Get the active form's information from cache which is created after page
+ * identified.
+ *
+ * @returns {Array<object> | null}
+ * Return target form's information from content cache
+ * (or return null if the information is not found in the cache).
+ *
+ */
+ get activeFormDetails() {
+ let formHandler = this.activeHandler;
+ return formHandler ? formHandler.fieldDetails : null;
+ }
+
+ get activeInput() {
+ return this._activeItems.elementWeakRef?.deref();
+ }
+
+ get activeHandler() {
+ const activeInput = this.activeInput;
+ if (!activeInput) {
+ return null;
+ }
+
+ // XXX: We are recomputing the activeHandler every time to avoid keeping a
+ // reference on the active element. This might be called quite frequently
+ // so if _getFormHandler/findRootForField become more costly, we should
+ // look into caching this result (eg by adding a weakmap).
+ let handler = this._getFormHandler(activeInput);
+ if (handler) {
+ handler.focusedInput = activeInput;
+ }
+ return handler;
+ }
+
+ get activeSection() {
+ let formHandler = this.activeHandler;
+ return formHandler ? formHandler.activeSection : null;
+ }
+
+ /**
+ * Get the form's handler from cache which is created after page identified.
+ *
+ * @param {HTMLInputElement} element Focused input which triggered profile searching
+ * @returns {Array<object> | null}
+ * Return target form's handler from content cache
+ * (or return null if the information is not found in the cache).
+ *
+ */
+ _getFormHandler(element) {
+ if (!element) {
+ return null;
+ }
+ let rootElement = lazy.FormLikeFactory.findRootForField(element);
+ return this._formsDetails.get(rootElement);
+ }
+
+ identifyAutofillFields(element) {
+ let formHandler = this._getFormHandler(element);
+ if (!formHandler) {
+ let formLike = lazy.FormLikeFactory.createFromField(element);
+ formHandler = new lazy.FormAutofillHandler(
+ formLike,
+ this.onSubmit,
+ this.onAutofillCallback
+ );
+ } else if (!formHandler.updateFormIfNeeded(element)) {
+ return formHandler.fieldDetails;
+ }
+ this._formsDetails.set(formHandler.form.rootElement, formHandler);
+ return formHandler.collectFormFields();
+ }
+
+ updateActiveInput(element) {
+ if (!element) {
+ this._activeItems = {};
+ return;
+ }
+ this._activeItems = {
+ elementWeakRef: new WeakRef(element),
+ fieldDetail: null,
+ };
+ }
+
+ getRecords(formElement, handler) {
+ handler = handler || this._formsDetails.get(formElement);
+ const records = handler?.createRecords();
+
+ if (
+ !handler ||
+ !records ||
+ !Object.values(records).some(typeRecords => typeRecords.length)
+ ) {
+ return null;
+ }
+ return records;
+ }
+
+ didDestroy() {
+ this._activeItems = null;
+ }
+}
+
+export default FormStateManager;
diff --git a/toolkit/components/formautofill/shared/HeuristicsRegExp.sys.mjs b/toolkit/components/formautofill/shared/HeuristicsRegExp.sys.mjs
new file mode 100644
index 0000000000..c4141628f8
--- /dev/null
+++ b/toolkit/components/formautofill/shared/HeuristicsRegExp.sys.mjs
@@ -0,0 +1,687 @@
+/* eslint-disable no-useless-concat */
+/* 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/. */
+
+// prettier-ignore
+export const HeuristicsRegExp = {
+ RULES: {
+ email: undefined,
+ tel: undefined,
+ organization: undefined,
+ "street-address": undefined,
+ "address-line1": undefined,
+ "address-line2": undefined,
+ "address-line3": undefined,
+ "address-level2": undefined,
+ "address-level1": undefined,
+ "postal-code": undefined,
+ country: undefined,
+ // Note: We place the `cc-name` field for Credit Card first, because
+ // it is more specific than the `name` field below and we want to check
+ // for it before we catch the more generic one.
+ "cc-name": undefined,
+ name: undefined,
+ "given-name": undefined,
+ "additional-name": undefined,
+ "family-name": undefined,
+ "cc-csc": undefined,
+ "cc-number": undefined,
+ "cc-exp-month": undefined,
+ "cc-exp-year": undefined,
+ "cc-exp": undefined,
+ "cc-type": undefined,
+ },
+
+ // regular expressions that only apply to label
+ LABEL_RULES: {
+ "address-line1": undefined,
+ "address-line2": undefined,
+ },
+
+ RULE_SETS: [
+ //=========================================================================
+ // Firefox-specific rules
+ {
+ "address-line1": "addrline1|address_1|addl1",
+ "address-line2": "addrline2|address_2|addl2",
+ "address-line3": "addrline3|address_3|addl3",
+ "address-level1": "land", // de-DE
+ "additional-name": "apellido.?materno|lastlastname",
+ "cc-name":
+ "accountholdername" +
+ "|titulaire", // fr-FR
+ "cc-number":
+ "(cc|kk)nr", // de-DE
+ "cc-exp":
+ "ważna.*do" + // pl-PL
+ "|data.*ważności" + // pl-PL
+ "|mm\\s*[\\-\\/]\\s*yy" + // en-US
+ "|mm\\s*[\\-\\/]\\s*aa" + // es-ES
+ "|mm\\s*[\\-\\/]\\s*jj" + // de-AT
+ "|vervaldatum", // nl-NL
+ "cc-exp-month":
+ "month" +
+ "|(cc|kk)month" + // de-DE
+ "|miesiąc" + // pl-PL
+ "|mes" + // es-ES
+ "|maand", // nl-NL
+ "cc-exp-year":
+ "year" +
+ "|(cc|kk)year" + // de-DE
+ "|rok" + // pl-PL
+ "|(anno|año)" + // es-ES
+ "|jaar", // nl-NL
+ "cc-type":
+ "type" +
+ "|kartenmarke" + // de-DE
+ "|typ.*karty", // pl-PL
+ "cc-csc":
+ "(\\bcvn\\b|\\bcvv\\b|\\bcvc\\b|\\bcsc\\b|\\bcvd\\b|\\bcid\\b|\\bccv\\b)",
+ },
+
+ //=========================================================================
+ // These are the rules used by Bitwarden [0], converted into RegExp form.
+ // [0] https://github.com/bitwarden/browser/blob/c2b8802201fac5e292d55d5caf3f1f78088d823c/src/services/autofill.service.ts#L436
+ {
+ email: "(^e-?mail$)|(^email-?address$)",
+
+ tel:
+ "(^phone$)" +
+ "|(^mobile$)" +
+ "|(^mobile-?phone$)" +
+ "|(^tel$)" +
+ "|(^telephone$)" +
+ "|(^phone-?number$)",
+
+ organization:
+ "(^company$)" +
+ "|(^company-?name$)" +
+ "|(^organization$)" +
+ "|(^organization-?name$)",
+
+ "street-address":
+ "(^address$)" +
+ "|(^street-?address$)" +
+ "|(^addr$)" +
+ "|(^street$)" +
+ "|(^mailing-?addr(ess)?$)" + // Modified to not grab lines, below
+ "|(^billing-?addr(ess)?$)" + // Modified to not grab lines, below
+ "|(^mail-?addr(ess)?$)" + // Modified to not grab lines, below
+ "|(^bill-?addr(ess)?$)", // Modified to not grab lines, below
+
+ "address-line1":
+ "(^address-?1$)" +
+ "|(^address-?line-?1$)" +
+ "|(^addr-?1$)" +
+ "|(^street-?1$)",
+
+ "address-line2":
+ "(^address-?2$)" +
+ "|(^address-?line-?2$)" +
+ "|(^addr-?2$)" +
+ "|(^street-?2$)",
+
+ "address-line3":
+ "(^address-?3$)" +
+ "|(^address-?line-?3$)" +
+ "|(^addr-?3$)" +
+ "|(^street-?3$)",
+
+ "address-level2":
+ "(^city$)" +
+ "|(^town$)" +
+ "|(^address-?level-?2$)" +
+ "|(^address-?city$)" +
+ "|(^address-?town$)",
+
+ "address-level1":
+ "(^state$)" +
+ "|(^province$)" +
+ "|(^provence$)" +
+ "|(^address-?level-?1$)" +
+ "|(^address-?state$)" +
+ "|(^address-?province$)",
+
+ "postal-code":
+ "(^postal$)" +
+ "|(^zip$)" +
+ "|(^zip2$)" +
+ "|(^zip-?code$)" +
+ "|(^postal-?code$)" +
+ "|(^post-?code$)" +
+ "|(^address-?zip$)" +
+ "|(^address-?postal$)" +
+ "|(^address-?code$)" +
+ "|(^address-?postal-?code$)" +
+ "|(^address-?zip-?code$)",
+
+ country:
+ "(^country$)" +
+ "|(^country-?code$)" +
+ "|(^country-?name$)" +
+ "|(^address-?country$)" +
+ "|(^address-?country-?name$)" +
+ "|(^address-?country-?code$)",
+
+ name: "(^name$)|full-?name|your-?name",
+
+ "given-name":
+ "(^f-?name$)" +
+ "|(^first-?name$)" +
+ "|(^given-?name$)" +
+ "|(^first-?n$)",
+
+ "additional-name":
+ "(^m-?name$)" +
+ "|(^middle-?name$)" +
+ "|(^additional-?name$)" +
+ "|(^middle-?initial$)" +
+ "|(^middle-?n$)" +
+ "|(^middle-?i$)",
+
+ "family-name":
+ "(^l-?name$)" +
+ "|(^last-?name$)" +
+ "|(^s-?name$)" +
+ "|(^surname$)" +
+ "|(^family-?name$)" +
+ "|(^family-?n$)" +
+ "|(^last-?n$)",
+
+ "cc-name":
+ "cc-?name" +
+ "|card-?name" +
+ "|cardholder-?name" +
+ "|cardholder" +
+ // "|(^name$)" + // Removed to avoid overwriting "name", above.
+ "|(^nom$)",
+
+ "cc-number":
+ "cc-?number" +
+ "|cc-?num" +
+ "|card-?number" +
+ "|card-?num" +
+ "|(^number$)" +
+ "|(^cc$)" +
+ "|cc-?no" +
+ "|card-?no" +
+ "|(^credit-?card$)" +
+ "|numero-?carte" +
+ "|(^carte$)" +
+ "|(^carte-?credit$)" +
+ "|num-?carte" +
+ "|cb-?num",
+
+ "cc-exp":
+ "(^cc-?exp$)" +
+ "|(^card-?exp$)" +
+ "|(^cc-?expiration$)" +
+ "|(^card-?expiration$)" +
+ "|(^cc-?ex$)" +
+ "|(^card-?ex$)" +
+ "|(^card-?expire$)" +
+ "|(^card-?expiry$)" +
+ "|(^validite$)" +
+ "|(^expiration$)" +
+ "|(^expiry$)" +
+ "|mm-?yy" +
+ "|mm-?yyyy" +
+ "|yy-?mm" +
+ "|yyyy-?mm" +
+ "|expiration-?date" +
+ "|payment-?card-?expiration" +
+ "|(^payment-?cc-?date$)",
+
+ "cc-exp-month":
+ "(^exp-?month$)" +
+ "|(^cc-?exp-?month$)" +
+ "|(^cc-?month$)" +
+ "|(^card-?month$)" +
+ "|(^cc-?mo$)" +
+ "|(^card-?mo$)" +
+ "|(^exp-?mo$)" +
+ "|(^card-?exp-?mo$)" +
+ "|(^cc-?exp-?mo$)" +
+ "|(^card-?expiration-?month$)" +
+ "|(^expiration-?month$)" +
+ "|(^cc-?mm$)" +
+ "|(^cc-?m$)" +
+ "|(^card-?mm$)" +
+ "|(^card-?m$)" +
+ "|(^card-?exp-?mm$)" +
+ "|(^cc-?exp-?mm$)" +
+ "|(^exp-?mm$)" +
+ "|(^exp-?m$)" +
+ "|(^expire-?month$)" +
+ "|(^expire-?mo$)" +
+ "|(^expiry-?month$)" +
+ "|(^expiry-?mo$)" +
+ "|(^card-?expire-?month$)" +
+ "|(^card-?expire-?mo$)" +
+ "|(^card-?expiry-?month$)" +
+ "|(^card-?expiry-?mo$)" +
+ "|(^mois-?validite$)" +
+ "|(^mois-?expiration$)" +
+ "|(^m-?validite$)" +
+ "|(^m-?expiration$)" +
+ "|(^expiry-?date-?field-?month$)" +
+ "|(^expiration-?date-?month$)" +
+ "|(^expiration-?date-?mm$)" +
+ "|(^exp-?mon$)" +
+ "|(^validity-?mo$)" +
+ "|(^exp-?date-?mo$)" +
+ "|(^cb-?date-?mois$)" +
+ "|(^date-?m$)",
+
+ "cc-exp-year":
+ "(^exp-?year$)" +
+ "|(^cc-?exp-?year$)" +
+ "|(^cc-?year$)" +
+ "|(^card-?year$)" +
+ "|(^cc-?yr$)" +
+ "|(^card-?yr$)" +
+ "|(^exp-?yr$)" +
+ "|(^card-?exp-?yr$)" +
+ "|(^cc-?exp-?yr$)" +
+ "|(^card-?expiration-?year$)" +
+ "|(^expiration-?year$)" +
+ "|(^cc-?yy$)" +
+ "|(^cc-?y$)" +
+ "|(^card-?yy$)" +
+ "|(^card-?y$)" +
+ "|(^card-?exp-?yy$)" +
+ "|(^cc-?exp-?yy$)" +
+ "|(^exp-?yy$)" +
+ "|(^exp-?y$)" +
+ "|(^cc-?yyyy$)" +
+ "|(^card-?yyyy$)" +
+ "|(^card-?exp-?yyyy$)" +
+ "|(^cc-?exp-?yyyy$)" +
+ "|(^expire-?year$)" +
+ "|(^expire-?yr$)" +
+ "|(^expiry-?year$)" +
+ "|(^expiry-?yr$)" +
+ "|(^card-?expire-?year$)" +
+ "|(^card-?expire-?yr$)" +
+ "|(^card-?expiry-?year$)" +
+ "|(^card-?expiry-?yr$)" +
+ "|(^an-?validite$)" +
+ "|(^an-?expiration$)" +
+ "|(^annee-?validite$)" +
+ "|(^annee-?expiration$)" +
+ "|(^expiry-?date-?field-?year$)" +
+ "|(^expiration-?date-?year$)" +
+ "|(^cb-?date-?ann$)" +
+ "|(^expiration-?date-?yy$)" +
+ "|(^expiration-?date-?yyyy$)" +
+ "|(^validity-?year$)" +
+ "|(^exp-?date-?year$)" +
+ "|(^date-?y$)",
+
+ "cc-type":
+ "(^cc-?type$)" +
+ "|(^card-?type$)" +
+ "|(^card-?brand$)" +
+ "|(^cc-?brand$)" +
+ "|(^cb-?type$)",
+ },
+
+ //=========================================================================
+ // These rules are from Chromium source codes [1]. Most of them
+ // converted to JS format have the same meaning with the original ones except
+ // the first line of "address-level1".
+ // [1] https://source.chromium.org/chromium/chromium/src/+/master:components/autofill/core/common/autofill_regex_constants.cc
+ {
+ // ==== Email ====
+ email:
+ "e.?mail" +
+ "|courriel" + // fr
+ "|correo.*electr(o|ó)nico" + // es-ES
+ "|メールアドレス" + // ja-JP
+ "|Электронной.?Почты" + // ru
+ "|邮件|邮箱" + // zh-CN
+ "|電郵地址" + // zh-TW
+ "|ഇ-മെയില്‍|ഇലക്ട്രോണിക്.?" +
+ "മെയിൽ" + // ml
+ "|ایمیل|پست.*الکترونیک" + // fa
+ "|ईमेल|इलॅक्ट्रॉनिक.?मेल" + // hi
+ "|(\\b|_)eposta(\\b|_)" + // tr
+ "|(?:이메일|전자.?우편|[Ee]-?mail)(.?주소)?", // ko-KR
+
+ // ==== Telephone ====
+ tel:
+ "phone|mobile|contact.?number" +
+ "|telefonnummer" + // de-DE
+ "|telefono|teléfono" + // es
+ "|telfixe" + // fr-FR
+ "|電話" + // ja-JP
+ "|telefone|telemovel" + // pt-BR, pt-PT
+ "|телефон" + // ru
+ "|मोबाइल" + // hi for mobile
+ "|(\\b|_|\\*)telefon(\\b|_|\\*)" + // tr
+ "|电话" + // zh-CN
+ "|മൊബൈല്‍" + // ml for mobile
+ "|(?:전화|핸드폰|휴대폰|휴대전화)(?:.?번호)?", // ko-KR
+
+ // ==== Address Fields ====
+ organization:
+ "company|business|organization|organisation" +
+ // In order to support webkit we convert all negative lookbehinds to a capture group
+ // (?<!not)word -> (?<neg>notword)|word
+ // TODO: Bug 1829583
+ "|(?<neg>confirma)" +
+ "|firma|firmenname" + // de-DE
+ "|empresa" + // es
+ "|societe|société" + // fr-FR
+ "|ragione.?sociale" + // it-IT
+ "|会社" + // ja-JP
+ "|название.?компании" + // ru
+ "|单位|公司" + // zh-CN
+ "|شرکت" + // fa
+ "|회사|직장", // ko-KR
+
+ "street-address": "streetaddress|street-address",
+ "address-line1":
+ "^address$|address[_-]?line(one)?|address1|addr1|street" +
+ "|(?:shipping|billing)address$" +
+ "|strasse|straße|hausnummer|housenumber" + // de-DE
+ "|house.?name" + // en-GB
+ "|direccion|dirección" + // es
+ "|adresse" + // fr-FR
+ "|indirizzo" + // it-IT
+ "|^住所$|住所1" + // ja-JP
+ "|morada" + // pt-BR, pt-PT
+ // In order to support webkit we convert all negative lookbehinds to a capture group
+ // (?<!not)word -> (?<neg>notword)|word
+ // TODO: Bug 1829583
+ "|(?<neg>identificação do endereço)" +
+ "|(endereço)" + // pt-BR, pt-PT
+ "|Адрес" + // ru
+ "|地址" + // zh-CN
+ "|(\\b|_)adres(?! (başlığı(nız)?|tarifi))(\\b|_)" + // tr
+ "|^주소.?$|주소.?1", // ko-KR
+
+ "address-line2":
+ "address[_-]?line(2|two)|address2|addr2|street|suite|unit(?!e)" + // Firefox adds `(?!e)` to unit to skip `United State`
+ "|adresszusatz|ergänzende.?angaben" + // de-DE
+ "|direccion2|colonia|adicional" + // es
+ "|addresssuppl|complementnom|appartement" + // fr-FR
+ "|indirizzo2" + // it-IT
+ "|住所2" + // ja-JP
+ "|complemento|addrcomplement" + // pt-BR, pt-PT
+ "|Улица" + // ru
+ "|地址2" + // zh-CN
+ "|주소.?2", // ko-KR
+
+ "address-line3":
+ "address[_-]?line(3|three)|address3|addr3|street|suite|unit(?!e)" + // Firefox adds `(?!e)` to unit to skip `United State`
+ "|adresszusatz|ergänzende.?angaben" + // de-DE
+ "|direccion3|colonia|adicional" + // es
+ "|addresssuppl|complementnom|appartement" + // fr-FR
+ "|indirizzo3" + // it-IT
+ "|住所3" + // ja-JP
+ "|complemento|addrcomplement" + // pt-BR, pt-PT
+ "|Улица" + // ru
+ "|地址3" + // zh-CN
+ "|주소.?3", // ko-KR
+
+ "address-level2":
+ "city|town" +
+ "|\\bort\\b|stadt" + // de-DE
+ "|suburb" + // en-AU
+ "|ciudad|provincia|localidad|poblacion" + // es
+ "|ville|commune" + // fr-FR
+ "|localita" + // it-IT
+ "|市区町村" + // ja-JP
+ "|cidade" + // pt-BR, pt-PT
+ "|Город" + // ru
+ "|市" + // zh-CN
+ "|分區" + // zh-TW
+ "|شهر" + // fa
+ "|शहर" + // hi for city
+ "|ग्राम|गाँव" + // hi for village
+ "|നഗരം|ഗ്രാമം" + // ml for town|village
+ "|((\\b|_|\\*)([İii̇]l[cç]e(miz|niz)?)(\\b|_|\\*))" + // tr
+ "|^시[^도·・]|시[·・]?군[·・]?구", // ko-KR
+
+ "address-level1":
+ // In order to support webkit we convert all negative lookbehinds to a capture group
+ // (?<!not)word -> (?<neg>notword)|word
+ // TODO: Bug 1829583
+ "(?<neg>united?.state|hist?.state|history?.state)" +
+ "|state|county|region|province" +
+ "|principality" + // en-UK
+ "|都道府県" + // ja-JP
+ "|estado|provincia" + // pt-BR, pt-PT
+ "|область" + // ru
+ "|省" + // zh-CN
+ "|地區" + // zh-TW
+ "|സംസ്ഥാനം" + // ml
+ "|استان" + // fa
+ "|राज्य" + // hi
+ "|((\\b|_|\\*)(eyalet|[şs]ehir|[İii̇]l(imiz)?|kent)(\\b|_|\\*))" + // tr
+ "|^시[·・]?도", // ko-KR
+
+ "postal-code":
+ "zip|postal|post.*code|pcode" +
+ "|pin.?code" + // en-IN
+ "|postleitzahl" + // de-DE
+ "|\\bcp\\b" + // es
+ "|\\bcdp\\b" + // fr-FR
+ "|\\bcap\\b" + // it-IT
+ "|郵便番号" + // ja-JP
+ "|codigo|codpos|\\bcep\\b" + // pt-BR, pt-PT
+ "|Почтовый.?Индекс" + // ru
+ "|पिन.?कोड" + // hi
+ "|പിന്‍കോഡ്" + // ml
+ "|邮政编码|邮编" + // zh-CN
+ "|郵遞區號" + // zh-TW
+ "|(\\b|_)posta kodu(\\b|_)" + // tr
+ "|우편.?번호", // ko-KR
+
+ country:
+ "country|countries" +
+ "|país|pais" + // es
+ "|(\\b|_)land(\\b|_)(?!.*(mark.*))" + // de-DE landmark is a type in india.
+ // In order to support webkit we convert all negative lookbehinds to a capture group
+ // (?<!not)word -> (?<neg>notword)|word
+ // TODO: Bug 1829583
+ "|(?<neg>入国|出国)" +
+ "|国" + // ja-JP
+ "|国家" + // zh-CN
+ "|국가|나라" + // ko-KR
+ "|(\\b|_)(ülke|ulce|ulke)(\\b|_)" + // tr
+ "|کشور", // fa
+
+ // ==== Name Fields ====
+ "cc-name":
+ "card.?(?:holder|owner)|name.*(\\b)?on(\\b)?.*card" +
+ "|^(credit[-\\s]?card|card).*name|cc.?full.?name" +
+ "|karteninhaber" + // de-DE
+ "|nombre.*tarjeta" + // es
+ "|nom.*carte" + // fr-FR
+ "|nome.*cart" + // it-IT
+ "|名前" + // ja-JP
+ "|Имя.*карты" + // ru
+ "|信用卡开户名|开户名|持卡人姓名" + // zh-CN
+ "|持卡人姓名", // zh-TW
+
+ name:
+ "^name|full.?name|your.?name|customer.?name|bill.?name|ship.?name" +
+ "|name.*first.*last|firstandlastname" +
+ "|nombre.*y.*apellidos" + // es
+ "|^nom(?!bre)" + // fr-FR
+ "|お名前|氏名" + // ja-JP
+ "|^nome" + // pt-BR, pt-PT
+ "|نام.*نام.*خانوادگی" + // fa
+ "|姓名" + // zh-CN
+ "|(\\b|_|\\*)ad[ı]? soyad[ı]?(\\b|_|\\*)" + // tr
+ "|성명", // ko-KR
+
+ "given-name":
+ "first.*name|initials|fname|first$|given.*name" +
+ "|vorname" + // de-DE
+ "|nombre" + // es
+ "|forename|prénom|prenom" + // fr-FR
+ "|名" + // ja-JP
+ "|nome" + // pt-BR, pt-PT
+ "|Имя" + // ru
+ "|نام" + // fa
+ "|이름" + // ko-KR
+ "|പേര്" + // ml
+ "|(\\b|_|\\*)(isim|ad|ad(i|ı|iniz|ınız)?)(\\b|_|\\*)" + // tr
+ "|नाम", // hi
+
+ "additional-name":
+ "middle.*name|mname|middle$|middle.*initial|m\\.i\\.|mi$|\\bmi\\b",
+
+ "family-name":
+ "last.*name|lname|surname|last$|secondname|family.*name" +
+ "|nachname" + // de-DE
+ "|apellidos?" + // es
+ "|famille|^nom(?!bre)" + // fr-FR
+ "|cognome" + // it-IT
+ "|姓" + // ja-JP
+ "|apelidos|surename|sobrenome" + // pt-BR, pt-PT
+ "|Фамилия" + // ru
+ "|نام.*خانوادگی" + // fa
+ "|उपनाम" + // hi
+ "|മറുപേര്" + // ml
+ "|(\\b|_|\\*)(soyisim|soyad(i|ı|iniz|ınız)?)(\\b|_|\\*)" + // tr
+ "|\\b성(?:[^명]|\\b)", // ko-KR
+
+ // ==== Credit Card Fields ====
+ // Note: `cc-name` expression has been moved up, above `name`, in
+ // order to handle specialization through ordering.
+ "cc-number":
+ "(add)?(?:card|cc|acct).?(?:number|#|no|num|field)" +
+ // In order to support webkit we convert all negative lookbehinds to a capture group
+ // (?<!not)word -> (?<neg>notword)|word
+ // TODO: Bug 1829583
+ "|(?<neg>telefonnummer|hausnummer|personnummer|fødselsnummer)" + // de-DE, sv-SE, no
+ "|nummer" +
+ "|カード番号" + // ja-JP
+ "|Номер.*карты" + // ru
+ "|信用卡号|信用卡号码" + // zh-CN
+ "|信用卡卡號" + // zh-TW
+ "|카드" + // ko-KR
+ // es/pt/fr
+ "|(numero|número|numéro)(?!.*(document|fono|phone|réservation))",
+
+ "cc-exp-month":
+ "expir|exp.*mo|exp.*date|ccmonth|cardmonth|addmonth" +
+ "|gueltig|gültig|monat" + // de-DE
+ "|fecha" + // es
+ "|date.*exp" + // fr-FR
+ "|scadenza" + // it-IT
+ "|有効期限" + // ja-JP
+ "|validade" + // pt-BR, pt-PT
+ "|Срок действия карты" + // ru
+ "|月", // zh-CN
+
+ "cc-exp-year":
+ "exp|^/|(add)?year" +
+ "|ablaufdatum|gueltig|gültig|jahr" + // de-DE
+ "|fecha" + // es
+ "|scadenza" + // it-IT
+ "|有効期限" + // ja-JP
+ "|validade" + // pt-BR, pt-PT
+ "|Срок действия карты" + // ru
+ "|年|有效期", // zh-CN
+
+ "cc-exp":
+ "expir|exp.*date|^expfield$" +
+ "|gueltig|gültig" + // de-DE
+ "|fecha" + // es
+ "|date.*exp" + // fr-FR
+ "|scadenza" + // it-IT
+ "|有効期限" + // ja-JP
+ "|validade" + // pt-BR, pt-PT
+ "|Срок действия карты", // ru
+
+ "cc-csc":
+ "verification|card.?identification|security.?code|card.?code" +
+ "|security.?value" +
+ "|security.?number|card.?pin|c-v-v" +
+ // We omit this regexp in favor of being less generic.
+ // See "Firefox-specific" rules for cc-csc
+ // "|(cvn|cvv|cvc|csc|cvd|cid|ccv)(field)?" +
+ "|\\bcid\\b",
+ },
+ ],
+
+ LABEL_RULE_SETS: [
+ {
+ "address-line1":
+ "(^\\W*address)" +
+ "|(address\\W*$)" +
+ "|(?:shipping|billing|mailing|pick.?up|drop.?off|delivery|sender|postal|" +
+ "recipient|home|work|office|school|business|mail)[\\s\\-]+address" +
+ "|address\\s+(of|for|to|from)" +
+ "|adresse" + // fr-FR
+ "|indirizzo" + // it-IT
+ "|住所" + // ja-JP
+ "|地址" + // zh-CN
+ "|(\\b|_)adres(?! tarifi)(\\b|_)" + // tr
+ "|주소" + // ko-KR
+ "|^alamat" + // id
+ // Should contain street and any other address component, in any order
+ "|street.*(house|building|apartment|floor)" + // en
+ "|(house|building|apartment|floor).*street" +
+ "|(sokak|cadde).*(apartman|bina|daire|mahalle)" + // tr
+ "|(apartman|bina|daire|mahalle).*(sokak|cadde)" +
+ "|улиц.*(дом|корпус|квартир|этаж)|(дом|корпус|квартир|этаж).*улиц", // ru
+ },
+ {
+ "address-line2":
+ "address|line" +
+ "|adresse" + // fr-FR
+ "|indirizzo" + // it-IT
+ "|地址" + // zh-CN
+ "|주소", // ko-KR
+ },
+ ],
+
+ _getRules(rules, rulesets) {
+ function computeRule(name) {
+ let regexps = [];
+ rulesets.forEach(set => {
+ if (set[name]) {
+ // Add the rule.
+ // We make the regex lower case so that we can match it against the
+ // lower-cased field name and get a rough equivalent of a case-insensitive
+ // match. This avoids a performance cliff with the "iu" flag on regular
+ // expressions.
+ regexps.push(`(${set[name].toLowerCase()})`.normalize("NFKC"));
+ }
+ });
+
+ const value = new RegExp(regexps.join("|"), "gu");
+
+ Object.defineProperty(rules, name, { get: undefined });
+ Object.defineProperty(rules, name, { value });
+ return value;
+ }
+
+ Object.keys(rules).forEach(field =>
+ Object.defineProperty(rules, field, {
+ get() {
+ return computeRule(field);
+ },
+ })
+ );
+
+ return rules;
+ },
+
+ getLabelRules() {
+ return this._getRules(this.LABEL_RULES, this.LABEL_RULE_SETS);
+ },
+
+ getRules() {
+ return this._getRules(this.RULES, this.RULE_SETS);
+ },
+};
+
+export default HeuristicsRegExp;
diff --git a/toolkit/components/formautofill/shared/LabelUtils.sys.mjs b/toolkit/components/formautofill/shared/LabelUtils.sys.mjs
new file mode 100644
index 0000000000..9bfedee105
--- /dev/null
+++ b/toolkit/components/formautofill/shared/LabelUtils.sys.mjs
@@ -0,0 +1,120 @@
+/* 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/. */
+
+/**
+ * This is a utility object to work with HTML labels in web pages,
+ * including finding label elements and label text extraction.
+ */
+export const LabelUtils = {
+ // The tag name list is from Chromium except for "STYLE":
+ // eslint-disable-next-line max-len
+ // https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e
+ EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"],
+
+ // A map object, whose keys are the id's of form fields and each value is an
+ // array consisting of label elements correponding to the id.
+ // @type {Map<string, array>}
+ _mappedLabels: null,
+
+ // An array consisting of label elements whose correponding form field doesn't
+ // have an id attribute.
+ // @type {Array<[HTMLLabelElement, HTMLElement]>}
+ _unmappedLabelControls: null,
+
+ // A weak map consisting of label element and extracted strings pairs.
+ // @type {WeakMap<HTMLLabelElement, array>}
+ _labelStrings: null,
+
+ /**
+ * Extract all strings of an element's children to an array.
+ * "element.textContent" is a string which is merged of all children nodes,
+ * and this function provides an array of the strings contains in an element.
+ *
+ * @param {object} element
+ * A DOM element to be extracted.
+ * @returns {Array}
+ * All strings in an element.
+ */
+ extractLabelStrings(element) {
+ if (this._labelStrings.has(element)) {
+ return this._labelStrings.get(element);
+ }
+ let strings = [];
+ let _extractLabelStrings = el => {
+ if (this.EXCLUDED_TAGS.includes(el.tagName)) {
+ return;
+ }
+
+ if (el.nodeType == el.TEXT_NODE || !el.childNodes.length) {
+ let trimmedText = el.textContent.trim();
+ if (trimmedText) {
+ strings.push(trimmedText);
+ }
+ return;
+ }
+
+ for (let node of el.childNodes) {
+ let nodeType = node.nodeType;
+ if (nodeType != node.ELEMENT_NODE && nodeType != node.TEXT_NODE) {
+ continue;
+ }
+ _extractLabelStrings(node);
+ }
+ };
+ _extractLabelStrings(element);
+ this._labelStrings.set(element, strings);
+ return strings;
+ },
+
+ generateLabelMap(doc) {
+ this._mappedLabels = new Map();
+ this._unmappedLabelControls = [];
+ this._labelStrings = new WeakMap();
+
+ for (let label of doc.querySelectorAll("label")) {
+ let id = label.htmlFor;
+ let control;
+ if (!id) {
+ control = label.control;
+ if (!control) {
+ continue;
+ }
+ id = control.id;
+ }
+ if (id) {
+ let labels = this._mappedLabels.get(id);
+ if (labels) {
+ labels.push(label);
+ } else {
+ this._mappedLabels.set(id, [label]);
+ }
+ } else {
+ // control must be non-empty here
+ this._unmappedLabelControls.push({ label, control });
+ }
+ }
+ },
+
+ clearLabelMap() {
+ this._mappedLabels = null;
+ this._unmappedLabelControls = null;
+ this._labelStrings = null;
+ },
+
+ findLabelElements(element) {
+ if (!this._mappedLabels) {
+ this.generateLabelMap(element.ownerDocument);
+ }
+
+ let id = element.id;
+ if (!id) {
+ return this._unmappedLabelControls
+ .filter(lc => lc.control == element)
+ .map(lc => lc.label);
+ }
+ return this._mappedLabels.get(id) || [];
+ },
+};
+
+export default LabelUtils;