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