1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
10 ChromeUtils.defineESModuleGetters(lazy, {
11 ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs",
12 CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
13 FormAutofillNameUtils:
14 "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs",
15 OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
16 AddressMetaDataLoader:
17 "resource://gre/modules/shared/AddressMetaDataLoader.sys.mjs",
20 ChromeUtils.defineLazyGetter(
25 ["toolkit/formautofill/formAutofill.ftl", "branding/brand.ftl"],
30 XPCOMUtils.defineLazyServiceGetter(
33 "@mozilla.org/login-manager/crypto/SDR;1",
34 "nsILoginManagerCrypto"
37 export let FormAutofillUtils;
39 const ADDRESSES_COLLECTION_NAME = "addresses";
40 const CREDITCARDS_COLLECTION_NAME = "creditCards";
41 const AUTOFILL_CREDITCARDS_REAUTH_PREF =
42 FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF;
43 const MANAGE_ADDRESSES_L10N_IDS = [
44 "autofill-add-address-title",
45 "autofill-manage-addresses-title",
47 const EDIT_ADDRESS_L10N_IDS = [
48 "autofill-address-name",
49 "autofill-address-organization",
50 "autofill-address-street",
51 "autofill-address-state",
52 "autofill-address-province",
53 "autofill-address-city",
54 "autofill-address-country",
55 "autofill-address-zip",
56 "autofill-address-postal-code",
57 "autofill-address-email",
58 "autofill-address-tel",
59 "autofill-edit-address-title",
60 "autofill-address-neighborhood",
61 "autofill-address-village-township",
62 "autofill-address-island",
63 "autofill-address-townland",
64 "autofill-address-district",
65 "autofill-address-county",
66 "autofill-address-post-town",
67 "autofill-address-suburb",
68 "autofill-address-parish",
69 "autofill-address-prefecture",
70 "autofill-address-area",
71 "autofill-address-do-si",
72 "autofill-address-department",
73 "autofill-address-emirate",
74 "autofill-address-oblast",
75 "autofill-address-pin",
76 "autofill-address-eircode",
77 "autofill-address-country-only",
78 "autofill-cancel-button",
79 "autofill-save-button",
81 const MANAGE_CREDITCARDS_L10N_IDS = [
82 "autofill-add-card-title",
83 "autofill-manage-payment-methods-title",
85 const EDIT_CREDITCARD_L10N_IDS = [
86 "autofill-card-number",
87 "autofill-card-name-on-card",
88 "autofill-card-expires-month",
89 "autofill-card-expires-year",
90 "autofill-card-network",
92 const FIELD_STATES = {
94 AUTO_FILLED: "autofill",
97 const FORM_SUBMISSION_REASON = {
98 FORM_SUBMIT_EVENT: "form-submit-event",
99 FORM_REMOVAL_AFTER_FETCH: "form-removal-after-fetch",
100 IFRAME_PAGEHIDE: "iframe-pagehide",
101 PAGE_NAVIGATION: "page-navigation",
104 const ELIGIBLE_INPUT_TYPES = ["text", "email", "tel", "number", "month"];
106 // The maximum length of data to be saved in a single field for preventing DoS
107 // attacks that fill the user's hard drive(s).
108 const MAX_FIELD_VALUE_LENGTH = 200;
110 FormAutofillUtils = {
111 get AUTOFILL_FIELDS_THRESHOLD() {
115 ADDRESSES_COLLECTION_NAME,
116 CREDITCARDS_COLLECTION_NAME,
117 AUTOFILL_CREDITCARDS_REAUTH_PREF,
118 MANAGE_ADDRESSES_L10N_IDS,
119 EDIT_ADDRESS_L10N_IDS,
120 MANAGE_CREDITCARDS_L10N_IDS,
121 EDIT_CREDITCARD_L10N_IDS,
122 MAX_FIELD_VALUE_LENGTH,
124 FORM_SUBMISSION_REASON,
128 "given-name": "name",
129 "additional-name": "name",
130 "family-name": "name",
131 organization: "organization",
132 "street-address": "address",
133 "address-line1": "address",
134 "address-line2": "address",
135 "address-line3": "address",
136 "address-level1": "address",
137 "address-level2": "address",
138 // DE addresses are often split into street name and house number;
139 // combined they form address-line1
140 "address-streetname": "address",
141 "address-housenumber": "address",
142 "postal-code": "address",
144 "country-name": "address",
146 "tel-country-code": "tel",
147 "tel-national": "tel",
148 "tel-area-code": "tel",
150 "tel-local-prefix": "tel",
151 "tel-local-suffix": "tel",
152 "tel-extension": "tel",
154 "cc-name": "creditCard",
155 "cc-given-name": "creditCard",
156 "cc-additional-name": "creditCard",
157 "cc-family-name": "creditCard",
158 "cc-number": "creditCard",
159 "cc-exp-month": "creditCard",
160 "cc-exp-year": "creditCard",
161 "cc-exp": "creditCard",
162 "cc-type": "creditCard",
163 "cc-csc": "creditCard",
167 _reAlternativeCountryNames: {},
169 isAddressField(fieldName) {
171 !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName)
175 isCreditCardField(fieldName) {
176 return this._fieldNameInfo?.[fieldName] == "creditCard";
179 isCCNumber(ccNumber) {
180 return ccNumber && lazy.CreditCard.isValidNumber(ccNumber);
184 * Get the decrypted value for a string pref.
186 * @param {string} prefName -> The pref whose value is needed.
187 * @param {string} safeDefaultValue -> Value to be returned incase the pref is not yet set.
190 getSecurePref(prefName, safeDefaultValue) {
191 if (Services.prefs.getBoolPref("security.nocertdb", false)) {
195 const encryptedValue = Services.prefs.getStringPref(prefName, "");
196 return encryptedValue === ""
198 : lazy.Crypto.decrypt(encryptedValue);
200 return safeDefaultValue;
205 * Set the pref to the encrypted form of the value.
207 * @param {string} prefName -> The pref whose value is to be set.
208 * @param {string} value -> The value to be set in its encrypted form.
210 setSecurePref(prefName, value) {
211 if (Services.prefs.getBoolPref("security.nocertdb", false)) {
215 const encryptedValue = lazy.Crypto.encrypt(value);
216 Services.prefs.setStringPref(prefName, encryptedValue);
218 Services.prefs.clearUserPref(prefName);
223 * Get whether the OSAuth is enabled or not.
225 * @param {string} prefName -> The name of the pref (creditcards or addresses)
228 getOSAuthEnabled(prefName) {
230 lazy.OSKeyStore.canReauth() &&
231 this.getSecurePref(prefName, "") !== "opt out"
236 * Set whether the OSAuth is enabled or not.
238 * @param {string} prefName -> The pref to encrypt.
239 * @param {boolean} enable -> Whether the pref is to be enabled.
241 setOSAuthEnabled(prefName, enable) {
242 this.setSecurePref(prefName, enable ? null : "opt out");
245 async verifyUserOSAuth(
250 generateKeyIfNotAvailable = true
252 if (!this.getOSAuthEnabled(prefName)) {
253 promptMessage = false;
257 await lazy.OSKeyStore.ensureLoggedIn(
261 generateKeyIfNotAvailable
265 // Since Win throws an exception whereas Mac resolves to false upon cancelling.
266 if (ex.result !== Cr.NS_ERROR_FAILURE) {
274 * Get the array of credit card network ids ("types") we expect and offer as valid choices
278 getCreditCardNetworks() {
279 return lazy.CreditCard.getSupportedNetworks();
282 getCategoryFromFieldName(fieldName) {
283 return this._fieldNameInfo[fieldName];
286 getCategoriesFromFieldNames(fieldNames) {
287 let categories = new Set();
288 for (let fieldName of fieldNames) {
289 let info = this.getCategoryFromFieldName(fieldName);
291 categories.add(info);
294 return Array.from(categories);
297 getCollectionNameFromFieldName(fieldName) {
298 return this.isCreditCardField(fieldName)
299 ? CREDITCARDS_COLLECTION_NAME
300 : ADDRESSES_COLLECTION_NAME;
303 getAddressSeparator() {
304 // The separator should be based on the L10N address format, and using a
305 // white space is a temporary solution.
310 * Get address display label. It should display information separated
313 * @param {object} address
316 getAddressLabel(address) {
317 // TODO: Implement a smarter way for deciding what to display
318 // as option text. Possibly improve the algorithm in
319 // ProfileAutoCompleteResult.sys.mjs and reuse it here.
322 "-moz-street-address-one-line", // Street address
323 "address-level3", // Townland / Neighborhood / Village
324 "address-level2", // City/Town
325 "organization", // Company or organization name
326 "address-level1", // Province/State (Standardized code if possible)
327 "country", // Country name
328 "postal-code", // Postal code
329 "tel", // Phone number
330 "email", // Email address
333 address = { ...address };
335 if (address["street-address"]) {
336 address["-moz-street-address-one-line"] = this.toOneLineAddress(
337 address["street-address"]
341 if (!("name" in address)) {
342 address.name = lazy.FormAutofillNameUtils.joinNameParts({
343 given: address["given-name"],
344 middle: address["additional-name"],
345 family: address["family-name"],
349 for (const fieldName of fieldOrder) {
350 let string = address[fieldName];
355 return parts.join(", ");
359 * Internal method to split an address to multiple parts per the provided delimiter,
360 * removing blank parts.
362 * @param {string} address The address the split
363 * @param {string} [delimiter] The separator that is used between lines in the address
364 * @returns {string[]}
366 _toStreetAddressParts(address, delimiter = "\n") {
367 let array = typeof address == "string" ? address.split(delimiter) : address;
369 if (!Array.isArray(array)) {
372 return array.map(s => (s ? s.trim() : "")).filter(s => s);
376 * Converts a street address to a single line, removing linebreaks marked by the delimiter
378 * @param {string} address The address the convert
379 * @param {string} [delimiter] The separator that is used between lines in the address
382 toOneLineAddress(address, delimiter = "\n") {
383 let addressParts = this._toStreetAddressParts(address, delimiter);
384 return addressParts.join(this.getAddressSeparator());
388 * Returns false if an address is written <number> <street>
389 * and true if an address is written <street> <number>. In the future, this
390 * can be expanded to format an address
392 getAddressReversed(region) {
393 return this.getCountryAddressData(region).address_reversed;
397 * In-place concatenate tel-related components into a single "tel" field and
398 * delete unnecessary fields.
400 * @param {object} address An address record.
402 compressTel(address) {
403 let telCountryCode = address["tel-country-code"] || "";
404 let telAreaCode = address["tel-area-code"] || "";
407 if (address["tel-national"]) {
408 address.tel = telCountryCode + address["tel-national"];
409 } else if (address["tel-local"]) {
410 address.tel = telCountryCode + telAreaCode + address["tel-local"];
411 } else if (address["tel-local-prefix"] && address["tel-local-suffix"]) {
415 address["tel-local-prefix"] +
416 address["tel-local-suffix"];
420 for (let field in address) {
421 if (field != "tel" && this.getCategoryFromFieldName(field) == "tel") {
422 delete address[field];
428 * Determines if an element can be autofilled or not.
430 * @param {HTMLElement} element
431 * @returns {boolean} true if the element can be autofilled
433 isFieldAutofillable(element) {
434 return element && !element.readOnly && !element.disabled;
438 * Determines if an element is visually hidden or not.
440 * @param {HTMLElement} element
441 * @param {boolean} visibilityCheck true to run visiblity check against
442 * element.checkVisibility API. Otherwise, test by only checking
443 * `hidden` and `display` attributes
444 * @returns {boolean} true if the element is visible
446 isFieldVisible(element, visibilityCheck = true) {
449 element.checkVisibility &&
450 !FormAutofillUtils.ignoreVisibilityCheck
453 !element.checkVisibility({
455 checkVisibilityCSS: true,
460 } else if (element.hidden || element.style.display == "none") {
464 return element.getAttribute("aria-hidden") != "true";
468 * Determines if an element is eligible to be used by credit card or address autofill.
470 * @param {HTMLElement} element
471 * @returns {boolean} true if element can be used by credit card or address autofill
473 isCreditCardOrAddressFieldType(element) {
478 if (HTMLInputElement.isInstance(element)) {
479 // `element.type` can be recognized as `text`, if it's missing or invalid.
480 return ELIGIBLE_INPUT_TYPES.includes(element.type);
483 return HTMLSelectElement.isInstance(element);
486 loadDataFromScript(url, sandbox = {}) {
487 Services.scriptloader.loadSubScript(url, sandbox);
492 * Get country address data and fallback to US if not found.
493 * See AddressMetaDataLoader.#loadData for more details of addressData structure.
495 * @param {string} [country=FormAutofill.DEFAULT_REGION]
496 * The country code for requesting specific country's metadata. It'll be
497 * default region if parameter is not set.
498 * @param {string} [level1=null]
499 * Return address level 1/level 2 metadata if parameter is set.
500 * @returns {object|null}
501 * Return metadata of specific region with default locale and other supported
502 * locales. We need to return a default country metadata for layout format
503 * and collator, but for sub-region metadata we'll just return null if not found.
505 getCountryAddressRawData(
506 country = FormAutofill.DEFAULT_REGION,
509 let metadata = lazy.AddressMetaDataLoader.getData(country, level1);
514 // Fallback to default region if we couldn't get data from given country.
515 if (country != FormAutofill.DEFAULT_REGION) {
516 metadata = lazy.AddressMetaDataLoader.getData(
517 FormAutofill.DEFAULT_REGION
522 // TODO: Now we fallback to US if we couldn't get data from default region,
523 // but it could be removed in bug 1423464 if it's not necessary.
525 metadata = lazy.AddressMetaDataLoader.getData("US");
531 * Get country address data with default locale.
533 * @param {string} country
534 * @param {string} level1
535 * @returns {object|null} Return metadata of specific region with default locale.
536 * NOTE: The returned data may be for a default region if the
537 * specified one cannot be found. Callers who only want the specific
538 * region should check the returned country code.
540 getCountryAddressData(country, level1) {
541 let metadata = this.getCountryAddressRawData(country, level1);
542 return metadata && metadata.defaultLocale;
546 * Get country address data with all locales.
548 * @param {string} country
549 * @param {string} level1
550 * @returns {Array<object> | null}
551 * Return metadata of specific region with all the locales.
552 * NOTE: The returned data may be for a default region if the
553 * specified one cannot be found. Callers who only want the specific
554 * region should check the returned country code.
556 getCountryAddressDataWithLocales(country, level1) {
557 let metadata = this.getCountryAddressRawData(country, level1);
558 return metadata && [metadata.defaultLocale, ...metadata.locales];
562 * Get the collators based on the specified country.
564 * @param {string} country The specified country.
565 * @param {object} [options = {}] a list of options for this method
566 * @param {boolean} [options.ignorePunctuation = true] Whether punctuation should be ignored.
567 * @param {string} [options.sensitivity = 'base'] Which differences in the strings should lead to non-zero result values
568 * @param {string} [options.usage = 'search'] Whether the comparison is for sorting or for searching for matching strings
569 * @returns {Array} An array containing several collator objects.
573 { ignorePunctuation = true, sensitivity = "base", usage = "search" } = {}
575 // TODO: Only one language should be used at a time per country. The locale
576 // of the page should be taken into account to do this properly.
577 // We are going to support more countries in bug 1370193 and this
578 // should be addressed when we start to implement that bug.
580 if (!this._collators[country]) {
581 let dataset = this.getCountryAddressData(country);
582 let languages = dataset.languages || [dataset.lang];
588 this._collators[country] = languages.map(
589 lang => new Intl.Collator(lang, options)
592 return this._collators[country];
595 // Based on the list of fields abbreviations in
596 // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata
609 * Parse a country address format string and outputs an array of fields.
610 * Spaces, commas, and other literals are ignored in this implementation.
611 * For example, format string "%A%n%C, %S" should return:
613 * {fieldId: "street-address", newLine: true},
614 * {fieldId: "address-level2"},
615 * {fieldId: "address-level1"},
618 * @param {string} fmt Country address format string
619 * @returns {Array<object>} List of fields
621 parseAddressFormat(fmt) {
623 throw new Error("fmt string is missing.");
626 return fmt.match(/%[^%]/g).reduce((parsed, part) => {
627 // Take the first letter of each segment and try to identify it
628 let fieldId = this.FIELDS_LOOKUP[part[1]];
629 // Early return if cannot identify part.
633 // If a new line is detected, add an attribute to the previous field.
634 if (fieldId == "newLine") {
635 let size = parsed.length;
637 parsed[size - 1].newLine = true;
641 return parsed.concat({ fieldId });
646 * Used to populate dropdowns in the UI (e.g. FormAutofill preferences).
647 * Use findAddressSelectOption for matching a value to a region.
649 * @param {string[]} subKeys An array of regionCode strings
650 * @param {string[]} subIsoids An array of ISO ID strings, if provided will be preferred over the key
651 * @param {string[]} subNames An array of regionName strings
652 * @param {string[]} subLnames An array of latinised regionName strings
653 * @returns {Map?} Returns null if subKeys or subNames are not truthy.
654 * Otherwise, a Map will be returned mapping keys -> names.
656 buildRegionMapIfAvailable(subKeys, subIsoids, subNames, subLnames) {
657 // Not all regions have sub_keys. e.g. DE
661 (!subNames && !subLnames) ||
662 (subNames && subKeys.length != subNames.length) ||
663 (subLnames && subKeys.length != subLnames.length)
668 // Overwrite subKeys with subIsoids, when available
669 if (subIsoids && subIsoids.length && subIsoids.length == subKeys.length) {
670 for (let i = 0; i < subIsoids.length; i++) {
672 subKeys[i] = subIsoids[i];
677 // Apply sub_lnames if sub_names does not exist
678 let names = subNames || subLnames;
679 return new Map(subKeys.map((key, index) => [key, names[index]]));
683 * Parse a require string and outputs an array of fields.
684 * Spaces, commas, and other literals are ignored in this implementation.
685 * For example, a require string "ACS" should return:
686 * ["street-address", "address-level2", "address-level1"]
688 * @param {string} requireString Country address require string
689 * @returns {Array<string>} List of fields
691 parseRequireString(requireString) {
692 if (!requireString) {
693 throw new Error("requireString string is missing.");
696 return requireString.split("").map(fieldId => this.FIELDS_LOOKUP[fieldId]);
700 * Use address data and alternative country name list to identify a country code from a
701 * specified country name.
703 * @param {string} countryName A country name to be identified
704 * @param {string} [countrySpecified] A country code indicating that we only
705 * search its alternative names if specified.
706 * @returns {string} The matching country code.
708 identifyCountryCode(countryName, countrySpecified) {
713 if (lazy.AddressMetaDataLoader.getData(countryName)) {
717 const countries = countrySpecified
719 : [...FormAutofill.countries.keys()];
721 for (const country of countries) {
722 let collators = this.getSearchCollators(country);
723 let metadata = this.getCountryAddressData(country);
724 if (country != metadata.key) {
725 // We hit the fallback logic in getCountryAddressRawData so ignore it as
726 // it's not related to `country` and use the name from l10n instead.
728 id: `data/${country}`,
730 name: FormAutofill.countries.get(country),
733 let alternativeCountryNames = metadata.alternative_names || [
736 let reAlternativeCountryNames = this._reAlternativeCountryNames[country];
737 if (!reAlternativeCountryNames) {
738 reAlternativeCountryNames = this._reAlternativeCountryNames[country] =
742 if (countryName.length == 3) {
743 if (this.strCompare(metadata.alpha_3_code, countryName, collators)) {
748 for (let i = 0; i < alternativeCountryNames.length; i++) {
749 let name = alternativeCountryNames[i];
750 let reName = reAlternativeCountryNames[i];
752 reName = reAlternativeCountryNames[i] = new RegExp(
753 "\\b" + this.escapeRegExp(name) + "\\b",
759 this.strCompare(name, countryName, collators) ||
760 reName.test(countryName)
770 findSelectOption(selectEl, record, fieldName) {
771 if (this.isAddressField(fieldName)) {
772 return this.findAddressSelectOption(selectEl.options, record, fieldName);
774 if (this.isCreditCardField(fieldName)) {
775 return this.findCreditCardSelectOption(selectEl, record, fieldName);
781 * Try to find the abbreviation of the given sub-region name
783 * @param {string[]} subregionValues A list of inferable sub-region values.
784 * @param {string} [country] A country name to be identified.
785 * @returns {string} The matching sub-region abbreviation.
787 getAbbreviatedSubregionName(subregionValues, country) {
788 let values = Array.isArray(subregionValues)
792 let collators = this.getSearchCollators(country);
793 for (let metadata of this.getCountryAddressDataWithLocales(country)) {
797 sub_lnames: subLnames,
800 // Not all regions have sub_keys. e.g. DE
803 // Apply sub_lnames if sub_names does not exist
804 subNames = subNames || subLnames;
806 let speculatedSubIndexes = [];
807 for (const val of values) {
808 let identifiedValue = this.identifyValue(
814 if (identifiedValue) {
815 return identifiedValue;
818 // Predict the possible state by partial-matching if no exact match.
819 [subKeys, subNames].forEach(sub => {
820 speculatedSubIndexes.push(
821 sub.findIndex(token => {
822 let pattern = new RegExp(
823 "\\b" + this.escapeRegExp(token) + "\\b"
826 return pattern.test(val);
831 let subKey = subKeys[speculatedSubIndexes.find(i => !!~i)];
840 * Find the option element from select element.
841 * 1. Try to find the locale using the country from address.
842 * 2. First pass try to find exact match.
843 * 3. Second pass try to identify values from address value and options,
844 * and look for a match.
846 * @param {Array<{text: string, value: string}>} options
847 * @param {object} address
848 * @param {string} fieldName
849 * @returns {DOMElement}
851 findAddressSelectOption(options, address, fieldName) {
852 if (options.length > 512) {
853 // Allow enough space for all countries (roughly 300 distinct values) and all
854 // timezones (roughly 400 distinct values), plus some extra wiggle room.
857 let value = address[fieldName];
862 let collators = this.getSearchCollators(address.country);
864 for (const option of options) {
866 this.strCompare(value, option.value, collators) ||
867 this.strCompare(value, option.text, collators)
874 case "address-level1": {
875 let { country } = address;
876 let identifiedValue = this.getAbbreviatedSubregionName(
880 // No point going any further if we cannot identify value from address level 1
881 if (!identifiedValue) {
884 for (let dataset of this.getCountryAddressDataWithLocales(country)) {
885 let keys = dataset.sub_keys;
887 // Not all regions have sub_keys. e.g. DE
890 // Apply sub_lnames if sub_names does not exist
891 let names = dataset.sub_names || dataset.sub_lnames;
893 // Go through options one by one to find a match.
894 // Also check if any option contain the address-level1 key.
895 let pattern = new RegExp(
896 "\\b" + this.escapeRegExp(identifiedValue) + "\\b",
899 for (const option of options) {
900 let optionValue = this.identifyValue(
906 let optionText = this.identifyValue(
914 identifiedValue === optionValue ||
915 identifiedValue === optionText ||
916 pattern.test(option.value)
925 if (this.getCountryAddressData(value)) {
926 for (const option of options) {
928 this.identifyCountryCode(option.text, value) ||
929 this.identifyCountryCode(option.value, value)
943 * Find the option element from xul menu popups, as used in address capture
946 * This is a proxy to `findAddressSelectOption`, which expects HTML select
947 * DOM nodes and operates on options instead of xul menuitems.
949 * NOTE: This is a temporary solution until Bug 1886949 is landed. This
950 * method will then be removed `findAddressSelectOption` will be used
953 * @param {XULPopupElement} menupopup
954 * @param {object} address
955 * @param {string} fieldName
956 * @returns {XULElement}
958 findAddressSelectOptionWithMenuPopup(menupopup, address, fieldName) {
959 const options = Array.from(menupopup.childNodes).map(menuitem => ({
960 text: menuitem.label,
961 value: menuitem.value,
965 return this.findAddressSelectOption(options, address, fieldName)?.menuitem;
968 findCreditCardSelectOption(selectEl, creditCard, fieldName) {
969 let oneDigitMonth = creditCard["cc-exp-month"]
970 ? creditCard["cc-exp-month"].toString()
972 let twoDigitsMonth = oneDigitMonth ? oneDigitMonth.padStart(2, "0") : null;
973 let fourDigitsYear = creditCard["cc-exp-year"]
974 ? creditCard["cc-exp-year"].toString()
976 let twoDigitsYear = fourDigitsYear ? fourDigitsYear.substr(2, 2) : null;
977 let options = Array.from(selectEl.options);
980 case "cc-exp-month": {
981 if (!oneDigitMonth) {
984 for (let option of options) {
986 [option.text, option.label, option.value].some(s => {
987 let result = /[1-9]\d*/.exec(s);
988 return result && result[0] == oneDigitMonth;
996 case "cc-exp-year": {
997 if (!fourDigitsYear) {
1000 for (let option of options) {
1002 [option.text, option.label, option.value].some(
1003 s => s == twoDigitsYear || s == fourDigitsYear
1012 if (!oneDigitMonth || !fourDigitsYear) {
1016 oneDigitMonth + "/" + twoDigitsYear, // 8/22
1017 oneDigitMonth + "/" + fourDigitsYear, // 8/2022
1018 twoDigitsMonth + "/" + twoDigitsYear, // 08/22
1019 twoDigitsMonth + "/" + fourDigitsYear, // 08/2022
1020 oneDigitMonth + "-" + twoDigitsYear, // 8-22
1021 oneDigitMonth + "-" + fourDigitsYear, // 8-2022
1022 twoDigitsMonth + "-" + twoDigitsYear, // 08-22
1023 twoDigitsMonth + "-" + fourDigitsYear, // 08-2022
1024 twoDigitsYear + "-" + twoDigitsMonth, // 22-08
1025 fourDigitsYear + "-" + twoDigitsMonth, // 2022-08
1026 fourDigitsYear + "/" + oneDigitMonth, // 2022/8
1027 twoDigitsMonth + twoDigitsYear, // 0822
1028 twoDigitsYear + twoDigitsMonth, // 2208
1031 for (let option of options) {
1033 [option.text, option.label, option.value].some(str =>
1034 patterns.some(pattern => str.includes(pattern))
1043 let network = creditCard["cc-type"] || "";
1044 for (let option of options) {
1046 [option.text, option.label, option.value].some(
1047 s => lazy.CreditCard.getNetworkFromName(s) == network
1061 * Try to match value with keys and names, but always return the key.
1062 * If inexactMatch is true, then a substring match is performed, otherwise
1063 * the string must match exactly.
1065 * @param {Array<string>} keys
1066 * @param {Array<string>} names
1067 * @param {string} value
1068 * @param {Array} collators
1069 * @param {bool} inexactMatch
1072 identifyValue(keys, names, value, collators, inexactMatch = false) {
1073 let resultKey = keys.find(key => this.strCompare(value, key, collators));
1078 let index = names.findIndex(name =>
1080 ? this.strInclude(value, name, collators)
1081 : this.strCompare(value, name, collators)
1091 * Compare if two strings are the same.
1095 * @param {Array} collators
1096 * @returns {boolean}
1098 strCompare(a = "", b = "", collators) {
1099 return collators.some(collator => !collator.compare(a, b));
1103 * Determine whether one string(b) may be found within another string(a)
1107 * @param {Array} collators
1108 * @returns {boolean} True if the string is found
1110 strInclude(a = "", b = "", collators) {
1111 const len = a.length - b.length;
1112 for (let i = 0; i <= len; i++) {
1113 if (this.strCompare(a.substring(i, i + b.length), b, collators)) {
1121 * Escaping user input to be treated as a literal string within a regular
1124 * @param {string} string
1127 escapeRegExp(string) {
1128 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1132 * Get formatting information of a given country
1134 * @param {string} country
1137 * {string} addressLevel3L10nId
1138 * {string} addressLevel2L10nId
1139 * {string} addressLevel1L10nId
1140 * {string} postalCodeL10nId
1141 * {object} fieldsOrder
1142 * {string} postalCodePattern
1145 getFormFormat(country) {
1146 let dataset = this.getCountryAddressData(country);
1147 // We hit a country fallback in `getCountryAddressRawData` but it's not relevant here.
1148 if (country != dataset.key) {
1149 // Use a sparse object so the below default values take effect.
1152 * Even though data/ZZ only has address-level2, include the other levels
1153 * in case they are needed for unknown countries. Users can leave the
1154 * unnecessary fields blank which is better than forcing users to enter
1155 * the data in incorrect fields.
1157 fmt: "%N%n%O%n%A%n%C %S %Z",
1161 // When particular values are missing for a country, the
1162 // data/ZZ value should be used instead:
1163 // https://chromium-i18n.appspot.com/ssl-aggregate-address/data/ZZ
1164 addressLevel3L10nId: this.getAddressFieldL10nId(
1165 dataset.sublocality_name_type || "suburb"
1167 addressLevel2L10nId: this.getAddressFieldL10nId(
1168 dataset.locality_name_type || "city"
1170 addressLevel1L10nId: this.getAddressFieldL10nId(
1171 dataset.state_name_type || "province"
1173 addressLevel1Options: this.buildRegionMapIfAvailable(
1179 countryRequiredFields: this.parseRequireString(dataset.require || "AC"),
1180 fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
1181 postalCodeL10nId: this.getAddressFieldL10nId(
1182 dataset.zip_name_type || "postal-code"
1184 postalCodePattern: dataset.zip,
1188 * Converts a Map to an array of objects with `value` and `text` properties ( option like).
1190 * @param {Map} optionsMap
1191 * @returns {Array<{ value: string, text: string }>|null}
1193 optionsMapToArray(optionsMap) {
1194 return optionsMap?.size
1195 ? [...optionsMap].map(([value, text]) => ({ value, text }))
1200 * Get flattened form layout information of a given country
1201 * TODO(Bug 1891730): Remove getFormFormat and use this instead.
1203 * @param {object} record - An object containing at least the 'country' property.
1204 * @returns {Array} Flattened array with the address fiels in order.
1206 getFormLayout(record) {
1207 const formFormat = this.getFormFormat(record.country);
1208 let fieldsInOrder = formFormat.fieldsOrder;
1210 // Add missing fields that are always present but not in the .fmt of addresses
1211 // TODO: extend libaddress later to support this if possible
1216 options: this.optionsMapToArray(FormAutofill.countries),
1219 { fieldId: "tel", type: "tel" },
1220 { fieldId: "email", type: "email" },
1223 const addressLevel1Options = this.optionsMapToArray(
1224 formFormat.addressLevel1Options
1227 const addressLevel1SelectedValue = addressLevel1Options
1228 ? this.findAddressSelectOption(
1229 addressLevel1Options,
1233 : record["address-level1"];
1235 for (const field of fieldsInOrder) {
1236 const flattenedObject = {
1237 fieldId: field.fieldId,
1238 newLine: field.newLine,
1239 l10nId: this.getAddressFieldL10nId(field.fieldId),
1240 required: formFormat.countryRequiredFields.includes(field.fieldId),
1241 value: record[field.fieldId] ?? "",
1242 ...(field.fieldId === "street-address" && {
1243 l10nId: "autofill-address-street",
1246 ...(field.fieldId === "address-level1" && {
1247 l10nId: formFormat.addressLevel1L10nId,
1248 options: addressLevel1Options,
1249 value: addressLevel1SelectedValue,
1251 ...(field.fieldId === "address-level2" && {
1252 l10nId: formFormat.addressLevel2L10nId,
1254 ...(field.fieldId === "address-level3" && {
1255 l10nId: formFormat.addressLevel3L10nId,
1257 ...(field.fieldId === "postal-code" && {
1258 pattern: formFormat.postalCodePattern,
1259 l10nId: formFormat.postalCodeL10nId,
1262 Object.assign(field, flattenedObject);
1265 return fieldsInOrder;
1268 getAddressFieldL10nId(type) {
1269 return "autofill-address-" + type.replace(/_/g, "-");
1274 CC_FATHOM_NATIVE: 2,
1275 isFathomCreditCardsEnabled() {
1276 return this.ccHeuristicsMode != this.CC_FATHOM_NONE;
1280 * Transform the key in FormAutofillConfidences (defined in ChromeUtils.webidl)
1281 * to fathom recognized field type.
1283 * @param {string} key key from FormAutofillConfidences dictionary
1284 * @returns {string} fathom field type
1286 formAutofillConfidencesKeyToCCFieldType(key) {
1288 ccNumber: "cc-number",
1292 ccExpMonth: "cc-exp-month",
1293 ccExpYear: "cc-exp-year",
1298 * Generates the localized os dialog message that
1299 * prompts the user to reauthenticate
1301 * @param {string} msgMac fluent message id for macos clients
1302 * @param {string} msgWin fluent message id for windows clients
1303 * @param {string} msgOther fluent message id for other clients
1304 * @param {string} msgLin (optional) fluent message id for linux clients
1305 * @returns {string} localized os prompt message
1307 reauthOSPromptMessage(msgMac, msgWin, msgOther, msgLin = null) {
1308 const platform = AppConstants.platform;
1319 messageID = msgLin ?? msgOther;
1322 messageID = msgOther;
1324 return lazy.l10n.formatValueSync(messageID);
1328 * Retrieves a unique identifier for a given DOM element.
1329 * Note that the identifier generated by ContentDOMReference is an object but
1330 * this API serializes it to string to make lookup easier.
1332 * @param {Element} element The DOM element from which to generate an identifier.
1333 * @returns {string} A unique identifier for the element.
1335 getElementIdentifier(element) {
1338 id = JSON.stringify(lazy.ContentDOMReference.get(element));
1340 // This is needed because when running in xpc-shell test, we don't have
1341 const entry = Object.entries(this._elementByElementId).find(
1342 e => e[1] == element
1347 id = Services.uuid.generateUUID().toString();
1348 this._elementByElementId[id] = element;
1355 * Maps element identifiers to their corresponding DOM elements.
1356 * Only used when we can't get the identifier via ContentDOMReference,
1357 * for example, xpcshell test.
1359 _elementByElementId: {},
1362 * Retrieves the DOM element associated with the specific identifier.
1363 * The identifier should be generated with the `getElementIdentifier` API
1365 * @param {string} elementId The identifier of the element.
1366 * @returns {Element} The DOM element associated with the given identifier.
1368 getElementByIdentifier(elementId) {
1371 element = lazy.ContentDOMReference.resolve(JSON.parse(elementId));
1373 element = this._elementByElementId[elementId];
1379 * This function is used to determine the frames that can also be autofilled
1380 * when users trigger autofill on the focusd frame.
1382 * Currently we also autofill when for frames that
1384 * 2. is same origin with the top-level.
1385 * 3. is same origin with the frame that triggers autofill.
1387 * @param {BrowsingContext} browsingContext
1388 * frame to be checked whether we can also autofill
1390 isBCSameOriginWithTop(browsingContext) {
1392 browsingContext.top == browsingContext ||
1393 browsingContext.currentWindowGlobal.documentPrincipal.equals(
1394 browsingContext.top.currentWindowGlobal.documentPrincipal
1400 ChromeUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function () {
1401 return Services.strings.createBundle(
1402 "chrome://formautofill/locale/formautofill.properties"
1406 ChromeUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function () {
1407 return Services.strings.createBundle(
1408 "chrome://branding/locale/brand.properties"
1412 XPCOMUtils.defineLazyPreferenceGetter(
1414 "_reauthEnabledByUser",
1415 "extensions.formautofill.reauth.enabled",
1419 XPCOMUtils.defineLazyPreferenceGetter(
1422 "extensions.formautofill.creditCards.heuristics.mode",
1426 XPCOMUtils.defineLazyPreferenceGetter(
1428 "ccFathomConfidenceThreshold",
1429 "extensions.formautofill.creditCards.heuristics.fathom.confidenceThreshold",
1432 pref => parseFloat(pref)
1435 XPCOMUtils.defineLazyPreferenceGetter(
1437 "ccFathomHighConfidenceThreshold",
1438 "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold",
1441 pref => parseFloat(pref)
1444 XPCOMUtils.defineLazyPreferenceGetter(
1446 "ccFathomTestConfidence",
1447 "extensions.formautofill.creditCards.heuristics.fathom.testConfidence",
1450 pref => parseFloat(pref)
1453 // This is only used in iOS
1454 XPCOMUtils.defineLazyPreferenceGetter(
1457 "extensions.formautofill.focusOnAutofill",
1461 // This is only used for testing
1462 XPCOMUtils.defineLazyPreferenceGetter(
1464 "ignoreVisibilityCheck",
1465 "extensions.formautofill.test.ignoreVisibilityCheck",