From 7638a7a25c941277f5b5a8e45aea8fa429f5e4a6 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Tue, 5 May 2026 08:19:24 +0200 Subject: [PATCH] =?UTF-8?q?Admin=20versions=20:=20nom=20du=20ZIP=20libre?= =?UTF-8?q?=20+=20renommage=20post-cr=C3=A9ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit À l'ajout d'une version, nouveau champ "Nom du fichier ZIP (optionnel)" : par défaut proserve-{version}.zip mais l'admin peut surcharger pour distinguer des builds homonymes entre channels : builds/asterion-vr/proserve-1.4.6-asterion.zip builds/client-foo/proserve-1.4.6-foo.zip Sur les versions existantes, la dialog "Méta" gagne un champ "Renommer le ZIP" qui : - ne touche que le filename, garde le préfixe builds/{channel}/ - invalide le sha256 (REPLACE_AFTER_BUILD) + sizeBytes pour forcer le recalcul au prochain Sync — le ZIP physique change, le manifest refléterait sinon l'ancien hash et le client ferait fail la vérif. Normalisation côté serveur via ps_normalize_zip_filename() : - basename() pour empêcher tout path traversal (../) - whitelist [a-zA-Z0-9_.-] (laisse les majuscules, points, tirets) - auto-suffix .zip si manquant - fallback sur proserve-{version}.zip si l'input devient vide après nettoyage Co-Authored-By: Claude Opus 4.7 (1M context) --- server/admin/versions.php | 63 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/server/admin/versions.php b/server/admin/versions.php index c42def6..7e1a769 100644 --- a/server/admin/versions.php +++ b/server/admin/versions.php @@ -88,6 +88,27 @@ function saveManifest(string $path, array $manifest): void ); } +/** + * Normalise un nom de fichier ZIP saisi par l'admin : + * - strip tout chemin (path traversal défense, basename) + * - whitelist [a-zA-Z0-9_.-] (laisse les majuscules, points, tirets, underscores) + * - assure le suffixe .zip + * - retombe sur la valeur par défaut si l'input est vide ou inexploitable + */ +function ps_normalize_zip_filename(string $raw, string $version): string +{ + $default = "proserve-{$version}.zip"; + $name = trim($raw); + if ($name === '') return $default; + // Strip tout chemin → garde juste le filename (sécurité) + $name = basename($name); + // Whitelist : on garde un set de chars filename-safe sur Windows + Linux + $name = preg_replace('/[^a-zA-Z0-9_.\-]/', '', $name); + if ($name === '' || $name === '.zip' || $name === '.') return $default; + if (!str_ends_with(strtolower($name), '.zip')) $name .= '.zip'; + return $name; +} + if ($_SERVER['REQUEST_METHOD'] === 'POST') { Auth::checkCsrf(); $action = $_POST['action'] ?? ''; @@ -119,15 +140,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher'; // Pour un channel non-default, le ZIP vit dans builds/{channel}/. - // L'admin peut surcharger l'URL après-coup si la convention diffère. $zipPathPrefix = $channel === '' ? 'builds' : "builds/{$channel}"; + // Le nom du fichier ZIP est libre : par défaut proserve-{version}.zip, + // mais l'admin peut surcharger pour distinguer des builds homonymes + // entre channels (ex. proserve-1.4.6-asterion.zip vs proserve-1.4.6-foo.zip) + // ou pour un naming custom du fournisseur. + $zipFilename = ps_normalize_zip_filename((string)($_POST['zip_filename'] ?? ''), $version); $entry = [ 'version' => $version, 'releasedAt' => $releasedAtIso, 'executable' => 'PROSERVE_UE_5_5.exe', 'installFolderTemplate' => 'PROSERVE v{version}', 'download' => [ - 'url' => "{$base}/{$zipPathPrefix}/proserve-{$version}.zip", + 'url' => "{$base}/{$zipPathPrefix}/{$zipFilename}", 'sizeBytes' => 0, 'sha256' => 'REPLACE_AFTER_BUILD', ], @@ -161,6 +186,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $version = $_POST['version'] ?? ''; $minLicDate = trim($_POST['min_license_date'] ?? ''); $releasedAt = trim($_POST['released_at'] ?? ''); + // Renommer le ZIP : on accepte un nouveau filename qui remplace + // celui en bout d'URL (le préfixe builds/{channel}/ est conservé). + // Vide = pas de changement. + $newZipName = trim((string)($_POST['zip_filename'] ?? '')); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $minLicDate)) { throw new Exception('min_license_date invalide.'); } @@ -170,12 +199,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($releasedAt !== '') { $v['releasedAt'] = (new DateTime($releasedAt, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); } + if ($newZipName !== '') { + $normalized = ps_normalize_zip_filename($newZipName, $version); + // Reconstruit l'URL en remplaçant juste le filename, pas + // le path. Permet de garder un éventuel sous-dossier custom + // tout en renommant le ZIP. Force aussi la re-vérif du sha256 + // au prochain Sync (le ZIP cherché change physiquement). + $oldUrl = $v['download']['url'] ?? ''; + $parsed = parse_url($oldUrl); + if ($parsed !== false && isset($parsed['path'])) { + $newPath = rtrim(dirname($parsed['path']), '/') . '/' . $normalized; + $newUrl = ($parsed['scheme'] ?? 'https') . '://' . ($parsed['host'] ?? 'asterionvr.com') . $newPath; + $v['download']['url'] = $newUrl; + // Invalide l'ancien hash : un nouveau ZIP est attendu + $v['download']['sha256'] = 'REPLACE_AFTER_BUILD'; + $v['download']['sizeBytes'] = 0; + } + } break; } } unset($v); saveManifest($manifestPath, $manifest); - $message = "Méta de v{$version} mises à jour."; + $message = "Méta de v{$version} mises à jour" . ($newZipName !== '' ? ". ⚠️ Le ZIP a été renommé : upload le nouveau fichier puis re-Sync." : '.'); } elseif ($action === 'set_beta') { $version = $_POST['version'] ?? ''; @@ -390,6 +436,10 @@ Layout::header('Versions', 'versions'); +
+ + +
@@ -532,7 +582,7 @@ Layout::header('Versions', 'versions');
Méta -
+ @@ -544,6 +594,11 @@ Layout::header('Versions', 'versions');
+
+ + + ⚠️ Renommer = invalide le sha256, il faudra re-uploader le nouveau ZIP en SFTP puis re-Sync. +