001/** 002 * 003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved. 004 * 005 * This library is free software; you can redistribute it and/or 006 * modify it under the terms of the GNU Lesser General Public 007 * License as published by the Free Software Foundation; either 008 * version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 * Lesser General Public License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public 016 * License along with this library. If not, see <http://www.gnu.org/licenses/>. 017 * 018 **/ 019package lucee.runtime.op.validators; 020 021import lucee.commons.lang.StringUtil; 022import lucee.runtime.exp.ExpressionException; 023import lucee.runtime.op.Caster; 024 025/** 026 * logic to determine if a credit card number is valid. no GUI, just 027 * calculation. 028 * 029 */ 030public final class ValidateCreditCard { 031 032 /** 033 * enum for Amex 034 */ 035 static final int AMEX = 1; 036 037 /** 038 * true if debugging output wanted 039 */ 040 static final boolean DEBUGGING = true; 041 042 /** 043 * enum for Diner's club 044 */ 045 static final int DINERS = 2; // includes Carte Blanche 046 047 /** 048 * enum for Discover Card 049 */ 050 static final int DISCOVER = 3; 051 052 /** 053 * enum for Enroute 054 */ 055 static final int ENROUTE = 4; 056 057 /** 058 * enum for JCB 059 */ 060 static final int JCB = 5; 061 062 /** 063 * enum for Mastercard 064 */ 065 static final int MASTERCARD = 6; 066 067 /** 068 * enum for insufficient digits 069 */ 070 static final int NOT_ENOUGH_DIGITS = -3; 071 072 /** 073 * enum for too many digits 074 */ 075 static final int TOO_MANY_DIGITS = -2; 076 077 /** 078 * enum for unknown vendor 079 */ 080 static final int UNKNOWN_VENDOR = -1; 081 082 /** 083 * enum for Visa 084 */ 085 static final int VISA = 7; 086 087 /** 088 * Used to speed up findMatchingRange by caching the last hit. 089 */ 090 private static int cachedLastFind = 0; 091 092 /** 093 * ranges of credit card number that belong to each company. buildRanges 094 * initialises. 095 */ 096 private static LCR[] ranges; 097 098 /** 099 * used by vendorToString to describe the enumerations 100 */ 101 private static final String[] vendors = 102 {"Error: not enough digits", 103 "Error: too many digits", 104 "Error: unknown credit card company", 105 "dummy", 106 "Amex", 107 "Diners/Carte Blanche", 108 "Discover", 109 "enRoute", 110 "JCB", 111 "MasterCard", 112 "Visa"}; 113 114 // -------------------------- STATIC METHODS -------------------------- 115 116 static 117 { 118 // now that all enum constants defined 119 buildRanges(); 120 } 121 122 /** 123 * build table of which ranges of credit card number belong to which vendor 124 */ 125 private static void buildRanges() 126 { 127 // careful, no lead zeros allowed 128 // low high len vendor mod-10? 129 ranges = 130 new LCR[] {new LCR( 4000000000000L, 131 4999999999999L, 132 13, 133 VISA, 134 true ), 135 new LCR( 30000000000000L, 136 30599999999999L, 137 14, 138 DINERS, 139 true ), 140 new LCR( 36000000000000L, 141 36999999999999L, 142 14, 143 DINERS, 144 true ), 145 new LCR( 38000000000000L, 146 38999999999999L, 147 14, 148 DINERS, 149 true ), 150 new LCR( 180000000000000L, 151 180099999999999L, 152 15, 153 JCB, 154 true ), 155 new LCR( 201400000000000L, 156 201499999999999L, 157 15, 158 ENROUTE, 159 false ), 160 new LCR( 213100000000000L, 161 213199999999999L, 162 15, 163 JCB, 164 true ), 165 new LCR( 214900000000000L, 166 214999999999999L, 167 15, 168 ENROUTE, 169 false ), 170 new LCR( 340000000000000L, 171 349999999999999L, 172 15, 173 AMEX, 174 true ), 175 new LCR( 370000000000000L, 176 379999999999999L, 177 15, 178 AMEX, 179 true ), 180 new LCR( 3000000000000000L, 181 3999999999999999L, 182 16, 183 JCB, 184 true ), 185 new LCR( 4000000000000000L, 186 4999999999999999L, 187 16, 188 VISA, 189 true ), 190 new LCR( 5100000000000000L, 191 5599999999999999L, 192 16, 193 MASTERCARD, 194 true ), 195 new LCR( 6011000000000000L, 196 6011999999999999L, 197 16, 198 DISCOVER, 199 true )}; // end table initialisation 200 } 201 202 /** 203 * Finds a matching range in the ranges array for a given creditCardNumber. 204 * 205 * @param creditCardNumber number on card. 206 * 207 * @return index of matching range, or NOT_ENOUGH_DIGITS or UNKNOWN_VENDOR 208 * on failure. 209 */ 210 protected static int findMatchingRange( long creditCardNumber ) 211 { 212 if ( creditCardNumber < 1000000000000L ) 213 { 214 return NOT_ENOUGH_DIGITS; 215 } 216 if ( creditCardNumber > 9999999999999999L ) 217 { 218 return TOO_MANY_DIGITS; 219 } 220 // check the cached index first, where we last found a number. 221 if ( ranges[ cachedLastFind ].low <= creditCardNumber 222 && creditCardNumber <= ranges[ cachedLastFind ].high ) 223 { 224 return cachedLastFind; 225 } 226 for ( int i = 0; i < ranges.length; i++ ) 227 { 228 if ( ranges[ i ].low <= creditCardNumber 229 && creditCardNumber <= ranges[ i ].high ) 230 { 231 // we have a match 232 cachedLastFind = i; 233 return i; 234 } 235 } // end for 236 return UNKNOWN_VENDOR; 237 } // end findMatchingRange 238 239 240 public static boolean isValid(String strCreditCardNumber) { 241 return isValid(toLong(strCreditCardNumber, 0L)); 242 } 243 244 private static long toLong(String strCreditCardNumber, long defaultValue) { 245 246 if(strCreditCardNumber==null) return defaultValue; 247 248 // strip commas, spaces, + AND - 249 StringBuffer sb = new StringBuffer( strCreditCardNumber.length() ); 250 for (int i = 0; i < strCreditCardNumber.length(); i++ ) { 251 char c = strCreditCardNumber.charAt( i ); 252 if(!StringUtil.isWhiteSpace(c) && c!=',' && c!='+' && c!='-') 253 sb.append( c ); 254 } 255 long num=(long) Caster.toDoubleValue(sb.toString(),0L); 256 if(num==0L) return defaultValue; 257 258 return num; 259 } 260 261 /** 262 * Determine if the credit card number is valid, i.e. has good prefix and 263 * checkdigit. Does _not_ ask the credit card company if this card has been 264 * issued or is in good standing. 265 * 266 * @param creditCardNumber number on card. 267 * 268 * @return true if card number is good. 269 */ 270 public static boolean isValid( long creditCardNumber ) { 271 int i = findMatchingRange( creditCardNumber ); 272 if ( i < 0 ) { 273 return false; 274 } 275 //else { 276 // we have a match 277 if ( ranges[ i ].hasCheckDigit ) 278 { 279 // there is a checkdigit to be validated 280 /* 281 * Manual method MOD 10 checkdigit 706-511-227 7 0 6 5 1 1 2 2 7 282 * 2 * 2 * 2 * 2 --------------------------------- 7 + 0 + 6 283 * +1+0+ 1 + 2 + 2 + 4 = 23 23 MOD 10 = 3 10 - 3 = 7 -- the 284 * check digit Note digits of multiplication results must be 285 * added before sum. Computer Method MOD 10 checkdigit 286 * 706-511-227 7 0 6 5 1 1 2 2 7 Z Z Z Z 287 * --------------------------------- 7 + 0 + 6 + 1 + 1 + 2 + 2 + 288 * 4 + 7 = 30 30 MOD 10 had better = 0 289 */ 290 long number = creditCardNumber; 291 int checksum = 0; 292 // work right to left 293 for ( int place = 0; place < 16; place++ ) 294 { 295 int digit = (int) ( number % 10 ); 296 number /= 10; 297 if ( ( place & 1 ) == 0 ) 298 { 299 // even position (0-based from right), just add digit 300 checksum += digit; 301 } 302 else 303 { // odd position (0-based from right), must double 304 // and add 305 checksum += z( digit ); 306 } 307 if ( number == 0 ) 308 { 309 break; 310 } 311 } // end for 312 // good checksum should be 0 mod 10 313 return ( checksum % 10 ) == 0; 314 } 315 return true; // no checksum needed 316 317 //} // end if have match 318 } // end isValid 319 320 321 322 /** 323 * Determine the credit card company. Does NOT validate checkdigit. 324 * 325 * @param creditCardNumber number on card. 326 * 327 * @return credit card vendor enumeration constant. 328 */ 329 public static int recognizeVendor( long creditCardNumber ) 330 { 331 int i = findMatchingRange( creditCardNumber ); 332 if ( i < 0 ) 333 { 334 return i; 335 } 336 return ranges[ i ].vendor; 337 338 } // end recognize 339 340 // From http://www.icverify.com/ 341 // Vendor Prefix len checkdigit 342 // MASTERCARD 51-55 16 mod 10 343 // VISA 4 13, 16 mod 10 344 // AMEX 34,37 15 mod 10 345 // Diners Club/ 346 // Carte Blanche 347 // 300-305 14 348 // 36 14 349 // 38 14 mod 10 350 // Discover 6011 16 mod 10 351 // enRoute 2014 15 352 // 2149 15 any 353 // JCB 3 16 mod 10 354 // JCB 2131 15 355 // 1800 15 mod 10 356 357 public static String toCreditcard(String strCreditCardNumber) throws ExpressionException { 358 long number=toLong(strCreditCardNumber, -1); 359 if(number==-1) throw new ExpressionException("invalid creditcard number ["+strCreditCardNumber+"]"); 360 return toPrettyString(number); 361 } 362 public static String toCreditcard(String strCreditCardNumber, String defaultValue) { 363 long number=toLong(strCreditCardNumber, -1); 364 if(number==-1) return defaultValue; 365 return toPrettyString(number); 366 } 367 368 /** 369 * Convert a creditCardNumber as long to a formatted String. Currently it 370 * breaks 16-digit numbers into groups of 4. 371 * 372 * @param creditCardNumber number on card. 373 * 374 * @return String representation of the credit card number. 375 */ 376 private static String toPrettyString( long creditCardNumber ) 377 { 378 String plain = Long.toString( creditCardNumber ); 379 // int i = findMatchingRange(creditCardNumber); 380 int length = plain.length(); 381 382 switch ( length ) 383 { 384 case 12 : 385 // 12 pattern 3-3-3-3 386 return plain.substring( 0, 3 ) 387 + ' ' 388 + plain.substring( 3, 6 ) 389 + ' ' 390 + plain.substring( 6, 9 ) 391 + ' ' 392 + plain.substring( 9, 12 ); 393 394 case 13 : 395 // 13 pattern 4-3-3-3 396 return plain.substring( 0, 4 ) 397 + ' ' 398 + plain.substring( 4, 7 ) 399 + ' ' 400 + plain.substring( 7, 10 ) 401 + ' ' 402 + plain.substring( 10, 13 ); 403 404 case 14 : 405 // 14 pattern 2-4-4-4 406 return plain.substring( 0, 2 ) 407 + ' ' 408 + plain.substring( 2, 6 ) 409 + ' ' 410 + plain.substring( 6, 10 ) 411 + ' ' 412 + plain.substring( 10, 14 ); 413 414 case 15 : 415 // 15 pattern 3-4-4-4 416 return plain.substring( 0, 3 ) 417 + ' ' 418 + plain.substring( 3, 7 ) 419 + ' ' 420 + plain.substring( 7, 11 ) 421 + ' ' 422 + plain.substring( 11, 15 ); 423 424 case 16 : 425 // 16 pattern 4-4-4-4 426 return plain.substring( 0, 4 ) 427 + ' ' 428 + plain.substring( 4, 8 ) 429 + ' ' 430 + plain.substring( 8, 12 ) 431 + ' ' 432 + plain.substring( 12, 16 ); 433 434 case 17 : 435 // 17 pattern 1-4-4-4-4 436 return plain.substring( 0, 1 ) 437 + ' ' 438 + plain.substring( 1, 5 ) 439 + ' ' 440 + plain.substring( 5, 9 ) 441 + ' ' 442 + plain.substring( 9, 13 ) 443 + ' ' 444 + plain.substring( 13, 17 ); 445 446 default : 447 // 0..11, 18+ digits long 448 // plain 449 return plain; 450 } // end switch 451 } // end toPrettyString 452 453 /** 454 * Converts a vendor index enumeration to the equivalent words. It will 455 * trigger an ArrayIndexOutOfBoundsException if you feed it an illegal 456 * value. 457 * 458 * @param vendorEnum e.g. AMEX, UNKNOWN_VENDOR, TOO_MANY_DIGITS 459 * 460 * @return equivalent string in words, e.g. "Amex" "Error: unknown vendor". 461 */ 462 public static String vendorToString( int vendorEnum ) 463 { 464 return vendors[ vendorEnum - NOT_ENOUGH_DIGITS ]; 465 } // end vendorToString 466 467 /** 468 * used in computing checksums, doubles and adds resulting digits. 469 * 470 * @param digit the digit to be doubled, and digit summed. 471 * 472 * @return // 0->0 1->2 2->4 3->6 4->8 5->1 6->3 7->5 8->7 9->9 473 */ 474 private static int z( int digit ) { 475 if ( digit == 0 ) { 476 return 0; 477 } 478 return ( digit * 2 - 1 ) % 9 + 1; 479 } 480 481 // --------------------------- main() method --------------------------- 482 483 /** 484 * Test driver 485 * 486 * @param args not used 487 */ 488 public static void main( String[] args ) 489 { 490 if ( DEBUGGING ) 491 { 492 493 494 495 System.out.println( isValid( "0" ) ); 496 System.out.println( isValid( "4000000000006" ) ); 497 System.out.println( isValid( "40000000-000,06" ) ); 498 499 System.out.println( isValid( 0 ) ); 500 System.out.println( isValid( 6010222233334444L ) ); // false 501 System.out.println( isValid( 4000000000000L ) ); // false 502 System.out.println( isValid( 4000000000006L ) ); // true 503 System.out.println( isValid( 4000000000009L ) ); // false 504 System.out.println( isValid( 4999999999999L ) ); // false 505 System.out.println( isValid( 378888888888858L ) ); // true, Amex 506 System.out.println( isValid( 4888888888888838L ) ); // true, Visa; 507 System.out.println( isValid( 5588888888888838L ) ); // true, MC 508 System.out.println( isValid( 6011222233334444L ) ); // true, Dicover 509 510 } // end if debugging 511 } // end main 512} // end class CreditCard 513 514/** 515 * Describes a single Legal Card Range 516 */ 517class LCR { 518 519 // ------------------------------ FIELDS ------------------------------ 520 521 /** 522 * does this range have a MOD-10 checkdigit? 523 */ 524 public boolean hasCheckDigit; 525 526 /** 527 * low and high bounds on range covered by this vendor 528 */ 529 public long high; 530 531 /** 532 * how many digits in this type of number. 533 */ 534 public int length; 535 536 /** 537 * low bounds on range covered by this vendor 538 */ 539 public long low; 540 541 /** 542 * enumeration credit card service 543 */ 544 public int vendor; 545 546 // --------------------------- CONSTRUCTORS --------------------------- 547 548 /** 549 * public constructor 550 * 551 * @param low lowest credit card number in range. 552 * @param high highest credit card number in range 553 * @param length length of number in digits 554 * @param vendor enum constant for vendor 555 * @param hasCheckDigit true if uses mod 10 check digit. 556 */ 557 public LCR( long low, 558 long high, 559 int length, 560 int vendor, 561 boolean hasCheckDigit ) 562 { 563 this.low = low; 564 this.high = high; 565 this.length = length; 566 this.vendor = vendor; 567 this.hasCheckDigit = hasCheckDigit; 568 } // end public constructor 569} // end class LCR