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