exec("CREATE TABLE IF NOT EXISTS recipes ( slug VARCHAR(255) PRIMARY KEY, data JSONB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $stmt = $pdo->query("SELECT COUNT(*) FROM recipes"); if ($stmt && $stmt->fetchColumn() == 0) { $path = __DIR__ . '/data/recipes.json'; if (file_exists($path)) { $json = file_get_contents($path); $data = json_decode($json, true); if (is_array($data) && count($data) > 0) { $insert = $pdo->prepare("INSERT INTO recipes (slug, data) VALUES (:slug, :data)"); foreach ($data as $recipe) { if (isset($recipe['slug'])) { $insert->execute([ 'slug' => $recipe['slug'], 'data' => json_encode($recipe, JSON_UNESCAPED_UNICODE) ]); } } } } } } catch (\Exception $e) { error_log("DB Init Error: " . $e->getMessage()); } } function load_recipes_local(): array { $pdo = get_db_connection(); $data = []; if (!$pdo) { $path = __DIR__ . '/data/recipes.json'; if (file_exists($path)) { $json = file_get_contents($path); $data = json_decode($json, true) ?: []; } } else { static $initialized = false; if (!$initialized) { init_db(); $initialized = true; } try { $stmt = $pdo->query("SELECT data FROM recipes"); if ($stmt) { while ($row = $stmt->fetch()) { $recipe = json_decode($row['data'], true); if (is_array($recipe)) { $data[] = $recipe; } } } } catch (\Exception $e) { error_log("Failed to load recipes from DB: " . $e->getMessage()); } } foreach ($data as &$recipe) { if (!empty($recipe['hero']) && is_string($recipe['hero'])) { $recipe['hero'] = normalize_asset_path($recipe['hero']); } } unset($recipe); return $data; } function load_recipes(): array { return load_recipes_local(); } function load_recipe_by_slug(string $slug): ?array { return find_recipe_by_slug(load_recipes_local(), $slug); } function save_recipes(array $recipes): bool { $pdo = get_db_connection(); $path = __DIR__ . '/data/recipes.json'; if (!$pdo) { $json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return (bool) file_put_contents($path, $json); } try { $pdo->beginTransaction(); $slugs = []; $insert = $pdo->prepare("INSERT INTO recipes (slug, data) VALUES (:slug, :data) ON CONFLICT (slug) DO UPDATE SET data = EXCLUDED.data, updated_at = CURRENT_TIMESTAMP"); foreach ($recipes as $recipe) { if (isset($recipe['slug'])) { $slugs[] = $recipe['slug']; $insert->execute([ 'slug' => $recipe['slug'], 'data' => json_encode($recipe, JSON_UNESCAPED_UNICODE) ]); } } if (!empty($slugs)) { $placeholders = implode(',', array_fill(0, count($slugs), '?')); $delete = $pdo->prepare("DELETE FROM recipes WHERE slug NOT IN ($placeholders)"); $delete->execute($slugs); } else { $pdo->exec("DELETE FROM recipes"); } $pdo->commit(); // Also update local JSON as backup $json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); file_put_contents($path, $json); return true; } catch (Exception $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } error_log("Failed to save recipes to DB: " . $e->getMessage()); return false; } } /** * Default settings for imprint and privacy pages. */ function default_site_settings(): array { $today = date('Y-m-d'); return [ 'imprint' => [ 'de' => [ 'owner_name' => 'FlixCooks (bitte anpassen)', 'address' => 'Straße Hausnummer, PLZ Ort, Österreich', 'email' => 'contact@flixcooks.at', 'phone' => '+43 660 0000000', 'legal_form' => 'Kleines Impressum; kein Firmenbucheintrag/UID hinterlegt.', 'business_purpose' => 'Foodblog & Rezeptmarketing.', 'wko_membership' => '', 'authority' => '', 'uid' => '', 'odr' => 'https://ec.europa.eu/consumers/odr', 'last_updated' => $today, ], 'en' => [ 'owner_name' => 'FlixCooks (please update)', 'address' => 'Street number, ZIP City, Austria', 'email' => 'contact@flixcooks.at', 'phone' => '+43 660 0000000', 'legal_form' => 'Small website notice; no commercial register/UID provided.', 'business_purpose' => 'Food blog & recipe marketing.', 'wko_membership' => '', 'authority' => '', 'uid' => '', 'odr' => 'https://consumer-redress.ec.europa.eu/', 'last_updated' => $today, ], ], 'privacy' => [ 'de' => [ 'controller' => 'FlixCooks (bitte anpassen)', 'contact_email' => 'contact@flixcooks.at', 'contact_phone' => '+43 660 0000000', 'address' => 'Straße Hausnummer, PLZ Ort, Österreich', 'hosting_provider' => 'Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Deutschland', 'log_retention_days' => 30, 'cookie_statement' => 'Keine Tracking- oder Marketing-Cookies; nur technisch notwendige.', 'purposes' => 'Betrieb und Bereitstellung der Website, Beantwortung von Kontaktanfragen.', 'last_updated' => $today, ], 'en' => [ 'controller' => 'FlixCooks (please update)', 'contact_email' => 'contact@flixcooks.at', 'contact_phone' => '+43 660 0000000', 'address' => 'Street number, ZIP City, Austria', 'hosting_provider' => 'Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen, Germany', 'log_retention_days' => 30, 'cookie_statement' => 'No tracking or marketing cookies; only technically necessary cookies.', 'purposes' => 'Operating and providing the website, responding to contact requests.', 'last_updated' => $today, ], ], ]; } /** * Load site-wide settings for legal pages, merging with defaults. */ function load_site_settings(): array { $defaults = default_site_settings(); $path = __DIR__ . '/data/site.json'; if (!file_exists($path)) { return $defaults; } $raw = file_get_contents($path); $data = json_decode($raw, true); if (!is_array($data)) { return $defaults; } return array_replace_recursive($defaults, $data); } /** * Persist site-wide settings for legal pages. */ function save_site_settings(array $settings): bool { $path = __DIR__ . '/data/site.json'; $json = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return (bool) file_put_contents($path, $json); } function slugify(string $text): string { $text = strtolower($text); $text = preg_replace('~[^a-z0-9]+~', '-', $text); $text = trim($text, '-'); return $text ?: uniqid('recipe-'); } function find_recipe_by_slug(array $recipes, string $slug): ?array { foreach ($recipes as $recipe) { if (($recipe['slug'] ?? '') === $slug) { return $recipe; } } return null; } function localize_recipe(array $recipe, string $lang): array { $localized = $recipe; $i18n = $recipe['i18n'][$lang] ?? ($recipe['i18n']['en'] ?? []); foreach ($i18n as $key => $value) { $localized[$key] = $value; } return $localized; } function localize_recipes(array $recipes, string $lang): array { return array_map(function ($recipe) use ($lang) { return localize_recipe($recipe, $lang); }, $recipes); } function filter_recipes(array $recipes, ?string $query = null, ?string $tag = null, string $lang = 'en'): array { $query = $query ? strtolower($query) : null; $tag = $tag ? strtolower($tag) : null; $localized = localize_recipes($recipes, $lang); return array_values(array_filter($localized, function ($recipe) use ($query, $tag) { $matchesQuery = true; if ($query) { $haystack = strtolower(($recipe['title'] ?? '') . ' ' . ($recipe['description'] ?? '')); $matchesQuery = strpos($haystack, $query) !== false; } $matchesTag = true; if ($tag) { $tags = array_map('strtolower', $recipe['tags'] ?? []); $matchesTag = in_array($tag, $tags, true); } return $matchesQuery && $matchesTag; })); } function format_minutes(int $minutes): string { if ($minutes < 60) { return $minutes . ' min'; } $hours = intdiv($minutes, 60); $mins = $minutes % 60; return $hours . ' hr' . ($hours > 1 ? 's ' : ' ') . ($mins ? $mins . ' min' : ''); } function e(?string $value): string { return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); } function normalize_asset_path(string $path): string { if (!str_starts_with($path, '/assets/')) { return $path; } $absolute = __DIR__ . $path; if (file_exists($absolute)) { return $path; } $dir = dirname($absolute); $target = basename($absolute); if (!is_dir($dir)) { return $path; } foreach (scandir($dir) ?: [] as $entry) { if (strcasecmp($entry, $target) === 0) { return rtrim(dirname($path), '/') . '/' . $entry; } } return $path; } /** * Build a URL to the current path with a specific language parameter, preserving other query params. */ function lang_url(string $lang): string { $params = $_GET; $params['lang'] = $lang; $path = strtok($_SERVER['REQUEST_URI'], '?') ?: '/'; return $path . '?' . http_build_query($params); } // Fallback polyfills for environments without mbstring extension if (!function_exists('mb_substr')) { function mb_substr(string $string, int $start, ?int $length = null, ?string $encoding = null): string { return substr($string, $start, $length ?? strlen($string)); } } if (!function_exists('mb_strlen')) { function mb_strlen(string $string, ?string $encoding = null): int { return strlen($string); } }