1119 lines
30 KiB
JavaScript
1119 lines
30 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
|
|
|
|
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://gre/modules/shared/PhoneNumber.sys.mjs",
|
|
PhoneNumberNormalizer:
|
|
"resource://gre/modules/shared/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}$`, "i");
|
|
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,
|
|
};
|
|
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;
|
|
}
|
|
}
|