2026-03-22 22:16:44 +01:00
|
|
|
<?php
|
|
|
|
|
// Shared helper functions for the FlixCooks food blog.
|
|
|
|
|
|
2026-05-21 14:14:10 +02:00
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
|
|
|
session_start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
|
|
2026-03-22 22:16:44 +01:00
|
|
|
function load_recipes(): array {
|
|
|
|
|
$path = __DIR__ . '/data/recipes.json';
|
|
|
|
|
if (!file_exists($path)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
$json = file_get_contents($path);
|
|
|
|
|
$data = json_decode($json, true);
|
|
|
|
|
if (!is_array($data)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($data as &$recipe) {
|
|
|
|
|
if (!empty($recipe['hero']) && is_string($recipe['hero'])) {
|
|
|
|
|
$recipe['hero'] = normalize_asset_path($recipe['hero']);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
unset($recipe);
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function save_recipes(array $recipes): bool {
|
|
|
|
|
$path = __DIR__ . '/data/recipes.json';
|
|
|
|
|
$json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
|
|
|
return (bool) file_put_contents($path, $json);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 16:26:45 +01:00
|
|
|
/**
|
|
|
|
|
* 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' => '',
|
2026-03-23 16:34:39 +01:00
|
|
|
'odr' => 'https://consumer-redress.ec.europa.eu/',
|
2026-03-23 16:26:45 +01:00
|
|
|
'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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 22:16:44 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-21 14:02:09 +02:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|