This commit is contained in:
2025-12-04 22:33:05 +01:00
parent 316175e158
commit 9dee06cdd6
145 changed files with 16865 additions and 88 deletions

636
inc/ApiKernel (Kopie).php Normal file
View File

@@ -0,0 +1,636 @@
<?php
declare(strict_types=1);
// Lade den AuthService
require_once __DIR__ . '/AuthService.php';
// -----------------------------------------------------------------
// ApiKernel.php (OPTIMIERT & BEREINIGT)
// -----------------------------------------------------------------
class ApiKernel
{
// Klassen-Eigenschaften
private array $conf;
private ?PDO $pdo = null;
private array $in;
private string $action;
private array $tableMap;
private AuthService $authService;
// --- Initialisierung & Konstruktor (Unverändert) ---
public function __construct()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
try {
$this->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) {
$this->fail('Initialization error', get_class($e) . ': ' . $e->getMessage(), 500);
}
}
// --- Core Responder-Methoden (Unverändert) ---
public function respond($data, int $code = 200): void
{
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.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.php', 'config.php 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['templates']) || !is_array($this->conf['templates'])) {
$this->fail('Missing templates DB config', null, 500);
}
$c = $this->conf['templates'];
$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' => [],
]
];
}
// 💡 KORREKTUR: Bereinigungsmethode (von vorheriger Version übernommen)
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;
}
// =================================================================
// 🚀 NEUE CRUD HANDLER METHODEN (Logik aus run() extrahiert)
// =================================================================
/**
* 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 (DEUTLICH VEREINFACHT)
// =================================================================
public function run(): void
{
header('Content-Type: application/json; charset=utf-8');
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);
}
}
}

657
inc/ApiKernel.php Normal file
View File

@@ -0,0 +1,657 @@
<?php
declare(strict_types=1);
// 💡 NEUE KORREKTUR: Starte Output Buffering so früh wie möglich, um Whitespace/Errors
// von inkludierten Dateien (AuthService.php, config.php) abzufangen.
ob_start();
// Lade den AuthService
require_once __DIR__ . '/AuthService.php';
// -----------------------------------------------------------------
// ApiKernel.php (OPTIMIERT & KORRIGIERT)
// -----------------------------------------------------------------
class ApiKernel
{
// Klassen-Eigenschaften
private array $conf;
private ?PDO $pdo = null;
private array $in;
private string $action;
private array $tableMap;
private AuthService $authService;
// --- Initialisierung & Konstruktor (Optimiert) ---
public function __construct()
{
// ob_start() wurde an den Anfang der Datei verschoben und wird hier entfernt.
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
try {
$this->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.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.php', 'config.php 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['templates']) || !is_array($this->conf['templates'])) {
$this->fail('Missing templates DB config', null, 500);
}
$c = $this->conf['templates'];
$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);
}
}
}

105
inc/AuthService.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
// -----------------------------------------------------------------
// AuthService.php: Kapselt die gesamte Authentifizierungslogik.
// -----------------------------------------------------------------
class AuthService
{
private array $conf;
private PDO $pdo;
// Abhängigkeiten (Konfiguration und PDO) werden per Konstruktor übergeben
public function __construct(array $conf, PDO $pdo)
{
$this->conf = $conf;
$this->pdo = $pdo;
}
// --- Private Utility Methoden ---
private function fail(string $msg, $detail = null, int $code = 400): void
{
// Wir müssen hier direkt antworten, da wir das Fail-Verhalten des Kernels benötigen.
// Im ApiKernel werden wir die respond/fail-Methoden als public lassen,
// um sie hier injizieren zu können, oder wir lassen sie hier im Global Scope
// (WENN Sie die ursprünglichen globalen Funktionen respond/fail wieder zulassen).
// Für eine saubere Kapselung injizieren wir die Respond-Logik.
// HIER verwenden wir eine einfache JSON-Antwort, da die fail-Methode
// normalerweise den gesamten Kernel stoppt. Wir nutzen exit.
http_response_code($code);
echo json_encode(['ok'=>false,'error'=>$msg,'detail'=>$detail], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
exit;
}
private function verifyPassword(string $input, string $stored, array $authDbConf): bool
{
if (preg_match('~^\$2[aby]\$~', $stored) || strpos($stored, '$argon2') === 0) return password_verify($input, $stored);
$legacy = strtolower($authDbConf['legacy'] ?? '');
if ($legacy === 'md5') return hash_equals($stored, md5($input));
if ($legacy === 'sha1') return hash_equals($stored, sha1($input));
if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored);
return hash_equals($stored, $input);
}
// --- Public Service Methoden ---
public function requireAuth(): array
{
if (empty($_SESSION['auth'])) $this->fail('Not authenticated', null, 401);
return $_SESSION['auth'];
}
public function logout(): bool
{
$_SESSION = [];
if (session_id() !== '') session_destroy();
return true;
}
public function login(array $in): array
{
$authDb = $this->conf['auth']['db'] ?? [];
$colUser = $authDb['col_user'] ?? 'email';
$colPass = $authDb['col_pass'] ?? 'password';
$colName = $authDb['col_name'] ?? 'name';
$colId = $authDb['col_id'] ?? 'id';
$colStatus = $authDb['col_status']?? null;
$activeValues = $authDb['active_values'] ?? ['active','1',1];
$table = $authDb['table'] ?? 'emailtemplate_users';
$identifier = trim((string)($in['username'] ?? $in['user'] ?? $in['email'] ?? $in['login'] ?? ''));
$password = (string)($in['password'] ?? $in['pass'] ?? $in['pwd'] ?? '');
if ($identifier === '' || $password === '') $this->fail('username/password required', null, 422);
$stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1");
$stmt->execute([':u'=>$identifier]);
$row = $stmt->fetch();
if (!$row) $this->fail('Invalid credentials', null, 401);
if ($colStatus && isset($row[$colStatus])) {
if (!in_array($row[$colStatus], $activeValues, true)) {
$this->fail('Account inactive', null, 403);
}
}
$stored = (string)($row[$colPass] ?? '');
if ($stored === '' || !$this->verifyPassword($password, $stored, $authDb)) {
$this->fail('Invalid credentials', null, 401);
}
$_SESSION['auth'] = [
'id' => $row[$colId] ?? null,
'name' => $row[$colName] ?? ($row[$colUser] ?? $identifier),
'email' => $row[$colUser] ?? $identifier,
'at' => time(),
];
$token = base64_encode(hash('sha256', ($_SESSION['auth']['id'] ?? $identifier).'|'.session_id(), true));
return ['user'=>$_SESSION['auth'], 'token'=>$token];
}
}

View File

@@ -0,0 +1,138 @@
<?php
// inc/auth_helpers.php robustes Session-Handling (2025-09-05)
declare(strict_types=1);
/** ===== Session ===== */
function auth_start_session(array $cfg): void {
if (session_status() === PHP_SESSION_NONE) {
$name = $cfg['auth']['session_name'] ?? 'et_session';
session_name($name);
// Defaults
$cookiePath = $cfg['auth']['cookie_path'] ?? '/';
$cookieDomain = $cfg['auth']['cookie_domain'] ?? '';
$cookieSecure = $cfg['auth']['cookie_secure'] ?? true;
$cookieHttpOnly = $cfg['auth']['cookie_httponly'] ?? true;
$cookieSameSite = $cfg['auth']['cookie_samesite'] ?? 'Lax';
// PHP 7.3+ array API
session_set_cookie_params([
'lifetime' => 0,
'path' => $cookiePath,
'domain' => $cookieDomain,
'secure' => $cookieSecure,
'httponly' => $cookieHttpOnly,
'samesite' => $cookieSameSite,
]);
session_start();
}
}
/** ===== Auth Core ===== */
function auth_login(PDO $pdoCustomers, array $cfg, string $email, string $password): array {
auth_start_session($cfg);
$sql = "SELECT cu.id, cu.customer_id, cu.email, cu.password_hash, cu.role,
c.slug AS customer_slug, c.plan, c.status
FROM customer_users cu
JOIN customers c ON c.id = cu.customer_id
WHERE cu.email = :email AND cu.is_active = 1
LIMIT 1";
$st = $pdoCustomers->prepare($sql);
$st->execute([':email' => $email]);
$u = $st->fetch(PDO::FETCH_ASSOC);
if (!$u || !password_verify($password, $u['password_hash'])) {
return ['ok' => false, 'error' => 'invalid_credentials'];
}
if (($u['status'] ?? 'active') !== 'active') {
return ['ok' => false, 'error' => 'customer_inactive'];
}
// neue Session-ID, alte wird invalidiert
session_regenerate_id(true);
$_SESSION['user'] = [
'id' => (int)$u['id'],
'email' => $u['email'],
'role' => $u['role'],
'customer_id' => (int)$u['customer_id'],
'customer_slug' => $u['customer_slug'],
'plan' => $u['plan'],
];
return ['ok' => true, 'user' => $_SESSION['user']];
}
function auth_logout(array $cfg): void {
auth_start_session($cfg);
// Sessiondaten löschen
$_SESSION = [];
// Cookie-Parameter aus der aktiven Session
$params = session_get_cookie_params();
$name = session_name();
// Kandidaten für Domain/Path, um "falsch" gesetzte Cookies sicher zu treffen
$host = $_SERVER['HTTP_HOST'] ?? '';
$cfgDomain = $cfg['auth']['cookie_domain'] ?? '';
$paths = array_values(array_unique([$params['path'] ?? '/', '/', '']));
$domains = array_values(array_unique([
$params['domain'] ?? '',
$cfgDomain,
$host,
ltrim($host, '.'),
(strpos($host, '.') !== false ? '.' . ltrim($host, '.') : $host),
]));
// Alle Varianten invalidieren (secure/httponly wie gesetzt)
foreach ($paths as $p) {
foreach ($domains as $d) {
if ($d === null) continue;
setcookie($name, '', time() - 3600, $p, $d, $params['secure'] ?? true, $params['httponly'] ?? true);
}
// zusätzlich: ohne Domain (trifft Host-spezifische Cookies)
setcookie($name, '', time() - 3600, $p, '', $params['secure'] ?? true, $params['httponly'] ?? true);
}
// Session beenden
session_destroy();
session_write_close();
// In Staging aggressiv: Browser bitten, Cookies zu löschen (nicht jeder Browser respektiert das sofort)
if (($cfg['env'] ?? 'prod') === 'staging') {
header('Clear-Site-Data: "cookies"', false);
}
}
function auth_require(array $cfg): void {
auth_start_session($cfg);
if (empty($_SESSION['user'])) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'unauthorized']);
exit;
}
}
function require_role(array $cfg, array $roles): void {
auth_start_session($cfg);
$r = $_SESSION['user']['role'] ?? null;
if (!$r || !in_array($r, $roles, true)) {
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'forbidden']);
exit;
}
}
function current_user(array $cfg): ?array {
auth_start_session($cfg);
return $_SESSION['user'] ?? null;
}
function current_customer_id(array $cfg): ?int {
$u = current_user($cfg);
return $u['customer_id'] ?? null;
}

