Parcourir la source

Add CSRF protection and access logging to admin functions

Medowar il y a 1 mois
Parent
commit
aa18a6e399
13 fichiers modifiés avec 1747 ajouts et 930 suppressions
  1. 200 131
      admin/admins.php
  2. 135 90
      admin/categories.php
  3. 27 17
      admin/faq.php
  4. 58 31
      admin/login.php
  5. 236 93
      admin/orders.php
  6. 163 87
      admin/organizations.php
  7. 293 151
      admin/products.php
  8. 51 26
      admin/settings.php
  9. 29 16
      cart.php
  10. 95 29
      checkout.php
  11. 344 202
      includes/functions.php
  12. 46 22
      index.php
  13. 70 35
      product.php

+ 200 - 131
admin/admins.php

@@ -1,158 +1,202 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
 // Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
-    header('Location: login.php');
-    exit;
+if (!isset($_SESSION["admin_logged_in"]) || !$_SESSION["admin_logged_in"]) {
+    header("Location: login.php");
+    exit();
 }
 
-$pageTitle = 'Admins verwalten';
-$message = '';
-$messageType = '';
+$pageTitle = "Admins verwalten";
+$message = "";
+$messageType = "";
 
-function isValidAdminPasswordInput($password) {
+function isValidAdminPasswordInput($password)
+{
     return is_string($password) && strlen($password) >= 8;
 }
 
 $adminAccounts = getAdminAccounts();
 
-function isValidAdminEmailInput($email) {
+function isValidAdminEmailInput($email)
+{
     return isValidAdminEmail($email);
 }
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    if (isset($_POST['add_admin'])) {
-        $username = normalizeAdminUsername($_POST['username'] ?? '');
-        $description = normalizeAdminDescription($_POST['description'] ?? '');
-        $email = normalizeAdminEmail($_POST['email'] ?? '');
-        $password = $_POST['password'] ?? '';
-        $passwordConfirm = $_POST['password_confirm'] ?? '';
+if ($_SERVER["REQUEST_METHOD"] === "POST") {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        if (isset($_POST["add_admin"])) {
+            $username = normalizeAdminUsername($_POST["username"] ?? "");
+            $description = normalizeAdminDescription(
+                $_POST["description"] ?? "",
+            );
+            $email = normalizeAdminEmail($_POST["email"] ?? "");
+            $password = $_POST["password"] ?? "";
+            $passwordConfirm = $_POST["password_confirm"] ?? "";
 
-        if (!isValidAdminUsername($username)) {
-            $message = 'Ungültiger Benutzername. Erlaubt: 3-50 Zeichen (Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich).';
-            $messageType = 'error';
-        } elseif (isset($adminAccounts[$username])) {
-            $message = 'Dieser Benutzername existiert bereits.';
-            $messageType = 'error';
-        } elseif (!isValidAdminDescription($description)) {
-            $message = 'Beschreibung ist erforderlich (max. 120 Zeichen).';
-            $messageType = 'error';
-        } elseif (!isValidAdminEmailInput($email)) {
-            $message = 'Gültige E-Mail ist erforderlich.';
-            $messageType = 'error';
-        } elseif (!isValidAdminPasswordInput($password)) {
-            $message = 'Passwort muss mindestens 8 Zeichen lang sein.';
-            $messageType = 'error';
-        } elseif ($password !== $passwordConfirm) {
-            $message = 'Passwort und Bestätigung stimmen nicht überein.';
-            $messageType = 'error';
-        } else {
-            $adminAccounts[$username] = [
-                'password_hash' => password_hash($password, PASSWORD_BCRYPT),
-                'description' => $description,
-                'email' => $email
-            ];
-            saveAdminAccounts($adminAccounts);
-            $message = 'Admin wurde erfolgreich angelegt.';
-            $messageType = 'success';
+            if (!isValidAdminUsername($username)) {
+                $message =
+                    "Ungültiger Benutzername. Erlaubt: 3-50 Zeichen (Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich).";
+                $messageType = "error";
+            } elseif (isset($adminAccounts[$username])) {
+                $message = "Dieser Benutzername existiert bereits.";
+                $messageType = "error";
+            } elseif (!isValidAdminDescription($description)) {
+                $message = "Beschreibung ist erforderlich (max. 120 Zeichen).";
+                $messageType = "error";
+            } elseif (!isValidAdminEmailInput($email)) {
+                $message = "Gültige E-Mail ist erforderlich.";
+                $messageType = "error";
+            } elseif (!isValidAdminPasswordInput($password)) {
+                $message = "Passwort muss mindestens 8 Zeichen lang sein.";
+                $messageType = "error";
+            } elseif ($password !== $passwordConfirm) {
+                $message = "Passwort und Bestätigung stimmen nicht überein.";
+                $messageType = "error";
+            } else {
+                $adminAccounts[$username] = [
+                    "password_hash" => password_hash(
+                        $password,
+                        PASSWORD_BCRYPT,
+                    ),
+                    "description" => $description,
+                    "email" => $email,
+                ];
+                saveAdminAccounts($adminAccounts);
+                logAccess("Admin added admin account", [
+                    "username" => $username,
+                    "description" => $description,
+                ]);
+                $message = "Admin wurde erfolgreich angelegt.";
+                $messageType = "success";
+            }
         }
-    }
 
-    if (isset($_POST['update_description'])) {
-        $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? '');
-        $description = normalizeAdminDescription($_POST['description'] ?? '');
-        $email = normalizeAdminEmail($_POST['email'] ?? '');
+        if (isset($_POST["update_description"])) {
+            $targetUsername = normalizeAdminUsername(
+                $_POST["target_username"] ?? "",
+            );
+            $description = normalizeAdminDescription(
+                $_POST["description"] ?? "",
+            );
+            $email = normalizeAdminEmail($_POST["email"] ?? "");
 
-        if (!isset($adminAccounts[$targetUsername])) {
-            $message = 'Admin nicht gefunden.';
-            $messageType = 'error';
-        } elseif (!isValidAdminDescription($description)) {
-            $message = 'Beschreibung ist erforderlich (max. 120 Zeichen).';
-            $messageType = 'error';
-        } elseif (!isValidAdminEmailInput($email)) {
-            $message = 'Gültige E-Mail ist erforderlich.';
-            $messageType = 'error';
-        } else {
-            $adminAccounts[$targetUsername]['description'] = $description;
-            $adminAccounts[$targetUsername]['email'] = $email;
-            saveAdminAccounts($adminAccounts);
-            $message = 'Beschreibung und E-Mail wurden aktualisiert.';
-            $messageType = 'success';
+            if (!isset($adminAccounts[$targetUsername])) {
+                $message = "Admin nicht gefunden.";
+                $messageType = "error";
+            } elseif (!isValidAdminDescription($description)) {
+                $message = "Beschreibung ist erforderlich (max. 120 Zeichen).";
+                $messageType = "error";
+            } elseif (!isValidAdminEmailInput($email)) {
+                $message = "Gültige E-Mail ist erforderlich.";
+                $messageType = "error";
+            } else {
+                $adminAccounts[$targetUsername]["description"] = $description;
+                $adminAccounts[$targetUsername]["email"] = $email;
+                saveAdminAccounts($adminAccounts);
+                logAccess("Admin updated admin description", [
+                    "username" => $targetUsername,
+                ]);
+                $message = "Beschreibung und E-Mail wurden aktualisiert.";
+                $messageType = "success";
+            }
         }
-    }
 
-    if (isset($_POST['change_password'])) {
-        $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? '');
-        $newPassword = $_POST['new_password'] ?? '';
-        $newPasswordConfirm = $_POST['new_password_confirm'] ?? '';
+        if (isset($_POST["change_password"])) {
+            $targetUsername = normalizeAdminUsername(
+                $_POST["target_username"] ?? "",
+            );
+            $newPassword = $_POST["new_password"] ?? "";
+            $newPasswordConfirm = $_POST["new_password_confirm"] ?? "";
 
-        if (!isset($adminAccounts[$targetUsername])) {
-            $message = 'Admin nicht gefunden.';
-            $messageType = 'error';
-        } elseif (!isValidAdminPasswordInput($newPassword)) {
-            $message = 'Passwort muss mindestens 8 Zeichen lang sein.';
-            $messageType = 'error';
-        } elseif ($newPassword !== $newPasswordConfirm) {
-            $message = 'Passwort und Bestätigung stimmen nicht überein.';
-            $messageType = 'error';
-        } else {
-            $adminAccounts[$targetUsername]['password_hash'] = password_hash($newPassword, PASSWORD_BCRYPT);
-            saveAdminAccounts($adminAccounts);
-            $message = 'Passwort wurde aktualisiert.';
-            $messageType = 'success';
+            if (!isset($adminAccounts[$targetUsername])) {
+                $message = "Admin nicht gefunden.";
+                $messageType = "error";
+            } elseif (!isValidAdminPasswordInput($newPassword)) {
+                $message = "Passwort muss mindestens 8 Zeichen lang sein.";
+                $messageType = "error";
+            } elseif ($newPassword !== $newPasswordConfirm) {
+                $message = "Passwort und Bestätigung stimmen nicht überein.";
+                $messageType = "error";
+            } else {
+                $adminAccounts[$targetUsername][
+                    "password_hash"
+                ] = password_hash($newPassword, PASSWORD_BCRYPT);
+                saveAdminAccounts($adminAccounts);
+                logAccess("Admin changed admin password", [
+                    "username" => $targetUsername,
+                ]);
+                $message = "Passwort wurde aktualisiert.";
+                $messageType = "success";
+            }
         }
-    }
 
-    if (isset($_POST['delete_admin'])) {
-        $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? '');
+        if (isset($_POST["delete_admin"])) {
+            $targetUsername = normalizeAdminUsername(
+                $_POST["target_username"] ?? "",
+            );
 
-        if (!isset($adminAccounts[$targetUsername])) {
-            $message = 'Admin nicht gefunden.';
-            $messageType = 'error';
-        } else {
-            unset($adminAccounts[$targetUsername]);
-            saveAdminAccounts($adminAccounts);
+            if (!isset($adminAccounts[$targetUsername])) {
+                $message = "Admin nicht gefunden.";
+                $messageType = "error";
+            } else {
+                unset($adminAccounts[$targetUsername]);
+                saveAdminAccounts($adminAccounts);
+                logAccess("Admin deleted admin account", [
+                    "username" => $targetUsername,
+                ]);
 
-            if (isset($_SESSION['admin_username']) && $_SESSION['admin_username'] === $targetUsername) {
-                $_SESSION['admin_logged_in'] = false;
-                unset($_SESSION['admin_username']);
-                session_destroy();
-                header('Location: login.php');
-                exit;
-            }
+                if (
+                    isset($_SESSION["admin_username"]) &&
+                    $_SESSION["admin_username"] === $targetUsername
+                ) {
+                    $_SESSION["admin_logged_in"] = false;
+                    unset($_SESSION["admin_username"]);
+                    session_destroy();
+                    header("Location: login.php");
+                    exit();
+                }
 
-            $message = 'Admin wurde gelöscht.';
-            $messageType = 'success';
+                $message = "Admin wurde gelöscht.";
+                $messageType = "success";
+            }
         }
-    }
 
-    $adminAccounts = getAdminAccounts();
+        $adminAccounts = getAdminAccounts();
+    }
 }
 
-$currentAdmin = isset($_SESSION['admin_username']) ? normalizeAdminUsername($_SESSION['admin_username']) : '';
-$changeUsername = normalizeAdminUsername($_GET['change'] ?? '');
+$currentAdmin = isset($_SESSION["admin_username"])
+    ? normalizeAdminUsername($_SESSION["admin_username"])
+    : "";
+$changeUsername = normalizeAdminUsername($_GET["change"] ?? "");
 $selectedChangeUser = null;
-$editDescriptionUsername = normalizeAdminUsername($_GET['edit_description'] ?? '');
+$editDescriptionUsername = normalizeAdminUsername(
+    $_GET["edit_description"] ?? "",
+);
 $selectedDescriptionUser = null;
 
-if ($changeUsername !== '') {
+if ($changeUsername !== "") {
     if (!isset($adminAccounts[$changeUsername])) {
-        if ($message === '') {
-            $message = 'Ausgewählter Admin wurde nicht gefunden.';
-            $messageType = 'error';
+        if ($message === "") {
+            $message = "Ausgewählter Admin wurde nicht gefunden.";
+            $messageType = "error";
         }
     } else {
         $selectedChangeUser = $changeUsername;
     }
 }
 
-if ($editDescriptionUsername !== '') {
+if ($editDescriptionUsername !== "") {
     if (!isset($adminAccounts[$editDescriptionUsername])) {
-        if ($message === '') {
-            $message = 'Ausgewählter Admin wurde nicht gefunden.';
-            $messageType = 'error';
+        if ($message === "") {
+            $message = "Ausgewählter Admin wurde nicht gefunden.";
+            $messageType = "error";
         }
     } else {
         $selectedDescriptionUser = $editDescriptionUsername;
@@ -161,8 +205,8 @@ if ($editDescriptionUsername !== '') {
 
 ksort($adminAccounts);
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -172,19 +216,22 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php if ($message !== ''): ?>
+<?php if ($message !== ""): ?>
     <div class="alert alert-<?php echo $messageType; ?>">
         <?php echo htmlspecialchars($message); ?>
     </div>
 <?php endif; ?>
 
 <div class="panel">
-    <p><strong>Eingeloggt als:</strong> <?php echo htmlspecialchars($currentAdmin !== '' ? $currentAdmin : 'Unbekannt'); ?></p>
+    <p><strong>Eingeloggt als:</strong> <?php echo htmlspecialchars(
+        $currentAdmin !== "" ? $currentAdmin : "Unbekannt",
+    ); ?></p>
 </div>
 
 <div class="panel">
     <h3>Neuen Admin anlegen</h3>
     <form method="POST">
+        <?php echo csrfField(); ?>
         <div class="form-group">
             <label for="username">Benutzername *</label>
             <input type="text" id="username" name="username" required maxlength="50" pattern="[A-Za-z0-9][A-Za-z0-9._-]{2,49}" placeholder="z.B. max.mustermann">
@@ -225,22 +272,30 @@ include __DIR__ . '/../includes/header.php';
             <?php foreach ($adminAccounts as $username => $account): ?>
                 <tr>
                     <td data-label="Benutzername">
-                        <strong><?php echo htmlspecialchars($username); ?></strong>
+                        <strong><?php echo htmlspecialchars(
+                            $username,
+                        ); ?></strong>
                         <?php if ($username === $currentAdmin): ?>
                             <span class="status status-open status-self">Du</span>
                         <?php endif; ?>
                     </td>
                     <td data-label="Beschreibung">
-                        <?php echo htmlspecialchars($account['description']); ?>
+                        <?php echo htmlspecialchars($account["description"]); ?>
                     </td>
                     <td data-label="E-Mail">
-                        <?php echo htmlspecialchars($account['email']); ?>
+                        <?php echo htmlspecialchars($account["email"]); ?>
                     </td>
                     <td data-label="Aktionen">
-                        <a href="admins.php?edit_description=<?php echo urlencode($username); ?>" class="btn btn-small btn-secondary">Profil ändern</a>
-                        <a href="admins.php?change=<?php echo urlencode($username); ?>" class="btn btn-small btn-secondary">Passwort ändern</a>
+                        <a href="admins.php?edit_description=<?php echo urlencode(
+                            $username,
+                        ); ?>" class="btn btn-small btn-secondary">Profil ändern</a>
+                        <a href="admins.php?change=<?php echo urlencode(
+                            $username,
+                        ); ?>" class="btn btn-small btn-secondary">Passwort ändern</a>
                         <form method="POST" class="inline-form" onsubmit="return confirm('Admin wirklich löschen?');">
-                            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars($username); ?>">
+                            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars(
+                                $username,
+                            ); ?>">
                             <button type="submit" name="delete_admin" class="btn btn-small">Löschen</button>
                         </form>
                     </td>
@@ -253,16 +308,25 @@ include __DIR__ . '/../includes/header.php';
 
 <?php if ($selectedDescriptionUser !== null): ?>
     <div class="panel">
-        <h3>Profil ändern: <?php echo htmlspecialchars($selectedDescriptionUser); ?></h3>
+        <h3>Profil ändern: <?php echo htmlspecialchars(
+            $selectedDescriptionUser,
+        ); ?></h3>
         <form method="POST">
-            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars($selectedDescriptionUser); ?>">
+            <?php echo csrfField(); ?>
+            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars(
+                $selectedDescriptionUser,
+            ); ?>">
             <div class="form-group">
                 <label for="description_edit">Beschreibung *</label>
-                <input type="text" id="description_edit" name="description" maxlength="120" required value="<?php echo htmlspecialchars($adminAccounts[$selectedDescriptionUser]['description']); ?>">
+                <input type="text" id="description_edit" name="description" maxlength="120" required value="<?php echo htmlspecialchars(
+                    $adminAccounts[$selectedDescriptionUser]["description"],
+                ); ?>">
             </div>
             <div class="form-group">
                 <label for="email_edit">E-Mail *</label>
-                <input type="email" id="email_edit" name="email" maxlength="190" required value="<?php echo htmlspecialchars($adminAccounts[$selectedDescriptionUser]['email']); ?>">
+                <input type="email" id="email_edit" name="email" maxlength="190" required value="<?php echo htmlspecialchars(
+                    $adminAccounts[$selectedDescriptionUser]["email"],
+                ); ?>">
             </div>
             <button type="submit" name="update_description" class="btn">Profil speichern</button>
             <a href="admins.php" class="btn btn-secondary">Abbrechen</a>
@@ -272,9 +336,14 @@ include __DIR__ . '/../includes/header.php';
 
 <?php if ($selectedChangeUser !== null): ?>
     <div class="panel">
-        <h3>Passwort ändern: <?php echo htmlspecialchars($selectedChangeUser); ?></h3>
+        <h3>Passwort ändern: <?php echo htmlspecialchars(
+            $selectedChangeUser,
+        ); ?></h3>
         <form method="POST">
-            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars($selectedChangeUser); ?>">
+            <?php echo csrfField(); ?>
+            <input type="hidden" name="target_username" value="<?php echo htmlspecialchars(
+                $selectedChangeUser,
+            ); ?>">
             <div class="form-group">
                 <label for="new_password">Neues Passwort (mind. 8 Zeichen) *</label>
                 <input type="password" id="new_password" name="new_password" required minlength="8">
@@ -289,4 +358,4 @@ include __DIR__ . '/../includes/header.php';
     </div>
 <?php endif; ?>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 135 - 90
admin/categories.php

@@ -1,110 +1,139 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
 // Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
-    header('Location: login.php');
-    exit;
+if (!isset($_SESSION["admin_logged_in"]) || !$_SESSION["admin_logged_in"]) {
+    header("Location: login.php");
+    exit();
 }
 
-$pageTitle = 'Kategorien verwalten';
-$message = '';
-$messageType = '';
+$pageTitle = "Kategorien verwalten";
+$message = "";
+$messageType = "";
 $categories = getCategories();
 $products = getProducts();
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    if (isset($_POST['add_category'])) {
-        $label = normalizeCategoryLabel($_POST['label'] ?? '');
-
-        if (!isValidCategoryLabel($label)) {
-            $message = 'Bitte geben Sie einen Kategorienamen mit maximal 80 Zeichen ein.';
-            $messageType = 'error';
-        } else {
-            $categoryId = generateCategoryIdFromLabel($label, $categories);
-            $categories[] = [
-                'id' => $categoryId,
-                'label' => $label
-            ];
-            saveCategories($categories);
-            $message = 'Kategorie wurde erfolgreich angelegt.';
-            $messageType = 'success';
+if ($_SERVER["REQUEST_METHOD"] === "POST") {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        if (isset($_POST["add_category"])) {
+            $label = normalizeCategoryLabel($_POST["label"] ?? "");
+
+            if (!isValidCategoryLabel($label)) {
+                $message =
+                    "Bitte geben Sie einen Kategorienamen mit maximal 80 Zeichen ein.";
+                $messageType = "error";
+            } else {
+                $categoryId = generateCategoryIdFromLabel($label, $categories);
+                $categories[] = [
+                    "id" => $categoryId,
+                    "label" => $label,
+                ];
+                saveCategories($categories);
+                logAccess("Admin added category", [
+                    "category_id" => $categoryId,
+                    "label" => $label,
+                ]);
+                $message = "Kategorie wurde erfolgreich angelegt.";
+                $messageType = "success";
+            }
         }
-    }
 
-    if (isset($_POST['update_category'])) {
-        $categoryId = normalizeCategoryId($_POST['category_id'] ?? '');
-        $label = normalizeCategoryLabel($_POST['label'] ?? '');
-        $found = false;
-
-        if (!isValidCategoryLabel($label)) {
-            $message = 'Bitte geben Sie einen Kategorienamen mit maximal 80 Zeichen ein.';
-            $messageType = 'error';
-        } else {
-            foreach ($categories as &$category) {
-                if ($category['id'] === $categoryId) {
-                    $category['label'] = $label;
-                    $found = true;
-                    break;
-                }
-            }
-            unset($category);
+        if (isset($_POST["update_category"])) {
+            $categoryId = normalizeCategoryId($_POST["category_id"] ?? "");
+            $label = normalizeCategoryLabel($_POST["label"] ?? "");
+            $found = false;
 
-            if (!$found) {
-                $message = 'Kategorie nicht gefunden.';
-                $messageType = 'error';
+            if (!isValidCategoryLabel($label)) {
+                $message =
+                    "Bitte geben Sie einen Kategorienamen mit maximal 80 Zeichen ein.";
+                $messageType = "error";
             } else {
-                saveCategories($categories);
-                $message = 'Kategorie wurde erfolgreich aktualisiert.';
-                $messageType = 'success';
+                foreach ($categories as &$category) {
+                    if ($category["id"] === $categoryId) {
+                        $category["label"] = $label;
+                        $found = true;
+                        break;
+                    }
+                }
+                unset($category);
+
+                if (!$found) {
+                    $message = "Kategorie nicht gefunden.";
+                    $messageType = "error";
+                } else {
+                    saveCategories($categories);
+                    logAccess("Admin updated category", [
+                        "category_id" => $categoryId,
+                        "label" => $label,
+                    ]);
+                    $message = "Kategorie wurde erfolgreich aktualisiert.";
+                    $messageType = "success";
+                }
             }
         }
-    }
 
-    if (isset($_POST['delete_category'])) {
-        $categoryId = normalizeCategoryId($_POST['category_id'] ?? '');
-        $found = false;
-
-        if (isCategoryInUse($categoryId)) {
-            $message = 'Diese Kategorie wird noch von Produkten verwendet und kann nicht gelöscht werden.';
-            $messageType = 'error';
-        } else {
-            $categories = array_values(array_filter($categories, function ($category) use ($categoryId, &$found) {
-                if ($category['id'] === $categoryId) {
-                    $found = true;
-                    return false;
-                }
-                return true;
-            }));
+        if (isset($_POST["delete_category"])) {
+            $categoryId = normalizeCategoryId($_POST["category_id"] ?? "");
+            $label = "";
+            $found = false;
 
-            if (!$found) {
-                $message = 'Kategorie nicht gefunden.';
-                $messageType = 'error';
+            if (isCategoryInUse($categoryId)) {
+                $message =
+                    "Diese Kategorie wird noch von Produkten verwendet und kann nicht gelöscht werden.";
+                $messageType = "error";
             } else {
-                saveCategories($categories);
-                $message = 'Kategorie wurde gelöscht.';
-                $messageType = 'success';
+                $categories = array_values(
+                    array_filter($categories, function ($category) use (
+                        $categoryId,
+                        &$found,
+                        &$label,
+                    ) {
+                        if ($category["id"] === $categoryId) {
+                            $found = true;
+                            $label = $category["label"];
+                            return false;
+                        }
+                        return true;
+                    }),
+                );
+
+                if (!$found) {
+                    $message = "Kategorie nicht gefunden.";
+                    $messageType = "error";
+                } else {
+                    saveCategories($categories);
+                    logAccess("Admin deleted category", [
+                        "category_id" => $categoryId,
+                        "label" => $label,
+                    ]);
+                    $message = "Kategorie wurde gelöscht.";
+                    $messageType = "success";
+                }
             }
         }
-    }
 
-    $categories = getCategories();
-    $products = getProducts();
+        $categories = getCategories();
+        $products = getProducts();
+    }
 }
 
-$editCategoryId = normalizeCategoryId($_GET['edit'] ?? '');
+$editCategoryId = normalizeCategoryId($_GET["edit"] ?? "");
 $editingCategory = null;
-if ($editCategoryId !== '') {
+if ($editCategoryId !== "") {
     $editingCategory = getCategoryById($editCategoryId);
-    if ($editingCategory === null && $message === '') {
-        $message = 'Ausgewählte Kategorie wurde nicht gefunden.';
-        $messageType = 'error';
+    if ($editingCategory === null && $message === "") {
+        $message = "Ausgewählte Kategorie wurde nicht gefunden.";
+        $messageType = "error";
     }
 }
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -114,7 +143,7 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php if ($message !== ''): ?>
+<?php if ($message !== ""): ?>
     <div class="alert alert-<?php echo $messageType; ?>">
         <?php echo htmlspecialchars($message); ?>
     </div>
@@ -124,15 +153,22 @@ include __DIR__ . '/../includes/header.php';
     <div class="panel panel-lg">
         <h3>Kategorie bearbeiten</h3>
         <form method="POST">
-            <input type="hidden" name="category_id" value="<?php echo htmlspecialchars($editingCategory['id']); ?>">
+            <?php echo csrfField(); ?>
+            <input type="hidden" name="category_id" value="<?php echo htmlspecialchars(
+                $editingCategory["id"],
+            ); ?>">
             <div class="form-group">
                 <label for="category_id_display">ID</label>
-                <input type="text" id="category_id_display" value="<?php echo htmlspecialchars($editingCategory['id']); ?>" readonly>
+                <input type="text" id="category_id_display" value="<?php echo htmlspecialchars(
+                    $editingCategory["id"],
+                ); ?>" readonly>
                 <small>Die ID sollte gleichbleiben, damit Produktzuordnungen und Filter-URLs gültig bleiben.</small>
             </div>
             <div class="form-group">
                 <label for="label_edit">Name *</label>
-                <input type="text" id="label_edit" name="label" maxlength="80" required value="<?php echo htmlspecialchars($editingCategory['label']); ?>">
+                <input type="text" id="label_edit" name="label" maxlength="80" required value="<?php echo htmlspecialchars(
+                    $editingCategory["label"],
+                ); ?>">
             </div>
             <button type="submit" name="update_category" class="btn">Kategorie aktualisieren</button>
             <a href="categories.php" class="btn btn-secondary">Abbrechen</a>
@@ -169,19 +205,28 @@ include __DIR__ . '/../includes/header.php';
                     <?php
                     $productCount = 0;
                     foreach ($products as $product) {
-                        if (productHasCategory($product, $category['id'])) {
+                        if (productHasCategory($product, $category["id"])) {
                             $productCount++;
                         }
                     }
                     ?>
                     <tr>
-                        <td data-label="Name"><?php echo htmlspecialchars($category['label']); ?></td>
-                        <td data-label="ID"><code><?php echo htmlspecialchars($category['id']); ?></code></td>
+                        <td data-label="Name"><?php echo htmlspecialchars(
+                            $category["label"],
+                        ); ?></td>
+                        <td data-label="ID"><code><?php echo htmlspecialchars(
+                            $category["id"],
+                        ); ?></code></td>
                         <td data-label="Produkte"><?php echo $productCount; ?></td>
                         <td data-label="Aktionen">
-                            <a href="categories.php?edit=<?php echo urlencode($category['id']); ?>" class="btn btn-small btn-secondary">Bearbeiten</a>
+                            <a href="categories.php?edit=<?php echo urlencode(
+                                $category["id"],
+                            ); ?>" class="btn btn-small btn-secondary">Bearbeiten</a>
                             <form method="POST" class="inline-form" onsubmit="return confirm('Kategorie wirklich löschen?');">
-                                <input type="hidden" name="category_id" value="<?php echo htmlspecialchars($category['id']); ?>">
+                                <?php echo csrfField(); ?>
+                                <input type="hidden" name="category_id" value="<?php echo htmlspecialchars(
+                                    $category["id"],
+                                ); ?>">
                                 <button type="submit" name="delete_category" class="btn btn-small">Löschen</button>
                             </form>
                         </td>
@@ -192,4 +237,4 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 27 - 17
admin/faq.php

@@ -1,28 +1,35 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
 // Check admin login
-if (!isset($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) {
-    header('Location: login.php');
-    exit;
+if (!isset($_SESSION["admin_logged_in"]) || !$_SESSION["admin_logged_in"]) {
+    header("Location: login.php");
+    exit();
 }
 
-$pageTitle = 'FAQ bearbeiten';
-$message = '';
-$messageType = '';
+$pageTitle = "FAQ bearbeiten";
+$message = "";
+$messageType = "";
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_faq'])) {
-    $content = isset($_POST['content']) ? (string) $_POST['content'] : '';
-    saveFaqContent($content);
-    $message = 'FAQ-Inhalt wurde gespeichert.';
-    $messageType = 'success';
+if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["save_faq"])) {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $content = isset($_POST["content"]) ? (string) $_POST["content"] : "";
+        saveFaqContent($content);
+        logAccess("Admin updated FAQ content");
+        $message = "FAQ-Inhalt wurde gespeichert.";
+        $messageType = "success";
+    }
 }
 
 $faqContent = getFaqContent();
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -44,12 +51,15 @@ include __DIR__ . '/../includes/header.php';
     </p>
 
     <form method="POST">
+        <?php echo csrfField(); ?>
         <div class="form-group">
             <label for="content">FAQ-Inhalt (Markdown)</label>
-            <textarea id="content" name="content" rows="18"><?php echo htmlspecialchars($faqContent); ?></textarea>
+            <textarea id="content" name="content" rows="18"><?php echo htmlspecialchars(
+                $faqContent,
+            ); ?></textarea>
         </div>
         <button type="submit" name="save_faq" class="btn">Speichern</button>
     </form>
 </div>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 58 - 31
admin/login.php

@@ -1,37 +1,47 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
 // Handle logout
-if (isset($_GET['logout'])) {
-    $_SESSION['admin_logged_in'] = false;
-    unset($_SESSION['admin_username']);
+if (isset($_GET["logout"])) {
+    $_SESSION["admin_logged_in"] = false;
+    unset($_SESSION["admin_username"]);
     session_destroy();
-    header('Location: login.php');
-    exit;
+    header("Location: login.php");
+    exit();
 }
 
-$error = '';
+$error = "";
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    $username = normalizeAdminUsername($_POST['username'] ?? '');
-    $password = $_POST['password'] ?? '';
-    
-    $users = getAdminUsers();
-    if (isset($users[$username]) && password_verify($password, $users[$username])) {
-        $_SESSION['admin_logged_in'] = true;
-        $_SESSION['admin_username'] = $username;
-        header('Location: index.php');
-        exit;
+if ($_SERVER["REQUEST_METHOD"] === "POST") {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $error = "Ungültiges Token. Bitte versuchen Sie es erneut.";
     } else {
-        $error = 'Benutzername oder Passwort falsch.';
+        $username = normalizeAdminUsername($_POST["username"] ?? "");
+        $password = $_POST["password"] ?? "";
+
+        $users = getAdminUsers();
+        if (
+            isset($users[$username]) &&
+            password_verify($password, $users[$username])
+        ) {
+            $_SESSION["admin_logged_in"] = true;
+            $_SESSION["admin_username"] = $username;
+            logAccess("Admin login successful", ["username" => $username]);
+            header("Location: index.php");
+            exit();
+        } else {
+            logAccess("Admin login failed", ["username" => $username]);
+            $error = "Benutzername oder Passwort falsch.";
+        }
     }
 }
 
 // Redirect if already logged in
-if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in']) {
-    header('Location: index.php');
-    exit;
+if (isset($_SESSION["admin_logged_in"]) && $_SESSION["admin_logged_in"]) {
+    header("Location: index.php");
+    exit();
 }
 ?>
 <!DOCTYPE html>
@@ -40,32 +50,45 @@ if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in']) {
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Administration - <?php echo escape(SITE_FULL_NAME); ?></title>
-    <link rel="stylesheet" href="<?php echo escape(SITE_URL); ?>/assets/css/style.css">
+    <link rel="stylesheet" href="<?php echo escape(
+        SITE_URL,
+    ); ?>/assets/css/style.css">
 </head>
 <body class="admin-page">
     <header class="site-header">
         <div class="container header-inner">
             <a class="brand" href="<?php echo escape(SITE_URL); ?>/index.php">
-                <img class="brand-logo" src="<?php echo escape(SITE_URL); ?>/assets/branding/stadt-freising-logo.png" alt="Wappen der Stadt Freising">
+                <img class="brand-logo" src="<?php echo escape(
+                    SITE_URL,
+                ); ?>/assets/branding/stadt-freising-logo.png" alt="Wappen der Stadt Freising">
                 <div class="brand-text">
-                    <span class="brand-title"><?php echo escape(SITE_NAME); ?></span>
-                    <span class="brand-subtitle"><?php echo escape(SITE_SERVICE_HEADER); ?></span>
+                    <span class="brand-title"><?php echo escape(
+                        SITE_NAME,
+                    ); ?></span>
+                    <span class="brand-subtitle"><?php echo escape(
+                        SITE_SERVICE_HEADER,
+                    ); ?></span>
                 </div>
             </a>
-            <a href="<?php echo escape(SITE_URL); ?>/index.php" class="btn btn-secondary">Zurück zu <?php echo escape(SITE_SERVICE_NAME); ?></a>
+            <a href="<?php echo escape(
+                SITE_URL,
+            ); ?>/index.php" class="btn btn-secondary">Zurück zu <?php echo escape(
+    SITE_SERVICE_NAME,
+); ?></a>
         </div>
     </header>
     <main>
         <div class="container container-narrow page-top-gap">
             <h2>Administration</h2>
-            
+
             <?php if ($error): ?>
                 <div class="alert alert-error">
                     <?php echo htmlspecialchars($error); ?>
                 </div>
             <?php endif; ?>
-            
+
             <form method="POST" class="panel panel-lg">
+                <?php echo csrfField(); ?>
                 <div class="form-group">
                     <label for="username">Benutzername:</label>
                     <input type="text" id="username" name="username" required autofocus>
@@ -76,9 +99,13 @@ if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in']) {
                 </div>
                 <button type="submit" class="btn btn-block">Anmelden</button>
             </form>
-            
+
             <div class="text-center mt-2">
-                <a href="<?php echo escape(SITE_URL); ?>/index.php">Zurück zu <?php echo escape(SITE_SERVICE_NAME); ?></a>
+                <a href="<?php echo escape(
+                    SITE_URL,
+                ); ?>/index.php">Zurück zu <?php echo escape(
+    SITE_SERVICE_NAME,
+); ?></a>
             </div>
         </div>
     </main>

