This commit is contained in:
2026-01-21 22:44:32 +01:00
parent 107de0b7db
commit a09a07bcfc
7 changed files with 324 additions and 32 deletions

View File

@@ -372,6 +372,8 @@ class ApiKernel
'editor' => $this->firstExisting($cols, ['editor_type', 'editor']),
'craft' => $this->firstExisting($cols, ['craft_json', 'craft_content', 'craft_data']),
'settings' => $this->firstExisting($cols, ['settings_json', 'settings']),
'is_active' => $this->firstExisting($cols, ['is_active']),
'was_active' => $this->firstExisting($cols, ['was_active']),
];
}
@@ -380,12 +382,12 @@ class ApiKernel
return $this->tableExists($this->contentItemsTable()) && $this->tableExists($this->contentSectionsTable());
}
private function createContentVersion(array $current, array $itemCols, int $customerId, int $sectionId): void
private function createContentVersion(array $current, array $itemCols, int $customerId, int $sectionId): ?int
{
$table = $this->contentVersionsTable();
if (!$this->tableExists($table)) return;
if (!$this->tableExists($table)) return null;
$contentId = (int)($current['id'] ?? 0);
if ($contentId <= 0) return;
if ($contentId <= 0) return null;
$jsonCol = $itemCols['json'] ?? null;
$htmlCol = $itemCols['html'] ?? null;
@@ -426,6 +428,7 @@ class ApiKernel
$stmt = $this->pdo->prepare("INSERT INTO `$table` ($insertCols) VALUES ($placeholders)");
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
$stmt->execute();
$newId = (int)$this->pdo->lastInsertId();
$cleanup = $this->pdo->prepare(
"DELETE FROM `$table` WHERE `id` IN (
@@ -435,11 +438,32 @@ class ApiKernel
)"
);
$cleanup->execute([':cid' => $contentId]);
return $newId;
} catch (Throwable $e) {
// Versioning darf nicht das Speichern blockieren.
return null;
}
}
private function activateContentVersion(int $customerId, int $contentId, int $versionId): void
{
$table = $this->contentVersionsTable();
if (!$this->tableExists($table)) return;
$this->pdo->prepare("UPDATE `$table` SET `is_active` = 0 WHERE `customer_id` = :cid AND `content_id` = :content")
->execute([':cid' => $customerId, ':content' => $contentId]);
$this->pdo->prepare(
"UPDATE `$table` SET `is_active` = 1, `was_active` = 1 WHERE `customer_id` = :cid AND `content_id` = :content AND `id` = :id LIMIT 1"
)->execute([':cid' => $customerId, ':content' => $contentId, ':id' => $versionId]);
}
private function deactivateContentVersion(int $customerId, int $contentId): void
{
$table = $this->contentVersionsTable();
if (!$this->tableExists($table)) return;
$this->pdo->prepare("UPDATE `$table` SET `is_active` = 0 WHERE `customer_id` = :cid AND `content_id` = :content")
->execute([':cid' => $customerId, ':content' => $contentId]);
}
private function isLegacyContentKind(string $kind): bool
{
return in_array($kind, ['templates', 'sections', 'blocks', 'snippets'], true);
@@ -635,6 +659,7 @@ class ApiKernel
$itemsTable = $this->contentItemsTable();
$sectionsTable = $this->contentSectionsTable();
$versionsTable = $this->contentVersionsTable();
if (!$this->tableExists($itemsTable) || !$this->tableExists($sectionsTable)) {
$this->fail('Content tables not available', null, 500);
}
@@ -642,6 +667,10 @@ class ApiKernel
$catCol = $itemCols['category'];
$htmlCol = $itemCols['html'];
$jsonCol = $itemCols['json'];
$onlyActive = (int)$this->val($this->in, ['active_only', 'only_active', 'active'], 0) === 1;
$versionCols = $onlyActive && $this->tableExists($versionsTable)
? $this->resolveContentVersionColumns($versionsTable)
: null;
$section = $fixedSection ?: $this->resolveSectionFromInput($customerId);
$q = trim((string)$this->val($this->in, 'q', ''));
@@ -663,9 +692,20 @@ class ApiKernel
$params[':q'] = '%' . $q . '%';
}
$sql = "SELECT i.*, s.`name` AS section_name, s.`slug` AS section_slug, s.`position` AS section_position, s.`is_template` AS section_is_template
$join = '';
$select = "i.*, s.`name` AS section_name, s.`slug` AS section_slug, s.`position` AS section_position, s.`is_template` AS section_is_template";
if ($onlyActive && $versionCols) {
$join = " JOIN `$versionsTable` v ON v.`content_id` = i.`id` AND v.`is_active` = 1";
$select .= ", v.`id` AS active_version_id, v.`version_no` AS active_version_no, v.`is_active` AS version_is_active, v.`was_active` AS version_was_active";
if (!empty($versionCols['html'])) $select .= ", v.`{$versionCols['html']}` AS version_html";
if (!empty($versionCols['json'])) $select .= ", v.`{$versionCols['json']}` AS version_json";
if (!empty($versionCols['craft'])) $select .= ", v.`{$versionCols['craft']}` AS version_craft";
if (!empty($versionCols['editor'])) $select .= ", v.`{$versionCols['editor']}` AS version_editor";
}
$sql = "SELECT $select
FROM `$itemsTable` i
JOIN `$sectionsTable` s ON s.`id` = i.`section_id`
$join
$where
ORDER BY i.`updated_at` DESC, i.`id` DESC
LIMIT :off,:lim";
@@ -691,8 +731,16 @@ class ApiKernel
'updated_at' => $r['updated_at'] ?? null,
'created_at' => $r['created_at'] ?? null,
];
if ($htmlCol && array_key_exists($htmlCol, $r)) $item['html'] = (string)($r[$htmlCol] ?? '');
if ($jsonCol && array_key_exists($jsonCol, $r)) $item['content'] = $r[$jsonCol];
if ($onlyActive && $versionCols) {
if (!empty($r['active_version_id'])) $item['active_version_id'] = (int)$r['active_version_id'];
if (array_key_exists('version_html', $r)) $item['html'] = (string)($r['version_html'] ?? '');
if (array_key_exists('version_json', $r)) $item['content'] = $r['version_json'];
if (array_key_exists('version_craft', $r)) $item['craft_json'] = $r['version_craft'];
if (array_key_exists('version_editor', $r)) $item['editor_type'] = $r['version_editor'];
} else {
if ($htmlCol && array_key_exists($htmlCol, $r)) $item['html'] = (string)($r[$htmlCol] ?? '');
if ($jsonCol && array_key_exists($jsonCol, $r)) $item['content'] = $r[$jsonCol];
}
$out[] = $item;
}
@@ -857,12 +905,16 @@ class ApiKernel
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
$stmt->execute();
$newId = (int)$this->pdo->lastInsertId();
$activateVersion = (int)$this->val($this->in, ['activate_version', 'activate', 'set_active'], 0) === 1;
try {
$stmt = $this->pdo->prepare("SELECT * FROM `$itemsTable` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1");
$stmt->execute([':id' => $newId, ':cid' => $customerId]);
$row = $stmt->fetch();
if ($row) {
$this->createContentVersion($row, $itemCols, $customerId, (int)$section['id']);
$vid = $this->createContentVersion($row, $itemCols, $customerId, (int)$section['id']);
if ($activateVersion && $vid) {
$this->activateContentVersion($customerId, (int)$row['id'], $vid);
}
}
} catch (Throwable $e) {
// ignore versioning failures on create
@@ -964,6 +1016,7 @@ class ApiKernel
return;
}
$activateVersion = (int)$this->val($this->in, ['activate_version', 'activate', 'set_active'], 0) === 1;
$versionCols = array_filter([$jsonCol, $htmlCol, $craftCol, $settingsCol, $editorCol]);
$shouldSnapshot = false;
foreach ($versionCols as $col) {
@@ -972,9 +1025,6 @@ class ApiKernel
break;
}
}
if ($shouldSnapshot) {
$this->createContentVersion($current, $itemCols, $customerId, (int)($section['id'] ?? 0));
}
$set = implode(',', array_map(static fn($c) => "`$c` = :$c", array_keys($data)));
$data['id'] = $id;
@@ -983,6 +1033,21 @@ class ApiKernel
$stmt = $this->pdo->prepare($sql);
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
$stmt->execute();
if ($shouldSnapshot) {
try {
$stmt = $this->pdo->prepare("SELECT * FROM `$itemsTable` WHERE `customer_id` = :cid AND `id` = :id LIMIT 1");
$stmt->execute([':cid' => $customerId, ':id' => $id]);
$row = $stmt->fetch();
if ($row) {
$vid = $this->createContentVersion($row, $itemCols, $customerId, (int)($section['id'] ?? 0));
if ($activateVersion && $vid) {
$this->activateContentVersion($customerId, (int)$row['id'], $vid);
}
}
} catch (Throwable $e) {
// ignore versioning failures
}
}
$this->respond(['ok' => true, 'kind' => 'content', 'id' => $id, 'updated' => true]);
}
@@ -1049,6 +1114,8 @@ class ApiKernel
'version_no' => (int)($row['version_no'] ?? 0),
'editor_type' => $row['editor_type'] ?? null,
'created_at' => $row['created_at'] ?? null,
'is_active' => (int)($row['is_active'] ?? 0),
'was_active' => (int)($row['was_active'] ?? 0),
];
}, $rows);
$this->respond(['ok' => true, 'items' => $items, 'data' => $items]);
@@ -1130,6 +1197,38 @@ class ApiKernel
$this->respond(['ok' => true, 'restored' => true, 'content_id' => (int)($version['content_id'] ?? 0)]);
}
private function handleContentVersionsActivate(): void
{
$auth = $this->requireAuth();
$customerId = (int)($auth['customer_id'] ?? 0);
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
$versionId = (int)$this->val($this->in, ['id', 'version_id', 'version'], 0);
if ($versionId <= 0) $this->fail('version id required', null, 422);
$table = $this->contentVersionsTable();
if (!$this->tableExists($table)) $this->fail('Versions table not available', null, 500);
$stmt = $this->pdo->prepare("SELECT `id`,`content_id` FROM `$table` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1");
$stmt->execute([':id' => $versionId, ':cid' => $customerId]);
$row = $stmt->fetch();
if (!$row) $this->fail('Not found', ['id' => $versionId], 404);
$this->activateContentVersion($customerId, (int)$row['content_id'], $versionId);
$this->respond(['ok' => true, 'activated' => true, 'id' => $versionId]);
}
private function handleContentVersionsDeactivate(): void
{
$auth = $this->requireAuth();
$customerId = (int)($auth['customer_id'] ?? 0);
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
$contentId = (int)$this->val($this->in, ['content_id', 'content'], 0);
if ($contentId <= 0) $this->fail('content_id required', null, 422);
$this->deactivateContentVersion($customerId, $contentId);
$this->respond(['ok' => true, 'deactivated' => true, 'content_id' => $contentId]);
}
private function handleSectionsConfigList(): void
{
$auth = $this->requireAuth();
@@ -2325,6 +2424,12 @@ class ApiKernel
case 'content_versions.restore':
$this->handleContentVersionsRestore();
break;
case 'content_versions.activate':
$this->handleContentVersionsActivate();
break;
case 'content_versions.deactivate':
$this->handleContentVersionsDeactivate();
break;
/* ---------- CRUD HANDLER ---------- */
default:
@@ -2645,16 +2750,20 @@ class ApiKernel
}
$itemsTable = $this->contentItemsTable();
$itemCols = $this->resolveContentItemColumns($itemsTable);
$versionsTable = $this->contentVersionsTable();
$versionCols = $this->tableExists($versionsTable) ? $this->resolveContentVersionColumns($versionsTable) : null;
$htmlCol = $itemCols['html'];
$jsonCol = $itemCols['json'];
if (!$htmlCol && !$jsonCol) {
if (!$htmlCol && !$jsonCol && (!$versionCols || !$versionCols['html'])) {
$cache[$cacheKey] = null;
return null;
}
$selectCols = [];
if ($htmlCol) $selectCols[] = "`$htmlCol`";
if ($jsonCol) $selectCols[] = "`$jsonCol`";
$sql = "SELECT " . implode(',', $selectCols) . " FROM `$itemsTable` WHERE `customer_id` = :cid AND `section_id` = :sid AND `id` = :id LIMIT 1";
if ($htmlCol) $selectCols[] = "i.`$htmlCol` AS item_html";
if ($jsonCol) $selectCols[] = "i.`$jsonCol` AS item_json";
if ($versionCols && $versionCols['html']) $selectCols[] = "v.`{$versionCols['html']}` AS version_html";
$join = $versionCols ? "LEFT JOIN `$versionsTable` v ON v.`content_id` = i.`id` AND v.`is_active` = 1" : "";
$sql = "SELECT " . implode(',', $selectCols) . " FROM `$itemsTable` i $join WHERE i.`customer_id` = :cid AND i.`section_id` = :sid AND i.`id` = :id LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':cid' => $customerId, ':sid' => (int)$section['id'], ':id' => $id]);
$row = $stmt->fetch();
@@ -2662,7 +2771,7 @@ class ApiKernel
$cache[$cacheKey] = null;
return null;
}
$html = $htmlCol ? (string)($row[$htmlCol] ?? '') : '';
$html = (string)($row['version_html'] ?? $row['item_html'] ?? '');
} else {
if (!$kindKey) return null;
$table = $this->tableMap[$kindKey] ?? null;