305 lines
7.2 KiB
PHP
305 lines
7.2 KiB
PHP
<?php
|
||
/**
|
||
* app/lib/helpers.php
|
||
*
|
||
* Zentrale Hilfsfunktionen
|
||
* - Output-Escaping
|
||
* - Redirects
|
||
* - Flash-Messages
|
||
* - Request-Helper
|
||
* - Allgemeine Utilities
|
||
*
|
||
* KEINE Business-Logik
|
||
*/
|
||
|
||
/**
|
||
* Sitzungs-Keys
|
||
*/
|
||
const FLASH_SESSION_KEY = 'flash_messages';
|
||
|
||
/* =========================
|
||
* Output / Sicherheit
|
||
* ========================= */
|
||
|
||
/**
|
||
* HTML-Escaping
|
||
*
|
||
* @param string|null $value
|
||
* @return string
|
||
*/
|
||
function e(?string $value): string
|
||
{
|
||
if ($value === null) {
|
||
return '';
|
||
}
|
||
|
||
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
/* =========================
|
||
* Redirects
|
||
* ========================= */
|
||
|
||
/**
|
||
* HTTP Redirect
|
||
*
|
||
* @param string $url
|
||
* @param int $code
|
||
*/
|
||
function redirect(string $url, int $code = 302): void
|
||
{
|
||
if (!headers_sent()) {
|
||
header('Location: ' . $url, true, $code);
|
||
}
|
||
|
||
exit;
|
||
}
|
||
|
||
/* =========================
|
||
* Flash Messages
|
||
* ========================= */
|
||
|
||
/**
|
||
* Flash-Message setzen
|
||
*
|
||
* @param string $type (success, error, info)
|
||
* @param string $message
|
||
*/
|
||
function flash(string $type, string $message): void
|
||
{
|
||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||
session_start();
|
||
}
|
||
|
||
if (!isset($_SESSION[FLASH_SESSION_KEY])) {
|
||
$_SESSION[FLASH_SESSION_KEY] = [];
|
||
}
|
||
|
||
$_SESSION[FLASH_SESSION_KEY][] = ['type' => $type, 'message' => $message];
|
||
}
|
||
|
||
/**
|
||
* Flash-Messages abrufen & löschen
|
||
*
|
||
* @return array
|
||
*/
|
||
function getFlashes(): array
|
||
{
|
||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||
session_start();
|
||
}
|
||
|
||
$messages = $_SESSION[FLASH_SESSION_KEY] ?? [];
|
||
unset($_SESSION[FLASH_SESSION_KEY]);
|
||
|
||
return $messages;
|
||
}
|
||
|
||
/* =========================
|
||
* Request Helper
|
||
* ========================= */
|
||
|
||
/**
|
||
* POST-Wert holen
|
||
*
|
||
* @param string $key
|
||
* @param mixed $default
|
||
* @return mixed
|
||
*/
|
||
function post(string $key, $default = null)
|
||
{
|
||
return $_POST[$key] ?? $default;
|
||
}
|
||
|
||
/**
|
||
* GET-Wert holen
|
||
*
|
||
* @param string $key
|
||
* @param mixed $default
|
||
* @return mixed
|
||
*/
|
||
function get(string $key, $default = null)
|
||
{
|
||
return $_GET[$key] ?? $default;
|
||
}
|
||
|
||
/**
|
||
* Prüfen, ob Request POST ist
|
||
*
|
||
* @return bool
|
||
*/
|
||
function isPost(): bool
|
||
{
|
||
return ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
|
||
}
|
||
|
||
/* =========================
|
||
* Validierung
|
||
* ========================= */
|
||
|
||
/**
|
||
* Prüft auf leeren Wert
|
||
*
|
||
* @param mixed $value
|
||
* @return bool
|
||
*/
|
||
function isEmpty($value): bool
|
||
{
|
||
if (is_string($value)) {
|
||
return trim($value) === '';
|
||
}
|
||
|
||
return empty($value);
|
||
}
|
||
|
||
/* =========================
|
||
* Pfade / URLs
|
||
* ========================= */
|
||
|
||
/**
|
||
* Baut eine URL zur App
|
||
*
|
||
* @param string $path
|
||
* @return string
|
||
*/
|
||
function url(string $path = ''): string
|
||
{
|
||
if ($path === '') {
|
||
$path = '/';
|
||
}
|
||
|
||
if (preg_match('~^([a-z]+:)?//~i', $path)) {
|
||
return $path;
|
||
}
|
||
|
||
$script = $_SERVER['SCRIPT_NAME'] ?? '';
|
||
$baseDir = rtrim(strtr(dirname($script), '\\\\', '/'), '/');
|
||
|
||
if ($baseDir === '.' || $baseDir === '\\\\') {
|
||
$baseDir = '';
|
||
}
|
||
|
||
$segment = ltrim($path, '/');
|
||
$prefix = $baseDir === '' ? '' : $baseDir;
|
||
|
||
if ($segment === '') {
|
||
return $prefix === '' ? '/' : $prefix;
|
||
}
|
||
|
||
return ($prefix === '' ? '' : $prefix) . '/' . $segment;
|
||
}
|
||
|
||
/* =========================
|
||
* Debug / Entwicklung
|
||
* ========================= */
|
||
|
||
/**
|
||
* Dump & Die (nur Dev)
|
||
*
|
||
* @param mixed $value
|
||
*/
|
||
function dd($value): void
|
||
{
|
||
echo '<pre>';
|
||
var_dump($value);
|
||
echo '</pre>';
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Zeigt eine sauber gestaltete Fehlerseite für 40x-Status-Codes mit Erklärung.
|
||
*
|
||
* @param int $statusCode Client-Error-Status (400–499)
|
||
* @param string $explanation Optionale Zusatzinfo zur Problemursache
|
||
* @param string[] $tips Handlungsvorschläge für Benutzer (optional)
|
||
*/
|
||
function renderClientError(int $statusCode, string $explanation = '', array $tips = []): void
|
||
{
|
||
if ($statusCode < 400 || $statusCode >= 500) {
|
||
$statusCode = 400;
|
||
}
|
||
|
||
$reasons = [
|
||
400 => 'Ungültige Anfrage',
|
||
401 => 'Nicht authentifiziert',
|
||
403 => 'Zugriff verweigert',
|
||
404 => 'Seite nicht gefunden',
|
||
405 => 'Methode nicht erlaubt',
|
||
408 => 'Anfrage abgelaufen',
|
||
429 => 'Zu viele Anfragen',
|
||
];
|
||
|
||
$reason = $reasons[$statusCode] ?? 'Clientseitiger Fehler';
|
||
$defaultExplanation = match ($statusCode) {
|
||
400 => 'Die Anfrage konnte aufgrund fehlender oder falscher Daten nicht verstanden werden.',
|
||
401 => 'Bitte melden Sie sich an oder verwenden gültige Zugangsdaten.',
|
||
403 => 'Sie besitzen keinen Zugriff auf diesen Bereich.',
|
||
404 => 'Die angeforderte Ressource existiert nicht oder wurde verschoben.',
|
||
405 => 'Diese Aktion ist auf dem Server nicht erlaubt.',
|
||
408 => 'Die Anfrage hat zu lange gedauert; bitte erneut versuchen.',
|
||
429 => 'Sie senden zu viele Anfragen in kurzer Zeit.',
|
||
default => 'Die Anfrage kann nicht verarbeitet werden; überprüfen Sie die Eingaben.',
|
||
};
|
||
|
||
$message = $explanation !== '' ? $explanation : $defaultExplanation;
|
||
|
||
http_response_code($statusCode);
|
||
|
||
$css = <<<CSS
|
||
body { font-family: 'Segoe UI', system-ui, sans-serif; margin: 0; background: #0b1220; color: #e0e7ff; }
|
||
.page { min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 2rem; }
|
||
.card { max-width: 640px; background: linear-gradient(145deg, #16213d, #0d0f1f); border: 1px solid rgba(224,231,255,0.2); border-radius: 1rem; box-shadow: 0 20px 45px rgba(0,0,0,0.45); padding: 2rem; }
|
||
h1 { margin: 0 0 0.5rem; font-size: clamp(2.5rem, 3vw, 3.5rem); }
|
||
p { margin: 0 0 1rem; line-height: 1.6; color: rgba(224,231,255,0.9); }
|
||
.badge { display: inline-flex; align-items: center; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.85rem; background: rgba(255,255,255,0.08); color: #a5b4fc; margin-bottom: 1rem; }
|
||
ul { margin: 1rem 0 0; padding-left: 1.25rem; color: rgba(224,231,255,0.85); }
|
||
a { color: #7dd3fc; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
CSS;
|
||
|
||
$rendered = strtr(<<<'HTML'
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Fehler {{code}}</title>
|
||
<style>{{css}}</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<section class="card">
|
||
<div class="badge">{{status}}</div>
|
||
<h1>{{code}} · {{reason}}</h1>
|
||
<p>{{message}}</p>
|
||
{{tips}}
|
||
<p>Zurück zur Startseite: <a href="/">Dashboard</a></p>
|
||
</section>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
HTML, [
|
||
'{{code}}' => e((string)$statusCode),
|
||
'{{reason}}' => e($reason),
|
||
'{{status}}' => e(sprintf('%d Fehler', $statusCode)),
|
||
'{{message}}' => e($message),
|
||
'{{css}}' => $css,
|
||
'{{tips}}' => count($tips) === 0
|
||
? ''
|
||
: '<ul>' . implode('', array_map(fn($tip) => '<li>' . e($tip) . '</li>', $tips)) . '</ul>',
|
||
]);
|
||
|
||
echo $rendered;
|
||
exit;
|
||
}
|
||
|
||
/* =========================
|
||
* Sonstiges
|
||
* ========================= */
|
||
|
||
// TODO: Weitere Helfer nach Bedarf
|
||
// - Datum formatieren
|
||
// - Bytes → MB
|
||
// - UUID erzeugen
|
||
// - SVG-Koordinaten normalisieren
|