This commit was merged in pull request #86.
This commit is contained in:
@@ -8,6 +8,7 @@ use DateTime;
|
||||
use OCA\Mitgliederverwaltung\Db\Family;
|
||||
use OCA\Mitgliederverwaltung\Db\FamilyMapper;
|
||||
use OCA\Mitgliederverwaltung\Db\MemberMapper;
|
||||
use OCA\Mitgliederverwaltung\Validation\IbanValidator;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\Exception;
|
||||
@@ -42,6 +43,7 @@ class FamilyService {
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
$this->validateRequiredFields($data);
|
||||
$this->validateIban($data);
|
||||
|
||||
$now = (new DateTime())->format('Y-m-d H:i:s');
|
||||
|
||||
@@ -112,6 +114,7 @@ class FamilyService {
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update(int $id, array $data): array {
|
||||
$this->validateIban($data);
|
||||
$family = $this->familyMapper->findById($id);
|
||||
|
||||
if (isset($data['name'])) {
|
||||
@@ -225,6 +228,21 @@ class FamilyService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IBAN if provided and non-empty.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateIban(array $data): void {
|
||||
$iban = $data['ibanEncrypted'] ?? null;
|
||||
if ($iban !== null && trim($iban) !== '') {
|
||||
$error = IbanValidator::getError($iban);
|
||||
if ($error !== null) {
|
||||
throw new ValidationException($error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build response array with linked members.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user