Skip to content

Instantly share code, notes, and snippets.

@asumansenol
Created March 7, 2023 17:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save asumansenol/81d2e855e8a012b7f9745d7876bd412a to your computer and use it in GitHub Desktop.
Save asumansenol/81d2e855e8a012b7f9745d7876bd412a to your computer and use it in GitHub Desktop.
/* 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/. */
/*
* Form Autofill field Heuristics RegExp.
*/
/* exported HeuristicsRegExp */
"use strict";
var HeuristicsRegExp = {
RULES: {
email: undefined,
tel: undefined,
organization: undefined,
"street-address": undefined,
"address-line1": undefined,
"address-line2": undefined,
"address-line3": undefined,
"address-level2": undefined,
"address-level1": undefined,
"postal-code": undefined,
country: undefined,
// Note: We place the `cc-name` field for Credit Card first, because
// it is more specific than the `name` field below and we want to check
// for it before we catch the more generic one.
"cc-name": undefined,
name: undefined,
"given-name": undefined,
"additional-name": undefined,
"family-name": undefined,
"cc-number": undefined,
"cc-exp-month": undefined,
"cc-exp-year": undefined,
"cc-exp": undefined,
"cc-type": undefined
},
RULE_SETS: [
//=========================================================================
// Firefox-specific rules
{
"address-line1": "addrline1|address_1",
"address-line2": "addrline2|address_2",
"address-line3": "addrline3|address_3",
"address-level1": "land", // de-DE
"additional-name": "apellido.?materno|lastlastname",
"cc-number": "(cc|kk)nr", // de-DE
"cc-exp-month": "(cc|kk)month", // de-DE
"cc-exp-year": "(cc|kk)year", // de-DE
"cc-type": "type",
},
//=========================================================================
// These are the rules used by Bitwarden [0], converted into RegExp form.
// [0] https://github.com/bitwarden/browser/blob/c2b8802201fac5e292d55d5caf3f1f78088d823c/src/services/autofill.service.ts#L436
{
email: "(^e-?mail$)|(^email-?address$)",
tel:
"(^phone$)" +
"|(^mobile$)" +
"|(^mobile-?phone$)" +
"|(^tel$)" +
"|(^telephone$)" +
"|(^phone-?number$)",
organization:
"(^company$)" +
"|(^company-?name$)" +
"|(^organization$)" +
"|(^organization-?name$)",
"street-address":
"(^address$)" +
"|(^street-?address$)" +
"|(^addr$)" +
"|(^street$)" +
"|(^mailing-?addr(ess)?$)" + // Modified to not grab lines, below
"|(^billing-?addr(ess)?$)" + // Modified to not grab lines, below
"|(^mail-?addr(ess)?$)" + // Modified to not grab lines, below
"|(^bill-?addr(ess)?$)", // Modified to not grab lines, below
"address-line1":
"(^address-?1$)" +
"|(^address-?line-?1$)" +
"|(^addr-?1$)" +
"|(^street-?1$)",
"address-line2":
"(^address-?2$)" +
"|(^address-?line-?2$)" +
"|(^addr-?2$)" +
"|(^street-?2$)",
"address-line3":
"(^address-?3$)" +
"|(^address-?line-?3$)" +
"|(^addr-?3$)" +
"|(^street-?3$)",
"address-level2":
"(^city$)" +
"|(^town$)" +
"|(^address-?level-?2$)" +
"|(^address-?city$)" +
"|(^address-?town$)",
"address-level1":
"(^state$)" +
"|(^province$)" +
"|(^provence$)" +
"|(^address-?level-?1$)" +
"|(^address-?state$)" +
"|(^address-?province$)",
"postal-code":
"(^postal$)" +
"|(^zip$)" +
"|(^zip2$)" +
"|(^zip-?code$)" +
"|(^postal-?code$)" +
"|(^post-?code$)" +
"|(^address-?zip$)" +
"|(^address-?postal$)" +
"|(^address-?code$)" +
"|(^address-?postal-?code$)" +
"|(^address-?zip-?code$)",
country:
"(^country$)" +
"|(^country-?code$)" +
"|(^country-?name$)" +
"|(^address-?country$)" +
"|(^address-?country-?name$)" +
"|(^address-?country-?code$)",
name: "(^name$)|full-?name|your-?name",
"given-name":
"(^f-?name$)" +
"|(^first-?name$)" +
"|(^given-?name$)" +
"|(^first-?n$)",
"additional-name":
"(^m-?name$)" +
"|(^middle-?name$)" +
"|(^additional-?name$)" +
"|(^middle-?initial$)" +
"|(^middle-?n$)" +
"|(^middle-?i$)",
"family-name":
"(^l-?name$)" +
"|(^last-?name$)" +
"|(^s-?name$)" +
"|(^surname$)" +
"|(^family-?name$)" +
"|(^family-?n$)" +
"|(^last-?n$)",
"cc-name":
"cc-?name" +
"|card-?name" +
"|cardholder-?name" +
"|cardholder" +
// "|(^name$)" + // Removed to avoid overwriting "name", above.
"|(^nom$)",
"cc-number":
"cc-?number" +
"|cc-?num" +
"|card-?number" +
"|card-?num" +
"|(^number$)" +
"|(^cc$)" +
"|cc-?no" +
"|card-?no" +
"|(^credit-?card$)" +
"|numero-?carte" +
"|(^carte$)" +
"|(^carte-?credit$)" +
"|num-?carte" +
"|cb-?num",
"cc-exp":
"(^cc-?exp$)" +
"|(^card-?exp$)" +
"|(^cc-?expiration$)" +
"|(^card-?expiration$)" +
"|(^cc-?ex$)" +
"|(^card-?ex$)" +
"|(^card-?expire$)" +
"|(^card-?expiry$)" +
"|(^validite$)" +
"|(^expiration$)" +
"|(^expiry$)" +
"|mm-?yy" +
"|mm-?yyyy" +
"|yy-?mm" +
"|yyyy-?mm" +
"|expiration-?date" +
"|payment-?card-?expiration" +
"|(^payment-?cc-?date$)",
"cc-exp-month":
"(^exp-?month$)" +
"|(^cc-?exp-?month$)" +
"|(^cc-?month$)" +
"|(^card-?month$)" +
"|(^cc-?mo$)" +
"|(^card-?mo$)" +
"|(^exp-?mo$)" +
"|(^card-?exp-?mo$)" +
"|(^cc-?exp-?mo$)" +
"|(^card-?expiration-?month$)" +
"|(^expiration-?month$)" +
"|(^cc-?mm$)" +
"|(^cc-?m$)" +
"|(^card-?mm$)" +
"|(^card-?m$)" +
"|(^card-?exp-?mm$)" +
"|(^cc-?exp-?mm$)" +
"|(^exp-?mm$)" +
"|(^exp-?m$)" +
"|(^expire-?month$)" +
"|(^expire-?mo$)" +
"|(^expiry-?month$)" +
"|(^expiry-?mo$)" +
"|(^card-?expire-?month$)" +
"|(^card-?expire-?mo$)" +
"|(^card-?expiry-?month$)" +
"|(^card-?expiry-?mo$)" +
"|(^mois-?validite$)" +
"|(^mois-?expiration$)" +
"|(^m-?validite$)" +
"|(^m-?expiration$)" +
"|(^expiry-?date-?field-?month$)" +
"|(^expiration-?date-?month$)" +
"|(^expiration-?date-?mm$)" +
"|(^exp-?mon$)" +
"|(^validity-?mo$)" +
"|(^exp-?date-?mo$)" +
"|(^cb-?date-?mois$)" +
"|(^date-?m$)",
"cc-exp-year":
"(^exp-?year$)" +
"|(^cc-?exp-?year$)" +
"|(^cc-?year$)" +
"|(^card-?year$)" +
"|(^cc-?yr$)" +
"|(^card-?yr$)" +
"|(^exp-?yr$)" +
"|(^card-?exp-?yr$)" +
"|(^cc-?exp-?yr$)" +
"|(^card-?expiration-?year$)" +
"|(^expiration-?year$)" +
"|(^cc-?yy$)" +
"|(^cc-?y$)" +
"|(^card-?yy$)" +
"|(^card-?y$)" +
"|(^card-?exp-?yy$)" +
"|(^cc-?exp-?yy$)" +
"|(^exp-?yy$)" +
"|(^exp-?y$)" +
"|(^cc-?yyyy$)" +
"|(^card-?yyyy$)" +
"|(^card-?exp-?yyyy$)" +
"|(^cc-?exp-?yyyy$)" +
"|(^expire-?year$)" +
"|(^expire-?yr$)" +
"|(^expiry-?year$)" +
"|(^expiry-?yr$)" +
"|(^card-?expire-?year$)" +
"|(^card-?expire-?yr$)" +
"|(^card-?expiry-?year$)" +
"|(^card-?expiry-?yr$)" +
"|(^an-?validite$)" +
"|(^an-?expiration$)" +
"|(^annee-?validite$)" +
"|(^annee-?expiration$)" +
"|(^expiry-?date-?field-?year$)" +
"|(^expiration-?date-?year$)" +
"|(^cb-?date-?ann$)" +
"|(^expiration-?date-?yy$)" +
"|(^expiration-?date-?yyyy$)" +
"|(^validity-?year$)" +
"|(^exp-?date-?year$)" +
"|(^date-?y$)",
"cc-type":
"(^cc-?type$)" +
"|(^card-?type$)" +
"|(^card-?brand$)" +
"|(^cc-?brand$)" +
"|(^cb-?type$)",
},
//=========================================================================
// These rules are from Chromium source codes [1]. Most of them
// converted to JS format have the same meaning with the original ones except
// the first line of "address-level1".
// [1] https://source.chromium.org/chromium/chromium/src/+/master:components/autofill/core/common/autofill_regex_constants.cc
{
// ==== Email ====
email:
"e.?mail" +
"|courriel" + // fr
"|correo.*electr(o|ó)nico" + // es-ES
"|メールアドレス" + // ja-JP
"|Электронной.?Почты" + // ru
"|邮件|邮箱" + // zh-CN
"|電郵地址" + // zh-TW
"|ഇ-മെയില്‍|ഇലക്ട്രോണിക്.?" +
"മെയിൽ" + // ml
"|ایمیل|پست.*الکترونیک" + // fa
"|ईमेल|इलॅक्ट्रॉनिक.?मेल" + // hi
"|(\\b|_)eposta(\\b|_)" + // tr
"|(?:이메일|전자.?우편|[Ee]-?mail)(.?주소)?", // ko-KR
// ==== Telephone ====
tel:
"phone|mobile|contact.?number" +
"|telefonnummer" + // de-DE
"|telefono|teléfono" + // es
"|telfixe" + // fr-FR
"|電話" + // ja-JP
"|telefone|telemovel" + // pt-BR, pt-PT
"|телефон" + // ru
"|मोबाइल" + // hi for mobile
"|(\\b|_|\\*)telefon(\\b|_|\\*)" + // tr
"|电话" + // zh-CN
"|മൊബൈല്‍" + // ml for mobile
"|(?:전화|핸드폰|휴대폰|휴대전화)(?:.?번호)?", // ko-KR
// ==== Address Fields ====
organization:
"company|business|organization|organisation" +
"|(?<!con)firma|firmenname" + // de-DE
"|empresa" + // es
"|societe|société" + // fr-FR
"|ragione.?sociale" + // it-IT
"|会社" + // ja-JP
"|название.?компании" + // ru
"|单位|公司" + // zh-CN
"|شرکت" + // fa
"|회사|직장", // ko-KR
"street-address": "streetaddress|street-address",
"address-line1":
"^address$|address[_-]?line(one)?|address1|addr1|street" +
"|(?:shipping|billing)address$" +
"|strasse|straße|hausnummer|housenumber" + // de-DE
"|house.?name" + // en-GB
"|direccion|dirección" + // es
"|adresse" + // fr-FR
"|indirizzo" + // it-IT
"|^住所$|住所1" + // ja-JP
"|morada|((?<!identificação do )endereço)" + // pt-BR, pt-PT
"|Адрес" + // ru
"|地址" + // zh-CN
"|(\\b|_)adres(?! (başlığı(nız)?|tarifi))(\\b|_)" + // tr
"|^주소.?$|주소.?1", // ko-KR
"address-line2":
"address[_-]?line(2|two)|address2|addr2|street|suite|unit(?!e)" + // Firefox adds `(?!e)` to unit to skip `United State`
"|adresszusatz|ergänzende.?angaben" + // de-DE
"|direccion2|colonia|adicional" + // es
"|addresssuppl|complementnom|appartement" + // fr-FR
"|indirizzo2" + // it-IT
"|住所2" + // ja-JP
"|complemento|addrcomplement" + // pt-BR, pt-PT
"|Улица" + // ru
"|地址2" + // zh-CN
"|주소.?2", // ko-KR
"address-line3":
"address[_-]?line(3|three)|address3|addr3|street|suite|unit(?!e)" + // Firefox adds `(?!e)` to unit to skip `United State`
"|adresszusatz|ergänzende.?angaben" + // de-DE
"|direccion3|colonia|adicional" + // es
"|addresssuppl|complementnom|appartement" + // fr-FR
"|indirizzo3" + // it-IT
"|住所3" + // ja-JP
"|complemento|addrcomplement" + // pt-BR, pt-PT
"|Улица" + // ru
"|地址3" + // zh-CN
"|주소.?3", // ko-KR
"address-level2":
"city|town" +
"|\\bort\\b|stadt" + // de-DE
"|suburb" + // en-AU
"|ciudad|provincia|localidad|poblacion" + // es
"|ville|commune" + // fr-FR
"|localita" + // it-IT
"|市区町村" + // ja-JP
"|cidade" + // pt-BR, pt-PT
"|Город" + // ru
"|市" + // zh-CN
"|分區" + // zh-TW
"|شهر" + // fa
"|शहर" + // hi for city
"|ग्राम|गाँव" + // hi for village
"|നഗരം|ഗ്രാമം" + // ml for town|village
"|((\\b|_|\\*)([İii̇]l[cç]e(miz|niz)?)(\\b|_|\\*))" + // tr
"|^시[^도·・]|시[·・]?군[·・]?구", // ko-KR
"address-level1":
"(?<!(united|hist|history).?)state|county|region|province" +
"|county|principality" + // en-UK
"|都道府県" + // ja-JP
"|estado|provincia" + // pt-BR, pt-PT
"|область" + // ru
"|省" + // zh-CN
"|地區" + // zh-TW
"|സംസ്ഥാനം" + // ml
"|استان" + // fa
"|राज्य" + // hi
"|((\\b|_|\\*)(eyalet|[şs]ehir|[İii̇]l(imiz)?|kent)(\\b|_|\\*))" + // tr
"|^시[·・]?도", // ko-KR
"postal-code":
"zip|postal|post.*code|pcode" +
"|pin.?code" + // en-IN
"|postleitzahl" + // de-DE
"|\\bcp\\b" + // es
"|\\bcdp\\b" + // fr-FR
"|\\bcap\\b" + // it-IT
"|郵便番号" + // ja-JP
"|codigo|codpos|\\bcep\\b" + // pt-BR, pt-PT
"|Почтовый.?Индекс" + // ru
"|पिन.?कोड" + // hi
"|പിന്‍കോഡ്" + // ml
"|邮政编码|邮编" + // zh-CN
"|郵遞區號" + // zh-TW
"|(\\b|_)posta kodu(\\b|_)" + // tr
"|우편.?번호", // ko-KR
country:
"country|countries" +
"|país|pais" + // es
"|(\\b|_)land(\\b|_)(?!.*(mark.*))" + // de-DE landmark is a type in india.
"|(?<!(入|出))国" + // ja-JP
"|国家" + // zh-CN
"|국가|나라" + // ko-KR
"|(\\b|_)(ülke|ulce|ulke)(\\b|_)" + // tr
"|کشور", // fa
// ==== Name Fields ====
"cc-name":
"card.?(?:holder|owner)|name.*(\\b)?on(\\b)?.*card" +
"|(?:card|cc).?name|cc.?full.?name" +
"|karteninhaber" + // de-DE
"|nombre.*tarjeta" + // es
"|nom.*carte" + // fr-FR
"|nome.*cart" + // it-IT
"|名前" + // ja-JP
"|Имя.*карты" + // ru
"|信用卡开户名|开户名|持卡人姓名" + // zh-CN
"|持卡人姓名", // zh-TW
name:
"^name|full.?name|your.?name|customer.?name|bill.?name|ship.?name" +
"|name.*first.*last|firstandlastname" +
"|nombre.*y.*apellidos" + // es
"|^nom(?!bre)" + // fr-FR
"|お名前|氏名" + // ja-JP
"|^nome" + // pt-BR, pt-PT
"|نام.*نام.*خانوادگی" + // fa
"|姓名" + // zh-CN
"|(\\b|_|\\*)ad[ı]? soyad[ı]?(\\b|_|\\*)" + // tr
"|성명", // ko-KR
"given-name":
"first.*name|initials|fname|first$|given.*name" +
"|vorname" + // de-DE
"|nombre" + // es
"|forename|prénom|prenom" + // fr-FR
"|名" + // ja-JP
"|nome" + // pt-BR, pt-PT
"|Имя" + // ru
"|نام" + // fa
"|이름" + // ko-KR
"|പേര്" + // ml
"|(\\b|_|\\*)(isim|ad|ad(i|ı|iniz|ınız)?)(\\b|_|\\*)" + // tr
"|नाम", // hi
"additional-name":
"middle.*name|mname|middle$|middle.*initial|m\\.i\\.|mi$|\\bmi\\b",
"family-name":
"last.*name|lname|surname|last$|secondname|family.*name" +
"|nachname" + // de-DE
"|apellidos?" + // es
"|famille|^nom(?!bre)" + // fr-FR
"|cognome" + // it-IT
"|姓" + // ja-JP
"|apelidos|surename|sobrenome" + // pt-BR, pt-PT
"|Фамилия" + // ru
"|نام.*خانوادگی" + // fa
"|उपनाम" + // hi
"|മറുപേര്" + // ml
"|(\\b|_|\\*)(soyisim|soyad(i|ı|iniz|ınız)?)(\\b|_|\\*)" + // tr
"|\\b성(?:[^명]|\\b)", // ko-KR
// ==== Credit Card Fields ====
// Note: `cc-name` expression has been moved up, above `name`, in
// order to handle specialization through ordering.
"cc-number":
"(add)?(?:card|cc|acct).?(?:number|#|no|num|field)" +
"|(?<!telefon|haus|person|fødsels)nummer" + // de-DE, sv-SE, no
"|カード番号" + // ja-JP
"|Номер.*карты" + // ru
"|信用卡号|信用卡号码" + // zh-CN
"|信用卡卡號" + // zh-TW
"|카드" + // ko-KR
// es/pt/fr
"|(numero|número|numéro)(?!.*(document|fono|phone|réservation))",
"cc-exp-month":
"expir|exp.*mo|exp.*date|ccmonth|cardmonth|addmonth" +
"|gueltig|gültig|monat" + // de-DE
"|fecha" + // es
"|date.*exp" + // fr-FR
"|scadenza" + // it-IT
"|有効期限" + // ja-JP
"|validade" + // pt-BR, pt-PT
"|Срок действия карты" + // ru
"|月", // zh-CN
"cc-exp-year":
"exp|^/|(add)?year" +
"|ablaufdatum|gueltig|gültig|jahr" + // de-DE
"|fecha" + // es
"|scadenza" + // it-IT
"|有効期限" + // ja-JP
"|validade" + // pt-BR, pt-PT
"|Срок действия карты" + // ru
"|年|有效期", // zh-CN
"cc-exp":
"expir|exp.*date|^expfield$" +
"|gueltig|gültig" + // de-DE
"|fecha" + // es
"|date.*exp" + // fr-FR
"|scadenza" + // it-IT
"|有効期限" + // ja-JP
"|validade" + // pt-BR, pt-PT
"|Срок действия карты", // ru
},
],
_getRule(name) {
let rules = [];
this.RULE_SETS.forEach(set => {
if (set[name]) {
rules.push(`(${set[name]})`.normalize("NFKC"));
}
});
const value = new RegExp(rules.join("|"), "iu");
Object.defineProperty(this.RULES, name, { get: undefined });
Object.defineProperty(this.RULES, name, { value });
return value;
},
init() {
Object.keys(this.RULES).forEach(field =>
Object.defineProperty(this.RULES, field, {
get() {
return HeuristicsRegExp._getRule(field);
},
})
);
},
};
HeuristicsRegExp.init();
/* 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 = ["FormAutofill"];
// const { XPCOMUtils } = ChromeUtils.import(
// "resource://gre/modules/XPCOMUtils.jsm"
// );
// const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// XPCOMUtils.defineLazyModuleGetters(this, {
// Region: "resource://gre/modules/Region.jsm",
// });
const ADDRESSES_FIRST_TIME_USE_PREF = "extensions.formautofill.firstTimeUse";
const AUTOFILL_CREDITCARDS_AVAILABLE_PREF =
"extensions.formautofill.creditCards.available";
const CREDITCARDS_USED_STATUS_PREF = "extensions.formautofill.creditCards.used";
const ENABLED_AUTOFILL_ADDRESSES_PREF =
"extensions.formautofill.addresses.enabled";
const ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF =
"extensions.formautofill.addresses.capture.enabled";
const ENABLED_AUTOFILL_CREDITCARDS_PREF =
"extensions.formautofill.creditCards.enabled";
const ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF =
"extensions.formautofill.reauth.enabled";
const AUTOFILL_CREDITCARDS_HIDE_UI_PREF =
"extensions.formautofill.creditCards.hideui";
const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";
// XPCOMUtils.defineLazyPreferenceGetter(
// this,
// "logLevel",
// "extensions.formautofill.loglevel",
// "Warn"
// );
// A logging helper for debug logging to avoid creating Console objects
// or triggering expensive JS -> C++ calls when debug logging is not
// enabled.
//
// Console objects, even natively-implemented ones, can consume a lot of
// memory, and since this code may run in every content process, that
// memory can add up quickly. And, even when debug-level messages are
// being ignored, console.debug() calls can be expensive.
//
// This helper avoids both of those problems by never touching the
// console object unless debug logging is enabled.
function debug() {
if (logLevel.toLowerCase() == "debug") {
this.log.debug(...arguments);
}
}
var FormAutofill = {
ENABLED_AUTOFILL_ADDRESSES_PREF,
ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF,
ENABLED_AUTOFILL_CREDITCARDS_PREF,
ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF,
ADDRESSES_FIRST_TIME_USE_PREF,
CREDITCARDS_USED_STATUS_PREF,
get DEFAULT_REGION() {
return Region.home || "US";
},
get isAutofillEnabled() {
return (
FormAutofill.isAutofillAddressesEnabled ||
this.isAutofillCreditCardsEnabled
);
},
get isAutofillCreditCardsEnabled() {
return (
FormAutofill.isAutofillCreditCardsAvailable &&
FormAutofill._isAutofillCreditCardsEnabled
);
},
// defineLazyLogGetter(scope, logPrefix) {
// scope.debug = debug;
// XPCOMUtils.defineLazyGetter(scope, "log", () => {
// let ConsoleAPI = ChromeUtils.import(
// "resource://gre/modules/Console.jsm",
// {}
// ).ConsoleAPI;
// return new ConsoleAPI({
// maxLogLevelPref: "extensions.formautofill.loglevel",
// prefix: logPrefix,
// });
// });
// },
};
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "isAutofillAddressesEnabled",
// ENABLED_AUTOFILL_ADDRESSES_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "isAutofillAddressesCaptureEnabled",
// ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "isAutofillCreditCardsAvailable",
// AUTOFILL_CREDITCARDS_AVAILABLE_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "_isAutofillCreditCardsEnabled",
// ENABLED_AUTOFILL_CREDITCARDS_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "isAutofillCreditCardsHideUI",
// AUTOFILL_CREDITCARDS_HIDE_UI_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "isAutofillAddressesFirstTimeUse",
// ADDRESSES_FIRST_TIME_USE_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "AutofillCreditCardsUsedStatus",
// CREDITCARDS_USED_STATUS_PREF
// );
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofill,
// "supportedCountries",
// SUPPORTED_COUNTRIES_PREF,
// null,
// null,
// val => val.split(",")
// );
// // XXX: This should be invalidated on intl:app-locales-changed.
// XPCOMUtils.defineLazyGetter(FormAutofill, "countries", () => {
// let availableRegionCodes = Services.intl.getAvailableLocaleDisplayNames(
// "region"
// );
// let displayNames = Services.intl.getRegionDisplayNames(
// undefined,
// availableRegionCodes
// );
// let result = new Map();
// for (let i = 0; i < availableRegionCodes.length; i++) {
// result.set(availableRegionCodes[i].toUpperCase(), displayNames[i]);
// }
// return result;
// });
FormAutofill.isAutofillAddressesEnabled = ENABLED_AUTOFILL_ADDRESSES_PREF;
FormAutofill.isAutofillAddressesCaptureEnabled = ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF;
FormAutofill.isAutofillCreditCardsAvailable = AUTOFILL_CREDITCARDS_AVAILABLE_PREF;
FormAutofill._isAutofillCreditCardsEnabled = ENABLED_AUTOFILL_CREDITCARDS_PREF;
FormAutofill.isAutofillCreditCardsHideUI = AUTOFILL_CREDITCARDS_HIDE_UI_PREF;
FormAutofill.isAutofillAddressesFirstTimeUse = ADDRESSES_FIRST_TIME_USE_PREF;
FormAutofill.AutofillCreditCardsUsedStatus = CREDITCARDS_USED_STATUS_PREF;
FormAutofill.supportedCountries = SUPPORTED_COUNTRIES_PREF;
FormAutofill.countries = '';
/* 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 = ["CreditCard"];
// The list of known and supported credit card network ids ("types")
// This list mirrors the networks from dom/payments/BasicCardPayment.cpp
// and is defined by https://www.w3.org/Payments/card-network-ids
const SUPPORTED_NETWORKS = Object.freeze([
"amex",
"cartebancaire",
"diners",
"discover",
"jcb",
"mastercard",
"mir",
"unionpay",
"visa",
]);
// This lists stores lower cased variations of popular credit card network
// names for matching against strings.
const NETWORK_NAMES = {
"american express": "amex",
"master card": "mastercard",
"union pay": "unionpay",
};
// Based on https://en.wikipedia.org/wiki/Payment_card_number
//
// Notice:
// - CarteBancaire (`4035`, `4360`) is now recognized as Visa.
// - UnionPay (`63--`) is now recognized as Discover.
// This means that the order matters.
// First we'll try to match more specific card,
// and if that doesn't match we'll test against the more generic range.
const CREDIT_CARD_IIN = [
{ type: "amex", start: 34, end: 34, len: 15 },
{ type: "amex", start: 37, end: 37, len: 15 },
{ type: "cartebancaire", start: 4035, end: 4035, len: 16 },
{ type: "cartebancaire", start: 4360, end: 4360, len: 16 },
// We diverge from Wikipedia here, because Diners card
// support length of 14-19.
{ type: "diners", start: 300, end: 305, len: [14, 19] },
{ type: "diners", start: 3095, end: 3095, len: [14, 19] },
{ type: "diners", start: 36, end: 36, len: [14, 19] },
{ type: "diners", start: 38, end: 39, len: [14, 19] },
{ type: "discover", start: 6011, end: 6011, len: [16, 19] },
{ type: "discover", start: 622126, end: 622925, len: [16, 19] },
{ type: "discover", start: 624000, end: 626999, len: [16, 19] },
{ type: "discover", start: 628200, end: 628899, len: [16, 19] },
{ type: "discover", start: 64, end: 65, len: [16, 19] },
{ type: "jcb", start: 3528, end: 3589, len: [16, 19] },
{ type: "mastercard", start: 2221, end: 2720, len: 16 },
{ type: "mastercard", start: 51, end: 55, len: 16 },
{ type: "mir", start: 2200, end: 2204, len: 16 },
{ type: "unionpay", start: 62, end: 62, len: [16, 19] },
{ type: "unionpay", start: 81, end: 81, len: [16, 19] },
{ type: "visa", start: 4, end: 4, len: 16 },
].sort((a, b) => b.start - a.start);
class CreditCard {
/**
* A CreditCard object represents a credit card, with
* number, name, expiration, network, and CCV.
* The number is the only required information when creating
* an object, all other members are optional. The number
* is validated during construction and will throw if invalid.
*
* @param {string} name, optional
* @param {string} number
* @param {string} expirationString, optional
* @param {string|number} expirationMonth, optional
* @param {string|number} expirationYear, optional
* @param {string} network, optional
* @param {string|number} ccv, optional
* @param {string} encryptedNumber, optional
* @throws if number is an invalid credit card number
*/
constructor({
name,
number,
expirationString,
expirationMonth,
expirationYear,
network,
ccv,
encryptedNumber,
}) {
this._name = name;
this._unmodifiedNumber = number;
this._encryptedNumber = encryptedNumber;
this._ccv = ccv;
this.number = number;
let { month, year } = CreditCard.normalizeExpiration({
expirationString,
expirationMonth,
expirationYear,
});
this._expirationMonth = month;
this._expirationYear = year;
this.network = network;
}
set name(value) {
this._name = value;
}
set expirationMonth(value) {
if (typeof value == "undefined") {
this._expirationMonth = undefined;
return;
}
this._expirationMonth = CreditCard.normalizeExpirationMonth(value);
}
get expirationMonth() {
return this._expirationMonth;
}
set expirationYear(value) {
if (typeof value == "undefined") {
this._expirationYear = undefined;
return;
}
this._expirationYear = CreditCard.normalizeExpirationYear(value);
}
get expirationYear() {
return this._expirationYear;
}
set expirationString(value) {
let { month, year } = CreditCard.parseExpirationString(value);
this.expirationMonth = month;
this.expirationYear = year;
}
set ccv(value) {
this._ccv = value;
}
get number() {
return this._number;
}
/**
* Sets the number member of a CreditCard object. If the number
* is not valid according to the Luhn algorithm then the member
* will get set to the empty string before throwing an exception.
*
* @param {string} value
* @throws if the value is an invalid credit card number
*/
set number(value) {
if (value) {
let normalizedNumber = value.replace(/[-\s]/g, "");
// Based on the information on wiki[1], the shortest valid length should be
// 12 digits (Maestro).
// [1] https://en.wikipedia.org/wiki/Payment_card_number
normalizedNumber = normalizedNumber.match(/^\d{12,}$/)
? normalizedNumber
: "";
this._number = normalizedNumber;
} else {
this._number = "";
}
if (value && !this.isValidNumber()) {
this._number = "";
throw new Error("Invalid credit card number");
}
}
get network() {
return this._network;
}
set network(value) {
this._network = value || undefined;
}
// Implements the Luhn checksum algorithm as described at
// http://wikipedia.org/wiki/Luhn_algorithm
// Number digit lengths vary with network, but should fall within 12-19 range. [2]
// More details at https://en.wikipedia.org/wiki/Payment_card_number
isValidNumber() {
if (!this._number) {
return false;
}
// Remove dashes and whitespace
let number = this._number.replace(/[\-\s]/g, "");
let len = number.length;
if (len < 12 || len > 19) {
return false;
}
if (!/^\d+$/.test(number)) {
return false;
}
let total = 0;
for (let i = 0; i < len; i++) {
let ch = parseInt(number[len - i - 1], 10);
if (i % 2 == 1) {
// Double it, add digits together if > 10
ch *= 2;
if (ch > 9) {
ch -= 9;
}
}
total += ch;
}
return total % 10 == 0;
}
/**
* Attempts to match the number against known network identifiers.
*
* @param {string} ccNumber
*
* @returns {string|null}
*/
static getType(ccNumber) {
for (let i = 0; i < CREDIT_CARD_IIN.length; i++) {
const range = CREDIT_CARD_IIN[i];
if (typeof range.len == "number") {
if (range.len != ccNumber.length) {
continue;
}
} else if (
ccNumber.length < range.len[0] ||
ccNumber.length > range.len[1]
) {
continue;
}
const prefixLength = Math.floor(Math.log10(range.start)) + 1;
const prefix = parseInt(ccNumber.substring(0, prefixLength), 10);
if (prefix >= range.start && prefix <= range.end) {
return range.type;
}
}
return null;
}
/**
* Attempts to retrieve a card network identifier based
* on a name.
*
* @param {string|undefined|null} name
*
* @returns {string|null}
*/
static getNetworkFromName(name) {
if (!name) {
return null;
}
let lcName = name
.trim()
.toLowerCase()
.normalize("NFKC");
if (SUPPORTED_NETWORKS.includes(lcName)) {
return lcName;
}
for (let term in NETWORK_NAMES) {
if (lcName.includes(term)) {
return NETWORK_NAMES[term];
}
}
return null;
}
/**
* Returns true if the card number is valid and the
* expiration date has not passed. Otherwise false.
*
* @returns {boolean}
*/
isValid() {
if (!this.isValidNumber()) {
return false;
}
let currentDate = new Date();
let currentYear = currentDate.getFullYear();
if (this._expirationYear > currentYear) {
return true;
}
// getMonth is 0-based, so add 1 because credit cards are 1-based
let currentMonth = currentDate.getMonth() + 1;
return (
this._expirationYear == currentYear &&
this._expirationMonth >= currentMonth
);
}
get maskedNumber() {
return CreditCard.getMaskedNumber(this._number);
}
get longMaskedNumber() {
return CreditCard.getLongMaskedNumber(this._number);
}
/**
* Get credit card display label. It should display masked numbers, the
* cardholder's name, and the expiration date, separated by a commas.
* In addition, the card type is provided in the accessibility label.
*/
static getLabelInfo({ number, name, month, year, type }) {
let formatSelector = ["number"];
if (name) {
formatSelector.push("name");
}
if (month && year) {
formatSelector.push("expiration");
}
let stringId = `credit-card-label-${formatSelector.join("-")}-2`;
return {
id: stringId,
args: {
number: CreditCard.getMaskedNumber(number),
name,
month: month?.toString(),
year: year?.toString(),
type,
},
};
}
/**
* !!! DEPRECATED !!!
* Please use getLabelInfo above, as it allows for localization.
*/
static getLabel({ number, name }) {
let parts = [];
if (number) {
parts.push(CreditCard.getMaskedNumber(number));
}
if (name) {
parts.push(name);
}
return parts.join(", ");
}
static normalizeExpirationMonth(month) {
month = parseInt(month, 10);
if (isNaN(month) || month < 1 || month > 12) {
return undefined;
}
return month;
}
static normalizeExpirationYear(year) {
year = parseInt(year, 10);
if (isNaN(year) || year < 0) {
return undefined;
}
if (year < 100) {
year += 2000;
}
return year;
}
static parseExpirationString(expirationString) {
let rules = [
{
regex: "(\\d{4})[-/](\\d{1,2})",
yearIndex: 1,
monthIndex: 2,
},
{
regex: "(\\d{1,2})[-/](\\d{4})",
yearIndex: 2,
monthIndex: 1,
},
{
regex: "(\\d{1,2})[-/](\\d{1,2})",
},
{
regex: "(\\d{2})(\\d{2})",
},
];
for (let rule of rules) {
let result = new RegExp(`(?:^|\\D)${rule.regex}(?!\\d)`).exec(
expirationString
);
if (!result) {
continue;
}
let year, month;
if (!rule.yearIndex || !rule.monthIndex) {
month = parseInt(result[1], 10);
if (month > 12) {
year = parseInt(result[1], 10);
month = parseInt(result[2], 10);
} else {
year = parseInt(result[2], 10);
}
} else {
year = parseInt(result[rule.yearIndex], 10);
month = parseInt(result[rule.monthIndex], 10);
}
if (month < 1 || month > 12 || (year >= 100 && year < 2000)) {
continue;
}
return { month, year };
}
return { month: undefined, year: undefined };
}
static normalizeExpiration({
expirationString,
expirationMonth,
expirationYear,
}) {
// Only prefer the string version if missing one or both parsed formats.
let parsedExpiration = {};
if (expirationString && (!expirationMonth || !expirationYear)) {
parsedExpiration = CreditCard.parseExpirationString(expirationString);
}
return {
month: CreditCard.normalizeExpirationMonth(
parsedExpiration.month || expirationMonth
),
year: CreditCard.normalizeExpirationYear(
parsedExpiration.year || expirationYear
),
};
}
static formatMaskedNumber(maskedNumber) {
return {
affix: "****",
label: maskedNumber.replace(/^\**/, ""),
};
}
static getMaskedNumber(number) {
return "*".repeat(4) + " " + number.substr(-4);
}
static getLongMaskedNumber(number) {
return "*".repeat(number.length - 4) + number.substr(-4);
}
/*
* Validates the number according to the Luhn algorithm. This
* method does not throw an exception if the number is invalid.
*/
static isValidNumber(number) {
try {
new CreditCard({ number });
} catch (ex) {
return false;
}
return true;
}
static isValidNetwork(network) {
return SUPPORTED_NETWORKS.includes(network);
}
}
CreditCard.SUPPORTED_NETWORKS = SUPPORTED_NETWORKS;
/* 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://autofill/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://autofill/FormAutofill.jsm"
// );
// XPCOMUtils.defineLazyModuleGetters(this, {
// CreditCard: "resource://gre/modules/CreditCard.jsm",
// OSKeyStore: "resource://gre/modules/OSKeyStore.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);
},
ensureLoggedIn(promptMessage) {
return OSKeyStore.ensureLoggedIn(
this._reauthEnabledByUser && promptMessage ? promptMessage : false
);
},
/**
* Get the array of credit card network ids ("types") we expect and offer as valid choices
*
* @returns {Array}
*/
getCreditCardNetworks() {
return 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]);
FormAutofillUtils.stringBundle = FormAutofill.properties;
// XPCOMUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function() {
// return Services.strings.createBundle(
// "chrome://formautofill/locale/formautofill.properties"
// );
// });
FormAutofillUtils.brandBundle = '';
// XPCOMUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function() {
// return Services.strings.createBundle(
// "chrome://branding/locale/brand.properties"
// );
// });
// XPCOMUtils.defineLazyPreferenceGetter(
// FormAutofillUtils,
// "_reauthEnabledByUser",
// "extensions.formautofill.reauth.enabled",
// false
// );
FormAutofillUtils._reauthEnabledByUser = ENABLED_AUTOFILL_CREDITCARDS_REAUTH_PREF;
/* 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/. */
/*
* Form Autofill field heuristics.
*/
"use strict";
var EXPORTED_SYMBOLS = ["FormAutofillHeuristics", "LabelUtils"];
// const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
// const { XPCOMUtils } = ChromeUtils.import(
// "resource://gre/modules/XPCOMUtils.jsm"
// );
// const { FormAutofill } = ChromeUtils.import(
// "resource://autofill/FormAutofill.jsm"
// );
// ChromeUtils.defineModuleGetter(
// this,
// "FormAutofillUtils",
// "resource://autofill/FormAutofillUtils.jsm"
// );
// XPCOMUtils.defineLazyModuleGetters(this, {
// CreditCard: "resource://gre/modules/CreditCard.jsm",
// });
this.log = null;
// FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
const PREF_SECTION_ENABLED = "extensions.formautofill.section.enabled";
const DEFAULT_SECTION_NAME = "-moz-section-default";
/**
* To help us classify sections, we want to know what fields can appear
* multiple times in a row.
* Such fields, like `address-line{X}`, should not break sections.
*/
const MULTI_FIELD_NAMES = [
"address-level3",
"address-level2",
"address-level1",
"tel",
"postal-code",
"email",
"street-address",
];
/**
* A scanner for traversing all elements in a form and retrieving the field
* detail with FormAutofillHeuristics.getInfo function. It also provides a
* cursor (parsingIndex) to indicate which element is waiting for parsing.
*/
class FieldScanner {
/**
* Create a FieldScanner based on form elements with the existing
* fieldDetails.
*
* @param {Array.DOMElement} elements
* The elements from a form for each parser.
*/
constructor(elements, { allowDuplicates = false, sectionEnabled = true }) {
this._elementsWeakRef = new WeakRef(elements);
this.fieldDetails = [];
this._parsingIndex = 0;
this._sections = [];
this._allowDuplicates = allowDuplicates;
this._sectionEnabled = sectionEnabled;
}
get _elements() {
return this._elementsWeakRef.deref();
}
/**
* This cursor means the index of the element which is waiting for parsing.
*
* @returns {number}
* The index of the element which is waiting for parsing.
*/
get parsingIndex() {
return this._parsingIndex;
}
/**
* Move the parsingIndex to the next elements. Any elements behind this index
* means the parsing tasks are finished.
*
* @param {number} index
* The latest index of elements waiting for parsing.
*/
set parsingIndex(index) {
if (index > this._elements.length) {
throw new Error("The parsing index is out of range.");
}
this._parsingIndex = index;
}
/**
* Retrieve the field detail by the index. If the field detail is not ready,
* the elements will be traversed until matching the index.
*
* @param {number} index
* The index of the element that you want to retrieve.
* @returns {Object}
* The field detail at the specific index.
*/
getFieldDetailByIndex(index) {
if (index >= this._elements.length) {
throw new Error(
`The index ${index} is out of range.(${this._elements.length})`
);
}
if (index < this.fieldDetails.length) {
return this.fieldDetails[index];
}
for (let i = this.fieldDetails.length; i < index + 1; i++) {
this.pushDetail();
}
return this.fieldDetails[index];
}
get parsingFinished() {
return this.parsingIndex >= this._elements.length;
}
_pushToSection(name, fieldDetail) {
for (let section of this._sections) {
if (section.name == name) {
section.fieldDetails.push(fieldDetail);
return;
}
}
this._sections.push({
name,
fieldDetails: [fieldDetail],
});
}
_classifySections() {
let fieldDetails = this._sections[0].fieldDetails;
this._sections = [];
let seenTypes = new Set();
let previousType;
let sectionCount = 0;
for (let fieldDetail of fieldDetails) {
if (!fieldDetail.fieldName) {
continue;
}
if (
seenTypes.has(fieldDetail.fieldName) &&
(previousType != fieldDetail.fieldName ||
!MULTI_FIELD_NAMES.includes(fieldDetail.fieldName))
) {
seenTypes.clear();
sectionCount++;
}
previousType = fieldDetail.fieldName;
seenTypes.add(fieldDetail.fieldName);
this._pushToSection(
DEFAULT_SECTION_NAME + "-" + sectionCount,
fieldDetail
);
}
}
/**
* The result is an array contains the sections with its belonging field
* details. If `this._sections` contains one section only with the default
* section name (DEFAULT_SECTION_NAME), `this._classifySections` should be
* able to identify all sections in the heuristic way.
*
* @returns {Array<Object>}
* The array with the sections, and the belonging fieldDetails are in
* each section.
*/
getSectionFieldDetails() {
// When the section feature is disabled, `getSectionFieldDetails` should
// provide a single address and credit card section result.
if (!this._sectionEnabled) {
return this._getFinalDetails(this.fieldDetails);
}
if (!this._sections.length) {
return [];
}
if (
this._sections.length == 1 &&
this._sections[0].name == DEFAULT_SECTION_NAME
) {
this._classifySections();
}
return this._sections.reduce((sections, current) => {
sections.push(...this._getFinalDetails(current.fieldDetails));
return sections;
}, []);
}
/**
* This function will prepare an autocomplete info object with getInfo
* function and push the detail to fieldDetails property.
* Any field will be pushed into `this._sections` based on the section name
* in `autocomplete` attribute.
*
* Any element without the related detail will be used for adding the detail
* to the end of field details.
*/
pushDetail() {
let elementIndex = this.fieldDetails.length;
if (elementIndex >= this._elements.length) {
throw new Error("Try to push the non-existing element info.");
}
let element = this._elements[elementIndex];
let info = FormAutofillHeuristics.getInfo(element);
let fieldInfo = {
fieldName: info ? info.fieldName : "",
element: new WeakRef(element).deref(),
};
if (info && info._reason) {
fieldInfo._reason = info._reason;
}
this.fieldDetails.push(fieldInfo);
this._pushToSection(this._getSectionName(fieldInfo), fieldInfo);
}
_getSectionName(info) {
let names = [];
if (info.section) {
names.push(info.section);
}
if (info.addressType) {
names.push(info.addressType);
}
return names.length ? names.join(" ") : DEFAULT_SECTION_NAME;
}
/**
* When a field detail should be changed its fieldName after parsing, use
* this function to update the fieldName which is at a specific index.
*
* @param {number} index
* The index indicates a field detail to be updated.
* @param {string} fieldName
* The new fieldName
*/
updateFieldName(index, fieldName) {
if (index >= this.fieldDetails.length) {
throw new Error("Try to update the non-existing field detail.");
}
this.fieldDetails[index].fieldName = fieldName;
}
_isSameField(field1, field2) {
return (
field1.section == field2.section &&
field1.addressType == field2.addressType &&
field1.fieldName == field2.fieldName
);
}
/**
* Provide the final field details without invalid field name, and the
* duplicated fields will be removed as well. For the debugging purpose,
* the final `fieldDetails` will include the duplicated fields if
* `_allowDuplicates` is true.
*
* Each item should contain one type of fields only, and the two valid types
* are Address and CreditCard.
*
* @param {Array<Object>} fieldDetails
* The field details for trimming.
* @returns {Array<Object>}
* The array with the field details without invalid field name and
* duplicated fields.
*/
_getFinalDetails(fieldDetails) {
let addressFieldDetails = [];
let creditCardFieldDetails = [];
for (let fieldDetail of fieldDetails) {
let fieldName = fieldDetail.fieldName;
if (FormAutofillUtils.isAddressField(fieldName)) {
addressFieldDetails.push(fieldDetail);
} else if (FormAutofillUtils.isCreditCardField(fieldName)) {
creditCardFieldDetails.push(fieldDetail);
} else {
log.debug(
"Not collecting a field with a unknown fieldName",
fieldDetail
);
}
}
return [
{
type: FormAutofillUtils.SECTION_TYPES.ADDRESS,
fieldDetails: addressFieldDetails,
},
{
type: FormAutofillUtils.SECTION_TYPES.CREDIT_CARD,
fieldDetails: creditCardFieldDetails,
},
]
.map(section => {
if (this._allowDuplicates) {
return section;
}
// Deduplicate each set of fieldDetails
let details = section.fieldDetails;
section.fieldDetails = details.filter((detail, index) => {
let previousFields = details.slice(0, index);
return !previousFields.find(f => this._isSameField(detail, f));
});
return section;
})
.filter(section => !!section.fieldDetails.length);
}
elementExisting(index) {
return index < this._elements.length;
}
}
var LabelUtils = {
// The tag name list is from Chromium except for "STYLE":
// eslint-disable-next-line max-len
// https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e
EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"],
// A map object, whose keys are the id's of form fields and each value is an
// array consisting of label elements correponding to the id.
// @type {Map<string, array>}
_mappedLabels: null,
// An array consisting of label elements whose correponding form field doesn't
// have an id attribute.
// @type {Array<HTMLLabelElement>}
_unmappedLabels: null,
// A weak map consisting of label element and extracted strings pairs.
// @type {WeakMap<HTMLLabelElement, array>}
_labelStrings: null,
/**
* Extract all strings of an element's children to an array.
* "element.textContent" is a string which is merged of all children nodes,
* and this function provides an array of the strings contains in an element.
*
* @param {Object} element
* A DOM element to be extracted.
* @returns {Array}
* All strings in an element.
*/
extractLabelStrings(element) {
if (this._labelStrings.has(element)) {
return this._labelStrings.get(element);
}
let strings = [];
let _extractLabelStrings = el => {
if (this.EXCLUDED_TAGS.includes(el.tagName)) {
return;
}
if (el.nodeType == el.TEXT_NODE || !el.childNodes.length) {
let trimmedText = el.textContent.trim();
if (trimmedText) {
strings.push(trimmedText);
}
return;
}
for (let node of el.childNodes) {
let nodeType = node.nodeType;
if (nodeType != node.ELEMENT_NODE && nodeType != node.TEXT_NODE) {
continue;
}
_extractLabelStrings(node);
}
};
_extractLabelStrings(element);
this._labelStrings.set(element, strings);
return strings;
},
generateLabelMap(doc) {
let mappedLabels = new Map();
let unmappedLabels = [];
for (let label of doc.querySelectorAll("label")) {
let id = label.htmlFor;
if (!id) {
let control = label.control;
if (!control) {
continue;
}
id = control.id;
}
if (id) {
let labels = mappedLabels.get(id);
if (labels) {
labels.push(label);
} else {
mappedLabels.set(id, [label]);
}
} else {
unmappedLabels.push(label);
}
}
this._mappedLabels = mappedLabels;
this._unmappedLabels = unmappedLabels;
this._labelStrings = new WeakMap();
},
clearLabelMap() {
this._mappedLabels = null;
this._unmappedLabels = null;
this._labelStrings = null;
},
findLabelElements(element) {
if (!this._mappedLabels) {
this.generateLabelMap(element.ownerDocument);
}
let id = element.id;
if (!id) {
return this._unmappedLabels.filter(label => label.control == element);
}
return this._mappedLabels.get(id) || [];
},
};
/**
* Returns the autocomplete information of fields according to heuristics.
*/
this.FormAutofillHeuristics = {
RULES: HeuristicsRegExp.RULES,
/**
* Try to find a contiguous sub-array within an array.
*
* @param {Array} array
* @param {Array} subArray
*
* @returns {boolean}
* Return whether subArray was found within the array or not.
*/
_matchContiguousSubArray(array, subArray) {
return array.some((elm, i) =>
subArray.every((sElem, j) => sElem == array[i + j])
);
},
/**
* Try to find the field that is look like a month select.
*
* @param {DOMElement} element
* @returns {boolean}
* Return true if we observe the trait of month select in
* the current element.
*/
_isExpirationMonthLikely(element) {
if (element.className !== "HTMLSelectElement") {
return false;
}
const options = [...element.options];
const desiredValues = Array(12)
.fill(1)
.map((v, i) => v + i);
// The number of month options shouldn't be less than 12 or larger than 13
// including the default option.
if (options.length < 12 || options.length > 13) {
return false;
}
return (
this._matchContiguousSubArray(
options.map(e => +e.value),
desiredValues
) ||
this._matchContiguousSubArray(
options.map(e => +e.label),
desiredValues
)
);
},
/**
* Try to find the field that is look like a year select.
*
* @param {DOMElement} element
* @returns {boolean}
* Return true if we observe the trait of year select in
* the current element.
*/
_isExpirationYearLikely(element) {
if (element.className !== "HTMLSelectElement") {
return false;
}
const options = [...element.options];
// A normal expiration year select should contain at least the last three years
// in the list.
const curYear = new Date().getFullYear();
const desiredValues = Array(3)
.fill(0)
.map((v, i) => v + curYear + i);
return (
this._matchContiguousSubArray(
options.map(e => +e.value),
desiredValues
) ||
this._matchContiguousSubArray(
options.map(e => +e.label),
desiredValues
)
);
},
/**
* Try to match the telephone related fields to the grammar
* list to see if there is any valid telephone set and correct their
* field names.
*
* @param {FieldScanner} fieldScanner
* The current parsing status for all elements
* @returns {boolean}
* Return true if there is any field can be recognized in the parser,
* otherwise false.
*/
_parsePhoneFields(fieldScanner) {
let matchingResult;
const GRAMMARS = this.PHONE_FIELD_GRAMMARS;
for (let i = 0; i < GRAMMARS.length; i++) {
let detailStart = fieldScanner.parsingIndex;
let ruleStart = i;
for (
;
i < GRAMMARS.length &&
GRAMMARS[i][0] &&
fieldScanner.elementExisting(detailStart);
i++, detailStart++
) {
let detail = fieldScanner.getFieldDetailByIndex(detailStart);
if (
!detail ||
GRAMMARS[i][0] != detail.fieldName ||
(detail._reason && detail._reason == "autocomplete")
) {
break;
}
let element = detail.elementWeakRef;
if (!element) {
break;
}
if (
GRAMMARS[i][2] &&
(!element.maxLength || GRAMMARS[i][2] < element.maxLength)
) {
break;
}
}
if (i >= GRAMMARS.length) {
break;
}
if (!GRAMMARS[i][0]) {
matchingResult = {
ruleFrom: ruleStart,
ruleTo: i,
};
break;
}
// Fast rewinding to the next rule.
for (; i < GRAMMARS.length; i++) {
if (!GRAMMARS[i][0]) {
break;
}
}
}
let parsedField = false;
if (matchingResult) {
let { ruleFrom, ruleTo } = matchingResult;
let detailStart = fieldScanner.parsingIndex;
for (let i = ruleFrom; i < ruleTo; i++) {
fieldScanner.updateFieldName(detailStart, GRAMMARS[i][1]);
fieldScanner.parsingIndex++;
detailStart++;
parsedField = true;
}
}
if (fieldScanner.parsingFinished) {
return parsedField;
}
let nextField = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
if (
nextField &&
nextField._reason != "autocomplete" &&
fieldScanner.parsingIndex > 0
) {
const regExpTelExtension = new RegExp(
"\\bext|ext\\b|extension|ramal", // pt-BR, pt-PT
"iu"
);
const previousField = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex - 1
);
const previousFieldType = FormAutofillUtils.getCategoryFromFieldName(
previousField.fieldName
);
if (
previousField &&
previousFieldType == "tel" &&
this._matchRegexp(nextField.elementWeakRef, regExpTelExtension)
) {
fieldScanner.updateFieldName(
fieldScanner.parsingIndex,
"tel-extension"
);
fieldScanner.parsingIndex++;
parsedField = true;
}
}
return parsedField;
},
/**
* Try to find the correct address-line[1-3] sequence and correct their field
* names.
*
* @param {FieldScanner} fieldScanner
* The current parsing status for all elements
* @returns {boolean}
* Return true if there is any field can be recognized in the parser,
* otherwise false.
*/
_parseAddressFields(fieldScanner) {
let parsedFields = false;
const addressLines = ["address-line1", "address-line2", "address-line3"];
// TODO: These address-line* regexps are for the lines with numbers, and
// they are the subset of the regexps in `heuristicsRegexp.js`. We have to
// find a better way to make them consistent.
const addressLineRegexps = {
"address-line1": new RegExp(
"address[_-]?line(1|one)|address1|addr1" +
"|addrline1|address_1" + // Extra rules by Firefox
"|indirizzo1" + // it-IT
"|住所1" + // ja-JP
"|地址1" + // zh-CN
"|주소.?1", // ko-KR
"iu"
),
"address-line2": new RegExp(
"address[_-]?line(2|two)|address2|addr2" +
"|addrline2|address_2" + // Extra rules by Firefox
"|indirizzo2" + // it-IT
"|住所2" + // ja-JP
"|地址2" + // zh-CN
"|주소.?2", // ko-KR
"iu"
),
"address-line3": new RegExp(
"address[_-]?line(3|three)|address3|addr3" +
"|addrline3|address_3" + // Extra rules by Firefox
"|indirizzo3" + // it-IT
"|住所3" + // ja-JP
"|地址3" + // zh-CN
"|주소.?3", // ko-KR
"iu"
),
};
while (!fieldScanner.parsingFinished) {
let detail = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
if (
!detail ||
!addressLines.includes(detail.fieldName) ||
detail._reason == "autocomplete"
) {
// When the field is not related to any address-line[1-3] fields or
// determined by autocomplete attr, it means the parsing process can be
// terminated.
break;
}
const elem = detail.elementWeakRef;
for (let regexp of Object.keys(addressLineRegexps)) {
if (this._matchRegexp(elem, addressLineRegexps[regexp])) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, regexp);
parsedFields = true;
}
}
fieldScanner.parsingIndex++;
}
return parsedFields;
},
/**
* Try to look for expiration date fields and revise the field names if needed.
*
* @param {FieldScanner} fieldScanner
* The current parsing status for all elements
* @returns {boolean}
* Return true if there is any field can be recognized in the parser,
* otherwise false.
*/
_parseCreditCardFields(fieldScanner) {
if (fieldScanner.parsingFinished) {
return false;
}
const savedIndex = fieldScanner.parsingIndex;
const detail = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
// Respect to autocomplete attr
if (!detail || (detail._reason && detail._reason == "autocomplete")) {
return false;
}
const monthAndYearFieldNames = ["cc-exp-month", "cc-exp-year"];
// Skip the uninteresting fields
if (
!["cc-exp", "cc-type", ...monthAndYearFieldNames].includes(
detail.fieldName
)
) {
return false;
}
const element = detail.elementWeakRef
// If we didn't auto-discover type field, check every select for options that
// match credit card network names in value or label.
if (element.className == "HTMLSelectElement") {
for (let option of element.querySelectorAll("option")) {
if (
CreditCard.getNetworkFromName(option.value) ||
CreditCard.getNetworkFromName(option.text)
) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-type");
fieldScanner.parsingIndex++;
return true;
}
}
}
// If the input type is a month picker, then assume it's cc-exp.
if (element.type == "month") {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
fieldScanner.parsingIndex++;
return true;
}
// Don't process the fields if expiration month and expiration year are already
// matched by regex in correct order.
if (
fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex++)
.fieldName == "cc-exp-month" &&
!fieldScanner.parsingFinished &&
fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex++)
.fieldName == "cc-exp-year"
) {
return true;
}
fieldScanner.parsingIndex = savedIndex;
// Determine the field name by checking if the fields are month select and year select
// likely.
if (this._isExpirationMonthLikely(element)) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-month");
fieldScanner.parsingIndex++;
if (!fieldScanner.parsingFinished) {
const nextDetail = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
const nextElement = nextDetail.elementWeakRef;
if (this._isExpirationYearLikely(nextElement)) {
fieldScanner.updateFieldName(
fieldScanner.parsingIndex,
"cc-exp-year"
);
fieldScanner.parsingIndex++;
return true;
}
}
}
fieldScanner.parsingIndex = savedIndex;
// Verify that the following consecutive two fields can match cc-exp-month and cc-exp-year
// respectively.
if (this._findMatchedFieldName(element, ["cc-exp-month"])) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-month");
fieldScanner.parsingIndex++;
if (!fieldScanner.parsingFinished) {
const nextDetail = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
const nextElement = nextDetail.elementWeakRef;
if (this._findMatchedFieldName(nextElement, ["cc-exp-year"])) {
fieldScanner.updateFieldName(
fieldScanner.parsingIndex,
"cc-exp-year"
);
fieldScanner.parsingIndex++;
return true;
}
}
}
fieldScanner.parsingIndex = savedIndex;
// Look for MM and/or YY(YY).
if (this._matchRegexp(element, /^mm$/gi)) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-month");
fieldScanner.parsingIndex++;
if (!fieldScanner.parsingFinished) {
const nextDetail = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
const nextElement = nextDetail.elementWeakRef;
if (this._matchRegexp(nextElement, /^(yy|yyyy)$/)) {
fieldScanner.updateFieldName(
fieldScanner.parsingIndex,
"cc-exp-year"
);
fieldScanner.parsingIndex++;
return true;
}
}
}
fieldScanner.parsingIndex = savedIndex;
// Look for a cc-exp with 2-digit or 4-digit year.
if (
this._matchRegexp(
element,
/(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yy(?:[^y]|$)/gi
) ||
this._matchRegexp(
element,
/(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yyyy(?:[^y]|$)/gi
)
) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
fieldScanner.parsingIndex++;
return true;
}
fieldScanner.parsingIndex = savedIndex;
// Match general cc-exp regexp at last.
if (this._findMatchedFieldName(element, ["cc-exp"])) {
fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
fieldScanner.parsingIndex++;
return true;
}
fieldScanner.parsingIndex = savedIndex;
// Set current field name to null as it failed to match any patterns.
fieldScanner.updateFieldName(fieldScanner.parsingIndex, null);
fieldScanner.parsingIndex++;
return true;
},
/**
* This function should provide all field details of a form which are placed
* in the belonging section. The details contain the autocomplete info
* (e.g. fieldName, section, etc).
*
* `allowDuplicates` is used for the xpcshell-test purpose currently because
* the heuristics should be verified that some duplicated elements still can
* be predicted correctly.
*
* @param {any} formOrDivs
* the elements in this form to be predicted the field info.
* @param {boolean} allowDuplicates
* true to remain any duplicated field details otherwise to remove the
* duplicated ones.
* @returns {Array<Array<Object>>}
* all sections within its field details in the form.
*/
getFormInfo(formOrDivs, allowDuplicates = false) {
let eligibleFields;
if(formOrDivs.length>0){
eligibleFields = Array.from(formOrDivs).filter(elem =>
FormAutofillUtils.isFieldEligibleForAutofill(elem)
);
}else{
eligibleFields = Array.from(formOrDivs.elements).filter(elem =>
FormAutofillUtils.isFieldEligibleForAutofill(elem)
);
}
if (eligibleFields.length <= 0) {
return [];
}
let fieldScanner = new FieldScanner(eligibleFields, {
allowDuplicates,
sectionEnabled: this._sectionEnabled,
});
while (!fieldScanner.parsingFinished) {
let parsedPhoneFields = this._parsePhoneFields(fieldScanner);
let parsedAddressFields = this._parseAddressFields(fieldScanner);
let parsedExpirationDateFields = this._parseCreditCardFields(
fieldScanner
);
// If there is no any field parsed, the parsing cursor can be moved
// forward to the next one.
if (
!parsedPhoneFields &&
!parsedAddressFields &&
!parsedExpirationDateFields
) {
fieldScanner.parsingIndex++;
}
}
LabelUtils.clearLabelMap();
return fieldScanner.getSectionFieldDetails();
},
_regExpTableHashValue(...signBits) {
return signBits.reduce((p, c, i) => p | (!!c << i), 0);
},
_setRegExpListCache(regexps, b0, b1, b2) {
if (!this._regexpList) {
this._regexpList = [];
}
this._regexpList[this._regExpTableHashValue(b0, b1, b2)] = regexps;
},
_getRegExpListCache(b0, b1, b2) {
if (!this._regexpList) {
return null;
}
return this._regexpList[this._regExpTableHashValue(b0, b1, b2)] || null;
},
_getRegExpList(isAutoCompleteOff, elementTagName) {
let isSelectElem = elementTagName == "SELECT";
let regExpListCache = this._getRegExpListCache(
false, // isAutoCompleteOff,
FormAutofill.isAutofillCreditCardsAvailable,
isSelectElem
);
if (regExpListCache) {
return regExpListCache;
}
const FIELDNAMES_IGNORING_AUTOCOMPLETE_OFF = [
"cc-name",
"cc-number",
"cc-exp-month",
"cc-exp-year",
"cc-exp",
"cc-type",
];
let regexps = false // isAutoCompleteOff was set to false
? FIELDNAMES_IGNORING_AUTOCOMPLETE_OFF
: Object.keys(this.RULES);
if (!FormAutofill.isAutofillCreditCardsAvailable) {
regexps = regexps.filter(
name => !FormAutofillUtils.isCreditCardField(name)
);
}
if (isSelectElem) {
const FIELDNAMES_FOR_SELECT_ELEMENT = [
"address-level1",
"address-level2",
"country",
"cc-exp-month",
"cc-exp-year",
"cc-exp",
"cc-type",
];
regexps = regexps.filter(name =>
FIELDNAMES_FOR_SELECT_ELEMENT.includes(name)
);
}
this._setRegExpListCache(
regexps,
false, // isAutoCompleteOff was set to false
FormAutofill.isAutofillCreditCardsAvailable,
isSelectElem
);
return regexps;
},
getInfo(element) {
let info = element.getAttribute('autocomplete');
// An input[autocomplete="on"] will not be early return here since it stll
// needs to find the field name.
if (
info &&
info.fieldName &&
info.fieldName != "on" &&
info.fieldName != "off"
) {
info._reason = "autocomplete";
return info;
}
if (!this._prefEnabled) {
return null;
}
let isAutoCompleteOff = false; // isAutoCompleteOff was set to false
// element.autocomplete == "off" ||
// (element.form && element.form.autocomplete == "off");
// "email" type of input is accurate for heuristics to determine its Email
// field or not. However, "tel" type is used for ZIP code for some web site
// (e.g. HomeDepot, BestBuy), so "tel" type should be not used for "tel"
// prediction.
if (element.type == "email" && !false) { // isAutoCompleteOff was set to false
return {
fieldName: "email",
section: "",
addressType: "",
contactType: "",
};
}
let regexps = this._getRegExpList(false, element.tagName); // isAutoCompleteOff was set to false
if (!regexps.length) {
return null;
}
let matchedFieldName = this._findMatchedFieldName(element, regexps);
if (matchedFieldName) {
return {
fieldName: matchedFieldName,
section: "",
addressType: "",
contactType: "",
};
}
return null;
},
/**
* @typedef ElementStrings
* @type {object}
* @yield {string} id - element id.
* @yield {string} name - element name.
* @yield {Array<string>} labels - extracted labels.
*/
/**
* Extract all the signature strings of an element.
*
* @param {HTMLElement} element
* @returns {ElementStrings}
*/
_getElementStrings(element) {
return {
*[Symbol.iterator]() {
yield element.id;
yield element.name;
const labels = LabelUtils.findLabelElements(element);
for (let label of labels) {
yield* LabelUtils.extractLabelStrings(label);
}
},
};
},
/**
* Find the first matched field name of the element wih given regex list.
*
* @param {HTMLElement} element
* @param {Array<string>} regexps
* The regex key names that correspond to pattern in the rule list.
* @returns {?string} The first matched field name
*/
_findMatchedFieldName(element, regexps) {
const getElementStrings = this._getElementStrings(element);
for (let regexp of regexps) {
for (let string of getElementStrings) {
if (this.RULES[regexp].test(string)) {
return regexp;
}
}
}
return null;
},
/**
* Determine whether the regexp can match any of element strings.
*
* @param {HTMLElement} element
* @param {RegExp} regexp
*
* @returns {boolean}
*/
_matchRegexp(element, regexp) {
if(element === undefined){
return false;
}
const elemStrings = this._getElementStrings(element);
for (const str of elemStrings) {
if (regexp.test(str)) {
return true;
}
}
return false;
},
/**
* Phone field grammars - first matched grammar will be parsed. Grammars are
* separated by { REGEX_SEPARATOR, FIELD_NONE, 0 }. Suffix and extension are
* parsed separately unless they are necessary parts of the match.
* The following notation is used to describe the patterns:
* <cc> - country code field.
* <ac> - area code field.
* <phone> - phone or prefix.
* <suffix> - suffix.
* <ext> - extension.
* :N means field is limited to N characters, otherwise it is unlimited.
* (pattern <field>)? means pattern is optional and matched separately.
*
* This grammar list from Chromium will be enabled partially once we need to
* support more cases of Telephone fields.
*/
PHONE_FIELD_GRAMMARS: [
// Country code: <cc> Area Code: <ac> Phone: <phone> (- <suffix>
// (Ext: <ext>)?)?
// {REGEX_COUNTRY, FIELD_COUNTRY_CODE, 0},
// {REGEX_AREA, FIELD_AREA_CODE, 0},
// {REGEX_PHONE, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// \( <ac> \) <phone>:3 <suffix>:4 (Ext: <ext>)?
// {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 3},
// {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 3},
// {REGEX_PHONE, FIELD_SUFFIX, 4},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <cc> <ac>:3 - <phone>:3 - <suffix>:4 (Ext: <ext>)?
// {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
// {REGEX_PHONE, FIELD_AREA_CODE, 3},
// {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 3},
// {REGEX_SUFFIX_SEPARATOR, FIELD_SUFFIX, 4},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <cc>:3 <ac>:3 <phone>:3 <suffix>:4 (Ext: <ext>)?
["tel", "tel-country-code", 3],
["tel", "tel-area-code", 3],
["tel", "tel-local-prefix", 3],
["tel", "tel-local-suffix", 4],
[null, null, 0],
// Area Code: <ac> Phone: <phone> (- <suffix> (Ext: <ext>)?)?
// {REGEX_AREA, FIELD_AREA_CODE, 0},
// {REGEX_PHONE, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <ac> <phone>:3 <suffix>:4 (Ext: <ext>)?
// {REGEX_PHONE, FIELD_AREA_CODE, 0},
// {REGEX_PHONE, FIELD_PHONE, 3},
// {REGEX_PHONE, FIELD_SUFFIX, 4},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <cc> \( <ac> \) <phone> (- <suffix> (Ext: <ext>)?)?
// {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
// {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 0},
// {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: \( <ac> \) <phone> (- <suffix> (Ext: <ext>)?)?
// {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
// {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 0},
// {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <cc> - <ac> - <phone> - <suffix> (Ext: <ext>)?
// {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
// {REGEX_PREFIX_SEPARATOR, FIELD_AREA_CODE, 0},
// {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
// {REGEX_SUFFIX_SEPARATOR, FIELD_SUFFIX, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Area code: <ac>:3 Prefix: <prefix>:3 Suffix: <suffix>:4 (Ext: <ext>)?
// {REGEX_AREA, FIELD_AREA_CODE, 3},
// {REGEX_PREFIX, FIELD_PHONE, 3},
// {REGEX_SUFFIX, FIELD_SUFFIX, 4},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <ac> Prefix: <phone> Suffix: <suffix> (Ext: <ext>)?
// {REGEX_PHONE, FIELD_AREA_CODE, 0},
// {REGEX_PREFIX, FIELD_PHONE, 0},
// {REGEX_SUFFIX, FIELD_SUFFIX, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <ac> - <phone>:3 - <suffix>:4 (Ext: <ext>)?
["tel", "tel-area-code", 0],
["tel", "tel-local-prefix", 3],
["tel", "tel-local-suffix", 4],
[null, null, 0],
// Phone: <cc> - <ac> - <phone> (Ext: <ext>)?
// {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
// {REGEX_PREFIX_SEPARATOR, FIELD_AREA_CODE, 0},
// {REGEX_SUFFIX_SEPARATOR, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <ac> - <phone> (Ext: <ext>)?
// {REGEX_AREA, FIELD_AREA_CODE, 0},
// {REGEX_PHONE, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <cc>:3 - <phone>:10 (Ext: <ext>)?
// {REGEX_PHONE, FIELD_COUNTRY_CODE, 3},
// {REGEX_PHONE, FIELD_PHONE, 10},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Ext: <ext>
// {REGEX_EXTENSION, FIELD_EXTENSION, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
// Phone: <phone> (Ext: <ext>)?
// {REGEX_PHONE, FIELD_PHONE, 0},
// {REGEX_SEPARATOR, FIELD_NONE, 0},
],
};
// XPCOMUtils.defineLazyGetter(FormAutofillHeuristics, "RULES", () => {
// let sandbox = {};
// const HEURISTICS_REGEXP = "resource://autofill/content/heuristicsRegexp.js";
// Services.scriptloader.loadSubScript(HEURISTICS_REGEXP, sandbox);
// return sandbox.HeuristicsRegExp.RULES;
// });
// XPCOMUtils.defineLazyGetter(FormAutofillHeuristics, "_prefEnabled", () => {
// return Services.prefs.getBoolPref(PREF_HEURISTICS_ENABLED);
// });
FormAutofillHeuristics._prefEnabled = PREF_HEURISTICS_ENABLED;
// Services.prefs.addObserver(PREF_HEURISTICS_ENABLED, () => {
// FormAutofillHeuristics._prefEnabled = Services.prefs.getBoolPref(
// PREF_HEURISTICS_ENABLED
// );
// });
FormAutofillHeuristics._sectionEnabled = PREF_SECTION_ENABLED;
// XPCOMUtils.defineLazyGetter(FormAutofillHeuristics, "_sectionEnabled", () => {
// return Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
// });
// Services.prefs.addObserver(PREF_SECTION_ENABLED, () => {
// FormAutofillHeuristics._sectionEnabled = Services.prefs.getBoolPref(
// PREF_SECTION_ENABLED
// );
// });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment