Files
deskhub/plugin-webseite/admin.php
T
2026-10-24 20:17:11 +00:00

167 lines
12 KiB
PHP

<?php
declare(strict_types=1);
/** Verwaltung der Webseite: Anmelden, Pakete veröffentlichen, Versionen entfernen. */
require_once __DIR__ . '/inc/layout.php';
require_once __DIR__ . '/inc/push.php';
session_set_cookie_params(['lifetime' => 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']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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 (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 === '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.';
}
}
site_header('Verwaltung');
?>
<h1>Verwaltung</h1>
<?php if ($error): ?><div class="alert err"><?= site_h($error) ?></div><?php endif; ?>
<?php if ($message): ?><div class="alert okbox"><?= site_h($message) ?></div><?php endif; ?>
<?php if (!$loggedIn): ?>
<form method="post" class="form narrow">
<input type="hidden" name="action" value="login">
<label>Benutzername<input type="text" name="user" autocomplete="username" required></label>
<label>Passwort<input type="password" name="password" autocomplete="current-password" required></label>
<button type="submit">Anmelden</button>
</form>
<?php else: ?>
<form method="post" class="logout"><input type="hidden" name="action" value="logout"><input type="hidden" name="csrf" value="<?= site_h(admin_csrf()) ?>"><button type="submit" class="link">Abmelden</button></form>
<p class="<?= site_public_key() !== '' ? 'ok' : 'warn' ?>"><?= site_public_key() !== '' ? '✓ Signierschlüssel eingerichtet: Neue Pakete werden automatisch signiert.' : 'Kein Signierschlüssel eingerichtet (signing_key_file in config.php): Pakete werden unsigniert veröffentlicht.' ?></p>
<details class="form" style="max-width:none"><summary><strong>Einstellungen für DeskHub (offizieller Katalog)</strong></summary>
<p class="muted">Diesen Inhalt in <code>includes/catalog_defaults.php</code> von DeskHub einsetzen, bevor ihr ein Release baut. Dann installieren eure Kunden Plugins nur aus diesem Katalog, und nur signierte.</p>
<?php if (site_public_key() === ''): ?><p class="warn">Es ist noch kein Signierschlüssel eingerichtet (php einrichten.php). Ohne Schlüssel ist der geprüfte Modus nicht nutzbar.</p><?php endif; ?>
<pre class="key"><?= site_h(site_deskhub_defaults_snippet()) ?></pre>
</details>
<h2>Neues Paket veröffentlichen</h2>
<form method="post" enctype="multipart/form-data" class="form">
<input type="hidden" name="action" value="publish"><input type="hidden" name="csrf" value="<?= site_h(admin_csrf()) ?>">
<label>Art<select name="type" id="type"><option value="plugin">Plugin</option><option value="theme">Theme</option></select></label>
<label>ZIP-Paket<input type="file" name="package" accept=".zip" required></label>
<fieldset class="theme-only"><legend>Nur für Themes (bei Plugins steht alles in der plugin.json)</legend>
<label>Kennung (Slug)<input type="text" name="slug" placeholder="nordlicht" pattern="[a-z0-9][a-z0-9_\-]{1,40}"></label>
<label>Name<input type="text" name="name" placeholder="Nordlicht"></label>
<label>Version<input type="text" name="version" placeholder="1.0.0 (sonst aus dem CSS-Kopf)"></label>
<label>Autor<input type="text" name="author"></label>
<label>Beschreibung<textarea name="description" rows="3"></textarea></label>
</fieldset>
<label>Vorschaubild (optional, PNG/JPG/WebP, höchstens 1 MB)<input type="file" name="preview" accept="image/png,image/jpeg,image/webp"></label>
<button type="submit">Veröffentlichen</button>
</form>
<h2>Push-Dienst (Android-App)</h2>
<?php $pushSa = push_service_account(); $pushRows = push_db()->query('SELECT * FROM push_installs ORDER BY COALESCE(last_used_at, created_at) DESC LIMIT 200')->fetchAll(); ?>
<p class="<?= $pushSa ? 'ok' : 'warn' ?>"><?= $pushSa ? '✓ Firebase-Dienstkonto eingebaut (Projekt ' . site_h($pushSa['project_id']) . ').' : 'Kein Firebase-Dienstkonto eingebaut: schluessel/firebase-dienstkonto.json fehlt beim Bauen, Push-Nachrichten sind nicht möglich.' ?></p>
<?php if (!$pushRows): ?><p class="muted">Noch keine DeskHub-Installation angemeldet.</p><?php else: ?>
<table class="admin">
<caption>Angemeldete DeskHub-Installationen (<?= count($pushRows) ?>)</caption>
<?php foreach ($pushRows as $p): ?>
<tr><td><?= site_h($p['name'] !== '' ? $p['name'] : $p['id']) ?></td><td><?= site_h(substr((string) $p['created_at'], 0, 16)) ?></td><td><?= $p['last_used_at'] ? 'zuletzt ' . site_h(substr((string) $p['last_used_at'], 0, 16)) : 'noch nie genutzt' ?></td><td><?= (int) $p['sent_total'] ?> gesendet</td>
<td><?= (int) $p['blocked'] === 1 ? '<span class="warn">gesperrt</span>' : '<span class="ok">aktiv</span>' ?></td>
<td><form method="post"><input type="hidden" name="action" value="<?= (int) $p['blocked'] === 1 ? 'push_unblock' : 'push_block' ?>"><input type="hidden" name="csrf" value="<?= site_h(admin_csrf()) ?>"><input type="hidden" name="id" value="<?= site_h($p['id']) ?>"><button type="submit" class="link"><?= (int) $p['blocked'] === 1 ? 'Entsperren' : 'Sperren' ?></button></form></td>
<td><form method="post" data-confirm="Installation wirklich löschen?"><input type="hidden" name="action" value="push_delete"><input type="hidden" name="csrf" value="<?= site_h(admin_csrf()) ?>"><input type="hidden" name="id" value="<?= site_h($p['id']) ?>"><button type="submit" class="link">Löschen</button></form></td></tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>Veröffentlichte Pakete</h2>
<?php foreach (['plugin' => 'Plugins', 'theme' => 'Themes'] as $t => $label): $items = site_items($t); ?>
<h3><?= site_h($label) ?></h3>
<?php if (!$items): ?><p class="muted">Noch keine.</p><?php endif; ?>
<?php foreach ($items as $it): ?>
<table class="admin">
<caption><?= site_h($it['name']) ?> <span class="muted small"><?= site_h($it['slug']) ?></span> <?= !empty($it['featured']) ? '<span class="ok">★ Empfohlen</span>' : '' ?> <?php if (!empty($it['language']) && is_array($__l = json_decode((string) $it['language'], true))): ?><span class="muted small">🌐 Sprachpaket: <?= site_h((string) $__l['name']) ?> (<?= site_h((string) $__l['code']) ?>)</span><?php endif; ?>
<form method="post" style="display:inline"><input type="hidden" name="action" value="<?= !empty($it['featured']) ? 'unfeature' : 'feature' ?>"><input type="hidden" name="csrf" value="<?= site_h(admin_csrf()) ?>"><input type="hidden" name="id" value="<?= (int) $it['id'] ?>"><button type="submit" class="link"><?= !empty($it['featured']) ? 'Empfehlung entfernen' : 'Als Empfehlung markieren' ?></button></form></caption>
<?php foreach (site_versions((int) $it['id']) as $v): ?>
<tr><td><?= site_h($v['version']) ?></td><td><?= site_h(substr($v['created_at'], 0, 16)) ?></td><td><?= (int) $v['downloads'] ?> Downloads</td>
<td><?= $v['signature'] !== '' ? '<span class="ok">signiert</span>' : '<span class="warn">nicht signiert</span>' ?></td>
<td><form method="post" data-confirm="Version wirklich entfernen?"><input type="hidden" name="action" value="delete"><input type="hidden" name="csrf" value="<?= site_h(admin_csrf()) ?>"><input type="hidden" name="id" value="<?= (int) $v['id'] ?>"><button type="submit" class="link danger">Entfernen</button></form></td></tr>
<?php endforeach; ?>
</table>
<?php endforeach; ?>
<?php endforeach; ?>
<script src="assets/admin.js"></script>
<?php endif; ?>
<?php site_footer();