i18n()->get($key, $default, $vars); } function current_client_id(): string { $session = app()->session(); $session->start(); return $session->ensureClientId(); } function user_theme(): string { $pdo = app()->basePdo(); if (!$pdo) { return 'light'; } $clientId = current_client_id(); $stmt = $pdo->prepare("SELECT theme FROM nexus_user_prefs WHERE client_id = :id LIMIT 1"); $stmt->execute(['id' => $clientId]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); $theme = is_array($row) ? (string)($row['theme'] ?? '') : ''; return match ($theme) { 'dark', 'night' => 'night', 'light', 'day', '' => 'day', default => $theme, }; } function set_user_theme(string $theme): void { $pdo = app()->basePdo(); if (!$pdo) { return; } $clientId = current_client_id(); $stmt = $pdo->prepare( "INSERT INTO nexus_user_prefs (client_id, theme, updated_at) VALUES (:id, :theme, CURRENT_TIMESTAMP) ON CONFLICT(client_id) DO UPDATE SET theme = excluded.theme, updated_at = CURRENT_TIMESTAMP" ); $stmt->execute(['id' => $clientId, 'theme' => $theme]); } function current_module_name(): ?string { $path = app()->request()->path(); if (preg_match('~^/module/([a-zA-Z0-9_-]+)~', $path, $m)) { return $m[1]; } return null; } function auth_enabled(): bool { return app()->auth()->isEnabled(); } function auth_user(): ?array { return app()->auth()->user(); } function auth_display_name(): string { $user = auth_user(); if (!$user) { return ''; } $name = trim((string)($user['name'] ?? '')); if ($name !== '') { return $name; } $email = trim((string)($user['email'] ?? '')); return $email; } function auth_initials(): string { $name = auth_display_name(); if ($name === '') { return 'U'; } $parts = preg_split('/\s+/', $name) ?: []; $letters = ''; foreach ($parts as $p) { $p = trim($p); if ($p !== '') { $letters .= mb_strtoupper(mb_substr($p, 0, 1)); } if (mb_strlen($letters) >= 2) { break; } } return $letters !== '' ? $letters : 'U'; } function auth_groups(): array { $user = auth_user(); return is_array($user['groups'] ?? null) ? $user['groups'] : []; } function parse_group_list(string $value): array { $parts = preg_split('/[,\s]+/', $value) ?: []; $out = []; foreach ($parts as $p) { $p = trim($p); if ($p !== '') { $out[] = $p; } } return $out; } function auth_is_admin(): bool { $config = app()->config(); $groups = auth_groups(); $allowed = parse_group_list($config->oidcAdminGroup); foreach ($allowed as $g) { if (in_array($g, $groups, true)) { return true; } } return false; } function auth_is_user(): bool { $config = app()->config(); $groups = auth_groups(); $admins = parse_group_list($config->oidcAdminGroup); foreach ($admins as $g) { if (in_array($g, $groups, true)) { return true; } } $users = parse_group_list($config->oidcUserGroup); foreach ($users as $g) { if (in_array($g, $groups, true)) { return true; } } return false; } function require_auth(): void { if (!auth_enabled()) { return; } if (auth_user()) { return; } redirect('/auth/login'); } function require_admin(): void { require_auth(); if (!auth_is_admin()) { http_response_code(403); echo '
Keine Berechtigung.
'; exit; } } function modules(): \App\ModuleManager { return app()->modules(); } function module_fn(string $module, string $name, mixed ...$args): mixed { return modules()->call($module, $name, ...$args); } /** * Lädt ein Template-Partial. * * @param string $name Dateiname ohne .php * @param string $folder Unterordner in /partials/ * @param array $data Daten, die im Template verfügbar sein sollen */ function tpl(string $name, string $folder = 'landingpages', array $data = []): void { $base = __DIR__ . '/../../partials/'; foreach ([$name, $folder] as $value) { if (preg_match('/[^a-zA-Z0-9_\-]/', $value)) { echo ""; return; } } $paths = []; if ($folder === 'landingpages') { $paths[] = $base . 'landingpages/' . $name . '.php'; } else { $paths[] = $base . 'structure/' . $name . '.php'; } $path = null; foreach ($paths as $candidate) { if (file_exists($candidate)) { $path = $candidate; break; } } if ($path === null) { echo ""; return; } extract($data); require $path; } function module_tpl(string $module, string $name, array $data = []): void { $base = __DIR__ . '/../../modules/' . $module . '/partials/'; foreach ([$module, $name] as $value) { if (preg_match('/[^a-zA-Z0-9_\-]/', $value)) { echo ""; return; } } $path = $base . $name . '.php'; if (file_exists($path)) { extract($data); require $path; } else { echo ""; } } function module_design(string $module): array { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { return []; } static $cache = []; if (array_key_exists($module, $cache)) { return $cache[$module]; } $path = __DIR__ . '/../../modules/' . $module . '/design.json'; if (!is_file($path)) { $cache[$module] = []; return $cache[$module]; } $raw = file_get_contents($path); $decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null; $cache[$module] = is_array($decoded) ? $decoded : []; return $cache[$module]; } function module_debug_entries(string $module): array { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { return []; } app()->session()->start(); $entries = $_SESSION['module_debug'][$module] ?? []; return is_array($entries) ? array_values(array_filter($entries, 'is_array')) : []; } function module_debug_push(string $module, array $entry): void { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { return; } app()->session()->start(); if (!isset($_SESSION['module_debug']) || !is_array($_SESSION['module_debug'])) { $_SESSION['module_debug'] = []; } if (!isset($_SESSION['module_debug'][$module]) || !is_array($_SESSION['module_debug'][$module])) { $_SESSION['module_debug'][$module] = []; } $entry['at'] = $entry['at'] ?? date('Y-m-d H:i:s'); array_unshift($_SESSION['module_debug'][$module], $entry); $_SESSION['module_debug'][$module] = array_slice($_SESSION['module_debug'][$module], 0, 25); } function module_debug_clear(string $module): void { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { return; } app()->session()->start(); unset($_SESSION['module_debug'][$module]); } function module_shell_header(string $module, array $options = []): string { $design = module_design($module); $requestPath = app()->request()->path(); $title = trim((string) ($options['title'] ?? '')); $description = trim((string) ($options['description'] ?? '')); $eyebrow = trim((string) ($options['eyebrow'] ?? $design['eyebrow'] ?? 'Modul')); $actions = is_array($options['actions'] ?? null) ? $options['actions'] : (is_array($design['actions'] ?? null) ? $design['actions'] : []); $tabs = is_array($options['tabs'] ?? null) ? $options['tabs'] : (is_array($design['tabs'] ?? null) ? $design['tabs'] : []); $renderActions = static function (array $actions): string { $html = ''; foreach ($actions as $action) { if (!is_array($action)) { continue; } $label = trim((string) ($action['label'] ?? '')); $href = trim((string) ($action['href'] ?? '')); if ($label === '' || $href === '') { continue; } $variant = trim((string) ($action['variant'] ?? 'secondary')); $size = trim((string) ($action['size'] ?? 'sm')); $class = $variant === 'ghost' ? 'module-button module-button--ghost' : 'module-button module-button--secondary'; if ($size === 'sm') { $class .= ' module-button--small'; } $html .= '' . e($label) . ''; } return $html; }; $html = '
'; $html .= '
'; if ($tabs !== [] || $actions !== []) { $html .= '
'; if ($tabs !== []) { $html .= ''; } if ($actions !== []) { $html .= '
' . $renderActions($actions) . '
'; } $html .= '
'; } elseif ($title !== '' || $description !== '' || !empty($options['show_eyebrow'])) { $html .= '
'; if (!empty($options['show_eyebrow'])) { $html .= '
' . e($eyebrow) . '
'; } if ($title !== '') { $html .= '

' . e($title) . '

'; } if ($description !== '') { $html .= '

' . e($description) . '

'; } $html .= '
'; } if ($title !== '') { $moduleTitle = trim((string) ($design['title'] ?? ucfirst($module))); $selector = '.home-hero[data-module-name="' . $module . '"] .brand-copy h1'; $script = '(function(){' . 'var root=document.querySelector(' . json_encode($selector, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ');' . 'if(!root){return;}' . 'var old=root.querySelector(".module-page-context");' . 'if(old){old.remove();}' . 'root.textContent=' . json_encode($moduleTitle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ';' . 'var pageTitle=' . json_encode($title, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ';' . 'if(pageTitle&&pageTitle!==' . json_encode($moduleTitle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '){' . 'var span=document.createElement("span");' . 'span.className="module-page-context";' . 'span.textContent=" / "+pageTitle;' . 'root.appendChild(span);' . '}' . '})();'; $html .= ''; } $html .= '
'; return $html; } function module_shell_footer(): string { $html = ''; $module = current_module_name(); if (is_string($module) && $module !== '') { if ((string) ($_GET['module_debug_clear'] ?? '') === '1') { module_debug_clear($module); } $entries = module_debug_entries($module); $currentPath = app()->request()->path(); $clearHref = $currentPath . '?module_debug_clear=1'; $html .= '
'; $html .= '
'; $html .= ''; $html .= 'Debug'; $html .= '' . e((string) count($entries)) . ' Eintraege'; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
Standard-Debugbereich des Moduls. Zeigt die letzten Request-/Response-Daten der aktuellen Sitzung.
'; $html .= 'Debug leeren'; $html .= '
'; if ($entries === []) { $html .= '
Noch keine Debug-Daten vorhanden.
'; } else { foreach ($entries as $index => $entry) { $label = trim((string) ($entry['label'] ?? ('Eintrag ' . ($index + 1)))); $at = trim((string) ($entry['at'] ?? '')); $payload = json_encode($entry, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (!is_string($payload)) { $payload = '{}'; } $html .= '
'; $html .= '' . e($label) . '' . ($at !== '' ? '' . e($at) . '' : '') . ''; $html .= '
' . e($payload) . '
'; $html .= '
'; } } $html .= '
'; $html .= '
'; $html .= '
'; } return $html . '
'; } /** * HTML Escaping Helper. */ function e(?string $string): string { return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8'); } function app_primary_domain(): string { if (defined('APP_DOMAIN_PRIMARY')) { return APP_DOMAIN_PRIMARY; } if (defined('APP_DOMAIN_NAME')) { return APP_DOMAIN_NAME; } return $_SERVER['HTTP_HOST'] ?? ''; } function app_fakecheck_domain(): string { if (defined('APP_DOMAIN_FAKECHECK')) { return APP_DOMAIN_FAKECHECK; } return app_primary_domain(); } function asset_styles(): void { $styles = app()->assets()->styles(); $order = ['early' => 0, 'normal' => 1, 'late' => 2]; usort($styles, fn($a, $b) => ($order[$a['priority']] ?? 1) <=> ($order[$b['priority']] ?? 1)); foreach ($styles as $s) { $href = $s['href']; $v = $s['version']; if ($v !== null && $v !== '') { $sep = (str_contains($href, '?') ? '&' : '?'); $href = $href . $sep . 'v=' . rawurlencode((string)$v); } echo '' . "\n"; } } function asset_scripts(string $pos = 'footer'): void { $scripts = ($pos === 'header') ? app()->assets()->headerScripts() : app()->assets()->footerScripts(); foreach ($scripts as $s) { $src = $s['src']; $v = $s['version']; if ($v !== null && $v !== '') { $sep = (str_contains($src, '?') ? '&' : '?'); $src = $src . $sep . 'v=' . rawurlencode((string)$v); } $attrs = ''; if (!empty($s['defer'])) { $attrs .= ' defer'; } if (!empty($s['async'])) { $attrs .= ' async'; } echo '' . "\n"; } } function redirect(string $path): void { header('Location: ' . $path, true, 303); exit; }