1<?php 2 3/** 4 * @see https://github.com/laminas/laminas-validator for the canonical source repository 5 * @copyright https://github.com/laminas/laminas-validator/blob/master/COPYRIGHT.md 6 * @license https://github.com/laminas/laminas-validator/blob/master/LICENSE.md New BSD License 7 */ 8 9namespace Laminas\Validator; 10 11use Laminas\Stdlib\ArrayUtils; 12use Traversable; 13 14/** 15 * Validates IBAN Numbers (International Bank Account Numbers) 16 */ 17class Iban extends AbstractValidator 18{ 19 const NOTSUPPORTED = 'ibanNotSupported'; 20 const SEPANOTSUPPORTED = 'ibanSepaNotSupported'; 21 const FALSEFORMAT = 'ibanFalseFormat'; 22 const CHECKFAILED = 'ibanCheckFailed'; 23 24 /** 25 * Validation failure message template definitions 26 * 27 * @var array 28 */ 29 protected $messageTemplates = [ 30 self::NOTSUPPORTED => 'Unknown country within the IBAN', 31 self::SEPANOTSUPPORTED => 'Countries outside the Single Euro Payments Area (SEPA) are not supported', 32 self::FALSEFORMAT => 'The input has a false IBAN format', 33 self::CHECKFAILED => 'The input has failed the IBAN check', 34 ]; 35 36 /** 37 * Optional country code by ISO 3166-1 38 * 39 * @var string|null 40 */ 41 protected $countryCode; 42 43 /** 44 * Optionally allow IBAN codes from non-SEPA countries. Defaults to true 45 * 46 * @var bool 47 */ 48 protected $allowNonSepa = true; 49 50 /** 51 * The SEPA country codes 52 * 53 * @var array<ISO 3166-1> 54 */ 55 protected static $sepaCountries = [ 56 'AT', 'BE', 'BG', 'CY', 'CZ', 'DK', 'FO', 'GL', 'EE', 'FI', 'FR', 'DE', 57 'GI', 'GR', 'HU', 'IS', 'IE', 'IT', 'LV', 'LI', 'LT', 'LU', 'MT', 'MC', 58 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'CH', 'GB', 'SM', 59 'HR', 60 ]; 61 62 /** 63 * IBAN regexes by country code 64 * 65 * @var array 66 */ 67 protected static $ibanRegex = [ 68 'AD' => 'AD[0-9]{2}[0-9]{4}[0-9]{4}[A-Z0-9]{12}', 69 'AE' => 'AE[0-9]{2}[0-9]{3}[0-9]{16}', 70 'AL' => 'AL[0-9]{2}[0-9]{8}[A-Z0-9]{16}', 71 'AT' => 'AT[0-9]{2}[0-9]{5}[0-9]{11}', 72 'AZ' => 'AZ[0-9]{2}[A-Z]{4}[A-Z0-9]{20}', 73 'BA' => 'BA[0-9]{2}[0-9]{3}[0-9]{3}[0-9]{8}[0-9]{2}', 74 'BE' => 'BE[0-9]{2}[0-9]{3}[0-9]{7}[0-9]{2}', 75 'BG' => 'BG[0-9]{2}[A-Z]{4}[0-9]{4}[0-9]{2}[A-Z0-9]{8}', 76 'BH' => 'BH[0-9]{2}[A-Z]{4}[A-Z0-9]{14}', 77 'BR' => 'BR[0-9]{2}[0-9]{8}[0-9]{5}[0-9]{10}[A-Z][A-Z0-9]', 78 'BY' => 'BY[0-9]{2}[A-Z0-9]{4}[0-9]{4}[A-Z0-9]{16}', 79 'CH' => 'CH[0-9]{2}[0-9]{5}[A-Z0-9]{12}', 80 'CR' => 'CR[0-9]{2}[0-9]{3}[0-9]{14}', 81 'CY' => 'CY[0-9]{2}[0-9]{3}[0-9]{5}[A-Z0-9]{16}', 82 'CZ' => 'CZ[0-9]{2}[0-9]{20}', 83 'DE' => 'DE[0-9]{2}[0-9]{8}[0-9]{10}', 84 'DO' => 'DO[0-9]{2}[A-Z0-9]{4}[0-9]{20}', 85 'DK' => 'DK[0-9]{2}[0-9]{14}', 86 'EE' => 'EE[0-9]{2}[0-9]{2}[0-9]{2}[0-9]{11}[0-9]{1}', 87 'ES' => 'ES[0-9]{2}[0-9]{4}[0-9]{4}[0-9]{1}[0-9]{1}[0-9]{10}', 88 'FI' => 'FI[0-9]{2}[0-9]{6}[0-9]{7}[0-9]{1}', 89 'FO' => 'FO[0-9]{2}[0-9]{4}[0-9]{9}[0-9]{1}', 90 'FR' => 'FR[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}', 91 'GB' => 'GB[0-9]{2}[A-Z]{4}[0-9]{6}[0-9]{8}', 92 'GE' => 'GE[0-9]{2}[A-Z]{2}[0-9]{16}', 93 'GI' => 'GI[0-9]{2}[A-Z]{4}[A-Z0-9]{15}', 94 'GL' => 'GL[0-9]{2}[0-9]{4}[0-9]{9}[0-9]{1}', 95 'GR' => 'GR[0-9]{2}[0-9]{3}[0-9]{4}[A-Z0-9]{16}', 96 'GT' => 'GT[0-9]{2}[A-Z0-9]{4}[A-Z0-9]{20}', 97 'HR' => 'HR[0-9]{2}[0-9]{7}[0-9]{10}', 98 'HU' => 'HU[0-9]{2}[0-9]{3}[0-9]{4}[0-9]{1}[0-9]{15}[0-9]{1}', 99 'IE' => 'IE[0-9]{2}[A-Z]{4}[0-9]{6}[0-9]{8}', 100 'IL' => 'IL[0-9]{2}[0-9]{3}[0-9]{3}[0-9]{13}', 101 'IS' => 'IS[0-9]{2}[0-9]{4}[0-9]{2}[0-9]{6}[0-9]{10}', 102 'IT' => 'IT[0-9]{2}[A-Z]{1}[0-9]{5}[0-9]{5}[A-Z0-9]{12}', 103 'KW' => 'KW[0-9]{2}[A-Z]{4}[0-9]{22}', 104 'KZ' => 'KZ[0-9]{2}[0-9]{3}[A-Z0-9]{13}', 105 'LB' => 'LB[0-9]{2}[0-9]{4}[A-Z0-9]{20}', 106 'LI' => 'LI[0-9]{2}[0-9]{5}[A-Z0-9]{12}', 107 'LT' => 'LT[0-9]{2}[0-9]{5}[0-9]{11}', 108 'LU' => 'LU[0-9]{2}[0-9]{3}[A-Z0-9]{13}', 109 'LV' => 'LV[0-9]{2}[A-Z]{4}[A-Z0-9]{13}', 110 'MC' => 'MC[0-9]{2}[0-9]{5}[0-9]{5}[A-Z0-9]{11}[0-9]{2}', 111 'MD' => 'MD[0-9]{2}[A-Z0-9]{20}', 112 'ME' => 'ME[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}', 113 'MK' => 'MK[0-9]{2}[0-9]{3}[A-Z0-9]{10}[0-9]{2}', 114 'MR' => 'MR13[0-9]{5}[0-9]{5}[0-9]{11}[0-9]{2}', 115 'MT' => 'MT[0-9]{2}[A-Z]{4}[0-9]{5}[A-Z0-9]{18}', 116 'MU' => 'MU[0-9]{2}[A-Z]{4}[0-9]{2}[0-9]{2}[0-9]{12}[0-9]{3}[A-Z]{3}', 117 'NL' => 'NL[0-9]{2}[A-Z]{4}[0-9]{10}', 118 'NO' => 'NO[0-9]{2}[0-9]{4}[0-9]{6}[0-9]{1}', 119 'PK' => 'PK[0-9]{2}[A-Z]{4}[A-Z0-9]{16}', 120 'PL' => 'PL[0-9]{2}[0-9]{8}[0-9]{16}', 121 'PS' => 'PS[0-9]{2}[A-Z]{4}[A-Z0-9]{21}', 122 'PT' => 'PT[0-9]{2}[0-9]{4}[0-9]{4}[0-9]{11}[0-9]{2}', 123 'RO' => 'RO[0-9]{2}[A-Z]{4}[A-Z0-9]{16}', 124 'RS' => 'RS[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}', 125 'SA' => 'SA[0-9]{2}[0-9]{2}[A-Z0-9]{18}', 126 'SE' => 'SE[0-9]{2}[0-9]{3}[0-9]{16}[0-9]{1}', 127 'SI' => 'SI[0-9]{2}[0-9]{5}[0-9]{8}[0-9]{2}', 128 'SK' => 'SK[0-9]{2}[0-9]{4}[0-9]{6}[0-9]{10}', 129 'SM' => 'SM[0-9]{2}[A-Z]{1}[0-9]{5}[0-9]{5}[A-Z0-9]{12}', 130 'TN' => 'TN59[0-9]{2}[0-9]{3}[0-9]{13}[0-9]{2}', 131 'TR' => 'TR[0-9]{2}[0-9]{5}[A-Z0-9]{1}[A-Z0-9]{16}', 132 'VG' => 'VG[0-9]{2}[A-Z]{4}[0-9]{16}', 133 ]; 134 135 /** 136 * Sets validator options 137 * 138 * @param array|Traversable $options OPTIONAL 139 */ 140 public function __construct($options = []) 141 { 142 if ($options instanceof Traversable) { 143 $options = ArrayUtils::iteratorToArray($options); 144 } 145 146 if (array_key_exists('country_code', $options)) { 147 $this->setCountryCode($options['country_code']); 148 } 149 150 if (array_key_exists('allow_non_sepa', $options)) { 151 $this->setAllowNonSepa($options['allow_non_sepa']); 152 } 153 154 parent::__construct($options); 155 } 156 157 /** 158 * Returns the optional country code by ISO 3166-1 159 * 160 * @return string|null 161 */ 162 public function getCountryCode() 163 { 164 return $this->countryCode; 165 } 166 167 /** 168 * Sets an optional country code by ISO 3166-1 169 * 170 * @param string|null $countryCode 171 * @return $this provides a fluent interface 172 * @throws Exception\InvalidArgumentException 173 */ 174 public function setCountryCode($countryCode = null) 175 { 176 if ($countryCode !== null) { 177 $countryCode = (string) $countryCode; 178 179 if (! isset(static::$ibanRegex[$countryCode])) { 180 throw new Exception\InvalidArgumentException( 181 "Country code '{$countryCode}' invalid by ISO 3166-1 or not supported" 182 ); 183 } 184 } 185 186 $this->countryCode = $countryCode; 187 return $this; 188 } 189 190 /** 191 * Returns the optional allow non-sepa countries setting 192 * 193 * @return bool 194 */ 195 public function allowNonSepa() 196 { 197 return $this->allowNonSepa; 198 } 199 200 /** 201 * Sets the optional allow non-sepa countries setting 202 * 203 * @param bool $allowNonSepa 204 * @return $this provides a fluent interface 205 */ 206 public function setAllowNonSepa($allowNonSepa) 207 { 208 $this->allowNonSepa = (bool) $allowNonSepa; 209 return $this; 210 } 211 212 /** 213 * Returns true if $value is a valid IBAN 214 * 215 * @param string $value 216 * @return bool 217 */ 218 public function isValid($value) 219 { 220 if (! is_string($value)) { 221 $this->error(self::FALSEFORMAT); 222 return false; 223 } 224 225 $value = str_replace(' ', '', strtoupper($value)); 226 $this->setValue($value); 227 228 $countryCode = $this->getCountryCode(); 229 if ($countryCode === null) { 230 $countryCode = substr($value, 0, 2); 231 } 232 233 if (! array_key_exists($countryCode, static::$ibanRegex)) { 234 $this->setValue($countryCode); 235 $this->error(self::NOTSUPPORTED); 236 return false; 237 } 238 239 if (! $this->allowNonSepa && ! in_array($countryCode, static::$sepaCountries)) { 240 $this->setValue($countryCode); 241 $this->error(self::SEPANOTSUPPORTED); 242 return false; 243 } 244 245 if (! preg_match('/^' . static::$ibanRegex[$countryCode] . '$/', $value)) { 246 $this->error(self::FALSEFORMAT); 247 return false; 248 } 249 250 $format = substr($value, 4) . substr($value, 0, 4); 251 $format = str_replace( 252 ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 253 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'], 254 ['10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', 255 '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35'], 256 $format 257 ); 258 259 $temp = intval(substr($format, 0, 1)); 260 $len = strlen($format); 261 for ($x = 1; $x < $len; ++$x) { 262 $temp *= 10; 263 $temp += intval(substr($format, $x, 1)); 264 $temp %= 97; 265 } 266 267 if ($temp != 1) { 268 $this->error(self::CHECKFAILED); 269 return false; 270 } 271 272 return true; 273 } 274} 275