+ 236 - 93
admin/orders.php

@@ -1,70 +1,117 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
-if (empty($_SESSION['admin_logged_in'])) {
-    header('Location: login.php');
-    exit;
+if (empty($_SESSION["admin_logged_in"])) {
+    header("Location: login.php");
+    exit();
 }
 
 expirePendingOrders();
 
-$pageTitle = 'Bestellungen';
-$message = '';
-$messageType = '';
+$pageTitle = "Bestellungen";
+$message = "";
+$messageType = "";
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_item_processed'])) {
-    $result = toggleOrderItemProcessed($_POST['order_id'] ?? '', (int) ($_POST['item_index'] ?? -1));
-    $message = $result['success'] ? 'Position wurde aktualisiert.' : $result['message'];
-    $messageType = $result['success'] ? 'success' : 'error';
+if (
+    $_SERVER["REQUEST_METHOD"] === "POST" &&
+    isset($_POST["toggle_item_processed"])
+) {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $result = toggleOrderItemProcessed(
+            $_POST["order_id"] ?? "",
+            (int) ($_POST["item_index"] ?? -1),
+        );
+        $message = $result["success"]
+            ? "Position wurde aktualisiert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin toggled order item", [
+                "admin" => $_SESSION["admin_username"] ?? "unknown",
+                "order_id" => $_POST["order_id"] ?? "",
+                "item_index" => $_POST["item_index"] ?? -1,
+            ]);
+        }
+    }
 }
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['cancel_order'])) {
-    $adminUsername = $_SESSION['admin_username'] ?? '';
-    $result = cancelOrder($_POST['order_id'] ?? '', $adminUsername, $_POST['cancellation_reason'] ?? '');
-    $message = $result['success'] ? 'Bestellung wurde storniert.' : $result['message'];
-    $messageType = $result['success'] ? 'success' : 'error';
+if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["cancel_order"])) {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $adminUsername = $_SESSION["admin_username"] ?? "";
+        $result = cancelOrder(
+            $_POST["order_id"] ?? "",
+            $adminUsername,
+            $_POST["cancellation_reason"] ?? "",
+        );
+        $message = $result["success"]
+            ? "Bestellung wurde storniert."
+            : $result["message"];
+        $messageType = $result["success"] ? "success" : "error";
+
+        if ($result["success"]) {
+            logAccess("Admin cancelled order", [
+                "admin" => $adminUsername,
+                "order_id" => $_POST["order_id"] ?? "",
+            ]);
+        }
+    }
 }
 
 $orders = getOrders();
 usort($orders, function ($left, $right) {
-    return strcmp($right['created_at'], $left['created_at']);
+    return strcmp($right["created_at"], $left["created_at"]);
 });
 
-$filter = trim((string) ($_GET['filter'] ?? 'all'));
-$searchOrderId = trim((string) ($_GET['order_id'] ?? ''));
-$selectedOrderId = trim((string) ($_GET['details'] ?? $searchOrderId));
+$filter = trim((string) ($_GET["filter"] ?? "all"));
+$searchOrderId = trim((string) ($_GET["order_id"] ?? ""));
+$selectedOrderId = trim((string) ($_GET["details"] ?? $searchOrderId));
 
-if ($searchOrderId !== '') {
-    $orders = array_values(array_filter($orders, function ($order) use ($searchOrderId) {
-        return stripos($order['id'], $searchOrderId) !== false;
-    }));
+if ($searchOrderId !== "") {
+    $orders = array_values(
+        array_filter($orders, function ($order) use ($searchOrderId) {
+            return stripos($order["id"], $searchOrderId) !== false;
+        }),
+    );
 }
 
-if ($filter !== 'all') {
-    $orders = array_values(array_filter($orders, function ($order) use ($filter) {
-        switch ($filter) {
-            case 'unconfirmed':
-                return $order['confirmation_status'] === 'pending';
-            case 'expired':
-                return $order['confirmation_status'] === 'expired';
-            case 'open':
-                return $order['confirmation_status'] !== 'pending' && $order['status'] === 'open';
-            case 'partial':
-                return $order['status'] === 'partial';
-            case 'processed':
-                return $order['status'] === 'processed';
-            case 'cancelled':
-                return $order['status'] === 'cancelled';
-        }
-        return true;
-    }));
+if ($filter !== "all") {
+    $orders = array_values(
+        array_filter($orders, function ($order) use ($filter) {
+            switch ($filter) {
+                case "unconfirmed":
+                    return $order["confirmation_status"] === "pending";
+                case "expired":
+                    return $order["confirmation_status"] === "expired";
+                case "open":
+                    return $order["confirmation_status"] !== "pending" &&
+                        $order["status"] === "open";
+                case "partial":
+                    return $order["status"] === "partial";
+                case "processed":
+                    return $order["status"] === "processed";
+                case "cancelled":
+                    return $order["status"] === "cancelled";
+            }
+            return true;
+        }),
+    );
 }
 
-$selectedOrder = $selectedOrderId !== '' ? getOrderById($selectedOrderId) : null;
+$selectedOrder =
+    $selectedOrderId !== "" ? getOrderById($selectedOrderId) : null;
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -74,7 +121,7 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php if ($message !== ''): ?>
+<?php if ($message !== ""): ?>
     <div class="alert alert-<?php echo escape($messageType); ?>">
         <?php echo escape($message); ?>
     </div>
@@ -84,18 +131,34 @@ include __DIR__ . '/../includes/header.php';
     <form method="GET" class="admin-filter-form">
         <div class="admin-filter-field admin-filter-field-wide">
             <label for="order_id">Bestellnummer suchen</label>
-            <input type="text" id="order_id" name="order_id" value="<?php echo escape($searchOrderId); ?>" placeholder="z. B. FWFS-2026-001">
+            <input type="text" id="order_id" name="order_id" value="<?php echo escape(
+                $searchOrderId,
+            ); ?>" placeholder="z. B. FWFS-2026-001">
         </div>
         <div>
             <label for="filter">Filter</label>
             <select id="filter" name="filter">
-                <option value="all" <?php echo $filter === 'all' ? 'selected' : ''; ?>>Alle</option>
-                <option value="unconfirmed" <?php echo $filter === 'unconfirmed' ? 'selected' : ''; ?>>Unbestätigt</option>
-                <option value="expired" <?php echo $filter === 'expired' ? 'selected' : ''; ?>>Bestätigung abgelaufen</option>
-                <option value="open" <?php echo $filter === 'open' ? 'selected' : ''; ?>>Offen</option>
-                <option value="partial" <?php echo $filter === 'partial' ? 'selected' : ''; ?>>Teilweise bearbeitet</option>
-                <option value="processed" <?php echo $filter === 'processed' ? 'selected' : ''; ?>>Bearbeitet</option>
-                <option value="cancelled" <?php echo $filter === 'cancelled' ? 'selected' : ''; ?>>Storniert</option>
+                <option value="all" <?php echo $filter === "all"
+                    ? "selected"
+                    : ""; ?>>Alle</option>
+                <option value="unconfirmed" <?php echo $filter === "unconfirmed"
+                    ? "selected"
+                    : ""; ?>>Unbestätigt</option>
+                <option value="expired" <?php echo $filter === "expired"
+                    ? "selected"
+                    : ""; ?>>Bestätigung abgelaufen</option>
+                <option value="open" <?php echo $filter === "open"
+                    ? "selected"
+                    : ""; ?>>Offen</option>
+                <option value="partial" <?php echo $filter === "partial"
+                    ? "selected"
+                    : ""; ?>>Teilweise bearbeitet</option>
+                <option value="processed" <?php echo $filter === "processed"
+                    ? "selected"
+                    : ""; ?>>Bearbeitet</option>
+                <option value="cancelled" <?php echo $filter === "cancelled"
+                    ? "selected"
+                    : ""; ?>>Storniert</option>
             </select>
         </div>
         <div class="admin-filter-actions">
@@ -126,14 +189,30 @@ include __DIR__ . '/../includes/header.php';
             <tbody>
                 <?php foreach ($orders as $order): ?>
                     <tr>
-                        <td data-label="Bestellnummer"><strong><?php echo escape($order['id']); ?></strong></td>
-                        <td data-label="Name"><?php echo escape($order['customer_name']); ?></td>
-                        <td data-label="Organisation"><?php echo escape($order['organization_label']); ?></td>
-                        <td data-label="Artikel"><?php echo count($order['items']); ?></td>
-                        <td data-label="Erstellt"><?php echo escape(formatDate($order['created_at'])); ?></td>
-                        <td data-label="Status"><span class="status <?php echo escape(getOrderStatusClass($order)); ?>"><?php echo escape(getOrderStatusLabel($order)); ?></span></td>
+                        <td data-label="Bestellnummer"><strong><?php echo escape(
+                            $order["id"],
+                        ); ?></strong></td>
+                        <td data-label="Name"><?php echo escape(
+                            $order["customer_name"],
+                        ); ?></td>
+                        <td data-label="Organisation"><?php echo escape(
+                            $order["organization_label"],
+                        ); ?></td>
+                        <td data-label="Artikel"><?php echo count(
+                            $order["items"],
+                        ); ?></td>
+                        <td data-label="Erstellt"><?php echo escape(
+                            formatDate($order["created_at"]),
+                        ); ?></td>
+                        <td data-label="Status"><span class="status <?php echo escape(
+                            getOrderStatusClass($order),
+                        ); ?>"><?php echo escape(
+    getOrderStatusLabel($order),
+); ?></span></td>
                         <td data-label="Aktionen">
-                            <a href="orders.php?details=<?php echo urlencode($order['id']); ?>" class="btn btn-small">Details</a>
+                            <a href="orders.php?details=<?php echo urlencode(
+                                $order["id"],
+                            ); ?>" class="btn btn-small">Details</a>
                         </td>
                     </tr>
                 <?php endforeach; ?>
@@ -144,28 +223,58 @@ include __DIR__ . '/../includes/header.php';
 
 <?php if ($selectedOrder !== null): ?>
     <div class="panel">
-        <h3>Bestellung <?php echo escape($selectedOrder['id']); ?></h3>
-        <p><strong>Status:</strong> <span class="status <?php echo escape(getOrderStatusClass($selectedOrder)); ?>"><?php echo escape(getOrderStatusLabel($selectedOrder)); ?></span></p>
-        <p><strong>Name:</strong> <?php echo escape($selectedOrder['customer_name']); ?></p>
-        <p><strong>E-Mail:</strong> <?php echo escape($selectedOrder['customer_email']); ?></p>
-        <p><strong>Organisation:</strong> <?php echo escape($selectedOrder['organization_label']); ?></p>
-        <p><strong>Erstellt:</strong> <?php echo escape(formatDate($selectedOrder['created_at'])); ?></p>
-        <?php if ($selectedOrder['confirmed_at'] !== ''): ?>
-            <p><strong>Bestätigt:</strong> <?php echo escape(formatDate($selectedOrder['confirmed_at'])); ?></p>
+        <h3>Bestellung <?php echo escape($selectedOrder["id"]); ?></h3>
+        <p><strong>Status:</strong> <span class="status <?php echo escape(
+            getOrderStatusClass($selectedOrder),
+        ); ?>"><?php echo escape(
+    getOrderStatusLabel($selectedOrder),
+); ?></span></p>
+        <p><strong>Name:</strong> <?php echo escape(
+            $selectedOrder["customer_name"],
+        ); ?></p>
+        <p><strong>E-Mail:</strong> <?php echo escape(
+            $selectedOrder["customer_email"],
+        ); ?></p>
+        <p><strong>Organisation:</strong> <?php echo escape(
+            $selectedOrder["organization_label"],
+        ); ?></p>
+        <p><strong>Erstellt:</strong> <?php echo escape(
+            formatDate($selectedOrder["created_at"]),
+        ); ?></p>
+        <?php if ($selectedOrder["confirmed_at"] !== ""): ?>
+            <p><strong>Bestätigt:</strong> <?php echo escape(
+                formatDate($selectedOrder["confirmed_at"]),
+            ); ?></p>
         <?php endif; ?>
-        <?php if ($selectedOrder['confirmation_status'] === 'pending'): ?>
-            <p><strong>Bestätigung offen bis:</strong> <?php echo escape(formatDate($selectedOrder['confirmation_expires_at'])); ?></p>
+        <?php if ($selectedOrder["confirmation_status"] === "pending"): ?>
+            <p><strong>Bestätigung offen bis:</strong> <?php echo escape(
+                formatDate($selectedOrder["confirmation_expires_at"]),
+            ); ?></p>
         <?php endif; ?>
-        <?php if ($selectedOrder['admin_notified_at'] !== ''): ?>
-            <p><strong>Intern weitergeleitet:</strong> <?php echo escape(formatDate($selectedOrder['admin_notified_at'])); ?></p>
+        <?php if ($selectedOrder["admin_notified_at"] !== ""): ?>
+            <p><strong>Intern weitergeleitet:</strong> <?php echo escape(
+                formatDate($selectedOrder["admin_notified_at"]),
+            ); ?></p>
         <?php endif; ?>
-        <p><strong>Kommentar:</strong><br><?php echo $selectedOrder['comment'] !== '' ? nl2br(escape($selectedOrder['comment'])) : 'Kein Kommentar'; ?></p>
+        <p><strong>Kommentar:</strong><br><?php echo $selectedOrder[
+            "comment"
+        ] !== ""
+            ? nl2br(escape($selectedOrder["comment"]))
+            : "Kein Kommentar"; ?></p>
 
-        <?php if ($selectedOrder['status'] === 'cancelled'): ?>
+        <?php if ($selectedOrder["status"] === "cancelled"): ?>
             <div class="alert alert-warning">
-                <p><strong>Storniert am:</strong> <?php echo escape(formatDate($selectedOrder['cancelled_at'])); ?></p>
-                <p><strong>Storniert durch:</strong> <?php echo escape($selectedOrder['cancelled_by']); ?></p>
-                <p><strong>Stornogrund:</strong><br><?php echo $selectedOrder['cancellation_reason'] !== '' ? nl2br(escape($selectedOrder['cancellation_reason'])) : 'Kein Grund angegeben'; ?></p>
+                <p><strong>Storniert am:</strong> <?php echo escape(
+                    formatDate($selectedOrder["cancelled_at"]),
+                ); ?></p>
+                <p><strong>Storniert durch:</strong> <?php echo escape(
+                    $selectedOrder["cancelled_by"],
+                ); ?></p>
+                <p><strong>Stornogrund:</strong><br><?php echo $selectedOrder[
+                    "cancellation_reason"
+                ] !== ""
+                    ? nl2br(escape($selectedOrder["cancellation_reason"]))
+                    : "Kein Grund angegeben"; ?></p>
             </div>
         <?php endif; ?>
 
@@ -182,23 +291,54 @@ include __DIR__ . '/../includes/header.php';
                     </tr>
                 </thead>
                 <tbody>
-                    <?php foreach ($selectedOrder['items'] as $index => $item): ?>
+                    <?php foreach (
+                        $selectedOrder["items"]
+                        as $index => $item
+                    ): ?>
                         <tr>
-                            <td data-label="Artikel"><?php echo escape($item['product_name']); ?></td>
-                            <td data-label="Größe"><?php echo $item['size'] !== '' ? escape($item['size']) : '-'; ?></td>
-                            <td data-label="Lieferhinweis"><?php echo $item['availability_label'] !== '' ? escape($item['availability_label']) : '-'; ?></td>
+                            <td data-label="Artikel"><?php echo escape(
+                                $item["product_name"],
+                            ); ?></td>
+                            <td data-label="Größe"><?php echo $item["size"] !==
+                            ""
+                                ? escape($item["size"])
+                                : "-"; ?></td>
+                            <td data-label="Lieferhinweis"><?php echo $item[
+                                "availability_label"
+                            ] !== ""
+                                ? escape($item["availability_label"])
+                                : "-"; ?></td>
                             <td data-label="Bearbeitet">
-                                <span class="status <?php echo !empty($item['is_processed']) ? 'status-processed' : 'status-open'; ?>">
-                                    <?php echo !empty($item['is_processed']) ? 'Ja' : 'Nein'; ?>
+                                <span class="status <?php echo !empty(
+                                    $item["is_processed"]
+                                )
+                                    ? "status-processed"
+                                    : "status-open"; ?>">
+                                    <?php echo !empty($item["is_processed"])
+                                        ? "Ja"
+                                        : "Nein"; ?>
                                 </span>
                             </td>
-                            <td data-label="Aktion">
-                                <?php if ($selectedOrder['status'] !== 'cancelled' && $selectedOrder['confirmation_status'] !== 'pending' && $selectedOrder['confirmation_status'] !== 'expired'): ?>
-                                    <form method="POST">
-                                        <input type="hidden" name="order_id" value="<?php echo escape($selectedOrder['id']); ?>">
+                            <td data-label="Aktionen">
+                                <?php if (
+                                    $selectedOrder["status"] !== "cancelled" &&
+                                    $selectedOrder["confirmation_status"] !==
+                                        "pending" &&
+                                    $selectedOrder["confirmation_status"] !==
+                                        "expired"
+                                ): ?>
+                                     <form method="POST">
+                                        <?php echo csrfField(); ?>
+                                        <input type="hidden" name="order_id" value="<?php echo escape(
+                                            $selectedOrder["id"],
+                                        ); ?>">
                                         <input type="hidden" name="item_index" value="<?php echo (int) $index; ?>">
                                         <button type="submit" name="toggle_item_processed" class="btn btn-small">
-                                            <?php echo !empty($item['is_processed']) ? 'Als offen markieren' : 'Als bearbeitet markieren'; ?>
+                                            <?php echo !empty(
+                                                $item["is_processed"]
+                                            )
+                                                ? "Als offen markieren"
+                                                : "Als bearbeitet markieren"; ?>
                                         </button>
                                     </form>
                                 <?php else: ?>
@@ -211,10 +351,13 @@ include __DIR__ . '/../includes/header.php';
             </table>
         </div>
 
-        <?php if ($selectedOrder['status'] !== 'cancelled'): ?>
+        <?php if ($selectedOrder["status"] !== "cancelled"): ?>
             <h4>Bestellung stornieren</h4>
             <form method="POST" onsubmit="return confirm('Bestellung wirklich stornieren?');">
-                <input type="hidden" name="order_id" value="<?php echo escape($selectedOrder['id']); ?>">
+                <?php echo csrfField(); ?>
+                <input type="hidden" name="order_id" value="<?php echo escape(
+                    $selectedOrder["id"],
+                ); ?>">
                 <div class="form-group">
                     <label for="cancellation_reason">Stornogrund</label>
                     <textarea id="cancellation_reason" name="cancellation_reason" rows="3" placeholder="Optionaler Grund"></textarea>
@@ -225,4 +368,4 @@ include __DIR__ . '/../includes/header.php';
     </div>
 <?php endif; ?>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 163 - 87
admin/organizations.php

@@ -1,91 +1,133 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
-if (empty($_SESSION['admin_logged_in'])) {
-    header('Location: login.php');
-    exit;
+if (empty($_SESSION["admin_logged_in"])) {
+    header("Location: login.php");
+    exit();
 }
 
-$pageTitle = 'Organisationen verwalten';
-$message = '';
-$messageType = '';
+$pageTitle = "Organisationen verwalten";
+$message = "";
+$messageType = "";
 $organizations = getOrganizations(false);
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    if (isset($_POST['add_organization'])) {
-        $label = normalizeOrganizationLabel($_POST['label'] ?? '');
-        $sortOrder = (int) ($_POST['sort_order'] ?? 0);
-        $active = isset($_POST['active']);
-
-        if (!isValidOrganizationLabel($label)) {
-            $message = 'Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.';
-            $messageType = 'error';
-        } else {
-            $organizations[] = [
-                'id' => generateOrganizationIdFromLabel($label, $organizations),
-                'label' => $label,
-                'sort_order' => $sortOrder,
-                'active' => $active,
-            ];
-            saveOrganizations($organizations);
-            $message = 'Organisation wurde angelegt.';
-            $messageType = 'success';
+if ($_SERVER["REQUEST_METHOD"] === "POST") {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        if (isset($_POST["add_organization"])) {
+            $label = normalizeOrganizationLabel($_POST["label"] ?? "");
+            $sortOrder = (int) ($_POST["sort_order"] ?? 0);
+            $active = isset($_POST["active"]);
+
+            if (!isValidOrganizationLabel($label)) {
+                $message =
+                    "Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.";
+                $messageType = "error";
+            } else {
+                $orgId = generateOrganizationIdFromLabel(
+                    $label,
+                    $organizations,
+                );
+                $organizations[] = [
+                    "id" => $orgId,
+                    "label" => $label,
+                    "sort_order" => $sortOrder,
+                    "active" => $active,
+                ];
+                saveOrganizations($organizations);
+                logAccess("Admin added organization", [
+                    "org_id" => $orgId,
+                    "label" => $label,
+                ]);
+                $message = "Organisation wurde angelegt.";
+                $messageType = "success";
+            }
         }
-    }
 
-    if (isset($_POST['update_organization'])) {
-        $organizationId = normalizeOrganizationId($_POST['organization_id'] ?? '');
-        $label = normalizeOrganizationLabel($_POST['label'] ?? '');
-        $sortOrder = (int) ($_POST['sort_order'] ?? 0);
-        $active = isset($_POST['active']);
-        $updated = false;
-
-        if (!isValidOrganizationLabel($label)) {
-            $message = 'Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.';
-            $messageType = 'error';
-        } else {
-            foreach ($organizations as &$organization) {
-                if ($organization['id'] !== $organizationId) {
-                    continue;
+        if (isset($_POST["update_organization"])) {
+            $organizationId = normalizeOrganizationId(
+                $_POST["organization_id"] ?? "",
+            );
+            $label = normalizeOrganizationLabel($_POST["label"] ?? "");
+            $sortOrder = (int) ($_POST["sort_order"] ?? 0);
+            $active = isset($_POST["active"]);
+            $updated = false;
+
+            if (!isValidOrganizationLabel($label)) {
+                $message =
+                    "Bitte einen Organisationsnamen mit maximal 120 Zeichen eingeben.";
+                $messageType = "error";
+            } else {
+                foreach ($organizations as &$organization) {
+                    if ($organization["id"] !== $organizationId) {
+                        continue;
+                    }
+
+                    $organization["label"] = $label;
+                    $organization["sort_order"] = $sortOrder;
+                    $organization["active"] = $active;
+                    $updated = true;
+                    break;
+                }
+                unset($organization);
+
+                if ($updated) {
+                    saveOrganizations($organizations);
+                    logAccess("Admin updated organization", [
+                        "org_id" => $organizationId,
+                        "label" => $label,
+                    ]);
+                    $message = "Organisation wurde aktualisiert.";
+                    $messageType = "success";
+                } else {
+                    $message = "Organisation nicht gefunden.";
+                    $messageType = "error";
                 }
-
-                $organization['label'] = $label;
-                $organization['sort_order'] = $sortOrder;
-                $organization['active'] = $active;
-                $updated = true;
-                break;
             }
-            unset($organization);
+        }
 
-            if ($updated) {
-                saveOrganizations($organizations);
-                $message = 'Organisation wurde aktualisiert.';
-                $messageType = 'success';
-            } else {
-                $message = 'Organisation nicht gefunden.';
-                $messageType = 'error';
+        if (isset($_POST["delete_organization"])) {
+            $organizationId = normalizeOrganizationId(
+                $_POST["organization_id"] ?? "",
+            );
+            $orgLabel = "";
+            $found = false;
+            foreach ($organizations as $organization) {
+                if ($organization["id"] === $organizationId) {
+                    $orgLabel = $organization["label"];
+                    break;
+                }
             }
+            $organizations = array_values(
+                array_filter($organizations, function ($organization) use (
+                    $organizationId,
+                ) {
+                    return $organization["id"] !== $organizationId;
+                }),
+            );
+            saveOrganizations($organizations);
+            logAccess("Admin deleted organization", [
+                "org_id" => $organizationId,
+                "label" => $orgLabel,
+            ]);
+            $message = "Organisation wurde gelöscht.";
+            $messageType = "success";
         }
-    }
 
-    if (isset($_POST['delete_organization'])) {
-        $organizationId = normalizeOrganizationId($_POST['organization_id'] ?? '');
-        $organizations = array_values(array_filter($organizations, function ($organization) use ($organizationId) {
-            return $organization['id'] !== $organizationId;
-        }));
-        saveOrganizations($organizations);
-        $message = 'Organisation wurde gelöscht.';
-        $messageType = 'success';
+        $organizations = getOrganizations(false);
     }
-
-    $organizations = getOrganizations(false);
 }
 
-$editingOrganization = isset($_GET['edit']) ? getOrganizationById($_GET['edit']) : null;
+$editingOrganization = isset($_GET["edit"])
+    ? getOrganizationById($_GET["edit"])
+    : null;
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -95,38 +137,55 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php if ($message !== ''): ?>
+<?php if ($message !== ""): ?>
     <div class="alert alert-<?php echo escape($messageType); ?>">
         <?php echo escape($message); ?>
     </div>
 <?php endif; ?>
 
 <div class="panel panel-lg">
-    <h3><?php echo $editingOrganization ? 'Organisation bearbeiten' : 'Neue Organisation'; ?></h3>
+    <h3><?php echo $editingOrganization
+        ? "Organisation bearbeiten"
+        : "Neue Organisation"; ?></h3>
     <form method="POST">
+        <?php echo csrfField(); ?>
         <?php if ($editingOrganization): ?>
-            <input type="hidden" name="organization_id" value="<?php echo escape($editingOrganization['id']); ?>">
+            <input type="hidden" name="organization_id" value="<?php echo escape(
+                $editingOrganization["id"],
+            ); ?>">
         <?php endif; ?>
 
         <div class="form-group">
             <label for="label">Name *</label>
-            <input type="text" id="label" name="label" required maxlength="120" value="<?php echo escape($editingOrganization['label'] ?? ''); ?>">
+            <input type="text" id="label" name="label" required maxlength="120" value="<?php echo escape(
+                $editingOrganization["label"] ?? "",
+            ); ?>">
         </div>
 
         <div class="form-group">
             <label for="sort_order">Sortierung</label>
-            <input type="number" id="sort_order" name="sort_order" value="<?php echo escape((string) ($editingOrganization['sort_order'] ?? 0)); ?>">
+            <input type="number" id="sort_order" name="sort_order" value="<?php echo escape(
+                (string) ($editingOrganization["sort_order"] ?? 0),
+            ); ?>">
         </div>
 
         <div class="form-group">
             <label>
-                <input type="checkbox" name="active" value="1" <?php echo (!isset($editingOrganization['active']) || !empty($editingOrganization['active'])) ? 'checked' : ''; ?>>
+                <input type="checkbox" name="active" value="1" <?php echo !isset(
+                    $editingOrganization["active"],
+                ) || !empty($editingOrganization["active"])
+                    ? "checked"
+                    : ""; ?>>
                 Organisation ist auswählbar
             </label>
         </div>
 
-        <button type="submit" name="<?php echo $editingOrganization ? 'update_organization' : 'add_organization'; ?>" class="btn">
-            <?php echo $editingOrganization ? 'Speichern' : 'Organisation anlegen'; ?>
+        <button type="submit" name="<?php echo $editingOrganization
+            ? "update_organization"
+            : "add_organization"; ?>" class="btn">
+            <?php echo $editingOrganization
+                ? "Speichern"
+                : "Organisation anlegen"; ?>
         </button>
         <?php if ($editingOrganization): ?>
             <a href="organizations.php" class="btn btn-secondary">Abbrechen</a>
@@ -150,18 +209,35 @@ include __DIR__ . '/../includes/header.php';
             <tbody>
                 <?php foreach ($organizations as $organization): ?>
                     <tr>
-                        <td data-label="Name"><?php echo escape($organization['label']); ?></td>
-                        <td data-label="ID"><code><?php echo escape($organization['id']); ?></code></td>
-                        <td data-label="Sortierung"><?php echo (int) $organization['sort_order']; ?></td>
+                        <td data-label="Name"><?php echo escape(
+                            $organization["label"],
+                        ); ?></td>
+                        <td data-label="ID"><code><?php echo escape(
+                            $organization["id"],
+                        ); ?></code></td>
+                        <td data-label="Sortierung"><?php echo (int) $organization[
+                            "sort_order"
+                        ]; ?></td>
                         <td data-label="Status">
-                            <span class="status <?php echo !empty($organization['active']) ? 'status-open' : 'status-cancelled'; ?>">
-                                <?php echo !empty($organization['active']) ? 'Aktiv' : 'Inaktiv'; ?>
+                            <span class="status <?php echo !empty(
+                                $organization["active"]
+                            )
+                                ? "status-open"
+                                : "status-cancelled"; ?>">
+                                <?php echo !empty($organization["active"])
+                                    ? "Aktiv"
+                                    : "Inaktiv"; ?>
                             </span>
                         </td>
                         <td data-label="Aktionen">
-                            <a href="organizations.php?edit=<?php echo urlencode($organization['id']); ?>" class="btn btn-small">Bearbeiten</a>
-                            <form method="POST" class="inline-form" onsubmit="return confirm('Organisation wirklich löschen?');">
-                                <input type="hidden" name="organization_id" value="<?php echo escape($organization['id']); ?>">
+                            <a href="organizations.php?edit=<?php echo urlencode(
+                                $organization["id"],
+                            ); ?>" class="btn btn-small">Bearbeiten</a>
+                             <form method="POST" class="inline-form" onsubmit="return confirm('Organisation wirklich löschen?');">
+                                <?php echo csrfField(); ?>
+                                <input type="hidden" name="organization_id" value="<?php echo escape(
+                                    $organization["id"],
+                                ); ?>">
                                 <button type="submit" name="delete_organization" class="btn btn-secondary btn-small">Löschen</button>
                             </form>
                         </td>
@@ -172,4 +248,4 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 293 - 151
admin/products.php

@@ -1,44 +1,58 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
-if (empty($_SESSION['admin_logged_in'])) {
-    header('Location: login.php');
-    exit;
+if (empty($_SESSION["admin_logged_in"])) {
+    header("Location: login.php");
+    exit();
 }
 
-$pageTitle = 'Produkte verwalten';
-$message = '';
-$messageType = '';
+$pageTitle = "Produkte verwalten";
+$message = "";
+$messageType = "";
 $categories = getCategories();
 
-function handleImageUpload($fileInputName = 'image_file') {
-    if (!isset($_FILES[$fileInputName]) || $_FILES[$fileInputName]['error'] === UPLOAD_ERR_NO_FILE) {
-        return ['success' => true, 'filename' => null];
+function handleImageUpload($fileInputName = "image_file")
+{
+    if (
+        !isset($_FILES[$fileInputName]) ||
+        $_FILES[$fileInputName]["error"] === UPLOAD_ERR_NO_FILE
+    ) {
+        return ["success" => true, "filename" => null];
     }
 
     $file = $_FILES[$fileInputName];
-    if ($file['error'] !== UPLOAD_ERR_OK) {
-        return ['success' => false, 'message' => 'Upload fehlgeschlagen.'];
+    if ($file["error"] !== UPLOAD_ERR_OK) {
+        return ["success" => false, "message" => "Upload fehlgeschlagen."];
     }
 
-    $allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
-    $originalName = basename($file['name']);
+    $allowedExtensions = ["jpg", "jpeg", "png", "webp", "gif"];
+    $originalName = basename($file["name"]);
     $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
     if (!in_array($extension, $allowedExtensions, true)) {
-        return ['success' => false, 'message' => 'Ungültiger Dateityp.'];
+        return ["success" => false, "message" => "Ungültiger Dateityp."];
     }
 
     $finfo = new finfo(FILEINFO_MIME_TYPE);
-    $mimeType = $finfo->file($file['tmp_name']);
-    $allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+    $mimeType = $finfo->file($file["tmp_name"]);
+    $allowedMimes = ["image/jpeg", "image/png", "image/webp", "image/gif"];
     if (!in_array($mimeType, $allowedMimes, true)) {
-        return ['success' => false, 'message' => 'Die Datei ist kein gültiges Bild.'];
+        return [
+            "success" => false,
+            "message" => "Die Datei ist kein gültiges Bild.",
+        ];
     }
 
-    $uploadsDir = rtrim(UPLOADS_DIR, '/\\');
-    if (!is_dir($uploadsDir) && !mkdir($uploadsDir, 02775, true) && !is_dir($uploadsDir)) {
-        return ['success' => false, 'message' => 'Upload-Verzeichnis konnte nicht erstellt werden.'];
+    $uploadsDir = rtrim(UPLOADS_DIR, "/\\");
+    if (
+        !is_dir($uploadsDir) &&
+        !mkdir($uploadsDir, 02775, true) &&
+        !is_dir($uploadsDir)
+    ) {
+        return [
+            "success" => false,
+            "message" => "Upload-Verzeichnis konnte nicht erstellt werden.",
+        ];
     }
 
     if (is_dir($uploadsDir)) {
@@ -46,55 +60,77 @@ function handleImageUpload($fileInputName = 'image_file') {
     }
 
     if (!is_writable($uploadsDir)) {
-        return ['success' => false, 'message' => 'Upload-Verzeichnis ist nicht beschreibbar: ' . $uploadsDir];
+        return [
+            "success" => false,
+            "message" =>
+                "Upload-Verzeichnis ist nicht beschreibbar: " . $uploadsDir,
+        ];
     }
 
-    $safeBaseName = preg_replace('/[^a-zA-Z0-9_-]/', '-', pathinfo($originalName, PATHINFO_FILENAME));
-    $safeBaseName = trim((string) $safeBaseName, '-');
-    if ($safeBaseName === '') {
-        $safeBaseName = 'bild';
+    $safeBaseName = preg_replace(
+        "/[^a-zA-Z0-9_-]/",
+        "-",
+        pathinfo($originalName, PATHINFO_FILENAME),
+    );
+    $safeBaseName = trim((string) $safeBaseName, "-");
+    if ($safeBaseName === "") {
+        $safeBaseName = "bild";
     }
 
-    $targetFilename = $safeBaseName . '.' . $extension;
-    $targetPath = $uploadsDir . '/' . $targetFilename;
+    $targetFilename = $safeBaseName . "." . $extension;
+    $targetPath = $uploadsDir . "/" . $targetFilename;
     $counter = 1;
     while (file_exists($targetPath)) {
-        $targetFilename = $safeBaseName . '-' . $counter . '.' . $extension;
-        $targetPath = $uploadsDir . '/' . $targetFilename;
+        $targetFilename = $safeBaseName . "-" . $counter . "." . $extension;
+        $targetPath = $uploadsDir . "/" . $targetFilename;
         $counter++;
     }
 
-    if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
-        return ['success' => false, 'message' => 'Bild konnte nicht gespeichert werden.'];
+    if (!move_uploaded_file($file["tmp_name"], $targetPath)) {
+        return [
+            "success" => false,
+            "message" => "Bild konnte nicht gespeichert werden.",
+        ];
     }
 
-    return ['success' => true, 'filename' => $targetFilename];
+    return ["success" => true, "filename" => $targetFilename];
 }
 
-function buildProductAvailabilityFields($sizesInput, $submittedValues = [], $existingValues = []) {
-    $sizes = getProductSizes(['sizes' => (string) $sizesInput]);
+function buildProductAvailabilityFields(
+    $sizesInput,
+    $submittedValues = [],
+    $existingValues = [],
+) {
+    $sizes = getProductSizes(["sizes" => (string) $sizesInput]);
     if (empty($sizes)) {
-        $sizes = ['Standard'];
+        $sizes = ["Standard"];
     }
 
     $availabilityLabels = [];
     foreach ($sizes as $size) {
-        $fieldName = 'availability_' . str_replace([' ', ','], '_', $size);
+        $fieldName = "availability_" . str_replace([" ", ","], "_", $size);
         if (isset($submittedValues[$fieldName])) {
-            $availabilityLabels[$size] = trim((string) $submittedValues[$fieldName]);
+            $availabilityLabels[$size] = trim(
+                (string) $submittedValues[$fieldName],
+            );
         } else {
-            $availabilityLabels[$size] = trim((string) ($existingValues[$size] ?? ''));
+            $availabilityLabels[$size] = trim(
+                (string) ($existingValues[$size] ?? ""),
+            );
         }
     }
 
     return [
-        'sizes' => implode(',', $sizes),
-        'availability_labels' => $availabilityLabels,
+        "sizes" => implode(",", $sizes),
+        "availability_labels" => $availabilityLabels,
     ];
 }
 
-function getSubmittedProductCategoryIds($submittedValues) {
-    $selectedCategoryIds = normalizeProductCategoryIds($submittedValues['categories'] ?? []);
+function getSubmittedProductCategoryIds($submittedValues)
+{
+    $selectedCategoryIds = normalizeProductCategoryIds(
+        $submittedValues["categories"] ?? [],
+    );
     $validCategoryIds = [];
 
     foreach ($selectedCategoryIds as $categoryId) {
@@ -106,96 +142,145 @@ function getSubmittedProductCategoryIds($submittedValues) {
     return $validCategoryIds;
 }
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    $products = getProducts();
-
-    if (empty($categories)) {
-        $message = 'Bitte zuerst mindestens eine Kategorie anlegen.';
-        $messageType = 'error';
-    } elseif (isset($_POST['add_product']) || isset($_POST['update_product'])) {
-        $uploadResult = handleImageUpload();
-        if (!$uploadResult['success']) {
-            $message = $uploadResult['message'];
-            $messageType = 'error';
-        } else {
-            $categoryIds = getSubmittedProductCategoryIds($_POST);
-            $existingLabels = [];
-            $productId = isset($_POST['product_id']) ? (int) $_POST['product_id'] : 0;
-            foreach ($products as $product) {
-                if ((int) $product['id'] === $productId) {
-                    $existingLabels = $product['availability_labels'] ?? [];
-                    break;
+if ($_SERVER["REQUEST_METHOD"] === "POST") {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $products = getProducts();
+
+        if (empty($categories)) {
+            $message = "Bitte zuerst mindestens eine Kategorie anlegen.";
+            $messageType = "error";
+        } elseif (
+            isset($_POST["add_product"]) ||
+            isset($_POST["update_product"])
+        ) {
+            $uploadResult = handleImageUpload();
+            if (!$uploadResult["success"]) {
+                $message = $uploadResult["message"];
+                $messageType = "error";
+            } else {
+                $categoryIds = getSubmittedProductCategoryIds($_POST);
+                $existingLabels = [];
+                $productId = isset($_POST["product_id"])
+                    ? (int) $_POST["product_id"]
+                    : 0;
+                foreach ($products as $product) {
+                    if ((int) $product["id"] === $productId) {
+                        $existingLabels = $product["availability_labels"] ?? [];
+                        break;
+                    }
                 }
-            }
 
-            $sizeData = buildProductAvailabilityFields($_POST['sizes'] ?? '', $_POST, $existingLabels);
+                $sizeData = buildProductAvailabilityFields(
+                    $_POST["sizes"] ?? "",
+                    $_POST,
+                    $existingLabels,
+                );
 
-            if (empty($categoryIds)) {
-                $message = 'Bitte mindestens eine gültige Kategorie auswählen.';
-                $messageType = 'error';
-            } else {
-                $record = [
-                    'name' => sanitize($_POST['name'] ?? ''),
-                    'description' => trim((string) ($_POST['description'] ?? '')),
-                    'categories' => $categoryIds,
-                    'image' => $uploadResult['filename'] !== null ? $uploadResult['filename'] : trim((string) ($_POST['image'] ?? '')),
-                    'sizes' => $sizeData['sizes'],
-                    'availability_labels' => $sizeData['availability_labels'],
-                ];
-
-                if ($record['name'] === '') {
-                    $message = 'Bitte einen Produktnamen eingeben.';
-                    $messageType = 'error';
-                } elseif (isset($_POST['add_product'])) {
-                    $newId = empty($products) ? 1 : (max(array_map(function ($product) {
-                        return (int) $product['id'];
-                    }, $products)) + 1);
-                    $record['id'] = $newId;
-                    $products[] = $record;
-                    saveProducts($products);
-                    $message = 'Produkt wurde angelegt.';
-                    $messageType = 'success';
+                if (empty($categoryIds)) {
+                    $message =
+                        "Bitte mindestens eine gültige Kategorie auswählen.";
+                    $messageType = "error";
                 } else {
-                    $updated = false;
-                    foreach ($products as &$product) {
-                        if ((int) $product['id'] === $productId) {
-                            $record['id'] = $productId;
-                            $product = $record;
-                            $updated = true;
-                            break;
-                        }
-                    }
-                    unset($product);
-
-                    if ($updated) {
+                    $record = [
+                        "name" => sanitize($_POST["name"] ?? ""),
+                        "description" => trim(
+                            (string) ($_POST["description"] ?? ""),
+                        ),
+                        "categories" => $categoryIds,
+                        "image" =>
+                            $uploadResult["filename"] !== null
+                                ? $uploadResult["filename"]
+                                : trim((string) ($_POST["image"] ?? "")),
+                        "sizes" => $sizeData["sizes"],
+                        "availability_labels" =>
+                            $sizeData["availability_labels"],
+                    ];
+
+                    if ($record["name"] === "") {
+                        $message = "Bitte einen Produktnamen eingeben.";
+                        $messageType = "error";
+                    } elseif (isset($_POST["add_product"])) {
+                        $newId = empty($products)
+                            ? 1
+                            : max(
+                                    array_map(function ($product) {
+                                        return (int) $product["id"];
+                                    }, $products),
+                                ) + 1;
+                        $record["id"] = $newId;
+                        $products[] = $record;
                         saveProducts($products);
-                        $message = 'Produkt wurde aktualisiert.';
-                        $messageType = 'success';
+                        logAccess("Admin added product", [
+                            "product_id" => $newId,
+                            "product_name" => $record["name"],
+                        ]);
+                        $message = "Produkt wurde angelegt.";
+                        $messageType = "success";
                     } else {
-                        $message = 'Produkt nicht gefunden.';
-                        $messageType = 'error';
+                        $updated = false;
+                        foreach ($products as &$product) {
+                            if ((int) $product["id"] === $productId) {
+                                $record["id"] = $productId;
+                                $product = $record;
+                                $updated = true;
+                                break;
+                            }
+                        }
+                        unset($product);
+
+                        if ($updated) {
+                            saveProducts($products);
+                            logAccess("Admin updated product", [
+                                "product_id" => $productId,
+                                "product_name" => $record["name"],
+                            ]);
+                            $message = "Produkt wurde aktualisiert.";
+                            $messageType = "success";
+                        } else {
+                            $message = "Produkt nicht gefunden.";
+                            $messageType = "error";
+                        }
                     }
                 }
             }
         }
-    }
 
-    if (isset($_POST['delete_product'])) {
-        $productId = (int) ($_POST['product_id'] ?? 0);
-        $products = array_values(array_filter($products, function ($product) use ($productId) {
-            return (int) $product['id'] !== $productId;
-        }));
-        saveProducts($products);
-        $message = 'Produkt wurde gelöscht.';
-        $messageType = 'success';
+        if (isset($_POST["delete_product"])) {
+            $productId = (int) ($_POST["product_id"] ?? 0);
+            $productName = "";
+            foreach ($products as $product) {
+                if ((int) $product["id"] === $productId) {
+                    $productName = $product["name"];
+                    break;
+                }
+            }
+            $products = array_values(
+                array_filter($products, function ($product) use ($productId) {
+                    return (int) $product["id"] !== $productId;
+                }),
+            );
+            saveProducts($products);
+            logAccess("Admin deleted product", [
+                "product_id" => $productId,
+                "product_name" => $productName,
+            ]);
+            $message = "Produkt wurde gelöscht.";
+            $messageType = "success";
+        }
     }
 }
 
 $products = getProducts();
-$editingProduct = isset($_GET['edit']) ? getProductById((int) $_GET['edit']) : null;
+$editingProduct = isset($_GET["edit"])
+    ? getProductById((int) $_GET["edit"])
+    : null;
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -205,7 +290,7 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php if ($message !== ''): ?>
+<?php if ($message !== ""): ?>
     <div class="alert alert-<?php echo escape($messageType); ?>">
         <?php echo escape($message); ?>
     </div>
@@ -213,43 +298,63 @@ include __DIR__ . '/../includes/header.php';
 
 <?php
 $currentProduct = $editingProduct ?: [
-    'id' => '',
-    'name' => '',
-    'description' => '',
-    'categories' => [],
-    'sizes' => 'Standard',
-    'availability_labels' => ['Standard' => ''],
-    'image' => '',
+    "id" => "",
+    "name" => "",
+    "description" => "",
+    "categories" => [],
+    "sizes" => "Standard",
+    "availability_labels" => ["Standard" => ""],
+    "image" => "",
 ];
 $currentSizes = getProductSizes($currentProduct);
 if (empty($currentSizes)) {
-    $currentSizes = ['Standard'];
+    $currentSizes = ["Standard"];
 }
 ?>
 
 <div class="panel panel-lg">
-    <h3><?php echo $editingProduct ? 'Produkt bearbeiten' : 'Neues Produkt anlegen'; ?></h3>
+    <h3><?php echo $editingProduct
+        ? "Produkt bearbeiten"
+        : "Neues Produkt anlegen"; ?></h3>
     <form method="POST" enctype="multipart/form-data">
+        <?php echo csrfField(); ?>
         <?php if ($editingProduct): ?>
-            <input type="hidden" name="product_id" value="<?php echo (int) $editingProduct['id']; ?>">
+            <input type="hidden" name="product_id" value="<?php echo (int) $editingProduct[
+                "id"
+            ]; ?>">
         <?php endif; ?>
 
         <div class="form-group">
             <label for="name">Name *</label>
-            <input type="text" id="name" name="name" required value="<?php echo escape($currentProduct['name']); ?>">
+            <input type="text" id="name" name="name" required value="<?php echo escape(
+                $currentProduct["name"],
+            ); ?>">
         </div>
 
         <div class="form-group">
             <label for="description">Beschreibung</label>
-            <textarea id="description" name="description" rows="4"><?php echo escape($currentProduct['description']); ?></textarea>
+            <textarea id="description" name="description" rows="4"><?php echo escape(
+                $currentProduct["description"],
+            ); ?></textarea>
         </div>
 
         <div class="form-group">
             <label for="categories">Kategorien *</label>
-            <select id="categories" name="categories[]" multiple size="<?php echo max(3, min(8, count($categories))); ?>" required>
+            <select id="categories" name="categories[]" multiple size="<?php echo max(
+                3,
+                min(8, count($categories)),
+            ); ?>" required>
                 <?php foreach ($categories as $category): ?>
-                    <option value="<?php echo escape($category['id']); ?>" <?php echo in_array($category['id'], getProductCategoryIds($currentProduct), true) ? 'selected' : ''; ?>>
-                        <?php echo escape($category['label']); ?>
+                    <option value="<?php echo escape(
+                        $category["id"],
+                    ); ?>" <?php echo in_array(
+    $category["id"],
+    getProductCategoryIds($currentProduct),
+    true,
+)
+    ? "selected"
+    : ""; ?>>
+                        <?php echo escape($category["label"]); ?>
                     </option>
                 <?php endforeach; ?>
             </select>
@@ -257,22 +362,32 @@ if (empty($currentSizes)) {
 
         <div class="form-group">
             <label for="sizes">Größen (kommagetrennt) *</label>
-            <input type="text" id="sizes" name="sizes" required value="<?php echo escape($currentProduct['sizes']); ?>" placeholder="Standard" oninput="updateAvailabilityFields()">
+            <input type="text" id="sizes" name="sizes" required value="<?php echo escape(
+                $currentProduct["sizes"],
+            ); ?>" placeholder="Standard" oninput="updateAvailabilityFields()">
             <small>Für Artikel ohne Varianten z. B. <code>Standard</code> oder <code>Einheitsgröße</code>.</small>
         </div>
 
         <div id="availability-group">
             <?php foreach ($currentSizes as $size): ?>
                 <div class="form-group">
-                    <label>Lieferhinweis für Größe "<?php echo escape($size); ?>"</label>
-                    <textarea name="availability_<?php echo escape(str_replace([' ', ','], '_', $size)); ?>" rows="2" placeholder="Optionaler Hinweis"><?php echo escape($currentProduct['availability_labels'][$size] ?? ''); ?></textarea>
+                    <label>Lieferhinweis für Größe "<?php echo escape(
+                        $size,
+                    ); ?>"</label>
+                    <textarea name="availability_<?php echo escape(
+                        str_replace([" ", ","], "_", $size),
+                    ); ?>" rows="2" placeholder="Optionaler Hinweis"><?php echo escape(
+    $currentProduct["availability_labels"][$size] ?? "",
+); ?></textarea>
                 </div>
             <?php endforeach; ?>
         </div>
 
         <div class="form-group">
             <label for="image">Bilddateiname</label>
-            <input type="text" id="image" name="image" value="<?php echo escape($currentProduct['image']); ?>">
+            <input type="text" id="image" name="image" value="<?php echo escape(
+                $currentProduct["image"],
+            ); ?>">
         </div>
 
         <div class="form-group">
@@ -280,8 +395,12 @@ if (empty($currentSizes)) {
             <input type="file" id="image_file" name="image_file" accept=".jpg,.jpeg,.png,.webp,.gif,image/*">
         </div>
 
-        <button type="submit" name="<?php echo $editingProduct ? 'update_product' : 'add_product'; ?>" class="btn">
-            <?php echo $editingProduct ? 'Produkt aktualisieren' : 'Produkt anlegen'; ?>
+        <button type="submit" name="<?php echo $editingProduct
+            ? "update_product"
+            : "add_product"; ?>" class="btn">
+            <?php echo $editingProduct
+                ? "Produkt aktualisieren"
+                : "Produkt anlegen"; ?>
         </button>
         <?php if ($editingProduct): ?>
             <a href="products.php" class="btn btn-secondary">Abbrechen</a>
@@ -340,25 +459,48 @@ function updateAvailabilityFields() {
             <tbody>
                 <?php foreach ($products as $product): ?>
                     <tr>
-                        <td data-label="ID"><?php echo (int) $product['id']; ?></td>
-                        <td data-label="Name"><?php echo escape($product['name']); ?></td>
-                        <td data-label="Kategorien"><?php echo escape(implode(', ', getCategoryLabels(getProductCategoryIds($product)))); ?></td>
-                        <td data-label="Größen"><?php echo escape(implode(', ', getProductSizes($product))); ?></td>
+                        <td data-label="ID"><?php echo (int) $product[
+                            "id"
+                        ]; ?></td>
+                        <td data-label="Name"><?php echo escape(
+                            $product["name"],
+                        ); ?></td>
+                        <td data-label="Kategorien"><?php echo escape(
+                            implode(
+                                ", ",
+                                getCategoryLabels(
+                                    getProductCategoryIds($product),
+                                ),
+                            ),
+                        ); ?></td>
+                        <td data-label="Größen"><?php echo escape(
+                            implode(", ", getProductSizes($product)),
+                        ); ?></td>
                         <td data-label="Lieferhinweise">
                             <?php
                             $labels = [];
-                            foreach (($product['availability_labels'] ?? []) as $size => $label) {
-                                if (trim((string) $label) !== '') {
-                                    $labels[] = $size . ': ' . $label;
+                            foreach (
+                                $product["availability_labels"] ?? []
+                                as $size => $label
+                            ) {
+                                if (trim((string) $label) !== "") {
+                                    $labels[] = $size . ": " . $label;
                                 }
                             }
-                            echo empty($labels) ? 'Keine' : escape(implode(' | ', $labels));
+                            echo empty($labels)
+                                ? "Keine"
+                                : escape(implode(" | ", $labels));
                             ?>
                         </td>
                         <td data-label="Aktionen">
-                            <a href="?edit=<?php echo (int) $product['id']; ?>" class="btn btn-small">Bearbeiten</a>
+                            <a href="?edit=<?php echo (int) $product[
+                                "id"
+                            ]; ?>" class="btn btn-small">Bearbeiten</a>
                             <form method="POST" class="inline-form" onsubmit="return confirm('Produkt wirklich löschen?');">
-                                <input type="hidden" name="product_id" value="<?php echo (int) $product['id']; ?>">
+                                <?php echo csrfField(); ?>
+                                <input type="hidden" name="product_id" value="<?php echo (int) $product[
+                                    "id"
+                                ]; ?>">
                                 <button type="submit" name="delete_product" class="btn btn-secondary btn-small">Löschen</button>
                             </form>
                         </td>
@@ -369,4 +511,4 @@ function updateAvailabilityFields() {
     </div>
 <?php endif; ?>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 51 - 26
admin/settings.php

@@ -1,33 +1,45 @@
 <?php
-require_once __DIR__ . '/../config.php';
-require_once __DIR__ . '/../includes/functions.php';
+require_once __DIR__ . "/../config.php";
+require_once __DIR__ . "/../includes/functions.php";
 
-if (empty($_SESSION['admin_logged_in'])) {
-    header('Location: login.php');
-    exit;
+if (empty($_SESSION["admin_logged_in"])) {
+    header("Location: login.php");
+    exit();
 }
 
-$pageTitle = 'Einstellungen';
-$message = '';
-$messageType = '';
+$pageTitle = "Einstellungen";
+$message = "";
+$messageType = "";
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
-    $settings = [
-        'order_recipient_email' => $_POST['order_recipient_email'] ?? '',
-        'order_confirmation_required' => isset($_POST['order_confirmation_required']),
-        'order_confirmation_expiry_days' => (int) ($_POST['order_confirmation_expiry_days'] ?? 7),
-        'attach_order_pdf_to_admin_email' => isset($_POST['attach_order_pdf_to_admin_email']),
-    ];
+if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["save_settings"])) {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        $settings = [
+            "order_recipient_email" => $_POST["order_recipient_email"] ?? "",
+            "order_confirmation_required" => isset(
+                $_POST["order_confirmation_required"],
+            ),
+            "order_confirmation_expiry_days" =>
+                (int) ($_POST["order_confirmation_expiry_days"] ?? 7),
+            "attach_order_pdf_to_admin_email" => isset(
+                $_POST["attach_order_pdf_to_admin_email"],
+            ),
+        ];
 
-    saveSystemSettings($settings);
-    $message = 'Einstellungen wurden gespeichert.';
-    $messageType = 'success';
+        saveSystemSettings($settings);
+        logAccess("Admin updated system settings");
+        $message = "Einstellungen wurden gespeichert.";
+        $messageType = "success";
+    }
 }
 
 $settings = getSystemSettings();
 
-$bodyClass = 'admin-page';
-include __DIR__ . '/../includes/header.php';
+$bodyClass = "admin-page";
+include __DIR__ . "/../includes/header.php";
 ?>
 
 <div class="admin-header">
@@ -37,7 +49,7 @@ include __DIR__ . '/../includes/header.php';
     </div>
 </div>
 
-<?php if ($message !== ''): ?>
+<?php if ($message !== ""): ?>
     <div class="alert alert-<?php echo escape($messageType); ?>">
         <?php echo escape($message); ?>
     </div>
@@ -45,26 +57,39 @@ include __DIR__ . '/../includes/header.php';
 
 <div class="panel panel-lg">
     <form method="POST">
+        <?php echo csrfField(); ?>
         <div class="form-group">
             <label for="order_recipient_email">Empfängeradresse für interne Bestellungen *</label>
-            <input type="email" id="order_recipient_email" name="order_recipient_email" required value="<?php echo escape($settings['order_recipient_email']); ?>">
+            <input type="email" id="order_recipient_email" name="order_recipient_email" required value="<?php echo escape(
+                $settings["order_recipient_email"],
+            ); ?>">
         </div>
 
         <div class="form-group">
             <label>
-                <input type="checkbox" name="order_confirmation_required" value="1" <?php echo !empty($settings['order_confirmation_required']) ? 'checked' : ''; ?>>
+                <input type="checkbox" name="order_confirmation_required" value="1" <?php echo !empty(
+                    $settings["order_confirmation_required"]
+                )
+                    ? "checked"
+                    : ""; ?>>
                 Bestellungen müssen vor interner Weiterleitung per E-Mail bestätigt werden
             </label>
         </div>
 
         <div class="form-group">
             <label for="order_confirmation_expiry_days">Bestätigungsfrist in Tagen *</label>
-            <input type="number" id="order_confirmation_expiry_days" name="order_confirmation_expiry_days" min="1" required value="<?php echo (int) $settings['order_confirmation_expiry_days']; ?>">
+            <input type="number" id="order_confirmation_expiry_days" name="order_confirmation_expiry_days" min="1" required value="<?php echo (int) $settings[
+                "order_confirmation_expiry_days"
+            ]; ?>">
         </div>
 
         <div class="form-group">
             <label>
-                <input type="checkbox" name="attach_order_pdf_to_admin_email" value="1" <?php echo !empty($settings['attach_order_pdf_to_admin_email']) ? 'checked' : ''; ?>>
+                <input type="checkbox" name="attach_order_pdf_to_admin_email" value="1" <?php echo !empty(
+                    $settings["attach_order_pdf_to_admin_email"]
+                )
+                    ? "checked"
+                    : ""; ?>>
                 PDF an interne Bestell-E-Mails anhängen
             </label>
         </div>
@@ -73,4 +98,4 @@ include __DIR__ . '/../includes/header.php';
     </form>
 </div>
 
-<?php include __DIR__ . '/../includes/footer.php'; ?>
+<?php include __DIR__ . "/../includes/footer.php"; ?>

+ 29 - 16
cart.php

@@ -1,24 +1,30 @@
 <?php
-require_once __DIR__ . '/config.php';
-require_once __DIR__ . '/includes/functions.php';
+require_once __DIR__ . "/config.php";
+require_once __DIR__ . "/includes/functions.php";
 
-$pageTitle = 'Warenkorb';
+$pageTitle = "Warenkorb";
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_item_index'])) {
-    removeCartItemByIndex((int) $_POST['remove_item_index']);
+if (
+    $_SERVER["REQUEST_METHOD"] === "POST" &&
+    isset($_POST["remove_item_index"])
+) {
+    // Validate CSRF token
+    if (validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        removeCartItemByIndex((int) $_POST["remove_item_index"]);
+    }
 }
 
 $cartItems = getCartItemsDetailed();
-$cartNotice = consumeFlashMessage('cart_notice');
+$cartNotice = consumeFlashMessage("cart_notice");
 
-include __DIR__ . '/includes/header.php';
+include __DIR__ . "/includes/header.php";
 ?>
 
 <h2>Warenkorb</h2>
 
 <?php if ($cartNotice !== null): ?>
-    <div class="alert alert-<?php echo escape($cartNotice['type']); ?>">
-        <?php echo escape($cartNotice['message']); ?>
+    <div class="alert alert-<?php echo escape($cartNotice["type"]); ?>">
+        <?php echo escape($cartNotice["message"]); ?>
     </div>
 <?php endif; ?>
 
@@ -31,17 +37,24 @@ include __DIR__ . '/includes/header.php';
     <?php foreach ($cartItems as $cartItem): ?>
         <div class="cart-item">
             <div class="cart-item-info">
-                <h3><?php echo escape($cartItem['product']['name']); ?></h3>
-                <?php if ($cartItem['size'] !== ''): ?>
-                    <p><strong>Größe:</strong> <?php echo escape($cartItem['size']); ?></p>
+                <h3><?php echo escape($cartItem["product"]["name"]); ?></h3>
+                <?php if ($cartItem["size"] !== ""): ?>
+                    <p><strong>Größe:</strong> <?php echo escape(
+                        $cartItem["size"],
+                    ); ?></p>
                 <?php endif; ?>
-                <?php if ($cartItem['availability_label'] !== ''): ?>
-                    <p><strong>Lieferhinweis:</strong> <?php echo escape($cartItem['availability_label']); ?></p>
+                <?php if ($cartItem["availability_label"] !== ""): ?>
+                    <p><strong>Lieferhinweis:</strong> <?php echo escape(
+                        $cartItem["availability_label"],
+                    ); ?></p>
                 <?php endif; ?>
             </div>
             <div class="cart-item-actions">
                 <form method="POST">
-                    <button type="submit" name="remove_item_index" value="<?php echo (int) $cartItem['cart_index']; ?>" class="btn btn-secondary btn-small">Entfernen</button>
+                    <?php echo csrfField(); ?>
+                    <button type="submit" name="remove_item_index" value="<?php echo (int) $cartItem[
+                        "cart_index"
+                    ]; ?>" class="btn btn-secondary btn-small">Entfernen</button>
                 </form>
             </div>
         </div>
@@ -55,4 +68,4 @@ include __DIR__ . '/includes/header.php';
     </div>
 <?php endif; ?>
 
-<?php include __DIR__ . '/includes/footer.php'; ?>
+<?php include __DIR__ . "/includes/footer.php"; ?>

+ 95 - 29
checkout.php

@@ -1,34 +1,77 @@
 <?php
-require_once __DIR__ . '/config.php';
-require_once __DIR__ . '/includes/functions.php';
+require_once __DIR__ . "/config.php";
+require_once __DIR__ . "/includes/functions.php";
 
-$pageTitle = 'Bestellung abschließen';
+$pageTitle = "Bestellung abschließen";
 $cartItems = getCartItemsDetailed();
 $organizations = getOrganizations(true);
 $errors = [];
 
 if (empty($cartItems)) {
-    header('Location: cart.php');
-    exit;
+    header("Location: cart.php");
+    exit();
 }
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_order'])) {
-    $customerName = $_POST['customer_name'] ?? '';
-    $customerEmail = $_POST['customer_email'] ?? '';
-    $organizationId = $_POST['organization_id'] ?? '';
-    $comment = $_POST['comment'] ?? '';
-
-    $result = createOrder($customerName, $customerEmail, $organizationId, $comment, buildOrderItemsFromCart());
-    if (!$result['success']) {
-        $errors[] = $result['message'];
+if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["create_order"])) {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $errors[] = "Ungültiges Token. Bitte versuchen Sie es erneut.";
     } else {
-        clearCart();
-        header('Location: order-success.php?id=' . urlencode($result['order']['id']));
-        exit;
+        $validator = new Validator($_POST);
+
+        $validator
+            ->required("customer_name", "Name")
+            ->minLength("customer_name", 2, "Name")
+            ->maxLength("customer_name", 100, "Name")
+            ->required("customer_email", "E-Mail-Adresse")
+            ->email("customer_email", "E-Mail-Adresse")
+            ->maxLength("customer_email", 255, "E-Mail-Adresse")
+            ->required("organization_id", "Organisation")
+            ->maxLength("comment", 1000, "Kommentar");
+
+        // Validate organization exists
+        $organizationId = $_POST["organization_id"] ?? "";
+        $organizations = getOrganizations(true);
+        $validOrgIds = array_column($organizations, "id");
+
+        if (!in_array($organizationId, $validOrgIds, true)) {
+            $validator->errors[] = "Die gewählte Organisation ist ungültig.";
+        }
+
+        if (!$validator->isValid()) {
+            $errors = array_merge($errors, $validator->getErrors());
+        } else {
+            $customerName = trim($_POST["customer_name"]);
+            $customerEmail = trim(strtolower($_POST["customer_email"]));
+            $comment = trim($_POST["comment"] ?? "");
+
+            $result = createOrder(
+                $customerName,
+                $customerEmail,
+                $organizationId,
+                $comment,
+                buildOrderItemsFromCart(),
+            );
+
+            if (!$result["success"]) {
+                $errors[] = $result["message"];
+            } else {
+                clearCart();
+                logAccess("Order created", [
+                    "order_id" => $result["order"]["id"],
+                    "customer" => $customerEmail,
+                ]);
+                header(
+                    "Location: order-success.php?id=" .
+                        urlencode($result["order"]["id"]),
+                );
+                exit();
+            }
+        }
     }
 }
 
-include __DIR__ . '/includes/header.php';
+include __DIR__ . "/includes/header.php";
 ?>
 
 <h2>Bestellung abschließen</h2>
@@ -49,12 +92,16 @@ include __DIR__ . '/includes/header.php';
 
         <?php foreach ($cartItems as $cartItem): ?>
             <div class="panel panel-compact">
-                <strong><?php echo escape($cartItem['product']['name']); ?></strong><br>
-                <?php if ($cartItem['size'] !== ''): ?>
-                    Größe: <?php echo escape($cartItem['size']); ?><br>
+                <strong><?php echo escape(
+                    $cartItem["product"]["name"],
+                ); ?></strong><br>
+                <?php if ($cartItem["size"] !== ""): ?>
+                    Größe: <?php echo escape($cartItem["size"]); ?><br>
                 <?php endif; ?>
-                <?php if ($cartItem['availability_label'] !== ''): ?>
-                    Lieferhinweis: <?php echo escape($cartItem['availability_label']); ?>
+                <?php if ($cartItem["availability_label"] !== ""): ?>
+                    Lieferhinweis: <?php echo escape(
+                        $cartItem["availability_label"],
+                    ); ?>
                 <?php endif; ?>
             </div>
         <?php endforeach; ?>
@@ -65,12 +112,20 @@ include __DIR__ . '/includes/header.php';
         <form method="POST">
             <div class="form-group">
                 <label for="customer_name">Name *</label>
-                <input type="text" id="customer_name" name="customer_name" required value="<?php echo isset($_POST['customer_name']) ? escape($_POST['customer_name']) : ''; ?>">
+                <input type="text" id="customer_name" name="customer_name" required value="<?php echo isset(
+                    $_POST["customer_name"],
+                )
+                    ? escape($_POST["customer_name"])
+                    : ""; ?>">
             </div>
 
             <div class="form-group">
                 <label for="customer_email">E-Mail-Adresse *</label>
-                <input type="email" id="customer_email" name="customer_email" required value="<?php echo isset($_POST['customer_email']) ? escape($_POST['customer_email']) : ''; ?>">
+                <input type="email" id="customer_email" name="customer_email" required value="<?php echo isset(
+                    $_POST["customer_email"],
+                )
+                    ? escape($_POST["customer_email"])
+                    : ""; ?>">
             </div>
 
             <div class="form-group">
@@ -78,8 +133,13 @@ include __DIR__ . '/includes/header.php';
                 <select id="organization_id" name="organization_id" required>
                     <option value="">Bitte wählen</option>
                     <?php foreach ($organizations as $organization): ?>
-                        <option value="<?php echo escape($organization['id']); ?>" <?php echo (isset($_POST['organization_id']) && $_POST['organization_id'] === $organization['id']) ? 'selected' : ''; ?>>
-                            <?php echo escape($organization['label']); ?>
+                        <option value="<?php echo escape(
+                            $organization["id"],
+                        ); ?>" <?php echo isset($_POST["organization_id"]) &&
+$_POST["organization_id"] === $organization["id"]
+    ? "selected"
+    : ""; ?>>
+                            <?php echo escape($organization["label"]); ?>
                         </option>
                     <?php endforeach; ?>
                 </select>
@@ -87,7 +147,11 @@ include __DIR__ . '/includes/header.php';
 
             <div class="form-group">
                 <label for="comment">Kommentar</label>
-                <textarea id="comment" name="comment" rows="5"><?php echo isset($_POST['comment']) ? escape($_POST['comment']) : ''; ?></textarea>
+                <textarea id="comment" name="comment" rows="5"><?php echo isset(
+                    $_POST["comment"],
+                )
+                    ? escape($_POST["comment"])
+                    : ""; ?></textarea>
             </div>
 
             <div class="alert alert-info">
@@ -98,6 +162,8 @@ include __DIR__ . '/includes/header.php';
                 <?php endif; ?>
             </div>
 
+            <?php echo csrfField(); ?>
+
             <button type="submit" name="create_order" class="btn btn-block">Bestellung absenden</button>
         </form>
 
@@ -107,4 +173,4 @@ include __DIR__ . '/includes/header.php';
     </div>
 </div>
 
-<?php include __DIR__ . '/includes/footer.php'; ?>
+<?php include __DIR__ . "/includes/footer.php"; ?>

Fichier diff supprimé car celui-ci est trop grand
+ 344 - 202
includes/functions.php


+ 46 - 22
index.php

@@ -1,21 +1,25 @@
 <?php
