Boersenchecker
This commit is contained in:
22
src/App/ModuleConfigException.php
Normal file
22
src/App/ModuleConfigException.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user