232
inc/OUTDATED bootstrap.php Normal file
View File

@@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
// =================================================================
// 🚨 KRITISCHE STARTSEQUENZ / BOOTSTRAP (VERSION MIT FUNKTIONEN) 🚨
// Alle Helfer sind jetzt reguläre, globale Funktionen, um Scope-Probleme zu vermeiden.
// -----------------------------------------------------------------
// 1. Composer Autoload
$composerAutoload = __DIR__ . '/../../vendor/autoload.php';
if (is_file($composerAutoload)) {
require_once $composerAutoload;
}
// 2. Session Start (Muss VOR dem Senden des Session-Cookies/Headers erfolgen)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// --- Globale Helferfunktionen (Die in api.php aufgerufen werden) ---
function respond($data, int $code = 200): void {
http_response_code($code);
echo is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
exit;
}
function fail(string $msg, $detail = null, int $code = 400): void {
respond(['ok'=>false,'error'=>$msg,'detail'=>$detail], $code);
}
// RESTLICHE HELFER (MÜSSEN KEINE GLOBALS VERWENDEN)
function load_config(): array {
$paths = [
__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;
}
}
fail('Invalid config.php', 'config.php not found or not returning array', 500);
}
function cors(array $conf): void {
$cors = $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') respond(['ok'=>true]);
}
function get_input(): array {
$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;
return $data;
}
function val(array $in, $keys, $default=null) {
if (!is_array($keys)) $keys = [$keys];
foreach ($keys as $k) if (array_key_exists($k,$in)) return $in[$k];
return $default;
}
function first_existing(array $columns, array $candidates): ?string {
foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c;
return null;
}
function pdo_templates(array $conf): PDO {
if (!isset($conf['templates']) || !is_array($conf['templates'])) {
fail('Missing templates DB config', null, 500);
}
$c = $conf['templates'];
$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);
}
function verify_password(string $input, string $stored, array $authDbConf): bool {
if (preg_match('~^\$2[aby]\$~', $stored) || strpos($stored, '$argon2') === 0) return password_verify($input, $stored);
$legacy = strtolower($authDbConf['legacy'] ?? '');
if ($legacy === 'md5') return hash_equals($stored, md5($input));
if ($legacy === 'sha1') return hash_equals($stored, sha1($input));
if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored);
return hash_equals($stored, $input);
}
function table_columns(PDO $pdo, string $table): array {
$cols = [];
$stmt = $pdo->query("SHOW COLUMNS FROM `$table`");
foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field'];
return $cols;
}
function primary_key(PDO $pdo, string $table): ?string {
$stmt = $pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
$stmt->execute();
$row = $stmt->fetch();
return $row['Column_name'] ?? null;
}
// --- Neue, reguläre Funktionen (ersetzen Closures) ---
function requireAuth(): array {
// Muss auf globale $_SESSION zugreifen
if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401);
return $_SESSION['auth'];
}
function pullId(array $src) {
$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;
}
function tenantWhere(array $session): array {
// Muss auf globale $conf zugreifen, um $tenantCol und $mapSess zu erhalten
global $conf;
$multi = $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]];
}
function tenantAssign(array $session, array $columns): array {
// Muss auf globale $conf zugreifen
global $conf;
$multi = $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];
}
function resolveIdCol(string $kind): array {
// Muss auf globale $conf, $pdo, und $tableMap zugreifen
global $conf, $pdo, $tableMap;
$t = $tableMap[$kind];
$cfg = $conf['columns'][$kind] ?? [];
$cols = table_columns($pdo, $t);
$idCol = $cfg['id'] ?? (in_array('id', $cols, true) ? 'id' : primary_key($pdo, $t));
if (!$idCol) $idCol = 'id';
return [$idCol, $cols];
}
// --- Haupt-Setup-Logik (Setzt die globalen Variablen) ---
try {
// Deklariere alle Variablen, die im Router von api.php benötigt werden, als global
global $conf, $pdo, $in, $action, $tableMap;
// 1. Globale Konfiguration und CORS
$conf = load_config();
cors($conf);
// 2. Cookie-Parameter setzen
if (!empty($conf['auth']['cookie'])) {
$c = $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);
}
// 3. Input-Daten abrufen
$in = get_input();
// 4. Datenbankverbindung herstellen
$pdo = pdo_templates($conf);
// 5. Action / Resource auflösen
$action = val($in, 'action', '');
$resource = val($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;
}
// 6. Tabellenzuweisungen
$tables = $conf['tables'] ?? [];
$tableMap = [
'templates' => $tables['templates'] ?? 'emailtemplate_templates',
'sections' => $tables['sections'] ?? 'emailtemplate_sections',
'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks',
'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets',
];
} catch (Throwable $e) {
// Fehler während der Initialisierung abfangen
fail('Initialization error', get_class($e).': '.$e->getMessage(), 500);
}

