1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-validator for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-validator/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-validator/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Validator;
10
11use Laminas\Stdlib\ArrayUtils;
12use Traversable;
13
14class CreditCard extends AbstractValidator
15{
16    /**
17     * Detected CCI list
18     *
19     * @var string
20     */
21    const ALL              = 'All';
22    const AMERICAN_EXPRESS = 'American_Express';
23    const UNIONPAY         = 'Unionpay';
24    const DINERS_CLUB      = 'Diners_Club';
25    const DINERS_CLUB_US   = 'Diners_Club_US';
26    const DISCOVER         = 'Discover';
27    const JCB              = 'JCB';
28    const LASER            = 'Laser';
29    const MAESTRO          = 'Maestro';
30    const MASTERCARD       = 'Mastercard';
31    const SOLO             = 'Solo';
32    const VISA             = 'Visa';
33    const MIR              = 'Mir';
34
35    const CHECKSUM       = 'creditcardChecksum';
36    const CONTENT        = 'creditcardContent';
37    const INVALID        = 'creditcardInvalid';
38    const LENGTH         = 'creditcardLength';
39    const PREFIX         = 'creditcardPrefix';
40    const SERVICE        = 'creditcardService';
41    const SERVICEFAILURE = 'creditcardServiceFailure';
42
43    /**
44     * Validation failure message template definitions
45     *
46     * @var array
47     */
48    protected $messageTemplates = [
49        self::CHECKSUM       => 'The input seems to contain an invalid checksum',
50        self::CONTENT        => 'The input must contain only digits',
51        self::INVALID        => 'Invalid type given. String expected',
52        self::LENGTH         => 'The input contains an invalid amount of digits',
53        self::PREFIX         => 'The input is not from an allowed institute',
54        self::SERVICE        => 'The input seems to be an invalid credit card number',
55        self::SERVICEFAILURE => 'An exception has been raised while validating the input',
56    ];
57
58    /**
59     * List of CCV names
60     *
61     * @var array
62     */
63    protected $cardName = [
64        0  => self::AMERICAN_EXPRESS,
65        1  => self::DINERS_CLUB,
66        2  => self::DINERS_CLUB_US,
67        3  => self::DISCOVER,
68        4  => self::JCB,
69        5  => self::LASER,
70        6  => self::MAESTRO,
71        7  => self::MASTERCARD,
72        8  => self::SOLO,
73        9  => self::UNIONPAY,
74        10 => self::VISA,
75        11 => self::MIR,
76    ];
77
78    /**
79     * List of allowed CCV lengths
80     *
81     * @var array
82     */
83    protected $cardLength = [
84        self::AMERICAN_EXPRESS => [15],
85        self::DINERS_CLUB      => [14],
86        self::DINERS_CLUB_US   => [16],
87        self::DISCOVER         => [16, 19],
88        self::JCB              => [15, 16],
89        self::LASER            => [16, 17, 18, 19],
90        self::MAESTRO          => [12, 13, 14, 15, 16, 17, 18, 19],
91        self::MASTERCARD       => [16],
92        self::SOLO             => [16, 18, 19],
93        self::UNIONPAY         => [16, 17, 18, 19],
94        self::VISA             => [13, 16, 19],
95        self::MIR              => [13, 16],
96    ];
97
98    /**
99     * List of accepted CCV provider tags
100     *
101     * @var array
102     */
103    protected $cardType = [
104        self::AMERICAN_EXPRESS => ['34', '37'],
105        self::DINERS_CLUB      => ['300', '301', '302', '303', '304', '305', '36'],
106        self::DINERS_CLUB_US   => ['54', '55'],
107        self::DISCOVER         => ['6011', '622126', '622127', '622128', '622129', '62213',
108                                        '62214', '62215', '62216', '62217', '62218', '62219',
109                                        '6222', '6223', '6224', '6225', '6226', '6227', '6228',
110                                        '62290', '62291', '622920', '622921', '622922', '622923',
111                                        '622924', '622925', '644', '645', '646', '647', '648',
112                                        '649', '65'],
113        self::JCB              => ['1800', '2131', '3528', '3529', '353', '354', '355', '356', '357', '358'],
114        self::LASER            => ['6304', '6706', '6771', '6709'],
115        self::MAESTRO          => ['5018', '5020', '5038', '6304', '6759', '6761', '6762', '6763',
116                                        '6764', '6765', '6766', '6772'],
117        self::MASTERCARD       => ['2221', '2222', '2223', '2224', '2225', '2226', '2227', '2228', '2229',
118                                        '223', '224', '225', '226', '227', '228', '229',
119                                        '23', '24', '25', '26', '271', '2720',
120                                        '51', '52', '53', '54', '55'],
121        self::SOLO             => ['6334', '6767'],
122        self::UNIONPAY         => ['622126', '622127', '622128', '622129', '62213', '62214',
123                                        '62215', '62216', '62217', '62218', '62219', '6222', '6223',
124                                        '6224', '6225', '6226', '6227', '6228', '62290', '62291',
125                                        '622920', '622921', '622922', '622923', '622924', '622925'],
126        self::VISA             => ['4'],
127        self::MIR              => ['2200', '2201', '2202', '2203', '2204'],
128    ];
129
130    /**
131     * Options for this validator
132     *
133     * @var array
134     */
135    protected $options = [
136        'service' => null,     // Service callback for additional validation
137        'type'    => [],  // CCIs which are accepted by validation
138    ];
139
140    /**
141     * Constructor
142     *
143     * @param string|array|Traversable $options OPTIONAL Type of CCI to allow
144     */
145    public function __construct($options = [])
146    {
147        if ($options instanceof Traversable) {
148            $options = ArrayUtils::iteratorToArray($options);
149        } elseif (! is_array($options)) {
150            $options = func_get_args();
151            $temp['type'] = array_shift($options);
152            if (! empty($options)) {
153                $temp['service'] = array_shift($options);
154            }
155
156            $options = $temp;
157        }
158
159        if (! array_key_exists('type', $options)) {
160            $options['type'] = self::ALL;
161        }
162
163        $this->setType($options['type']);
164        unset($options['type']);
165
166        if (array_key_exists('service', $options)) {
167            $this->setService($options['service']);
168            unset($options['service']);
169        }
170
171        parent::__construct($options);
172    }
173
174    /**
175     * Returns a list of accepted CCIs
176     *
177     * @return array
178     */
179    public function getType()
180    {
181        return $this->options['type'];
182    }
183
184    /**
185     * Sets CCIs which are accepted by validation
186     *
187     * @param  string|array $type Type to allow for validation
188     * @return CreditCard Provides a fluid interface
189     */
190    public function setType($type)
191    {
192        $this->options['type'] = [];
193        return $this->addType($type);
194    }
195
196    /**
197     * Adds a CCI to be accepted by validation
198     *
199     * @param  string|array $type Type to allow for validation
200     * @return $this Provides a fluid interface
201     */
202    public function addType($type)
203    {
204        if (is_string($type)) {
205            $type = [$type];
206        }
207
208        foreach ($type as $typ) {
209            if ($typ == self::ALL) {
210                $this->options['type'] = array_keys($this->cardLength);
211                continue;
212            }
213
214            if (in_array($typ, $this->options['type'])) {
215                continue;
216            }
217
218            $constant = 'static::' . strtoupper($typ);
219            if (! defined($constant) || in_array(constant($constant), $this->options['type'])) {
220                continue;
221            }
222            $this->options['type'][] = constant($constant);
223        }
224
225        return $this;
226    }
227
228    /**
229     * Returns the actual set service
230     *
231     * @return callable
232     */
233    public function getService()
234    {
235        return $this->options['service'];
236    }
237
238    /**
239     * Sets a new callback for service validation
240     *
241     * @param  callable $service
242     * @return $this
243     * @throws Exception\InvalidArgumentException on invalid service callback
244     */
245    public function setService($service)
246    {
247        if (! is_callable($service)) {
248            throw new Exception\InvalidArgumentException('Invalid callback given');
249        }
250
251        $this->options['service'] = $service;
252        return $this;
253    }
254
255    /**
256     * Returns true if and only if $value follows the Luhn algorithm (mod-10 checksum)
257     *
258     * @param  string $value
259     * @return bool
260     */
261    public function isValid($value)
262    {
263        $this->setValue($value);
264
265        if (! is_string($value)) {
266            $this->error(self::INVALID, $value);
267            return false;
268        }
269
270        if (! ctype_digit($value)) {
271            $this->error(self::CONTENT, $value);
272            return false;
273        }
274
275        $length = strlen($value);
276        $types  = $this->getType();
277        $foundp = false;
278        $foundl = false;
279        foreach ($types as $type) {
280            foreach ($this->cardType[$type] as $prefix) {
281                if (0 === strpos($value, $prefix)) {
282                    $foundp = true;
283                    if (in_array($length, $this->cardLength[$type])) {
284                        $foundl = true;
285                        break 2;
286                    }
287                }
288            }
289        }
290
291        if ($foundp == false) {
292            $this->error(self::PREFIX, $value);
293            return false;
294        }
295
296        if ($foundl == false) {
297            $this->error(self::LENGTH, $value);
298            return false;
299        }
300
301        $sum    = 0;
302        $weight = 2;
303
304        for ($i = $length - 2; $i >= 0; $i--) {
305            $digit = $weight * $value[$i];
306            $sum += floor($digit / 10) + $digit % 10;
307            $weight = $weight % 2 + 1;
308        }
309
310        if ((10 - $sum % 10) % 10 != $value[$length - 1]) {
311            $this->error(self::CHECKSUM, $value);
312            return false;
313        }
314
315        $service = $this->getService();
316        if (! empty($service)) {
317            try {
318                $callback = new Callback($service);
319                $callback->setOptions($this->getType());
320                if (! $callback->isValid($value)) {
321                    $this->error(self::SERVICE, $value);
322                    return false;
323                }
324            } catch (\Exception $e) {
325                $this->error(self::SERVICEFAILURE, $value);
326                return false;
327            }
328        }
329
330        return true;
331    }
332}
333