forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			531 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			531 lines
		
	
	
	
		
			14 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/. */
 | |
| 
 | |
| // 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.
 | |
| export 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);
 | |
| 
 | |
| export 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 = CreditCard.normalizeCardNumber(value);
 | |
|       // 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;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Normalizes a credit card number.
 | |
|    * @param {string} number
 | |
|    * @return {string | null}
 | |
|    * @memberof CreditCard
 | |
|    */
 | |
|   static normalizeCardNumber(number) {
 | |
|     if (!number) {
 | |
|       return null;
 | |
|     }
 | |
|     return number.replace(/[\-\s]/g, "");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Attempts to match the number against known network identifiers.
 | |
|    *
 | |
|    * @param {string} ccNumber Credit card number with no spaces or special characters in it.
 | |
|    *
 | |
|    * @returns {string|null}
 | |
|    */
 | |
|   static getType(ccNumber) {
 | |
|     if (!ccNumber) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     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,
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * Please use getLabelInfo above, as it allows for localization.
 | |
|    * @deprecated
 | |
|    */
 | |
|   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)(\d{2})(\d{2})(?!\d)/,
 | |
|       },
 | |
|       {
 | |
|         regex: /(?:^|\D)(\d{4})[-/](\d{1,2})(?!\d)/,
 | |
|         yearIndex: 0,
 | |
|         monthIndex: 1,
 | |
|       },
 | |
|       {
 | |
|         regex: /(?:^|\D)(\d{1,2})[-/](\d{4})(?!\d)/,
 | |
|         yearIndex: 1,
 | |
|         monthIndex: 0,
 | |
|       },
 | |
|       {
 | |
|         regex: /(?:^|\D)(\d{1,2})[-/](\d{1,2})(?!\d)/,
 | |
|       },
 | |
|       {
 | |
|         regex: /(?:^|\D)(\d{2})(\d{2})(?!\d)/,
 | |
|       },
 | |
|     ];
 | |
| 
 | |
|     expirationString = expirationString.replaceAll(" ", "");
 | |
|     for (let rule of rules) {
 | |
|       let result = rule.regex.exec(expirationString);
 | |
|       if (!result) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       let year, month;
 | |
|       const parsedResults = [parseInt(result[1], 10), parseInt(result[2], 10)];
 | |
|       if (!rule.yearIndex || !rule.monthIndex) {
 | |
|         month = parsedResults[0];
 | |
|         if (month > 12) {
 | |
|           year = parsedResults[0];
 | |
|           month = parsedResults[1];
 | |
|         } else {
 | |
|           year = parsedResults[1];
 | |
|         }
 | |
|       } else {
 | |
|         year = parsedResults[rule.yearIndex];
 | |
|         month = parsedResults[rule.monthIndex];
 | |
|       }
 | |
| 
 | |
|       if (month >= 1 && month <= 12 && (year < 100 || year > 2000)) {
 | |
|         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 getCreditCardLogo(network) {
 | |
|     const PATH = "chrome://formautofill/content/";
 | |
|     const THIRD_PARTY_PATH = PATH + "third-party/";
 | |
|     switch (network) {
 | |
|       case "amex":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-amex.png";
 | |
|       case "cartebancaire":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-cartebancaire.png";
 | |
|       case "diners":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-diners.svg";
 | |
|       case "discover":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-discover.png";
 | |
|       case "jcb":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-jcb.svg";
 | |
|       case "mastercard":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-mastercard.svg";
 | |
|       case "mir":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-mir.svg";
 | |
|       case "unionpay":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-unionpay.svg";
 | |
|       case "visa":
 | |
|         return THIRD_PARTY_PATH + "cc-logo-visa.svg";
 | |
|       default:
 | |
|         return PATH + "icon-credit-card-generic.svg";
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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);
 | |
|   }
 | |
| 
 | |
|   static getSupportedNetworks() {
 | |
|     return SUPPORTED_NETWORKS;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Localised names for supported networks are available in
 | |
|    * `browser/preferences/formAutofill.ftl`.
 | |
|    */
 | |
|   static getNetworkL10nId(network) {
 | |
|     return this.isValidNetwork(network)
 | |
|       ? `autofill-card-network-${network}`
 | |
|       : null;
 | |
|   }
 | |
| }
 | 