View File

@@ -0,0 +1,36 @@
<?php
return [
'templates' => [
'db_host' => '127.0.0.1',
'db_port' => 3306,
'db_name' => 'YOUR_DB_NAME',
'db_user' => 'YOUR_DB_USER',
'db_pass' => 'YOUR_DB_PASS',
'db_charset' => 'utf8mb4',
'prefix' => 'emailtemplate_',
],
'project' => [
'db_host' => '127.0.0.1',
'db_port' => 3306,
'db_name' => 'YOUR_PROJECT_DB',
'db_user' => 'YOUR_PROJECT_USER',
'db_pass' => 'YOUR_PROJECT_PASS',
'db_charset' => 'utf8mb4',
],
// SMTP / Testversand
'smtp' => [
'host' => getenv('SMTP_HOST') ?: '',
'port' => getenv('SMTP_PORT') ?: 587,
'user' => getenv('SMTP_USER') ?: '',
'pass' => getenv('SMTP_PASS') ?: '',
'secure' => getenv('SMTP_SECURE') ?: 'tls',
'from_email' => getenv('SMTP_FROM_EMAIL') ?: 'no-reply@example.com',
'from_name' => getenv('SMTP_FROM_NAME') ?: 'EmailTemplate',
],
// Export-API: statische API-Keys
'export' => [
'api_keys' => explode(',', getenv('EXPORT_API_KEYS') ?: 'dev-key-123'),
],
];

