Upload via GUI (32 Dateien)
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+113
-16
@@ -107,10 +107,55 @@ function site_db(): PDO
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/** Plattformen der Apps-Seite: Dateiendung, Dateikopf, Name beim Herunterladen. "zip" = Paket mit Angaben zur Version (wird automatisch gelesen). */
|
||||
function site_app_platforms(): array
|
||||
{
|
||||
return [
|
||||
'windows' => ['label' => 'Windows', 'what' => 'Setup (EXE)', 'ext' => 'exe', 'magic' => 'MZ', 'file' => 'DeskHub-Setup.exe', 'mime' => 'application/vnd.microsoft.portable-executable'],
|
||||
'android' => ['label' => 'Android', 'what' => 'Installationsdatei (APK)', 'ext' => 'apk', 'magic' => 'PK', 'file' => 'DeskHub.apk', 'mime' => 'application/vnd.android.package-archive'],
|
||||
'minecraft' => ['label' => 'Minecraft', 'what' => 'Plugin (JAR)', 'ext' => 'jar', 'magic' => 'PK', 'file' => 'DeskHub-Minecraft.jar', 'mime' => 'application/java-archive'],
|
||||
'wordpress' => ['label' => 'WordPress', 'what' => 'Plugin (ZIP)', 'ext' => 'zip', 'magic' => 'PK', 'file' => 'deskhub-tickets-wordpress.zip', 'mime' => 'application/zip'],
|
||||
'chrome' => ['label' => 'Chrome', 'what' => 'Erweiterung (ZIP)', 'ext' => 'zip', 'magic' => 'PK', 'file' => 'deskhub-chrome-erweiterung.zip', 'mime' => 'application/zip'],
|
||||
];
|
||||
}
|
||||
|
||||
/** Hochgeladene Datei (nur in den Tests mit PLUGIN_SITE_TESTING=1 auch eine lokale Datei). */
|
||||
function site_is_upload(string $tmp): bool
|
||||
{
|
||||
return is_uploaded_file($tmp) || (getenv('PLUGIN_SITE_TESTING') === '1' && is_file($tmp));
|
||||
}
|
||||
|
||||
function site_move_upload(string $tmp, string $dest): bool
|
||||
{
|
||||
return is_uploaded_file($tmp) ? move_uploaded_file($tmp, $dest) : (getenv('PLUGIN_SITE_TESTING') === '1' && copy($tmp, $dest));
|
||||
}
|
||||
|
||||
/** Liest die Versionsnummer aus einem WordPress-Plugin-ZIP (Kopf der Hauptdatei) oder einer Chrome-Erweiterung (manifest.json); null, wenn es keines von beiden ist. */
|
||||
function site_zip_app_version(string $platform, string $zipPath): ?string
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath) !== true || $zip->numFiles > 2000) return null;
|
||||
$found = null;
|
||||
for ($i = 0; $i < $zip->numFiles && $found === null; $i++) {
|
||||
$name = str_replace(chr(92), '/', (string) $zip->getNameIndex($i));
|
||||
if (substr_count($name, '/') > 1) continue;
|
||||
if ($platform === 'chrome' && preg_match('#^(?:[^/]+/)?manifest\.json$#', $name)) {
|
||||
$data = json_decode((string) $zip->getFromIndex($i), true);
|
||||
if (is_array($data) && isset($data['manifest_version'], $data['version'])) $found = (string) $data['version'];
|
||||
} elseif ($platform === 'wordpress' && preg_match('#\.php$#i', $name)) {
|
||||
$head = (string) $zip->getFromIndex($i, 8192);
|
||||
if (preg_match('/^[ \/*#@]*Plugin Name:/mi', $head) && preg_match('/^[ \/*#@]*Version:\s*([0-9][0-9A-Za-z.+-]*)/mi', $head, $m)) $found = $m[1];
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
if ($found !== null && preg_match('/^\d+\.\d+$/', $found)) $found .= '.0';
|
||||
return $found;
|
||||
}
|
||||
|
||||
/** Latest published app release per platform: the HIGHEST version, not the one uploaded last. */
|
||||
function site_app_release(string $platform): ?array
|
||||
{
|
||||
if (!in_array($platform, ['android', 'windows', 'minecraft'], true)) return null;
|
||||
if (!isset(site_app_platforms()[$platform])) return null;
|
||||
$stmt = site_db()->prepare('SELECT * FROM app_releases WHERE platform = ?');
|
||||
$stmt->execute([$platform]);
|
||||
$rows = $stmt->fetchAll();
|
||||
@@ -128,18 +173,25 @@ function site_app_releases(): array
|
||||
|
||||
function site_publish_app(string $platform, string $tmp, string $version, string $notes): array
|
||||
{
|
||||
$types = ['android' => ['apk', "PK"], 'windows' => ['exe', "MZ"], 'minecraft' => ['jar', "PK"]];
|
||||
$types = site_app_platforms();
|
||||
if (!isset($types[$platform])) return ['ok' => false, 'error' => 'Unbekannte Plattform.'];
|
||||
if (!preg_match('/^\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?$/', $version)) return ['ok' => false, 'error' => 'Bitte eine Versionsnummer wie 3.4.0 eingeben.'];
|
||||
if (!is_uploaded_file($tmp)) return ['ok' => false, 'error' => 'Die hochgeladene Datei wurde nicht gefunden.'];
|
||||
$kind = $types[$platform];
|
||||
if (!site_is_upload($tmp)) return ['ok' => false, 'error' => 'Die hochgeladene Datei wurde nicht gefunden.'];
|
||||
$size = (int) filesize($tmp);
|
||||
if ($size < 1024 || $size > SITE_MAX_APP_BYTES) return ['ok' => false, 'error' => 'Die Datei muss zwischen 1 KB und 300 MB groß sein.'];
|
||||
$head = (string) file_get_contents($tmp, false, null, 0, 2);
|
||||
if ($head !== $types[$platform][1]) return ['ok' => false, 'error' => 'Dateityp passt nicht zur gewählten Plattform.'];
|
||||
if ($head !== $kind['magic']) return ['ok' => false, 'error' => 'Dateityp passt nicht zur gewählten Plattform.'];
|
||||
if (in_array($platform, ['wordpress', 'chrome'], true)) {
|
||||
// Version steht im Paket (Plugin-Kopf bzw. manifest.json); eine eingetippte Nummer hat Vorrang
|
||||
$inZip = site_zip_app_version($platform, $tmp);
|
||||
if ($inZip === null) return ['ok' => false, 'error' => $platform === 'wordpress' ? 'Das ist kein WordPress-Plugin: In einer PHP-Datei fehlt der Kopf mit "Plugin Name:" und "Version:".' : 'Das ist keine Chrome-Erweiterung: manifest.json mit "version" fehlt.'];
|
||||
if ($version === '') $version = $inZip;
|
||||
}
|
||||
if (!preg_match('/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/', $version)) return ['ok' => false, 'error' => 'Bitte eine Versionsnummer wie 3.4.0 eingeben.'];
|
||||
$dir = site_storage() . '/apps/' . $platform;
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) return ['ok' => false, 'error' => 'App-Speicher konnte nicht angelegt werden.'];
|
||||
$file = bin2hex(random_bytes(24)) . '.' . $types[$platform][0];
|
||||
if (!move_uploaded_file($tmp, $dir . '/' . $file)) return ['ok' => false, 'error' => 'App-Datei konnte nicht gespeichert werden.'];
|
||||
$file = bin2hex(random_bytes(24)) . '.' . $kind['ext'];
|
||||
if (!site_move_upload($tmp, $dir . '/' . $file)) return ['ok' => false, 'error' => 'App-Datei konnte nicht gespeichert werden.'];
|
||||
$sha = hash_file('sha256', $dir . '/' . $file);
|
||||
try {
|
||||
$stmt = site_db()->prepare('INSERT INTO app_releases(platform,version,file,sha256,size,notes,created_at) VALUES(?,?,?,?,?,?,?)');
|
||||
@@ -148,7 +200,7 @@ function site_publish_app(string $platform, string $tmp, string $version, string
|
||||
@unlink($dir . '/' . $file);
|
||||
return ['ok' => false, 'error' => str_contains(strtolower($e->getMessage()), 'unique') ? 'Diese Version ist für diese Plattform bereits vorhanden.' : 'Release konnte nicht gespeichert werden.'];
|
||||
}
|
||||
return ['ok' => true];
|
||||
return ['ok' => true, 'version' => $version];
|
||||
}
|
||||
|
||||
/** Plugin bzw. Theme als „Empfohlen“ markieren oder die Markierung entfernen. */
|
||||
@@ -328,6 +380,12 @@ function site_publish(string $type, string $zipPath, array $form = [], ?string $
|
||||
if ((int) $dup->fetchColumn() > 0) return ['ok' => false, 'error' => "Version {$meta['version']} existiert bereits. Bitte eine neue Versionsnummer verwenden, veröffentlichte Versionen bleiben unverändert."];
|
||||
}
|
||||
|
||||
// Kein Bild hochgeladen und noch keines vorhanden: ein Bild im Paket (vorschau.png / preview.png) verwenden
|
||||
$fromZip = null;
|
||||
if (($previewPath === null || !is_file($previewPath)) && !($item && !empty($item['preview']))) {
|
||||
$fromZip = site_zip_preview($zipPath);
|
||||
if ($fromZip !== null) { $previewPath = $fromZip['path']; $previewName = $fromZip['name']; }
|
||||
}
|
||||
$preview = null;
|
||||
if ($previewPath !== null && is_file($previewPath)) {
|
||||
$ext = strtolower(pathinfo((string) $previewName, PATHINFO_EXTENSION));
|
||||
@@ -343,6 +401,7 @@ function site_publish(string $type, string $zipPath, array $form = [], ?string $
|
||||
if (!is_dir(dirname($dest))) mkdir(dirname($dest), 0755, true);
|
||||
if (!(is_uploaded_file($zipPath) ? move_uploaded_file($zipPath, $dest) : copy($zipPath, $dest))) return ['ok' => false, 'error' => 'Das Paket konnte nicht gespeichert werden.'];
|
||||
if ($preview !== null) (is_uploaded_file($previewPath) ? move_uploaded_file($previewPath, site_storage() . '/previews/' . $preview) : copy($previewPath, site_storage() . '/previews/' . $preview));
|
||||
if ($fromZip !== null) @unlink($fromZip['path']);
|
||||
|
||||
$signature = site_sign($type, $meta['slug'], $meta['version'], $sha);
|
||||
$pdo->beginTransaction();
|
||||
@@ -367,6 +426,51 @@ function site_publish(string $type, string $zipPath, array $form = [], ?string $
|
||||
return ['ok' => true, 'slug' => $meta['slug'], 'version' => $meta['version'], 'signed' => $signature !== '', 'sha256' => $sha];
|
||||
}
|
||||
|
||||
/** Sucht im Paket ein Bild namens vorschau.* oder preview.* (im Hauptordner oder im einzigen Unterordner) und legt es als Temp-Datei ab. */
|
||||
function site_zip_preview(string $zipPath): ?array
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipPath) !== true) return null;
|
||||
$found = null;
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$name = str_replace(chr(92), '/', (string) $zip->getNameIndex($i));
|
||||
if (preg_match('#^(?:[^/]+/)?(?:vorschau|preview)\.(png|jpe?g|webp)$#i', $name, $m)) {
|
||||
$stat = $zip->statIndex($i);
|
||||
if ($stat && $stat['size'] > 0 && $stat['size'] <= SITE_MAX_PREVIEW_BYTES) { $found = [$i, strtolower($m[1])]; break; }
|
||||
}
|
||||
}
|
||||
if ($found === null) { $zip->close(); return null; }
|
||||
$data = $zip->getFromIndex($found[0]);
|
||||
$zip->close();
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'dhprev');
|
||||
if ($data === false || $tmp === false || file_put_contents($tmp, $data) === false) return null;
|
||||
if (!@getimagesize($tmp)) { @unlink($tmp); return null; }
|
||||
return ['path' => $tmp, 'name' => 'vorschau.' . $found[1]];
|
||||
}
|
||||
|
||||
/** Ändert oder entfernt das Vorschaubild eines Plugins/Themes, ohne eine neue Version zu veröffentlichen. */
|
||||
function site_set_preview(int $itemId, ?string $tmp, ?string $name): array
|
||||
{
|
||||
$pdo = site_db();
|
||||
$row = $pdo->prepare('SELECT type, slug, preview FROM items WHERE id = ?');
|
||||
$row->execute([$itemId]);
|
||||
$row = $row->fetch();
|
||||
if (!$row) return ['ok' => false, 'error' => 'Nicht gefunden.'];
|
||||
$new = null;
|
||||
if ($tmp !== null) {
|
||||
$ext = strtolower(pathinfo((string) $name, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['png', 'jpg', 'jpeg', 'webp'], true) || !is_file($tmp) || !@getimagesize($tmp) || filesize($tmp) > SITE_MAX_PREVIEW_BYTES) {
|
||||
return ['ok' => false, 'error' => 'Das Vorschaubild muss PNG, JPG oder WebP sein und darf höchstens 1 MB groß sein.'];
|
||||
}
|
||||
$new = $row['type'] . '-' . $row['slug'] . '-' . substr(hash_file('sha256', $tmp), 0, 8) . '.' . ($ext === 'jpeg' ? 'jpg' : $ext);
|
||||
$dest = site_storage() . '/previews/' . $new;
|
||||
if (!(is_uploaded_file($tmp) ? move_uploaded_file($tmp, $dest) : copy($tmp, $dest))) return ['ok' => false, 'error' => 'Das Bild konnte nicht gespeichert werden.'];
|
||||
}
|
||||
$pdo->prepare('UPDATE items SET preview = ?, updated_at = ? WHERE id = ?')->execute([$new, date('Y-m-d H:i:s'), $itemId]);
|
||||
if (!empty($row['preview']) && $row['preview'] !== $new) site_remove_file(site_storage() . '/previews/' . basename((string) $row['preview']));
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
/** Löscht eine Datei und versucht es kurz erneut, falls sie noch von einem Virenscanner oder einem anderen Prozess gehalten wird. */
|
||||
function site_remove_file(string $path): bool
|
||||
{
|
||||
@@ -387,14 +491,7 @@ function site_delete_version(int $versionId): bool
|
||||
if (!$v) return false;
|
||||
site_remove_file(site_storage() . '/packages/' . $v['file']);
|
||||
$pdo->prepare('DELETE FROM versions WHERE id = ?')->execute([$versionId]);
|
||||
$left = $pdo->prepare('SELECT COUNT(*) FROM versions WHERE item_id = ?');
|
||||
$left->execute([$v['item_id']]);
|
||||
if ((int) $left->fetchColumn() === 0) {
|
||||
$item = $pdo->prepare('SELECT preview FROM items WHERE id = ?');
|
||||
$item->execute([$v['item_id']]);
|
||||
if ($p = $item->fetchColumn()) site_remove_file(site_storage() . '/previews/' . $p);
|
||||
$pdo->prepare('DELETE FROM items WHERE id = ?')->execute([$v['item_id']]);
|
||||
}
|
||||
// Der Eintrag samt Vorschaubild bleibt auch ohne Version erhalten (er ist dann unsichtbar): Wer die alte Version entfernt und eine neue hochlädt, muss das Bild nicht noch einmal hochladen.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user