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-05-22 19:07:03 +02:00
function init_db () {
$pdo = get_db_connection ();
if ( ! $pdo ) return ;
try {
$pdo -> 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 ( PDOException $e ) {
error_log ( "DB Init Error: " . $e -> getMessage ());
2026-05-22 16:16:34 +02:00
}
}
function load_recipes_local () : array {
2026-05-22 19:07:03 +02:00
$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 ( PDOException $e ) {
error_log ( "Failed to load recipes from DB: " . $e -> getMessage ());
}
2026-03-22 22:16:44 +01:00
}
foreach ( $data as & $recipe ) {
if ( ! empty ( $recipe [ 'hero' ]) && is_string ( $recipe [ 'hero' ])) {
$recipe [ 'hero' ] = normalize_asset_path ( $recipe [ 'hero' ]);
}
}
unset ( $recipe );
return $data ;
}
2026-05-22 16:16:34 +02:00
function load_recipes () : array {
2026-05-22 19:07:03 +02:00
return load_recipes_local ();
2026-05-22 16:16:34 +02:00
}
function load_recipe_by_slug ( string $slug ) : ? array {
2026-05-22 19:07:03 +02:00
return find_recipe_by_slug ( load_recipes_local (), $slug );
2026-05-22 16:16:34 +02:00
}
2026-03-22 22:16:44 +01:00
function save_recipes ( array $recipes ) : bool {
2026-05-22 19:07:03 +02:00
$pdo = get_db_connection ();
2026-03-22 22:16:44 +01:00
$path = __DIR__ . '/data/recipes.json' ;
2026-05-22 19:07:03 +02:00
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 ;
}
2026-03-22 22:16:44 +01:00
}
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 );
}
}