Bug 1642060 - Handle Credit Card Type select. r=abr

Differential Revision: https://phabricator.services.mozilla.com/D81015
This commit is contained in:
Zibi Braniecki 2020-07-02 15:36:44 +00:00
parent 608d46df7a
commit ea5b8a7d68
14 changed files with 244 additions and 15 deletions

View File

@ -52,6 +52,10 @@ XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => {
);
});
XPCOMUtils.defineLazyModuleGetters(this, {
CreditCard: "resource://gre/modules/CreditCard.jsm",
});
this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
@ -956,6 +960,35 @@ class FormAutofillCreditCardSection extends FormAutofillSection {
this.adaptFieldMaxLength(profile);
}
computeFillingValue(value, fieldDetail, element) {
if (
fieldDetail.fieldName != "cc-type" ||
ChromeUtils.getClassName(element) !== "HTMLSelectElement"
) {
return value;
}
if (CreditCard.isValidNetwork(value)) {
return value;
}
// Don't save the record when the option value is empty *OR* there
// are multiple options being selected. The empty option is usually
// assumed to be default along with a meaningless text to users.
if (value && element.selectedOptions.length == 1) {
let selectedOption = element.selectedOptions[0];
let networkType =
CreditCard.getNetworkFromName(selectedOption.text) ??
CreditCard.getNetworkFromName(selectedOption.value);
if (networkType) {
return networkType;
}
}
// If we couldn't match the value to any network, we'll
// strip this field when submitting.
return value;
}
/**
* Customize for previewing prorifle.
*

View File

@ -23,6 +23,10 @@ ChromeUtils.defineModuleGetter(
"resource://formautofill/FormAutofillUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
CreditCard: "resource://gre/modules/CreditCard.jsm",
});
this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
@ -709,27 +713,48 @@ this.FormAutofillHeuristics = {
* Return true if there is any field can be recognized in the parser,
* otherwise false.
*/
_parseCreditCardExpirationDateFields(fieldScanner) {
_parseCreditCardFields(fieldScanner) {
if (fieldScanner.parsingFinished) {
return false;
}
const savedIndex = fieldScanner.parsingIndex;
const monthAndYearFieldNames = ["cc-exp-month", "cc-exp-year"];
const detail = fieldScanner.getFieldDetailByIndex(
fieldScanner.parsingIndex
);
const element = detail.elementWeakRef.get();
// Respect to autocomplete attr and skip the uninteresting fields
// 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 (
!detail ||
(detail._reason && detail._reason == "autocomplete") ||
!["cc-exp", ...monthAndYearFieldNames].includes(detail.fieldName)
!["cc-exp", "cc-type", ...monthAndYearFieldNames].includes(
detail.fieldName
)
) {
return false;
}
const element = detail.elementWeakRef.get();
// If we didn't auto-discover type field, check every select for options that
// match credit card network names in value or label.
if (ChromeUtils.getClassName(element) == "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");
@ -881,7 +906,7 @@ this.FormAutofillHeuristics = {
while (!fieldScanner.parsingFinished) {
let parsedPhoneFields = this._parsePhoneFields(fieldScanner);
let parsedAddressFields = this._parseAddressFields(fieldScanner);
let parsedExpirationDateFields = this._parseCreditCardExpirationDateFields(
let parsedExpirationDateFields = this._parseCreditCardFields(
fieldScanner
);
@ -935,6 +960,7 @@ this.FormAutofillHeuristics = {
"cc-exp-month",
"cc-exp-year",
"cc-exp",
"cc-type",
];
let regexps = isAutoCompleteOff
? FIELDNAMES_IGNORING_AUTOCOMPLETE_OFF
@ -954,6 +980,7 @@ this.FormAutofillHeuristics = {
"cc-exp-month",
"cc-exp-year",
"cc-exp",
"cc-type",
];
regexps = regexps.filter(name =>
FIELDNAMES_FOR_SELECT_ELEMENT.includes(name)

View File

@ -588,7 +588,9 @@ class FormAutofillParent extends JSWindowActorParent {
creditCard.record["cc-type"] &&
!CreditCard.isValidNetwork(creditCard.record["cc-type"])
) {
delete creditCard.record["cc-type"];
// Let's reset the credit card to empty, and then network auto-detect will
// pick it up.
creditCard.record["cc-type"] = "";
}
// We'll show the credit card doorhanger if:

View File

@ -35,6 +35,7 @@ var HeuristicsRegExp = {
"cc-exp-month": undefined,
"cc-exp-year": undefined,
"cc-exp": undefined,
"cc-type": undefined,
},
RULE_SETS: [
@ -49,6 +50,7 @@ var HeuristicsRegExp = {
"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",
},
//=========================================================================

View File

@ -15,6 +15,12 @@ runHeuristicsTest(
contactType: "",
fieldName: "cc-name",
},
{
section: "",
addressType: "",
contactType: "",
fieldName: "cc-type",
},
{
section: "",
addressType: "",

View File

@ -130,7 +130,12 @@ runHeuristicsTest(
},
],
[
// {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-type"},
{
section: "",
addressType: "",
contactType: "",
fieldName: "cc-type",
}, // ac-off
{
section: "",
addressType: "",

View File

@ -170,7 +170,12 @@ runHeuristicsTest(
expectedResult: [
[
[
// {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-type"},
{
section: "",
addressType: "",
contactType: "",
fieldName: "cc-type",
}, // ac-off
{
section: "",
addressType: "",

View File

@ -113,7 +113,12 @@ runHeuristicsTest(
},
],
[
// {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-type"}, // ac-off
{
section: "",
addressType: "",
contactType: "",
fieldName: "cc-type",
}, // ac-off
{
section: "",
addressType: "",

View File

@ -21,7 +21,12 @@ runHeuristicsTest(
// {"section": "", "addressType": "", "contactType": "", "fieldName": "bday-year"},
],
[
// {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-type"},
{
section: "",
addressType: "",
contactType: "",
fieldName: "cc-type",
},
{
section: "",
addressType: "",
@ -74,7 +79,12 @@ runHeuristicsTest(
// {"section": "", "addressType": "", "contactType": "", "fieldName": "bday-year"}, // select
],
[
// {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-type"}, // select
{
section: "",
addressType: "",
contactType: "",
fieldName: "cc-type",
}, // ac-off
{
section: "",
addressType: "",

View File

@ -248,16 +248,18 @@ const TESTCASES = [
},
{
description:
"A credit card form with the value of cc-number, cc-exp, and cc-name.",
"A credit card form with the value of cc-number, cc-exp, cc-name and cc-type.",
document: `<form>
<input id="cc-number" autocomplete="cc-number">
<input id="cc-name" autocomplete="cc-name">
<input id="cc-exp" autocomplete="cc-exp">
<input id="cc-type" autocomplete="cc-type">
</form>`,
formValue: {
"cc-number": "5105105105105100",
"cc-name": "Foo Bar",
"cc-exp": "2022-06",
"cc-type": "Visa",
},
expectedRecord: {
address: [],
@ -266,6 +268,7 @@ const TESTCASES = [
"cc-number": "5105105105105100",
"cc-name": "Foo Bar",
"cc-exp": "2022-06",
"cc-type": "Visa",
},
],
},
@ -395,6 +398,52 @@ const TESTCASES = [
],
},
},
{
description: "A credit card form with a cc-type select.",
document: `<form>
<input id="cc-number" autocomplete="cc-number">
<label for="field1">Card Type:</label>
<select id="field1">
<option value="visa" selected>Visa</option>
</select>
</form>`,
formValue: {
"cc-number": "5105105105105100",
},
expectedRecord: {
address: [],
creditCard: [
{
"cc-number": "5105105105105100",
"cc-type": "visa",
},
],
},
},
{
description: "A credit card form with a cc-type select from label.",
document: `<form>
<input id="cc-number" autocomplete="cc-number">
<label for="cc-type">Card Type:</label>
<select id="cc-type">
<option value="V" selected>Visa</option>
<option value="A">American Express</option>
</select>
</form>`,
formValue: {
"cc-number": "5105105105105100",
"cc-type": "A",
},
expectedRecord: {
address: [],
creditCard: [
{
"cc-number": "5105105105105100",
"cc-type": "amex",
},
],
},
},
];
for (let testcase of TESTCASES) {

View File

@ -239,6 +239,20 @@ const TESTCASES = [
contactType: "",
},
},
{
description: "Identify credit card type field",
document: `<form>
<label for="targetElement">Card Type</label>
<input id="targetElement" type="text">
</form>`,
elementId: "targetElement",
expectedReturnValue: {
fieldName: "cc-type",
section: "",
addressType: "",
contactType: "",
},
},
];
TESTCASES.forEach(testcase => {

View File

@ -32,6 +32,12 @@ const MOCK_DOC = MockDocument.createTestDocument(
<input id="cc-number" autocomplete="cc-number">
<input id="cc-exp-month" autocomplete="cc-exp-month">
<input id="cc-exp-year" autocomplete="cc-exp-year">
<select id="cc-type">
<option value="">Select</option>
<option value="visa">Visa</option>
<option value="mastercard">Master Card</option>
<option value="amex">American Express</option>
</select>
<input id="submit" type="submit">
</form>`
);
@ -95,6 +101,7 @@ const TESTCASES = [
"cc-number": "5105105105105100",
"cc-exp-month": 12,
"cc-exp-year": 2000,
"cc-type": "amex",
},
expectedResult: {
formSubmission: true,
@ -108,6 +115,7 @@ const TESTCASES = [
"cc-number": "5105105105105100",
"cc-exp-month": 12,
"cc-exp-year": 2000,
"cc-type": "amex",
},
untouchedFields: [],
},
@ -125,6 +133,7 @@ const TESTCASES = [
"cc-number": "5105105105105100",
"cc-exp-month": 12,
"cc-exp-year": 2000,
"cc-type": "visa",
},
expectedResult: {
formSubmission: true,
@ -151,6 +160,7 @@ const TESTCASES = [
"cc-number": "5105105105105100",
"cc-exp-month": 12,
"cc-exp-year": 2000,
"cc-type": "visa",
},
untouchedFields: [],
},

View File

@ -21,6 +21,14 @@ const SUPPORTED_NETWORKS = Object.freeze([
"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:
@ -231,6 +239,33 @@ class CreditCard {
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.

View File

@ -528,3 +528,29 @@ add_task(async function test_getType() {
);
}
});
add_task(async function test_getNetworkFromName() {
const RECOGNIZED_NAMES = [
["amex", "amex"],
["American Express", "amex"],
["american express", "amex"],
["mastercard", "mastercard"],
["master card", "mastercard"],
["MasterCard", "mastercard"],
["Master Card", "mastercard"],
["Union Pay", "unionpay"],
["UnionPay", "unionpay"],
["Unionpay", "unionpay"],
["unionpay", "unionpay"],
["Unknown", null],
["", null],
];
for (let [value, type] of RECOGNIZED_NAMES) {
Assert.equal(
CreditCard.getNetworkFromName(value),
type,
`Expected ${value} to be recognized as ${type}`
);
}
});