Admin versions : nom du ZIP libre + renommage post-création
À 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
Auth::checkCsrf();
|
Auth::checkCsrf();
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
@@ -119,15 +140,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
|
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
|
||||||
// Pour un channel non-default, le ZIP vit dans builds/{channel}/.
|
// 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}";
|
$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 = [
|
$entry = [
|
||||||
'version' => $version,
|
'version' => $version,
|
||||||
'releasedAt' => $releasedAtIso,
|
'releasedAt' => $releasedAtIso,
|
||||||
'executable' => 'PROSERVE_UE_5_5.exe',
|
'executable' => 'PROSERVE_UE_5_5.exe',
|
||||||
'installFolderTemplate' => 'PROSERVE v{version}',
|
'installFolderTemplate' => 'PROSERVE v{version}',
|
||||||
'download' => [
|
'download' => [
|
||||||
'url' => "{$base}/{$zipPathPrefix}/proserve-{$version}.zip",
|
'url' => "{$base}/{$zipPathPrefix}/{$zipFilename}",
|
||||||
'sizeBytes' => 0,
|
'sizeBytes' => 0,
|
||||||
'sha256' => 'REPLACE_AFTER_BUILD',
|
'sha256' => 'REPLACE_AFTER_BUILD',
|
||||||
],
|
],
|
||||||
@@ -161,6 +186,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$version = $_POST['version'] ?? '';
|
$version = $_POST['version'] ?? '';
|
||||||
$minLicDate = trim($_POST['min_license_date'] ?? '');
|
$minLicDate = trim($_POST['min_license_date'] ?? '');
|
||||||
$releasedAt = trim($_POST['released_at'] ?? '');
|
$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)) {
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $minLicDate)) {
|
||||||
throw new Exception('min_license_date invalide.');
|
throw new Exception('min_license_date invalide.');
|
||||||
}
|
}
|
||||||
@@ -170,12 +199,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($releasedAt !== '') {
|
if ($releasedAt !== '') {
|
||||||
$v['releasedAt'] = (new DateTime($releasedAt, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
|
$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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
saveManifest($manifestPath, $manifest);
|
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') {
|
elseif ($action === 'set_beta') {
|
||||||
$version = $_POST['version'] ?? '';
|
$version = $_POST['version'] ?? '';
|
||||||
@@ -390,6 +436,10 @@ Layout::header('Versions', 'versions');
|
|||||||
<input type="date" name="min_license_date" required>
|
<input type="date" name="min_license_date" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Nom du fichier ZIP <span class="muted" style="font-weight: normal; font-size: 11px;">(optionnel, par défaut <code>proserve-{version}.zip</code> ; utile pour distinguer un build homonyme entre channels)</span></label>
|
||||||
|
<input type="text" name="zip_filename" placeholder="proserve-1.4.8-asterion.zip" maxlength="120">
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Release notes (Markdown)</label>
|
<label>Release notes (Markdown)</label>
|
||||||
<textarea name="notes" rows="8" placeholder="# Proserve v1.4.8 ## Nouveautés - ..." style="font-family: 'Cascadia Code', Consolas, monospace;"></textarea>
|
<textarea name="notes" rows="8" placeholder="# Proserve v1.4.8 ## Nouveautés - ..." style="font-family: 'Cascadia Code', Consolas, monospace;"></textarea>
|
||||||
@@ -532,7 +582,7 @@ Layout::header('Versions', 'versions');
|
|||||||
</details>
|
</details>
|
||||||
<details style="display: inline-block; margin: 0 4px;">
|
<details style="display: inline-block; margin: 0 4px;">
|
||||||
<summary class="btn btn-secondary">Méta</summary>
|
<summary class="btn btn-secondary">Méta</summary>
|
||||||
<form method="post" style="margin-top: 8px;">
|
<form method="post" style="margin-top: 8px; min-width: 360px;">
|
||||||
<?= Layout::csrfField() ?>
|
<?= Layout::csrfField() ?>
|
||||||
<input type="hidden" name="action" value="edit_meta">
|
<input type="hidden" name="action" value="edit_meta">
|
||||||
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
||||||
@@ -544,6 +594,11 @@ Layout::header('Versions', 'versions');
|
|||||||
<label>Date de release (UTC)</label>
|
<label>Date de release (UTC)</label>
|
||||||
<input type="datetime-local" name="released_at" value="<?= htmlspecialchars(substr($v['releasedAt'] ?? '', 0, 16)) ?>">
|
<input type="datetime-local" name="released_at" value="<?= htmlspecialchars(substr($v['releasedAt'] ?? '', 0, 16)) ?>">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Renommer le ZIP <span class="muted" style="font-weight: normal; font-size: 11px;">(actuel : <code><?= htmlspecialchars(basename(parse_url($v['download']['url'] ?? '', PHP_URL_PATH) ?: '')) ?></code>)</span></label>
|
||||||
|
<input type="text" name="zip_filename" placeholder="laisse vide pour ne rien changer">
|
||||||
|
<span class="muted" style="font-size: 11px;">⚠️ Renommer = invalide le sha256, il faudra re-uploader le nouveau ZIP en SFTP puis re-Sync.</span>
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">Enregistrer</button>
|
<button class="btn btn-primary" type="submit">Enregistrer</button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Reference in New Issue
Block a user