|
|
|
@@ -41,22 +41,62 @@ function sanitizeChannels(array $posted, array $allowedNames): array
|
|
|
|
return $out;
|
|
|
|
return $out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* ID stable interne pour identifier une entrée du manifest. Permet d'avoir
|
|
|
|
|
|
|
|
* plusieurs entrées avec le même numéro de version (ex. v1.5.3 pour police +
|
|
|
|
|
|
|
|
* v1.5.3 pour pompier) — la "version" devient un libellé human-readable, pas
|
|
|
|
|
|
|
|
* un identifiant unique. Le tag de channels distingue qui voit quoi.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* Format : 'v' + 8 hex chars random. Immutable une fois généré.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
function generate_entry_id(): string
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
try { return 'v' . bin2hex(random_bytes(4)); }
|
|
|
|
|
|
|
|
catch (\Throwable) { return 'v' . dechex(mt_rand(0, 0xffffffff)); }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadManifest(string $path): array
|
|
|
|
function loadManifest(string $path): array
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (!is_file($path)) return ['schemaVersion' => 1, 'product' => 'PROSERVE_UE', 'latest' => null, 'versions' => []];
|
|
|
|
if (!is_file($path)) return ['schemaVersion' => 1, 'product' => 'PROSERVE_UE', 'latest' => null, 'versions' => []];
|
|
|
|
return json_decode(file_get_contents($path), true) ?? [];
|
|
|
|
$manifest = json_decode(file_get_contents($path), true) ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Migration douce : on ajoute un `id` aux entrées qui n'en ont pas (legacy
|
|
|
|
|
|
|
|
// pre-v0.27.1). Auto-save à la fin pour que l'id soit persisté et ne change
|
|
|
|
|
|
|
|
// plus aux reloads suivants. Sans ça, les forms pointeraient sur des ids
|
|
|
|
|
|
|
|
// jetables et les actions tomberaient à plat au refresh.
|
|
|
|
|
|
|
|
$dirty = false;
|
|
|
|
|
|
|
|
foreach ($manifest['versions'] ?? [] as &$v) {
|
|
|
|
|
|
|
|
if (empty($v['id'])) { $v['id'] = generate_entry_id(); $dirty = true; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($v);
|
|
|
|
|
|
|
|
if ($dirty) saveManifest($path, $manifest);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return $manifest;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveManifest(string $path, array $manifest): void
|
|
|
|
function saveManifest(string $path, array $manifest): void
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// Re-tri par SemVer descendant
|
|
|
|
// Re-tri par SemVer descendant. Pour des entrées de même version,
|
|
|
|
usort($manifest['versions'], fn($a, $b) => version_compare($b['version'], $a['version']));
|
|
|
|
// tiebreak sur l'id (stable mais arbitraire) — l'admin peut compter
|
|
|
|
|
|
|
|
// sur un ordre déterministe.
|
|
|
|
|
|
|
|
usort($manifest['versions'], function($a, $b) {
|
|
|
|
|
|
|
|
$cmp = version_compare($b['version'] ?? '0.0.0', $a['version'] ?? '0.0.0');
|
|
|
|
|
|
|
|
return $cmp !== 0 ? $cmp : strcmp($a['id'] ?? '', $b['id'] ?? '');
|
|
|
|
|
|
|
|
});
|
|
|
|
file_put_contents(
|
|
|
|
file_put_contents(
|
|
|
|
$path,
|
|
|
|
$path,
|
|
|
|
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
|
|
|
|
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function find_entry_index_by_id(array $manifest, string $id): ?int
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
foreach ($manifest['versions'] ?? [] as $i => $v) {
|
|
|
|
|
|
|
|
if (($v['id'] ?? '') === $id) return $i;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Normalise un nom de fichier ZIP saisi par l'admin :
|
|
|
|
* Normalise un nom de fichier ZIP saisi par l'admin :
|
|
|
|
* - strip tout chemin (path traversal défense, basename)
|
|
|
|
* - strip tout chemin (path traversal défense, basename)
|
|
|
|
@@ -99,8 +139,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $minLicDate)) {
|
|
|
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $minLicDate)) {
|
|
|
|
throw new Exception('Date min_license_date invalide.');
|
|
|
|
throw new Exception('Date min_license_date invalide.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Le numéro de version peut être DUPLIQUÉ entre channels — c'est
|
|
|
|
|
|
|
|
// exactement le cas d'usage (v1.5.3 pour police + v1.5.3 pour
|
|
|
|
|
|
|
|
// pompier coexistent). On rejette uniquement la collision sur le
|
|
|
|
|
|
|
|
// nom de ZIP, parce que là on aurait une vraie ambiguïté physique
|
|
|
|
|
|
|
|
// : deux entries pointant sur le même fichier sur disque.
|
|
|
|
|
|
|
|
$zipFilenameProposed = ps_normalize_zip_filename((string)($_POST['zip_filename'] ?? ''), $version);
|
|
|
|
foreach ($manifest['versions'] as $v) {
|
|
|
|
foreach ($manifest['versions'] as $v) {
|
|
|
|
if ($v['version'] === $version) throw new Exception("v{$version} existe déjà dans le manifest.");
|
|
|
|
$existingZip = basename(parse_url($v['download']['url'] ?? '', PHP_URL_PATH) ?: '');
|
|
|
|
|
|
|
|
if ($existingZip === $zipFilenameProposed) {
|
|
|
|
|
|
|
|
throw new Exception(
|
|
|
|
|
|
|
|
"Le ZIP « {$zipFilenameProposed} » est déjà utilisé par v"
|
|
|
|
|
|
|
|
. htmlspecialchars($v['version'] ?? '?')
|
|
|
|
|
|
|
|
. ". Choisis un autre nom de ZIP (ex. proserve-{$version}-police.zip)."
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$releasedAtIso = $releasedAt !== ''
|
|
|
|
$releasedAtIso = $releasedAt !== ''
|
|
|
|
@@ -108,14 +162,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
|
: (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
|
|
|
|
: (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
|
|
|
|
|
|
|
|
|
|
|
|
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
|
|
|
|
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
|
|
|
|
// Tous les ZIPs vivent dans builds/ flat. Le nom du fichier est libre :
|
|
|
|
// Le ZIP a déjà été normalisé pour le check anti-collision plus haut.
|
|
|
|
// par défaut proserve-{version}.zip, l'admin peut surcharger pour des
|
|
|
|
$zipFilename = $zipFilenameProposed;
|
|
|
|
// builds homonymes (proserve-1.4.6-police.zip, proserve-1.4.6-asterion.zip…).
|
|
|
|
|
|
|
|
$zipFilename = ps_normalize_zip_filename((string)($_POST['zip_filename'] ?? ''), $version);
|
|
|
|
|
|
|
|
// Tags : liste des channels qui voient cette version. Sanitized contre
|
|
|
|
|
|
|
|
// le registre — un channel inconnu est silencieusement filtré. Vide → ['default'].
|
|
|
|
|
|
|
|
$channels = sanitizeChannels((array)($_POST['channels'] ?? []), $channelNames);
|
|
|
|
$channels = sanitizeChannels((array)($_POST['channels'] ?? []), $channelNames);
|
|
|
|
|
|
|
|
$entryId = generate_entry_id();
|
|
|
|
$entry = [
|
|
|
|
$entry = [
|
|
|
|
|
|
|
|
'id' => $entryId,
|
|
|
|
'version' => $version,
|
|
|
|
'version' => $version,
|
|
|
|
'releasedAt' => $releasedAtIso,
|
|
|
|
'releasedAt' => $releasedAtIso,
|
|
|
|
'executable' => 'PROSERVE_UE_5_5.exe',
|
|
|
|
'executable' => 'PROSERVE_UE_5_5.exe',
|
|
|
|
@@ -126,7 +178,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
|
'sizeBytes' => 0,
|
|
|
|
'sizeBytes' => 0,
|
|
|
|
'sha256' => 'REPLACE_AFTER_BUILD',
|
|
|
|
'sha256' => 'REPLACE_AFTER_BUILD',
|
|
|
|
],
|
|
|
|
],
|
|
|
|
'releaseNotesUrl' => "{$base}/api/releasenotes/{$version}",
|
|
|
|
// Release notes adressables par id stable — supporte plusieurs entries
|
|
|
|
|
|
|
|
// de même version sans collision (chaque release a son propre .md).
|
|
|
|
|
|
|
|
'releaseNotesUrl' => "{$base}/api/releasenotes/{$entryId}",
|
|
|
|
'minLicenseDate' => $minLicDate,
|
|
|
|
'minLicenseDate' => $minLicDate,
|
|
|
|
'availableForDownload' => $available,
|
|
|
|
'availableForDownload' => $available,
|
|
|
|
];
|
|
|
|
];
|
|
|
|
@@ -140,106 +194,105 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
|
|
|
|
|
|
|
|
|
if ($notes !== '') {
|
|
|
|
if ($notes !== '') {
|
|
|
|
if (!is_dir($notesDir)) mkdir($notesDir, 0755, true);
|
|
|
|
if (!is_dir($notesDir)) mkdir($notesDir, 0755, true);
|
|
|
|
file_put_contents("$notesDir/{$version}.md", $notes);
|
|
|
|
file_put_contents("$notesDir/{$entryId}.md", $notes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$message = "v{$version} ajoutée. Upload le ZIP <code>{$zipFilename}</code> en SFTP dans <code>builds/</code>, puis clique « Sync (sign-manifest) ».";
|
|
|
|
$message = "v{$version} ajoutée (id <code>{$entryId}</code>). Upload le ZIP <code>{$zipFilename}</code> en SFTP dans <code>builds/</code>, puis clique « Sync (sign-manifest) ».";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'edit_notes') {
|
|
|
|
elseif ($action === 'edit_notes') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
$notes = $_POST['notes'] ?? '';
|
|
|
|
$notes = $_POST['notes'] ?? '';
|
|
|
|
if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) throw new Exception('Version invalide.');
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
if (!is_dir($notesDir)) mkdir($notesDir, 0755, true);
|
|
|
|
if (!is_dir($notesDir)) mkdir($notesDir, 0755, true);
|
|
|
|
file_put_contents("$notesDir/{$version}.md", $notes);
|
|
|
|
file_put_contents("$notesDir/{$entryId}.md", $notes);
|
|
|
|
$message = "Release notes de v{$version} mises à jour.";
|
|
|
|
$message = "Release notes de v" . ($manifest['versions'][$idx]['version'] ?? '?') . " mises à jour.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'edit_meta') {
|
|
|
|
elseif ($action === 'edit_meta') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
$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'] ?? ''));
|
|
|
|
$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.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
foreach ($manifest['versions'] as &$v) {
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
if ($v['version'] === $version) {
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
$v['minLicenseDate'] = $minLicDate;
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$manifest['versions'][$idx]['minLicenseDate'] = $minLicDate;
|
|
|
|
if ($releasedAt !== '') {
|
|
|
|
if ($releasedAt !== '') {
|
|
|
|
$v['releasedAt'] = (new DateTime($releasedAt, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
|
|
|
|
$manifest['versions'][$idx]['releasedAt'] =
|
|
|
|
|
|
|
|
(new DateTime($releasedAt, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($newZipName !== '') {
|
|
|
|
if ($newZipName !== '') {
|
|
|
|
$normalized = ps_normalize_zip_filename($newZipName, $version);
|
|
|
|
$normalized = ps_normalize_zip_filename($newZipName, $version);
|
|
|
|
// Reconstruit l'URL : tous les ZIPs sont à la racine de builds/
|
|
|
|
// Anti-collision : interdit de renommer vers un ZIP déjà utilisé par une autre entry
|
|
|
|
// donc on remplace juste le filename, sans sous-dossier.
|
|
|
|
foreach ($manifest['versions'] as $other) {
|
|
|
|
|
|
|
|
if (($other['id'] ?? '') === $entryId) continue;
|
|
|
|
|
|
|
|
$otherZip = basename(parse_url($other['download']['url'] ?? '', PHP_URL_PATH) ?: '');
|
|
|
|
|
|
|
|
if ($otherZip === $normalized) {
|
|
|
|
|
|
|
|
throw new Exception("Le ZIP « {$normalized} » est déjà utilisé par une autre entry. Choisis un autre nom.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
|
|
|
|
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
|
|
|
|
$v['download']['url'] = "{$base}/builds/{$normalized}";
|
|
|
|
$manifest['versions'][$idx]['download']['url'] = "{$base}/builds/{$normalized}";
|
|
|
|
// Invalide l'ancien hash : un nouveau ZIP est attendu
|
|
|
|
$manifest['versions'][$idx]['download']['sha256'] = 'REPLACE_AFTER_BUILD';
|
|
|
|
$v['download']['sha256'] = 'REPLACE_AFTER_BUILD';
|
|
|
|
$manifest['versions'][$idx]['download']['sizeBytes'] = 0;
|
|
|
|
$v['download']['sizeBytes'] = 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($v);
|
|
|
|
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
$message = "Méta de v{$version} mises à jour" . ($newZipName !== '' ? ". ⚠️ Le ZIP a été renommé : upload le nouveau fichier puis re-Sync." : '.');
|
|
|
|
$message = "Méta de v{$version} mises à jour" . ($newZipName !== '' ? ". ⚠️ Le ZIP a été renommé : upload le nouveau fichier puis re-Sync." : '.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'set_channels') {
|
|
|
|
elseif ($action === 'set_channels') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
$channels = sanitizeChannels((array)($_POST['channels'] ?? []), $channelNames);
|
|
|
|
$channels = sanitizeChannels((array)($_POST['channels'] ?? []), $channelNames);
|
|
|
|
foreach ($manifest['versions'] as &$v) {
|
|
|
|
$manifest['versions'][$idx]['channels'] = $channels;
|
|
|
|
if ($v['version'] === $version) {
|
|
|
|
|
|
|
|
$v['channels'] = $channels;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($v);
|
|
|
|
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '?';
|
|
|
|
$message = "Channels de v{$version} : " . implode(', ', $channels) . ". Re-Sync pour rafraîchir la signature du manifest.";
|
|
|
|
$message = "Channels de v{$version} : " . implode(', ', $channels) . ". Re-Sync pour rafraîchir la signature du manifest.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'set_beta') {
|
|
|
|
elseif ($action === 'set_beta') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
$isBeta = !empty($_POST['is_beta']);
|
|
|
|
$isBeta = !empty($_POST['is_beta']);
|
|
|
|
$betaNotes = trim($_POST['beta_notes'] ?? '') ?: null;
|
|
|
|
$betaNotes = trim($_POST['beta_notes'] ?? '') ?: null;
|
|
|
|
foreach ($manifest['versions'] as &$v) {
|
|
|
|
|
|
|
|
if ($v['version'] === $version) {
|
|
|
|
|
|
|
|
if ($isBeta) {
|
|
|
|
if ($isBeta) {
|
|
|
|
$v['isBeta'] = true;
|
|
|
|
$manifest['versions'][$idx]['isBeta'] = true;
|
|
|
|
if ($betaNotes !== null) {
|
|
|
|
if ($betaNotes !== null) {
|
|
|
|
$v['betaNotes'] = $betaNotes;
|
|
|
|
$manifest['versions'][$idx]['betaNotes'] = $betaNotes;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
unset($v['betaNotes']);
|
|
|
|
unset($manifest['versions'][$idx]['betaNotes']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
unset($v['isBeta']);
|
|
|
|
unset($manifest['versions'][$idx]['isBeta']);
|
|
|
|
unset($v['betaNotes']);
|
|
|
|
unset($manifest['versions'][$idx]['betaNotes']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($v);
|
|
|
|
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '?';
|
|
|
|
$message = "Statut BÊTA de v{$version} mis à jour. N'oublie pas de re-signer le manifest (« 🔁 Sync »).";
|
|
|
|
$message = "Statut BÊTA de v{$version} mis à jour. N'oublie pas de re-signer le manifest (« 🔁 Sync »).";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'toggle_available') {
|
|
|
|
elseif ($action === 'toggle_available') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
foreach ($manifest['versions'] as &$v) {
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
if ($v['version'] === $version) {
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
$v['availableForDownload'] = !($v['availableForDownload'] ?? true);
|
|
|
|
$manifest['versions'][$idx]['availableForDownload'] = !($manifest['versions'][$idx]['availableForDownload'] ?? true);
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($v);
|
|
|
|
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '?';
|
|
|
|
$message = "Disponibilité de v{$version} mise à jour.";
|
|
|
|
$message = "Disponibilité de v{$version} mise à jour.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'delete') {
|
|
|
|
elseif ($action === 'delete') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '?';
|
|
|
|
|
|
|
|
// Cleanup release notes file (best effort)
|
|
|
|
|
|
|
|
$notesFile = "$notesDir/{$entryId}.md";
|
|
|
|
|
|
|
|
if (is_file($notesFile)) @unlink($notesFile);
|
|
|
|
$manifest['versions'] = array_values(array_filter(
|
|
|
|
$manifest['versions'] = array_values(array_filter(
|
|
|
|
$manifest['versions'],
|
|
|
|
$manifest['versions'],
|
|
|
|
fn($v) => $v['version'] !== $version
|
|
|
|
fn($v) => ($v['id'] ?? '') !== $entryId
|
|
|
|
));
|
|
|
|
));
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
$message = "v{$version} retirée du manifest. Le ZIP reste dans builds/ (à supprimer en SFTP si voulu).";
|
|
|
|
$message = "v{$version} retirée du manifest. Le ZIP reste dans builds/ (à supprimer en SFTP si voulu).";
|
|
|
|
@@ -256,39 +309,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
|
|
if (!$result['ok']) $messageType = 'error';
|
|
|
|
if (!$result['ok']) $messageType = 'error';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'sync_one') {
|
|
|
|
elseif ($action === 'sync_one') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
if ($version === '') throw new Exception("Version manquante");
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '?';
|
|
|
|
require_once "$root/tools/SignManifest.php";
|
|
|
|
require_once "$root/tools/SignManifest.php";
|
|
|
|
$signer = new \PSLauncher\Tools\SignManifest($root);
|
|
|
|
$signer = new \PSLauncher\Tools\SignManifest($root);
|
|
|
|
$force = !empty($_POST['force']);
|
|
|
|
$force = !empty($_POST['force']);
|
|
|
|
|
|
|
|
// SignManifest::run filtre par numéro de version ; en cas d'entries
|
|
|
|
|
|
|
|
// de même version sur des channels différents, le hash sera recalculé
|
|
|
|
|
|
|
|
// pour toutes celles qui matchent (chacune pointe sur son propre ZIP,
|
|
|
|
|
|
|
|
// donc le résultat est correct, juste un peu plus de boulot).
|
|
|
|
$result = $signer->run('versions', $force, $version);
|
|
|
|
$result = $signer->run('versions', $force, $version);
|
|
|
|
$forceLabel = $force ? ' [FORCE]' : '';
|
|
|
|
$forceLabel = $force ? ' [FORCE]' : '';
|
|
|
|
$message = "Hash de v{$version}{$forceLabel} :\n" . implode("\n", $result['log']);
|
|
|
|
$message = "Hash de v{$version}{$forceLabel} :\n" . implode("\n", $result['log']);
|
|
|
|
if (!$result['ok']) $messageType = 'error';
|
|
|
|
if (!$result['ok']) $messageType = 'error';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($action === 'set_skip_hash') {
|
|
|
|
elseif ($action === 'set_skip_hash') {
|
|
|
|
$version = $_POST['version'] ?? '';
|
|
|
|
$entryId = (string)($_POST['id'] ?? '');
|
|
|
|
|
|
|
|
$idx = find_entry_index_by_id($manifest, $entryId);
|
|
|
|
|
|
|
|
if ($idx === null) throw new Exception("Entry id={$entryId} introuvable.");
|
|
|
|
$skipHash = !empty($_POST['skip']);
|
|
|
|
$skipHash = !empty($_POST['skip']);
|
|
|
|
foreach ($manifest['versions'] as &$v) {
|
|
|
|
$version = $manifest['versions'][$idx]['version'] ?? '?';
|
|
|
|
if ($v['version'] === $version) {
|
|
|
|
|
|
|
|
if ($skipHash) {
|
|
|
|
if ($skipHash) {
|
|
|
|
$v['download']['hashAlgorithm'] = 'none';
|
|
|
|
$manifest['versions'][$idx]['download']['hashAlgorithm'] = 'none';
|
|
|
|
$v['download']['sha256'] = '';
|
|
|
|
$manifest['versions'][$idx]['download']['sha256'] = '';
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
unset($v['download']['hashAlgorithm']);
|
|
|
|
unset($manifest['versions'][$idx]['download']['hashAlgorithm']);
|
|
|
|
if (empty($v['download']['sha256'])) {
|
|
|
|
if (empty($manifest['versions'][$idx]['download']['sha256'])) {
|
|
|
|
$v['download']['sha256'] = 'REPLACE_AFTER_BUILD';
|
|
|
|
$manifest['versions'][$idx]['download']['sha256'] = 'REPLACE_AFTER_BUILD';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($v);
|
|
|
|
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
saveManifest($manifestPath, $manifest);
|
|
|
|
|
|
|
|
|
|
|
|
// Re-signature auto pour garder le manifest valide après le changement.
|
|
|
|
// Re-signature auto pour garder le manifest valide après le changement.
|
|
|
|
// En mode skip, on n'a rien à hasher : on appelle run() qui se contentera
|
|
|
|
|
|
|
|
// de détecter hashAlgorithm=none et re-signera. Pas de calcul lourd.
|
|
|
|
|
|
|
|
require_once "$root/tools/SignManifest.php";
|
|
|
|
require_once "$root/tools/SignManifest.php";
|
|
|
|
$signer = new \PSLauncher\Tools\SignManifest($root);
|
|
|
|
$signer = new \PSLauncher\Tools\SignManifest($root);
|
|
|
|
$resign = $signer->run('versions', false);
|
|
|
|
$resign = $signer->run('versions', false);
|
|
|
|
@@ -310,11 +364,21 @@ $zips = is_dir($buildsDir) ? array_map('basename', glob("$buildsDir/*.zip")
|
|
|
|
$zipSizes = [];
|
|
|
|
$zipSizes = [];
|
|
|
|
foreach ($zips as $z) $zipSizes[$z] = filesize("$buildsDir/$z");
|
|
|
|
foreach ($zips as $z) $zipSizes[$z] = filesize("$buildsDir/$z");
|
|
|
|
|
|
|
|
|
|
|
|
// Pour l'édition : pré-charge les release notes existantes
|
|
|
|
// Pour l'édition : pré-charge les release notes existantes (indexées par
|
|
|
|
|
|
|
|
// id stable maintenant). Fallback sur l'ancien format {version}.md pour les
|
|
|
|
|
|
|
|
// notes créées avant v0.27.1, qu'on n'a pas encore migrées.
|
|
|
|
$existingNotes = [];
|
|
|
|
$existingNotes = [];
|
|
|
|
foreach ($manifest['versions'] ?? [] as $v) {
|
|
|
|
foreach ($manifest['versions'] ?? [] as $v) {
|
|
|
|
$f = "$notesDir/{$v['version']}.md";
|
|
|
|
$id = $v['id'] ?? '';
|
|
|
|
$existingNotes[$v['version']] = is_file($f) ? file_get_contents($f) : '';
|
|
|
|
$byId = $id !== '' ? "$notesDir/{$id}.md" : null;
|
|
|
|
|
|
|
|
$byVersion = "$notesDir/{$v['version']}.md";
|
|
|
|
|
|
|
|
if ($byId !== null && is_file($byId)) {
|
|
|
|
|
|
|
|
$existingNotes[$id] = file_get_contents($byId);
|
|
|
|
|
|
|
|
} elseif (is_file($byVersion)) {
|
|
|
|
|
|
|
|
$existingNotes[$id] = file_get_contents($byVersion);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
$existingNotes[$id] = '';
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Layout::header('Versions', 'versions');
|
|
|
|
Layout::header('Versions', 'versions');
|
|
|
|
@@ -478,7 +542,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="display:inline">
|
|
|
|
<form method="post" style="display:inline">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="toggle_available">
|
|
|
|
<input type="hidden" name="action" value="toggle_available">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<button class="btn btn-secondary" type="submit">
|
|
|
|
<button class="btn btn-secondary" type="submit">
|
|
|
|
<?= ($v['availableForDownload'] ?? true) ? '✓' : '✗' ?>
|
|
|
|
<?= ($v['availableForDownload'] ?? true) ? '✓' : '✗' ?>
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
@@ -489,7 +553,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="display:inline" title="Recalcule le SHA-256 de cette version uniquement (utilise le cache si le ZIP n'a pas changé)">
|
|
|
|
<form method="post" style="display:inline" title="Recalcule le SHA-256 de cette version uniquement (utilise le cache si le ZIP n'a pas changé)">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="sync_one">
|
|
|
|
<input type="hidden" name="action" value="sync_one">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<button class="btn btn-secondary" type="submit">🔁 Hash</button>
|
|
|
|
<button class="btn btn-secondary" type="submit">🔁 Hash</button>
|
|
|
|
</form>
|
|
|
|
</form>
|
|
|
|
<?php endif; ?>
|
|
|
|
<?php endif; ?>
|
|
|
|
@@ -509,7 +573,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="display:inline">
|
|
|
|
<form method="post" style="display:inline">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="set_skip_hash">
|
|
|
|
<input type="hidden" name="action" value="set_skip_hash">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<input type="hidden" name="skip" value="<?= $hashSkipped ? '0' : '1' ?>">
|
|
|
|
<input type="hidden" name="skip" value="<?= $hashSkipped ? '0' : '1' ?>">
|
|
|
|
<button class="btn <?= $hashSkipped ? 'btn-success' : 'btn-warning' ?>" type="submit">
|
|
|
|
<button class="btn <?= $hashSkipped ? 'btn-success' : 'btn-warning' ?>" type="submit">
|
|
|
|
<?= $hashSkipped ? '✓ Réactiver la vérif' : '⏭ Désactiver la vérif' ?>
|
|
|
|
<?= $hashSkipped ? '✓ Réactiver la vérif' : '⏭ Désactiver la vérif' ?>
|
|
|
|
@@ -519,7 +583,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="margin-top: 8px;" title="Recalcule en ignorant le cache même si le ZIP n'a pas changé">
|
|
|
|
<form method="post" style="margin-top: 8px;" title="Recalcule en ignorant le cache même si le ZIP n'a pas changé">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="sync_one">
|
|
|
|
<input type="hidden" name="action" value="sync_one">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<input type="hidden" name="force" value="1">
|
|
|
|
<input type="hidden" name="force" value="1">
|
|
|
|
<button class="btn btn-secondary" type="submit">🔄 Force re-hash</button>
|
|
|
|
<button class="btn btn-secondary" type="submit">🔄 Force re-hash</button>
|
|
|
|
</form>
|
|
|
|
</form>
|
|
|
|
@@ -531,7 +595,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="margin-top: 8px; min-width: 360px;">
|
|
|
|
<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="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<div class="field">
|
|
|
|
<div class="field">
|
|
|
|
<label>min_license_date</label>
|
|
|
|
<label>min_license_date</label>
|
|
|
|
<input type="date" name="min_license_date" value="<?= htmlspecialchars($v['minLicenseDate'] ?? '') ?>" required>
|
|
|
|
<input type="date" name="min_license_date" value="<?= htmlspecialchars($v['minLicenseDate'] ?? '') ?>" required>
|
|
|
|
@@ -553,8 +617,8 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="margin-top: 8px;">
|
|
|
|
<form method="post" style="margin-top: 8px;">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="edit_notes">
|
|
|
|
<input type="hidden" name="action" value="edit_notes">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<textarea name="notes" rows="10" style="font-family: 'Cascadia Code', Consolas, monospace; min-width: 400px;"><?= htmlspecialchars($existingNotes[$v['version']] ?? '') ?></textarea>
|
|
|
|
<textarea name="notes" rows="10" style="font-family: 'Cascadia Code', Consolas, monospace; min-width: 400px;"><?= htmlspecialchars($existingNotes[$v['id'] ?? ''] ?? '') ?></textarea>
|
|
|
|
<br><br>
|
|
|
|
<br><br>
|
|
|
|
<button class="btn btn-primary" type="submit">Enregistrer les notes</button>
|
|
|
|
<button class="btn btn-primary" type="submit">Enregistrer les notes</button>
|
|
|
|
</form>
|
|
|
|
</form>
|
|
|
|
@@ -564,7 +628,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="margin-top: 8px; min-width: 360px;">
|
|
|
|
<form method="post" style="margin-top: 8px; min-width: 360px;">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="set_beta">
|
|
|
|
<input type="hidden" name="action" value="set_beta">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<div class="field">
|
|
|
|
<div class="field">
|
|
|
|
<label><input type="checkbox" name="is_beta" value="1" <?= !empty($v['isBeta']) ? 'checked' : '' ?>>
|
|
|
|
<label><input type="checkbox" name="is_beta" value="1" <?= !empty($v['isBeta']) ? 'checked' : '' ?>>
|
|
|
|
Cette version est une BÊTA
|
|
|
|
Cette version est une BÊTA
|
|
|
|
@@ -584,7 +648,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="margin-top: 8px; min-width: 280px;">
|
|
|
|
<form method="post" style="margin-top: 8px; min-width: 280px;">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="set_channels">
|
|
|
|
<input type="hidden" name="action" value="set_channels">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<div class="field">
|
|
|
|
<div class="field">
|
|
|
|
<?php foreach ($channelRegistry['channels'] as $c):
|
|
|
|
<?php foreach ($channelRegistry['channels'] as $c):
|
|
|
|
$cn = $c['name'] ?? '';
|
|
|
|
$cn = $c['name'] ?? '';
|
|
|
|
@@ -604,7 +668,7 @@ Layout::header('Versions', 'versions');
|
|
|
|
<form method="post" style="display:inline" onsubmit="return confirm('Retirer v<?= htmlspecialchars($v['version']) ?> du manifest ?\n(Le ZIP reste dans builds/, supprime-le en SFTP si voulu.)')">
|
|
|
|
<form method="post" style="display:inline" onsubmit="return confirm('Retirer v<?= htmlspecialchars($v['version']) ?> du manifest ?\n(Le ZIP reste dans builds/, supprime-le en SFTP si voulu.)')">
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<?= Layout::csrfField() ?>
|
|
|
|
<input type="hidden" name="action" value="delete">
|
|
|
|
<input type="hidden" name="action" value="delete">
|
|
|
|
<input type="hidden" name="version" value="<?= htmlspecialchars($v['version']) ?>">
|
|
|
|
<input type="hidden" name="id" value="<?= htmlspecialchars($v['id'] ?? '') ?>">
|
|
|
|
<button class="btn btn-danger" type="submit">Retirer</button>
|
|
|
|
<button class="btn btn-danger" type="submit">Retirer</button>
|
|
|
|
</form>
|
|
|
|
</form>
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
|