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 }
|