Upload via GUI (32 Dateien)

This commit is contained in:
2026-11-01 11:17:07 +00:00
parent e1c6f8dfca
commit 53f8bf0577
7 changed files with 626 additions and 491 deletions
+2
View File
@@ -7,6 +7,8 @@ Eigenständige Webseite, von der DeskHub-Installationen Plugins und Kundenportal
- Verwaltung mit allen Paketen, Versionen, Signaturstatus und Download-Zähler (keine öffentliche Übersichtsseite: Die Startseite führt zur Verwaltung)
- Katalog für DeskHub: `catalog.php?type=plugins` und `catalog.php?type=themes`
- Verwaltung (`admin.php`): Pakete hochladen, Versionen entfernen, Vorschaubilder
- **Vorschaubild nur einmal:** Das Bild eines Plugins oder Themes bleibt bei jeder neuen Version erhalten, auch wenn du die alte Version vorher entfernst. Beim Eintrag in der Verwaltung lässt es sich ersetzen oder entfernen. Liegt im ZIP eine Datei `vorschau.png`, wird sie beim ersten Veröffentlichen automatisch verwendet.
- **WordPress-Plugin und Chrome-Erweiterung** (Tab „Apps“): ZIP hochladen, die Versionsnummer wird aus dem Plugin-Kopf (`Version:`) bzw. der `manifest.json` gelesen (eine eingetippte Nummer hat Vorrang). Abruf der neuesten Version: `app-release.php?platform=wordpress` oder `platform=chrome` (mit `&download=1` als Datei).
- Prüft jedes Paket beim Hochladen (Dateitypen, Pfade, `plugin.json`, Berechtigungen) und **signiert es automatisch** mit eurem privaten Schlüssel
- Veröffentlichte Versionen sind unveränderlich: Für Änderungen gibt es eine neue Version
+26 -10
View File
@@ -84,7 +84,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$tooBig) {
$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 veröffentlicht.';
if ($res['ok']) $message = 'App-Version ' . $res['version'] . ' veröffentlicht.';
else $error = $res['error'];
}
} elseif ($action === 'delete_app') {
@@ -107,6 +107,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$tooBig) {
} 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.';
@@ -144,7 +153,7 @@ site_header($loggedIn ? $tabs[$tab][0] . ' · Verwaltung' : 'Anmelden', $tab, $l
<?php else:
$signed = site_public_key() !== '';
$appReleases = site_app_releases();
$latestApp = ['android' => null, 'windows' => null, 'minecraft' => null];
$latestApp = array_fill_keys(array_keys(site_app_platforms()), null);
foreach ($appReleases as $r) if (array_key_exists($r['platform'], $latestApp) && $latestApp[$r['platform']] === null) $latestApp[$r['platform']] = $r;
$pushSa = push_service_account();
$pushRows = push_db()->query('SELECT * FROM push_installs ORDER BY COALESCE(last_used_at, created_at) DESC LIMIT 200')->fetchAll();
@@ -200,7 +209,8 @@ site_header($loggedIn ? $tabs[$tab][0] . ' · Verwaltung' : 'Anmelden', $tab, $l
</div>
<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>
<label>Vorschaubild (nur beim ersten Mal nötig, PNG/JPG/WebP, höchstens 1 MB)<input type="file" name="preview" accept="image/png,image/jpeg,image/webp"></label>
<p class="muted small">Bei einem Update von Plugin oder Theme bleibt das vorhandene Bild erhalten: einfach weglassen. Ändern oder entfernen kannst du es unten bei dem Eintrag.</p>
<div><button type="submit">Veröffentlichen</button></div>
</form>
</section>
@@ -218,6 +228,12 @@ site_header($loggedIn ? $tabs[$tab][0] . ' · Verwaltung' : 'Anmelden', $tab, $l
<?php if (!empty($it['language']) && is_array($__l = json_decode((string) $it['language'], true))): ?><span class="badge">🌐 <?= site_h((string) $__l['name']) ?> (<?= site_h((string) $__l['code']) ?>)</span><?php endif; ?>
<?= admin_btn(!empty($it['featured']) ? 'unfeature' : 'feature', ['id' => (int) $it['id']], !empty($it['featured']) ? 'Empfehlung entfernen' : 'Als Empfehlung markieren', 'ghost sm') ?>
</div>
<div class="previewrow">
<?php if (!empty($it['preview'])): ?><img class="thumb-sm" src="preview.php?file=<?= site_h(rawurlencode((string) $it['preview'])) ?>" alt="Vorschaubild von <?= site_h($it['name']) ?>"><?php else: ?><span class="thumb-sm none">kein Bild</span><?php endif; ?>
<form method="post" enctype="multipart/form-data" class="inline"><input type="hidden" name="action" value="set_preview"><input type="hidden" name="csrf" value="<?= site_h($csrf) ?>"><input type="hidden" name="id" value="<?= (int) $it['id'] ?>">
<input type="file" name="preview" accept="image/png,image/jpeg,image/webp" required> <button type="submit" class="ghost sm"><?= !empty($it['preview']) ? 'Bild ersetzen' : 'Bild hinzufügen' ?></button></form>
<?php if (!empty($it['preview'])) echo admin_btn('remove_preview', ['id' => (int) $it['id']], 'Bild entfernen', 'link danger', 'Vorschaubild wirklich entfernen?'); ?>
</div>
<div class="tablewrap flat"><table>
<?php foreach (site_versions((int) $it['id']) as $v): ?>
<tr><td><strong><?= site_h($v['version']) ?></strong></td><td class="muted"><?= site_h(substr($v['created_at'], 0, 10)) ?></td><td><?= (int) $v['downloads'] ?> Downloads</td>
@@ -232,24 +248,24 @@ site_header($loggedIn ? $tabs[$tab][0] . ' · Verwaltung' : 'Anmelden', $tab, $l
</div>
<?php elseif ($tab === 'apps'): ?>
<div class="pagehead"><div><h1>Apps</h1><p class="sub">Windows-, Android-App und Minecraft-Plugin veröffentlichen. Die Apps zeigen ihren Nutzern automatisch „Update verfügbar“, sobald hier eine höhere Version liegt.</p></div></div>
<div class="pagehead"><div><h1>Apps</h1><p class="sub">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.</p></div></div>
<section class="card">
<header><div><h2>Neue App-Version hochladen</h2><p>Windows-Setups dürfen bis 300 MB groß sein (PHP-Grenzen <code>upload_max_filesize</code> / <code>post_max_size</code> beachten).</p></div></header>
<form method="post" enctype="multipart/form-data" class="form">
<input type="hidden" name="action" value="publish_app"><input type="hidden" name="csrf" value="<?= site_h($csrf) ?>">
<div class="row2">
<label>Plattform<select name="platform"><option value="windows">Windows (EXE)</option><option value="android">Android (APK)</option><option value="minecraft">Minecraft-Plugin (JAR)</option></select></label>
<label>Versionsnummer<input type="text" name="version" placeholder="3.4.0" pattern="[0-9]+[.][0-9]+[.][0-9]+([+-][A-Za-z0-9.-]+)?" required></label>
<label>Plattform<select name="platform" id="platform"><?php foreach (site_app_platforms() as $pk => $pv): ?><option value="<?= site_h($pk) ?>" data-zip="<?= $pv['ext'] === 'zip' ? '1' : '0' ?>"><?= site_h($pv['label'] . ' – ' . $pv['what']) ?></option><?php endforeach; ?></select></label>
<label>Versionsnummer <span class="muted small" id="versionhint" hidden>(leer lassen: wird aus dem Paket gelesen)</span><input type="text" id="appversion" name="version" placeholder="3.4.0" pattern="[0-9]+[.][0-9]+[.][0-9]+([+-][A-Za-z0-9.-]+)?" required></label>
</div>
<label>Installationsdatei<input type="file" name="app_file" accept=".apk,.exe,.jar,application/vnd.android.package-archive,application/vnd.microsoft.portable-executable" required></label>
<label>Installationsdatei<input type="file" name="app_file" accept=".apk,.exe,.jar,.zip,application/vnd.android.package-archive,application/vnd.microsoft.portable-executable,application/zip" required></label>
<label>Änderungen (optional, erscheint im Update-Hinweis)<textarea name="notes" rows="2" maxlength="2000"></textarea></label>
<div><button type="submit">App-Version hochladen</button></div>
</form>
</section>
<div class="cols three">
<?php foreach (['windows' => 'Windows', 'android' => 'Android', 'minecraft' => 'Minecraft'] as $pf => $pl): $mine = array_values(array_filter($appReleases, fn($r) => $r['platform'] === $pf)); $cur = $latestApp[$pf]; ?>
<div class="cols">
<?php foreach (array_map(fn($p) => $p['label'], site_app_platforms()) as $pf => $pl): $mine = array_values(array_filter($appReleases, fn($r) => $r['platform'] === $pf)); $cur = $latestApp[$pf]; ?>
<section class="card">
<header><div><h2><?= site_h($pl) ?></h2><p><?= ['windows' => 'Setup (EXE)', 'android' => 'Installationsdatei (APK)', 'minecraft' => 'Plugin (JAR)'][$pf] ?></p></div>
<header><div><h2><?= site_h($pl) ?></h2><p><?= site_h(site_app_platforms()[$pf]['what']) ?></p></div>
<?= $cur ? '<span class="badge ok">aktuell ' . site_h($cur['version']) . '</span>' : '<span class="badge">noch nichts veröffentlicht</span>' ?></header>
<?php if (!$mine): ?><div class="empty">Noch keine Version veröffentlicht.</div><?php else: ?>
<div class="tablewrap"><table>
+3 -2
View File
@@ -29,9 +29,10 @@ if (($_GET['download'] ?? '') !== '1') {
exit;
}
$filename = ['android' => 'DeskHub.apk', 'windows' => 'DeskHub-Setup.exe', 'minecraft' => 'DeskHub-Minecraft.jar'][$platform];
$kind = site_app_platforms()[$platform];
$filename = $kind['file'];
while (ob_get_level() > 0) @ob_end_clean();
header('Content-Type: ' . ['android' => 'application/vnd.android.package-archive', 'windows' => 'application/vnd.microsoft.portable-executable', 'minecraft' => 'application/java-archive'][$platform]);
header('Content-Type: ' . $kind['mime']);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($path));
header('X-Content-Type-Options: nosniff');
+9
View File
@@ -7,3 +7,12 @@ function toggleTheme() {
document.querySelectorAll('.theme-only').forEach(function (el) { el.style.display = type.value === 'theme' ? '' : 'none'; });
}
if (type) { type.addEventListener('change', toggleTheme); toggleTheme(); }
// Bei ZIP-Paketen (WordPress, Chrome) kann die Versionsnummer leer bleiben: sie wird aus dem Paket gelesen
var platform = document.getElementById('platform');
function toggleVersion() {
var zip = platform.options[platform.selectedIndex].dataset.zip === '1';
document.getElementById('appversion').required = !zip;
document.getElementById('versionhint').hidden = !zip;
}
if (platform) { platform.addEventListener('change', toggleVersion); toggleVersion(); }
+5
View File
@@ -132,3 +132,8 @@ details.card p { overflow-wrap: anywhere; }
.cols.two td.actions { padding-right: 12px; }
.cols.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1100px) { .cols.three { grid-template-columns: 1fr; } }
.previewrow { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 10px 14px; border-bottom: 1px solid var(--line); }
.thumb-sm { width: 96px; height: 60px; object-fit: cover; border-radius: 8px; background: var(--line); }
.thumb-sm.none { display: inline-flex; align-items: center; justify-content: center; font-size: 12px; border: 1px dashed var(--line); opacity: .7; }
.previewrow input[type=file] { max-width: 230px; }
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -2,7 +2,12 @@
declare(strict_types=1);
/** Liefert Vorschaubilder aus dem geschützten Speicher aus. */
require_once __DIR__ . '/inc/lib.php';
site_require_access();
// DeskHub kommt mit dem Zugangsschlüssel; die Verwaltung (angemeldeter Administrator) sieht die Bilder in ihrer Übersicht
if (!site_access_ok()) {
session_name('plugin_site_admin');
session_start(['read_and_close' => true]);
if (empty($_SESSION['admin'])) site_require_access();
}
$file = basename((string) ($_GET['file'] ?? ''));
$path = site_storage() . '/previews/' . $file;