diff --git a/README.md b/README.md index c192b51..4de69bf 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,42 @@ V::lang('ar'); * `dateBefore` - Field is a valid date and is before the given date * `dateAfter` - Field is a valid date and is after the given date * `contains` - Field is a string and contains the given string + * `creditCard` - Field is a valid credit card number + + +## Credit Card Validation usage + +Credit card validation currently allows you to validate a Visa `visa`, +Mastercard `mastercard`, Dinersclub `dinersclub`, American Express `amex` +or Discover `discover` + +This will check the credit card against each card type + +```php +$v->rule('creditCard', 'credit_card'); +``` + +To optionally filter card types, add the slug to an array as the next parameter: + +```php +$v->rule('creditCard', 'credit_card', ['visa', 'mastercard']); +``` + +If you only want to validate one type of card, put it as a string: + +```php +$v->rule('creditCard', 'credit_card', 'visa'); +``` + +If the card type information is coming from the client, you might also want to +still specify an array of valid card types: + +```php +$cardType = 'amex'; +$v->rule('creditCard', 'credit_card', $cardType, ['visa', 'mastercard']); +$v->validate(); // false +``` + ## Adding Custom Validation Rules diff --git a/lang/en.php b/lang/en.php index d2f3a0b..0a83aeb 100644 --- a/lang/en.php +++ b/lang/en.php @@ -25,7 +25,8 @@ return array( 'dateAfter' => "must be date after '%s'", 'contains' => "must contain %s", 'boolean' => "must be a boolean", - 'lengthBetween' => "must be between %d and %d characters" + 'lengthBetween' => "must be between %d and %d characters", + 'creditCard' => "must be a valid credit card number" ); diff --git a/src/Valitron/Validator.php b/src/Valitron/Validator.php index 6632a7c..d92e2e6 100644 --- a/src/Valitron/Validator.php +++ b/src/Valitron/Validator.php @@ -472,6 +472,116 @@ class Validator return (is_bool($value)) ? true : false; } + /** + * Validate that a field contains a valid credit card + * optionally filtered by an array + * + * @param string $field + * @param string $value + * @param array $params + * @return bool + */ + protected function validateCreditCard($field, $value, $params) + { + /** + * I there has been an array of valid cards supplied, or the name of the users card + * or the name and an array of valid cards + */ + if (!empty($params)) { + /** + * array of valid cards + */ + if (is_array($params[0])) { + $cards = $params[0]; + } else if (is_string($params[0])){ + $cardType = $params[0]; + if (isset($params[1]) && is_array($params[1])) { + $cards = $params[1]; + if (!in_array($cardType, $cards)) { + return false; + } + } + } + } + /** + * Luhn algorithm + * + * @return bool + */ + $numberIsValid = function () use ($value) { + $number = preg_replace('/[^0-9]+/', '', $value); + $sum = 0; + + $strlen = strlen($number); + if ($strlen < 13) { + return false; + } + for ($i = 0; $i < $strlen; $i++) { + $digit = (int) substr($number, $strlen - $i - 1, 1); + if ($i % 2 == 1) { + $sub_total = $digit * 2; + if ($sub_total > 9) { + $sub_total = ($sub_total - 10) + 1; + } + } else { + $sub_total = $digit; + } + $sum += $sub_total; + } + if ($sum > 0 && $sum % 10 == 0) { + return true; + } + return false; + }; + + if ($numberIsValid()) { + if (!isset($cards)) { + return true; + } else { + $cardRegex = array( + 'visa' => '#^4[0-9]{12}(?:[0-9]{3})?$#', + 'mastercard' => '#^5[1-5][0-9]{14}$#', + 'amex' => '#^3[47][0-9]{13}$#', + 'dinersclub' => '#^3(?:0[0-5]|[68][0-9])[0-9]{11}$#', + 'discover' => '#^6(?:011|5[0-9]{2})[0-9]{12}$#', + ); + + if (isset($cardType)) { + // if we don't have any valid cards specified and the card we've been given isn't in our regex array + if (!isset($cards) && !in_array($cardType, array_keys($cardRegex))) { + return false; + } + + // we only need to test against one card type + return (preg_match($cardRegex[$cardType], $value) === 1); + + } else if (isset($cards)) { + // if we have cards, check our users card against only the ones we have + foreach($cards as $card) { + if (in_array($card, array_keys($cardRegex))) { + // if the card is valid, we want to stop looping + if (preg_match($cardRegex[$card], $value) === 1) { + return true; + } + } + } + } else { + // loop through every card + foreach($cardRegex as $regex) { + // until we find a valid one + if (preg_match($regex, $value) === 1) { + return true; + } + } + } + } + } + + // if we've got this far, the card has passed no validation so it's invalid! + return false; + } + + /** * Get array of fields and data */ diff --git a/tests/Valitron/ValidateTest.php b/tests/Valitron/ValidateTest.php index dce9829..d1ed886 100644 --- a/tests/Valitron/ValidateTest.php +++ b/tests/Valitron/ValidateTest.php @@ -654,6 +654,58 @@ class ValidateTest extends BaseTestCase $v->rule('boolean', 'test'); $this->assertFalse($v->validate()); } + + public function testCreditCardValid() + { + $visa = array(4539511619543489, 4532949059629052, 4024007171194938, 4929646403373269, 4539135861690622); + $mastercard = array(5162057048081965, 5382687859049349, 5484388880142230, 5464941521226434, 5473481232685965); + $amex = array(371442067262027, 340743030537918, 345509167493596, 343665795576848, 346087552944316); + $dinersclub = array(30363194756249, 30160097740704, 38186521192206, 38977384214552, 38563220301454); + $discover = array(6011712400392605, 6011536340491809, 6011785775263015, 6011984124619056, 6011320958064251); + + foreach (compact('visa', 'mastercard', 'amex', 'dinersclub', 'discover') as $type => $numbers) { + foreach($numbers as $number) { + $v = new Validator(array('test' => $number)); + $v->rule('creditCard', 'test'); + $this->assertTrue($v->validate()); + $v->rule('creditCard', 'test', array($type, 'mastercard', 'visa')); + $this->assertTrue($v->validate()); + $v->rule('creditCard', 'test', $type); + $this->assertTrue($v->validate()); + $v->rule('creditCard', 'test', $type, array($type, 'mastercard', 'visa')); + $this->assertTrue($v->validate()); + unset($v); + } + } + } + + public function testcreditCardInvalid() + { + $visa = array(3539511619543489, 3532949059629052, 3024007171194938, 3929646403373269, 3539135861690622); + $mastercard = array(4162057048081965, 4382687859049349, 4484388880142230, 4464941521226434, 4473481232685965); + $amex = array(271442067262027, 240743030537918, 245509167493596, 243665795576848, 246087552944316); + $dinersclub = array(20363194756249, 20160097740704, 28186521192206, 28977384214552, 28563220301454); + $discover = array(5011712400392605, 5011536340491809, 5011785775263015, 5011984124619056, 5011320958064251); + + foreach (compact('visa', 'mastercard', 'amex', 'dinersclub', 'discover') as $type => $numbers) { + foreach($numbers as $number) { + $v = new Validator(array('test' => $number)); + $v->rule('creditCard', 'test'); + $this->assertFalse($v->validate()); + $v->rule('creditCard', 'test', array($type, 'mastercard', 'visa')); + $this->assertFalse($v->validate()); + $v->rule('creditCard', 'test', $type); + $this->assertFalse($v->validate()); + $v->rule('creditCard', 'test', $type, array($type, 'mastercard', 'visa')); + $this->assertFalse($v->validate()); + $v->rule('creditCard', 'test', 'invalidCardName'); + $this->assertFalse($v->validate()); + $v->rule('creditCard', 'test', 'invalidCardName', array('invalidCardName', 'mastercard', 'visa')); + $this->assertFalse($v->validate()); + unset($v); + } + } + } } function sampleFunctionCallback($field, $value, array $params) {