Commit 303f4384 authored by mathp's avatar mathp Committed by Commit bot

[Autofill] Improve support for various credit card expiration dates.

Supports 2-digit years, and various combinations of mm/yyyy and single digit month combinations (e.g. m/yy).

BUG=626537
TEST=CreditCardTest,PersonalDataManagerTest

Review-Url: https://codereview.chromium.org/2136453003
Cr-Commit-Position: refs/heads/master@{#404467}
parent c73edfd2
......@@ -35,6 +35,8 @@
#include "third_party/icu/source/i18n/unicode/dtfmtsym.h"
#include "ui/base/l10n/l10n_util.h"
using base::ASCIIToUTF16;
namespace autofill {
const base::char16 kMidlineEllipsis[] = { 0x22ef, 0 };
......@@ -118,7 +120,7 @@ CreditCard::~CreditCard() {}
// static
const base::string16 CreditCard::StripSeparators(const base::string16& number) {
base::string16 stripped;
base::RemoveChars(number, base::ASCIIToUTF16("- "), &stripped);
base::RemoveChars(number, ASCIIToUTF16("- "), &stripped);
return stripped;
}
......@@ -290,7 +292,7 @@ base::string16 CreditCard::GetRawInfo(ServerFieldType type) const {
base::string16 month = ExpirationMonthAsString();
base::string16 year = Expiration2DigitYearAsString();
if (!month.empty() && !year.empty())
return month + base::ASCIIToUTF16("/") + year;
return month + ASCIIToUTF16("/") + year;
return base::string16();
}
......@@ -298,7 +300,7 @@ base::string16 CreditCard::GetRawInfo(ServerFieldType type) const {
base::string16 month = ExpirationMonthAsString();
base::string16 year = Expiration4DigitYearAsString();
if (!month.empty() && !year.empty())
return month + base::ASCIIToUTF16("/") + year;
return month + ASCIIToUTF16("/") + year;
return base::string16();
}
......@@ -331,7 +333,7 @@ void CreditCard::SetRawInfo(ServerFieldType type,
break;
case CREDIT_CARD_EXP_2_DIGIT_YEAR:
// This is a read-only attribute.
SetExpirationYearFromString(value);
break;
case CREDIT_CARD_EXP_4_DIGIT_YEAR:
......@@ -339,11 +341,11 @@ void CreditCard::SetRawInfo(ServerFieldType type,
break;
case CREDIT_CARD_EXP_DATE_2_DIGIT_YEAR:
// This is a read-only attribute.
SetExpirationDateFromString(value);
break;
case CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR:
// This is a read-only attribute.
SetExpirationDateFromString(value);
break;
case CREDIT_CARD_TYPE:
......@@ -431,7 +433,7 @@ const std::pair<base::string16, base::string16> CreditCard::LabelPieces()
return std::make_pair(obfuscated_cc_number, base::string16());
base::string16 formatted_date(ExpirationMonthAsString());
formatted_date.append(base::ASCIIToUTF16("/"));
formatted_date.append(ASCIIToUTF16("/"));
formatted_date.append(Expiration4DigitYearAsString());
base::string16 separator =
......@@ -445,8 +447,7 @@ void CreditCard::SetInfoForMonthInputType(const base::string16& value) {
return;
std::vector<base::StringPiece16> year_month = base::SplitStringPiece(
value, base::ASCIIToUTF16("-"),
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
value, ASCIIToUTF16("-"), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
DCHECK_EQ(2u, year_month.size());
int num = 0;
bool converted = false;
......@@ -465,9 +466,20 @@ void CreditCard::SetExpirationMonth(int expiration_month) {
}
void CreditCard::SetExpirationYear(int expiration_year) {
if (expiration_year != 0 &&
(expiration_year < 2006 || expiration_year > 10000))
// If |expiration_year| is beyond this millenium, or more than 2 digits but
// before the current millenium (e.g. "545", "1995"), return. What is left are
// values like "45" or "2018".
if (expiration_year > 2999 ||
(expiration_year > 99 && expiration_year < 2000))
return;
// Will normalize 2-digit years to the 4-digit version.
if (expiration_year > 0 && expiration_year < 100) {
base::Time::Exploded now_exploded;
base::Time::Now().LocalExplode(&now_exploded);
expiration_year += (now_exploded.year / 100) * 100;
}
expiration_year_ = expiration_year;
}
......@@ -677,7 +689,7 @@ base::string16 CreditCard::ExpirationMonthAsString() const {
if (expiration_month_ >= 10)
return month;
base::string16 zero = base::ASCIIToUTF16("0");
base::string16 zero = ASCIIToUTF16("0");
zero.append(month);
return zero;
}
......@@ -721,6 +733,52 @@ void CreditCard::SetExpirationYearFromString(const base::string16& text) {
SetExpirationYear(year);
}
void CreditCard::SetExpirationDateFromString(const base::string16& text) {
// Check that |text| fits the supported patterns: mmyy, mmyyyy, m-yy,
// mm-yy, m-yyyy and mm-yyyy. Note that myy and myyyy matched by this pattern
// but are not supported (ambiguous). Separators: -, / and |.
if (!MatchesPattern(text, base::UTF8ToUTF16("^[0-9]{1,2}[-/|]?[0-9]{2,4}$")))
return;
base::string16 month;
base::string16 year;
// Check for a separator.
base::string16 found_separator;
const std::vector<base::string16> kSeparators{
ASCIIToUTF16("-"), ASCIIToUTF16("/"), ASCIIToUTF16("|")};
for (const base::string16& separator : kSeparators) {
if (text.find(separator) != base::string16::npos) {
found_separator = separator;
break;
}
}
if (!found_separator.empty()) {
std::vector<base::string16> month_year = base::SplitString(
text, found_separator, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
DCHECK_EQ(2u, month_year.size());
month = month_year[0];
year = month_year[1];
} else if (text.size() % 2 == 0) {
// If there are no separators, the supported formats are mmyy and mmyyyy.
month = text.substr(0, 2);
year = text.substr(2);
} else {
// Odd number of digits with no separator is too ambiguous.
return;
}
int num = 0;
bool converted = false;
converted = base::StringToInt(month, &num);
DCHECK(converted);
SetExpirationMonth(num);
converted = base::StringToInt(year, &num);
DCHECK(converted);
SetExpirationYear(num);
}
void CreditCard::SetNumber(const base::string16& number) {
number_ = number;
......@@ -773,10 +831,10 @@ bool CreditCard::ConvertMonth(const base::string16& month,
// Some abbreviations have . at the end (e.g., "janv." in French). We don't
// care about matching that.
base::string16 trimmed_month;
base::TrimString(month, base::ASCIIToUTF16("."), &trimmed_month);
base::TrimString(month, ASCIIToUTF16("."), &trimmed_month);
for (int32_t i = 0; i < num_months; ++i) {
base::string16 icu_month(months[i].getBuffer(), months[i].length());
base::TrimString(icu_month, base::ASCIIToUTF16("."), &icu_month);
base::TrimString(icu_month, ASCIIToUTF16("."), &icu_month);
if (compare.StringsEqual(icu_month, trimmed_month)) {
*num = i + 1; // Adjust from 0-indexed to 1-indexed.
return true;
......
......@@ -11,6 +11,7 @@
#include <vector>
#include "base/compiler_specific.h"
#include "base/gtest_prod_util.h"
#include "base/strings/string16.h"
#include "build/build_config.h"
#include "components/autofill/core/browser/autofill_data_model.h"
......@@ -204,6 +205,9 @@ class CreditCard : public AutofillDataModel {
}
private:
FRIEND_TEST_ALL_PREFIXES(CreditCardTest, SetExpirationDateFromString);
FRIEND_TEST_ALL_PREFIXES(CreditCardTest, SetExpirationYearFromString);
// FormGroup:
void GetSupportedTypes(ServerFieldTypeSet* supported_types) const override;
......@@ -222,9 +226,15 @@ class CreditCard : public AutofillDataModel {
bool SetExpirationMonthFromString(const base::string16& text,
const std::string& app_locale);
// Sets |expiration_year_| to the integer conversion of |text|.
// Sets |expiration_year_| to the integer conversion of |text|. Will handle
// 4-digit year or 2-digit year (eventually converted to 4-digit year).
void SetExpirationYearFromString(const base::string16& text);
// Sets |expiration_year_| and |expiration_month_| to the integer conversion
// of |text|. Will handle mmyy, mmyyyy, mm-yyyy and mm-yy as well as single
// digit months, with various separators.
void SetExpirationDateFromString(const base::string16& text);
// See enum definition above.
RecordType record_type_;
......
......@@ -167,6 +167,79 @@ TEST(CreditCardTest, AssignmentOperator) {
EXPECT_TRUE(a == b);
}
TEST(CreditCardTest, SetExpirationYearFromString) {
static const struct {
std::string expiration_year;
int expected_year;
} kTestCases[] = {
// Valid values.
{"2040", 2040},
{"45", 2045},
{"045", 2045},
{"9", 2009},
// Unrecognized year values.
{"052045", 0},
{"123", 0},
{"y2045", 0},
};
for (size_t i = 0; i < arraysize(kTestCases); ++i) {
CreditCard card(base::GenerateGUID(), "some origin");
card.SetExpirationYearFromString(
ASCIIToUTF16(kTestCases[i].expiration_year));
EXPECT_EQ(kTestCases[i].expected_year, card.expiration_year())
<< kTestCases[i].expiration_year << " " << kTestCases[i].expected_year;
}
}
TEST(CreditCardTest, SetExpirationDateFromString) {
static const struct {
std::string expiration_date;
int expected_month;
int expected_year;
} kTestCases[] = {{"10", 0, 0}, // Too small.
{"1020451", 0, 0}, // Too long.
// No separators.
{"105", 0, 0}, // Too ambiguous.
{"0545", 5, 2045},
{"52045", 0, 0}, // Too ambiguous.
{"052045", 5, 2045},
// "/" separator.
{"05/45", 5, 2045},
{"5/2045", 5, 2045},
{"05/2045", 5, 2045},
// "-" separator.
{"05-45", 5, 2045},
{"5-2045", 5, 2045},
{"05-2045", 5, 2045},
// "|" separator.
{"05|45", 5, 2045},
{"5|2045", 5, 2045},
{"05|2045", 5, 2045},
// Invalid values.
{"13/2016", 0, 2016},
{"16/13", 0, 2013},
{"May-2015", 0, 0},
{"05-/2045", 0, 0},
{"05_2045", 0, 0}};
for (size_t i = 0; i < arraysize(kTestCases); ++i) {
CreditCard card(base::GenerateGUID(), "some origin");
card.SetExpirationDateFromString(
ASCIIToUTF16(kTestCases[i].expiration_date));
EXPECT_EQ(kTestCases[i].expected_month, card.expiration_month());
EXPECT_EQ(kTestCases[i].expected_year, card.expiration_year());
}
}
TEST(CreditCardTest, Copy) {
CreditCard a(base::GenerateGUID(), "https://www.example.com");
test::SetCreditCardInfo(&a, "John Dillinger", "123456789012", "01", "2010");
......@@ -465,7 +538,7 @@ TEST(CreditCardTest, IsValid) {
// Invalid because card number is not complete
card.SetRawInfo(CREDIT_CARD_EXP_MONTH, ASCIIToUTF16("12"));
card.SetRawInfo(CREDIT_CARD_EXP_4_DIGIT_YEAR, ASCIIToUTF16("9999"));
card.SetRawInfo(CREDIT_CARD_EXP_4_DIGIT_YEAR, ASCIIToUTF16("2999"));
card.SetRawInfo(CREDIT_CARD_NUMBER, ASCIIToUTF16("41111"));
EXPECT_FALSE(card.IsValid());
......@@ -727,7 +800,7 @@ TEST(CreditCardTest, LastFourDigits) {
TEST(CreditCardTest, CanBuildFromCardNumberAndExpirationDate) {
base::string16 card_number = base::ASCIIToUTF16("test");
int month = 1;
int year = 3000;
int year = 2999;
CreditCard card(card_number, month, year);
EXPECT_EQ(card_number, card.number());
EXPECT_EQ(month, card.expiration_month());
......
......@@ -17,26 +17,6 @@
namespace autofill {
bool IsValidCreditCardExpirationDate(const base::string16& year,
const base::string16& month,
const base::Time& now) {
base::string16 year_cleaned, month_cleaned;
base::TrimWhitespace(year, base::TRIM_ALL, &year_cleaned);
base::TrimWhitespace(month, base::TRIM_ALL, &month_cleaned);
if (year_cleaned.length() != 4)
return false;
int cc_year;
if (!base::StringToInt(year_cleaned, &cc_year))
return false;
int cc_month;
if (!base::StringToInt(month_cleaned, &cc_month))
return false;
return IsValidCreditCardExpirationDate(cc_year, cc_month, now);
}
bool IsValidCreditCardExpirationDate(int year,
int month,
const base::Time& now) {
......
......@@ -15,9 +15,6 @@ namespace autofill {
// Returns true if |year| and |month| describe a date later than |now|.
// |year| must have 4 digits.
bool IsValidCreditCardExpirationDate(const base::string16& year,
const base::string16& month,
const base::Time& now);
bool IsValidCreditCardExpirationDate(int year,
int month,
const base::Time& now);
......
......@@ -55,24 +55,11 @@ const char* const kInvalidNumbers[] = {
"3056 9309 0259 04aa", /* non-digit characters */
};
const char kCurrentDate[]="1 May 2013";
const ExpirationDate kValidCreditCardExpirationDate[] = {
{ "2013", "5" }, // Valid month in current year.
{ "2014", "1" }, // Any month in next year.
{ "2014", "12" }, // Edge condition.
{ "2014", " 1" }, // Whitespace in month.
{ " 2014", "1" }, // Whitespace in year.
};
const IntExpirationDate kValidCreditCardIntExpirationDate[] = {
{ 2013, 5 }, // Valid month in current year.
{ 2014, 1 }, // Any month in next year.
{ 2014, 12 }, // Edge condition.
};
const ExpirationDate kInvalidCreditCardExpirationDate[] = {
{ "2013", "04" }, // Previous month in current year.
{ "2012", "12" }, // Any month in previous year.
{ "2015", "13" }, // Not a real month.
{ "2015", "00" }, // Zero is legal in the CC class but is not a valid date.
};
const IntExpirationDate kInvalidCreditCardIntExpirationDate[] = {
{ 2013, 4 }, // Previous month in current year.
{ 2012, 12 }, // Any month in previous year.
......@@ -118,28 +105,6 @@ TEST(AutofillValidation, IsValidCreditCardNumber) {
}
}
TEST(AutofillValidation, IsValidCreditCardExpirationDate) {
base::Time now;
ASSERT_TRUE(base::Time::FromString(kCurrentDate, &now));
for (size_t i = 0; i < arraysize(kValidCreditCardExpirationDate); ++i) {
const ExpirationDate& data = kValidCreditCardExpirationDate[i];
SCOPED_TRACE(data.year);
SCOPED_TRACE(data.month);
EXPECT_TRUE(IsValidCreditCardExpirationDate(ASCIIToUTF16(data.year),
ASCIIToUTF16(data.month),
now));
}
for (size_t i = 0; i < arraysize(kInvalidCreditCardExpirationDate); ++i) {
const ExpirationDate& data = kInvalidCreditCardExpirationDate[i];
SCOPED_TRACE(data.year);
SCOPED_TRACE(data.month);
EXPECT_TRUE(!IsValidCreditCardExpirationDate(ASCIIToUTF16(data.year),
ASCIIToUTF16(data.month),
now));
}
}
TEST(AutofillValidation, IsValidCreditCardIntExpirationDate) {
base::Time now;
ASSERT_TRUE(base::Time::FromString(kCurrentDate, &now));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment