panel.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <?php
  2. declare(strict_types=1);
  3. // Drop-in admin page for the host application.
  4. //
  5. // The host is expected to have established its own session and authentication
  6. // BEFORE including this file. Adjust the default guard below to whatever the
  7. // host project uses (see 02_INTEGRATION.md).
  8. //
  9. // Typical integration, as myproject/admin/manage.php:
  10. //
  11. // require_once __DIR__ . "/../config.php";
  12. // require_once __DIR__ . "/../includes/functions.php";
  13. // if (empty($_SESSION["admin_logged_in"])) { header("Location: login.php"); exit; }
  14. // require __DIR__ . "/../manage-client/ui/panel.php";
  15. //
  16. // This page contains no update or backup logic of its own: every action calls
  17. // the same public functions as the CLI.
  18. require_once dirname(__DIR__) . "/lib/client.php";
  19. if (session_status() === PHP_SESSION_NONE) {
  20. session_start();
  21. }
  22. // --- Authentication guard --------------------------------------------------
  23. // Replace this block if the host application uses a different session flag.
  24. if (!defined("MANAGE_PANEL_SKIP_AUTH_GUARD") && empty($_SESSION["admin_logged_in"])) {
  25. http_response_code(403);
  26. exit("Zugriff verweigert. Dieses Panel setzt eine angemeldete Sitzung voraus.");
  27. }
  28. function managePanelEscape($value): string
  29. {
  30. return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
  31. }
  32. function managePanelCsrfToken(): string
  33. {
  34. if (empty($_SESSION["manage_panel_csrf"])) {
  35. $_SESSION["manage_panel_csrf"] = bin2hex(random_bytes(32));
  36. }
  37. return (string) $_SESSION["manage_panel_csrf"];
  38. }
  39. function managePanelCsrfValid(string $token): bool
  40. {
  41. return !empty($_SESSION["manage_panel_csrf"]) &&
  42. hash_equals((string) $_SESSION["manage_panel_csrf"], $token);
  43. }
  44. $messages = [];
  45. $errors = [];
  46. $warnings = [];
  47. if (($_SERVER["REQUEST_METHOD"] ?? "") === "POST") {
  48. try {
  49. if (!managePanelCsrfValid((string) ($_POST["csrf_token"] ?? ""))) {
  50. throw new RuntimeException("Ungültiges Sicherheitstoken. Bitte die Seite neu laden.");
  51. }
  52. $action = (string) ($_POST["action"] ?? "");
  53. if ($action === "backup") {
  54. $record = manageBackupCreate("manual");
  55. $messages[] = "Backup erstellt: " . $record["filename"] .
  56. " (" . $record["file_count"] . " Dateien, " . manageFormatBytes((int) $record["size"]) . ")";
  57. foreach ($record["remote_uploads"] as $upload) {
  58. if (empty($upload["success"])) {
  59. $warnings[] = "Upload an " . (string) $upload["target"] . " fehlgeschlagen: " .
  60. (string) ($upload["error"] ?? "unbekannt");
  61. }
  62. }
  63. manageHeartbeatSendQuietly();
  64. } elseif ($action === "update") {
  65. $result = manageUpdateApply(["force" => !empty($_POST["force"])]);
  66. $messages[] = "Update ausgerollt: " . $result["from_version"] . " → " . $result["to_version"];
  67. $messages[] = $result["copied"] . " Dateien kopiert, " . $result["backed_up"] . " gesichert.";
  68. $messages[] = "Sicherungsverzeichnis: " . $result["backup_dir"];
  69. $hook = $result["hook"];
  70. if (is_array($hook)) {
  71. $applied = $hook["migrations"]["applied"] ?? [];
  72. if ($applied !== []) {
  73. $messages[] = "Migrationen ausgeführt: " . implode(", ", $applied);
  74. }
  75. if (empty($hook["success"])) {
  76. // The files are deployed; only the post-update step failed.
  77. if (!empty($hook["failed_migration"])) {
  78. $errors[] = "Die Dateien wurden ausgerollt, aber die Migration \"" .
  79. (string) $hook["failed_migration"] . "\" ist fehlgeschlagen: " .
  80. (string) ($hook["error"] ?? "");
  81. $errors[] = "Verbleibende Migrationen wurden nicht ausgeführt. " .
  82. "Nach Behebung der Ursache unten \"Migrationen ausführen\" verwenden.";
  83. } else {
  84. $errors[] = "Die Dateien wurden ausgerollt, aber der Post-Update-Hook ist " .
  85. "fehlgeschlagen: " . (string) ($hook["error"] ?? "");
  86. }
  87. }
  88. }
  89. manageHeartbeatSendQuietly();
  90. } elseif ($action === "migrate") {
  91. $report = manageUpdateRunMigrations();
  92. if ($report["applied"] !== []) {
  93. $messages[] = "Migrationen ausgeführt: " . implode(", ", $report["applied"]);
  94. }
  95. if (!$report["success"]) {
  96. $errors[] = "Migration \"" . (string) $report["failed"] . "\" ist fehlgeschlagen: " .
  97. (string) $report["error"];
  98. } elseif ($report["applied"] === []) {
  99. $messages[] = "Keine offenen Migrationen.";
  100. }
  101. } elseif ($action === "heartbeat") {
  102. $result = manageHeartbeatSend();
  103. $messages[] = "Heartbeat gesendet. Aktuelles Release: " .
  104. ($result["latest"] !== "" ? $result["latest"] : "keines") . ".";
  105. } elseif ($action === "download") {
  106. $path = manageBackupPath((string) ($_POST["filename"] ?? ""));
  107. $size = filesize($path);
  108. $handle = fopen($path, "rb");
  109. if ($size === false || $handle === false) {
  110. throw new RuntimeException("Backup konnte nicht geöffnet werden.");
  111. }
  112. header("Content-Type: application/zip");
  113. header("Content-Disposition: attachment; filename=\"" . addcslashes(basename($path), "\"\\") . "\"");
  114. header("Content-Length: " . (string) $size);
  115. header("Cache-Control: private, no-store");
  116. header("X-Content-Type-Options: nosniff");
  117. fpassthru($handle);
  118. fclose($handle);
  119. exit;
  120. }
  121. } catch (Throwable $exception) {
  122. $errors[] = $exception->getMessage();
  123. }
  124. }
  125. $status = manageClientStatus();
  126. $capabilities = manageRemoteCapabilities();
  127. ?>
  128. <!DOCTYPE html>
  129. <html lang="de">
  130. <head>
  131. <meta charset="UTF-8">
  132. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  133. <title>Update &amp; Backup</title>
  134. <style>
  135. /* Self-contained so the panel looks reasonable in any host application.
  136. Override by loading the host's stylesheet after this file. */
  137. .mc-wrap { max-width: 60rem; margin: 0 auto; padding: 1.5rem 1rem 3rem;
  138. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  139. line-height: 1.55; color: #16191d; }
  140. .mc-wrap h1 { font-size: 1.5rem; margin: 0 0 1rem; }
  141. .mc-wrap h2 { font-size: 1.1rem; margin: 1.75rem 0 .75rem; }
  142. .mc-alert { padding: .7rem .9rem; border-radius: 8px; margin-bottom: .5rem; border: 1px solid transparent; }
  143. .mc-ok { background: #eaf6ee; border-color: #bfe0cb; color: #1a6b3c; }
  144. .mc-warn { background: #fdf3e0; border-color: #e6cf9d; color: #8a5a00; }
  145. .mc-err { background: #fceceb; border-color: #f0c3bf; color: #a52218; }
  146. .mc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .75rem; }
  147. .mc-card { border: 1px solid #d9dde3; border-radius: 8px; padding: .8rem .9rem; background: #fff; }
  148. .mc-label { font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; color: #5c6470; }
  149. .mc-value { font-size: 1.2rem; font-weight: 600; }
  150. .mc-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: .75rem 0; }
  151. .mc-btn { padding: .45rem .9rem; border: 1px solid #1f3b63; border-radius: 8px; background: #1f3b63;
  152. color: #fff; font: inherit; font-size: .9rem; cursor: pointer; }
  153. .mc-btn.sec { background: #fff; color: #1f3b63; }
  154. .mc-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
  155. .mc-table th, .mc-table td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid #d9dde3; }
  156. .mc-table thead th { font-size: .75rem; text-transform: uppercase; color: #5c6470; }
  157. .mc-scroll { overflow-x: auto; border: 1px solid #d9dde3; border-radius: 8px; background: #fff; }
  158. .mc-muted { color: #5c6470; }
  159. .mc-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8rem; }
  160. </style>
  161. </head>
  162. <body>
  163. <div class="mc-wrap">
  164. <h1>Update &amp; Backup</h1>
  165. <?php foreach ($messages as $message): ?>
  166. <p class="mc-alert mc-ok"><?php echo managePanelEscape($message); ?></p>
  167. <?php endforeach; ?>
  168. <?php foreach ($warnings as $warning): ?>
  169. <p class="mc-alert mc-warn"><?php echo managePanelEscape($warning); ?></p>
  170. <?php endforeach; ?>
  171. <?php foreach ($errors as $error): ?>
  172. <p class="mc-alert mc-err"><?php echo managePanelEscape($error); ?></p>
  173. <?php endforeach; ?>
  174. <?php if (!$status["configured"]): ?>
  175. <p class="mc-alert mc-err">
  176. Der Manage-Client ist nicht konfiguriert. In <code>manage-client/config.php</code> müssen
  177. <code>MANAGE_SERVER_URL</code>, <code>MANAGE_INSTANCE</code> und <code>MANAGE_TOKEN</code> gesetzt sein.
  178. </p>
  179. <?php endif; ?>
  180. <div class="mc-grid">
  181. <div class="mc-card">
  182. <p class="mc-label">Installierte Version</p>
  183. <p class="mc-value"><?php echo managePanelEscape($status["version"] !== "" ? $status["version"] : "unbekannt"); ?></p>
  184. </div>
  185. <div class="mc-card">
  186. <p class="mc-label">Aktuelles Release</p>
  187. <p class="mc-value">
  188. <?php echo managePanelEscape($status["update"]["latest"] ?? "–"); ?>
  189. </p>
  190. <?php if ($status["update_error"] !== null): ?>
  191. <p class="mc-muted"><?php echo managePanelEscape($status["update_error"]); ?></p>
  192. <?php endif; ?>
  193. </div>
  194. <div class="mc-card">
  195. <p class="mc-label">Lokale Backups</p>
  196. <p class="mc-value"><?php echo count($status["backups"]); ?></p>
  197. <p class="mc-muted">Letztes: <?php echo managePanelEscape($status["last_backup_at"] ?? "nie"); ?></p>
  198. </div>
  199. <div class="mc-card">
  200. <p class="mc-label">Offene Migrationen</p>
  201. <p class="mc-value"><?php echo count($status["pending_migrations"]); ?></p>
  202. </div>
  203. </div>
  204. <?php if ($status["update"] !== null && $status["update"]["available"]): ?>
  205. <p class="mc-alert mc-warn">
  206. Version <?php echo managePanelEscape($status["update"]["latest"]); ?> steht bereit.
  207. Vor dem Ausrollen sollte ein aktuelles Backup vorliegen.
  208. </p>
  209. <?php endif; ?>
  210. <h2>Aktionen</h2>
  211. <div class="mc-row">
  212. <form method="POST">
  213. <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
  214. <input type="hidden" name="action" value="backup">
  215. <button type="submit" class="mc-btn">Backup jetzt erstellen</button>
  216. </form>
  217. <form method="POST" onsubmit="return confirm('Update jetzt ausrollen? Dateien werden überschrieben.');">
  218. <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
  219. <input type="hidden" name="action" value="update">
  220. <label class="mc-muted">
  221. <input type="checkbox" name="force" value="1"> erneut ausrollen
  222. </label>
  223. <button type="submit" class="mc-btn">Update ausrollen</button>
  224. </form>
  225. <?php if ($status["pending_migrations"] !== []): ?>
  226. <form method="POST">
  227. <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
  228. <input type="hidden" name="action" value="migrate">
  229. <button type="submit" class="mc-btn sec">Migrationen ausführen</button>
  230. </form>
  231. <?php endif; ?>
  232. <form method="POST">
  233. <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
  234. <input type="hidden" name="action" value="heartbeat">
  235. <button type="submit" class="mc-btn sec">Status melden</button>
  236. </form>
  237. </div>
  238. <?php if ($status["pending_migrations"] !== []): ?>
  239. <h2>Offene Migrationen</h2>
  240. <ul>
  241. <?php foreach ($status["pending_migrations"] as $migration): ?>
  242. <li class="mc-mono"><?php echo managePanelEscape($migration["id"]); ?></li>
  243. <?php endforeach; ?>
  244. </ul>
  245. <?php endif; ?>
  246. <h2>Lokale Backups</h2>
  247. <?php if ($status["backups"] === []): ?>
  248. <p class="mc-muted">Es wurde noch kein Backup erstellt.</p>
  249. <?php else: ?>
  250. <div class="mc-scroll">
  251. <table class="mc-table">
  252. <thead>
  253. <tr>
  254. <th>Datei</th>
  255. <th>Erstellt</th>
  256. <th>Auslöser</th>
  257. <th>Dateien</th>
  258. <th>Größe</th>
  259. <th>Upload</th>
  260. <th></th>
  261. </tr>
  262. </thead>
  263. <tbody>
  264. <?php foreach ($status["backups"] as $backup): ?>
  265. <tr>
  266. <td class="mc-mono"><?php echo managePanelEscape($backup["filename"] ?? ""); ?></td>
  267. <td><?php echo managePanelEscape($backup["created_at"] ?? ""); ?></td>
  268. <td><?php echo managePanelEscape($backup["trigger"] ?? ""); ?></td>
  269. <td><?php echo (int) ($backup["file_count"] ?? 0); ?></td>
  270. <td><?php echo managePanelEscape(manageFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
  271. <td>
  272. <?php
  273. $uploads = is_array($backup["remote_uploads"] ?? null) ? $backup["remote_uploads"] : [];
  274. if ($uploads === []) {
  275. echo "–";
  276. } else {
  277. foreach ($uploads as $upload) {
  278. $ok = !empty($upload["success"]);
  279. echo managePanelEscape((string) ($upload["target"] ?? "?")) .
  280. ": " . ($ok ? "OK" : "Fehler") . "<br>";
  281. }
  282. }
  283. ?>
  284. </td>
  285. <td>
  286. <form method="POST">
  287. <input type="hidden" name="csrf_token" value="<?php echo managePanelEscape(managePanelCsrfToken()); ?>">
  288. <input type="hidden" name="action" value="download">
  289. <input type="hidden" name="filename" value="<?php echo managePanelEscape($backup["filename"] ?? ""); ?>">
  290. <button type="submit" class="mc-btn sec">Herunterladen</button>
  291. </form>
  292. </td>
  293. </tr>
  294. <?php endforeach; ?>
  295. </tbody>
  296. </table>
  297. </div>
  298. <?php endif; ?>
  299. <?php
  300. $capabilityWarnings = [];
  301. foreach ($capabilities as $type => $capability) {
  302. if ($capability["configured"] && !$capability["available"]) {
  303. $capabilityWarnings[] = $type;
  304. }
  305. }
  306. ?>
  307. <?php if ($capabilityWarnings !== []): ?>
  308. <p class="mc-alert mc-warn">
  309. Konfigurierte Backup-Ziele ohne Systemunterstützung:
  310. <?php echo managePanelEscape(implode(", ", $capabilityWarnings)); ?>.
  311. Diese Uploads werden fehlschlagen.
  312. </p>
  313. <?php endif; ?>
  314. <?php foreach ($status["errors"] as $error): ?>
  315. <p class="mc-alert mc-warn"><?php echo managePanelEscape($error); ?></p>
  316. <?php endforeach; ?>
  317. </div>
  318. </body>
  319. </html>