$record) { $normalizedUsername = normalizeAdminUsername($username); if ($normalizedUsername === '') { continue; } $hash = ''; $description = ''; $email = getDefaultAdminEmail(); if (is_string($record)) { $hash = $record; $description = getDefaultAdminDescription($normalizedUsername); } elseif (is_array($record)) { $hash = isset($record['password_hash']) && is_string($record['password_hash']) ? $record['password_hash'] : ''; $description = isset($record['description']) ? normalizeAdminDescription($record['description']) : getDefaultAdminDescription($normalizedUsername); $email = isset($record['email']) ? normalizeAdminEmail($record['email']) : getDefaultAdminEmail(); } if ($hash === '') { continue; } if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($normalizedUsername); } if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $accounts[$normalizedUsername] = [ 'password_hash' => $hash, 'description' => $description, 'email' => $email ]; } } return $accounts; } /** * Get admin users for authentication (username => password hash). */ function getAdminUsers() { $accounts = getAdminAccounts(); $admins = []; foreach ($accounts as $username => $account) { if (!isset($account['password_hash']) || !is_string($account['password_hash']) || $account['password_hash'] === '') { continue; } $admins[$username] = $account['password_hash']; } return $admins; } /** * Save full admin accounts to JSON store. */ function saveAdminAccounts($accounts) { $sanitizedAccounts = []; foreach ($accounts as $username => $account) { $normalizedUsername = normalizeAdminUsername($username); if ($normalizedUsername === '' || !is_array($account)) { continue; } $hash = isset($account['password_hash']) && is_string($account['password_hash']) ? $account['password_hash'] : ''; if ($hash === '') { continue; } $description = isset($account['description']) ? normalizeAdminDescription($account['description']) : getDefaultAdminDescription($normalizedUsername); if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($normalizedUsername); } $email = isset($account['email']) ? normalizeAdminEmail($account['email']) : getDefaultAdminEmail(); if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $sanitizedAccounts[$normalizedUsername] = [ 'password_hash' => $hash, 'description' => $description, 'email' => $email ]; } ksort($sanitizedAccounts); writeJsonFile(ADMINS_FILE, ['admins' => $sanitizedAccounts]); } /** * Save admin users to JSON store (username => password hash). */ function saveAdminUsers($admins) { $existingAccounts = getAdminAccounts(); $normalizedAccounts = []; foreach ($admins as $username => $hash) { $normalizedUsername = normalizeAdminUsername($username); if ($normalizedUsername === '' || !is_string($hash) || $hash === '') { continue; } $description = isset($existingAccounts[$normalizedUsername]['description']) ? normalizeAdminDescription($existingAccounts[$normalizedUsername]['description']) : getDefaultAdminDescription($normalizedUsername); if (!isValidAdminDescription($description)) { $description = getDefaultAdminDescription($normalizedUsername); } $email = isset($existingAccounts[$normalizedUsername]['email']) ? normalizeAdminEmail($existingAccounts[$normalizedUsername]['email']) : getDefaultAdminEmail(); if (!isValidAdminEmail($email)) { $email = getDefaultAdminEmail(); } $normalizedAccounts[$normalizedUsername] = [ 'password_hash' => $hash, 'description' => $description, 'email' => $email ]; } saveAdminAccounts($normalizedAccounts); } /** * Get default category records. */ function getDefaultCategories() { return [ ['id' => 'apparel', 'label' => 'Bekleidung'], ['id' => 'merch', 'label' => 'Merchandise'] ]; } /** * Normalize category id input to a stable slug. */ 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); $id = trim((string) $id, '-'); return $id; } /** * Normalize category label input. */ function normalizeCategoryLabel($label) { return trim((string) $label); } /** * Validate category label. */ function isValidCategoryLabel($label) { $label = normalizeCategoryLabel($label); if ($label === '') { return false; } $length = function_exists('mb_strlen') ? mb_strlen($label) : strlen($label); return $length <= 80; } /** * Normalize category records from storage. */ 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); } /** * Get all categories. */ function getCategories() { $data = readJsonFile(CATEGORIES_FILE); $categories = isset($data['categories']) ? $data['categories'] : []; return normalizeCategories($categories); } /** * Get category by id. */ function getCategoryById($categoryId) { $categoryId = normalizeCategoryId($categoryId); foreach (getCategories() as $category) { if ($category['id'] === $categoryId) { return $category; } } return null; } /** * Get category label by id with fallback to raw id. */ function getCategoryLabel($categoryId) { $category = getCategoryById($categoryId); if ($category !== null) { return $category['label']; } $categoryId = trim((string) $categoryId); return $categoryId !== '' ? $categoryId : 'Unbekannt'; } /** * Get category labels by ids. */ function getCategoryLabels($categoryIds) { $labels = []; foreach (normalizeProductCategoryIds($categoryIds) as $categoryId) { $labels[] = getCategoryLabel($categoryId); } return $labels; } /** * Save categories. */ function saveCategories($categories) { writeJsonFile(CATEGORIES_FILE, ['categories' => normalizeCategories($categories)]); } /** * Generate a unique category id from a label. */ function generateCategoryIdFromLabel($label, $existingCategories = []) { $baseId = normalizeCategoryId($label); if ($baseId === '') { $baseId = 'category'; } $usedIds = []; foreach (normalizeCategories($existingCategories) as $category) { $usedIds[$category['id']] = true; } $candidate = $baseId; $counter = 2; while (isset($usedIds[$candidate])) { $candidate = $baseId . '-' . $counter; $counter++; } return $candidate; } /** * Check whether any product uses a category id. */ function isCategoryInUse($categoryId) { $categoryId = normalizeCategoryId($categoryId); foreach (getProducts() as $product) { if (productHasCategory($product, $categoryId)) { return true; } } return false; } /** * Normalize product category ids from legacy or current storage. */ function normalizeProductCategoryIds($categoryValue) { if (is_array($categoryValue)) { $rawCategoryIds = $categoryValue; } elseif ($categoryValue === null || $categoryValue === '') { $rawCategoryIds = []; } else { $rawCategoryIds = [$categoryValue]; } $normalized = []; foreach ($rawCategoryIds as $categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId === '') { continue; } $normalized[$categoryId] = $categoryId; } return array_values($normalized); } /** * Get normalized category ids for a product. */ function getProductCategoryIds($product) { if (isset($product['categories'])) { return normalizeProductCategoryIds($product['categories']); } return normalizeProductCategoryIds($product['category'] ?? []); } /** * Determine whether a product is assigned to a category. */ function productHasCategory($product, $categoryId) { $categoryId = normalizeCategoryId($categoryId); if ($categoryId === '') { return false; } return in_array($categoryId, getProductCategoryIds($product), true); } /** * Parse product sizes into a normalized array. */ 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 === '') { continue; } $normalized[$size] = $size; } return array_values($normalized); } /** * Determine whether a product uses size-based stock. */ function productUsesSizeStock($product) { return !empty(getProductSizes($product)); } /** * Normalize a single product record for backwards compatibility. */ function normalizeProductRecord($product, $defaultCategoryId = '') { if (!is_array($product)) { return null; } $product['id'] = isset($product['id']) ? (int) $product['id'] : 0; $product['name'] = isset($product['name']) ? trim((string) $product['name']) : ''; $product['description'] = isset($product['description']) ? trim((string) $product['description']) : ''; $product['price'] = isset($product['price']) ? (float) $product['price'] : 0.0; $product['image'] = isset($product['image']) ? trim((string) $product['image']) : ''; $categoryIds = getProductCategoryIds($product); if (empty($categoryIds) && $defaultCategoryId !== '') { $categoryIds = [$defaultCategoryId]; } $product['categories'] = $categoryIds; unset($product['category']); $sizes = getProductSizes($product); $stockBySize = isset($product['stock_by_size']) && is_array($product['stock_by_size']) ? $product['stock_by_size'] : []; if (empty($sizes) && array_key_exists('stock', $product)) { $sizes = ['Standard']; $stockBySize = ['Standard' => (int) $product['stock']]; } if (!empty($sizes)) { $normalizedStockBySize = []; foreach ($sizes as $size) { $normalizedStockBySize[$size] = isset($stockBySize[$size]) ? max(0, (int) $stockBySize[$size]) : 0; } $product['sizes'] = implode(',', $sizes); $product['stock_by_size'] = $normalizedStockBySize; unset($product['stock']); } else { $product['stock'] = isset($product['stock']) ? max(0, (int) $product['stock']) : 0; unset($product['stock_by_size']); unset($product['sizes']); } return $product; } /** * Get all products */ 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) { $normalizedProduct = normalizeProductRecord($product, $defaultCategoryId); if ($normalizedProduct === null) { continue; } $products[] = $normalizedProduct; } return $products; } /** * Get product by ID */ function getProductById($id) { $products = getProducts(); foreach ($products as $product) { if ($product['id'] == $id) { return $product; } } return null; } /** * Save products */ function saveProducts($products) { $categories = getCategories(); $defaultCategoryId = !empty($categories) ? $categories[0]['id'] : 'apparel'; $normalizedProducts = []; foreach ($products as $product) { $normalizedProduct = normalizeProductRecord($product, $defaultCategoryId); if ($normalizedProduct === null) { continue; } $normalizedProducts[] = $normalizedProduct; } $data = ['products' => $normalizedProducts]; writeJsonFile(PRODUCTS_FILE, $data); } /** * Resolve FAQ file path and force storage inside DATA_DIR. */ function getFaqFilePath(): string { $dataDir = defined('DATA_DIR') && is_string(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; } /** * Get FAQ markdown content from JSON store. */ function getFaqContent(): string { $defaultContent = "# FAQ\n\nHier kann der FAQ-Inhalt im Admin-Bereich bearbeitet werden."; $data = readJsonFile(getFaqFilePath()); if (!is_array($data)) { return $defaultContent; } if (!isset($data['content']) || !is_string($data['content'])) { return $defaultContent; } return $data['content']; } /** * Save FAQ markdown content to JSON store. */ function saveFaqContent(string $markdown): void { writeJsonFile(getFaqFilePath(), ['content' => (string) $markdown]); } /** * Render inline markdown safely (bold + italic). */ function renderFaqInlineMarkdown(string $text): string { $escaped = htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); // Apply markdown after escaping so raw HTML is never executed. $escaped = preg_replace('/\*\*(.+?)\*\*/s', '$1', $escaped); $escaped = preg_replace('/(?$1', $escaped); return $escaped; } /** * Render a minimal safe markdown subset for FAQ content. */ 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; } $renderedLines = []; foreach ($paragraphLines as $line) { $renderedLines[] = renderFaqInlineMarkdown($line); } $htmlParts[] = '

' . implode("
\n", $renderedLines) . '

'; $paragraphLines = []; }; $closeList = function () use (&$listType, &$htmlParts): void { if ($listType === '') { return; } $htmlParts[] = ''; $listType = ''; }; foreach ($lines as $line) { $line = rtrim($line); $trimmed = trim($line); if ($trimmed === '') { $flushParagraph(); $closeList(); continue; } if (preg_match('/^(#{1,3})\s+(.+)$/', $trimmed, $matches) === 1) { $flushParagraph(); $closeList(); $level = strlen($matches[1]); $htmlParts[] = '' . renderFaqInlineMarkdown($matches[2]) . ''; continue; } if (preg_match('/^\s*[-*]\s+(.+)$/', $line, $matches) === 1) { $flushParagraph(); if ($listType !== 'ul') { $closeList(); $listType = 'ul'; $htmlParts[] = '