ff14-mitigator/admin.php
2026-05-29 10:48:12 +02:00

391 lines
15 KiB
PHP

<?php
require_once __DIR__ . '/config.php';
session_start_safe();
if (empty($_SESSION['access_token']) || (($_SESSION['token_expires'] ?? 0) <= time())) {
header('Location: ' . auth_start_href(current_return_path()));
exit;
}
$user = current_fflogs_user();
if (!$user) {
http_response_code(502);
}
$allowed = $user && is_admin_user();
if (!$allowed && $user) {
http_response_code(403);
}
if (empty($_SESSION['admin_csrf'])) {
$_SESSION['admin_csrf'] = bin2hex(random_bytes(16));
}
$csrf = $_SESSION['admin_csrf'];
$returnPath = safe_return_path($_GET['return'] ?? null);
$adminAction = 'admin.php?return=' . rawurlencode($returnPath);
function admin_h(?string $value): string {
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function git_run(array $args): array {
$cmd = 'git -C ' . escapeshellarg(__DIR__);
foreach ($args as $arg) {
$cmd .= ' ' . escapeshellarg($arg);
}
$cmd .= ' 2>&1';
exec($cmd, $lines, $code);
return ['code' => $code, 'output' => trim(implode("\n", $lines))];
}
function git_output(array $args): string {
return git_run($args)['output'];
}
function git_local_branches(): array {
$out = git_output(['branch', '--list', '--no-color']);
$branches = array_values(array_filter(array_map(function ($line) {
return trim(ltrim(trim($line), '*'));
}, explode("\n", $out)), fn($v) => $v !== ''));
sort($branches, SORT_NATURAL | SORT_FLAG_CASE);
return $branches;
}
function git_remote_branches(): array {
$out = git_output(['branch', '-r', '--no-color']);
$branches = array_values(array_filter(array_map('trim', explode("\n", $out)), function ($branch) {
return $branch !== '' && !str_contains($branch, ' -> ') && !str_ends_with($branch, '/HEAD');
}));
sort($branches, SORT_NATURAL | SORT_FLAG_CASE);
return $branches;
}
function branch_from_remote(string $remoteBranch): string {
$parts = explode('/', $remoteBranch, 2);
return $parts[1] ?? $remoteBranch;
}
function git_last_commit(): string {
$out = git_output(['log', '-1', '--oneline', '--decorate=short']);
return $out !== '' ? $out : '(unknown)';
}
function git_current_branch(): string {
return git_output(['rev-parse', '--abbrev-ref', 'HEAD']);
}
function git_upstream_for_branch(string $branch): ?string {
$result = git_run(['rev-parse', '--abbrev-ref', $branch . '@{upstream}']);
return $result['code'] === 0 && $result['output'] !== '' ? $result['output'] : null;
}
function git_is_ancestor(string $ancestor, string $descendant): bool {
return git_run(['merge-base', '--is-ancestor', $ancestor, $descendant])['code'] === 0;
}
function git_pull_all_tracked_branches(): array {
$lines = [];
$exitCode = 0;
$fetch = git_run(['fetch', '--all', '--prune']);
$lines[] = '$ git fetch --all --prune';
$lines[] = $fetch['output'] ?: '(no output)';
if ($fetch['code'] !== 0) return ['code' => $fetch['code'], 'output' => implode("\n", $lines)];
$current = git_current_branch();
foreach (git_local_branches() as $branch) {
$upstream = git_upstream_for_branch($branch);
if ($upstream === null) {
$lines[] = '';
$lines[] = $branch . ': no upstream configured, skipped';
continue;
}
$lines[] = '';
$lines[] = $branch . ' <- ' . $upstream;
if ($branch === $current) {
$pull = git_run(['merge', '--ff-only', $upstream]);
$lines[] = '$ git merge --ff-only ' . $upstream;
$lines[] = $pull['output'] ?: '(no output)';
if ($pull['code'] !== 0) $exitCode = $pull['code'];
continue;
}
if (git_is_ancestor($branch, $upstream)) {
$update = git_run(['branch', '-f', $branch, $upstream]);
$lines[] = '$ git branch -f ' . $branch . ' ' . $upstream;
$lines[] = $update['output'] ?: '(fast-forwarded)';
if ($update['code'] !== 0) $exitCode = $update['code'];
} elseif (git_is_ancestor($upstream, $branch)) {
$lines[] = 'already up to date or ahead';
} else {
$lines[] = 'diverged, skipped';
if ($exitCode === 0) $exitCode = 1;
}
}
return ['code' => $exitCode, 'output' => implode("\n", $lines)];
}
function resolve_selected_branch_ref(string $ref, array $localBranches, array $remoteBranches, bool $preferLocalForRemote): ?string {
if (str_starts_with($ref, 'local:')) {
$branch = substr($ref, 6);
return in_array($branch, $localBranches, true) ? $branch : null;
}
if (str_starts_with($ref, 'remote:')) {
$remoteBranch = substr($ref, 7);
if (!in_array($remoteBranch, $remoteBranches, true)) return null;
$localBranch = branch_from_remote($remoteBranch);
return $preferLocalForRemote && in_array($localBranch, $localBranches, true) ? $localBranch : $remoteBranch;
}
return null;
}
$message = null;
$result = null;
if ($allowed && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($csrf, $_POST['csrf'] ?? '')) {
http_response_code(400);
$message = 'Invalid CSRF token.';
} else {
$action = $_POST['action'] ?? '';
if ($action === 'fetch') {
$result = git_run(['fetch', '--all', '--prune']);
$message = 'git fetch --all --prune';
} elseif ($action === 'pull') {
$result = git_pull_all_tracked_branches();
$message = 'pull all tracked branches';
} elseif ($action === 'push') {
$result = git_run(['push']);
$message = 'git push';
} elseif ($action === 'commit') {
$commitMessage = trim((string)($_POST['message'] ?? ''));
if ($commitMessage === '') {
$result = ['code' => 1, 'output' => 'Commit message is required.'];
} else {
$add = git_run(['add', '-A']);
if ($add['code'] !== 0) {
$result = $add;
$message = 'git add -A';
} else {
$result = git_run(['commit', '-m', $commitMessage]);
$message = 'git commit';
}
}
} elseif ($action === 'merge') {
$ref = trim((string)($_POST['merge_branch'] ?? ''));
git_run(['fetch', '--all', '--prune']);
$localBranches = git_local_branches();
$remoteBranches = git_remote_branches();
$branch = resolve_selected_branch_ref($ref, $localBranches, $remoteBranches, false);
if ($branch === null) {
$result = ['code' => 1, 'output' => 'Unknown branch selection.'];
} else {
$result = git_run(['merge', '--no-edit', $branch]);
$message = 'git merge --no-edit ' . $branch;
}
} elseif ($action === 'switch') {
$ref = trim((string)($_POST['branch'] ?? ''));
$localBranches = git_local_branches();
$remoteBranches = git_remote_branches();
if (str_starts_with($ref, 'remote:')) {
$remoteBranch = substr($ref, 7);
if (!in_array($remoteBranch, $remoteBranches, true)) {
$result = ['code' => 1, 'output' => 'Unknown remote branch: ' . $remoteBranch];
} else {
$branch = branch_from_remote($remoteBranch);
if (in_array($branch, $localBranches, true)) {
$result = git_run(['switch', $branch]);
$message = 'git switch ' . $branch;
} else {
$result = git_run(['switch', '--track', $remoteBranch]);
$message = 'git switch --track ' . $remoteBranch;
}
}
} else {
$branch = resolve_selected_branch_ref($ref, $localBranches, $remoteBranches, true);
if ($branch === null) {
$result = ['code' => 1, 'output' => 'Unknown branch selection.'];
} else {
$result = git_run(['switch', $branch]);
$message = 'git switch ' . $branch;
}
}
}
}
}
$currentBranch = '';
$status = '';
$branches = [];
$remoteBranches = [];
$lastCommit = '';
if ($allowed) {
$currentBranch = git_current_branch();
$status = git_output(['status', '-sb']);
$branches = git_local_branches();
$remoteBranches = git_remote_branches();
$lastCommit = git_last_commit();
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin · FFLogs Report Viewer</title>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/components.css">
<style>
.admin-shell { max-width: 980px; margin: 24px auto; width: min(92%, 980px); }
.admin-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.admin-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
.admin-pre {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 14px;
overflow: auto;
white-space: pre-wrap;
color: var(--t2);
font-size: 13px;
line-height: 1.5;
}
.admin-meta { color: var(--t2); line-height: 1.8; font-size: 14px; }
.admin-deny { max-width: 720px; margin: 24px auto; width: min(92%, 720px); }
@media (max-width: 800px) { .admin-grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div id="app">
<header id="topbar">
<a class="logo" href="<?= admin_h($returnPath) ?>" style="text-decoration:none">REPORT VIEWER <span>Admin</span></a>
<div class="topbar-actions">
<a class="topbar-link" href="<?= admin_h($returnPath) ?>">Back</a>
<div class="topbar-user">
<?= $user ? admin_h($user['name'] ?: 'FFLogs User') . ' #' . admin_h($user['id']) : 'FFLogs user unavailable' ?>
</div>
</div>
</header>
<?php if (!$user): ?>
<main class="admin-deny">
<div class="card">
<div class="card-title">User nicht verfügbar</div>
<p class="admin-meta">Ich konnte deine FFLogs User-ID nicht laden. <?= admin_h($_SESSION['fflogs_user_error'] ?? '') ?></p>
</div>
</main>
<?php elseif (!$allowed): ?>
<main class="admin-deny">
<div class="card">
<div class="card-title">Kein Zugriff</div>
<p class="admin-meta">
Deine FFLogs User-ID ist <strong><?= admin_h($user['id']) ?></strong>.
Füge diese ID in deiner <code>.env</code> zu <code>ADMIN_USER_IDS</code> hinzu, um diese Seite freizuschalten.
</p>
</div>
</main>
<?php else: ?>
<main class="admin-shell">
<div class="card">
<div class="card-title">Git Admin</div>
<div class="admin-meta">
Branch: <strong><?= admin_h($currentBranch) ?></strong><br>
Last commit: <?= admin_h($lastCommit) ?>
</div>
</div>
<div class="admin-grid section-gap">
<div class="card">
<div class="card-title">Aktionen</div>
<form method="post" action="<?= admin_h($adminAction) ?>" class="admin-actions">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<button class="btn" name="action" value="fetch" type="submit">Fetch</button>
<button class="btn btn-gold" name="action" value="pull" type="submit">Pull All</button>
<button class="btn" name="action" value="push" type="submit">Push</button>
</form>
<form method="post" action="<?= admin_h($adminAction) ?>" class="admin-actions section-gap">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<input type="hidden" name="action" value="switch">
<div class="fg">
<label for="branch">Branch</label>
<select id="branch" name="branch">
<?php foreach ($branches as $branch): ?>
<option value="local:<?= admin_h($branch) ?>" <?= $branch === $currentBranch ? 'selected' : '' ?>>
<?= admin_h($branch) ?>
</option>
<?php endforeach; ?>
<?php if ($remoteBranches): ?>
<optgroup label="Remote branches">
<?php foreach ($remoteBranches as $branch): ?>
<option value="remote:<?= admin_h($branch) ?>">
<?= admin_h($branch) ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
</select>
</div>
<button class="btn" type="submit">Switch</button>
</form>
<form method="post" action="<?= admin_h($adminAction) ?>" class="section-gap">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<input type="hidden" name="action" value="commit">
<div class="fg">
<label for="commit-message">Commit message</label>
<input id="commit-message" name="message" type="text" placeholder="Describe the change…" required>
</div>
<button class="btn btn-gold section-gap" type="submit">Commit all changes</button>
</form>
<form method="post" action="<?= admin_h($adminAction) ?>" class="admin-actions section-gap">
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
<input type="hidden" name="action" value="merge">
<div class="fg">
<label for="merge-branch">Merge from branch</label>
<select id="merge-branch" name="merge_branch">
<?php foreach ($branches as $branch): ?>
<?php if ($branch === $currentBranch) continue; ?>
<option value="local:<?= admin_h($branch) ?>">
<?= admin_h($branch) ?>
</option>
<?php endforeach; ?>
<?php if ($remoteBranches): ?>
<optgroup label="Remote branches">
<?php foreach ($remoteBranches as $branch): ?>
<option value="remote:<?= admin_h($branch) ?>">
<?= admin_h($branch) ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
</select>
</div>
<button class="btn" type="submit">Merge</button>
</form>
</div>
<div class="card">
<div class="card-title">Status</div>
<pre class="admin-pre"><?= admin_h($status ?: 'Clean') ?></pre>
</div>
</div>
<?php if ($result): ?>
<div class="card section-gap">
<div class="card-title"><?= admin_h($message ?? 'Output') ?> · Exit <?= (int)$result['code'] ?></div>
<pre class="admin-pre"><?= admin_h($result['output'] ?: '(no output)') ?></pre>
</div>
<?php endif; ?>
</main>
<?php endif; ?>
</div>
</body>
</html>