annotate vendor/symfony/validator/Constraints/IbanValidator.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /*
Chris@0 4 * This file is part of the Symfony package.
Chris@0 5 *
Chris@0 6 * (c) Fabien Potencier <fabien@symfony.com>
Chris@0 7 *
Chris@0 8 * For the full copyright and license information, please view the LICENSE
Chris@0 9 * file that was distributed with this source code.
Chris@0 10 */
Chris@0 11
Chris@0 12 namespace Symfony\Component\Validator\Constraints;
Chris@0 13
Chris@0 14 use Symfony\Component\Validator\Constraint;
Chris@0 15 use Symfony\Component\Validator\ConstraintValidator;
Chris@0 16 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
Chris@0 17
Chris@0 18 /**
Chris@0 19 * @author Manuel Reinhard <manu@sprain.ch>
Chris@0 20 * @author Michael Schummel
Chris@0 21 * @author Bernhard Schussek <bschussek@gmail.com>
Chris@0 22 *
Chris@0 23 * @see http://www.michael-schummel.de/2007/10/05/iban-prufung-mit-php/
Chris@0 24 */
Chris@0 25 class IbanValidator extends ConstraintValidator
Chris@0 26 {
Chris@0 27 /**
Chris@0 28 * IBAN country specific formats.
Chris@0 29 *
Chris@0 30 * The first 2 characters from an IBAN format are the two-character ISO country code.
Chris@0 31 * The following 2 characters represent the check digits calculated from the rest of the IBAN characters.
Chris@0 32 * The rest are up to thirty alphanumeric characters for
Chris@0 33 * a BBAN (Basic Bank Account Number) which has a fixed length per country and,
Chris@0 34 * included within it, a bank identifier with a fixed position and a fixed length per country
Chris@0 35 *
Chris@12 36 * @see https://www.swift.com/sites/default/files/resources/iban_registry.pdf
Chris@0 37 */
Chris@17 38 private static $formats = [
Chris@0 39 'AD' => 'AD\d{2}\d{4}\d{4}[\dA-Z]{12}', // Andorra
Chris@0 40 'AE' => 'AE\d{2}\d{3}\d{16}', // United Arab Emirates
Chris@0 41 'AL' => 'AL\d{2}\d{8}[\dA-Z]{16}', // Albania
Chris@0 42 'AO' => 'AO\d{2}\d{21}', // Angola
Chris@0 43 'AT' => 'AT\d{2}\d{5}\d{11}', // Austria
Chris@0 44 'AX' => 'FI\d{2}\d{6}\d{7}\d{1}', // Aland Islands
Chris@0 45 'AZ' => 'AZ\d{2}[A-Z]{4}[\dA-Z]{20}', // Azerbaijan
Chris@0 46 'BA' => 'BA\d{2}\d{3}\d{3}\d{8}\d{2}', // Bosnia and Herzegovina
Chris@0 47 'BE' => 'BE\d{2}\d{3}\d{7}\d{2}', // Belgium
Chris@0 48 'BF' => 'BF\d{2}\d{23}', // Burkina Faso
Chris@0 49 'BG' => 'BG\d{2}[A-Z]{4}\d{4}\d{2}[\dA-Z]{8}', // Bulgaria
Chris@0 50 'BH' => 'BH\d{2}[A-Z]{4}[\dA-Z]{14}', // Bahrain
Chris@0 51 'BI' => 'BI\d{2}\d{12}', // Burundi
Chris@0 52 'BJ' => 'BJ\d{2}[A-Z]{1}\d{23}', // Benin
Chris@14 53 'BY' => 'BY\d{2}[\dA-Z]{4}\d{4}[\dA-Z]{16}', // Belarus - https://bank.codes/iban/structure/belarus/
Chris@0 54 'BL' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Barthelemy
Chris@0 55 'BR' => 'BR\d{2}\d{8}\d{5}\d{10}[A-Z][\dA-Z]', // Brazil
Chris@0 56 'CG' => 'CG\d{2}\d{23}', // Congo
Chris@0 57 'CH' => 'CH\d{2}\d{5}[\dA-Z]{12}', // Switzerland
Chris@0 58 'CI' => 'CI\d{2}[A-Z]{1}\d{23}', // Ivory Coast
Chris@0 59 'CM' => 'CM\d{2}\d{23}', // Cameron
Chris@14 60 'CR' => 'CR\d{2}0\d{3}\d{14}', // Costa Rica
Chris@0 61 'CV' => 'CV\d{2}\d{21}', // Cape Verde
Chris@0 62 'CY' => 'CY\d{2}\d{3}\d{5}[\dA-Z]{16}', // Cyprus
Chris@0 63 'CZ' => 'CZ\d{2}\d{20}', // Czech Republic
Chris@0 64 'DE' => 'DE\d{2}\d{8}\d{10}', // Germany
Chris@0 65 'DO' => 'DO\d{2}[\dA-Z]{4}\d{20}', // Dominican Republic
Chris@0 66 'DK' => 'DK\d{2}\d{4}\d{10}', // Denmark
Chris@0 67 'DZ' => 'DZ\d{2}\d{20}', // Algeria
Chris@0 68 'EE' => 'EE\d{2}\d{2}\d{2}\d{11}\d{1}', // Estonia
Chris@0 69 'ES' => 'ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}', // Spain (also includes Canary Islands, Ceuta and Melilla)
Chris@0 70 'FI' => 'FI\d{2}\d{6}\d{7}\d{1}', // Finland
Chris@0 71 'FO' => 'FO\d{2}\d{4}\d{9}\d{1}', // Faroe Islands
Chris@0 72 'FR' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // France
Chris@0 73 'GF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Guyana
Chris@0 74 'GB' => 'GB\d{2}[A-Z]{4}\d{6}\d{8}', // United Kingdom of Great Britain and Northern Ireland
Chris@0 75 'GE' => 'GE\d{2}[A-Z]{2}\d{16}', // Georgia
Chris@0 76 'GI' => 'GI\d{2}[A-Z]{4}[\dA-Z]{15}', // Gibraltar
Chris@0 77 'GL' => 'GL\d{2}\d{4}\d{9}\d{1}', // Greenland
Chris@0 78 'GP' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Guadeloupe
Chris@0 79 'GR' => 'GR\d{2}\d{3}\d{4}[\dA-Z]{16}', // Greece
Chris@0 80 'GT' => 'GT\d{2}[\dA-Z]{4}[\dA-Z]{20}', // Guatemala
Chris@0 81 'HR' => 'HR\d{2}\d{7}\d{10}', // Croatia
Chris@0 82 'HU' => 'HU\d{2}\d{3}\d{4}\d{1}\d{15}\d{1}', // Hungary
Chris@0 83 'IE' => 'IE\d{2}[A-Z]{4}\d{6}\d{8}', // Ireland
Chris@0 84 'IL' => 'IL\d{2}\d{3}\d{3}\d{13}', // Israel
Chris@0 85 'IR' => 'IR\d{2}\d{22}', // Iran
Chris@0 86 'IS' => 'IS\d{2}\d{4}\d{2}\d{6}\d{10}', // Iceland
Chris@0 87 'IT' => 'IT\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // Italy
Chris@0 88 'JO' => 'JO\d{2}[A-Z]{4}\d{4}[\dA-Z]{18}', // Jordan
Chris@0 89 'KW' => 'KW\d{2}[A-Z]{4}\d{22}', // KUWAIT
Chris@0 90 'KZ' => 'KZ\d{2}\d{3}[\dA-Z]{13}', // Kazakhstan
Chris@0 91 'LB' => 'LB\d{2}\d{4}[\dA-Z]{20}', // LEBANON
Chris@0 92 'LI' => 'LI\d{2}\d{5}[\dA-Z]{12}', // Liechtenstein (Principality of)
Chris@0 93 'LT' => 'LT\d{2}\d{5}\d{11}', // Lithuania
Chris@0 94 'LU' => 'LU\d{2}\d{3}[\dA-Z]{13}', // Luxembourg
Chris@0 95 'LV' => 'LV\d{2}[A-Z]{4}[\dA-Z]{13}', // Latvia
Chris@0 96 'MC' => 'MC\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Monaco
Chris@0 97 'MD' => 'MD\d{2}[\dA-Z]{2}[\dA-Z]{18}', // Moldova
Chris@0 98 'ME' => 'ME\d{2}\d{3}\d{13}\d{2}', // Montenegro
Chris@0 99 'MF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Martin (French part)
Chris@0 100 'MG' => 'MG\d{2}\d{23}', // Madagascar
Chris@0 101 'MK' => 'MK\d{2}\d{3}[\dA-Z]{10}\d{2}', // Macedonia, Former Yugoslav Republic of
Chris@0 102 'ML' => 'ML\d{2}[A-Z]{1}\d{23}', // Mali
Chris@0 103 'MQ' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Martinique
Chris@0 104 'MR' => 'MR13\d{5}\d{5}\d{11}\d{2}', // Mauritania
Chris@0 105 'MT' => 'MT\d{2}[A-Z]{4}\d{5}[\dA-Z]{18}', // Malta
Chris@0 106 'MU' => 'MU\d{2}[A-Z]{4}\d{2}\d{2}\d{12}\d{3}[A-Z]{3}', // Mauritius
Chris@0 107 'MZ' => 'MZ\d{2}\d{21}', // Mozambique
Chris@0 108 'NC' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // New Caledonia
Chris@0 109 'NL' => 'NL\d{2}[A-Z]{4}\d{10}', // The Netherlands
Chris@0 110 'NO' => 'NO\d{2}\d{4}\d{6}\d{1}', // Norway
Chris@0 111 'PF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Polynesia
Chris@0 112 'PK' => 'PK\d{2}[A-Z]{4}[\dA-Z]{16}', // Pakistan
Chris@0 113 'PL' => 'PL\d{2}\d{8}\d{16}', // Poland
Chris@0 114 'PM' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Saint Pierre et Miquelon
Chris@0 115 'PS' => 'PS\d{2}[A-Z]{4}[\dA-Z]{21}', // Palestine, State of
Chris@0 116 'PT' => 'PT\d{2}\d{4}\d{4}\d{11}\d{2}', // Portugal (plus Azores and Madeira)
Chris@0 117 'QA' => 'QA\d{2}[A-Z]{4}[\dA-Z]{21}', // Qatar
Chris@0 118 'RE' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Reunion
Chris@0 119 'RO' => 'RO\d{2}[A-Z]{4}[\dA-Z]{16}', // Romania
Chris@0 120 'RS' => 'RS\d{2}\d{3}\d{13}\d{2}', // Serbia
Chris@0 121 'SA' => 'SA\d{2}\d{2}[\dA-Z]{18}', // Saudi Arabia
Chris@0 122 'SE' => 'SE\d{2}\d{3}\d{16}\d{1}', // Sweden
Chris@0 123 'SI' => 'SI\d{2}\d{5}\d{8}\d{2}', // Slovenia
Chris@0 124 'SK' => 'SK\d{2}\d{4}\d{6}\d{10}', // Slovak Republic
Chris@0 125 'SM' => 'SM\d{2}[A-Z]{1}\d{5}\d{5}[\dA-Z]{12}', // San Marino
Chris@0 126 'SN' => 'SN\d{2}[A-Z]{1}\d{23}', // Senegal
Chris@0 127 'TF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // French Southern Territories
Chris@0 128 'TL' => 'TL\d{2}\d{3}\d{14}\d{2}', // Timor-Leste
Chris@0 129 'TN' => 'TN59\d{2}\d{3}\d{13}\d{2}', // Tunisia
Chris@0 130 'TR' => 'TR\d{2}\d{5}[\dA-Z]{1}[\dA-Z]{16}', // Turkey
Chris@12 131 'UA' => 'UA\d{2}\d{6}[\dA-Z]{19}', // Ukraine
Chris@17 132 'VA' => 'VA\d{2}\d{3}\d{15}', // Vatican City State
Chris@0 133 'VG' => 'VG\d{2}[A-Z]{4}\d{16}', // Virgin Islands, British
Chris@0 134 'WF' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Wallis and Futuna Islands
Chris@0 135 'XK' => 'XK\d{2}\d{4}\d{10}\d{2}', // Republic of Kosovo
Chris@0 136 'YT' => 'FR\d{2}\d{5}\d{5}[\dA-Z]{11}\d{2}', // Mayotte
Chris@17 137 ];
Chris@0 138
Chris@0 139 /**
Chris@0 140 * {@inheritdoc}
Chris@0 141 */
Chris@0 142 public function validate($value, Constraint $constraint)
Chris@0 143 {
Chris@0 144 if (!$constraint instanceof Iban) {
Chris@0 145 throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Iban');
Chris@0 146 }
Chris@0 147
Chris@0 148 if (null === $value || '' === $value) {
Chris@0 149 return;
Chris@0 150 }
Chris@0 151
Chris@17 152 if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
Chris@0 153 throw new UnexpectedTypeException($value, 'string');
Chris@0 154 }
Chris@0 155
Chris@0 156 $value = (string) $value;
Chris@0 157
Chris@0 158 // Remove spaces and convert to uppercase
Chris@0 159 $canonicalized = str_replace(' ', '', strtoupper($value));
Chris@0 160
Chris@0 161 // The IBAN must contain only digits and characters...
Chris@0 162 if (!ctype_alnum($canonicalized)) {
Chris@0 163 $this->context->buildViolation($constraint->message)
Chris@0 164 ->setParameter('{{ value }}', $this->formatValue($value))
Chris@0 165 ->setCode(Iban::INVALID_CHARACTERS_ERROR)
Chris@0 166 ->addViolation();
Chris@0 167
Chris@0 168 return;
Chris@0 169 }
Chris@0 170
Chris@0 171 // ...start with a two-letter country code
Chris@0 172 $countryCode = substr($canonicalized, 0, 2);
Chris@0 173
Chris@0 174 if (!ctype_alpha($countryCode)) {
Chris@0 175 $this->context->buildViolation($constraint->message)
Chris@0 176 ->setParameter('{{ value }}', $this->formatValue($value))
Chris@0 177 ->setCode(Iban::INVALID_COUNTRY_CODE_ERROR)
Chris@0 178 ->addViolation();
Chris@0 179
Chris@0 180 return;
Chris@0 181 }
Chris@0 182
Chris@0 183 // ...have a format available
Chris@18 184 if (!\array_key_exists($countryCode, self::$formats)) {
Chris@0 185 $this->context->buildViolation($constraint->message)
Chris@0 186 ->setParameter('{{ value }}', $this->formatValue($value))
Chris@0 187 ->setCode(Iban::NOT_SUPPORTED_COUNTRY_CODE_ERROR)
Chris@0 188 ->addViolation();
Chris@0 189
Chris@0 190 return;
Chris@0 191 }
Chris@0 192
Chris@0 193 // ...and have a valid format
Chris@0 194 if (!preg_match('/^'.self::$formats[$countryCode].'$/', $canonicalized)
Chris@0 195 ) {
Chris@0 196 $this->context->buildViolation($constraint->message)
Chris@0 197 ->setParameter('{{ value }}', $this->formatValue($value))
Chris@0 198 ->setCode(Iban::INVALID_FORMAT_ERROR)
Chris@0 199 ->addViolation();
Chris@0 200
Chris@0 201 return;
Chris@0 202 }
Chris@0 203
Chris@0 204 // Move the first four characters to the end
Chris@0 205 // e.g. CH93 0076 2011 6238 5295 7
Chris@0 206 // -> 0076 2011 6238 5295 7 CH93
Chris@0 207 $canonicalized = substr($canonicalized, 4).substr($canonicalized, 0, 4);
Chris@0 208
Chris@0 209 // Convert all remaining letters to their ordinals
Chris@0 210 // The result is an integer, which is too large for PHP's int
Chris@0 211 // data type, so we store it in a string instead.
Chris@0 212 // e.g. 0076 2011 6238 5295 7 CH93
Chris@0 213 // -> 0076 2011 6238 5295 7 121893
Chris@0 214 $checkSum = self::toBigInt($canonicalized);
Chris@0 215
Chris@0 216 // Do a modulo-97 operation on the large integer
Chris@0 217 // We cannot use PHP's modulo operator, so we calculate the
Chris@0 218 // modulo step-wisely instead
Chris@0 219 if (1 !== self::bigModulo97($checkSum)) {
Chris@0 220 $this->context->buildViolation($constraint->message)
Chris@0 221 ->setParameter('{{ value }}', $this->formatValue($value))
Chris@0 222 ->setCode(Iban::CHECKSUM_FAILED_ERROR)
Chris@0 223 ->addViolation();
Chris@0 224 }
Chris@0 225 }
Chris@0 226
Chris@0 227 private static function toBigInt($string)
Chris@0 228 {
Chris@0 229 $chars = str_split($string);
Chris@0 230 $bigInt = '';
Chris@0 231
Chris@0 232 foreach ($chars as $char) {
Chris@0 233 // Convert uppercase characters to ordinals, starting with 10 for "A"
Chris@0 234 if (ctype_upper($char)) {
Chris@17 235 $bigInt .= (\ord($char) - 55);
Chris@0 236
Chris@0 237 continue;
Chris@0 238 }
Chris@0 239
Chris@0 240 // Simply append digits
Chris@0 241 $bigInt .= $char;
Chris@0 242 }
Chris@0 243
Chris@0 244 return $bigInt;
Chris@0 245 }
Chris@0 246
Chris@0 247 private static function bigModulo97($bigInt)
Chris@0 248 {
Chris@0 249 $parts = str_split($bigInt, 7);
Chris@0 250 $rest = 0;
Chris@0 251
Chris@0 252 foreach ($parts as $part) {
Chris@0 253 $rest = ($rest.$part) % 97;
Chris@0 254 }
Chris@0 255
Chris@0 256 return $rest;
Chris@0 257 }
Chris@0 258 }