conf = $this->loadConfig(); $this->cors(); $this->setInput(); $this->pdo = $this->getPdoTemplates(); $this->resolveAction(); $this->resolveTableMap(); $this->authService = new AuthService($this->conf, $this->pdo); } catch (Throwable $e) { // Im Fehlerfall ruft fail() die respond() Methode auf, die den Header setzt und den Buffer leert. $this->fail('Initialization error', get_class($e) . ': ' . $e->getMessage(), 500); } } // --- Core Responder-Methoden (KORRIGIERT) --- public function respond($data, int $code = 200): void { // 1. Output-Puffer leeren, um jeglichen unbeabsichtigten Output zu verwerfen (z.B. PHP Notices). if (ob_get_level() > 0) { ob_clean(); } // 2. 💡 KRITISCHE KORREKTUR: Content-Type Header setzen. // Dies ist der entscheidende Schritt, der dem Browser sagt: "Dies ist JSON!" if (!headers_sent() && !isset($this->conf['no_content_type'])) { header('Content-Type: application/json; charset=utf-8'); } http_response_code($code); echo is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } public function fail(string $msg, $detail = null, int $code = 400): void { $this->respond(['ok' => false, 'error' => $msg, 'detail' => $detail], $code); } // --- Private Initialisierungs- & Utility-Methoden (Unverändert) --- private function loadConfig(): array { /* ... Logik bleibt unverändert ... */ $paths = [ __DIR__ . '/../config/emailtemplate.conf.php', __DIR__ . '/../inc/config.php', __DIR__ . '/config.php', __DIR__ . '/../config.php', __DIR__ . '/../../config.php', ]; foreach ($paths as $p) { if (is_file($p)) { $conf = @include $p; if (is_array($conf)) return $conf; } } $this->fail('Invalid config', 'config file not found or not returning array', 500); } private function cors(): void { /* ... Logik bleibt unverändert ... */ $cors = $this->conf['cors'] ?? '*'; if ($cors) { header('Access-Control-Allow-Origin: ' . $cors); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); header('Access-Control-Allow-Credentials: true'); } if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') $this->respond(['ok' => true]); if (!empty($this->conf['auth']['cookie'])) { $c = $this->conf['auth']['cookie']; $params = session_get_cookie_params(); $params['lifetime'] = $c['lifetime'] ?? $params['lifetime']; $params['path'] = $c['path'] ?? $params['path']; $params['domain'] = $c['domain'] ?? $params['domain']; $params['secure'] = $c['secure'] ?? $params['secure']; $params['httponly'] = $c['httponly'] ?? $params['httponly']; if (isset($c['samesite'])) $params['samesite'] = $c['samesite']; session_set_cookie_params($params); } } private function setInput(): void { /* ... Logik bleibt unverändert ... */ $data = []; $ct = $_SERVER['CONTENT_TYPE'] ?? ''; if (stripos($ct, 'application/json') !== false) { $raw = file_get_contents('php://input'); if ($raw !== false && $raw !== '') { $js = json_decode($raw, true); if (is_array($js)) $data = $js; } } foreach ($_POST as $k => $v) $data[$k] = $v; foreach ($_GET as $k => $v) if (!array_key_exists($k, $data)) $data[$k] = $v; $this->in = $data; } private function getPdoTemplates(): PDO { /* ... Logik bleibt unverändert ... */ if (!isset($this->conf['projectdb']) || !is_array($this->conf['projectdb'])) { $this->fail('Missing project DB config', null, 500); } $c = $this->conf['projectdb']; $host = $c['db_host'] ?? 'localhost'; $db = $c['db_name'] ?? ($c['database'] ?? ''); $user = $c['db_user'] ?? ($c['username'] ?? ''); $pass = $c['db_pass'] ?? ($c['password'] ?? ''); $charset = $c['db_charset'] ?? 'utf8mb4'; $port = $c['db_port'] ?? 3306; $dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset"; $opt = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]; return new PDO($dsn, $user, $pass, $opt); } private function resolveAction(): void { /* ... Logik bleibt unverändert ... */ $action = $this->val($this->in, 'action', ''); $resource = $this->val($this->in, 'resource', null); $allowedResources = ['templates', 'sections', 'blocks', 'snippets']; if ($resource && in_array($resource, $allowedResources, true) && strpos((string)$action, '.') === false) { $verb = strtolower((string)$action); if (in_array($verb, ['list', 'get', 'create', 'update', 'delete'], true)) $action = $resource . '.' . $verb; } $this->action = $action; } private function resolveTableMap(): void { /* ... Logik bleibt unverändert ... */ $tables = $this->conf['tables'] ?? []; $this->tableMap = [ 'templates' => $tables['templates'] ?? 'emailtemplate_templates', 'sections' => $tables['sections'] ?? 'emailtemplate_sections', 'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks', 'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets', ]; } private function val(array $in, $keys, $default = null) { /* ... Logik bleibt unverändert ... */ if (!is_array($keys)) $keys = [$keys]; foreach ($keys as $k) if (array_key_exists($k, $in)) return $in[$k]; return $default; } private function firstExisting(array $columns, array $candidates): ?string { /* ... Logik bleibt unverändert ... */ foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c; return null; } private function tableColumns(string $table): array { /* ... Logik bleibt unverändert ... */ $cols = []; $stmt = $this->pdo->query("SHOW COLUMNS FROM `$table`"); foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field']; return $cols; } private function primaryKey(string $table): ?string { /* ... Logik bleibt unverändert ... */ $stmt = $this->pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'"); $stmt->execute(); $row = $stmt->fetch(); return $row['Column_name'] ?? null; } private function requireAuth(): array { /* ... Logik bleibt unverändert ... */ return $this->authService->requireAuth(); } private function pullId(array $src) { /* ... Logik bleibt unverändert ... */ $aliases = ['id', 'item_id', 'template_id', 'tpl_id', 'section_id', 'sec_id', 'block_id', 'blk_id', 'snippet_id', 'snip_id']; foreach ($aliases as $a) if (isset($src[$a]) && $src[$a] !== '') return $src[$a]; return null; } private function tenantWhere(array $session): array { /* ... Logik bleibt unverändert ... */ $multi = $this->conf['multi'] ?? []; $tenantCol = $multi['tenant_col'] ?? null; $mapSess = $multi['map_session_to'] ?? 'id'; if (!$tenantCol) return ['', []]; if (!$session) return [' AND 1=0 ', []]; $val = $session[$mapSess] ?? null; if ($val === null || $val === '') return [' AND 1=0 ', []]; return [" AND `$tenantCol` = :__tenant", [':__tenant' => $val]]; } private function tenantAssign(array $session, array $columns): array { /* ... Logik bleibt unverändert ... */ $multi = $this->conf['multi'] ?? []; $tenantCol = $multi['tenant_col'] ?? null; $mapSess = $multi['map_session_to'] ?? 'id'; if (!$tenantCol || !in_array($tenantCol, $columns, true)) return []; $val = $session[$mapSess] ?? null; return ($val === null || $val === '') ? [] : [$tenantCol => $val]; } private function resolveIdCol(string $kind): array { /* ... Logik bleibt unverändert ... */ $t = $this->tableMap[$kind]; $cfg = $this->conf['columns'][$kind] ?? []; $cols = $this->tableColumns($t); $idCol = $cfg['id'] ?? ($this->firstExisting($cols, ['id']) ?: $this->primaryKey($t)); if (!$idCol) $idCol = 'id'; return [$idCol, $cols]; } private function parseHtmlToGjsComponents(string $html): array { /* ... Logik bleibt unverändert ... */ if (trim($html) === '') return []; return [ [ 'type' => 'html', 'content' => $html, 'removable' => true, 'draggable' => true, 'droppable' => true, 'copyable' => true, 'selectable' => true, 'editable' => false, 'traits' => [], ] ]; } // 💡 Bereinigungsmethode private function cleanReferenceComponents(array $components): array { foreach ($components as &$component) { if (is_array($component) && isset($component['type'])) { if ($component['type'] === 'library-reference') { if (isset($component['content'])) { $component['content'] = ''; } if (isset($component['components'])) { $component['components'] = []; } } if (isset($component['components']) && is_array($component['components'])) { $component['components'] = $this->cleanReferenceComponents($component['components']); } } } return $components; } // ================================================================= // 🚀 CRUD HANDLER METHODEN // ================================================================= /** * Allgemeine Methode zur Handhabung von LIST-Anfragen. */ private function handleList(string $kind): void { $auth = $this->requireAuth(); $t = $this->tableMap[$kind]; [$idCol, $allCols] = $this->resolveIdCol($kind); $cfg = $this->conf['columns'][$kind] ?? []; $nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol); $descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']); $catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']); $updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']); $q = trim((string)$this->val($this->in, 'q', '')); $limit = max(1, (int)$this->val($this->in, 'limit', 500)); $offset = max(0, (int)$this->val($this->in, 'offset', 0)); $where = ' WHERE 1=1 '; $params = []; // Suchlogik (q) if ($q !== '') { $parts = ["`$nameCol` LIKE :q"]; if ($descCol) $parts[] = "`$descCol` LIKE :q"; if ($catCol) $parts[] = "`$catCol` LIKE :q"; $where .= " AND (" . implode(' OR ', $parts) . ") "; $params[':q'] = '%' . $q . '%'; } // Filterlogik (parentFilters) $parentFilters = [ 'template_id' => $this->val($this->in, ['template_id', 'tpl_id'], null), 'section_id' => $this->val($this->in, ['section_id', 'sec_id'], null), 'block_id' => $this->val($this->in, ['block_id', 'blk_id'], null), ]; foreach ($parentFilters as $col => $v) { if ($v === null || $v === '') continue; if (in_array($col, $allCols, true)) { $where .= " AND `$col` = :$col "; $params[":$col"] = $v; } } // Tenant-Filter [$tw, $tp] = $this->tenantWhere($auth); $where .= $tw; foreach ($tp as $k => $v) $params[$k] = $v; $order = $updCol ? " ORDER BY `$updCol` DESC " : " ORDER BY `$nameCol` ASC "; $sql = "SELECT * FROM `$t` $where $order LIMIT :off,:lim"; $stmt = $this->pdo->prepare($sql); // Bind parameters foreach ($params as $k => $v) $stmt->bindValue($k, $v, is_int($v) ? PDO::PARAM_INT : PDO::PARAM_STR); $stmt->bindValue(':off', $offset, PDO::PARAM_INT); $stmt->bindValue(':lim', $limit, PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(); $out = []; foreach ($rows as $r) { $item = [ 'id' => $r[$idCol] ?? null, 'name' => $r[$nameCol] ?? null, ]; if ($descCol && isset($r[$descCol])) $item['desc'] = $r[$descCol]; if ($catCol && isset($r[$catCol])) $item['category'] = $r[$catCol]; if ($updCol && isset($r[$updCol])) $item['updated_at'] = $r[$updCol]; // Lade HTML und JSON aus den korrekten Spalten $htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']); if ($htmlCol && isset($r[$htmlCol])) $item['html'] = (string)$r[$htmlCol]; $jsonCol = $this->firstExisting($allCols, ['json_content']); if ($jsonCol && isset($r[$jsonCol])) $item['content'] = $r[$jsonCol]; $out[] = $item; } $this->respond(['ok' => true, 'kind' => $kind, 'items' => $out, 'data' => $out, 'count' => count($out), 'offset' => $offset, 'limit' => $limit]); } /** * Allgemeine Methode zur Handhabung von GET-Anfragen. */ private function handleGet(string $kind): void { $auth = $this->requireAuth(); $t = $this->tableMap[$kind]; [$idCol, $allCols] = $this->resolveIdCol($kind); $id = $this->pullId($this->in); if ($id === null || $id === '') $this->fail('id required', null, 422); [$tw, $tp] = $this->tenantWhere($auth); $sql = "SELECT * FROM `$t` WHERE `$idCol` = :id" . $tw . " LIMIT 1"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':id', $id); foreach ($tp as $k => $v) $stmt->bindValue($k, $v); $stmt->execute(); $row = $stmt->fetch(); if (!$row) $this->fail('Not found', ['kind' => $kind, 'id' => $id], 404); $rowOut = ['id' => $row[$idCol] ?? $id] + $row; // Lade HTML und JSON aus den korrekten Spalten $htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']); $topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null; $jsonCol = $this->firstExisting($allCols, ['json_content']); $topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null; $gjsComponents = []; if ($topContent !== null) { $decodedContent = json_decode($topContent, true); if (is_array($decodedContent)) { $gjsComponents = $decodedContent; } } if (empty($gjsComponents) && $topHtml !== null) { $gjsComponents = $this->parseHtmlToGjsComponents($topHtml); } $this->respond([ 'ok' => true, 'kind' => $kind, 'id' => $rowOut['id'], 'item' => $rowOut, 'data' => $rowOut, 'html' => $topHtml, 'content' => $topContent, 'gjs_components' => $gjsComponents ]); } /** * Allgemeine Methode zur Handhabung von CREATE-Anfragen (inkl. JSON-Bereinigung). */ private function handleCreate(string $kind): void { $auth = $this->requireAuth(); $t = $this->tableMap[$kind]; [$idCol, $allCols] = $this->resolveIdCol($kind); $cfg = $this->conf['columns'][$kind] ?? []; $nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol); $descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']); $catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']); $updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']); $name = trim((string)$this->val($this->in, ['name', 'title'], '')); if ($name === '') $this->fail('name required', null, 422); $desc = (string)$this->val($this->in, ['description', 'desc'], null); $cat = (string)$this->val($this->in, ['category', 'cat'], null); $html = (string)$this->val($this->in, ['html', 'body', 'markup'], null); $json = $this->val($this->in, ['content_json', 'json', 'content', 'structure_json'], null); $settings = $this->val($this->in, ['settings_json', 'settings'], null); $templateId = $this->val($this->in, ['template_id', 'tpl_id'], null); $sectionId = $this->val($this->in, ['section_id', 'sec_id'], null); $blockId = $this->val($this->in, ['block_id', 'blk_id'], null); $data = [$nameCol => $name]; if ($desc !== null && $descCol) $data[$descCol] = $desc; if ($cat !== null && $catCol) $data[$catCol] = $cat; $htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup'])); $jsonDbCol = $this->firstExisting($allCols, ['json_content']); // --- LOGIK mit ERWEITERTER PRÜFUNG START --- // 1. JSON-Content behandeln if ($json !== null) { if ($jsonDbCol) { $components = is_string($json) ? json_decode($json, true) : $json; if (is_array($components)) { $components = $this->cleanReferenceComponents($components); // BEREINIGUNG $data[$jsonDbCol] = json_encode($components, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $data[$jsonDbCol] = is_string($json) ? $json : ''; } } else { // FALLBACK: Wenn JSON gesendet wurde, aber keine json_content Spalte existiert, FEHLER ausgeben $this->fail( 'JSON content provided but no `json_content` column found', ['table' => $t, 'available_cols' => $allCols], 422 ); } } // 2. HTML-Content speichern if ($htmlDbCol && $html !== null) { $data[$htmlDbCol] = $html; } // --- LOGIK mit ERWEITERTER PRÜFUNG ENDE --- $c = $this->firstExisting($allCols, ['settings_json', 'settings']); if ($c && $settings !== null) $data[$c] = is_string($settings) ? $settings : json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($templateId !== null && in_array('template_id', $allCols, true)) $data['template_id'] = $templateId; if ($sectionId !== null && in_array('section_id', $allCols, true)) $data['section_id'] = $sectionId; if ($blockId !== null && in_array('block_id', $allCols, true)) $data['block_id'] = $blockId; $data = $data + $this->tenantAssign($_SESSION['auth'] ?? [], $allCols); $now = date('Y-m-d H:i:s'); $createdCol = $this->firstExisting($allCols, ['created_at', 'created', 'createdAt']); if ($createdCol) $data[$createdCol] = $now; if ($updCol) $data[$updCol] = $now; $fields = array_keys($data); $place = array_map(fn($c) => ":$c", $fields); $sql = "INSERT INTO `$t` (" . implode(',', array_map(fn($c) => "`$c`", $fields)) . ") VALUES (" . implode(',', $place) . ")"; $stmt = $this->pdo->prepare($sql); foreach ($data as $k => $v) $stmt->bindValue(":$k", $v); $stmt->execute(); $newId = $this->pdo->lastInsertId(); $out = ['id' => $newId, 'name' => $name]; if ($desc !== null) $out['desc'] = $desc; if ($cat !== null) $out['category'] = $cat; $this->respond(['ok' => true, 'kind' => $kind, 'id' => $newId, 'item' => $out, 'data' => $out]); } /** * Allgemeine Methode zur Handhabung von UPDATE-Anfragen (inkl. JSON-Bereinigung). */ private function handleUpdate(string $kind): void { $auth = $this->requireAuth(); $t = $this->tableMap[$kind]; [$idCol, $allCols] = $this->resolveIdCol($kind); $cfg = $this->conf['columns'][$kind] ?? []; $nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol); $descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']); $catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']); $updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']); $id = $this->pullId($this->in); if ($id === null || $id === '') $this->fail('id required', null, 422); $data = []; $name = $this->val($this->in, ['name', 'title'], null); $desc = $this->val($this->in, ['description', 'desc'], null); $cat = $this->val($this->in, ['category', 'cat'], null); $html = $this->val($this->in, ['html', 'body', 'markup'], null); $json = $this->val($this->in, ['content_json', 'json', 'content', 'structure_json'], null); $settings = $this->val($this->in, ['settings_json', 'settings'], null); if ($name !== null) $data[$nameCol] = (string)$name; if ($desc !== null && $descCol) $data[$descCol] = (string)$desc; if ($cat !== null && $catCol) $data[$catCol] = (string)$cat; $htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup'])); $jsonDbCol = $this->firstExisting($allCols, ['json_content']); // --- LOGIK mit ERWEITERTER PRÜFUNG START --- // 1. JSON-Content behandeln if ($json !== null) { if ($jsonDbCol) { // Wenn JSON-Spalte existiert, JSON verarbeiten und speichern $components = is_string($json) ? json_decode($json, true) : $json; if (is_array($components)) { $components = $this->cleanReferenceComponents($components); // BEREINIGUNG $data[$jsonDbCol] = json_encode($components, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $data[$jsonDbCol] = is_string($json) ? $json : ''; } } else { // FALLBACK: Wenn JSON gesendet wurde, aber keine json_content Spalte existiert, FEHLER ausgeben $this->fail( 'JSON content provided but no `json_content` column found', ['table' => $t, 'available_cols' => $allCols], 422 ); } // 2. Den zugehörigen HTML-Output speichern (wird vom Editor immer mitgesendet, wenn JSON da ist) if ($html !== null && $htmlDbCol) { $data[$htmlDbCol] = (string)$html; } } elseif ($html !== null && $htmlDbCol) { // Wenn NUR HTML gesendet wird (für minimale Änderungen), speichern wir nur HTML. $data[$htmlDbCol] = (string)$html; } // --- LOGIK mit ERWEITERTER PRÜFUNG ENDE --- $c = $this->firstExisting($allCols, ['settings_json', 'settings']); if ($settings !== null && $c) $data[$c] = is_string($settings) ? $settings : json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $tpl = $this->val($this->in, ['template_id', 'tpl_id'], null); if ($tpl !== null && in_array('template_id', $allCols, true)) $data['template_id'] = $tpl; $sec = $this->val($this->in, ['section_id', 'sec_id'], null); if ($sec !== null && in_array('section_id', $allCols, true)) $data['section_id'] = $sec; $blk = $this->val($this->in, ['block_id', 'blk_id'], null); if ($blk !== null && in_array('block_id', $allCols, true)) $data['block_id'] = $blk; if ($updCol) $data[$updCol] = date('Y-m-d H:i:s'); if (!$data) $this->fail('nothing to update', null, 422); [$tw, $tp] = $this->tenantWhere($auth); $set = []; foreach (array_keys($data) as $c) $set[] = "`$c` = :$c"; $sql = "UPDATE `$t` SET " . implode(',', $set) . " WHERE `$idCol` = :__id" . $tw . " LIMIT 1"; $stmt = $this->pdo->prepare($sql); foreach ($data as $k => $v) $stmt->bindValue(":$k", $v); $stmt->bindValue(':__id', $id); foreach ($tp as $k => $v) $stmt->bindValue($k, $v); $stmt->execute(); $this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'updated' => array_keys($data)]); } /** * Allgemeine Methode zur Handhabung von DELETE-Anfragen. */ private function handleDelete(string $kind): void { $auth = $this->requireAuth(); $t = $this->tableMap[$kind]; [$idCol, $allCols] = $this->resolveIdCol($kind); $id = $this->pullId($this->in); if ($id === null || $id === '') $this->fail('id required', null, 422); [$tw, $tp] = $this->tenantWhere($auth); $sql = "DELETE FROM `$t` WHERE `$idCol` = :__id" . $tw . " LIMIT 1"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':__id', $id); foreach ($tp as $k => $v) $stmt->bindValue($k, $v); $stmt->execute(); $this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'deleted' => true]); } // ================================================================= // 💡 Öffentliche run()-Methode (KORRIGIERT) // ================================================================= public function run(): void { // 💡 KORREKTUR: Der Content-Type Header wird hier entfernt, da er jetzt in respond() // zentralisiert wurde, um sicherzustellen, dass er auch bei Fehlern im Konstruktor oder // im try-Block korrekt gesetzt wird. // header('Content-Type: application/json; charset=utf-8'); // DIESE ZEILE ENTFERNT try { // Extrahiere den Ressourcen-Typ und die Operation (z.B. 'templates' und 'list') [$kind, $operation] = explode('.', $this->action, 2) + [1 => '']; switch ($this->action) { case 'health': $this->respond(['ok' => true, 'time' => date('c')]); /* ---------- AUTH ---------- */ case 'auth.login': $result = $this->authService->login($this->in); $this->respond(['ok' => true] + $result); break; case 'auth.me': if (empty($_SESSION['auth'])) $this->fail('Not authenticated', null, 401); $this->respond(['ok' => true, 'user' => $_SESSION['auth']]); break; case 'auth.logout': $this->authService->logout(); $this->respond(['ok' => true]); break; /* ---------- CRUD HANDLER ---------- */ default: if (in_array($kind, ['templates', 'sections', 'blocks', 'snippets'])) { switch ($operation) { case 'list': $this->handleList($kind); break; case 'get': $this->handleGet($kind); break; case 'create': $this->handleCreate($kind); break; case 'update': $this->handleUpdate($kind); break; case 'delete': $this->handleDelete($kind); break; default: $this->fail('Unknown operation for resource: ' . $this->action, null, 404); break; } } else { $this->fail('Unknown action', $this->action ?: 'missing', 404); } break; } } catch (Throwable $e) { $this->fail('Server error', get_class($e) . ': ' . $e->getMessage(), 500); } } }