| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- <?php
- declare(strict_types=1);
- // Drop-in admin page for the host application.
- //
- // The host is expected to have established its own session and authentication
- // BEFORE including this file. The default guard below matches the PSA order
- // system; adjust it to whatever the host project uses (see 02_INTEGRATION.md).
- //
- // Typical integration, as myproject/admin/manage.php:
- //
- // require_once __DIR__ . "/../config.php";
- // require_once __DIR__ . "/../includes/functions.php";
- // if (empty($_SESSION["admin_logged_in"])) { header("Location: login.php"); exit; }
- // require __DIR__ . "/../manage-client/ui/panel.php";
- //
- // This page contains no update or backup logic of its own: every action calls
- // the same public functions as the CLI.
- require_once dirname(__DIR__) . "/lib/client.php";
- if (session_status() === PHP_SESSION_NONE) {
- session_start();
- }
- // --- Authentication guard --------------------------------------------------
- // Replace this block if the host application uses a different session flag.
- if (!defined("MANAGE_PANEL_SKIP_AUTH_GUARD") && empty($_SESSION["admin_logged_in"])) {
- http_response_code(403);
- exit("Zugriff verweigert. Dieses Panel setzt eine angemeldete Sitzung voraus.");
- }
- function managePanelEscape($value): string
- {
- return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
- }
- function managePanelCsrfToken(): string
- {
- if (empty($_SESSION["manage_panel_csrf"])) {
- $_SESSION["manage_panel_csrf"] = bin2hex(random_bytes(32));
- }
- return (string) $_SESSION["manage_panel_csrf"];
- }
- function managePanelCsrfValid(string $token): bool
- {
- return !empty($_SESSION["manage_panel_csrf"]) &&
- hash_equals((string) $_SESSION["manage_panel_csrf"], $token);
- }
- $messages = [];
- $errors = [];
- $warnings = [];
- if (($_SERVER["REQUEST_METHOD"] ?? "") === "POST") {
- try {
- if (!managePanelCsrfValid((string) ($_POST["csrf_token"] ?? ""))) {
- throw new RuntimeException("Ungültiges Sicherheitstoken. Bitte die Seite neu laden.");
- }
- $action = (string) ($_POST["action"] ?? "");
- if ($action === "backup") {
- $record = manageBackupCreate("manual");
- $messages[] = "Backup erstellt: " . $record["filename"] .
- " (" . $record["file_count"] . " Dateien, " . manageFormatBytes((int) $record["size"]) . ")";
- foreach ($record["remote_uploads"] as $upload) {
- if (empty($upload["success"])) {
- $warnings[] = "Upload an " . (string) $upload["target"] . " fehlgeschlagen: " .
- (string) ($upload["error"] ?? "unbekannt");
- }
- }
- manageHeartbeatSendQuietly();
- } elseif ($action === "update") {
- $result = manageUpdateApply(["force" => !empty($_POST["force"])]);
- $messages[] = "Update ausgerollt: " . $result["from_version"] . " → " . $result["to_version"];
- $messages[] = $result["copied"] . " Dateien kopiert, " . $result["backed_up"] . " gesichert.";
- $messages[] = "Sicherungsverzeichnis: " . $result["backup_dir"];
- $hook = $result["hook"];
- if (is_array($hook)) {
- $applied = $hook["migrations"]["applied"] ?? [];
- if ($applied !== []) {
- $messages[] = "Migrationen ausgeführt: " . implode(", ", $applied);
- }
- if (empty($hook["success"])) {
- // The files are deployed; only the post-update step failed.
- if (!empty($hook["failed_migration"])) {
- $errors[] = "Die Dateien wurden ausgerollt, aber die Migration \"" .
- (string) $hook["failed_migration"] . "\" ist fehlgeschlagen: " .
- (string) ($hook["error"] ?? "");
- $errors[] = "Verbleibende Migrationen wurden nicht ausgeführt. " .
- "Nach Behebung der Ursache unten \"Migrationen ausführen\" verwenden.";
- } else {
- $errors[] = "Die Dateien wurden ausgerollt, aber der Post-Update-Hook ist " .
- "fehlgeschlagen: " . (string) ($hook["error"] ?? "");
- }
- }
- }
- manageHeartbeatSendQuietly();
- } elseif ($action === "migrate") {
- $report = manageUpdateRunMigrations();
- if ($report["applied"] !== []) {
- $messages[] = "Migrationen ausgeführt: " . implode(", ", $report["applied"]);
- }
- if (!$report["success"]) {
- $errors[] = "Migration \"" . (string) $report["failed"] . "\" ist fehlgeschlagen: " .
- (string) $report["error"];
- } elseif ($report["applied"] === []) {
- $messages[] = "Keine offenen Migrationen.";
- }
- } elseif ($action === "heartbeat") {
- $result = manageHeartbeatSend();
- $messages[] = "Heartbeat gesendet. Aktuelles Release: " .
- ($result["latest"] !== "" ? $result["latest"] : "keines") . ".";
- } elseif ($action === "download") {
- $path = manageBackupPath((string) ($_POST["filename"] ?? ""));
- $size = filesize($path);
- $handle = fopen($path, "rb");
- if ($size === false || $handle === false) {
- throw new RuntimeException("Backup konnte nicht geöffnet werden.");
- }
- header("Content-Type: application/zip");
- header("Content-Disposition: attachment; filename=\"" . addcslashes(basename($path), "\"\\") . "\"");
- header("Content-Length: " . (string) $size);
- header("Cache-Control: private, no-store");
- header("X-Content-Type-Options: nosniff");
- fpassthru($handle);
- fclose($handle);
- exit;
- }
- } catch (Throwable $exception) {
- $errors[] = $exception->getMessage();
- }
- }
- $status = manageClientStatus();
- $capabilities = manageRemoteCapabilities();
- ?>
- <!DOCTYPE html>
- <html lang="de">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Update & Backup</title>
- <style>
- /* Self-contained so the panel looks reasonable in any host application.
- Override by loading the host's stylesheet after this file. */
- .mc-wrap { max-width: 60rem; margin: 0 auto; padding: 1.5rem 1rem 3rem;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- line-height: 1.55; color: #16191d; }
- .mc-wrap h1 { font-size: 1.5rem; margin: 0 0 1rem; }
- .mc-wrap h2 { font-size: 1.1rem; margin: 1.75rem 0 .75rem; }
- .mc-alert { padding: .7rem .9rem; border-radius: 8px; margin-bottom: .5rem; border: 1px solid transparent; }
- .mc-ok { background: #eaf6ee; border-color: #bfe0cb; color: #1a6b3c; }
- .mc-warn { background: #fdf3e0; border-color: #e6cf9d; color: #8a5a00; }
- .mc-err { background: #fceceb; border-color: #f0c3bf; color: #a52218; }
- .mc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .75rem; }
- .mc-card { border: 1px solid #d9dde3; border-radius: 8px; padding: .8rem .9rem; background: #fff; }
- .mc-label { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: #5c6470; }
- .mc-value { font-size: 1.2rem; font-weight: 600; }
- .mc-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: .75rem 0; }
- .mc-btn { padding: .45rem .9rem; border: 1px solid #1f3b63; border-radius: 8px; background: #1f3b63;
- color: #fff; font: inherit; font-size: .9rem; cursor: pointer; }
- .mc-btn.sec { background: #fff; color: #1f3b63; }
- .mc-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
- .mc-table th, .mc-table td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid #d9dde3; }
- .mc-table thead th { font-size: .75rem; text-transform: uppercase; color: #5c6470; }
- .mc-scroll { overflow-x: auto; border: 1px solid #d9dde3; border-radius: 8px; background: #fff; }
- .mc-muted { color: #5c6470; }
- .mc-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8rem; }
- </style>
- </head>
- <body>
- <div class="mc-wrap">
- <h1>Update & Backup</h1>
- <?php foreach ($messages as $message): ?>
- <p class="mc-alert mc-ok"><?php echo managePanelEscape($message); ?></p>
- <?php endforeach; ?>
- <?php foreach ($warnings as $warning): ?>
- <p class="mc-alert mc-warn"><?php echo managePanelEscape($warning); ?></p>
- <?php endforeach; ?>
- <?php foreach ($errors as $error): ?>
- <p class="mc-alert mc-err"><?php echo managePanelEscape($error); ?></p>
- <?php endforeach; ?>
- <?php if (!$status["configured"]): ?>
- <p class="mc-alert mc-err">
- Der Manage-Client ist nicht konfiguriert. In <code>manage-client/config.php</code> müssen
- <code>MANAGE_SERVER_URL</code>, <code>MANAGE_INSTANCE</code> und <code>MANAGE_TOKEN</code> gesetzt sein.
- </p>
- <?php endif; ?>
- <div class="mc-grid">
- <div class="mc-card">
- <p class="mc-label">Installierte Version</p>
- <p class="mc-value"><?php echo managePanelEscape($status["version"] !== "" ? $status["version"] : "unbekannt"); ?></p>
- </div>
- <div class="mc-card">
- <p class="mc-label">Aktuelles Release</p>
- <p class="mc-value">
- <?php echo managePanelEscape($status["update"]["latest"] ?? "–"); ?>
- </p>
- <?php if ($status["update_error"] !== null): ?>
- <p class="mc-muted"><?php echo managePanelEscape($status["update_error"]); ?></p>
- <?php endif; ?>
- </div>
- <div class="mc-card">
- <p class="mc-label">Lokale Backups</p>
- <p class="mc-value"><?php echo count($status["backups"]); ?></p>
- <p class="mc-muted">Letztes: <?php echo managePanelEscape($status["last_backup_at"] ?? "nie"); ?></p>
- </div>
- <div class="mc-card">
- <p class="mc-label">Offene Migrationen</p>
- <p class="mc-value"><?php echo count($status["pending_migrations"]); ?></p>
- </div>
- </div>
- <?php if ($status["update"] !== null && $status["update"]["available"]): ?>
- <p class="mc-alert mc-warn">
- Version <?php echo managePanelEscape($status["update"]["latest"]); ?> steht bereit.
- Vor dem Ausrollen sollte ein aktuelles Backup vorliegen.
- </p>
- <?php endif; ?>
- <h2>Aktionen</h2>
- <div class="mc-row">
- <form method="POST">
- <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
- <input type="hidden" name="action" value="backup">
- <button type="submit" class="mc-btn">Backup jetzt erstellen</button>
- </form>
- <form method="POST" onsubmit="return confirm('Update jetzt ausrollen? Dateien werden überschrieben.');">
- <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
- <input type="hidden" name="action" value="update">
- <label class="mc-muted">
- <input type="checkbox" name="force" value="1"> erneut ausrollen
- </label>
- <button type="submit" class="mc-btn">Update ausrollen</button>
- </form>
- <?php if ($status["pending_migrations"] !== []): ?>
- <form method="POST">
- <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
- <input type="hidden" name="action" value="migrate">
- <button type="submit" class="mc-btn sec">Migrationen ausführen</button>
- </form>
- <?php endif; ?>
- <form method="POST">
- <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
- <input type="hidden" name="action" value="heartbeat">
- <button type="submit" class="mc-btn sec">Status melden</button>
- </form>
- </div>
- <?php if ($status["pending_migrations"] !== []): ?>
- <h2>Offene Migrationen</h2>
- <ul>
- <?php foreach ($status["pending_migrations"] as $migration): ?>
- <li class="mc-mono"><?php echo managePanelEscape($migration["id"]); ?></li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
- <h2>Lokale Backups</h2>
- <?php if ($status["backups"] === []): ?>
- <p class="mc-muted">Es wurde noch kein Backup erstellt.</p>
- <?php else: ?>
- <div class="mc-scroll">
- <table class="mc-table">
- <thead>
- <tr>
- <th>Datei</th>
- <th>Erstellt</th>
- <th>Auslöser</th>
- <th>Dateien</th>
- <th>Größe</th>
- <th>Upload</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <?php foreach ($status["backups"] as $backup): ?>
- <tr>
- <td class="mc-mono"><?php echo managePanelEscape($backup["filename"] ?? ""); ?></td>
- <td><?php echo managePanelEscape($backup["created_at"] ?? ""); ?></td>
- <td><?php echo managePanelEscape($backup["trigger"] ?? ""); ?></td>
- <td><?php echo (int) ($backup["file_count"] ?? 0); ?></td>
- <td><?php echo managePanelEscape(manageFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
- <td>
- <?php
- $uploads = is_array($backup["remote_uploads"] ?? null) ? $backup["remote_uploads"] : [];
- if ($uploads === []) {
- echo "–";
- } else {
- foreach ($uploads as $upload) {
- $ok = !empty($upload["success"]);
- echo managePanelEscape((string) ($upload["target"] ?? "?")) .
- ": " . ($ok ? "OK" : "Fehler") . "<br>";
- }
- }
- ?>
- </td>
- <td>
- <form method="POST">
- <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
- <input type="hidden" name="action" value="download">
- <input type="hidden" name="filename" value="<?php echo managePanelEscape($backup["filename"] ?? ""); ?>">
- <button type="submit" class="mc-btn sec">Herunterladen</button>
- </form>
- </td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- </div>
- <?php endif; ?>
- <?php
- $capabilityWarnings = [];
- foreach ($capabilities as $type => $capability) {
- if ($capability["configured"] && !$capability["available"]) {
- $capabilityWarnings[] = $type;
- }
- }
- ?>
- <?php if ($capabilityWarnings !== []): ?>
- <p class="mc-alert mc-warn">
- Konfigurierte Backup-Ziele ohne Systemunterstützung:
- <?php echo managePanelEscape(implode(", ", $capabilityWarnings)); ?>.
- Diese Uploads werden fehlschlagen.
- </p>
- <?php endif; ?>
- <?php foreach ($status["errors"] as $error): ?>
- <p class="mc-alert mc-warn"><?php echo managePanelEscape($error); ?></p>
- <?php endforeach; ?>
- </div>
- </body>
- </html>
|