[Validator] Simplified IBAN validation algorithm

This commit is contained in:
Bernhard Schussek 2014-04-10 18:24:43 +02:00
parent 97243bcd02
commit fd58870ac0
2 changed files with 82 additions and 21 deletions

View File

@ -13,10 +13,12 @@ namespace Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/** /**
* @author Manuel Reinhard <manu@sprain.ch> * @author Manuel Reinhard <manu@sprain.ch>
* @author Michael Schummel * @author Michael Schummel
* @author Bernhard Schussek <bschussek@gmail.com>
* @link http://www.michael-schummel.de/2007/10/05/iban-prufung-mit-php/ * @link http://www.michael-schummel.de/2007/10/05/iban-prufung-mit-php/
*/ */
class IbanValidator extends ConstraintValidator class IbanValidator extends ConstraintValidator
@ -30,41 +32,98 @@ class IbanValidator extends ConstraintValidator
return; return;
} }
// An IBAN without a country code is not an IBAN. if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
if (0 === preg_match('/[A-Z]/', $value)) { throw new UnexpectedTypeException($value, 'string');
$this->context->addViolation($constraint->message, array('{{ value }}' => $value)); }
// Remove spaces
$canonicalized = str_replace(' ', '', $value);
if (strlen($canonicalized) < 4) {
$this->context->addViolation($constraint->message, array(
'{{ value }}' => $value,
));
return; return;
} }
$teststring = preg_replace('/\s+/', '', $value); // The IBAN must have at least 4 characters, start with a country
// code and contain only digits and (uppercase) characters
if (strlen($teststring) < 4) { if (strlen($canonicalized) < 4 || !ctype_upper($canonicalized{0})
$this->context->addViolation($constraint->message, array('{{ value }}' => $value)); || !ctype_upper($canonicalized{1}) || !ctype_alnum($canonicalized)) {
$this->context->addViolation($constraint->message, array(
'{{ value }}' => $value,
));
return; return;
} }
$teststring = substr($teststring, 4) // Move the first four characters to the end
.strval(ord($teststring{0}) - 55) // e.g. CH93 0076 2011 6238 5295 7
.strval(ord($teststring{1}) - 55) // -> 0076 2011 6238 5295 7 CH93
.substr($teststring, 2, 2); $canonicalized = substr($canonicalized, 4).substr($canonicalized, 0, 4);
$teststring = preg_replace_callback('/[A-Z]/', function ($letter) { // Convert all remaining letters to their ordinals
return intval(ord(strtolower($letter[0])) - 87); // The result is an integer, which is too large for PHP's int
}, $teststring); // data type, so we store it in a string instead.
// e.g. 0076 2011 6238 5295 7 CH93
// -> 0076 2011 6238 5295 7 121893
$checkSum = $this->toBigInt($canonicalized);
$rest = 0; if (false === $checkSum) {
$strlen = strlen($teststring); $this->context->addViolation($constraint->message, array(
for ($pos = 0; $pos < $strlen; $pos += 7) { '{{ value }}' => $value,
$part = strval($rest).substr($teststring, $pos, 7); ));
$rest = intval($part) % 97;
return;
} }
if ($rest != 1) { // Do a modulo-97 operation on the large integer
$this->context->addViolation($constraint->message, array('{{ value }}' => $value)); // We cannot use PHP's modulo operator, so we calculate the
// modulo step-wisely instead
if (1 !== $this->bigModulo97($checkSum)) {
$this->context->addViolation($constraint->message, array(
'{{ value }}' => $value,
));
return; return;
} }
} }
private function toBigInt($string)
{
$chars = str_split($string);
$bigInt = '';
foreach ($chars as $char) {
// Convert uppercase characters to ordinals, starting with 10 for "A"
if (ctype_upper($char)) {
$bigInt .= (ord($char) - 55);
continue;
}
// Disallow lowercase characters
if (ctype_lower($char)) {
return false;
}
// Simply append digits
$bigInt .= $char;
}
return $bigInt;
}
private function bigModulo97($bigInt)
{
$parts = str_split($bigInt, 7);
$rest = 0;
foreach ($parts as $part) {
$rest = ($rest.$part) % 97;
}
return $rest;
}
} }

View File

@ -54,6 +54,7 @@ class IbanValidatorTest extends \PHPUnit_Framework_TestCase
{ {
return array( return array(
array('CH9300762011623852957'), // Switzerland without spaces array('CH9300762011623852957'), // Switzerland without spaces
array('CH93 0076 2011 6238 5295 7'), // Switzerland with multiple spaces
//Country list //Country list
//http://www.rbs.co.uk/corporate/international/g0/guide-to-international-business/regulatory-information/iban/iban-example.ashx //http://www.rbs.co.uk/corporate/international/g0/guide-to-international-business/regulatory-information/iban/iban-example.ashx
@ -182,6 +183,7 @@ class IbanValidatorTest extends \PHPUnit_Framework_TestCase
array('foo'), array('foo'),
array('123'), array('123'),
array('0750447346'), array('0750447346'),
array('CH930076201162385295]'),
//Ibans with lower case values are invalid //Ibans with lower case values are invalid
array('Ae260211000000230064016'), array('Ae260211000000230064016'),