0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Strict']); session_name('plugin_site_admin'); session_start(); $cfg = site_config(); $error = null; $message = null; function admin_csrf(): string { return $_SESSION['csrf'] ??= bin2hex(random_bytes(24)); } function admin_check_csrf(): bool { return hash_equals($_SESSION['csrf'] ?? '', (string) ($_POST['csrf'] ?? '')); } /** Höchstens 5 Fehlversuche pro IP in 15 Minuten. */ function admin_throttled(bool $record = false): bool { $file = site_storage() . '/login_attempts.json'; $data = is_file($file) ? (json_decode((string) file_get_contents($file), true) ?: []) : []; $ip = $_SERVER['REMOTE_ADDR'] ?? '?'; $now = time(); $data[$ip] = array_values(array_filter($data[$ip] ?? [], fn($t) => $t > $now - 900)); if ($record) { $data[$ip][] = $now; file_put_contents($file, json_encode($data), LOCK_EX); } return count($data[$ip]) >= 5; } $loggedIn = !empty($_SESSION['admin']); /** PHP verwirft bei zu großem Upload das ganze Formular (kein $_POST, kein Token) – das ist keine Abmeldung. */ $tooBig = $_SERVER['REQUEST_METHOD'] === 'POST' && !$_POST && !$_FILES && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0; if ($tooBig) { $error = 'Die Datei ist größer als die Servergrenze (post_max_size ' . ini_get('post_max_size') . ', upload_max_filesize ' . ini_get('upload_max_filesize') . '). Bitte die Grenzen erhöhen bzw. das Docker-Image neu bauen. Du bleibst angemeldet.'; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$tooBig) { $action = $_POST['action'] ?? ''; if ($action === 'login') { if ($cfg['admin_password_hash'] === '') { $error = 'Die Verwaltung ist nicht eingerichtet: In config.php fehlt admin_password_hash.'; } elseif (admin_throttled()) { $error = 'Zu viele Fehlversuche. Bitte in 15 Minuten erneut versuchen.'; } elseif (hash_equals((string) $cfg['admin_user'], (string) ($_POST['user'] ?? '')) && password_verify((string) ($_POST['password'] ?? ''), (string) $cfg['admin_password_hash'])) { session_regenerate_id(true); $_SESSION['admin'] = true; header('Location: admin.php'); exit; } else { admin_throttled(true); usleep(400000); $error = 'Benutzername oder Passwort ist falsch.'; } } elseif (!$loggedIn || !admin_check_csrf()) { http_response_code(403); $error = 'Ungültige Anfrage. Bitte neu anmelden.'; $loggedIn = false; } elseif ($action === 'logout') { session_destroy(); header('Location: admin.php'); exit; } elseif ($action === 'publish') { $zip = $_FILES['package'] ?? []; if (($zip['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $error = 'Upload-Fehler (Code ' . ($zip['error'] ?? '?') . '). Prüft upload_max_filesize und post_max_size.'; } elseif (strtolower(pathinfo((string) $zip['name'], PATHINFO_EXTENSION)) !== 'zip') { $error = 'Bitte eine ZIP-Datei hochladen.'; } else { $prev = $_FILES['preview'] ?? []; $hasPreview = ($prev['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK; $res = site_publish((string) ($_POST['type'] ?? ''), $zip['tmp_name'], $_POST, $hasPreview ? $prev['tmp_name'] : null, $hasPreview ? (string) $prev['name'] : null); if ($res['ok']) $message = "Veröffentlicht: {$res['slug']} {$res['version']} (" . ($res['signed'] ? 'signiert' : 'NICHT signiert, kein Signierschlüssel eingerichtet') . ').'; else $error = $res['error']; } } elseif ($action === 'publish_app') { $upload = $_FILES['app_file'] ?? []; if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $error = 'App-Upload fehlgeschlagen (Code ' . ($upload['error'] ?? '?') . '). Prüft upload_max_filesize, post_max_size und die Servergrenze.'; } else { $res = site_publish_app((string) ($_POST['platform'] ?? ''), (string) $upload['tmp_name'], trim((string) ($_POST['version'] ?? '')), (string) ($_POST['notes'] ?? '')); if ($res['ok']) $message = 'App-Version ' . $res['version'] . ' veröffentlicht.'; else $error = $res['error']; } } elseif ($action === 'delete_app') { $id = (int) ($_POST['id'] ?? 0); $stmt = site_db()->prepare('SELECT platform,file FROM app_releases WHERE id = ?'); $stmt->execute([$id]); $release = $stmt->fetch(); if ($release) { site_db()->prepare('DELETE FROM app_releases WHERE id = ?')->execute([$id]); @unlink(site_storage() . '/apps/' . $release['platform'] . '/' . basename($release['file'])); $message = 'App-Version entfernt.'; } else $error = 'App-Version wurde nicht gefunden.'; } elseif (in_array($action, ['push_block', 'push_unblock', 'push_delete'], true)) { $pid = (string) ($_POST['id'] ?? ''); if (preg_match('/^[a-f0-9]{24}$/', $pid)) { $pp = push_db(); if ($action === 'push_delete') $pp->prepare('DELETE FROM push_installs WHERE id = ?')->execute([$pid]); else $pp->prepare('UPDATE push_installs SET blocked = ? WHERE id = ?')->execute([$action === 'push_block' ? 1 : 0, $pid]); $message = ['push_block' => 'Installation gesperrt.', 'push_unblock' => 'Installation entsperrt.', 'push_delete' => 'Installation gelöscht (sie meldet sich beim nächsten Senden von selbst neu an).'][$action]; } else { $error = 'Ungültige Installation.'; } } elseif ($action === 'set_preview' || $action === 'remove_preview') { $img = $_FILES['preview'] ?? []; if ($action === 'set_preview' && ($img['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $error = 'Bitte ein Bild auswählen (PNG, JPG oder WebP, höchstens 1 MB).'; } else { $res = site_set_preview((int) ($_POST['id'] ?? 0), $action === 'set_preview' ? (string) $img['tmp_name'] : null, $action === 'set_preview' ? (string) $img['name'] : null); if ($res['ok']) $message = $action === 'set_preview' ? 'Vorschaubild geändert. Es bleibt bei weiteren Versionen erhalten.' : 'Vorschaubild entfernt.'; else $error = $res['error']; } } elseif ($action === 'feature' || $action === 'unfeature') { $message = site_set_featured((int) ($_POST['id'] ?? 0), $action === 'feature') ? ($action === 'feature' ? 'Als „Empfohlen“ markiert. DeskHub zeigt es nach der nächsten Katalog-Aktualisierung (spätestens in wenigen Minuten).' : 'Empfehlung entfernt.') : null; $error = $message ? null : 'Eintrag nicht gefunden.'; } elseif ($action === 'delete') { $message = site_delete_version((int) ($_POST['id'] ?? 0)) ? 'Version entfernt.' : null; $error = $message ? null : 'Version nicht gefunden.'; } } $tabs = site_tabs(); $tab = (string) ($_GET['tab'] ?? 'overview'); if (!isset($tabs[$tab])) $tab = 'overview'; $csrf = $loggedIn ? admin_csrf() : ''; /** Kleines POST-Formular mit nur einem Knopf. */ function admin_btn(string $action, array $fields, string $label, string $class = 'link', string $confirm = ''): string { $h = '
'; } site_header($loggedIn ? $tabs[$tab][0] . ' · Verwaltung' : 'Anmelden', $tab, $loggedIn); ?>Alles Wichtige auf einen Blick: veröffentlichte Pakete, aktuelle App-Versionen und der Zustand der Dienste.
Einrichtung dieser Webseite
Häufige Aufgaben
Plugin oder Theme hochladen App-Version veröffentlichen DeskHub-Einstellungen
Plugins und Themes für den DeskHub-Katalog hochladen und verwalten.
ZIP-Datei mit plugin.json bzw. Theme-CSS
= count($items) ?> veröffentlicht
| = site_h($v['version']) ?> | = site_h(substr($v['created_at'], 0, 10)) ?> | = (int) $v['downloads'] ?> Downloads | = $v['signature'] !== '' ? 'signiert' : 'nicht signiert' ?> | = admin_btn('delete', ['id' => (int) $v['id']], 'Entfernen', 'link danger', 'Version wirklich entfernen?') ?> |
Windows-, Android-App, Minecraft-Plugin, WordPress-Plugin und Chrome-Erweiterung veröffentlichen. Die Apps zeigen ihren Nutzern automatisch „Update verfügbar“, sobald hier eine höhere Version liegt.
Windows-Setups dürfen bis 300 MB groß sein (PHP-Grenzen upload_max_filesize / post_max_size beachten).
= site_h(site_app_platforms()[$pf]['what']) ?>
| Version | Datum | Größe | |
|---|---|---|---|
| = site_h($release['version']) ?> = $isLatest ? 'aktuell' : '' ?> | = site_h(substr($release['created_at'], 0, 16)) ?> | = number_format((int) $release['size'] / 1048576, 1, ',', '.') ?> MB | = admin_btn('delete_app', ['id' => (int) $release['id']], 'Entfernen', 'link danger', 'Diese App-Version wirklich entfernen?') ?> |
Das ZIP des DeskHub-Servers für das System-Update. Alle DeskHub-Installationen holen neue Versionen von hier (Adminportal → System-Update). Die Version wird aus version.php im Paket gelesen, das Paket wird mit dem Schlüssel dieser Seite automatisch signiert.
ZIP des Projekts (ohne config/, Datenbank, Backups, Uploads; diese Ordner werden beim Update ohnehin nie überschrieben).
DeskHub zeigt die neuesten 20 unter „Bestimmte Version installieren“.
| Version | Datum | Größe | Änderungen | |
|---|---|---|---|---|
| = site_h($release['version']) ?> = $isLatest ? 'aktuell' : '' ?> | = site_h(substr($release['created_at'], 0, 16)) ?> | = number_format((int) $release['size'] / 1048576, 1, ',', '.') ?> MB | = site_h(mb_substr((string) $release['notes'], 0, 120)) ?> | = admin_btn('delete_app', ['id' => (int) $release['id']], 'Entfernen', 'link danger', 'Diese Version wirklich entfernen?') ?> |
Zentraler Push-Dienst der Android-App. Hier siehst du die angemeldeten DeskHub-Installationen.
= count($pushRows) ?> Einträge
| Name | Angemeldet | Zuletzt genutzt | Gesendet | Status | |
|---|---|---|---|---|---|
| = site_h($p['name'] !== '' ? $p['name'] : $p['id']) ?> | = site_h(substr((string) $p['created_at'], 0, 16)) ?> | = $p['last_used_at'] ? site_h(substr((string) $p['last_used_at'], 0, 16)) : 'noch nie' ?> | = (int) $p['sent_total'] ?> | = $blocked ? 'gesperrt' : 'aktiv' ?> | = admin_btn($blocked ? 'push_unblock' : 'push_block', ['id' => $p['id']], $blocked ? 'Entsperren' : 'Sperren') ?> = admin_btn('push_delete', ['id' => $p['id']], 'Löschen', 'link danger', 'Installation wirklich löschen?') ?> |
Einrichtung für DeskHub und Konto.
php einrichten.php) – ohne Schlüssel ist der geprüfte Modus nicht nutzbar.' ?>Diesen Inhalt in includes/catalog_defaults.php von DeskHub einsetzen, bevor ihr ein Release baut. Dann installieren eure Kunden Plugins nur aus diesem Katalog, und nur signierte.
= site_h(site_deskhub_defaults_snippet()) ?>
Du bist als Administrator angemeldet.