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