259
inc/api_kernel_log.txt Normal file
View File

@@ -0,0 +1,259 @@
[2025-10-31 01:21:54] --- Get::blocks - Raw JSON from DB ---
Array
(
[topContent] => {"dataSources":[],"assets":[],"styles":[{"selectors":[],"selectorsAdd":"*","style":{"box-sizing":"border-box"}},{"selectors":[],"selectorsAdd":"body","style":{"margin-top":"0px","margin-right":"0px","margin-bottom":"0px","margin-left":"0px"}}],"pages":[{"frames":[{"component":{"type":"wrapper","stylable":["background","background-color","background-image","background-repeat","background-attachment","background-position","background-size"],"components":[{"type":"library-reference","content":"Alles Neu macht der Mai","lib-kind":"snippets","lib-id":1}],"head":{"type":"head"},"docEl":{"tagName":"html"}},"id":"P4uy9DBKbT5yTO4c"}],"type":"main","id":"dJ5hyxgFUxsCWgbi"}],"symbols":[]}
)
[2025-10-31 01:21:54] --- Get::blocks - Decoded JSON ---
Array
(
[decodedContent] => Array
(
[dataSources] => Array
(
)
[assets] => Array
(
)
[styles] => Array
(
[0] => Array
(
[selectors] => Array
(
)
[selectorsAdd] => *
[style] => Array
(
[box-sizing] => border-box
)
)
[1] => Array
(
[selectors] => Array
(
)
[selectorsAdd] => body
[style] => Array
(
[margin-top] => 0px
[margin-right] => 0px
[margin-bottom] => 0px
[margin-left] => 0px
)
)
)
[pages] => Array
(
[0] => Array
(
[frames] => Array
(
[0] => Array
(
[component] => Array
(
[type] => wrapper
[stylable] => Array
(
[0] => background
[1] => background-color
[2] => background-image
[3] => background-repeat
[4] => background-attachment
[5] => background-position
[6] => background-size
)
[components] => Array
(
[0] => Array
(
[type] => library-reference
[content] => Alles Neu macht der Mai
[lib-kind] => snippets
[lib-id] => 1
)
)
[head] => Array
(
[type] => head
)
[docEl] => Array
(
[tagName] => html
)
)
[id] => P4uy9DBKbT5yTO4c
)
)
[type] => main
[id] => dJ5hyxgFUxsCWgbi
)
)
[symbols] => Array
(
)
)
)
[2025-10-31 01:21:54] --- Get::blocks - Final Gjs Components ---
Array
(
[count] => 5
[first_component] => N/A
)
[2025-10-31 01:21:54] --- Get::blocks - Raw JSON from DB ---
Array
(
[topContent] => {"dataSources":[],"assets":[],"styles":[{"selectors":[],"selectorsAdd":"*","style":{"box-sizing":"border-box"}},{"selectors":[],"selectorsAdd":"body","style":{"margin-top":"0px","margin-right":"0px","margin-bottom":"0px","margin-left":"0px"}}],"pages":[{"frames":[{"component":{"type":"wrapper","stylable":["background","background-color","background-image","background-repeat","background-attachment","background-position","background-size"],"components":[{"type":"library-reference","content":"Alles Neu macht der Mai","lib-kind":"snippets","lib-id":1}],"head":{"type":"head"},"docEl":{"tagName":"html"}},"id":"P4uy9DBKbT5yTO4c"}],"type":"main","id":"dJ5hyxgFUxsCWgbi"}],"symbols":[]}
)
[2025-10-31 01:21:54] --- Get::blocks - Decoded JSON ---
Array
(
[decodedContent] => Array
(
[dataSources] => Array
(
)
[assets] => Array
(
)
[styles] => Array
(
[0] => Array
(
[selectors] => Array
(
)
[selectorsAdd] => *
[style] => Array
(
[box-sizing] => border-box
)
)
[1] => Array
(
[selectors] => Array
(
)
[selectorsAdd] => body
[style] => Array
(
[margin-top] => 0px
[margin-right] => 0px
[margin-bottom] => 0px
[margin-left] => 0px
)
)
)
[pages] => Array
(
[0] => Array
(
[frames] => Array
(
[0] => Array
(
[component] => Array
(
[type] => wrapper
[stylable] => Array
(
[0] => background
[1] => background-color
[2] => background-image
[3] => background-repeat
[4] => background-attachment
[5] => background-position
[6] => background-size
)
[components] => Array
(
[0] => Array
(
[type] => library-reference
[content] => Alles Neu macht der Mai
[lib-kind] => snippets
[lib-id] => 1
)
)
[head] => Array
(
[type] => head
)
[docEl] => Array
(
[tagName] => html
)
)
[id] => P4uy9DBKbT5yTO4c
)
)
[type] => main
[id] => dJ5hyxgFUxsCWgbi
)
)
[symbols] => Array
(
)
)
)
[2025-10-31 01:21:54] --- Get::blocks - Final Gjs Components ---
Array
(
[count] => 5
[first_component] => N/A
)

