Boersenchecker
All checks were successful
Deploy / deploy-staging (push) Successful in 28s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-22 23:29:03 +02:00
parent 616b03474f
commit dc66f75eef
24 changed files with 5151 additions and 5 deletions

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App;
final class ModuleConfigException extends \RuntimeException
{
public function __construct(
public readonly string $moduleName,
string $message,
?string $detail = null,
int $code = 0,
?\Throwable $previous = null
) {
parent::__construct(
$detail !== null && $detail !== '' ? $message . ' ' . $detail : $message,
$code,
$previous
);
}
}

View File

@@ -6,6 +6,7 @@ namespace ModulesCore {
use App\ConfigLoader;
use App\KeycloakAuth;
use App\PdoFactory;
use RuntimeException;
final class LegacyCompat
@@ -41,6 +42,7 @@ namespace ModulesCore {
{
private ?LegacyCompatConfig $config = null;
private ?LegacyCompatAuthContext $auth = null;
private ?\PDO $basePdo = null;
public function __construct(
private readonly string $projectRoot,
@@ -64,6 +66,27 @@ namespace ModulesCore {
return $this->auth;
}
public function basePdo(): ?\PDO
{
if ($this->basePdo instanceof \PDO) {
return $this->basePdo;
}
$db = $this->config()->dbConfig;
if ($db === []) {
return null;
}
$db['options'] ??= [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
];
$this->basePdo = PdoFactory::createFromArray($db);
return $this->basePdo;
}
}
final class LegacyCompatConfig
@@ -123,6 +146,10 @@ namespace ModulesCore {
{
/** @var array<string, array<string, mixed>> */
private array $settingsCache = [];
/** @var array<string, callable> */
private array $callbacks = [];
/** @var array<string, bool> */
private array $booted = [];
public function __construct(
private readonly string $projectRoot,
@@ -136,11 +163,20 @@ namespace ModulesCore {
public function hasFunction(string $moduleName, string $functionName): bool
{
return false;
$this->ensureBooted($moduleName);
return isset($this->callbacks[$moduleName . ':' . $functionName]);
}
public function call(string $moduleName, string $functionName, mixed ...$args): mixed
{
$this->ensureBooted($moduleName);
$key = $moduleName . ':' . $functionName;
if (isset($this->callbacks[$key])) {
return ($this->callbacks[$key])(...$args);
}
throw new RuntimeException(sprintf(
'Modulfunktion %s::%s ist in dieser Laufzeit nicht registriert.',
$moduleName,
@@ -148,6 +184,11 @@ namespace ModulesCore {
));
}
public function registerFunction(string $moduleName, string $functionName, callable $callable): void
{
$this->callbacks[$moduleName . ':' . $functionName] = $callable;
}
/**
* @return array<string, mixed>
*/
@@ -158,19 +199,97 @@ namespace ModulesCore {
}
$configPath = $this->modulePath($moduleName) . '/config/module.php';
if (!is_file($configPath)) {
return $this->settingsCache[$moduleName] = [];
$config = is_file($configPath) ? require $configPath : [];
$defaults = is_array($config) ? $config : [];
$storedSettingsPath = $this->storedSettingsPath($moduleName);
if (!is_file($storedSettingsPath)) {
return $this->settingsCache[$moduleName] = $defaults;
}
$config = require $configPath;
$raw = file_get_contents($storedSettingsPath);
$stored = is_string($raw) && trim($raw) !== '' ? json_decode($raw, true) : [];
return $this->settingsCache[$moduleName] = is_array($config) ? $config : [];
return $this->settingsCache[$moduleName] = $this->mergeSettings(
$defaults,
is_array($stored) ? $stored : []
);
}
public function saveSettings(string $moduleName, array $settings): void
{
$path = $this->storedSettingsPath($moduleName);
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
file_put_contents(
$path,
json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
);
unset($this->settingsCache[$moduleName]);
}
public function modulePdo(string $moduleName, array $fallback = []): ?\PDO
{
$settings = $this->settings($moduleName);
$db = $settings['db'] ?? $fallback;
if (!is_array($db) || $db === []) {
throw new RuntimeException('Modul-Datenbank nicht konfiguriert.');
}
$db['options'] ??= [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
];
return PdoFactory::createFromArray($db);
}
private function modulePath(string $moduleName): string
{
return $this->projectRoot . '/modules/' . trim($moduleName, '/');
}
private function ensureBooted(string $moduleName): void
{
if (isset($this->booted[$moduleName])) {
return;
}
$this->booted[$moduleName] = true;
$bootstrapPath = $this->modulePath($moduleName) . '/bootstrap.php';
if (is_file($bootstrapPath)) {
$modules = $this;
require_once $bootstrapPath;
}
}
private function storedSettingsPath(string $moduleName): string
{
return $this->projectRoot . '/data/module-settings/' . trim($moduleName, '/') . '.json';
}
/**
* @param array<string, mixed> $defaults
* @param array<string, mixed> $stored
* @return array<string, mixed>
*/
private function mergeSettings(array $defaults, array $stored): array
{
foreach ($stored as $key => $value) {
if (is_array($value) && isset($defaults[$key]) && is_array($defaults[$key])) {
$defaults[$key] = $this->mergeSettings($defaults[$key], $value);
continue;
}
$defaults[$key] = $value;
}
return $defaults;
}
}
}
@@ -217,4 +336,186 @@ namespace {
return $timezone !== '' ? $timezone : 'Europe/Berlin';
}
}
if (!function_exists('auth_enabled')) {
function auth_enabled(): bool
{
return app()->auth()->isEnabled();
}
}
if (!function_exists('auth_user')) {
function auth_user(): ?array
{
return app()->auth()->user();
}
}
if (!function_exists('auth_groups')) {
function auth_groups(): array
{
$user = auth_user();
return is_array($user['groups'] ?? null) ? array_values($user['groups']) : [];
}
}
if (!function_exists('auth_is_admin')) {
function auth_is_admin(): bool
{
return in_array('administrators', auth_groups(), true);
}
}
if (!function_exists('require_auth')) {
function require_auth(): void
{
if (auth_user() !== null) {
return;
}
header('Location: /auth/keycloak', true, 302);
exit;
}
}
if (!function_exists('module_design')) {
function module_design(string $module): array
{
if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) {
return [];
}
$path = dirname(__DIR__, 2) . '/modules/' . $module . '/design.json';
if (!is_file($path)) {
return [];
}
$raw = file_get_contents($path);
$decoded = is_string($raw) && trim($raw) !== '' ? json_decode($raw, true) : null;
return is_array($decoded) ? $decoded : [];
}
}
if (!function_exists('e')) {
function e(?string $string): string
{
return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8');
}
}
if (!function_exists('module_shell_header')) {
function module_shell_header(string $module, array $options = []): string
{
$design = module_design($module);
$requestPath = (string) (parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH) ?: '');
$requestQuery = [];
parse_str((string) (parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_QUERY) ?: ''), $requestQuery);
$title = trim((string) ($options['title'] ?? ''));
$description = trim((string) ($options['description'] ?? ($design['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'] : []);
$html = '<div class="module-shell"><div class="module-page-bg"><div class="module-page-stack">';
$html .= '<header class="module-hero submenu-box">';
$html .= '<div class="module-hero-copy">';
$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 ($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;
}
$activeView = trim((string) ($tab['view'] ?? ''));
$isActive = !empty($tab['active']);
if (!$isActive && $activeView !== '') {
$isActive = (string) ($requestQuery['view'] ?? '') === $activeView;
}
if (!$isActive && $href === $requestPath) {
$isActive = true;
}
$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 module-submenu-actions">';
foreach ($actions as $action) {
if (!is_array($action)) {
continue;
}
$label = trim((string) ($action['label'] ?? ''));
$href = trim((string) ($action['href'] ?? ''));
if ($label === '' || $href === '') {
continue;
}
$html .= '<a class="module-button module-button--secondary module-button--small" href="' . e($href) . '">' . e($label) . '</a>';
}
$html .= '</div>';
}
$html .= '</div>';
}
$html .= '</header>';
return $html;
}
}
if (!function_exists('module_shell_footer')) {
function module_shell_footer(): string
{
return '</div></div></div>';
}
}
if (!function_exists('module_tpl')) {
function module_tpl(string $module, string $name, array $data = []): void
{
$path = dirname(__DIR__, 2) . '/modules/' . $module . '/partials/' . $name . '.php';
if (!is_file($path)) {
echo '<!-- module_tpl(): not found -->';
return;
}
extract($data);
require $path;
}
}
if (!function_exists('module_debug_push')) {
function module_debug_push(string $source, array $entry): void
{
if (!auth_is_admin()) {
return;
}
if (!isset($_SESSION['desktop_debug_server']) || !is_array($_SESSION['desktop_debug_server'])) {
$_SESSION['desktop_debug_server'] = [];
}
$entry['source'] = $entry['source'] ?? $source;
$entry['time'] = $entry['time'] ?? date(DATE_ATOM);
array_unshift($_SESSION['desktop_debug_server'], $entry);
$_SESSION['desktop_debug_server'] = array_slice($_SESSION['desktop_debug_server'], 0, 250);
}
}
}