Files

162 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\Mitgliederverwaltung\Validation;
/**
* IBAN validation with MOD-97 checksum and country-specific format checks.
*
* Supports all European IBAN formats with special focus on DE (Germany).
*
* Part of Issue #26.
*/
class IbanValidator {
/**
* Country-specific IBAN lengths.
* Source: ISO 13616 / SWIFT IBAN Registry.
*
* @var array<string, int>
*/
private const COUNTRY_LENGTHS = [
'AL' => 28, 'AD' => 24, 'AT' => 20, 'AZ' => 28, 'BH' => 22,
'BY' => 28, 'BE' => 16, 'BA' => 20, 'BR' => 29, 'BG' => 22,
'CR' => 22, 'HR' => 21, 'CY' => 28, 'CZ' => 24, 'DK' => 18,
'DO' => 28, 'TL' => 23, 'EE' => 20, 'FO' => 18, 'FI' => 18,
'FR' => 27, 'GE' => 22, 'DE' => 22, 'GI' => 23, 'GR' => 27,
'GL' => 18, 'GT' => 28, 'HU' => 28, 'IS' => 26, 'IQ' => 23,
'IE' => 22, 'IL' => 23, 'IT' => 27, 'JO' => 30, 'KZ' => 20,
'XK' => 20, 'KW' => 30, 'LV' => 21, 'LB' => 28, 'LI' => 21,
'LT' => 20, 'LU' => 20, 'MK' => 19, 'MT' => 31, 'MR' => 27,
'MU' => 30, 'MC' => 27, 'MD' => 24, 'ME' => 22, 'NL' => 18,
'NO' => 15, 'PK' => 24, 'PS' => 29, 'PL' => 28, 'PT' => 25,
'QA' => 29, 'RO' => 24, 'LC' => 32, 'SM' => 27, 'ST' => 25,
'SA' => 24, 'RS' => 22, 'SC' => 31, 'SK' => 24, 'SI' => 19,
'ES' => 24, 'SE' => 24, 'CH' => 21, 'TN' => 24, 'TR' => 26,
'UA' => 29, 'AE' => 23, 'GB' => 22, 'VG' => 24,
];
/**
* Validate an IBAN string.
*
* @param string $iban The IBAN to validate
* @return bool True if the IBAN is valid
*/
public static function isValid(string $iban): bool {
$cleaned = self::clean($iban);
if (strlen($cleaned) < 15 || strlen($cleaned) > 34) {
return false;
}
// Must start with 2 letters + 2 digits
if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', $cleaned)) {
return false;
}
// Check country-specific length
$country = substr($cleaned, 0, 2);
if (isset(self::COUNTRY_LENGTHS[$country])) {
if (strlen($cleaned) !== self::COUNTRY_LENGTHS[$country]) {
return false;
}
}
// MOD-97 checksum
return self::mod97Check($cleaned);
}
/**
* Get a human-readable validation error for an invalid IBAN.
*
* @param string $iban The IBAN to check
* @return string|null Error message in German, or null if valid
*/
public static function getError(string $iban): ?string {
$cleaned = self::clean($iban);
if (strlen($cleaned) === 0) {
return 'IBAN darf nicht leer sein.';
}
if (strlen($cleaned) < 15) {
return 'IBAN ist zu kurz (mindestens 15 Zeichen).';
}
if (strlen($cleaned) > 34) {
return 'IBAN ist zu lang (maximal 34 Zeichen).';
}
if (!preg_match('/^[A-Z]{2}/', $cleaned)) {
return 'IBAN muss mit einem Laendercode beginnen (z.B. DE).';
}
if (!preg_match('/^[A-Z]{2}[0-9]{2}/', $cleaned)) {
return 'IBAN muss nach dem Laendercode zwei Pruefziffern enthalten.';
}
if (!preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/', $cleaned)) {
return 'IBAN darf nur Buchstaben und Ziffern enthalten.';
}
$country = substr($cleaned, 0, 2);
if (isset(self::COUNTRY_LENGTHS[$country])) {
$expected = self::COUNTRY_LENGTHS[$country];
if (strlen($cleaned) !== $expected) {
return sprintf(
'IBAN fuer %s muss genau %d Zeichen lang sein (aktuell: %d).',
$country,
$expected,
strlen($cleaned)
);
}
}
if (!self::mod97Check($cleaned)) {
return 'IBAN-Pruefsumme ist ungueltig.';
}
return null; // Valid
}
/**
* Perform MOD-97 checksum validation per ISO 13616.
*
* 1. Move first 4 characters to end
* 2. Convert letters to numbers (A=10, B=11, ..., Z=35)
* 3. Compute number modulo 97
* 4. Result must equal 1
*/
private static function mod97Check(string $iban): bool {
// Move first 4 chars (country + check digits) to end
$rearranged = substr($iban, 4) . substr($iban, 0, 4);
// Convert letters to numbers
$numericStr = '';
for ($i = 0; $i < strlen($rearranged); $i++) {
$char = $rearranged[$i];
if ($char >= 'A' && $char <= 'Z') {
$numericStr .= (string)(ord($char) - 55);
} else {
$numericStr .= $char;
}
}
// Process in chunks to avoid integer overflow
$remainder = 0;
for ($i = 0; $i < strlen($numericStr); $i++) {
$remainder = ($remainder * 10 + (int)$numericStr[$i]) % 97;
}
return $remainder === 1;
}
/**
* Strip all whitespace and convert to uppercase.
*/
private static function clean(string $iban): string {
return strtoupper(preg_replace('/\s+/', '', trim($iban)));
}
}