From f291504bd0a748c14d45f0f66dab92fda129b17d Mon Sep 17 00:00:00 2001
From: Lorenz Stechauner <lorenz.stechauner@necronda.net>
Date: Mon, 27 Feb 2023 17:45:37 +0100
Subject: [PATCH] Add IBAN validation

---
 WGneu/Utils.cs                         |   6 ++
 WGneu/Validator.cs                     | 107 ++++++++++++++++++-------
 WGneu/Windows/MemberListWindow.xaml    |   1 +
 WGneu/Windows/MemberListWindow.xaml.cs |  28 ++++---
 4 files changed, 103 insertions(+), 39 deletions(-)

diff --git a/WGneu/Utils.cs b/WGneu/Utils.cs
index d9ae7b2..66982de 100644
--- a/WGneu/Utils.cs
+++ b/WGneu/Utils.cs
@@ -21,5 +21,11 @@ namespace WGneu {
                     yield return childOfChild;
             }
         }
+
+        public static int Modulo(string a, int b) {
+            if (!a.All(char.IsDigit))
+                throw new ArgumentException("First argument has to be a decimal string");
+            return a.Select(ch => ch - '0').Aggregate((sum, n) => (sum * 10 + n) % b);
+        }
     }
 }
diff --git a/WGneu/Validator.cs b/WGneu/Validator.cs
index 3a017cf..25acf20 100644
--- a/WGneu/Validator.cs
+++ b/WGneu/Validator.cs
@@ -32,12 +32,19 @@ namespace WGneu {
             { "423", Array.Empty<string[]>() },
         };
 
