gecko-dev/toolkit/modules/CreditCard.jsm

330 строки
7.8 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["CreditCard"];
ChromeUtils.defineModuleGetter(this, "MasterPassword",
"resource://formautofill/MasterPassword.jsm");
// 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",
]);
class CreditCard {
/**
* @param {string} name
* @param {string} number
* @param {string} expirationString
* @param {string|number} expirationMonth
* @param {string|number} expirationYear
* @param {string} network
* @param {string|number} ccv
* @param {string} encryptedNumber
*/
constructor({
name,
number,
expirationString,
expirationMonth,
expirationYear,
network,
ccv,
encryptedNumber,
}) {
this._name = name;
this._unmodifiedNumber = number;
this._encryptedNumber = encryptedNumber;
this._ccv = ccv;
this.number = number;
// Only prefer the string version if missing one or both parsed formats.
if (expirationString && (!expirationMonth || !expirationYear)) {
this.expirationString = expirationString;
} else {
this.expirationMonth = expirationMonth;
this.expirationYear = expirationYear;
}
if (network) {
this.network = network;
}
}
set name(value) {
this._name = value;
}
set expirationMonth(value) {
if (typeof value == "undefined") {
this._expirationMonth = undefined;
return;
}
this._expirationMonth = this._normalizeExpirationMonth(value);
}
get expirationMonth() {
return this._expirationMonth;
}
set expirationYear(value) {
if (typeof value == "undefined") {
this._expirationYear = undefined;
return;
}
this._expirationYear = this._normalizeExpirationYear(value);
}
get expirationYear() {
return this._expirationYear;
}
set expirationString(value) {
let {month, year} = this._parseExpirationString(value);
this.expirationMonth = month;
this.expirationYear = year;
}
set ccv(value) {
this._ccv = value;
}
get number() {
return this._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
// 9 digits (Canadian SIN).
// [1] https://en.wikipedia.org/wiki/Social_Insurance_Number
normalizedNumber = normalizedNumber.match(/^\d{9,}$/) ?
normalizedNumber : null;
this._number = normalizedNumber;
}
}
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
isValidNumber() {
if (!this._number) {
return false;
}
// Remove dashes and whitespace
let number = this._number.replace(/[\-\s]/g, "");
let len = number.length;
if (len != 9 && len != 15 && len != 16) {
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;
}
/**
* 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() {
if (!this.isValidNumber()) {
throw new Error("Invalid credit card number");
}
return "*".repeat(4) + " " + this._number.substr(-4);
}
get longMaskedNumber() {
if (!this.isValidNumber()) {
throw new Error("Invalid credit card number");
}
return "*".repeat(this.number.length - 4) + this.number.substr(-4);
}
/**
* Get credit card display label. It should display masked numbers and the
* cardholder's name, separated by a comma. If `showNumbers` is set to
* true, decrypted credit card numbers are shown instead.
*/
async getLabel({showNumbers} = {}) {
let parts = [];
let label;
if (showNumbers) {
if (this._encryptedNumber) {
label = await MasterPassword.decrypt(this._encryptedNumber);
} else {
label = this._number;
}
}
if (this._unmodifiedNumber && !label) {
if (this.isValidNumber()) {
label = this.maskedNumber;
} else {
let maskedNumber = CreditCard.formatMaskedNumber(this._unmodifiedNumber);
label = `${maskedNumber.affix} ${maskedNumber.label}`;
}
}
if (label) {
parts.push(label);
}
if (this._name) {
parts.push(this._name);
}
return parts.join(", ");
}
_normalizeExpirationMonth(month) {
month = parseInt(month, 10);
if (isNaN(month) || month < 1 || month > 12) {
return undefined;
}
return month;
}
_normalizeExpirationYear(year) {
year = parseInt(year, 10);
if (isNaN(year) || year < 0) {
return undefined;
}
if (year < 100) {
year += 2000;
}
return year;
}
_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 formatMaskedNumber(maskedNumber) {
return {
affix: "****",
label: maskedNumber.replace(/^\**/, ""),
};
}
static getMaskedNumber(number) {
let creditCard = new CreditCard({number});
return creditCard.maskedNumber;
}
static getLongMaskedNumber(number) {
let creditCard = new CreditCard({number});
return creditCard.longMaskedNumber;
}
static isValidNumber(number) {
let creditCard = new CreditCard({number});
return creditCard.isValidNumber();
}
static isValidNetwork(network) {
return SUPPORTED_NETWORKS.includes(network);
}
}
CreditCard.SUPPORTED_NETWORKS = SUPPORTED_NETWORKS;