diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 63655ff..0d009a1 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "1.0.7" +#define MyAppVersion "1.0.8" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/server/admin/versions.php b/server/admin/versions.php index 44fef6c..54962d4 100644 --- a/server/admin/versions.php +++ b/server/admin/versions.php @@ -450,6 +450,26 @@ HTML; * - assure le suffixe .zip * - retombe sur la valeur par défaut si l'input est vide ou inexploitable */ +/** + * Valide un installFolderTemplate côté admin. Contraintes : + * - doit contenir {version} (sinon deux entries au même template résolvent + * au même dossier une fois substitué, cassant l'anti-collision). + * - charset filename-safe : [A-Za-z0-9_.-] + espaces. Pas de séparateur de + * chemin (/, \) ni de caractères spéciaux qui casseraient un basename + * côté client (Path.GetFileName). + * - non vide, longueur raisonnable (backoffice UI n'accepte pas les URLs). + */ +function ps_is_valid_install_folder_template(string $tpl): bool +{ + $tpl = trim($tpl); + if ($tpl === '' || strlen($tpl) > 120) return false; + if (!str_contains($tpl, '{version}')) return false; + // On check le "reste" du template (partie non-{version}) — le placeholder + // {version} lui-même contient `{` et `}` qui ne sont pas dans notre whitelist. + $stripped = str_replace('{version}', '', $tpl); + return (bool)preg_match('/^[A-Za-z0-9 _.\-]*$/', $stripped); +} + function ps_normalize_zip_filename(string $raw, string $version): string { $default = "proserve-{$version}.zip"; @@ -521,12 +541,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!preg_match('/^[A-Za-z0-9_.\-]+\.exe$/', $exeName)) { $exeName = 'PROSERVE_UE_5_7.exe'; } + // installFolderTemplate — nom du sous-dossier créé côté client par le + // launcher. Doit contenir {version} et être unique par (version, channel) + // pour éviter l'écrasement silencieux à l'install quand deux entries + // partagent un numéro. Default intelligent : si un channel non-default + // est sélectionné, on préfixe avec "PROSERVE-". Sinon on garde + // le legacy "PROSERVE v{version}". + $installFolderTemplateInput = trim((string)($_POST['install_folder_template'] ?? '')); + if ($installFolderTemplateInput === '') { + $nonDefaultChannels = array_values(array_filter($channels, fn($c) => $c !== 'default')); + $installFolderTemplateInput = count($nonDefaultChannels) === 1 + ? "PROSERVE-{$nonDefaultChannels[0]} v{version}" + : "PROSERVE v{version}"; + } + if (!ps_is_valid_install_folder_template($installFolderTemplateInput)) { + throw new Exception( + "InstallFolderTemplate invalide « {$installFolderTemplateInput} » : doit contenir {version} et n'utiliser que [A-Za-z0-9_.-] et espaces." + ); + } + // Validation anti-collision cross-entries : résous le template avec la + // version courante, refuse si une autre entrée résoud au même dossier. + $installFolderResolved = str_replace('{version}', $version, $installFolderTemplateInput); + foreach ($manifest['versions'] as $vExisting) { + $existingTpl = (string)($vExisting['installFolderTemplate'] ?? 'PROSERVE v{version}'); + $existingResolved = str_replace('{version}', $vExisting['version'] ?? '', $existingTpl); + if ($existingResolved === $installFolderResolved) { + throw new Exception( + "Collision de dossier d'install : v" . htmlspecialchars($vExisting['version'] ?? '?') + . " utilise déjà le dossier « {$installFolderResolved} ». Choisis un installFolderTemplate distinct (ex : « PROSERVE-{$version}- v{version} » ou en dur « PROSERVE- v{version} »)." + ); + } + } $entry = [ 'id' => $entryId, 'version' => $version, 'releasedAt' => $releasedAtIso, 'executable' => $exeName, - 'installFolderTemplate' => 'PROSERVE v{version}', + 'installFolderTemplate' => $installFolderTemplateInput, 'channels' => $channels, 'download' => [ 'url' => "{$base}/builds/{$zipFilename}", @@ -594,6 +645,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $manifest['versions'][$idx]['download']['sha256'] = 'REPLACE_AFTER_BUILD'; $manifest['versions'][$idx]['download']['sizeBytes'] = 0; } + // installFolderTemplate — édition optionnelle. Laisser vide = ne pas + // changer. Valide + anti-collision cross-entries obligatoire. + $newFolderTpl = trim((string)($_POST['install_folder_template'] ?? '')); + if ($newFolderTpl !== '') { + if (!ps_is_valid_install_folder_template($newFolderTpl)) { + throw new Exception( + "InstallFolderTemplate invalide « {$newFolderTpl} » : doit contenir {version} et n'utiliser que [A-Za-z0-9_.-] et espaces." + ); + } + $newFolderResolved = str_replace('{version}', $version, $newFolderTpl); + foreach ($manifest['versions'] as $other) { + if (($other['id'] ?? '') === $entryId) continue; + $otherTpl = (string)($other['installFolderTemplate'] ?? 'PROSERVE v{version}'); + $otherResolved = str_replace('{version}', $other['version'] ?? '', $otherTpl); + if ($otherResolved === $newFolderResolved) { + throw new Exception( + "Collision de dossier d'install : v" . htmlspecialchars($other['version'] ?? '?') + . " utilise déjà le dossier « {$newFolderResolved} ». Choisis un installFolderTemplate distinct." + ); + } + } + $manifest['versions'][$idx]['installFolderTemplate'] = $newFolderTpl; + } saveManifest($manifestPath, $manifest); $message = "Méta de v{$version} mises à jour" . ($newZipName !== '' ? ". ⚠️ Le ZIP a été renommé : upload le nouveau fichier puis re-Sync." : '.'); } @@ -931,6 +1005,11 @@ Layout::header('Versions', 'versions'); +
+ + +
@@ -1161,6 +1240,15 @@ Layout::header('Versions', 'versions');

⚠ Renommer invalide le sha256 — re-upload le nouveau ZIP en SFTP puis « 🔁 Sync ».

+
+ + +

DOIT contenir {version}. Change-le si deux entries au même numéro se retrouvent à cibler le même dossier (ex : PROSERVE-firefighter v{version}).

+