IbanValidator.php 13 KB

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