UI overhaul: minimal top bar, license-first settings, footer-pinned check
Top bar (3-column layout) ------------------------- - Col 1: PROSERVE Launcher wordmark. - Col 2: license badge centered, the badge IS the click target now (a Border with an InputBindings MouseBinding LeftClick to OpenLicense). No more separate "🔑 Activer / changer" button cluttering the right. - Col 3: ⚙ Paramètres + window chrome (min/max/close). - "📁 Dossier" button removed from the top bar — install root is still reachable from Settings. Footer (always visible) ----------------------- - Row 1: "🔄 Vérifier les MAJ" pinned bottom-left, always shown. The optional "Annuler" button stays bottom-right while a download runs. - Row 2: status text + progress bar, only shown when busy or after a status message — the previous "footer entirely hidden when idle" hid the check button too. License flow split in two ------------------------- Click on the license badge: - License is active (valid / expired / revoked) → LicenseDetailsDialog opens. Header pill in matching status color (green/amber/red), shows owner, validity, issued date, machine ID with copy-to-clipboard. Two buttons: "🗑 Désactiver la license" (with confirmation) and Close. - No license OR after deactivation → falls through to the existing OnboardingDialog for re-keying. Settings rework --------------- LICENSE section is now first in SettingsDialog with the same green/amber/red colored chrome as the top bar — at a glance the user sees the same status everywhere. Machine ID copy moved into this card. Sign-manifest no longer needs exec() ------------------------------------ The "🔁 Sync" button in admin/versions.php previously shelled out to `php tools/sign-manifest.php` via exec(). OVH mutualisé often disables exec(), causing silent no-ops and the symptom the user just hit: the ZIP changed (new size 469,657,770 vs manifest's stale 469,831,428) and the launcher rejected it as size mismatch. Refactor: - New PSLauncher\Tools\SignManifest class with ->run() that does the hashing, latest-bump and Ed25519 signing in-process. - tools/sign-manifest.php is now a 6-line wrapper for the class. - admin/versions.php's 'sync' action calls the class directly via require_once + new — works on any host, no exec dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -133,12 +133,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$message = "v{$version} retirée du manifest. Le ZIP reste dans builds/ (à supprimer en SFTP si voulu).";
|
||||
}
|
||||
elseif ($action === 'sync') {
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
$cwd = escapeshellarg($root);
|
||||
exec("cd $cwd && php tools/sign-manifest.php 2>&1", $output, $exitCode);
|
||||
$message = "Sortie du script :\n" . implode("\n", $output);
|
||||
if ($exitCode !== 0) $messageType = 'error';
|
||||
// Appel direct à la classe — pas d'exec(), fonctionne même quand la fonction
|
||||
// est désactivée par le mutualisé OVH.
|
||||
require_once "$root/tools/SignManifest.php";
|
||||
$signer = new \PSLauncher\Tools\SignManifest($root);
|
||||
$result = $signer->run();
|
||||
$message = "Sortie du script :\n" . implode("\n", $result['log']);
|
||||
if (!$result['ok']) $messageType = 'error';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$message = $e->getMessage();
|
||||
|
||||
124
server/tools/SignManifest.php
Normal file
124
server/tools/SignManifest.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PSLauncher\Tools;
|
||||
|
||||
/**
|
||||
* Logique de re-signature du manifest, sans dépendance shell.
|
||||
* Utilisable depuis le CLI (tools/sign-manifest.php) et depuis le backoffice
|
||||
* web (admin/versions.php → action 'sync'). Évite tout appel à exec().
|
||||
*/
|
||||
final class SignManifest
|
||||
{
|
||||
public string $manifestPath;
|
||||
public string $buildsDir;
|
||||
public ?string $configPath;
|
||||
/** @var string[] */
|
||||
public array $log = [];
|
||||
|
||||
public function __construct(string $rootDir)
|
||||
{
|
||||
$this->manifestPath = $rootDir . '/manifest/versions.json';
|
||||
$this->buildsDir = $rootDir . '/builds';
|
||||
$this->configPath = $rootDir . '/api/config.php';
|
||||
}
|
||||
|
||||
private function out(string $line): void { $this->log[] = $line; }
|
||||
|
||||
/**
|
||||
* Recompute sizes / sha256 for every version that has its ZIP uploaded,
|
||||
* bump 'latest' to the highest signed version, then sign the manifest
|
||||
* with the Ed25519 private key from config.php (if available).
|
||||
*
|
||||
* @return array{ok:bool, log:string[]}
|
||||
*/
|
||||
public function run(): array
|
||||
{
|
||||
if (!is_file($this->manifestPath)) {
|
||||
$this->out("Manifest introuvable : {$this->manifestPath}");
|
||||
return ['ok' => false, 'log' => $this->log];
|
||||
}
|
||||
|
||||
$manifest = json_decode(file_get_contents($this->manifestPath), true);
|
||||
if (!is_array($manifest)) {
|
||||
$this->out("Manifest illisible (JSON invalide)");
|
||||
return ['ok' => false, 'log' => $this->log];
|
||||
}
|
||||
|
||||
$hashedVersions = [];
|
||||
foreach ($manifest['versions'] as &$v) {
|
||||
$version = $v['version'] ?? '?';
|
||||
$url = $v['download']['url'] ?? '';
|
||||
if ($url === '') {
|
||||
$this->out(" [skip] $version : pas d'URL dans le manifest");
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = basename(parse_url($url, PHP_URL_PATH) ?: '');
|
||||
$zip = "{$this->buildsDir}/$filename";
|
||||
|
||||
if (!is_file($zip)) {
|
||||
$candidates = 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");
|
||||
} else {
|
||||
$this->out(" [skip] $version : ZIP introuvable pour $url");
|
||||
if (count($candidates) > 1) {
|
||||
$this->out(" Plusieurs candidats : " . implode(', ', array_map('basename', $candidates)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$size = filesize($zip);
|
||||
$sha = hash_file('sha256', $zip);
|
||||
$this->out(" [hash] $version : " . basename($zip) . " ($size octets) sha256={$sha}");
|
||||
|
||||
$v['download']['sizeBytes'] = $size;
|
||||
$v['download']['sha256'] = $sha;
|
||||
$hashedVersions[] = $version;
|
||||
}
|
||||
unset($v);
|
||||
|
||||
// Bump auto de `latest` sur la plus haute version effectivement uploadée
|
||||
if (!empty($hashedVersions)) {
|
||||
usort($hashedVersions, 'version_compare');
|
||||
$newLatest = end($hashedVersions);
|
||||
if (($manifest['latest'] ?? null) !== $newLatest) {
|
||||
$this->out(" [latest] " . ($manifest['latest'] ?? '(none)') . " -> {$newLatest}");
|
||||
$manifest['latest'] = $newLatest;
|
||||
}
|
||||
}
|
||||
|
||||
// Signature Ed25519
|
||||
if ($this->configPath && is_file($this->configPath)) {
|
||||
$config = require $this->configPath;
|
||||
$sk = $config['ed25519']['private_key_hex'] ?? '';
|
||||
if ($sk !== '' && strlen($sk) === 128) {
|
||||
$manifest['signature'] = null;
|
||||
$cryptoPath = dirname($this->configPath) . '/lib/Crypto.php';
|
||||
if (is_file($cryptoPath)) require_once $cryptoPath;
|
||||
if (class_exists('\PSLauncher\Crypto')) {
|
||||
$payload = \PSLauncher\Crypto::canonicalJson($manifest);
|
||||
$manifest['signature'] = \PSLauncher\Crypto::signEd25519($payload, $sk);
|
||||
$this->out(" [sign] manifest signé (Ed25519)");
|
||||
} else {
|
||||
$this->out(" [warn] Classe \\PSLauncher\\Crypto introuvable, manifest non signé");
|
||||
}
|
||||
} else {
|
||||
$this->out(" [warn] ed25519.private_key_hex non configuré, manifest non signé");
|
||||
}
|
||||
}
|
||||
|
||||
$jsonOut = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
if (file_put_contents($this->manifestPath, $jsonOut) === false) {
|
||||
$this->out("Échec d'écriture du manifest : {$this->manifestPath}");
|
||||
return ['ok' => false, 'log' => $this->log];
|
||||
}
|
||||
|
||||
$this->out("Manifest mis à jour (" . count($hashedVersions) . " version(s)).");
|
||||
return ['ok' => true, 'log' => $this->log];
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Met à jour les champs `download.sizeBytes` et `download.sha256` de versions.json
|
||||
* en lisant les ZIPs présents dans /builds/. Le nom local du ZIP est dérivé du
|
||||
* champ `download.url` (basename de l'URL), donc tu peux nommer ton fichier comme
|
||||
* tu veux tant que le manifest pointe vers le bon nom.
|
||||
* Wrapper CLI pour PSLauncher\Tools\SignManifest.
|
||||
*
|
||||
* Usage (à exécuter via SSH OVH dans www/PS_Launcher/) :
|
||||
* php tools/sign-manifest.php
|
||||
* Usage : cd ~/www/PS_Launcher && php tools/sign-manifest.php
|
||||
*
|
||||
* (v0.4) Ajoutera la signature Ed25519 globale du manifest.
|
||||
* Le backoffice admin appelle directement la classe (pas d'exec).
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$manifestPath = "$root/manifest/versions.json";
|
||||
$buildsDir = "$root/builds";
|
||||
require __DIR__ . '/SignManifest.php';
|
||||
|
||||
if (!is_file($manifestPath)) {
|
||||
fwrite(STDERR, "Manifest not found: $manifestPath\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$manifest = json_decode(file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$updated = 0;
|
||||
$hashedVersions = [];
|
||||
foreach ($manifest['versions'] as &$v) {
|
||||
$version = $v['version'] ?? '?';
|
||||
$url = $v['download']['url'] ?? '';
|
||||
if ($url === '') {
|
||||
echo " [skip] $version : pas d'URL dans le manifest\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = basename(parse_url($url, PHP_URL_PATH) ?: '');
|
||||
$zip = "$buildsDir/$filename";
|
||||
|
||||
if (!is_file($zip)) {
|
||||
// Fallback : si le fichier n'existe pas exactement avec le nom de l'URL,
|
||||
// on tente une recherche tolérante par version (espaces / casse / séparateurs).
|
||||
$candidates = glob("$buildsDir/*{$version}*.zip", GLOB_NOSORT) ?: [];
|
||||
$candidates = array_values(array_filter($candidates, 'is_file'));
|
||||
if (count($candidates) === 1) {
|
||||
$zip = $candidates[0];
|
||||
echo " [info] $version : URL pointait vers '{$filename}', utilisé '" . basename($zip) . "' à la place\n";
|
||||
} else {
|
||||
echo " [skip] $version : ZIP introuvable pour $url\n";
|
||||
if (count($candidates) > 1) {
|
||||
echo " Plusieurs candidats : " . implode(', ', array_map('basename', $candidates)) . "\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$size = filesize($zip);
|
||||
echo " [hash] $version : " . basename($zip) . " ($size octets)...";
|
||||
$sha = hash_file('sha256', $zip);
|
||||
echo " sha256={$sha}\n";
|
||||
|
||||
$v['download']['sizeBytes'] = $size;
|
||||
$v['download']['sha256'] = $sha;
|
||||
$updated++;
|
||||
$hashedVersions[] = $version;
|
||||
}
|
||||
unset($v);
|
||||
|
||||
// Met à jour automatiquement le champ `latest` avec la plus haute version
|
||||
// effectivement uploadée (celle pour laquelle on a calculé un hash).
|
||||
if (!empty($hashedVersions)) {
|
||||
usort($hashedVersions, function ($a, $b) {
|
||||
return version_compare($a, $b);
|
||||
});
|
||||
$newLatest = end($hashedVersions);
|
||||
if (($manifest['latest'] ?? null) !== $newLatest) {
|
||||
echo " [latest] {$manifest['latest']} -> {$newLatest}\n";
|
||||
$manifest['latest'] = $newLatest;
|
||||
}
|
||||
}
|
||||
|
||||
// (v0.4) Signature Ed25519 du manifest
|
||||
$configPath = dirname(__DIR__) . '/api/config.php';
|
||||
if (is_file($configPath)) {
|
||||
require_once dirname(__DIR__) . '/api/lib/Crypto.php';
|
||||
$config = require $configPath;
|
||||
$sk = $config['ed25519']['private_key_hex'] ?? '';
|
||||
if ($sk !== '' && strlen($sk) === 128) {
|
||||
// Retire signature précédente, encode canonical, signe, ré-injecte
|
||||
$manifest['signature'] = null;
|
||||
$payload = \PSLauncher\Crypto::canonicalJson($manifest);
|
||||
$manifest['signature'] = \PSLauncher\Crypto::signEd25519($payload, $sk);
|
||||
echo " [sign] manifest signed (Ed25519)\n";
|
||||
} else {
|
||||
echo " [warn] ed25519.private_key_hex non configuré, manifest non signé\n";
|
||||
}
|
||||
} else {
|
||||
echo " [warn] config.php absent, manifest non signé\n";
|
||||
}
|
||||
|
||||
file_put_contents(
|
||||
$manifestPath,
|
||||
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
|
||||
);
|
||||
echo "Manifest mis à jour ({$updated} version(s)) : $manifestPath\n";
|
||||
$signer = new \PSLauncher\Tools\SignManifest(dirname(__DIR__));
|
||||
$result = $signer->run();
|
||||
foreach ($result['log'] as $line) echo $line . "\n";
|
||||
exit($result['ok'] ? 0 : 1);
|
||||
|
||||
Reference in New Issue
Block a user