| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389 |
- <?php
- require_once __DIR__ . '/../config.php';
- /**
- * Read JSON file and return decoded data
- */
- function readJsonFile($file) {
- if (!file_exists($file)) {
- return [];
- }
- $content = file_get_contents($file);
- if (empty($content)) {
- return [];
- }
- $data = json_decode($content, true);
- return $data ? $data : [];
- }
- /**
- * Write data to JSON file
- */
- function writeJsonFile($file, $data) {
- $dir = dirname($file);
- if (!is_dir($dir)) {
- mkdir($dir, 0755, true);
- }
- file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- }
- /**
- * Normalize admin username input.
- */
- function normalizeAdminUsername($username) {
- return trim((string)$username);
- }
- /**
- * Normalize admin description input.
- */
- function normalizeAdminDescription($description) {
- return trim((string)$description);
- }
- /**
- * Normalize admin email input.
- */
- function normalizeAdminEmail($email) {
- return strtolower(trim((string)$email));
- }
- /**
- * Validate admin username format.
- */
- function isValidAdminUsername($username) {
- $username = normalizeAdminUsername($username);
- return preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{2,49}$/', $username) === 1;
- }
- /**
- * Validate admin description.
- */
- function isValidAdminDescription($description) {
- $description = normalizeAdminDescription($description);
- if ($description === '') {
- return false;
- }
- $length = function_exists('mb_strlen') ? mb_strlen($description) : strlen($description);
- return $length <= 120;
- }
- /**
- * Validate admin email.
- */
- function isValidAdminEmail($email) {
- $email = normalizeAdminEmail($email);
- return $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
- }
- /**
- * Get default description for an admin account.
- */
- function getDefaultAdminDescription($username) {
- return 'Admin';
- }
- /**
- * Get fallback admin email address from config.
- */
- function getDefaultAdminEmail() {
- if (!defined('ADMIN_EMAIL') || !is_string(ADMIN_EMAIL)) {
- return '';
- }
- $fallbackEmail = normalizeAdminEmail(ADMIN_EMAIL);
- if (!isValidAdminEmail($fallbackEmail)) {
- return '';
- }
- return $fallbackEmail;
- }
- /**
- * Get full admin account records from JSON store.
- */
- function getAdminAccounts() {
- $data = readJsonFile(ADMINS_FILE);
- $accounts = [];
- if (isset($data['admins']) && is_array($data['admins'])) {
- foreach ($data['admins'] as $username => $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 all products
- */
- function getProducts() {
- $data = readJsonFile(PRODUCTS_FILE);
- return isset($data['products']) ? $data['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) {
- $data = ['products' => $products];
- 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', '<strong>$1</strong>', $escaped);
- $escaped = preg_replace('/(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)/s', '<em>$1</em>', $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[] = '<p>' . implode("<br>\n", $renderedLines) . '</p>';
- $paragraphLines = [];
- };
- $closeList = function () use (&$listType, &$htmlParts): void {
- if ($listType === '') {
- return;
- }
- $htmlParts[] = '</' . $listType . '>';
- $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[] = '<h' . $level . '>' . renderFaqInlineMarkdown($matches[2]) . '</h' . $level . '>';
- continue;
- }
- if (preg_match('/^\s*[-*]\s+(.+)$/', $line, $matches) === 1) {
- $flushParagraph();
- if ($listType !== 'ul') {
- $closeList();
- $listType = 'ul';
- $htmlParts[] = '<ul>';
- }
- $htmlParts[] = '<li>' . renderFaqInlineMarkdown($matches[1]) . '</li>';
- continue;
- }
- if (preg_match('/^\s*\d+\.\s+(.+)$/', $line, $matches) === 1) {
- $flushParagraph();
- if ($listType !== 'ol') {
- $closeList();
- $listType = 'ol';
- $htmlParts[] = '<ol>';
- }
- $htmlParts[] = '<li>' . renderFaqInlineMarkdown($matches[1]) . '</li>';
- continue;
- }
- $closeList();
- $paragraphLines[] = $trimmed;
- }
- $flushParagraph();
- $closeList();
- if (empty($htmlParts)) {
- return '<p>Keine FAQ-Inhalte vorhanden.</p>';
- }
- return implode("\n", $htmlParts);
- }
- /**
- * Get all reservations
- */
- function getReservations() {
- $data = readJsonFile(RESERVATIONS_FILE);
- return isset($data['reservations']) ? $data['reservations'] : [];
- }
- /**
- * Get reservation by order number
- */
- function getReservationByOrderNumber($orderNumber) {
- $reservations = getReservations();
- foreach ($reservations as $reservation) {
- if (isset($reservation['id']) && $reservation['id'] === $orderNumber && !isReservationHidden($reservation)) {
- return $reservation;
- }
- }
- return null;
- }
- /**
- * Get remembered order IDs for the current browser profile.
- */
- function getRememberedOrderIds(): array {
- return readSignedOrderHistoryCookie();
- }
- /**
- * Remember a newly created order ID in browser history cookie.
- */
- function rememberOrderId(string $orderId): void {
- if (!isValidOrderHistoryOrderId($orderId)) {
- return;
- }
- $existingIds = getRememberedOrderIds();
- $updatedIds = [$orderId];
- foreach ($existingIds as $existingId) {
- if ($existingId !== $orderId) {
- $updatedIds[] = $existingId;
- }
- }
- $maxIds = getOrderHistoryMaxIds();
- if (count($updatedIds) > $maxIds) {
- $updatedIds = array_slice($updatedIds, 0, $maxIds);
- }
- writeSignedOrderHistoryCookie($updatedIds);
- }
- /**
- * Read and validate signed browser order history cookie.
- */
- function readSignedOrderHistoryCookie(): array {
- $cookieName = getOrderHistoryCookieName();
- if (!isset($_COOKIE[$cookieName]) || !is_string($_COOKIE[$cookieName])) {
- return [];
- }
- $secret = getOrderHistorySecret();
- if ($secret === '') {
- return [];
- }
- $cookieValue = $_COOKIE[$cookieName];
- $parts = explode('.', $cookieValue, 2);
- if (count($parts) !== 2) {
- return [];
- }
- $encodedPayload = $parts[0];
- $signature = $parts[1];
- if ($encodedPayload === '' || $signature === '') {
- return [];
- }
- $expectedSignature = hash_hmac('sha256', $encodedPayload, $secret);
- if (!hash_equals($expectedSignature, $signature)) {
- return [];
- }
- $payloadJson = base64UrlDecode($encodedPayload);
- if ($payloadJson === null) {
- return [];
- }
- $payload = json_decode($payloadJson, true);
- if (!is_array($payload)) {
- return [];
- }
- $version = isset($payload['v']) ? (int)$payload['v'] : 0;
- if ($version !== 1) {
- return [];
- }
- $ids = isset($payload['ids']) && is_array($payload['ids']) ? $payload['ids'] : [];
- return sanitizeOrderHistoryIds($ids);
- }
- /**
- * Write signed browser order history cookie.
- */
- function writeSignedOrderHistoryCookie(array $ids): void {
- if (headers_sent()) {
- return;
- }
- $secret = getOrderHistorySecret();
- if ($secret === '') {
- return;
- }
- $sanitizedIds = sanitizeOrderHistoryIds($ids);
- $payload = [
- 'v' => 1,
- 'ids' => $sanitizedIds,
- 'iat' => time()
- ];
- $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
- if ($payloadJson === false) {
- return;
- }
- $encodedPayload = base64UrlEncode($payloadJson);
- $signature = hash_hmac('sha256', $encodedPayload, $secret);
- $cookieValue = $encodedPayload . '.' . $signature;
- $expires = time() + (getOrderHistoryTtlDays() * 86400);
- $success = setcookie(getOrderHistoryCookieName(), $cookieValue, [
- 'expires' => $expires,
- 'path' => getOrderHistoryCookiePath(),
- 'secure' => isHttpsRequest(),
- 'httponly' => true,
- 'samesite' => 'Lax'
- ]);
- if ($success) {
- $_COOKIE[getOrderHistoryCookieName()] = $cookieValue;
- }
- }
- /**
- * Build a safe, deduplicated order history ID list.
- */
- function sanitizeOrderHistoryIds(array $ids): array {
- $result = [];
- $seen = [];
- $maxIds = getOrderHistoryMaxIds();
- foreach ($ids as $id) {
- if (!is_string($id) || !isValidOrderHistoryOrderId($id)) {
- continue;
- }
- if (isset($seen[$id])) {
- continue;
- }
- $seen[$id] = true;
- $result[] = $id;
- if (count($result) >= $maxIds) {
- break;
- }
- }
- return $result;
- }
- /**
- * Check if order ID matches configured pattern.
- */
- function isValidOrderHistoryOrderId($orderId): bool {
- if (!is_string($orderId) || $orderId === '') {
- return false;
- }
- $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
- $pattern = '/^' . preg_quote($prefix, '/') . '-\d{4}-\d+$/';
- return preg_match($pattern, $orderId) === 1;
- }
- /**
- * Base64url encode helper.
- */
- function base64UrlEncode(string $data): string {
- return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
- }
- /**
- * Base64url decode helper.
- */
- function base64UrlDecode(string $data): ?string {
- $remainder = strlen($data) % 4;
- if ($remainder > 0) {
- $data .= str_repeat('=', 4 - $remainder);
- }
- $decoded = base64_decode(strtr($data, '-_', '+/'), true);
- if ($decoded === false) {
- return null;
- }
- return $decoded;
- }
- /**
- * Get cookie name for order history.
- */
- function getOrderHistoryCookieName(): string {
- return defined('ORDER_HISTORY_COOKIE_NAME') ? ORDER_HISTORY_COOKIE_NAME : 'order_history';
- }
- /**
- * Get secret for order history cookie signing.
- */
- function getOrderHistorySecret(): string {
- return defined('ORDER_HISTORY_COOKIE_SECRET') ? (string) ORDER_HISTORY_COOKIE_SECRET : '';
- }
- /**
- * Get maximum number of remembered order IDs.
- */
- function getOrderHistoryMaxIds(): int {
- $maxIds = defined('ORDER_HISTORY_MAX_IDS') ? (int) ORDER_HISTORY_MAX_IDS : 10;
- return $maxIds > 0 ? $maxIds : 10;
- }
- /**
- * Get order history cookie retention days.
- */
- function getOrderHistoryTtlDays(): int {
- $ttlDays = defined('ORDER_HISTORY_COOKIE_TTL_DAYS') ? (int) ORDER_HISTORY_COOKIE_TTL_DAYS : 365;
- return $ttlDays > 0 ? $ttlDays : 365;
- }
- /**
- * Get cookie path from SITE_URL.
- */
- function getOrderHistoryCookiePath(): string {
- $siteUrl = defined('SITE_URL') ? trim((string) SITE_URL) : '';
- if (strpos($siteUrl, '://') !== false) {
- $parsedPath = parse_url($siteUrl, PHP_URL_PATH);
- $siteUrl = is_string($parsedPath) ? $parsedPath : '';
- }
- if ($siteUrl === '' || $siteUrl === '/') {
- return '/';
- }
- if ($siteUrl[0] !== '/') {
- $siteUrl = '/' . $siteUrl;
- }
- return rtrim($siteUrl, '/');
- }
- /**
- * Detect whether current request uses HTTPS.
- */
- function isHttpsRequest(): bool {
- if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
- return true;
- }
- if (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
- return true;
- }
- if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
- return true;
- }
- return false;
- }
- /**
- * Check if reservation is hidden (spam/deleted)
- */
- function isReservationHidden($reservation) {
- return isset($reservation['is_hidden']) && $reservation['is_hidden'] === true;
- }
- /**
- * Save reservations
- */
- function saveReservations($reservations) {
- $data = ['reservations' => $reservations];
- writeJsonFile(RESERVATIONS_FILE, $data);
- }
- /**
- * Generate order number
- * Pattern: PREFIX-YEAR-SEQ
- */
- function generateReservationId() {
- $reservations = getReservations();
- $year = date('Y');
- $prefix = defined('ORDER_PREFIX') ? ORDER_PREFIX : 'ORD';
- $max = 0;
- $pattern = '/^' . preg_quote($prefix, '/') . '-\\d{4}-(\\d+)$/';
- foreach ($reservations as $reservation) {
- if (!isset($reservation['id'])) {
- continue;
- }
- if (preg_match($pattern, $reservation['id'], $matches)) {
- $num = (int)$matches[1];
- if ($num > $max) {
- $max = $num;
- }
- }
- }
- $next = $max + 1;
- return sprintf('%s-%s-%03d', $prefix, $year, $next);
- }
- /**
- * Check if product has enough stock
- * For apparel: checks stock for specific size
- * For merch: checks general stock
- */
- function checkStock($productId, $quantity, $size = null) {
- $product = getProductById($productId);
- if (!$product) {
- return false;
- }
-
- // For apparel with sizes, check stock per size
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
- if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
- return false;
- }
- $sizeStock = isset($product['stock_by_size'][$size]) ? (int)$product['stock_by_size'][$size] : 0;
- return $sizeStock >= $quantity;
- }
-
- // For merch or apparel without size-specific stock, use general stock
- $stock = isset($product['stock']) ? (int)$product['stock'] : 0;
- return $stock >= $quantity;
- }
- /**
- * Get stock for a product (per size for apparel, general for merch)
- */
- function getStock($product, $size = null) {
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
- if (isset($product['stock_by_size']) && is_array($product['stock_by_size'])) {
- return isset($product['stock_by_size'][$size]) ? (int)$product['stock_by_size'][$size] : 0;
- }
- }
- return isset($product['stock']) ? (int)$product['stock'] : 0;
- }
- /**
- * Get total stock for a product (sum of all sizes for apparel)
- */
- function getTotalStock($product) {
- if ($product['category'] === 'apparel' && isset($product['stock_by_size']) && is_array($product['stock_by_size'])) {
- return array_sum($product['stock_by_size']);
- }
- return isset($product['stock']) ? (int)$product['stock'] : 0;
- }
- /**
- * Allocate stock for reservation
- */
- function allocateStock($productId, $quantity, $size = null) {
- $products = getProducts();
- foreach ($products as &$product) {
- if ($product['id'] == $productId) {
- // For apparel with sizes, allocate per size
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
- if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
- $product['stock_by_size'] = [];
- }
- if (!isset($product['stock_by_size'][$size])) {
- $product['stock_by_size'][$size] = 0;
- }
- $product['stock_by_size'][$size] -= $quantity;
- if ($product['stock_by_size'][$size] < 0) {
- $product['stock_by_size'][$size] = 0;
- }
- } else {
- // For merch or general stock
- if (!isset($product['stock'])) {
- $product['stock'] = 0;
- }
- $product['stock'] -= $quantity;
- if ($product['stock'] < 0) {
- $product['stock'] = 0;
- }
- }
- break;
- }
- }
- saveProducts($products);
- }
- /**
- * Release stock from reservation
- */
- function releaseStock($productId, $quantity, $size = null) {
- $products = getProducts();
- foreach ($products as &$product) {
- if ($product['id'] == $productId) {
- // For apparel with sizes, release per size
- if ($product['category'] === 'apparel' && !empty($product['sizes']) && $size !== null) {
- if (!isset($product['stock_by_size']) || !is_array($product['stock_by_size'])) {
- $product['stock_by_size'] = [];
- }
- if (!isset($product['stock_by_size'][$size])) {
- $product['stock_by_size'][$size] = 0;
- }
- $product['stock_by_size'][$size] += $quantity;
- } else {
- // For merch or general stock
- if (!isset($product['stock'])) {
- $product['stock'] = 0;
- }
- $product['stock'] += $quantity;
- }
- break;
- }
- }
- saveProducts($products);
- }
- /**
- * Create new reservation
- */
- function createReservation($customerName, $customerEmail, $items) {
- $reservations = getReservations();
-
- // Validate stock for all items
- foreach ($items as $item) {
- $size = isset($item['size']) ? $item['size'] : null;
- if (!checkStock($item['product_id'], $item['quantity'], $size)) {
- $product = getProductById($item['product_id']);
- $productName = $product ? $product['name'] : 'Produkt';
- $sizeInfo = $size ? " (Größe: $size)" : '';
- return ['success' => false, 'message' => "Nicht genügend Lagerbestand für: $productName$sizeInfo"];
- }
- }
-
- // Allocate stock
- foreach ($items as $item) {
- $size = isset($item['size']) ? $item['size'] : null;
- allocateStock($item['product_id'], $item['quantity'], $size);
- }
-
- // Create reservation
- $now = new DateTime();
- $expires = clone $now;
- $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
-
- $reservation = [
- 'id' => generateReservationId(),
- 'customer_name' => $customerName,
- 'customer_email' => $customerEmail,
- 'items' => $items,
- 'created' => $now->format('Y-m-d H:i:s'),
- 'expires' => $expires->format('Y-m-d H:i:s'),
- 'status' => 'open',
- 'picked_up' => false,
- 'type' => 'regular',
- 'is_hidden' => false
- ];
-
- $reservations[] = $reservation;
- saveReservations($reservations);
-
- // Send confirmation emails
- sendReservationEmails($reservation);
-
- return ['success' => true, 'reservation' => $reservation];
- }
- /**
- * Create new backorder reservation
- */
- function createBackorderReservation($customerName, $customerEmail, $items) {
- $reservations = getReservations();
-
- $now = new DateTime();
-
- $reservation = [
- 'id' => generateReservationId(),
- 'customer_name' => $customerName,
- 'customer_email' => $customerEmail,
- 'items' => $items,
- 'created' => $now->format('Y-m-d H:i:s'),
- 'expires' => '',
- 'status' => 'open',
- 'picked_up' => false,
- 'type' => 'backorder',
- 'backorder_status' => 'pending',
- 'is_hidden' => false
- ];
-
- $reservations[] = $reservation;
- saveReservations($reservations);
-
- // Send confirmation emails
- sendBackorderEmails($reservation);
-
- return ['success' => true, 'reservation' => $reservation];
- }
- /**
- * Mark reservation as picked up
- */
- function markReservationPickedUp($reservationId) {
- $reservations = getReservations();
- foreach ($reservations as &$reservation) {
- if ($reservation['id'] === $reservationId) {
- if (isReservationHidden($reservation)) {
- break;
- }
- $reservation['picked_up'] = true;
- $reservation['status'] = 'picked_up';
- break;
- }
- }
- saveReservations($reservations);
- }
- /**
- * Mark reservation/backorder as spam/deleted and hide it from non-admin views.
- * For open regular reservations we release stock, because the order is discarded.
- */
- function markReservationHidden($reservationId) {
- $reservations = getReservations();
- foreach ($reservations as &$reservation) {
- if ($reservation['id'] !== $reservationId) {
- continue;
- }
- if (isReservationHidden($reservation)) {
- return ['success' => false, 'message' => 'Bestellung ist bereits als Spam/Gelöscht markiert.'];
- }
- $isBackorder = isset($reservation['type']) && $reservation['type'] === 'backorder';
- if (!$isBackorder && isset($reservation['status']) && $reservation['status'] === 'open' && empty($reservation['picked_up'])) {
- foreach ($reservation['items'] as $item) {
- $size = isset($item['size']) ? $item['size'] : null;
- releaseStock($item['product_id'], $item['quantity'], $size);
- }
- $reservation['status'] = 'deleted';
- }
- $reservation['is_hidden'] = true;
- $reservation['hidden_at'] = date('Y-m-d H:i:s');
- $reservation['hidden_reason'] = 'spam_deleted';
- saveReservations($reservations);
- return ['success' => true];
- }
- return ['success' => false, 'message' => 'Bestellung nicht gefunden.'];
- }
- /**
- * Check and expire old reservations
- */
- function expireOldReservations() {
- $reservations = getReservations();
- $now = new DateTime();
- $changed = false;
-
- foreach ($reservations as &$reservation) {
- if (isReservationHidden($reservation)) {
- continue;
- }
- if ($reservation['status'] === 'open' && !$reservation['picked_up']) {
- if (isset($reservation['type']) && $reservation['type'] === 'backorder') {
- continue;
- }
- if (empty($reservation['expires'])) {
- continue;
- }
- $expires = new DateTime($reservation['expires']);
- if ($now > $expires) {
- $reservation['status'] = 'expired';
- // Release stock
- foreach ($reservation['items'] as $item) {
- $size = isset($item['size']) ? $item['size'] : null;
- releaseStock($item['product_id'], $item['quantity'], $size);
- }
- $changed = true;
- }
- }
- }
-
- if ($changed) {
- saveReservations($reservations);
- }
- }
- /**
- * Check if all items are in stock
- */
- function canFulfillReservationItems($items) {
- foreach ($items as $item) {
- $size = isset($item['size']) ? $item['size'] : null;
- if (!checkStock($item['product_id'], $item['quantity'], $size)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Mark backorder as available
- */
- function markBackorderAvailable($reservationId) {
- $reservations = getReservations();
- foreach ($reservations as &$reservation) {
- if ($reservation['id'] === $reservationId) {
- if (isReservationHidden($reservation)) {
- return ['success' => false, 'message' => 'Diese Vorbestellung ist als Spam/Gelöscht markiert.'];
- }
- if (!isset($reservation['type']) || $reservation['type'] !== 'backorder') {
- return ['success' => false, 'message' => 'Diese Vorbestellung wurde bereits in eine Bestellung umgewandelt.'];
- }
- if (isset($reservation['backorder_status']) && $reservation['backorder_status'] === 'notified') {
- return ['success' => false, 'message' => 'Diese Vorbestellung wurde bereits informiert.'];
- }
- if (!canFulfillReservationItems($reservation['items'])) {
- return ['success' => false, 'message' => 'Nicht alle Artikel sind verfügbar.'];
- }
-
- foreach ($reservation['items'] as $item) {
- $size = isset($item['size']) ? $item['size'] : null;
- allocateStock($item['product_id'], $item['quantity'], $size);
- }
- $now = new DateTime();
- $expires = clone $now;
- $expires->modify('+' . RESERVATION_EXPIRY_DAYS . ' days');
- $reservation['type'] = 'regular';
- $reservation['status'] = 'open';
- $reservation['picked_up'] = false;
- $reservation['expires'] = $expires->format('Y-m-d H:i:s');
- if (isset($reservation['backorder_status'])) {
- unset($reservation['backorder_status']);
- }
- saveReservations($reservations);
-
- sendBackorderAvailableEmail($reservation);
-
- return ['success' => true, 'reservation' => $reservation];
- }
- }
- return ['success' => false, 'message' => 'Vorbestellung nicht gefunden.'];
- }
- /**
- * Sanitize input
- */
- function sanitize($input) {
- return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
- }
- /**
- * Format price
- */
- function formatPrice($price) {
- return number_format($price, 2, ',', '.') . ' €';
- }
- /**
- * Format date
- */
- function formatDate($dateString) {
- $date = new DateTime($dateString);
- return $date->format('d.m.Y H:i');
- }
- /**
- * Send email
- */
- function sendEmail($to, $subject, $message, $isHtml = true) {
- $headers = [];
- $headers[] = 'From: ' . FROM_NAME . ' <' . FROM_EMAIL . '>';
- $headers[] = 'Reply-To: ' . FROM_EMAIL;
- $headers[] = 'X-Mailer: PHP/' . phpversion();
-
- if ($isHtml) {
- $headers[] = 'MIME-Version: 1.0';
- $headers[] = 'Content-type: text/html; charset=UTF-8';
- }
-
- return mail($to, $subject, $message, implode("\r\n", $headers));
- }
- /**
- * Get all admin notification recipients from admin accounts.
- * Falls back to ADMIN_EMAIL if no account email is configured.
- */
- function getAdminNotificationEmails() {
- $accounts = getAdminAccounts();
- $emails = [];
- foreach ($accounts as $account) {
- if (!isset($account['email'])) {
- continue;
- }
- $email = normalizeAdminEmail($account['email']);
- if (!isValidAdminEmail($email)) {
- continue;
- }
- $emails[] = $email;
- }
- if (empty($emails)) {
- $fallbackEmail = getDefaultAdminEmail();
- if ($fallbackEmail !== '') {
- $emails[] = $fallbackEmail;
- }
- }
- return array_values(array_unique($emails));
- }
- /**
- * Send admin notifications to all configured recipients.
- */
- function sendAdminNotificationEmails($subject, $message, $isHtml = true) {
- $emails = getAdminNotificationEmails();
- if (empty($emails)) {
- return false;
- }
- $sent = false;
- foreach ($emails as $email) {
- $result = sendEmail($email, $subject, $message, $isHtml);
- if ($result) {
- $sent = true;
- }
- }
- return $sent;
- }
- /**
- * Send reservation confirmation emails
- */
- function sendReservationEmails($reservation) {
- $products = getProducts();
-
- // Build items list
- $itemsHtml = '<ul style="list-style: none; margin: 0; padding: 0;">';
- foreach ($reservation['items'] as $item) {
- $product = getProductById($item['product_id']);
- if ($product) {
- $sizeInfo = '';
- if (isset($item['size']) && !empty($item['size'])) {
- $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
- }
- $itemsHtml .= '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;"><strong style="color: #cac300;">' . htmlspecialchars($product['name']) . '</strong>' . $sizeInfo . ' - Menge: <strong>' . (int) $item['quantity'] . '</strong></li>';
- }
- }
- $itemsHtml .= '</ul>';
-
- // Customer email
- $customerSubject = 'Ihre Reservierung bei ' . SITE_NAME;
- $customerMessage = '
- <html>
- <head>
- <meta charset="UTF-8">
- </head>
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
- <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
- <h2 style="color: #cac300; margin-top: 0;">Reservierung bestätigt</h2>
- <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
- <p>vielen Dank für Ihre Reservierung bei ' . SITE_NAME . '.</p>
-
- <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
- <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
- <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
- </div>
-
- <h3>Reservierungsdetails:</h3>
- <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
- <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
- <p><strong>Gültig bis:</strong> ' . formatDate($reservation['expires']) . '</p>
-
- <h3>Reservierte Artikel:</h3>
- <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
-
- <p><strong>Wichtig:</strong> Bitte nennen Sie diese Bestellnummer bei der Abholung. Die Reservierung ist bis zum ' . formatDate($reservation['expires']) . ' gültig.</p>
-
- <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
- </div>
- </body>
- </html>';
-
- sendEmail($reservation['customer_email'], $customerSubject, $customerMessage);
-
- // Admin email
- $adminSubject = 'Neue Reservierung: ' . $reservation['id'];
- $adminMessage = '
- <html>
- <head>
- <meta charset="UTF-8">
- </head>
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
- <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
- <h2 style="color: #cac300; margin-top: 0;">Neue Reservierung</h2>
- <p>Eine neue Reservierung wurde erstellt:</p>
-
- <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
- <h3 style="margin-top: 0;">Bestellnummer:</h3>
- <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
- </div>
-
- <h3>Kundendaten:</h3>
- <p><strong>Name:</strong> ' . htmlspecialchars($reservation['customer_name']) . '</p>
- <p><strong>E-Mail:</strong> ' . htmlspecialchars($reservation['customer_email']) . '</p>
-
- <h3>Reservierungsdetails:</h3>
- <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
- <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
- <p><strong>Gültig bis:</strong> ' . formatDate($reservation['expires']) . '</p>
-
- <h3>Reservierte Artikel:</h3>
- <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
- </div>
- </body>
- </html>';
-
- sendAdminNotificationEmails($adminSubject, $adminMessage);
- }
- /**
- * Send backorder confirmation emails
- */
- function sendBackorderEmails($reservation) {
- // Build items list
- $itemsHtml = '<ul style="list-style: none; margin: 0; padding: 0;">';
- foreach ($reservation['items'] as $item) {
- $product = getProductById($item['product_id']);
- if ($product) {
- $sizeInfo = '';
- if (isset($item['size']) && !empty($item['size'])) {
- $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
- }
- $itemsHtml .= '<li style="margin: 0 0 0.6rem 0; padding: 0.75rem 0.9rem; background: #28292a; border: 1px solid #4a5263; border-radius: 6px;"><strong style="color: #cac300;">' . htmlspecialchars($product['name']) . '</strong>' . $sizeInfo . ' - Menge: <strong>' . (int) $item['quantity'] . '</strong></li>';
- }
- }
- $itemsHtml .= '</ul>';
-
- // Customer email
- $customerSubject = 'Vorbestellung bei ' . SITE_NAME;
- $customerMessage = '
- <html>
- <head>
- <meta charset="UTF-8">
- </head>
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
- <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
- <h2 style="color: #cac300; margin-top: 0;">Vorbestellung bestätigt</h2>
- <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
- <p>vielen Dank für Ihre Vorbestellung bei ' . SITE_NAME . '.</p>
-
- <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
- <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
- <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
- </div>
-
- <h3>Vorbestellungsdetails:</h3>
- <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
- <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
-
- <h3>Vorbestellte Artikel:</h3>
- <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
-
- <div style="background: #28292a; border: 2px solid #cf2e2e; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
- <strong>Hinweis:</strong> Die Lieferzeiten sind nicht bekannt, da die Bestellung in Chargen erfolgt.
- </div>
-
- <p>Wir informieren Sie, sobald die komplette Vorbestellung zur Abholung bereit ist.</p>
-
- <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
- </div>
- </body>
- </html>';
-
- sendEmail($reservation['customer_email'], $customerSubject, $customerMessage);
-
- // Admin email
- $adminSubject = 'Neue Vorbestellung: ' . $reservation['id'];
- $adminMessage = '
- <html>
- <head>
- <meta charset="UTF-8">
- </head>
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
- <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
- <h2 style="color: #cac300; margin-top: 0;">Neue Vorbestellung</h2>
- <p>Eine neue Vorbestellung wurde erstellt:</p>
-
- <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px;">
- <h3 style="margin-top: 0;">Bestellnummer:</h3>
- <p style="margin: 0; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</p>
- </div>
-
- <h3>Kundendaten:</h3>
- <p><strong>Name:</strong> ' . htmlspecialchars($reservation['customer_name']) . '</p>
- <p><strong>E-Mail:</strong> ' . htmlspecialchars($reservation['customer_email']) . '</p>
-
- <h3>Vorbestellungsdetails:</h3>
- <p><strong>Bestellnummer:</strong> ' . htmlspecialchars($reservation['id']) . '</p>
- <p><strong>Erstellt am:</strong> ' . formatDate($reservation['created']) . '</p>
-
- <h3>Vorbestellte Artikel:</h3>
- <div style="background: #303745; border: 1px solid #4a5263; border-left: 4px solid #cac300; border-radius: 8px; padding: 1rem;">' . $itemsHtml . '</div>
- </div>
- </body>
- </html>';
-
- sendAdminNotificationEmails($adminSubject, $adminMessage);
- }
- /**
- * Send backorder availability email
- */
- function sendBackorderAvailableEmail($reservation) {
- $itemsHtml = '<ul>';
- foreach ($reservation['items'] as $item) {
- $product = getProductById($item['product_id']);
- if ($product) {
- $sizeInfo = '';
- if (isset($item['size']) && !empty($item['size'])) {
- $sizeInfo = ' - Größe: ' . htmlspecialchars($item['size']);
- }
- $itemsHtml .= '<li>' . htmlspecialchars($product['name']) . $sizeInfo . ' - Menge: ' . $item['quantity'] . '</li>';
- }
- }
- $itemsHtml .= '</ul>';
-
- $subject = 'Ihre Vorbestellung ist zur Abholung bereit';
- $message = '
- <html>
- <head>
- <meta charset="UTF-8">
- </head>
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #f5f7fb; background: #28292a; padding: 1.5rem;">
- <div style="max-width: 640px; margin: 0 auto; background: #2f3541; padding: 1.5rem 2rem; border-radius: 10px; border: 1px solid #3b4252;">
- <h2 style="color: #cac300; margin-top: 0;">Vorbestellung zur Abholung bereit</h2>
- <p>Sehr geehrte/r ' . htmlspecialchars($reservation['customer_name']) . ',</p>
- <p>Ihre komplette Vorbestellung ist jetzt zur Abholung bereit.</p>
-
- <div style="background: #28292a; border: 2px solid #cac300; padding: 1.5rem; margin: 1.5rem 0; border-radius: 8px; text-align: center;">
- <h3 style="margin-top: 0; color: #f5f7fb;">Ihre Bestellnummer:</h3>
- <h2 style="font-size: 2rem; letter-spacing: 0.2rem; color: #cac300; font-family: monospace;">' . htmlspecialchars($reservation['id']) . '</h2>
- </div>
-
- <h3>Bereitliegende Artikel:</h3>
- ' . $itemsHtml . '
-
- <p>Bitte nennen Sie die Bestellnummer bei der Abholung.</p>
-
- <p>Mit freundlichen Grüßen<br>' . SITE_NAME . '</p>
- </div>
- </body>
- </html>';
-
- sendEmail($reservation['customer_email'], $subject, $message);
- }
|