'; } function getUploadFilename($filename) { $filename = trim((string) $filename); if ($filename === "") { return ""; } return basename($filename); } function getUploadPath($filename) { $filename = getUploadFilename($filename); if ($filename === "") { return null; } return rtrim(UPLOADS_DIR, "/\\") . "/" . $filename; } function getUploadUrl($filename) { $filename = getUploadFilename($filename); if ($filename === "") { return null; } return rtrim(UPLOADS_URL, "/\\") . "/" . rawurlencode($filename); } function setFlashMessage($key, $type, $message) { $key = trim((string) $key); $type = trim((string) $type); $message = trim((string) $message); if ($key === "" || $type === "" || $message === "") { return; } $_SESSION["flash_messages"][$key] = [ "type" => $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); return 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) { return 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 getCategoryChipPalette($categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId === "") { return [ "background" => "#ebe8df", "border" => "#d0c8b5", "text" => "#4b4b4b", ]; } $hash = crc32($categoryId); if ($hash < 0) { $hash = $hash * -1; } // Spread hues across a distinct red -> blue range. $hueSteps = [0, 16, 32, 48, 66, 84, 104, 128, 152, 176, 200, 224]; $hue = $hueSteps[$hash % count($hueSteps)]; $saturation = 44 + (($hash >> 8) % 10); $lightness = 84 + (($hash >> 16) % 7); $background = "hsl(" . $hue . ", " . $saturation . "%, " . $lightness . "%)"; $border = "hsl(" . $hue . ", " . ($saturation + 8) . "%, " . ($lightness - 16) . "%)"; $text = "hsl(" . $hue . ", " . ($saturation + 18) . "%, 24%)"; return [ "background" => $background, "border" => $border, "text" => $text, ]; } 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; } } return 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): bool { return writeJsonFile(getFaqFilePath(), ["content" => (string) $markdown]); } function renderFaqInlineMarkdown(string $text): string { $escaped = escape($text); $escaped = preg_replace_callback( '/\[([^\]]+)\]\(([^)\s]+)\)/', function ($matches) { $label = $matches[1]; $url = trim(html_entity_decode($matches[2], ENT_QUOTES, "UTF-8")); if ($url === "") { return $matches[0]; } $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME)); if (!in_array($scheme, ["http", "https", "mailto"], true)) { return $matches[0]; } return '' . $label . ""; }, $escaped, ); $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) { return 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() { $startpageIntroText = ""; if (defined("DISCLAIMER_LINES") && is_array(DISCLAIMER_LINES)) { $lines = array_filter( array_map(function ($line) { return trim((string) $line); }, DISCLAIMER_LINES), function ($line) { return $line !== ""; }, ); $startpageIntroText = implode("\n", $lines); } 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, "startpage_intro_text" => $startpageIntroText, ]; } 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; } $startpageIntroText = trim( (string) ($settings["startpage_intro_text"] ?? $defaults["startpage_intro_text"]), ); 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"] ), "startpage_intro_text" => $startpageIntroText, ]; } function getSystemSettings() { $data = readJsonFile(SETTINGS_FILE); return normalizeSystemSettings($data["settings"] ?? []); } function saveSystemSettings($settings) { return 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 getStartpageIntroLines() { $settings = getSystemSettings(); $text = trim((string) ($settings["startpage_intro_text"] ?? "")); if ($text === "") { return []; } $lines = preg_split('/\R+/', $text) ?: []; $lines = array_values( array_filter( array_map(function ($line) { return trim((string) $line); }, $lines), function ($line) { return $line !== ""; }, ), ); return $lines; } 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; } } $result = writeJsonFile(ORDERS_FILE, [ "orders" => array_values($normalized), ]); if (!$result) { logError("Failed to save orders", [ "file" => ORDERS_FILE, "order_count" => count($normalized), ]); } return (bool) $result; } 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); logAccess("Order created in createOrder", [ "order_id" => $order["id"], "customer" => $customerEmail, "organization" => $organization["label"], ]); 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_HEADER) . '
' . 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 PSA-Bestellung 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 pdfEncodeWinAnsi($text) { $text = str_replace("\r", "", (string) $text); if (!function_exists("iconv")) { return preg_replace('/[^\x09\x0A\x20-\x7E]/', "?", $text); } $chars = preg_split("//u", $text, -1, PREG_SPLIT_NO_EMPTY); if (!is_array($chars)) { $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT//IGNORE", $text); return is_string($fallback) ? $fallback : $text; } $result = ""; foreach ($chars as $char) { $converted = @iconv("UTF-8", "Windows-1252", $char); if ($converted !== false) { $result .= $converted; continue; } $fallback = @iconv("UTF-8", "Windows-1252//TRANSLIT", $char); if ($fallback !== false && $fallback !== "") { $result .= $fallback; continue; } $result .= "?"; } return $result; } function pdfEscapeText($text) { $text = str_replace("\\", "\\\\", $text); $text = str_replace("(", "\(", $text); $text = str_replace(")", "\)", $text); return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', "", $text); } function pdfWrapAnsiText($text, $maxChars) { $lines = []; $paragraphs = explode("\n", str_replace("\r", "", (string) $text)); foreach ($paragraphs as $paragraph) { $normalized = trim(preg_replace("/\s+/", " ", $paragraph)); if ($normalized === "") { $lines[] = ""; continue; } $words = explode(" ", $normalized); $current = ""; foreach ($words as $word) { if ($word === "") { continue; } if (strlen($word) > $maxChars) { if ($current !== "") { $lines[] = $current; $current = ""; } $parts = str_split($word, $maxChars); $lastIndex = count($parts) - 1; for ($i = 0; $i < $lastIndex; $i++) { $lines[] = $parts[$i]; } $current = $parts[$lastIndex]; continue; } $candidate = $current === "" ? $word : $current . " " . $word; if (strlen($candidate) <= $maxChars) { $current = $candidate; } else { if ($current !== "") { $lines[] = $current; } $current = $word; } } if ($current !== "") { $lines[] = $current; } } if (empty($lines)) { $lines[] = ""; } return $lines; } function generateOrderPdf($order) { $pageWidth = 595; $pageHeight = 842; $leftMargin = 45; $topY = 800; $bottomY = 60; $lineHeight = 14; $pages = []; $pageContent = ""; $y = $topY; $pageNumber = 0; $encodeText = function ($text) { return pdfEncodeWinAnsi((string) $text); }; $siteName = $encodeText(SITE_FULL_NAME); $orderId = $encodeText($order["id"]); $createdAt = $encodeText(formatDate($order["created_at"])); $customerName = $encodeText($order["customer_name"]); $customerEmail = $encodeText($order["customer_email"]); $organization = $encodeText($order["organization_label"]); $commentRaw = (string) $order["comment"]; $writeText = function ($x, $y, $encodedText, $fontSize = 12) use (&$pageContent) { $pageContent .= "BT\n/F1 " . $fontSize . " Tf\n1 0 0 1 " . number_format($x, 2, ".", "") . " " . number_format($y, 2, ".", "") . " Tm\n(" . pdfEscapeText($encodedText) . ") Tj\nET\n"; }; $startPage = function () use ( &$pages, &$pageContent, &$y, &$pageNumber, $topY, $leftMargin, $lineHeight, $siteName, $orderId, $createdAt, $writeText, $encodeText ) { if ($pageContent !== "") { $pages[] = $pageContent; } $pageNumber++; $pageContent = ""; $y = $topY; foreach (pdfWrapAnsiText($siteName, 70) as $index => $line) { $writeText($leftMargin, $y, $line, $index === 0 ? 15 : 11); $y -= $index === 0 ? 19 : $lineHeight; } $headerLine = "Bestellung: " . $orderId; if ($pageNumber > 1) { $headerLine .= " | Seite " . $encodeText($pageNumber); } $writeText($leftMargin, $y, $headerLine, 12); $y -= 17; $writeText($leftMargin, $y, "Erstellt am: " . $createdAt, 11); $y -= 20; }; $ensureSpace = function ($requiredHeight) use (&$y, $bottomY, $startPage) { if ($y - $requiredHeight < $bottomY) { $startPage(); } }; $writeWrapped = function ( $encodedText, $maxChars, $fontSize = 11, $x = null ) use (&$y, $lineHeight, $leftMargin, $writeText, $ensureSpace) { $targetX = $x === null ? $leftMargin : $x; $lines = pdfWrapAnsiText($encodedText, $maxChars); foreach ($lines as $line) { $ensureSpace($lineHeight); $writeText($targetX, $y, $line, $fontSize); $y -= $lineHeight; } }; $writeSectionTitle = function ($titleText) use ( &$y, $leftMargin, $writeText, $ensureSpace, $encodeText ) { $ensureSpace(24); $writeText($leftMargin, $y, $encodeText($titleText), 13); $y -= 18; }; $startPage(); $writeSectionTitle("Gehört zu"); $writeWrapped("Name: " . $customerName, 80); $writeWrapped("Organisation: " . $organization, 80); $writeWrapped("E-Mail: " . $customerEmail, 80); $y -= 6; $writeSectionTitle("Artikelliste"); $itemNumber = 1; if (empty($order["items"])) { $writeWrapped($encodeText("Keine Artikel"), 80); } else { foreach ($order["items"] as $item) { $itemName = $encodeText($item["product_name"]); $sizeLabel = $encodeText($item["size"]); $hintLabel = $encodeText( preg_replace("/\s+/", " ", (string) $item["availability_label"]), ); $writeWrapped($encodeText($itemNumber . ". ") . $itemName, 78); if ($sizeLabel !== "") { $writeWrapped($encodeText(" Größe: ") . $sizeLabel, 76); } if ($hintLabel !== "") { $writeWrapped($encodeText(" Hinweis: ") . $hintLabel, 76); } $y -= 4; $itemNumber++; } } $y -= 4; $writeSectionTitle("Kommentar"); if (trim($commentRaw) === "") { $writeWrapped($encodeText("Kein Kommentar"), 80); } else { foreach (preg_split('/\r\n|\r|\n/', $commentRaw) as $commentLine) { $writeWrapped($encodeText($commentLine), 82); } } $y -= 6; $writeSectionTitle("Lagerbearbeitung"); $warehouseLines = [ "Ausgegeben am: ________________________", "Ausgegeben durch: _____________________", "Unterschrift: _________________________", "", "[ ] Vollständig ausgegeben", "[ ] Teilweise ausgegeben", ]; foreach ($warehouseLines as $line) { if ($line === "") { $ensureSpace($lineHeight); $y -= $lineHeight; continue; } $writeWrapped($encodeText($line), 80); } if ($pageContent !== "") { $pages[] = $pageContent; } if (empty($pages)) { $pages[] = ""; } $fontObjectNumber = 3; $objects = []; $objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; $objects[ $fontObjectNumber ] = "3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n"; $kids = []; $nextObjectNumber = 4; foreach ($pages as $pageStream) { $pageObjectNumber = $nextObjectNumber++; $contentObjectNumber = $nextObjectNumber++; $kids[] = $pageObjectNumber . " 0 R"; $objects[$pageObjectNumber] = $pageObjectNumber . " 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " . $pageWidth . " " . $pageHeight . "] /Contents " . $contentObjectNumber . " 0 R /Resources << /Font << /F1 " . $fontObjectNumber . " 0 R >> >> >>\nendobj\n"; $objects[$contentObjectNumber] = $contentObjectNumber . " 0 obj\n<< /Length " . strlen($pageStream) . " >>\nstream\n" . $pageStream . "\nendstream\nendobj\n"; } $objects[2] = "2 0 obj\n<< /Type /Pages /Kids [" . implode(" ", $kids) . "] /Count " . count($kids) . " >>\nendobj\n"; ksort($objects); $pdf = "%PDF-1.4\n"; $offsets = [0]; foreach ($objects as $number => $objectContent) { $offsets[$number] = strlen($pdf); $pdf .= $objectContent; } $lastObjectNumber = (int) max(array_keys($objects)); $xrefOffset = strlen($pdf); $pdf .= "xref\n0 " . ($lastObjectNumber + 1) . "\n"; $pdf .= "0000000000 65535 f \n"; for ($i = 1; $i <= $lastObjectNumber; $i++) { if (isset($offsets[$i])) { $pdf .= sprintf("%010d 00000 n \n", $offsets[$i]); } else { $pdf .= "0000000000 00000 f \n"; } } $pdf .= "trailer\n<< /Size " . ($lastObjectNumber + 1) . " /Root 1 0 R >>\n"; $pdf .= "startxref\n" . $xrefOffset . "\n%%EOF"; return $pdf; } /** * Input Validation Class */ class Validator { private $errors = []; private $data = []; public function __construct($data) { $this->data = $data; } public function required($field, $label = null) { $value = $this->get($field); if (empty(trim($value ?? ""))) { $this->errors[] = ($label ?? $field) . " ist erforderlich."; } return $this; } public function email($field, $label = null) { $value = $this->get($field); if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) { $this->errors[] = ($label ?? $field) . " muss eine gültige E-Mail-Adresse sein."; } return $this; } public function minLength($field, $min, $label = null) { $value = $this->get($field); if (!empty($value) && strlen($value) < $min) { $this->errors[] = ($label ?? $field) . " muss mindestens " . $min . " Zeichen lang sein."; } return $this; } public function maxLength($field, $max, $label = null) { $value = $this->get($field); if (!empty($value) && strlen($value) > $max) { $this->errors[] = ($label ?? $field) . " darf maximal " . $max . " Zeichen lang sein."; } return $this; } public function inArray($field, $allowed, $label = null) { $value = $this->get($field); if (!empty($value) && !in_array($value, $allowed, true)) { $this->errors[] = ($label ?? $field) . " ist ungültig."; } return $this; } public function get($field) { return $this->data[$field] ?? null; } public function getErrors() { return $this->errors; } public function isValid() { return empty($this->errors); } } /** * Error Logging Functions */ if (!defined("LOG_DIR")) { define("LOG_DIR", DATA_DIR . "logs/"); } if (!defined("ERROR_LOG_FILE")) { define("ERROR_LOG_FILE", LOG_DIR . "error.log"); } if (!defined("ACCESS_LOG_FILE")) { define("ACCESS_LOG_FILE", LOG_DIR . "access.log"); } function initLogging() { if (!is_dir(LOG_DIR)) { mkdir(LOG_DIR, 02775, true); @chmod(LOG_DIR, 02775); } } function logError($message, $context = [], $level = "ERROR") { initLogging(); $entry = [ "timestamp" => date("Y-m-d H:i:s.u"), "level" => $level, "message" => $message, "context" => $context, "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown", "user_agent" => $_SERVER["HTTP_USER_AGENT"] ?? "unknown", "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown", "session_id" => session_id() ? substr(session_id(), 0, 8) . "..." : "none", ]; $logLine = json_encode($entry, JSON_UNESCAPED_UNICODE) . PHP_EOL; file_put_contents(ERROR_LOG_FILE, $logLine, FILE_APPEND | LOCK_EX); if (defined("DEVELOPMENT_MODE") && DEVELOPMENT_MODE) { error_log( "PSA Order: " . $message . " | Context: " . json_encode($context), ); } } function logAccess($message, $context = []) { initLogging(); $entry = [ "timestamp" => date("Y-m-d H:i:s.u"), "message" => $message, "context" => $context, "ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown", "request_method" => $_SERVER["REQUEST_METHOD"] ?? "unknown", "request_uri" => $_SERVER["REQUEST_URI"] ?? "unknown", ]; $logLine = json_encode($entry, JSON_UNESCAPED_UNICODE) . PHP_EOL; file_put_contents(ACCESS_LOG_FILE, $logLine, FILE_APPEND | LOCK_EX); } function handleException($exception) { logError( "Uncaught exception: " . $exception->getMessage(), [ "file" => $exception->getFile(), "line" => $exception->getLine(), "trace" => $exception->getTraceAsString(), ], "CRITICAL", ); if (defined("DEVELOPMENT_MODE") && DEVELOPMENT_MODE) { echo "" .
escape($exception->getMessage()) .
"";
} else {
echo "Bitte versuchen Sie es später erneut.
"; } exit(); } set_exception_handler("handleException"); function handleError($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) { return false; } logError( "PHP Error: " . $errstr, [ "type" => $errno, "file" => $errfile, "line" => $errline, ], "WARNING", ); return true; } set_error_handler("handleError");