diff --git a/server/admin/versions.php b/server/admin/versions.php index 607db18..c42def6 100644 --- a/server/admin/versions.php +++ b/server/admin/versions.php @@ -283,9 +283,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $manifest = loadManifest($manifestPath); -$zips = is_dir($buildsDir) ? array_map('basename', glob("$buildsDir/*.zip") ?: []) : []; +// Liste des ZIPs : on inclut builds/ + builds/{channel}/ (1 niveau de +// sous-dossier) pour lister aussi les ZIPs d'un channel spécifique. Les +// chemins affichés sont relatifs à $buildsDir pour distinguer les ZIPs +// "racine" (default) des ZIPs en sous-dossier (asterion-vr/proserve-X.zip). +$zips = []; $zipSizes = []; -foreach ($zips as $z) $zipSizes[$z] = filesize("$buildsDir/$z"); +if (is_dir($buildsDir)) { + foreach (array_merge( + glob("$buildsDir/*.zip") ?: [], + glob("$buildsDir/*/*.zip") ?: [] + ) as $z) { + $rel = ltrim(str_replace($buildsDir, '', $z), '/\\'); + $rel = str_replace('\\', '/', $rel); + $zips[] = $rel; + $zipSizes[$rel] = filesize($z); + } +} // Pour l'édition : pré-charge les release notes existantes $existingNotes = []; @@ -342,7 +356,15 @@ Layout::header('Versions', 'versions');

Workflow d'une nouvelle release

  1. Ajoute l'entrée du manifest avec le formulaire ci-dessous (version, date min de license, release notes).
  2. -
  3. Upload le ZIP correspondant via SFTP dans www/PS_Launcher/builds/ en respectant le nom proserve-{version}.zip.
  4. +
  5. Upload le ZIP correspondant via SFTP en respectant le nom proserve-{version}.zip, dans le dossier qui correspond au channel actif : + +
  6. Clique 🔁 Sync (sign-manifest) pour calculer le SHA-256, mettre à jour sizeBytes, bumper latest, et signer le manifest avec Ed25519.
  7. Les clients PS_Launcher détecteront la nouvelle version au prochain « Vérifier les MAJ ».
@@ -581,12 +603,18 @@ Layout::header('Versions', 'versions'); FichierTailleRéférencé ? diff --git a/server/tools/SignManifest.php b/server/tools/SignManifest.php index fa6fe62..32f07a9 100644 --- a/server/tools/SignManifest.php +++ b/server/tools/SignManifest.php @@ -41,6 +41,32 @@ final class SignManifest private function out(string $line): void { $this->log[] = $line; } + /** + * Mappe le chemin URL d'un ZIP (ex. /PS_Launcher/builds/asterion-vr/proserve-1.4.6.zip) + * vers son chemin local sur disque (ex. {$buildsDir}/asterion-vr/proserve-1.4.6.zip). + * + * Stratégie : on cherche le segment '/builds/' dans le URL path et on prend + * tout ce qui vient après comme chemin relatif depuis $buildsDir. Sécurité : + * on neutralise '..' pour empêcher path traversal, même si l'URL est déjà + * sous notre contrôle (manifest signé Ed25519). + * + * Si le pattern '/builds/' n'est pas trouvé, fallback sur basename comme avant + * (compat ascendante avec d'éventuelles URLs custom). + */ + private function resolveZipPath(string $urlPath): string + { + $marker = '/builds/'; + $pos = strpos($urlPath, $marker); + if ($pos === false) { + return "{$this->buildsDir}/" . basename($urlPath); + } + $relative = substr($urlPath, $pos + strlen($marker)); + // Sécu : strip '..' segments pour éviter path traversal hors de buildsDir + $relative = str_replace(['..\\', '../', '..'], '', $relative); + $relative = ltrim($relative, '/\\'); + return "{$this->buildsDir}/$relative"; + } + /** * Lit le cache des hashs précalculés. Retourne un dictionnaire * [path => ['size' => int, 'mtime' => int, 'sha256' => string]]. @@ -150,19 +176,29 @@ final class SignManifest continue; } - $filename = basename(parse_url($url, PHP_URL_PATH) ?: ''); - $zip = "{$this->buildsDir}/$filename"; + // Le ZIP peut vivre dans builds/ directement (channel default) OU dans + // builds/{channel}/ pour les autres channels. On extrait donc TOUT le + // chemin après /builds/ dans l'URL plutôt qu'un simple basename(). + $urlPath = parse_url($url, PHP_URL_PATH) ?: ''; + $filename = basename($urlPath); + $zip = $this->resolveZipPath($urlPath); if (!is_file($zip)) { - $candidates = glob("{$this->buildsDir}/*{$version}*.zip", GLOB_NOSORT) ?: []; + // Fallback : recherche fuzzy sur la version dans builds/ + sous-dossiers + // immédiats (1 niveau) au cas où l'admin aurait renommé le ZIP. + $candidates = array_merge( + glob("{$this->buildsDir}/*{$version}*.zip", GLOB_NOSORT) ?: [], + glob("{$this->buildsDir}/*/*{$version}*.zip", GLOB_NOSORT) ?: [] + ); $candidates = array_values(array_filter($candidates, 'is_file')); if (count($candidates) === 1) { $zip = $candidates[0]; - $this->out(" [info] $version : URL pointait vers '{$filename}', utilisé '" . basename($zip) . "' à la place"); + $this->out(" [info] $version : URL pointait vers '{$filename}', utilisé '" . str_replace($this->buildsDir . '/', '', $zip) . "' à la place"); } else { $this->out(" [skip] $version : ZIP introuvable pour $url"); + $this->out(" Cherché : " . str_replace($this->buildsDir . '/', '', $zip)); if (count($candidates) > 1) { - $this->out(" Plusieurs candidats : " . implode(', ', array_map('basename', $candidates))); + $this->out(" Plusieurs candidats : " . implode(', ', array_map(fn($c) => str_replace($this->buildsDir . '/', '', $c), $candidates))); } continue; }