$type, 'message' => $message, ]; } function consumeFlashMessage($key) { $key = trim((string) $key); if ($key === '') { return null; } $messages = $_SESSION['flash_messages'] ?? []; if (!is_array($messages) || !isset($messages[$key]) || !is_array($messages[$key])) { return null; } $message = $messages[$key]; unset($_SESSION['flash_messages'][$key]); $type = trim((string) ($message['type'] ?? '')); $text = trim((string) ($message['message'] ?? '')); if ($type === '' || $text === '') { return null; } return [ 'type' => $type, 'message' => $text, ]; } function normalizeAdminUsername($username) { return trim((string) $username); } function normalizeAdminDescription($description) { return trim((string) $description); } function normalizeAdminEmail($email) { return strtolower(trim((string) $email)); } function isValidAdminUsername($username) { $username = normalizeAdminUsername($username); return preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{2,49}$/', $username) === 1; } function isValidAdminDescription($description) { $description = normalizeAdminDescription($description); if ($description === '') { return false; } $length = function_exists('mb_strlen') ? mb_strlen($description) : strlen($description); return $length <= 120; } function isValidAdminEmail($email) { $email = normalizeAdminEmail($email); return $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } function getDefaultAdminDescription($username) { return 'Admin'; } function getDefaultAdminEmail() { $email = defined('ADMIN_EMAIL') ? normalizeAdminEmail(ADMIN_EMAIL) : ''; return isValidAdminEmail($email) ? $email : ''; } function getAdminAccounts() { $data = readJsonFile(ADMINS_FILE); $records = isset($data['admins']) && is_array($data['admins']) ? $data['admins'] : []; $accounts = []; foreach ($records as $username => $record) { $username = normalizeAdminUsername($username); if ($username === '') { continue; } if (is_string($record)) { $record = [ 'password_hash' => $record, 'description' => getDefaultAdminDescription($username), 'email' => getDefaultAdminEmail(), ]; } if (!is_array($record)) { continue; } $hash = isset($record['password_hash']) ? (string) $record['password_hash'] : ''; if ($hash === '') { continue; } $description = normalizeAdminDescription($record['description'] ?? getDefaultAdminDescription($username)); if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($username); } $email = normalizeAdminEmail($record['email'] ?? getDefaultAdminEmail()); if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $accounts[$username] = [ 'password_hash' => $hash, 'description' => $description, 'email' => $email, ]; } ksort($accounts); return $accounts; } function getAdminUsers() { $users = []; foreach (getAdminAccounts() as $username => $record) { $users[$username] = $record['password_hash']; } return $users; } function saveAdminAccounts($accounts) { $result = []; foreach ($accounts as $username => $record) { $username = normalizeAdminUsername($username); if ($username === '' || !is_array($record)) { continue; } $hash = isset($record['password_hash']) ? (string) $record['password_hash'] : ''; if ($hash === '') { continue; } $description = normalizeAdminDescription($record['description'] ?? getDefaultAdminDescription($username)); if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($username); } $email = normalizeAdminEmail($record['email'] ?? getDefaultAdminEmail()); if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $result[$username] = [ 'password_hash' => $hash, 'description' => $description, 'email' => $email, ]; } ksort($result); writeJsonFile(ADMINS_FILE, ['admins' => $result]); } function getDefaultCategories() { return [ ['id' => 'apparel', 'label' => 'Bekleidung'], ]; } function normalizeCategoryId($id) { $id = trim((string) $id); if ($id === '') { return ''; } if (function_exists('iconv')) { $converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $id); if (is_string($converted) && $converted !== '') { $id = $converted; } } $id = strtolower($id); $id = preg_replace('/[^a-z0-9]+/', '-', $id); return trim((string) $id, '-'); } function normalizeCategoryLabel($label) { return trim((string) $label); } function isValidCategoryLabel($label) { $label = normalizeCategoryLabel($label); if ($label === '') { return false; } $length = function_exists('mb_strlen') ? mb_strlen($label) : strlen($label); return $length <= 80; } function normalizeCategories($categories) { $normalized = []; if (!is_array($categories)) { $categories = []; } foreach ($categories as $category) { if (!is_array($category)) { continue; } $id = normalizeCategoryId($category['id'] ?? ''); $label = normalizeCategoryLabel($category['label'] ?? ''); if ($id === '' || !isValidCategoryLabel($label)) { continue; } $normalized[$id] = [ 'id' => $id, 'label' => $label, ]; } if (empty($normalized)) { foreach (getDefaultCategories() as $category) { $normalized[$category['id']] = $category; } } uasort($normalized, function ($left, $right) { return strcasecmp($left['label'], $right['label']); }); return array_values($normalized); } function getCategories() { $data = readJsonFile(CATEGORIES_FILE); return normalizeCategories($data['categories'] ?? []); } function saveCategories($categories) { writeJsonFile(CATEGORIES_FILE, ['categories' => normalizeCategories($categories)]); } function getCategoryById($categoryId) { $categoryId = normalizeCategoryId($categoryId); foreach (getCategories() as $category) { if ($category['id'] === $categoryId) { return $category; } } return null; } function getCategoryLabel($categoryId) { $category = getCategoryById($categoryId); if ($category !== null) { return $category['label']; } return trim((string) $categoryId); } function getCategoryLabels($categoryIds) { $labels = []; foreach (normalizeProductCategoryIds($categoryIds) as $categoryId) { $labels[] = getCategoryLabel($categoryId); } return $labels; } function generateCategoryIdFromLabel($label, $existingCategories = []) { $baseId = normalizeCategoryId($label); if ($baseId === '') { $baseId = 'category'; } $used = []; foreach (normalizeCategories($existingCategories) as $category) { $used[$category['id']] = true; } $candidate = $baseId; $counter = 2; while (isset($used[$candidate])) { $candidate = $baseId . '-' . $counter; $counter++; } return $candidate; } function isCategoryInUse($categoryId) { foreach (getProducts() as $product) { if (productHasCategory($product, $categoryId)) { return true; } } return false; } function normalizeProductCategoryIds($categoryValue) { if (is_array($categoryValue)) { $rawIds = $categoryValue; } elseif ($categoryValue === null || $categoryValue === '') { $rawIds = []; } else { $rawIds = [$categoryValue]; } $normalized = []; foreach ($rawIds as $categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId !== '') { $normalized[$categoryId] = $categoryId; } } return array_values($normalized); } function getProductCategoryIds($product) { if (isset($product['categories'])) { return normalizeProductCategoryIds($product['categories']); } return normalizeProductCategoryIds($product['category'] ?? []); } function productHasCategory($product, $categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId === '') { return false; } return in_array($categoryId, getProductCategoryIds($product), true); } function getProductSizes($product) { if (isset($product['sizes']) && is_array($product['sizes'])) { $sizes = $product['sizes']; } elseif (isset($product['sizes']) && is_string($product['sizes'])) { $sizes = explode(',', $product['sizes']); } else { $sizes = []; } $normalized = []; foreach ($sizes as $size) { $size = trim((string) $size); if ($size !== '') { $normalized[$size] = $size; } } return array_values($normalized); } function productUsesSizeStock($product) { return !empty(getProductSizes($product)); } function normalizeAvailabilityLabels($sizes, $labels) { $result = []; if (!is_array($labels)) { $labels = []; } foreach ($sizes as $size) { $text = trim((string) ($labels[$size] ?? '')); $result[$size] = $text; } return $result; } function getAvailabilityLabel($product, $size) { $labels = isset($product['availability_labels']) && is_array($product['availability_labels']) ? $product['availability_labels'] : []; return trim((string) ($labels[$size] ?? '')); } function normalizeProductRecord($product, $defaultCategoryId = '') { if (!is_array($product)) { return null; } $productId = isset($product['id']) ? (int) $product['id'] : 0; $name = trim((string) ($product['name'] ?? '')); if ($productId <= 0 || $name === '') { return null; } $sizes = getProductSizes($product); if (empty($sizes)) { $sizes = ['Standard']; } $categories = getProductCategoryIds($product); if (empty($categories) && $defaultCategoryId !== '') { $categories = [$defaultCategoryId]; } $availabilityLabels = normalizeAvailabilityLabels( $sizes, isset($product['availability_labels']) && is_array($product['availability_labels']) ? $product['availability_labels'] : [] ); return [ 'id' => $productId, 'name' => $name, 'description' => trim((string) ($product['description'] ?? '')), 'image' => trim((string) ($product['image'] ?? '')), 'categories' => $categories, 'sizes' => implode(',', $sizes), 'availability_labels' => $availabilityLabels, ]; } function getProducts() { $data = readJsonFile(PRODUCTS_FILE); $rawProducts = isset($data['products']) && is_array($data['products']) ? $data['products'] : []; $categories = getCategories(); $defaultCategoryId = !empty($categories) ? $categories[0]['id'] : 'apparel'; $products = []; foreach ($rawProducts as $product) { $normalized = normalizeProductRecord($product, $defaultCategoryId); if ($normalized !== null) { $products[] = $normalized; } } usort($products, function ($left, $right) { return strcasecmp($left['name'], $right['name']); }); return $products; } function getProductById($id) { $id = (int) $id; foreach (getProducts() as $product) { if ((int) $product['id'] === $id) { return $product; } } return null; } function saveProducts($products) { $categories = getCategories(); $defaultCategoryId = !empty($categories) ? $categories[0]['id'] : 'apparel'; $normalized = []; foreach ($products as $product) { $record = normalizeProductRecord($product, $defaultCategoryId); if ($record !== null) { $normalized[] = $record; } } writeJsonFile(PRODUCTS_FILE, ['products' => array_values($normalized)]); } function getFaqFilePath(): string { $dataDir = defined('DATA_DIR') ? DATA_DIR : dirname(__DIR__) . '/data/'; $defaultPath = rtrim($dataDir, '/\\') . '/faq.json'; if (!defined('FAQ_FILE') || !is_string(FAQ_FILE) || FAQ_FILE === '') { return $defaultPath; } $configuredPath = FAQ_FILE; $normalizedDataDir = str_replace('\\', '/', rtrim($dataDir, '/\\')) . '/'; $normalizedConfigured = str_replace('\\', '/', $configuredPath); if (strpos($normalizedConfigured, $normalizedDataDir) !== 0) { return $defaultPath; } return $configuredPath; } function getFaqContent(): string { $defaultContent = "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden."; $data = readJsonFile(getFaqFilePath()); if (!isset($data['content']) || !is_string($data['content'])) { return $defaultContent; } return $data['content']; } function saveFaqContent(string $markdown): void { writeJsonFile(getFaqFilePath(), ['content' => (string) $markdown]); } function renderFaqInlineMarkdown(string $text): string { $escaped = escape($text); $escaped = preg_replace('/\*\*(.+?)\*\*/s', '$1', $escaped); $escaped = preg_replace('/(?$1', $escaped); return $escaped; } function renderFaqMarkdown(string $markdown): string { $normalized = str_replace(["\r\n", "\r"], "\n", $markdown); $lines = explode("\n", $normalized); $htmlParts = []; $paragraphLines = []; $listType = ''; $flushParagraph = function () use (&$paragraphLines, &$htmlParts): void { if (empty($paragraphLines)) { return; } $rendered = []; foreach ($paragraphLines as $line) { $rendered[] = renderFaqInlineMarkdown($line); } $htmlParts[] = '
' . implode("
\n", $rendered) . '
Keine FAQ-Inhalte vorhanden.
' : implode("\n", $htmlParts); } function getDefaultOrganizations() { return [ [ 'id' => 'feuerwehr-freising', 'label' => 'Amt 32 - Feuerwehr Freising', 'sort_order' => 10, 'active' => true, ], ]; } function normalizeOrganizationId($id) { return normalizeCategoryId($id); } function normalizeOrganizationLabel($label) { return trim((string) $label); } function isValidOrganizationLabel($label) { $label = normalizeOrganizationLabel($label); if ($label === '') { return false; } $length = function_exists('mb_strlen') ? mb_strlen($label) : strlen($label); return $length <= 120; } function normalizeOrganizations($organizations) { $normalized = []; if (!is_array($organizations)) { $organizations = []; } foreach ($organizations as $organization) { if (!is_array($organization)) { continue; } $id = normalizeOrganizationId($organization['id'] ?? ''); $label = normalizeOrganizationLabel($organization['label'] ?? ''); if ($id === '' || !isValidOrganizationLabel($label)) { continue; } $sortOrder = isset($organization['sort_order']) ? (int) $organization['sort_order'] : 0; $active = !isset($organization['active']) || (bool) $organization['active']; $normalized[$id] = [ 'id' => $id, 'label' => $label, 'sort_order' => $sortOrder, 'active' => $active, ]; } if (empty($normalized)) { foreach (getDefaultOrganizations() as $organization) { $normalized[$organization['id']] = $organization; } } uasort($normalized, function ($left, $right) { if ($left['sort_order'] === $right['sort_order']) { return strcasecmp($left['label'], $right['label']); } return $left['sort_order'] <=> $right['sort_order']; }); return array_values($normalized); } function getOrganizations($onlyActive = false) { $data = readJsonFile(ORGANIZATIONS_FILE); $organizations = normalizeOrganizations($data['organizations'] ?? []); if ($onlyActive) { $organizations = array_values(array_filter($organizations, function ($organization) { return !empty($organization['active']); })); } return $organizations; } function saveOrganizations($organizations) { writeJsonFile(ORGANIZATIONS_FILE, ['organizations' => normalizeOrganizations($organizations)]); } function getOrganizationById($organizationId) { $organizationId = normalizeOrganizationId($organizationId); foreach (getOrganizations(false) as $organization) { if ($organization['id'] === $organizationId) { return $organization; } } return null; } function generateOrganizationIdFromLabel($label, $existingOrganizations = []) { $baseId = normalizeOrganizationId($label); if ($baseId === '') { $baseId = 'organization'; } $used = []; foreach (normalizeOrganizations($existingOrganizations) as $organization) { $used[$organization['id']] = true; } $candidate = $baseId; $counter = 2; while (isset($used[$candidate])) { $candidate = $baseId . '-' . $counter; $counter++; } return $candidate; } function getDefaultSystemSettings() { return [ 'order_recipient_email' => defined('ORDER_RECIPIENT_EMAIL') ? ORDER_RECIPIENT_EMAIL : getDefaultAdminEmail(), 'order_confirmation_required' => defined('ORDER_CONFIRMATION_REQUIRED') ? (bool) ORDER_CONFIRMATION_REQUIRED : false, 'order_confirmation_expiry_days' => defined('ORDER_CONFIRMATION_EXPIRY_DAYS') ? (int) ORDER_CONFIRMATION_EXPIRY_DAYS : 7, 'attach_order_pdf_to_admin_email' => defined('ATTACH_ORDER_PDF_TO_ADMIN_EMAIL') ? (bool) ATTACH_ORDER_PDF_TO_ADMIN_EMAIL : true, ]; } function normalizeSystemSettings($settings) { $defaults = getDefaultSystemSettings(); if (!is_array($settings)) { $settings = []; } $recipientEmail = normalizeAdminEmail($settings['order_recipient_email'] ?? $defaults['order_recipient_email']); if (!isValidAdminEmail($recipientEmail)) { $recipientEmail = $defaults['order_recipient_email']; } $expiryDays = isset($settings['order_confirmation_expiry_days']) ? (int) $settings['order_confirmation_expiry_days'] : $defaults['order_confirmation_expiry_days']; if ($expiryDays < 1) { $expiryDays = 7; } return [ 'order_recipient_email' => $recipientEmail, 'order_confirmation_required' => !empty($settings['order_confirmation_required']), 'order_confirmation_expiry_days' => $expiryDays, 'attach_order_pdf_to_admin_email' => !empty($settings['attach_order_pdf_to_admin_email']), ]; } function getSystemSettings() { $data = readJsonFile(SETTINGS_FILE); return normalizeSystemSettings($data['settings'] ?? []); } function saveSystemSettings($settings) { writeJsonFile(SETTINGS_FILE, ['settings' => normalizeSystemSettings($settings)]); } function getOrderRecipientEmail() { $settings = getSystemSettings(); return $settings['order_recipient_email']; } function isOrderConfirmationRequired() { $settings = getSystemSettings(); return !empty($settings['order_confirmation_required']); } function getOrderConfirmationExpiryDays() { $settings = getSystemSettings(); return max(1, (int) $settings['order_confirmation_expiry_days']); } function shouldAttachOrderPdfToAdminEmail() { $settings = getSystemSettings(); return !empty($settings['attach_order_pdf_to_admin_email']); } function normalizeOrderItem($item) { if (!is_array($item)) { return null; } $productId = isset($item['product_id']) ? (int) $item['product_id'] : 0; if ($productId <= 0) { return null; } $product = getProductById($productId); if ($product === null) { return null; } $size = trim((string) ($item['size'] ?? '')); $sizes = getProductSizes($product); if (!empty($sizes)) { if ($size === '' || !in_array($size, $sizes, true)) { return null; } } else { $size = ''; } return [ 'product_id' => $productId, 'product_name' => $product['name'], 'size' => $size, 'availability_label' => $size !== '' ? getAvailabilityLabel($product, $size) : '', 'is_processed' => !empty($item['is_processed']), ]; } function normalizeOrderItems($items) { $normalized = []; $seen = []; if (!is_array($items)) { return []; } foreach ($items as $item) { $record = normalizeOrderItem($item); if ($record === null) { continue; } $key = $record['product_id'] . '|' . $record['size']; if (isset($seen[$key])) { continue; } $seen[$key] = true; $normalized[] = $record; } return array_values($normalized); } function getOrders() { $data = readJsonFile(ORDERS_FILE); $orders = isset($data['orders']) && is_array($data['orders']) ? $data['orders'] : []; $normalized = []; foreach ($orders as $order) { $record = normalizeOrderRecord($order); if ($record !== null) { $normalized[] = $record; } } return $normalized; } function saveOrders($orders) { $normalized = []; foreach ($orders as $order) { $record = normalizeOrderRecord($order); if ($record !== null) { $normalized[] = $record; } } writeJsonFile(ORDERS_FILE, ['orders' => array_values($normalized)]); } function generateOrderId() { $orders = getOrders(); $year = date('Y'); $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD'; $max = 0; $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-(\d+)$/'; foreach ($orders as $order) { if (preg_match($pattern, (string) $order['id'], $matches) === 1) { $number = (int) $matches[1]; if ($number > $max) { $max = $number; } } } return sprintf('%s-%s-%03d', $prefix, $year, $max + 1); } function normalizeOrderRecord($order) { if (!is_array($order)) { return null; } $id = trim((string) ($order['id'] ?? '')); $customerName = trim((string) ($order['customer_name'] ?? '')); $customerEmail = normalizeAdminEmail($order['customer_email'] ?? ''); $organizationId = normalizeOrganizationId($order['organization_id'] ?? ''); $organizationLabel = trim((string) ($order['organization_label'] ?? '')); $items = normalizeOrderItems($order['items'] ?? []); if ($id === '' || $customerName === '' || !isValidAdminEmail($customerEmail) || $organizationId === '' || $organizationLabel === '' || empty($items)) { return null; } $createdAt = trim((string) ($order['created_at'] ?? '')); if ($createdAt === '') { $createdAt = date('Y-m-d H:i:s'); } $confirmationStatus = trim((string) ($order['confirmation_status'] ?? 'confirmed')); $allowedConfirmationStatuses = ['not_required', 'pending', 'confirmed', 'expired']; if (!in_array($confirmationStatus, $allowedConfirmationStatuses, true)) { $confirmationStatus = 'confirmed'; } $status = trim((string) ($order['status'] ?? 'open')); $allowedStatuses = ['open', 'partial', 'processed', 'cancelled']; if (!in_array($status, $allowedStatuses, true)) { $status = 'open'; } $normalized = [ 'id' => $id, 'customer_name' => $customerName, 'customer_email' => $customerEmail, 'organization_id' => $organizationId, 'organization_label' => $organizationLabel, 'comment' => trim((string) ($order['comment'] ?? '')), 'items' => $items, 'status' => $status, 'confirmation_status' => $confirmationStatus, 'confirmation_token' => trim((string) ($order['confirmation_token'] ?? '')), 'confirmation_expires_at' => trim((string) ($order['confirmation_expires_at'] ?? '')), 'confirmed_at' => trim((string) ($order['confirmed_at'] ?? '')), 'created_at' => $createdAt, 'updated_at' => trim((string) ($order['updated_at'] ?? $createdAt)), 'cancelled_at' => trim((string) ($order['cancelled_at'] ?? '')), 'cancelled_by' => trim((string) ($order['cancelled_by'] ?? '')), 'cancellation_reason' => trim((string) ($order['cancellation_reason'] ?? '')), 'admin_notified_at' => trim((string) ($order['admin_notified_at'] ?? '')), ]; return refreshOrderState($normalized); } function refreshOrderState($order) { if (!is_array($order)) { return null; } if (($order['status'] ?? '') === 'cancelled') { return $order; } if (($order['confirmation_status'] ?? '') === 'pending' && !empty($order['confirmation_expires_at'])) { $expiresAt = strtotime((string) $order['confirmation_expires_at']); if ($expiresAt !== false && time() > $expiresAt) { $order['confirmation_status'] = 'expired'; } } $processedCount = 0; foreach ($order['items'] as $item) { if (!empty($item['is_processed'])) { $processedCount++; } } if ($processedCount <= 0) { $order['status'] = 'open'; } elseif ($processedCount >= count($order['items'])) { $order['status'] = 'processed'; } else { $order['status'] = 'partial'; } return $order; } function expirePendingOrders() { $orders = getOrders(); $changed = false; foreach ($orders as &$order) { $updated = refreshOrderState($order); if ($updated !== $order) { $order = $updated; $changed = true; } } unset($order); if ($changed) { saveOrders($orders); } } function getOrderById($orderId) { $orderId = trim((string) $orderId); if ($orderId === '') { return null; } foreach (getOrders() as $order) { if ($order['id'] === $orderId) { return $order; } } return null; } function findOrderByConfirmationToken($token) { $token = trim((string) $token); if ($token === '') { return null; } foreach (getOrders() as $order) { if (($order['confirmation_token'] ?? '') === $token) { return $order; } } return null; } function buildOrderConfirmationUrl($token) { $path = '/order-confirm.php?token=' . urlencode($token); return buildAbsoluteUrl($path); } function buildAbsoluteUrl($path) { $path = '/' . ltrim((string) $path, '/'); $siteUrl = defined('SITE_URL') ? trim((string) SITE_URL) : ''; if (strpos($siteUrl, '://') !== false) { return rtrim($siteUrl, '/') . $path; } $basePath = trim($siteUrl); if ($basePath !== '' && $basePath !== '/') { $path = '/' . trim($basePath, '/') . $path; } $scheme = isHttpsRequest() ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? ''; if ($host === '') { return $path; } return $scheme . '://' . $host . $path; } function isHttpsRequest(): bool { if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') { return true; } if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') { return true; } if (!empty($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) { return true; } return false; } function createOrder($customerName, $customerEmail, $organizationId, $comment, $items) { $customerName = sanitize($customerName); $customerEmail = normalizeAdminEmail($customerEmail); $organizationId = normalizeOrganizationId($organizationId); $comment = trim((string) $comment); $items = normalizeOrderItems($items); if ($customerName === '') { return ['success' => false, 'message' => 'Bitte geben Sie einen Namen ein.']; } if (!isValidAdminEmail($customerEmail)) { return ['success' => false, 'message' => 'Bitte geben Sie eine gültige E-Mail-Adresse ein.']; } if (empty($items)) { return ['success' => false, 'message' => 'Der Warenkorb ist leer oder enthält ungültige Positionen.']; } $organization = getOrganizationById($organizationId); if ($organization === null || empty($organization['active'])) { return ['success' => false, 'message' => 'Bitte wählen Sie eine gültige Organisation aus.']; } $now = date('Y-m-d H:i:s'); $requiresConfirmation = isOrderConfirmationRequired(); $confirmationToken = $requiresConfirmation ? bin2hex(random_bytes(24)) : ''; $confirmationExpiresAt = ''; if ($requiresConfirmation) { $expires = new DateTimeImmutable(); $expires = $expires->modify('+' . getOrderConfirmationExpiryDays() . ' days'); $confirmationExpiresAt = $expires->format('Y-m-d H:i:s'); } $order = [ 'id' => generateOrderId(), 'customer_name' => $customerName, 'customer_email' => $customerEmail, 'organization_id' => $organization['id'], 'organization_label' => $organization['label'], 'comment' => $comment, 'items' => $items, 'status' => 'open', 'confirmation_status' => $requiresConfirmation ? 'pending' : 'not_required', 'confirmation_token' => $confirmationToken, 'confirmation_expires_at' => $confirmationExpiresAt, 'confirmed_at' => $requiresConfirmation ? '' : $now, 'created_at' => $now, 'updated_at' => $now, 'cancelled_at' => '', 'cancelled_by' => '', 'cancellation_reason' => '', 'admin_notified_at' => '', ]; $orders = getOrders(); $orders[] = $order; saveOrders($orders); if ($requiresConfirmation) { sendOrderConfirmationRequestEmail($order); } else { $result = sendConfirmedOrderAdminNotification($order); if ($result) { markOrderAdminNotified($order['id']); $order = getOrderById($order['id']); } sendOrderCreatedCustomerEmail($order); } return ['success' => true, 'order' => $order]; } function markOrderAdminNotified($orderId) { $orders = getOrders(); foreach ($orders as &$order) { if ($order['id'] !== $orderId) { continue; } $order['admin_notified_at'] = date('Y-m-d H:i:s'); $order['updated_at'] = date('Y-m-d H:i:s'); break; } unset($order); saveOrders($orders); } function confirmOrderByToken($token) { $orders = getOrders(); $now = date('Y-m-d H:i:s'); foreach ($orders as &$order) { if (($order['confirmation_token'] ?? '') !== $token) { continue; } $order = refreshOrderState($order); if ($order['status'] === 'cancelled') { return ['success' => false, 'message' => 'Diese Bestellung wurde storniert und kann nicht mehr bestätigt werden.']; } if ($order['confirmation_status'] === 'confirmed') { return ['success' => false, 'message' => 'Diese Bestellung wurde bereits bestätigt.']; } if ($order['confirmation_status'] === 'expired') { return ['success' => false, 'message' => 'Der Bestätigungslink ist abgelaufen.']; } if ($order['confirmation_status'] !== 'pending') { return ['success' => false, 'message' => 'Für diese Bestellung ist keine Bestätigung erforderlich.']; } $expiresAt = strtotime((string) $order['confirmation_expires_at']); if ($expiresAt !== false && time() > $expiresAt) { $order['confirmation_status'] = 'expired'; $order['updated_at'] = $now; saveOrders($orders); return ['success' => false, 'message' => 'Der Bestätigungslink ist abgelaufen.']; } $order['confirmation_status'] = 'confirmed'; $order['confirmed_at'] = $now; $order['updated_at'] = $now; saveOrders($orders); $sent = sendConfirmedOrderAdminNotification($order); if ($sent) { markOrderAdminNotified($order['id']); } sendOrderConfirmedCustomerEmail(getOrderById($order['id'])); return ['success' => true, 'order' => getOrderById($order['id'])]; } unset($order); return ['success' => false, 'message' => 'Bestellung nicht gefunden.']; } function toggleOrderItemProcessed($orderId, $itemIndex) { $orders = getOrders(); $now = date('Y-m-d H:i:s'); foreach ($orders as &$order) { if ($order['id'] !== $orderId) { continue; } if ($order['status'] === 'cancelled') { return ['success' => false, 'message' => 'Stornierte Bestellungen können nicht mehr bearbeitet werden.']; } if (($order['confirmation_status'] ?? '') === 'pending') { return ['success' => false, 'message' => 'Unbestätigte Bestellungen können noch nicht bearbeitet werden.']; } if (($order['confirmation_status'] ?? '') === 'expired') { return ['success' => false, 'message' => 'Abgelaufene unbestätigte Bestellungen können nicht bearbeitet werden.']; } if (!isset($order['items'][$itemIndex])) { return ['success' => false, 'message' => 'Position nicht gefunden.']; } $order['items'][$itemIndex]['is_processed'] = empty($order['items'][$itemIndex]['is_processed']); $order['updated_at'] = $now; $order = refreshOrderState($order); saveOrders($orders); return ['success' => true, 'order' => $order]; } unset($order); return ['success' => false, 'message' => 'Bestellung nicht gefunden.']; } function cancelOrder($orderId, $adminUsername, $reason = '') { $orders = getOrders(); $now = date('Y-m-d H:i:s'); $adminUsername = normalizeAdminUsername($adminUsername); $reason = trim((string) $reason); foreach ($orders as &$order) { if ($order['id'] !== $orderId) { continue; } if ($order['status'] === 'cancelled') { return ['success' => false, 'message' => 'Die Bestellung ist bereits storniert.']; } $order['status'] = 'cancelled'; $order['cancelled_at'] = $now; $order['cancelled_by'] = $adminUsername; $order['cancellation_reason'] = $reason; $order['updated_at'] = $now; saveOrders($orders); return ['success' => true, 'order' => $order]; } unset($order); return ['success' => false, 'message' => 'Bestellung nicht gefunden.']; } function getOrderStatusLabel($order) { if (($order['status'] ?? '') === 'cancelled') { return 'Storniert'; } if (($order['confirmation_status'] ?? '') === 'pending') { return 'Unbestätigt'; } if (($order['confirmation_status'] ?? '') === 'expired') { return 'Bestätigung abgelaufen'; } if (($order['status'] ?? '') === 'processed') { return 'Bearbeitet'; } if (($order['status'] ?? '') === 'partial') { return 'Teilweise bearbeitet'; } return 'Offen'; } function getOrderStatusClass($order) { if (($order['status'] ?? '') === 'cancelled') { return 'status-cancelled'; } if (($order['confirmation_status'] ?? '') === 'pending') { return 'status-unconfirmed'; } if (($order['confirmation_status'] ?? '') === 'expired') { return 'status-expired'; } if (($order['status'] ?? '') === 'processed') { return 'status-processed'; } if (($order['status'] ?? '') === 'partial') { return 'status-partial'; } return 'status-open'; } function formatDate($dateString) { $dateString = trim((string) $dateString); if ($dateString === '') { return '-'; } try { $date = new DateTimeImmutable($dateString); return $date->format('d.m.Y H:i'); } catch (Exception $exception) { return $dateString; } } function getCart() { $cart = $_SESSION['cart'] ?? []; if (!is_array($cart)) { $cart = []; } $normalized = []; foreach ($cart as $item) { $productId = isset($item['product_id']) ? (int) $item['product_id'] : 0; $size = trim((string) ($item['size'] ?? '')); $product = getProductById($productId); if ($product === null) { continue; } $sizes = getProductSizes($product); if (!empty($sizes)) { if ($size === '' || !in_array($size, $sizes, true)) { continue; } } else { $size = ''; } if (isset($normalized[$productId])) { unset($normalized[$productId]); } $normalized[$productId] = [ 'product_id' => $productId, 'size' => $size, ]; } $_SESSION['cart'] = array_values($normalized); return $_SESSION['cart']; } function addCartItem($productId, $size = '') { $productId = (int) $productId; $size = trim((string) $size); $product = getProductById($productId); if ($product === null) { return [ 'success' => false, 'status' => 'error', ]; } $sizes = getProductSizes($product); if (!empty($sizes)) { if ($size === '' || !in_array($size, $sizes, true)) { return [ 'success' => false, 'status' => 'error', ]; } } else { $size = ''; } $cart = getCart(); foreach ($cart as $index => $item) { if ((int) $item['product_id'] !== $productId) { continue; } $existingSize = trim((string) ($item['size'] ?? '')); if ($existingSize === $size) { return [ 'success' => true, 'status' => 'unchanged', 'size' => $size, ]; } $cart[$index]['size'] = $size; $_SESSION['cart'] = array_values($cart); return [ 'success' => true, 'status' => 'replaced', 'size' => $size, 'previous_size' => $existingSize, ]; } $cart[] = [ 'product_id' => $productId, 'size' => $size, ]; $_SESSION['cart'] = array_values($cart); return [ 'success' => true, 'status' => 'added', 'size' => $size, ]; } function removeCartItemByIndex($index) { $cart = getCart(); if (isset($cart[$index])) { unset($cart[$index]); $_SESSION['cart'] = array_values($cart); } } function clearCart() { $_SESSION['cart'] = []; } function getCartItemsDetailed() { $items = []; foreach (getCart() as $index => $cartItem) { $product = getProductById($cartItem['product_id']); if ($product === null) { continue; } $size = trim((string) ($cartItem['size'] ?? '')); $items[] = [ 'cart_index' => $index, 'product' => $product, 'size' => $size, 'availability_label' => $size !== '' ? getAvailabilityLabel($product, $size) : '', ]; } return $items; } function buildOrderItemsFromCart() { $items = []; foreach (getCart() as $cartItem) { $product = getProductById($cartItem['product_id']); if ($product === null) { continue; } $size = trim((string) ($cartItem['size'] ?? '')); $items[] = [ 'product_id' => $product['id'], 'size' => $size, 'is_processed' => false, ]; } return normalizeOrderItems($items); } function buildOrderItemsHtml($order) { $parts = []; foreach ($order['items'] as $item) { $label = '' . escape($item['product_name']) . ''; if ($item['size'] !== '') { $label .= ' - Größe: ' . escape($item['size']); } if (!empty($item['availability_label'])) { $label .= '' . escape(SITE_DEPARTMENT_NAME) . '
' . escape(SITE_SERVICE_NAME) . '
' . escape($order['id']) . '
Name: ' . escape($order['customer_name']) . '
E-Mail: ' . escape($order['customer_email']) . '
Organisation: ' . escape($order['organization_label']) . '
Erstellt am: ' . escape(formatDate($order['created_at'])) . '
Kommentar:
' . ($order['comment'] !== '' ? nl2br(escape($order['comment'])) : 'Kein Kommentar') . '
' . escape(SITE_NAME) . ' | ' . escape(SITE_DEPARTMENT_NAME) . '
' . escape(SITE_ADDRESS_LINE) . '
Guten Tag ' . escape($order['customer_name']) . ',
bitte bestätigen Sie Ihre Bestellung im ' . escape(SITE_SERVICE_NAME) . ' der Stadt Freising über den folgenden Link.
'; $extra = 'Der Link ist gültig bis: ' . escape($expiryText) . '
Falls der Button nicht funktioniert, verwenden Sie bitte diesen Link:
' . escape($link) . '
Guten Tag ' . escape($order['customer_name']) . ',
Ihre Bestellung wurde erfasst und an ' . escape(SITE_DEPARTMENT_NAME) . ' weitergeleitet.
'; $message = buildOrderSummaryHtml($order, 'Bestellung eingegangen', $intro); return sendEmail($order['customer_email'], $subject, $message); } function sendOrderConfirmedCustomerEmail($order) { $subject = SITE_SERVICE_NAME . ': Bestellung bestätigt - ' . $order['id']; $intro = 'Guten Tag ' . escape($order['customer_name']) . ',
Ihre Bestellung wurde bestätigt und an ' . escape(SITE_DEPARTMENT_NAME) . ' weitergeleitet.
'; $message = buildOrderSummaryHtml($order, 'Bestellung bestätigt', $intro); return sendEmail($order['customer_email'], $subject, $message); } function sendConfirmedOrderAdminNotification($order) { $recipient = getOrderRecipientEmail(); if (!isValidAdminEmail($recipient)) { return false; } $subject = SITE_SERVICE_NAME . ': Neue Bestellung - ' . $order['id']; $intro = 'Eine neue Bestellung im ' . escape(SITE_SERVICE_NAME) . ' der Stadt Freising wurde freigegeben und muss bearbeitet werden.
'; $message = buildOrderSummaryHtml($order, 'Neue PSA-Bestellung', $intro); $attachments = []; if (shouldAttachOrderPdfToAdminEmail()) { $attachments[] = [ 'filename' => 'bestellung-' . strtolower($order['id']) . '.pdf', 'content_type' => 'application/pdf', 'content' => generateOrderPdf($order), ]; } return sendEmail($recipient, $subject, $message, true, $attachments); } function sendEmail($to, $subject, $message, $isHtml = true, $attachments = []) { $headers = []; $headers[] = 'From: ' . FROM_NAME . ' <' . FROM_EMAIL . '>'; $headers[] = 'Reply-To: ' . FROM_EMAIL; $headers[] = 'X-Mailer: PHP/' . phpversion(); if (empty($attachments)) { if ($isHtml) { $headers[] = 'MIME-Version: 1.0'; $headers[] = 'Content-Type: text/html; charset=UTF-8'; } else { $headers[] = 'Content-Type: text/plain; charset=UTF-8'; } return mail($to, $subject, $message, implode("\r\n", $headers)); } $boundary = '=_Boundary_' . bin2hex(random_bytes(8)); $headers[] = 'MIME-Version: 1.0'; $headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"'; $body = []; $body[] = '--' . $boundary; $body[] = 'Content-Type: ' . ($isHtml ? 'text/html' : 'text/plain') . '; charset=UTF-8'; $body[] = 'Content-Transfer-Encoding: 8bit'; $body[] = ''; $body[] = $message; foreach ($attachments as $attachment) { if (!is_array($attachment)) { continue; } $filename = trim((string) ($attachment['filename'] ?? 'attachment.bin')); $contentType = trim((string) ($attachment['content_type'] ?? 'application/octet-stream')); $content = isset($attachment['content']) ? (string) $attachment['content'] : ''; $body[] = '--' . $boundary; $body[] = 'Content-Type: ' . $contentType . '; name="' . $filename . '"'; $body[] = 'Content-Transfer-Encoding: base64'; $body[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; $body[] = ''; $body[] = chunk_split(base64_encode($content)); } $body[] = '--' . $boundary . '--'; $body[] = ''; return mail($to, $subject, implode("\r\n", $body), implode("\r\n", $headers)); } function buildOrderPdfLines($order) { $lines = [ SITE_FULL_NAME, SITE_DEPARTMENT_NAME, '', 'Bestellservice', 'Bestellnummer: ' . $order['id'], 'Erstellt am: ' . formatDate($order['created_at']), 'Name: ' . $order['customer_name'], 'E-Mail: ' . $order['customer_email'], 'Organisation: ' . $order['organization_label'], '', 'Artikel:', ]; foreach ($order['items'] as $item) { $line = '- ' . $item['product_name']; if ($item['size'] !== '') { $line .= ' | Größe: ' . $item['size']; } if ($item['availability_label'] !== '') { $line .= ' | Hinweis: ' . preg_replace('/\s+/', ' ', $item['availability_label']); } $lines[] = $line; } $lines[] = ''; $lines[] = 'Kommentar:'; if ($order['comment'] !== '') { foreach (preg_split('/\r\n|\r|\n/', $order['comment']) as $commentLine) { $lines[] = $commentLine; } } else { $lines[] = 'Kein Kommentar'; } return $lines; } function pdfEscapeText($text) { $text = str_replace('\\', '\\\\', $text); $text = str_replace('(', '\(', $text); $text = str_replace(')', '\)', $text); $text = str_replace("\r", '', $text); if (function_exists('iconv')) { $converted = @iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text); if (is_string($converted)) { $text = $converted; } } return $text; } function generateOrderPdf($order) { $lines = buildOrderPdfLines($order); $content = "BT\n/F1 12 Tf\n14 TL\n50 790 Td\n"; $first = true; foreach ($lines as $line) { if (!$first) { $content .= "T*\n"; } $first = false; $content .= '(' . pdfEscapeText($line) . ") Tj\n"; } $content .= "ET"; $length = strlen($content); $objects = []; $objects[] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; $objects[] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; $objects[] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n"; $objects[] = "4 0 obj\n<< /Length " . $length . " >>\nstream\n" . $content . "\nendstream\nendobj\n"; $objects[] = "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"; $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $object) { $offsets[] = strlen($pdf); $pdf .= $object; } $xrefOffset = strlen($pdf); $pdf .= "xref\n0 " . (count($objects) + 1) . "\n"; $pdf .= "0000000000 65535 f \n"; for ($i = 1; $i <= count($objects); $i++) { $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]); } $pdf .= "trailer\n<< /Size " . (count($objects) + 1) . " /Root 1 0 R >>\n"; $pdf .= "startxref\n" . $xrefOffset . "\n%%EOF"; return $pdf; }