зеркало из https://github.com/mozilla/gecko-dev.git
1114 строки
35 KiB
JavaScript
1114 строки
35 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/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["FormAutofillUtils", "AddressDataLoader"];
|
|
|
|
const ADDRESS_METADATA_PATH = "resource://formautofill/addressmetadata/";
|
|
const ADDRESS_REFERENCES = "addressReferences.js";
|
|
const ADDRESS_REFERENCES_EXT = "addressReferencesExt.js";
|
|
|
|
const ADDRESSES_COLLECTION_NAME = "addresses";
|
|
const CREDITCARDS_COLLECTION_NAME = "creditCards";
|
|
const MANAGE_ADDRESSES_KEYWORDS = [
|
|
"manageAddressesTitle",
|
|
"addNewAddressTitle",
|
|
];
|
|
const EDIT_ADDRESS_KEYWORDS = [
|
|
"givenName",
|
|
"additionalName",
|
|
"familyName",
|
|
"organization2",
|
|
"streetAddress",
|
|
"state",
|
|
"province",
|
|
"city",
|
|
"country",
|
|
"zip",
|
|
"postalCode",
|
|
"email",
|
|
"tel",
|
|
];
|
|
const MANAGE_CREDITCARDS_KEYWORDS = [
|
|
"manageCreditCardsTitle",
|
|
"addNewCreditCardTitle",
|
|
];
|
|
const EDIT_CREDITCARD_KEYWORDS = [
|
|
"cardNumber",
|
|
"nameOnCard",
|
|
"cardExpiresMonth",
|
|
"cardExpiresYear",
|
|
"cardNetwork",
|
|
];
|
|
const FIELD_STATES = {
|
|
NORMAL: "NORMAL",
|
|
AUTO_FILLED: "AUTO_FILLED",
|
|
PREVIEW: "PREVIEW",
|
|
};
|
|
const SECTION_TYPES = {
|
|
ADDRESS: "address",
|
|
CREDIT_CARD: "creditCard",
|
|
};
|
|
|
|
// 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;
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const { FormAutofill } = ChromeUtils.import(
|
|
"resource://formautofill/FormAutofill.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"CreditCard",
|
|
"resource://gre/modules/CreditCard.jsm"
|
|
);
|
|
|
|
let AddressDataLoader = {
|
|
// 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.
|
|
_dataLoaded: {
|
|
country: false,
|
|
level1: new Set(),
|
|
},
|
|
|
|
/**
|
|
* Load address data and extension script into a sandbox from different paths.
|
|
* @param {string} path
|
|
* The path for address data and extension script. It could be root of the address
|
|
* metadata folder(addressmetadata/) or under specific country(addressmetadata/TW/).
|
|
* @returns {object}
|
|
* A sandbox that contains address data object with properties from extension.
|
|
*/
|
|
_loadScripts(path) {
|
|
let sandbox = {};
|
|
let extSandbox = {};
|
|
|
|
try {
|
|
sandbox = FormAutofillUtils.loadDataFromScript(path + ADDRESS_REFERENCES);
|
|
extSandbox = FormAutofillUtils.loadDataFromScript(
|
|
path + ADDRESS_REFERENCES_EXT
|
|
);
|
|
} catch (e) {
|
|
// Will return only address references if extension loading failed or empty sandbox if
|
|
// address references loading failed.
|
|
return sandbox;
|
|
}
|
|
|
|
if (extSandbox.addressDataExt) {
|
|
for (let key in extSandbox.addressDataExt) {
|
|
let addressDataForKey = sandbox.addressData[key];
|
|
if (!addressDataForKey) {
|
|
addressDataForKey = sandbox.addressData[key] = {};
|
|
}
|
|
|
|
Object.assign(addressDataForKey, extSandbox.addressDataExt[key]);
|
|
}
|
|
}
|
|
return sandbox;
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
_parse(data) {
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
|
|
const properties = [
|
|
"languages",
|
|
"sub_keys",
|
|
"sub_isoids",
|
|
"sub_names",
|
|
"sub_lnames",
|
|
];
|
|
for (let 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
|
|
*/
|
|
_loadData(country, level1 = null) {
|
|
// Load the addressData if needed
|
|
if (!this._dataLoaded.country) {
|
|
this._addressData = this._loadScripts(ADDRESS_METADATA_PATH).addressData;
|
|
this._dataLoaded.country = true;
|
|
}
|
|
if (!level1) {
|
|
return this._parse(this._addressData[`data/${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._loadScripts(`${ADDRESS_METADATA_PATH}${country}/`).addressData
|
|
);
|
|
this._dataLoaded.level1.add(country);
|
|
}
|
|
return this._parse(this._addressData[`data/${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.
|
|
*/
|
|
getData(country, level1 = null) {
|
|
let defaultLocale = this._loadData(country, level1);
|
|
if (!defaultLocale) {
|
|
return null;
|
|
}
|
|
|
|
let countryData = this._parse(this._addressData[`data/${country}`]);
|
|
let locales = [];
|
|
// TODO: Should be able to support multi-locale level 1/ level 2 metadata query
|
|
// in Bug 1421886
|
|
if (countryData.languages) {
|
|
let list = countryData.languages.filter(key => key !== countryData.lang);
|
|
locales = list.map(key =>
|
|
this._parse(this._addressData[`${defaultLocale.id}--${key}`])
|
|
);
|
|
}
|
|
return { defaultLocale, locales };
|
|
},
|
|
};
|
|
|
|
this.FormAutofillUtils = {
|
|
get AUTOFILL_FIELDS_THRESHOLD() {
|
|
return 3;
|
|
},
|
|
|
|
ADDRESSES_COLLECTION_NAME,
|
|
CREDITCARDS_COLLECTION_NAME,
|
|
MANAGE_ADDRESSES_KEYWORDS,
|
|
EDIT_ADDRESS_KEYWORDS,
|
|
MANAGE_CREDITCARDS_KEYWORDS,
|
|
EDIT_CREDITCARD_KEYWORDS,
|
|
MAX_FIELD_VALUE_LENGTH,
|
|
FIELD_STATES,
|
|
SECTION_TYPES,
|
|
|
|
_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",
|
|
},
|
|
|
|
_collators: {},
|
|
_reAlternativeCountryNames: {},
|
|
|
|
isAddressField(fieldName) {
|
|
return (
|
|
!!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName)
|
|
);
|
|
},
|
|
|
|
isCreditCardField(fieldName) {
|
|
return this._fieldNameInfo[fieldName] == "creditCard";
|
|
},
|
|
|
|
isCCNumber(ccNumber) {
|
|
return CreditCard.isValidNumber(ccNumber);
|
|
},
|
|
|
|
/**
|
|
* Get the array of credit card network ids ("types") we expect and offer as valid choices
|
|
*
|
|
* @returns {Array}
|
|
*/
|
|
getCreditCardNetworks() {
|
|
return CreditCard.SUPPORTED_NETWORKS;
|
|
},
|
|
|
|
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
|
|
* @param {string?} addressFields Override the fields which can be displayed, but not the order.
|
|
* @returns {string}
|
|
*/
|
|
getAddressLabel(address, addressFields = null) {
|
|
// 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-name", // Country name
|
|
"postal-code", // Postal code
|
|
"tel", // Phone number
|
|
"email", // Email address
|
|
];
|
|
|
|
address = { ...address };
|
|
let parts = [];
|
|
if (addressFields) {
|
|
let requiredFields = addressFields.trim().split(/\s+/);
|
|
fieldOrder = fieldOrder.filter(name => requiredFields.includes(name));
|
|
}
|
|
if (address["street-address"]) {
|
|
address["-moz-street-address-one-line"] = this.toOneLineAddress(
|
|
address["street-address"]
|
|
);
|
|
}
|
|
for (const fieldName of fieldOrder) {
|
|
let string = address[fieldName];
|
|
if (string) {
|
|
parts.push(string);
|
|
}
|
|
if (parts.length == 2 && !addressFields) {
|
|
break;
|
|
}
|
|
}
|
|
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());
|
|
},
|
|
|
|
/**
|
|
* Compares two addresses, removing internal whitespace
|
|
* @param {string} a The first address to compare
|
|
* @param {string} b The second address to compare
|
|
* @param {array} collators Search collators that will be used for comparison
|
|
* @param {string} [delimiter="\n"] The separator that is used between lines in the address
|
|
* @returns {boolean} True if the addresses are equal, false otherwise
|
|
*/
|
|
compareStreetAddress(a, b, collators, delimiter = "\n") {
|
|
let oneLineA = this._toStreetAddressParts(a, delimiter)
|
|
.map(p => p.replace(/\s/g, ""))
|
|
.join("");
|
|
let oneLineB = this._toStreetAddressParts(b, delimiter)
|
|
.map(p => p.replace(/\s/g, ""))
|
|
.join("");
|
|
return this.strCompare(oneLineA, oneLineB, collators);
|
|
},
|
|
|
|
/**
|
|
* 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];
|
|
}
|
|
}
|
|
},
|
|
|
|
autofillFieldSelector(doc) {
|
|
return doc.querySelectorAll("input, select");
|
|
},
|
|
|
|
ALLOWED_TYPES: ["text", "email", "tel", "number", "month"],
|
|
isFieldEligibleForAutofill(element) {
|
|
let tagName = element.tagName;
|
|
if (tagName == "INPUT") {
|
|
// `element.type` can be recognized as `text`, if it's missing or invalid.
|
|
if (!this.ALLOWED_TYPES.includes(element.type)) {
|
|
return false;
|
|
}
|
|
} else if (tagName != "SELECT") {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
loadDataFromScript(url, sandbox = {}) {
|
|
Services.scriptloader.loadSubScript(url, sandbox);
|
|
return sandbox;
|
|
},
|
|
|
|
/**
|
|
* Get country address data and fallback to US if not found.
|
|
* See AddressDataLoader._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 = AddressDataLoader.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 = AddressDataLoader.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 = AddressDataLoader.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.
|
|
* @returns {array} An array containing several collator objects.
|
|
*/
|
|
getSearchCollators(country) {
|
|
// 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: true,
|
|
sensitivity: "base",
|
|
usage: "search",
|
|
};
|
|
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, Web Payments).
|
|
* 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 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) {
|
|
let countries = countrySpecified
|
|
? [countrySpecified]
|
|
: [...FormAutofill.countries.keys()];
|
|
|
|
for (let 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
|
|
] = [];
|
|
}
|
|
|
|
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) {
|
|
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).alternative_names) {
|
|
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 => s.trim().toLowerCase() == 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));
|
|
},
|
|
|
|
/**
|
|
* 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} addressLevel3Label
|
|
* {string} addressLevel2Label
|
|
* {string} addressLevel1Label
|
|
* {string} postalCodeLabel
|
|
* {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
|
|
addressLevel3Label: dataset.sublocality_name_type || "suburb",
|
|
addressLevel2Label: dataset.locality_name_type || "city",
|
|
addressLevel1Label: 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"),
|
|
postalCodeLabel: dataset.zip_name_type || "postalCode",
|
|
postalCodePattern: dataset.zip,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Localize "data-localization" or "data-localization-region" attributes.
|
|
* @param {Element} element
|
|
* @param {string} attributeName
|
|
*/
|
|
localizeAttributeForElement(element, attributeName) {
|
|
switch (attributeName) {
|
|
case "data-localization": {
|
|
element.textContent = this.stringBundle.GetStringFromName(
|
|
element.getAttribute(attributeName)
|
|
);
|
|
element.removeAttribute(attributeName);
|
|
break;
|
|
}
|
|
case "data-localization-region": {
|
|
let regionCode = element.getAttribute(attributeName);
|
|
element.textContent = Services.intl.getRegionDisplayNames(undefined, [
|
|
regionCode,
|
|
]);
|
|
element.removeAttribute(attributeName);
|
|
return;
|
|
}
|
|
default:
|
|
throw new Error("Unexpected attributeName");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Localize elements with "data-localization" or "data-localization-region" attributes.
|
|
* @param {Element} root
|
|
*/
|
|
localizeMarkup(root) {
|
|
let elements = root.querySelectorAll("[data-localization]");
|
|
for (let element of elements) {
|
|
this.localizeAttributeForElement(element, "data-localization");
|
|
}
|
|
|
|
elements = root.querySelectorAll("[data-localization-region]");
|
|
for (let element of elements) {
|
|
this.localizeAttributeForElement(element, "data-localization-region");
|
|
}
|
|
},
|
|
};
|
|
|
|
this.log = null;
|
|
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
|
|
|
|
XPCOMUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function() {
|
|
return Services.strings.createBundle(
|
|
"chrome://formautofill/locale/formautofill.properties"
|
|
);
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function() {
|
|
return Services.strings.createBundle(
|
|
"chrome://branding/locale/brand.properties"
|
|
);
|
|
});
|