-
-        public static ValidationResult CheckNumericInput(TextBox input) {
-            return CheckNumericInput(input, -1);
+        public static void SetInputInvalid(TextBox input) {
+            input.BorderBrush = System.Windows.Media.Brushes.Red;
         }
 
-        private static ValidationResult CheckNumericInput(TextBox input, int maxLen) {
+        public static void SetInputValid(TextBox input) {
+            input.ClearValue(TextBox.BorderBrushProperty);
+        }
+
+        public static ValidationResult CheckNumericInput(TextBox input, bool optional) {
+            return CheckNumericInput(input, optional, -1);
+        }
+
+        private static ValidationResult CheckNumericInput(TextBox input, bool optional, int maxLen) {
             string text = "";
             int pos = input.CaretIndex;
             for (int i = 0; i < input.Text.Length; i++) {
@@ -50,6 +57,11 @@ namespace WGneu {
             input.Text = text;
             input.CaretIndex = pos;
 
+            if (text.Length == 0) {
+                if (optional) return new(true, null);
+                return new(false, "Wert ist nicht optional");
+            }
+
             if (maxLen >= 0 && input.Text.Length > maxLen) {
                 input.Text = input.Text.Substring(0, maxLen);
                 input.CaretIndex = Math.Min(pos, maxLen);
@@ -58,14 +70,14 @@ namespace WGneu {
             return new(true, null);
         }
 
-        public static ValidationResult CheckPhoneNumber(TextBox input) {
+        public static ValidationResult CheckPhoneNumber(TextBox input, bool optional) {
             string text = "";
             int pos = input.CaretIndex;
-            for (int i = 0, v = 0; i < input.Text.Length; i++) {
+            for (int i = 0, v = 0; i < input.Text.Length && v < 30; i++) {
                 char ch = input.Text[i];
                 if (v == 0 && input.Text.Length - i >= 2 && ch == '0' && input.Text[i + 1] == '0') {
                     v++; i++;
-                    text += "+";
+                    text += '+';
                 } else if (ch == '(' && input.Text.Length - i >= 3 && input.Text[i + 1] == '0' && input.Text[i + 2] == ')') {
                     i += 2;
                 } else if (v == 0 && ch == '0') {
@@ -76,16 +88,16 @@ namespace WGneu {
                     text += ch;
                 } else if (v > 0 && char.IsDigit(ch)) {
                     if (PHONE_NRS.Any(kv => text == "+" + kv.Key))
-                        text += " ";
+                        text += ' ';
                     if (text.StartsWith("+43 ")) {
                         var nr = text[4..];
                         var vws = PHONE_NRS["43"];
                         if (v >= 4 && v - 4 < vws.Length && vws[v - 4].Any(vw => nr.StartsWith(vw)))
-                            text += " ";
+                            text += ' ';
                         else if (nr == "1")
-                            text += " ";
+                            text += ' ';
                         else if (v == 7 && nr.Length == 4)
-                            text += " ";
+                            text += ' ';
                     }
                     v++;
                     text += ch;
@@ -96,7 +108,7 @@ namespace WGneu {
             input.Text = text;
             input.CaretIndex = pos;
 
-            if (text.Length == 0)
+            if (optional && text.Length == 0)
                 return new(true, null);
             if (text.Length < 10)
                 return new(false, "Telefonnummer zu kurz");
@@ -104,11 +116,11 @@ namespace WGneu {
             return new(true, null);
         }
 
-        public static ValidationResult CheckEmailAddress(TextBox input) {
+        public static ValidationResult CheckEmailAddress(TextBox input, bool optional) {
             string text = "";
             int pos = input.CaretIndex;
             bool domain = false;
-            for (int i = 0; i < input.Text.Length; i++) {
+            for (int i = 0; i < input.Text.Length && text.Length < 256; i++) {
                 char ch = input.Text[i];
                 if (domain) {
                     if ((char.IsAscii(ch) && char.IsLetterOrDigit(ch)) || ".-_öäüßÖÄÜẞ".Any(c => c == ch)) {
@@ -116,7 +128,8 @@ namespace WGneu {
                             text += char.ToLower(ch);
                     }
                 } else {
-                    if (ch == '@') domain = true;
+                    if (ch == '@')
+                        domain = true;
                     if (!char.IsControl(ch) && !char.IsWhiteSpace(ch))
                         text += ch;
                 }
@@ -127,10 +140,13 @@ namespace WGneu {
             input.Text = text;
             input.CaretIndex = pos;
 
-            if (text.Length == 0)
-                return new(true, null);
-            else if (text[0] == '@' || !domain)
+            if (text.Length == 0) {
+                if (optional) return new(true, null);
+                return new(false, "E-Mail-Adresse ist nicht optional");
+            } else if (text[0] == '@' || !domain) {
                 return new(false, "E-Mail-Adresse ungültig");
+            }
+
             var last = text.Split(".").Last();
             if (last.Length < 2 || !last.All(ch => char.IsAscii(ch) && char.IsLower(ch)))
                 return new(false, "E-Mail-Adresse ungültig");
@@ -138,8 +154,49 @@ namespace WGneu {
             return new(true, null);
         }
 
-        public static ValidationResult CheckLfbisNr(TextBox input) {
-            var res = CheckNumericInput(input, 7);
+        public static ValidationResult CheckIban(TextBox input, bool optional) {
+            string text = "";
+            int pos = input.CaretIndex;
+            int v = 0;
+            for (int i = 0; i < input.Text.Length && v < 34; i++) {
+                char ch = input.Text[i];
+                if (char.IsLetterOrDigit(ch) && char.IsAscii(ch)) {
+                    if (((v < 2 && char.IsLetter(ch)) || (v >= 2 && v < 4 && char.IsDigit(ch)) || v >= 4) &&
+                        ((!text.StartsWith("AT") && !text.StartsWith("DE")) || char.IsDigit(ch)))
+                    {
+                        if (v != 0 && v % 4 == 0)
+                            text += ' ';
+                        v++;
+                        text += char.ToUpper(ch);
+                    }
+                }
+                if (i == input.CaretIndex - 1)
+                    pos = text.Length;
+                if (text.StartsWith("AT") && v >= 20)
+                    break;
+                else if (text.StartsWith("DE") && v >= 22)
+                    break;
+            }
+            input.Text = text;
+            input.CaretIndex = pos;
+
+            if (optional && text.Length == 0) {
+                return new(true, null);
+            } else if (v < 5 || (text.StartsWith("AT") && v != 20) || (text.StartsWith("DE") && v != 22)) {
+                return new(false, "IBAN hat falsche Länge");
+            }
+
+            var validation = (text[4..] + text[..4]).Replace(" ", "")
+                .Select(ch => char.IsDigit(ch) ? ch.ToString() : (ch - 'A' + 10).ToString())
+                .Aggregate((a, b) => a + b);
+            if (Utils.Modulo(validation, 97) != 1)
+                return new(false, "Prüfsumme der IBAN ist falsch");
+
+            return new(true, null);
+        }
+
+        public static ValidationResult CheckLfbisNr(TextBox input, bool optional) {
+            var res = CheckNumericInput(input, optional, 7);
             if (!res.IsValid)
                 return res;
             if (input.Text.Length == 0)
@@ -152,16 +209,8 @@ namespace WGneu {
             return new(true, "Not implemented yet");
         }
 
-        public static ValidationResult CheckUstIdInput(TextBox input) {
+        public static ValidationResult CheckUstIdInput(TextBox input, bool optional) {
             return new(false, "Not implemented yet");
         }
-
-        public static void SetInputInvalid(TextBox input) {
-            input.BorderBrush = System.Windows.Media.Brushes.Red;
-        }
-
-        public static void SetInputValid(TextBox input) {
-            input.ClearValue(TextBox.BorderBrushProperty);
-        }
     }
 }
diff --git a/WGneu/Windows/MemberListWindow.xaml b/WGneu/Windows/MemberListWindow.xaml
index fd39913..1efeb36 100644
--- a/WGneu/Windows/MemberListWindow.xaml
+++ b/WGneu/Windows/MemberListWindow.xaml
@@ -155,6 +155,7 @@
 
                 <Label Content="IBAN:" HorizontalAlignment="Left" Margin="10,12,0,0" VerticalAlignment="Top" Padding="2"/>
                 <TextBox x:Name="IbanInput" IsReadOnly="True"
+                         TextChanged="IbanInput_TextChanged" LostFocus="IbanInput_LostFocus"
                         Margin="0,10,10,0" VerticalAlignment="Top" FontSize="14" Padding="2" Grid.Column="1" Height="25"/>
 
                 <Label Content="BIC:" HorizontalAlignment="Left" Margin="10,42,0,0" VerticalAlignment="Top" Padding="2"/>
diff --git a/WGneu/Windows/MemberListWindow.xaml.cs b/WGneu/Windows/MemberListWindow.xaml.cs
index df454a9..5fd18e7 100644
--- a/WGneu/Windows/MemberListWindow.xaml.cs
+++ b/WGneu/Windows/MemberListWindow.xaml.cs
@@ -385,8 +385,8 @@ namespace WGneu.Windows {
             return true;  // TODO
         }
 
-        private void InputTextChanged(TextBox input, Func<TextBox, ValidationResult> checker) {
-            var res = checker(input);
+        private void InputTextChanged(TextBox input, bool optional, Func<TextBox, bool, ValidationResult> checker) {
+            var res = checker(input, optional);
             Valid[input] = res.IsValid;
             if (res.IsValid)
                 Validator.SetInputValid(input);
@@ -395,8 +395,8 @@ namespace WGneu.Windows {
             UpdateButtons();
         }
 
-        private void InputLostFocus(TextBox input, Func<TextBox, ValidationResult> checker, string? msg) {
-            var res = checker(input);
+        private void InputLostFocus(TextBox input, bool optional, Func<TextBox, bool, ValidationResult> checker, string? msg) {
+            var res = checker(input, optional);
             if (!res.IsValid)
                 MessageBox.Show(res.ErrorContent.ToString(), msg ?? res.ErrorContent.ToString(), MessageBoxButton.OK, MessageBoxImage.Warning);
         }
@@ -406,27 +406,35 @@ namespace WGneu.Windows {
         }
 
         private void PhoneNrInput_TextChanged(object sender, RoutedEventArgs e) {
-            InputTextChanged((TextBox)sender, Validator.CheckPhoneNumber);
+            InputTextChanged((TextBox)sender, true, Validator.CheckPhoneNumber);
         }
 
         private void PhoneNrInput_LostFocus(object sender, RoutedEventArgs e) {
-            InputLostFocus((TextBox)sender, Validator.CheckPhoneNumber, null);
+            InputLostFocus((TextBox)sender, true, Validator.CheckPhoneNumber, null);
         }
 
         private void EmailInput_TextChanged(object sender, RoutedEventArgs e) {
-            InputTextChanged((TextBox)sender, Validator.CheckEmailAddress);
+            InputTextChanged((TextBox)sender, true, Validator.CheckEmailAddress);
         }
 
         private void EmailInput_LostFocus(object sender, RoutedEventArgs e) {
-            InputLostFocus((TextBox)sender, Validator.CheckEmailAddress, null);
+            InputLostFocus((TextBox)sender, true, Validator.CheckEmailAddress, null);
+        }
+
+        private void IbanInput_TextChanged(object sender, RoutedEventArgs e) {
+            InputTextChanged((TextBox)sender, true, Validator.CheckIban);
+        }
+
+        private void IbanInput_LostFocus(object sender, RoutedEventArgs e) {
+            InputLostFocus((TextBox)sender, true, Validator.CheckIban, null);
         }
 
         private void LfbisNrInput_TextChanged(object sender, RoutedEventArgs e) {
-            InputTextChanged((TextBox)sender, Validator.CheckLfbisNr);
+            InputTextChanged((TextBox)sender, true, Validator.CheckLfbisNr);
         }
 
         private void LfbisNrInput_LostFocus(object sender, RoutedEventArgs e) {
-            InputLostFocus((TextBox)sender, Validator.CheckLfbisNr, "Betriebsnummer ungültig");
+            InputLostFocus((TextBox)sender, true, Validator.CheckLfbisNr, "Betriebsnummer ungültig");
         }
     }
 }