diff --git a/server/admin/versions.php b/server/admin/versions.php index e783521..607db18 100644 --- a/server/admin/versions.php +++ b/server/admin/versions.php @@ -16,15 +16,42 @@ $notesDir = "$root/releasenotes"; // Channel actif pour cette session d'édition. ?channel=X bascule sur // versions-X.json (créé à la volée si nécessaire), vide = manifest default. -// Whitelist regex anti-injection. -$channel = trim((string)($_GET['channel'] ?? $_POST['__channel'] ?? '')); -if ($channel !== '' && !preg_match('/^[a-z0-9_-]{1,64}$/', $channel)) { - $channel = ''; +// +// Normalisation tolérante : on lowercase, on remplace espaces et tirets-cadratins +// par des '-', on retire tout le reste. Comme ça l'admin peut taper "ASTERION VR" +// dans le champ et ça devient "asterion-vr" — pas de submit silencieusement +// bloqué par une regex stricte côté HTML (ce qui faisait rien quand on cliquait +// "Aller" en v0.26.0). +function ps_normalize_channel(string $raw): string +{ + $s = strtolower(trim($raw)); + // Espaces, tirets typo, tabs, etc. → tiret simple + $s = preg_replace('/[\s\x{2010}-\x{2015}]+/u', '-', $s); + // Retire tout caractère non whitelisté + $s = preg_replace('/[^a-z0-9_-]/', '', $s); + // Compresse les tirets multiples + trim de tirets en début/fin + $s = preg_replace('/-+/', '-', $s); + $s = trim($s, '-_'); + return substr($s, 0, 64); } + +$channelRaw = trim((string)($_GET['channel'] ?? $_POST['__channel'] ?? '')); +$channelNormalized = ps_normalize_channel($channelRaw); +$channelWasNormalized = $channelRaw !== '' && $channelNormalized !== $channelRaw; +$channelEmptyAfterNorm = $channelRaw !== '' && $channelNormalized === ''; + +$channel = $channelNormalized; $manifestFileName = $channel === '' ? 'versions.json' : "versions-{$channel}.json"; $manifestPath = "$manifestDir/$manifestFileName"; $message = null; $messageType = 'success'; +if ($channelEmptyAfterNorm) { + $message = "Nom de channel inexploitable « {$channelRaw} » (que des caractères non autorisés). Tape un nom avec des lettres / chiffres, ex. asterion-vr."; + $messageType = 'error'; +} elseif ($channelWasNormalized) { + $message = "Nom de channel normalisé : « {$channelRaw} » → « {$channelNormalized} »."; + $messageType = 'success'; +} /** * Liste les channels existants (= versions-*.json présents) + ajoute toujours @@ -268,6 +295,13 @@ foreach ($manifest['versions'] ?? [] as $v) { } $existingChannels = listExistingChannels($manifestDir); +// On force la présence du channel actif dans la liste, même si versions-X.json +// n'existe pas encore (cas où l'utilisateur vient de "créer" un channel et n'y +// a pas encore ajouté de version). Sinon le dropdown afficherait "(default)" +// alors que la session est sur le nouveau channel — confusion classique. +if ($channel !== '' && !isset($existingChannels[$channel])) { + $existingChannels[$channel] = $channel . ' (vide — pas encore de versions)'; +} $channelLabel = $channel === '' ? 'default' : $channel; Layout::header('Versions', 'versions'); @@ -298,8 +332,8 @@ Layout::header('Versions', 'versions');