-require_once __DIR__ . '/config.php';
-require_once __DIR__ . '/includes/functions.php';
+require_once __DIR__ . "/config.php";
+require_once __DIR__ . "/includes/functions.php";
 
-$pageTitle = 'Startseite';
+$pageTitle = "Startseite";
 $products = getProducts();
 $categories = getCategories();
 
-$category = isset($_GET['category']) ? normalizeCategoryId($_GET['category']) : '';
-if ($category !== '' && getCategoryById($category) !== null) {
-    $products = array_values(array_filter($products, function ($product) use ($category) {
-        return productHasCategory($product, $category);
-    }));
+$category = isset($_GET["category"])
+    ? normalizeCategoryId($_GET["category"])
+    : "";
+if ($category !== "" && getCategoryById($category) !== null) {
+    $products = array_values(
+        array_filter($products, function ($product) use ($category) {
+            return productHasCategory($product, $category);
+        }),
+    );
 } else {
-    $category = '';
+    $category = "";
 }
 
-include __DIR__ . '/includes/header.php';
+include __DIR__ . "/includes/header.php";
 ?>
 
 <h2><?php echo escape(SITE_SERVICE_HEADER); ?></h2>
@@ -27,10 +31,17 @@ include __DIR__ . '/includes/header.php';
 </div>
 
 <div class="category-filter-bar" aria-label="Produktkategorien">
-    <a href="?category=" class="btn btn-small <?php echo $category === '' ? '' : 'btn-secondary'; ?>">Alle</a>
+    <a href="?category=" class="btn btn-small <?php echo $category === ""
+        ? ""
+        : "btn-secondary"; ?>">Alle</a>
     <?php foreach ($categories as $categoryOption): ?>
-        <a href="?category=<?php echo urlencode($categoryOption['id']); ?>" class="btn btn-small <?php echo $category === $categoryOption['id'] ? '' : 'btn-secondary'; ?>">
-            <?php echo escape($categoryOption['label']); ?>
+        <a href="?category=<?php echo urlencode(
+            $categoryOption["id"],
+        ); ?>" class="btn btn-small <?php echo $category ===
+$categoryOption["id"]
+    ? ""
+    : "btn-secondary"; ?>">
+            <?php echo escape($categoryOption["label"]); ?>
         </a>
     <?php endforeach; ?>
 </div>
@@ -43,23 +54,36 @@ include __DIR__ . '/includes/header.php';
     <div class="products-grid">
         <?php foreach ($products as $product): ?>
             <div class="product-card">
-                <a href="product.php?id=<?php echo (int) $product['id']; ?>">
-                    <?php $imagePath = getUploadPath($product['image'] ?? ''); ?>
-                    <?php $imageUrl = getUploadUrl($product['image'] ?? ''); ?>
-                    <?php if ($imagePath !== null && $imageUrl !== null && file_exists($imagePath)): ?>
-                        <img src="<?php echo escape($imageUrl); ?>" alt="<?php echo escape($product['name']); ?>">
+                <a href="product.php?id=<?php echo (int) $product["id"]; ?>">
+                    <?php $imagePath = getUploadPath(
+                        $product["image"] ?? "",
+                    ); ?>
+                    <?php $imageUrl = getUploadUrl($product["image"] ?? ""); ?>
+                    <?php if (
+                        $imagePath !== null &&
+                        $imageUrl !== null &&
+                        file_exists($imagePath)
+                    ): ?>
+                        <img src="<?php echo escape(
+                            $imageUrl,
+                        ); ?>" alt="<?php echo escape($product["name"]); ?>">
                     <?php else: ?>
                         <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='250' height='200'%3E%3Crect fill='%233a4150' width='250' height='200'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23c7ccd6'%3EKein Bild%3C/text%3E%3C/svg%3E" alt="Kein Bild">
                     <?php endif; ?>
                 </a>
                 <div class="product-card-content">
-                    <h3><a href="product.php?id=<?php echo (int) $product['id']; ?>" class="product-card-title-link"><?php echo escape($product['name']); ?></a></h3>
-                    <p class="product-summary"><?php echo escape(implode(', ', getProductSizes($product))); ?></p>
-                    <a href="product.php?id=<?php echo (int) $product['id']; ?>" class="btn btn-block mt-2">Details ansehen</a>
+                    <h3><a href="product.php?id=<?php echo (int) $product[
+                        "id"
+                    ]; ?>" class="product-card-title-link"><?php echo escape(
+    $product["name"],
+); ?></a></h3>
+                    <a href="product.php?id=<?php echo (int) $product[
+                        "id"
+                    ]; ?>" class="btn btn-block mt-2">Details ansehen</a>
                 </div>
             </div>
         <?php endforeach; ?>
     </div>
 <?php endif; ?>
 
-<?php include __DIR__ . '/includes/footer.php'; ?>
+<?php include __DIR__ . "/includes/footer.php"; ?>

+ 70 - 35
product.php

@@ -1,44 +1,65 @@
 <?php
-require_once __DIR__ . '/config.php';
-require_once __DIR__ . '/includes/functions.php';
+require_once __DIR__ . "/config.php";
+require_once __DIR__ . "/includes/functions.php";
 
-$productId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
+$productId = isset($_GET["id"]) ? (int) $_GET["id"] : 0;
 $product = getProductById($productId);
 
 if ($product === null) {
-    header('Location: index.php');
-    exit;
+    header("Location: index.php");
+    exit();
 }
 
-$pageTitle = $product['name'];
+$pageTitle = $product["name"];
 $sizes = getProductSizes($product);
 
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
-    $size = trim((string) ($_POST['size'] ?? ''));
-
-    if (!empty($sizes) && ($size === '' || !in_array($size, $sizes, true))) {
-        $error = 'Bitte wählen Sie eine Größe aus.';
+if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["add_to_cart"])) {
+    // Validate CSRF token
+    if (!validateCsrfToken($_POST["csrf_token"] ?? "")) {
+        $error = "Ungültiges Token. Bitte versuchen Sie es erneut.";
     } else {
-        $result = addCartItem($product['id'], $size);
-        if (!$result['success']) {
-            $error = 'Der Artikel konnte nicht in den Warenkorb gelegt werden.';
-        } elseif ($result['status'] === 'replaced') {
-            setFlashMessage('cart_notice', 'success', 'Die Größe für diesen Artikel wurde im Warenkorb aktualisiert.');
-            header('Location: cart.php');
-            exit;
-        } elseif ($result['status'] === 'unchanged') {
-            setFlashMessage('cart_notice', 'info', 'Dieser Artikel ist bereits mit der gewählten Größe im Warenkorb.');
-            header('Location: cart.php');
-            exit;
+        $size = trim((string) ($_POST["size"] ?? ""));
+
+        if (
+            !empty($sizes) &&
+            ($size === "" || !in_array($size, $sizes, true))
+        ) {
+            $error = "Bitte wählen Sie eine Größe aus.";
         } else {
-            setFlashMessage('cart_notice', 'success', 'Der Artikel wurde zum Warenkorb hinzugefügt.');
-            header('Location: cart.php');
-            exit;
+            $result = addCartItem($product["id"], $size);
+            if (!$result["success"]) {
+                $error =
+                    "Der Artikel konnte nicht in den Warenkorb gelegt werden.";
+            } elseif ($result["status"] === "replaced") {
+                setFlashMessage(
+                    "cart_notice",
+                    "success",
+                    "Die Größe für diesen Artikel wurde im Warenkorb aktualisiert.",
+                );
+                header("Location: cart.php");
+                exit();
+            } elseif ($result["status"] === "unchanged") {
+                setFlashMessage(
+                    "cart_notice",
+                    "info",
+                    "Dieser Artikel ist bereits mit der gewählten Größe im Warenkorb.",
+                );
+                header("Location: cart.php");
+                exit();
+            } else {
+                setFlashMessage(
+                    "cart_notice",
+                    "success",
+                    "Der Artikel wurde zum Warenkorb hinzugefügt.",
+                );
+                header("Location: cart.php");
+                exit();
+            }
         }
     }
 }
 
-include __DIR__ . '/includes/header.php';
+include __DIR__ . "/includes/header.php";
 ?>
 
 <?php if (isset($error)): ?>
@@ -49,32 +70,46 @@ include __DIR__ . '/includes/header.php';
 
 <div class="product-detail-grid">
     <div>
-        <?php $imagePath = getUploadPath($product['image'] ?? ''); ?>
-        <?php $imageUrl = getUploadUrl($product['image'] ?? ''); ?>
-        <?php if ($imagePath !== null && $imageUrl !== null && file_exists($imagePath)): ?>
-            <img class="product-image" src="<?php echo escape($imageUrl); ?>" alt="<?php echo escape($product['name']); ?>">
+        <?php $imagePath = getUploadPath($product["image"] ?? ""); ?>
+        <?php $imageUrl = getUploadUrl($product["image"] ?? ""); ?>
+        <?php if (
+            $imagePath !== null &&
+            $imageUrl !== null &&
+            file_exists($imagePath)
+        ): ?>
+            <img class="product-image" src="<?php echo escape(
+                $imageUrl,
+            ); ?>" alt="<?php echo escape($product["name"]); ?>">
         <?php else: ?>
             <div class="product-placeholder">Kein Bild verfügbar</div>
         <?php endif; ?>
     </div>
 
     <div class="product-copy">
-        <h1><?php echo escape($product['name']); ?></h1>
+        <h1><?php echo escape($product["name"]); ?></h1>
 
         <div class="product-description-block">
             <h3>Beschreibung</h3>
-            <p class="product-description"><?php echo nl2br(escape($product['description'])); ?></p>
+            <p class="product-description"><?php echo nl2br(
+                escape($product["description"]),
+            ); ?></p>
         </div>
 
         <form method="POST" class="product-form">
+            <?php echo csrfField(); ?>
             <?php if (!empty($sizes)): ?>
                 <div class="form-group">
                     <label for="size">Größe *</label>
                     <select id="size" name="size" required onchange="updateAvailabilityNotice()">
                         <option value="">Bitte wählen</option>
                         <?php foreach ($sizes as $sizeOption): ?>
-                            <?php $label = getAvailabilityLabel($product, $sizeOption); ?>
-                            <option value="<?php echo escape($sizeOption); ?>" data-label="<?php echo escape($label); ?>">
+                            <?php $label = getAvailabilityLabel(
+                                $product,
+                                $sizeOption,
+                            ); ?>
+                            <option value="<?php echo escape(
+                                $sizeOption,
+                            ); ?>" data-label="<?php echo escape($label); ?>">
                                 <?php echo escape($sizeOption); ?>
                             </option>
                         <?php endforeach; ?>
@@ -111,4 +146,4 @@ include __DIR__ . '/includes/header.php';
     </div>
 </div>
 
-<?php include __DIR__ . '/includes/footer.php'; ?>
+<?php include __DIR__ . "/includes/footer.php"; ?>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff