HackmanCMS/web/api/themes.php
2026-05-03 20:56:15 +03:00

154 lines
6 KiB
PHP

<?php
header('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];
$project_id = (int)($_GET['project_id'] ?? 0);
$stmt = $db->prepare('SELECT * FROM projects WHERE id = ? AND is_active = 1');
$stmt->execute([$project_id]);
$project = $stmt->fetch();
if (!$project) { http_response_code(404); echo json_encode(['error' => 'Project not found']); exit; }
$base = realpath($project['path']);
$themesDir = $base ? $base . '/themes' : null;
$configFile = $base ? $base . '/_config.yml' : null;
if (!$base || !is_dir($base)) { echo json_encode(['error' => 'Project path not accessible']); exit; }
function readActiveTheme(?string $configFile): ?string {
if (!$configFile || !is_file($configFile)) return null;
foreach (file($configFile, FILE_IGNORE_NEW_LINES) as $line) {
if (preg_match('/^theme:\s*(.+?)\s*$/', $line, $m)) return trim($m[1], "\"' ");
}
return null;
}
function writeActiveTheme(string $configFile, string $name): bool {
if (!is_file($configFile)) return false;
$lines = file($configFile, FILE_IGNORE_NEW_LINES);
$found = false;
foreach ($lines as $i => $line) {
if (preg_match('/^theme:/', $line)) { $lines[$i] = 'theme: ' . $name; $found = true; break; }
}
if (!$found) $lines[] = 'theme: ' . $name;
return file_put_contents($configFile, implode("\n", $lines) . "\n") !== false;
}
function gitInfo(string $dir): array {
if (!is_dir($dir . '/.git')) return ['has_git' => false];
$run = function (string $cmd) use ($dir) {
$out = [];
exec('cd ' . escapeshellarg($dir) . ' && ' . $cmd . ' 2>/dev/null', $out);
return implode("\n", $out);
};
$branch = trim($run('git rev-parse --abbrev-ref HEAD'));
$remote = trim($run('git remote get-url origin'));
$commit = trim($run('git log -1 --pretty=format:"%h %s"'));
$status = trim($run('git status --porcelain'));
@exec('cd ' . escapeshellarg($dir) . ' && git rev-list --left-right --count @{u}...HEAD 2>/dev/null',
$countOut);
$ahead = $behind = null;
if (!empty($countOut[0]) && preg_match('/^(\d+)\s+(\d+)$/', $countOut[0], $m)) {
$behind = (int)$m[1]; $ahead = (int)$m[2];
}
return [
'has_git' => true,
'branch' => $branch,
'remote' => $remote ?: null,
'commit' => $commit ?: null,
'dirty' => $status !== '',
'ahead' => $ahead,
'behind' => $behind,
];
}
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $input['action'] ?? '';
$name = preg_replace('/[^A-Za-z0-9._-]/', '', (string)($input['name'] ?? ''));
if ($action === 'switch' && $name) {
if (!is_dir($themesDir . '/' . $name)) { echo json_encode(['error' => 'Theme not found']); exit; }
$ok = writeActiveTheme($configFile, $name);
Audit::log($db, 'theme_switch', $project_id, $name);
echo json_encode(['ok' => $ok, 'active' => $name]);
exit;
}
if ($action === 'clone') {
$url = trim($input['url'] ?? '');
if (!$url) { echo json_encode(['error' => 'url required']); exit; }
if (!$name) {
// derive from url
$name = preg_replace('/\.git$/', '', basename(parse_url($url, PHP_URL_PATH) ?: ''));
$name = preg_replace('/[^A-Za-z0-9._-]/', '', $name);
}
if (!$name) { echo json_encode(['error' => 'cannot derive name']); exit; }
$dest = $themesDir . '/' . $name;
if (is_dir($dest)) { echo json_encode(['error' => 'Theme directory already exists']); exit; }
if (!is_dir($themesDir)) mkdir($themesDir, 0755, true);
$cmd = 'git clone --depth 50 ' . escapeshellarg($url) . ' ' . escapeshellarg($dest) . ' 2>&1';
exec($cmd, $out, $rc);
$log = implode("\n", $out);
if ($rc !== 0) { echo json_encode(['error' => 'git clone failed', 'log' => $log]); exit; }
Audit::log($db, 'theme_clone', $project_id, $name);
echo json_encode(['ok' => true, 'name' => $name, 'log' => $log]);
exit;
}
if ($action === 'git' && $name) {
$op = $input['op'] ?? '';
$dir = $themesDir . '/' . $name;
if (!is_dir($dir . '/.git')) { echo json_encode(['error' => 'No git repo in theme']); exit; }
$cmd = match ($op) {
'pull' => 'git pull',
'push' => 'git push',
'fetch' => 'git fetch',
default => null,
};
if (!$cmd) { echo json_encode(['error' => 'Unknown op']); exit; }
exec('cd ' . escapeshellarg($dir) . ' && ' . $cmd . ' 2>&1', $out, $rc);
Audit::log($db, 'theme_git_' . $op, $project_id, $name);
echo json_encode(['ok' => $rc === 0, 'log' => implode("\n", $out)]);
exit;
}
if ($action === 'delete' && $name) {
if ($name === readActiveTheme($configFile)) {
echo json_encode(['error' => 'Cannot delete the active theme']); exit;
}
$dir = $themesDir . '/' . $name;
if (!is_dir($dir) || !str_starts_with(realpath($dir) . '/', realpath($themesDir) . '/')) {
echo json_encode(['error' => 'Theme not found']); exit;
}
exec('rm -rf ' . escapeshellarg($dir), $_o, $rc);
Audit::log($db, 'theme_delete', $project_id, $name);
echo json_encode(['ok' => $rc === 0]);
exit;
}
http_response_code(400);
echo json_encode(['error' => 'Unknown action']);
exit;
}
// GET list
if (!$themesDir || !is_dir($themesDir)) {
echo json_encode(['active' => null, 'themes' => [], 'no_themes_dir' => true]);
exit;
}
$active = readActiveTheme($configFile);
$themes = [];
foreach (scandir($themesDir) as $name) {
if ($name[0] === '.') continue;
$dir = $themesDir . '/' . $name;
if (!is_dir($dir)) continue;
$themes[] = [
'name' => $name,
'active' => $name === $active,
'git' => gitInfo($dir),
];
}
usort($themes, fn($a, $b) => ((int)$b['active']) - ((int)$a['active']) ?: strcmp($a['name'], $b['name']));
echo json_encode(['active' => $active, 'themes' => $themes]);