Files
netwatch/app/lib/helpers.php
fixclean c8fb5b140c feat: improve dashboard and connection workflows
- add connection delete endpoint and update connection list handling

- expand dashboard visualization behavior

- update helpers/header and project TODO tracking
2026-02-18 09:23:11 +01:00

380 lines
8.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 (400499)
* @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
* ========================= */
/**
* Formatiert Datum/Uhrzeit robust oder gibt Fallback zurueck.
*
* @param string|null $value
* @param string $format
* @param string $fallback
* @return string
*/
function formatDateTime(?string $value, string $format = 'd.m.Y H:i', string $fallback = '-'): string
{
if ($value === null || trim($value) === '') {
return $fallback;
}
$timestamp = strtotime($value);
if ($timestamp === false) {
return $fallback;
}
return date($format, $timestamp);
}
/**
* Formatiert Byte-Werte in menschenlesbare Einheit.
*
* @param int|float $bytes
* @param int $precision
* @return string
*/
function formatBytes($bytes, int $precision = 2): string
{
$value = (float)$bytes;
if ($value <= 0) {
return '0 B';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$power = min((int)floor(log($value, 1024)), count($units) - 1);
$scaled = $value / (1024 ** $power);
return number_format($scaled, $precision, '.', '') . ' ' . $units[$power];
}
/**
* Erzeugt eine UUID v4.
*
* @return string
* @throws Exception
*/
function generateUuidV4(): string
{
$bytes = random_bytes(16);
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
$hex = bin2hex($bytes);
return sprintf(
'%s-%s-%s-%s-%s',
substr($hex, 0, 8),
substr($hex, 8, 4),
substr($hex, 12, 4),
substr($hex, 16, 4),
substr($hex, 20, 12)
);
}
/**
* Klemmt eine SVG-Koordinate auf gueltigen Bereich.
*
* @param float $value
* @param float $min
* @param float $max
* @param int $precision
* @return float
*/
function normalizeSvgCoordinate(float $value, float $min, float $max, int $precision = 2): float
{
$normalized = max($min, min($max, $value));
return round($normalized, $precision);
}