This commit is contained in:
2025-12-08 02:19:23 +01:00
parent 4d23eb8223
commit 0daf69d159
4 changed files with 0 additions and 2037 deletions

View File

@@ -1,525 +0,0 @@
<?php
declare(strict_types=1);
// =================================================================
// 🚨 KRITISCHE STARTSEQUENZ 🚨
// Diese Blöcke MÜSSEN VOR jeder Ausgabe erfolgen.
// -----------------------------------------------------------------
// 1. Composer Autoload (Sie haben dies selbst eingebunden)
// ACHTUNG: Der Pfad wurde auf das Standard-Vendor-Verzeichnis korrigiert: 'vendor'
$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();
}
header('Content-Type: application/json; charset=utf-8');
/* ===================== Utilities ===================== */
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);
}
function load_config(): array {
$paths = [
__DIR__ . '/../inc/config.php',
__DIR__ . '/inc/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;
}
/**
* Inline-Styling für E-Mail-Templates (nutzt TijsVerkoyen/CssToInlineStyles)
*
* Diese Funktion steht nur bereit, wird aber noch nicht verwendet.
* @param string $html
* @param string|null $css
* @return string
*/
function inline_css(string $html, ?string $css = null): string {
// 1. Klasse existiert nicht
if (!class_exists('\TijsVerkoyen\CssToInlineStyles\CssToInlineStyles')) {
return $html;
}
// 2. Direkte Instanziierung und Aufruf in einer einzigen Zeile
$result = (new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles($html, $css))->convert();
return $result;
}
/* ===================== DB helpers ===================== */
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 ($legacy === 'plain')return hash_equals($stored, $input);
if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored);
return hash_equals($stored, $input);
}
/* ============ schema helpers ============ */
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;
}
function first_existing(array $columns, array $candidates): ?string {
foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c;
return null;
}
/* ===================== Boot ===================== */
try {
$conf = load_config();
cors($conf);
// if (session_status() === PHP_SESSION_NONE) session_start();
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);
}
$in = get_input();
/* ---- Compat: ?resource=blocks&action=list -> blocks.list ---- */
$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;
}
/* ---- Multi-tenant ---- */
$multi = $conf['multi'] ?? [];
$tenantCol = $multi['tenant_col'] ?? null;
$mapSess = $multi['map_session_to'] ?? 'id'; // 'id'|'email'|'name'
$tenantWhere = function(array $session) use ($tenantCol, $mapSess) {
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]];
};
$tenantAssign = function(array $session, array $columns) use ($tenantCol, $mapSess) {
if (!$tenantCol || !in_array($tenantCol, $columns, true)) return [];
$val = $session[$mapSess] ?? null;
return ($val===null || $val==='') ? [] : [$tenantCol => $val];
};
$requireAuth = function() {
if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401);
return $_SESSION['auth'];
};
/* ---- config tables/columns ---- */
$tables = $conf['tables'] ?? [];
$tableMap = [
'templates' => $tables['templates'] ?? 'emailtemplate_templates',
'sections' => $tables['sections'] ?? 'emailtemplate_sections',
'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks',
'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets',
];
$colsDefault = [
'id' => 'id',
'name' => 'name',
'desc' => 'description',
'cat' => 'category',
'upd' => 'updated_at',
];
$pdo = pdo_templates($conf);
/* helper: resolve id column for a table */
$resolveIdCol = function(string $kind) use ($conf, $colsDefault, $tableMap, $pdo) {
$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'; // fallback
return [$idCol, $cols];
};
/* helper: accept many id aliases */
$pullId = function(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;
};
/* ===================== Router ===================== */
switch ($action) {
case 'health':
respond(['ok'=>true,'time'=>date('c')]);
/* ---------- AUTH ---------- */
case 'auth.login': {
$identifier = trim((string)val($in, ['username','user','email','login'], ''));
$password = (string)val($in, ['password','pass','pwd'], '');
if ($identifier === '' || $password === '') fail('username/password required', null, 422);
$authDb = $conf['auth']['db'] ?? [];
$table = $authDb['table'] ?? 'emailtemplate_users';
$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];
$stmt = $pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1");
$stmt->execute([':u'=>$identifier]);
$row = $stmt->fetch();
if (!$row) fail('Invalid credentials', null, 401);
if ($colStatus && isset($row[$colStatus])) {
if (!in_array($row[$colStatus], $activeValues, true)) {
fail('Account inactive', null, 403);
}
}
$stored = (string)($row[$colPass] ?? '');
if ($stored === '' || !verify_password($password, $stored, $authDb)) {
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));
respond(['ok'=>true,'user'=>$_SESSION['auth'],'token'=>$token]);
}
case 'auth.me':
if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401);
respond(['ok'=>true,'user'=>$_SESSION['auth']]);
case 'auth.logout':
$_SESSION = [];
if (session_id() !== '') session_destroy();
respond(['ok'=>true]);
/* ---------- LIST (mit Parent-Filtern) ---------- */
case 'templates.list':
case 'sections.list':
case 'blocks.list':
case 'snippets.list': {
$auth = $requireAuth();
$kind = explode('.', $action)[0];
$t = $tableMap[$kind];
[$idCol, $allCols] = $resolveIdCol($kind);
$cfg = $conf['columns'][$kind] ?? [];
$nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol);
$descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']);
$catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']);
$updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']);
$q = trim((string)val($in,'q',''));
$limit = max(1, (int)val($in,'limit', 500));
$offset = max(0, (int)val($in,'offset',0));
$where = ' WHERE 1=1 ';
$params = [];
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.'%';
}
// Parent-Filtern (falls Spalten existieren)
$parentFilters = [
'template_id' => val($in, ['template_id','tpl_id'], null),
'section_id' => val($in, ['section_id','sec_id'], null),
'block_id' => val($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; }
}
// Mandant
[$tw,$tp] = $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 = $pdo->prepare($sql);
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];
$out[] = $item;
}
respond(['ok'=>true,'kind'=>$kind,'items'=>$out,'data'=>$out,'count'=>count($out),'offset'=>$offset,'limit'=>$limit]);
}
/* ---------- GET (JETZT mit top-level html/content) ---------- */
case 'templates.get':
case 'sections.get':
case 'blocks.get':
case 'snippets.get': {
$auth = $requireAuth();
$kind = explode('.', $action)[0];
$t = $tableMap[$kind];
[$idCol, $allCols] = $resolveIdCol($kind);
$id = $pullId($in);
if ($id === null || $id === '') fail('id required', null, 422);
[$tw,$tp] = $tenantWhere($auth);
$sql = "SELECT * FROM `$t` WHERE `$idCol` = :id".$tw." LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id);
foreach ($tp as $k=>$v) $stmt->bindValue($k,$v);
$stmt->execute();
$row = $stmt->fetch();
if (!$row) fail('Not found', ['kind'=>$kind,'id'=>$id], 404);
$rowOut = ['id' => $row[$idCol] ?? $id] + $row;
// NEU: Spalten für HTML/JSON erkennen und top-level ausgeben
$htmlCol = first_existing($allCols, ['html','body','markup']);
$jsonCol = first_existing($allCols, ['content_json','json','content','structure_json']);
$topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null;
$topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null;
respond([
'ok'=>true,
'kind'=>$kind,
'id'=>$rowOut['id'],
'item'=>$rowOut,
'data'=>$rowOut,
'html'=>$topHtml, // <— wichtig für Vorschau
'content'=>$topContent // optional (z. B. GrapesJS JSON)
]);
}
/* ---------- CREATE ---------- */
case 'templates.create':
case 'sections.create':
case 'blocks.create':
case 'snippets.create': {
$auth = $requireAuth();
$kind = explode('.', $action)[0];
$t = $tableMap[$kind];
[$idCol, $allCols] = $resolveIdCol($kind);
$cfg = $conf['columns'][$kind] ?? [];
$nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol);
$descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']);
$catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']);
$updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']);
$name = trim((string)val($in, ['name','title'], ''));
if ($name === '') fail('name required', null, 422);
$desc = (string)val($in, ['description','desc'], null);
$cat = (string)val($in, ['category','cat'], null);
$html = (string)val($in, ['html','body','markup'], null);
$json = val($in, ['content_json','json','content','structure_json'], null);
$settings = val($in, ['settings_json','settings'], null);
$templateId = val($in, ['template_id','tpl_id'], null);
$sectionId = val($in, ['section_id','sec_id'], null);
$blockId = val($in, ['block_id','blk_id'], null);
$data = [ $nameCol => $name ];
if ($desc !== null && $descCol) $data[$descCol] = $desc;
if ($cat !== null && $catCol) $data[$catCol] = $cat;
$c = first_existing($allCols, ['html','body','markup']); if ($c && $html !== null) $data[$c]=$html;
$c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($c && $json !== null) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$c = first_existing($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 + $tenantAssign($_SESSION['auth'] ?? [], $allCols);
$now = date('Y-m-d H:i:s');
$createdCol = first_existing($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 = $pdo->prepare($sql);
foreach ($data as $k=>$v) $stmt->bindValue(":$k", $v);
$stmt->execute();
$newId = $pdo->lastInsertId();
$out = ['id'=>$newId,'name'=>$name];
if ($desc !== null) $out['desc']=$desc;
if ($cat !== null) $out['category']=$cat;
respond(['ok'=>true,'kind'=>$kind,'id'=>$newId,'item'=>$out,'data'=>$out]);
}
/* ---------- UPDATE ---------- */
case 'templates.update':
case 'sections.update':
case 'blocks.update':
case 'snippets.update': {
$auth = $requireAuth();
$kind = explode('.', $action)[0];
$t = $tableMap[$kind];
[$idCol, $allCols] = $resolveIdCol($kind);
$cfg = $conf['columns'][$kind] ?? [];
$nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol);
$descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']);
$catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']);
$updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']);
$id = $pullId($in);
if ($id === null || $id === '') fail('id required', null, 422);
$data = [];
$name = val($in, ['name','title'], null);
$desc = val($in, ['description','desc'], null);
$cat = val($in, ['category','cat'], null);
$html = val($in, ['html','body','markup'], null);
$json = val($in, ['content_json','json','content','structure_json'], null);
$settings = val($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;
$c = first_existing($allCols, ['html','body','markup']); if ($html !== null && $c) $data[$c]=(string)$html;
$c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($json !== null && $c) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$c = first_existing($allCols, ['settings_json','settings']); if ($settings !== null && $c) $data[$c]= is_string($settings)?$settings:json_encode($settings, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
$tpl = val($in, ['template_id','tpl_id'], null); if ($tpl !== null && in_array('template_id',$allCols,true)) $data['template_id']=$tpl;
$sec = val($in, ['section_id','sec_id'], null); if ($sec !== null && in_array('section_id',$allCols,true)) $data['section_id']=$sec;
$blk = val($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) fail('nothing to update', null, 422);
[$tw,$tp] = $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 = $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();
respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'updated'=>array_keys($data)]);
}
/* ---------- DELETE ---------- */
case 'templates.delete':
case 'sections.delete':
case 'blocks.delete':
case 'snippets.delete': {
$auth = $requireAuth();
$kind = explode('.', $action)[0];
$t = $tableMap[$kind];
[$idCol, $allCols] = $resolveIdCol($kind);
$id = $pullId($in);
if ($id === null || $id === '') fail('id required', null, 422);
[$tw,$tp] = $tenantWhere($auth);
$sql = "DELETE FROM `$t` WHERE `$idCol` = :__id".$tw." LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':__id', $id);
foreach ($tp as $k=>$v) $stmt->bindValue($k,$v);
$stmt->execute();
respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'deleted'=>true]);
}
/* ---------- Platzhalter für kommende Features ---------- */
case 'render.preview':
fail('Not implemented', 'Preview wird im Feature-Patch geliefert', 501);
case 'templates.test_send':
fail('Not implemented', 'Testversand wird im Feature-Patch geliefert', 501);
case 'export.render':
fail('Not implemented', 'Export-API wird im Feature-Patch geliefert', 501);
default:
fail('Unknown action', $action ?: 'missing', 404);
}
} catch (Throwable $e) {
fail('Server error', get_class($e).': '.$e->getMessage(), 500);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,492 +0,0 @@
<?php
// api.php — Multi-User API mit Legacy-Kompatibilität
// Stand: 2025-09-05
declare(strict_types=1);
header('X-API-Version: 2025-09-05');
/* ------------------------- Fehler als JSON ------------------------- */
set_error_handler(static function ($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(static function (Throwable $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'ok' => false,
'error' => 'internal',
'type' => get_class($e),
'msg' => $e->getMessage(),
'file' => basename($e->getFile()),
'line' => $e->getLine(),
], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
exit;
});
/* ------------------------- Helpers ------------------------- */
function json_out(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
exit;
}
function in_json(): array {
$raw = file_get_contents('php://input') ?: '';
if ($raw === '') return [];
$j = json_decode($raw, true);
return is_array($j) ? $j : [];
}
/* ------------------------- Config laden ------------------------- */
// Parent /inc/config.php (wie von dir beschrieben)
$cfgFile = dirname(__DIR__) . '/inc/config.php';
if (!is_file($cfgFile)) {
json_out(['ok'=>false,'error'=>'config_missing','hint'=>'config.php nicht gefunden','path'=>$cfgFile], 500);
}
$CFG = include $cfgFile;
if (!is_array($CFG)) {
json_out(['ok'=>false,'error'=>'config_invalid','hint'=>'config.php muss ein Array zurückgeben'], 500);
}
$ENV = $CFG['env'] ?? 'prod';
/* ------------------------- PDO-Factories ------------------------- */
function pdo_from_cfg(?array $dbc): ?PDO {
if (!$dbc) return null;
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$dbc['db_host'] ?? 'localhost',
$dbc['db_name'] ?? '',
$dbc['db_charset']?? 'utf8mb4'
);
return new PDO($dsn, $dbc['db_user'] ?? '', $dbc['db_pass'] ?? '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
$pdoTpl = pdo_from_cfg($CFG['templates'] ?? null); // Template-Daten
$pdoCust = pdo_from_cfg(($CFG['customers'] ?? null) ?: ($CFG['templates'] ?? null)); // Kunden/Users
$TPL_DB = $CFG['templates']['db_name'] ?? null; // für information_schema
function has_column(PDO $pdo, ?string $db, string $table, string $col): bool {
if (!$db) return false;
$st = $pdo->prepare("SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=:db AND TABLE_NAME=:t AND COLUMN_NAME=:c
LIMIT 1");
$st->execute([':db'=>$db, ':t'=>$table, ':c'=>$col]);
return (bool)$st->fetchColumn();
}
/* ------------------------- Auth (Helper oder Fallback) ------------------------- */
$authFile = dirname(__DIR__) . '/inc/auth_helpers.php';
$useHelpers = is_file($authFile);
if ($useHelpers) {
require_once $authFile; // stellt auth_start_session(), auth_require(), auth_logout(), require_role() bereit
} else {
// interner Fallback kompatibel zu deinen Erwartungen
function auth_start_session(array $CFG): void {
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax',
'secure' => $secure,
'path' => rtrim(dirname($_SERVER['SCRIPT_NAME']),'/').'/'
]);
session_name('et_session');
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
}
function auth_require(array $CFG): void {
auth_start_session($CFG);
if (empty($_SESSION['user'])) {
json_out(['ok'=>false,'error'=>'unauthorized'], 401);
}
}
function auth_logout(array $CFG): void {
auth_start_session($CFG);
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time()-42000, $p['path'], $p['domain'] ?? '', $p['secure'], $p['httponly']);
}
session_destroy();
}
function require_role(array $CFG, array $roles): void {
auth_start_session($CFG);
$r = $_SESSION['user']['role'] ?? null;
if (!$r || !in_array($r, $roles, true)) json_out(['ok'=>false,'error'=>'forbidden'], 403);
}
}
/* ------------------------- Routing + Legacy-Mapping ------------------------- */
$action = $_GET['action'] ?? $_POST['action'] ?? null;
// Alt: ?resource=blocks&action=list|get|create|update|delete|sync
if (!empty($_GET['resource'])) {
$res = (string)$_GET['resource'];
$act = (string)($_GET['action'] ?? '');
$allowed = ['templates','sections','blocks','snippets','assets','template_items','section_items'];
if (in_array($res, $allowed, true)) {
if ($act === 'list') $action = $res.'.list';
if ($act === 'get') $action = $res.'.get';
if ($act === 'create') $action = $res.'.create';
if ($act === 'update') $action = $res.'.update';
if ($act === 'delete') $action = $res.'.delete';
if ($act === 'sync') $action = $res.'.sync';
}
}
/* ------------------------- Meta/Health ------------------------- */
if ($action === 'health' || $action === 'ping') json_out(['ok'=>true,'env'=>$ENV,'time'=>date('c')]);
if ($action === 'version') json_out(['ok'=>true,'version'=>'2025-09-05','env'=>$ENV]);
/* ------------------------- Diagnose (leichtgewichtig) ------------------------- */
if ($action === 'debug.diag') {
$diag = [
'php' => PHP_VERSION,
'pdo' => extension_loaded('pdo'),
'pdo_mysql'=> extension_loaded('pdo_mysql'),
'cfg' => ['templates'=>!!$pdoTpl, 'customers'=>!!$pdoCust],
];
json_out(['ok'=>true,'diag'=>$diag]);
}
/* ------------------------- STAGING: User-Debug ------------------------- */
if (in_array($action, ['debug.users','debug.users.check','debug.users.setpass','debug.users.peek'], true)) {
if ($ENV !== 'staging') json_out(['ok'=>false,'error'=>'forbidden'], 403);
if (!$pdoCust) json_out(['ok'=>false,'error'=>'customers_db_not_configured'], 500);
if ($action === 'debug.users') {
$email = isset($_GET['email']) ? trim((string)$_GET['email']) : '';
if ($email !== '') {
$st = $pdoCust->prepare("SELECT id, customer_id, email, role, is_active, created_at, updated_at
FROM customer_users WHERE email=:email");
$st->execute([':email'=>$email]);
$rows = $st->fetchAll();
} else {
$st = $pdoCust->query("SELECT id, customer_id, email, role, is_active, created_at, updated_at
FROM customer_users ORDER BY id DESC LIMIT 50");
$rows = $st->fetchAll();
}
json_out(['ok'=>true,'items'=>$rows]);
}
if ($action === 'debug.users.check') {
$in = in_json();
$email = trim((string)($in['email'] ?? ''));
$pass = (string)($in['password'] ?? '');
if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_params'], 400);
$st = $pdoCust->prepare("SELECT id, customer_id, email, password_hash, role, is_active FROM customer_users
WHERE email=:email LIMIT 1");
$st->execute([':email'=>$email]);
$u = $st->fetch();
if (!$u) json_out(['ok'=>true,'exists'=>false,'password_match'=>false]);
json_out(['ok'=>true,'exists'=>true,'password_match'=>password_verify($pass,$u['password_hash'])]);
}
if ($action === 'debug.users.setpass') {
$in = in_json();
$email = trim((string)($in['email'] ?? ''));
$pass = (string)($in['password'] ?? '');
if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_params'], 400);
$st = $pdoCust->prepare("SELECT id FROM customer_users WHERE email=:email LIMIT 1");
$st->execute([':email'=>$email]);
$u = $st->fetch();
if (!$u) json_out(['ok'=>false,'error'=>'user_not_found'], 404);
$hash = password_hash($pass, PASSWORD_DEFAULT);
$upd = $pdoCust->prepare("UPDATE customer_users SET password_hash=:h, is_active=1 WHERE id=:id");
$upd->execute([':h'=>$hash, ':id'=>$u['id']]);
json_out(['ok'=>true,'set'=>true]);
}
if ($action === 'debug.users.peek') {
$email = isset($_GET['email']) ? trim((string)$_GET['email']) : '';
if ($email==='') json_out(['ok'=>false,'error'=>'missing_email'], 400);
$st = $pdoCust->prepare("SELECT id, customer_id, email, LENGTH(password_hash) len
FROM customer_users WHERE email=:email");
$st->execute([':email'=>$email]);
json_out(['ok'=>true,'user'=>$st->fetch()]);
}
}
/* ------------------------- AUTH: Login / Logout / Me ------------------------- */
if ($action === 'auth.login') {
$in = in_json();
$email = trim(strtolower((string)($in['email'] ?? '')));
$pass = (string)($in['password'] ?? '');
if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_credentials'], 400);
if (!$pdoCust) json_out(['ok'=>false,'error'=>'customers_db_not_configured'], 500);
// Mehrfachkunden mit gleicher Mail erlauben → best match über password_verify
$st = $pdoCust->prepare("SELECT cu.id, cu.customer_id, cu.email, cu.password_hash, cu.role, cu.is_active,
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");
$st->execute([':email'=>$email]);
$rows = $st->fetchAll();
$match = null;
foreach ($rows as $r) {
if ((int)$r['is_active'] === 1 && !empty($r['password_hash']) && password_verify($pass, $r['password_hash'])) {
$match = $r; break;
}
}
if (!$match) json_out(['ok'=>false,'error'=>'invalid_credentials'], 401);
if (($match['status'] ?? 'active') !== 'active') json_out(['ok'=>false,'error'=>'customer_inactive'], 403);
auth_start_session($CFG);
$_SESSION['user'] = [
'id' => (int)$match['id'],
'email' => $match['email'],
'role' => $match['role'],
'customer_id' => (int)$match['customer_id'],
'customer_slug' => $match['customer_slug'],
'plan' => $match['plan'] ?? null,
];
json_out(['ok'=>true,'user'=>$_SESSION['user']]);
}
if ($action === 'auth.logout') { auth_logout($CFG); json_out(['ok'=>true]); }
if ($action === 'auth.me') {
auth_start_session($CFG);
json_out(['ok'=>!empty($_SESSION['user']), 'user'=>$_SESSION['user'] ?? null]);
}
/* ------------------------- ab hier: geschützt ------------------------- */
$public = ['auth.login','auth.logout','auth.me','health','ping','version','debug.diag','debug.users','debug.users.check','debug.users.setpass','debug.users.peek'];
if (!in_array($action, $public, true)) auth_require($CFG);
$customerId = (int)($_SESSION['user']['customer_id'] ?? 0);
/* ------------------------- Templates ------------------------- */
if ($action === 'templates.list') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$st = $pdoTpl->prepare("SELECT id, name, updated_at
FROM emailtemplate_templates
WHERE customer_id = :cid
ORDER BY updated_at DESC, id DESC
LIMIT 1000");
$st->execute([':cid'=>$customerId]);
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
}
if ($action === 'templates.get') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html');
$cols = $hasHtml ? "id, customer_id, name, html, updated_at" : "id, customer_id, name, NULL AS html, updated_at";
$st = $pdoTpl->prepare("SELECT $cols FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid LIMIT 1");
$st->execute([':id'=>$id, ':cid'=>$customerId]);
$row = $st->fetch();
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
json_out(['ok'=>true,'item'=>$row]);
}
if ($action === 'templates.create') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in = in_json();
$name = trim((string)($in['name'] ?? ''));
$html = (string)($in['html'] ?? '');
if ($name==='') json_out(['ok'=>false,'error'=>'name_required'], 400);
$hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html');
if ($hasHtml) {
$st = $pdoTpl->prepare("INSERT INTO emailtemplate_templates (customer_id,name,html,created_at,updated_at)
VALUES (:cid,:name,:html,NOW(),NOW())");
$st->execute([':cid'=>$customerId, ':name'=>$name, ':html'=>$html]);
} else {
$st = $pdoTpl->prepare("INSERT INTO emailtemplate_templates (customer_id,name,created_at,updated_at)
VALUES (:cid,:name,NOW(),NOW())");
$st->execute([':cid'=>$customerId, ':name'=>$name]);
}
json_out(['ok'=>true,'id'=>(int)$pdoTpl->lastInsertId()]);
}
if ($action === 'templates.update') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in = in_json();
$id = (int)($in['id'] ?? 0);
$name = array_key_exists('name',$in) ? trim((string)$in['name']) : null;
$html = array_key_exists('html',$in) ? (string)$in['html'] : null;
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html');
$sets=[]; $p=[':id'=>$id, ':cid'=>$customerId];
if ($name!==null) { $sets[]="name=:name"; $p[':name']=$name; }
if ($hasHtml && $html!==null) { $sets[]="html=:html"; $p[':html']=$html; }
if (!$sets) json_out(['ok'=>false,'error'=>'nothing_to_update'], 400);
$sql = "UPDATE emailtemplate_templates SET ".implode(',',$sets).", updated_at=NOW() WHERE id=:id AND customer_id=:cid";
$st = $pdoTpl->prepare($sql);
$st->execute($p);
json_out(['ok'=>true,'updated'=>$st->rowCount()]);
}
if ($action === 'templates.delete') {
require_role($CFG, ['owner','admin']);
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in = in_json(); $id=(int)($in['id'] ?? 0);
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$pdoTpl->beginTransaction();
try {
$pdoTpl->prepare("DELETE FROM emailtemplate_template_items WHERE template_id=:id AND customer_id=:cid")->execute([':id'=>$id, ':cid'=>$customerId]);
$pdoTpl->prepare("DELETE FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid")->execute([':id'=>$id, ':cid'=>$customerId]);
$pdoTpl->commit();
json_out(['ok'=>true]);
} catch (Throwable $e) { $pdoTpl->rollBack(); throw $e; }
}
/* ------------------------- Sections ------------------------- */
if ($action === 'sections.list') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$templateId = isset($_GET['template_id']) ? (int)$_GET['template_id'] : 0;
if ($templateId>0) {
$st = $pdoTpl->prepare("SELECT id, template_id, name, z_index, type, updated_at
FROM emailtemplate_sections
WHERE customer_id=:cid AND template_id=:tid
ORDER BY z_index ASC, id ASC
LIMIT 1000");
$st->execute([':cid'=>$customerId, ':tid'=>$templateId]);
} else {
$st = $pdoTpl->prepare("SELECT id, template_id, name, z_index, type, updated_at
FROM emailtemplate_sections
WHERE customer_id=:cid
ORDER BY template_id ASC, z_index ASC, id ASC
LIMIT 1000");
$st->execute([':cid'=>$customerId]);
}
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
}
if ($action === 'sections.get') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$st = $pdoTpl->prepare("SELECT id, customer_id, template_id, name, z_index, type, updated_at
FROM emailtemplate_sections
WHERE id=:id AND customer_id=:cid
LIMIT 1");
$st->execute([':id'=>$id, ':cid'=>$customerId]);
$row = $st->fetch();
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
json_out(['ok'=>true,'item'=>$row]);
}
/* ------------------------- Blocks ------------------------- */
if ($action === 'blocks.list') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$st = $pdoTpl->prepare("SELECT id, name, category, updated_at
FROM emailtemplate_blocks
WHERE customer_id=:cid
ORDER BY name ASC
LIMIT 1000");
$st->execute([':cid'=>$customerId]);
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
}
if ($action === 'blocks.get') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$st = $pdoTpl->prepare("SELECT id, customer_id, name, category, updated_at
FROM emailtemplate_blocks
WHERE id=:id AND customer_id=:cid
LIMIT 1");
$st->execute([':id'=>$id, ':cid'=>$customerId]);
$row = $st->fetch();
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
json_out(['ok'=>true,'item'=>$row]);
}
if ($action === 'blocks.create') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in=in_json(); $name=trim((string)($in['name']??'')); $cat=trim((string)($in['category']??''));
if ($name==='') json_out(['ok'=>false,'error'=>'name_required'], 400);
$st=$pdoTpl->prepare("INSERT INTO emailtemplate_blocks (customer_id,name,category,created_at,updated_at)
VALUES (:cid,:name,COALESCE(NULLIF(:cat,''),'Default'),NOW(),NOW())");
$st->execute([':cid'=>$customerId, ':name'=>$name, ':cat'=>$cat]);
json_out(['ok'=>true,'id'=>(int)$pdoTpl->lastInsertId()]);
}
if ($action === 'blocks.update') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in=in_json(); $id=(int)($in['id']??0); $name=trim((string)($in['name']??'')); $cat=trim((string)($in['category']??''));
if ($id<=0 || $name==='') json_out(['ok'=>false,'error'=>'invalid_params'], 400);
$st=$pdoTpl->prepare("UPDATE emailtemplate_blocks
SET name=:name, category=COALESCE(NULLIF(:cat,''),category), updated_at=NOW()
WHERE id=:id AND customer_id=:cid");
$st->execute([':name'=>$name, ':cat'=>$cat, ':id'=>$id, ':cid'=>$customerId]);
json_out(['ok'=>true,'updated'=>$st->rowCount()]);
}
if ($action === 'blocks.delete') {
require_role($CFG, ['owner','admin']);
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in=in_json(); $id=(int)($in['id']??0);
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$st=$pdoTpl->prepare("DELETE FROM emailtemplate_blocks WHERE id=:id AND customer_id=:cid");
$st->execute([':id'=>$id, ':cid'=>$customerId]);
json_out(['ok'=>true,'deleted'=>$st->rowCount()]);
}
/* ------------------------- Snippets ------------------------- */
if ($action === 'snippets.list') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$st=$pdoTpl->prepare("SELECT id, name, category, updated_at
FROM emailtemplate_snippets
WHERE customer_id=:cid
ORDER BY name ASC
LIMIT 1000");
$st->execute([':cid'=>$customerId]);
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
}
if ($action === 'snippets.get') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0);
if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400);
$st=$pdoTpl->prepare("SELECT id, customer_id, name, category, updated_at
FROM emailtemplate_snippets
WHERE id=:id AND customer_id=:cid
LIMIT 1");
$st->execute([':id'=>$id, ':cid'=>$customerId]);
$row=$st->fetch();
if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404);
json_out(['ok'=>true,'item'=>$row]);
}
/* ------------------------- Assets (READ) ------------------------- */
if ($action === 'assets.list') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$st=$pdoTpl->prepare("SELECT id, name, type, mime_type, size_bytes, public_url, updated_at
FROM emailtemplate_assets
WHERE customer_id=:cid
ORDER BY updated_at DESC, id DESC
LIMIT 200");
$st->execute([':cid'=>$customerId]);
json_out(['ok'=>true,'items'=>$st->fetchAll()]);
}
/* ------------------------- Editor-Referenzen (Placeholders) ------------------------- */
if ($action === 'template_items.sync') { json_out(['ok'=>true]); }
if ($action === 'section_items.sync') { json_out(['ok'=>true]); }
/* ------------------------- Render (Fallback) ------------------------- */
if ($action === 'render.preview') {
if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500);
$in = in_json();
$templateId = (int)($in['template_id'] ?? 0);
if ($templateId<=0) json_out(['ok'=>false,'error'=>'template_id_required'], 400);
$st = $pdoTpl->prepare("SELECT id, name FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid LIMIT 1");
$st->execute([':id'=>$templateId, ':cid'=>$customerId]);
$tpl = $st->fetch();
if (!$tpl) json_out(['ok'=>false,'error'=>'not_found'], 404);
$html = "<!-- preview {$tpl['name']} (#{$tpl['id']}) -->\n<div style=\"padding:16px;font:14px/1.4 system-ui\">Preview okay.</div>";
json_out(['ok'=>true, 'template'=>$tpl, 'html'=>$html]);
}
/* ------------------------- Fallback ------------------------- */
json_out(['ok'=>false,'error'=>'unknown_action','action'=>$action], 404);

View File

@@ -1,3 +0,0 @@
<?php
?>