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 = '
'; foreach ($fields as $k => $v) $h .= ''; return $h . '
'; } site_header($loggedIn ? $tabs[$tab][0] . ' · Verwaltung' : 'Anmelden', $tab, $loggedIn); ?>

Anmelden

Verwaltung der DeskHub-Plugins, Themes und Apps.

query('SELECT * FROM push_installs ORDER BY COALESCE(last_used_at, created_at) DESC LIMIT 200')->fetchAll(); $plugins = site_items('plugin'); $themes = site_items('theme'); ?>

Übersicht

Alles Wichtige auf einen Blick: veröffentlichte Pakete, aktuelle App-Versionen und der Zustand der Dienste.

Plugins
veröffentlicht
Themes
veröffentlicht
Windows
aktuelle App-Version
Android
aktuelle App-Version
DeskHub-Server
aktuelles System-Update
Push-Geräte
angemeldete Installationen

Systemstatus

Einrichtung dieser Webseite

Signierschlüssel
Push-Dienst
Zugangsschlüssel für Katalog und Downloads

Schnellzugriff

Häufige Aufgaben

Plugin oder Theme hochladen App-Version veröffentlichen DeskHub-Einstellungen

Pakete

Plugins und Themes für den DeskHub-Katalog hochladen und verwalten.

Neues Paket veröffentlichen

ZIP-Datei mit plugin.json bzw. Theme-CSS

Nur für Themes (bei Plugins steht alles in der plugin.json)

Bei einem Update von Plugin oder Theme bleibt das vorhandene Bild erhalten: einfach weglassen. Ändern oder entfernen kannst du es unten bei dem Eintrag.

['Plugins', $plugins], 'theme' => ['Themes', $themes]] as $t => [$label, $items]): ?>

veröffentlicht

Noch keine veröffentlicht.
★ Empfohlen 🌐 () (int) $it['id']], !empty($it['featured']) ? 'Empfehlung entfernen' : 'Als Empfehlung markieren', 'ghost sm') ?>
Vorschaubild von <?= site_h($it['name']) ?>kein Bild
(int) $it['id']], 'Bild entfernen', 'link danger', 'Vorschaubild wirklich entfernen?'); ?>
Downloads signiert' : 'nicht signiert' ?> (int) $v['id']], 'Entfernen', 'link danger', 'Version wirklich entfernen?') ?>

Apps

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.

Neue App-Version hochladen

Windows-Setups dürfen bis 300 MB groß sein (PHP-Grenzen upload_max_filesize / post_max_size beachten).

$p['label'], array_diff_key(site_app_platforms(), ['server' => 1])) as $pf => $pl): $mine = array_values(array_filter($appReleases, fn($r) => $r['platform'] === $pf)); $cur = $latestApp[$pf]; ?>

aktuell ' . site_h($cur['version']) . '' : 'noch nichts veröffentlicht' ?>
Noch keine Version veröffentlicht.
VersionDatumGröße
aktuell' : '' ?> MB (int) $release['id']], 'Entfernen', 'link danger', 'Diese App-Version wirklich entfernen?') ?>
$r['platform'] === 'server')); $serverCur = $latestApp['server']; ?>

DeskHub-Update

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.

Kein Signierschlüssel eingerichtet: Die Pakete werden nicht signiert, DeskHub mit eingebautem Herausgeber-Schlüssel lehnt sie ab. Siehe Einstellungen.

Neue Version hochladen

ZIP des Projekts (ohne config/, Datenbank, Backups, Uploads; diese Ordner werden beim Update ohnehin nie überschrieben).

aktuell ' . site_h($serverCur['version']) . '' : 'noch nichts veröffentlicht' ?>

Veröffentlichte Versionen

DeskHub zeigt die neuesten 20 unter „Bestimmte Version installieren“.

Noch keine Version veröffentlicht. Bis dahin nutzt DeskHub die Ausweichquelle (Gitea).
VersionDatumGrößeÄnderungen
aktuell' : '' ?> MB (int) $release['id']], 'Entfernen', 'link danger', 'Diese Version wirklich entfernen?') ?>

Push-Dienst

Zentraler Push-Dienst der Android-App. Hier siehst du die angemeldeten DeskHub-Installationen.

Angemeldete Installationen

Einträge

Noch keine DeskHub-Installation angemeldet.
NameAngemeldetZuletzt genutztGesendetStatus
gesperrt' : 'aktiv' ?> $p['id']], $blocked ? 'Entsperren' : 'Sperren') ?>   $p['id']], 'Löschen', 'link danger', 'Installation wirklich löschen?') ?>

Einstellungen

Einrichtung für DeskHub und Konto.

Signierschlüsselphp einrichten.php) – ohne Schlüssel ist der geprüfte Modus nicht nutzbar.' ?>

Einstellungen für DeskHub (offizieller Katalog)

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.

Konto

Du bist als Administrator angemeldet.