122 lines
3.0 KiB
PHP
122 lines
3.0 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Config-Wert holen: config('smtp.host') oder config() für das ganze Array.
|
||
|
|
*/
|
||
|
|
function config(?string $key = null, mixed $default = null): mixed
|
||
|
|
{
|
||
|
|
$value = $GLOBALS['__config'];
|
||
|
|
if ($key === null) {
|
||
|
|
return $value;
|
||
|
|
}
|
||
|
|
foreach (explode('.', $key) as $part) {
|
||
|
|
if (!is_array($value) || !array_key_exists($part, $value)) {
|
||
|
|
return $default;
|
||
|
|
}
|
||
|
|
$value = $value[$part];
|
||
|
|
}
|
||
|
|
return $value;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* HTML-Escaping — für JEDE dynamische Ausgabe verwenden.
|
||
|
|
*/
|
||
|
|
function e(string|int|float|null $value): string
|
||
|
|
{
|
||
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Interner Link aus Slug: url('fussball') → '/fussball', url('') → '/'.
|
||
|
|
*/
|
||
|
|
function url(string $slug = ''): string
|
||
|
|
{
|
||
|
|
return '/' . trim($slug, '/');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Absolute URL für Canonical, OG und Sitemap.
|
||
|
|
*/
|
||
|
|
function abs_url(string $slug = ''): string
|
||
|
|
{
|
||
|
|
return rtrim((string) config('base_url'), '/') . url($slug);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Asset-Pfad mit Cache-Busting über filemtime: asset('css/tokens.css').
|
||
|
|
*/
|
||
|
|
function asset(string $path): string
|
||
|
|
{
|
||
|
|
$path = ltrim($path, '/');
|
||
|
|
$file = PUBLIC_PATH . '/assets/' . $path;
|
||
|
|
$version = is_file($file) ? (string) filemtime($file) : '0';
|
||
|
|
return '/assets/' . $path . '?v=' . $version;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* JSON-Datendatei laden (data/<name>.json) mit Request-weitem Cache.
|
||
|
|
* Wirft bei kaputtem JSON — Datenfehler sollen laut scheitern, nicht leise.
|
||
|
|
*/
|
||
|
|
function json_load(string $name): array
|
||
|
|
{
|
||
|
|
static $cache = [];
|
||
|
|
if (!array_key_exists($name, $cache)) {
|
||
|
|
$file = DATA_PATH . '/' . $name . '.json';
|
||
|
|
if (!is_file($file)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
$cache[$name] = json_decode((string) file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
|
||
|
|
}
|
||
|
|
return $cache[$name];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Komponente rendern: component('hero', ['title' => …]).
|
||
|
|
* Props werden als lokale Variablen extrahiert; Komponenten sind dumme Includes.
|
||
|
|
*/
|
||
|
|
function component(string $name, array $props = []): void
|
||
|
|
{
|
||
|
|
extract($props, EXTR_SKIP);
|
||
|
|
require APP_PATH . '/components/' . $name . '.php';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Page-Datei ausführen: sie setzt $meta und emittiert ihren Body.
|
||
|
|
* Rückgabe: [$meta, $html].
|
||
|
|
*/
|
||
|
|
function render_page(string $file): array
|
||
|
|
{
|
||
|
|
$meta = [];
|
||
|
|
ob_start();
|
||
|
|
require $file;
|
||
|
|
return [$meta, (string) ob_get_clean()];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Signierten Zeitstempel für die Formular-Time-Trap erzeugen.
|
||
|
|
*/
|
||
|
|
function form_token(): string
|
||
|
|
{
|
||
|
|
$ts = (string) time();
|
||
|
|
return $ts . '.' . hash_hmac('sha256', $ts, (string) config('app_secret'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Time-Trap prüfen: Signatur gültig, älter als $min Sekunden, jünger als $max.
|
||
|
|
*/
|
||
|
|
function form_token_valid(string $token, int $min = 3, int $max = 7200): bool
|
||
|
|
{
|
||
|
|
$parts = explode('.', $token);
|
||
|
|
if (count($parts) !== 2) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
[$ts, $sig] = $parts;
|
||
|
|
if (!hash_equals(hash_hmac('sha256', $ts, (string) config('app_secret')), $sig)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
$age = time() - (int) $ts;
|
||
|
|
return $age >= $min && $age <= $max;
|
||
|
|
}
|