Files
nexus/src/App/functions.php
Lars Gebhardt-Kusche 42dd155904
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped
asdasd
2026-04-27 01:10:43 +02:00

551 lines
16 KiB
PHP

<?php
declare(strict_types=1);
use App\App;
function app(): App
{
return App::get();
}
function t(string $key, $default = '', array $vars = []): string
{
return app()->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 '<div class="card">Keine Berechtigung.</div>';
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 "<!-- tpl(): invalid parameter -->";
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 "<!-- Template not found: {$folder}/{$name} -->";
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 "<!-- module_tpl(): invalid parameter -->";
return;
}
}
$path = $base . $name . '.php';
if (file_exists($path)) {
extract($data);
require $path;
} else {
echo "<!-- module_tpl(): not found: {$module}/{$name} -->";
}
}
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 .= '<a class="' . e($class) . '" href="' . e($href) . '">' . e($label) . '</a>';
}
return $html;
};
$html = '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">';
$html .= '<header class="module-hero">';
if ($tabs !== [] || $actions !== []) {
$html .= '<div class="module-hero-top module-hero-top--compact">';
if ($tabs !== []) {
$html .= '<nav class="module-tabs" aria-label="Modulnavigation">';
foreach ($tabs as $tab) {
if (!is_array($tab)) {
continue;
}
$label = trim((string) ($tab['label'] ?? ''));
$href = trim((string) ($tab['href'] ?? ''));
if ($label === '' || $href === '') {
continue;
}
$matchPrefixes = is_array($tab['match_prefixes'] ?? null) ? $tab['match_prefixes'] : [];
$isActive = !empty($tab['active']) || $href === $requestPath;
if (!$isActive) {
foreach ($matchPrefixes as $prefix) {
$prefix = is_string($prefix) ? trim($prefix) : '';
if ($prefix !== '' && str_starts_with($requestPath, $prefix)) {
$isActive = true;
break;
}
}
}
$class = $isActive ? 'module-button module-button--tab-active' : 'module-button module-button--tab';
$html .= '<a class="' . e($class) . '" href="' . e($href) . '">' . e($label) . '</a>';
}
$html .= '</nav>';
}
if ($actions !== []) {
$html .= '<div class="module-hero-actions">' . $renderActions($actions) . '</div>';
}
$html .= '</div>';
} elseif ($title !== '' || $description !== '' || !empty($options['show_eyebrow'])) {
$html .= '<div class="module-hero-copy">';
if (!empty($options['show_eyebrow'])) {
$html .= '<div class="eyebrow">' . e($eyebrow) . '</div>';
}
if ($title !== '') {
$html .= '<h1 class="module-title">' . e($title) . '</h1>';
}
if ($description !== '') {
$html .= '<p class="module-lead">' . e($description) . '</p>';
}
$html .= '</div>';
}
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 .= '<script>' . $script . '</script>';
}
$html .= '</header>';
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 .= '<section class="module-debug">';
$html .= '<details class="module-debug-details"' . ($entries !== [] ? ' open' : '') . '>';
$html .= '<summary class="module-debug-summary">';
$html .= '<span>Debug</span>';
$html .= '<span class="module-debug-meta">' . e((string) count($entries)) . ' Eintraege</span>';
$html .= '</summary>';
$html .= '<div class="module-debug-body">';
$html .= '<div class="module-debug-toolbar">';
$html .= '<div class="muted">Standard-Debugbereich des Moduls. Zeigt die letzten Request-/Response-Daten der aktuellen Sitzung.</div>';
$html .= '<a class="module-button module-button--ghost module-button--small" href="' . e($clearHref) . '">Debug leeren</a>';
$html .= '</div>';
if ($entries === []) {
$html .= '<div class="module-debug-empty">Noch keine Debug-Daten vorhanden.</div>';
} 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 .= '<details class="module-debug-entry"' . ($index === 0 ? ' open' : '') . '>';
$html .= '<summary><strong>' . e($label) . '</strong>' . ($at !== '' ? '<span class="module-debug-entry-time">' . e($at) . '</span>' : '') . '</summary>';
$html .= '<pre class="module-debug-pre">' . e($payload) . '</pre>';
$html .= '</details>';
}
}
$html .= '</div>';
$html .= '</details>';
$html .= '</section>';
}
return $html . '</div></div></div>';
}
/**
* 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 '<link rel="stylesheet" href="' . htmlspecialchars($href, ENT_QUOTES) . '">' . "\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 '<script src="' . htmlspecialchars($src, ENT_QUOTES) . '"' . $attrs . '></script>' . "\n";
}
}
function redirect(string $path): void
{
header('Location: ' . $path, true, 303);
exit;
}