76
inc/config.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
return [
'templates' => [
'db_host' => getenv('DB_TPL_HOST') ?: 'localhost',
'db_name' => getenv('DB_TPL_NAME') ?: 'd044ae9e',
'db_user' => getenv('DB_TPL_USER') ?: 'd044ae9e',
'db_pass' => getenv('DB_TPL_PASS') ?: '9BVUn)Töcü@ÖVÜfgO8!J',
'db_charset' => 'utf8',
'prefix' => getenv('DB_TPL_PREFIX') ?: 'emailtemplate_',
],
'project' => [
'db_host' => getenv('DB_TPL_HOST') ?: 'w0207fd0.kasserver.com',
'db_name' => getenv('DB_TPL_NAME') ?: 'd0444c25',
'db_user' => getenv('DB_TPL_USER') ?: 'd0444c25',
'db_pass' => getenv('DB_TPL_PASS') ?: '/7ü9+§ÄfkiQvGPr§2Op7',
'db_charset' => 'utf8',
],
'cors' => getenv('CORS_ORIGIN') ?: '*',
'env' => 'staging',
'base_url' => 'https://staging.emailtemplate.it',
'auth' => [
'session_name' => 'et_session',
'cookie_domain' => 'staging.emailtemplate.it',
'cookie_secure' => true,
'cookie_httponly'=> true,
'cookie_samesite'=> 'Lax',
'db' => [
'table' => 'customer_users',
'col_user' => 'email', // alternativ: 'username'
'col_pass' => 'password_hash',
'col_name' => 'name', // optional
'col_id' => 'id', // optional
'col_status' => 'is_active', // optional
'active_values'=> ['active','1',1], // optional
'legacy' => 'md5' // optional: 'md5' | 'sha1' | 'plain' (sonst bcrypt/argon2)
],
],
'smtp' => [
'host' => 'smtp.example.com',
'port' => 587,
'user' => 'smtp-user',
'pass' => 'smtp-pass',
'secure' => 'tls', // oder 'ssl'
'from_email' => 'no-reply@example.com',
'from_name' => 'EmailTemplate',
],
'export' => [
'api_keys' => ['dev-key-123', 'noch-ein-key'], // füge hier deine Keys ein
],
'multi' => [
// Spalte in ALLEN Content-Tabellen, die dem Besitzer/Mandanten entspricht:
'tenant_col' => 'customer_id', // <— falls es bei dir z. B. 'owner_id' heißt: entsprechend anpassen.
// Welche Session-Info darauf gemappt wird:
'map_session_to' => 'id', // 'id' (Default) | 'email' | 'name'
],
// optional: abweichende Tabellennamen/Spalten:
'tables' => [
'templates' => 'emailtemplate_templates',
'sections' => 'emailtemplate_sections',
'blocks' => 'emailtemplate_blocks',
'snippets' => 'emailtemplate_snippets',
],
'columns' => [
// Nur anpassen, wenn deine Spaltennamen abweichen
'templates' => ['id'=>'id','name'=>'name','desc'=>null,'cat'=>null,'upd'=>'updated_at'],
'sections' => ['id'=>'id','name'=>'name','cat'=>null,'upd'=>'updated_at'],
'blocks' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
'snippets' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
],
];