check(Http::clientIp())) { Http::fail(429, 'rate_limited', 'Zu viele Anfragen.'); } $manager = new UpdateManager($db); $action = resolveUpdateAction(); switch ($action) { case 'check': $product = Http::str('product') ?? Http::str('product_slug'); if ($product === null) { Http::fail(400, 'missing_product', 'Der Parameter "product" wird benoetigt.'); } $current = Http::str('version') ?? Http::str('current_version') ?? '0.0.0'; $channel = Http::str('channel') ?? 'prod'; $latest = $manager->checkUpdate($product, $current, $channel); if ($latest === null) { $installed = $manager->latestRelease($product, $channel); Http::ok([ 'update_available' => false, 'current_version' => $current, 'latest_version' => $installed !== null ? $installed['version'] : $current, 'message' => 'Anwendung ist aktuell.', ]); } Http::ok([ 'update_available' => true, 'current_version' => $current, 'latest_version' => $latest['version'], 'is_critical' => (bool)$latest['is_critical'], 'latest_release' => $latest, ]); case 'latest': $product = Http::str('product') ?? Http::str('product_slug'); if ($product === null) { Http::fail(400, 'missing_product', 'Der Parameter "product" wird benoetigt.'); } $release = $manager->latestRelease($product, Http::str('channel') ?? 'prod'); if ($release === null) { Http::fail(404, 'no_release', sprintf('Fuer "%s" ist kein Release hinterlegt.', $product)); } Http::ok(['release' => $release]); case 'releases': $releases = $manager->getReleases( Http::str('product') ?? Http::str('product_slug'), Http::str('channel'), Http::int('limit', 200) ); Http::ok(['count' => count($releases), 'releases' => $releases]); case 'publish': if (Http::method() !== 'POST') { Http::fail(405, 'method_not_allowed', 'Das Veroeffentlichen erwartet POST.'); } $context = ApiAuth::requireScope($db, 'updateservice:publish'); $product = Http::str('product_slug') ?? Http::str('product'); $version = Http::str('version'); $url = Http::str('download_url'); $missing = []; if ($product === null) { $missing[] = 'product_slug'; } if ($version === null) { $missing[] = 'version'; } if ($url === null) { $missing[] = 'download_url'; } if ($missing !== []) { Http::fail(400, 'missing_fields', 'Pflichtfelder fehlen: ' . implode(', ', $missing), null, [ 'missing' => $missing, ]); } ApiAuth::enforceProject($context, $product); // Eine Version ohne Ziffern wuerde beim Vergleich als 0.0.0 gelten und // die Rangfolge aller Releases dieses Produkts durcheinanderbringen. if (preg_match('/^v?\d+(\.\d+)*([-+].*)?$/i', $version) !== 1) { Http::fail(400, 'invalid_version', sprintf( 'Version "%s" ist nicht interpretierbar. Erwartet wird eine Form wie 1.4.3, v1.4.3 oder 1.4.3-beta.1.', $version )); } $hash = Http::str('sha256_hash'); if ($hash !== null && preg_match('/^[0-9a-f]{64}$/i', $hash) !== 1) { Http::fail(400, 'invalid_hash', 'sha256_hash muss 64 Hexadezimalzeichen enthalten.'); } $result = $manager->addRelease( $product, $version, Http::str('channel') ?? 'prod', Http::str('release_notes'), $url, $hash, Http::str('git_commit'), Http::int('size_bytes', 0), null, filter_var(Http::input('is_critical', false), FILTER_VALIDATE_BOOLEAN), $context['actor'] ); Http::ok([ 'release_id' => $result['id'], 'created' => $result['created'], 'auto_resolved' => $result['auto_resolved'], 'message' => sprintf( 'Release %s (%s) fuer "%s" %s.%s', $version, Http::str('channel') ?? 'prod', $product, $result['created'] ? 'veroeffentlicht' : 'aktualisiert', $result['auto_resolved'] > 0 ? sprintf(' %d Bugtracker-Item(s) automatisch geschlossen.', $result['auto_resolved']) : '' ), ], $result['created'] ? 201 : 200); default: Http::fail(404, 'unknown_action', 'Endpunkt nicht gefunden.', null, [ 'available' => ['check', 'latest', 'releases', 'publish'], ]); } /** * Bestimmt die Aktion aus dem Pfadsegment oder ?action=. * Feste Liste statt Teilstring-Suche. */ function resolveUpdateAction(): string { $explicit = Http::str('action'); if ($explicit !== null) { // Altes Feld hiess publish_release return $explicit === 'publish_release' ? 'publish' : strtolower($explicit); } $segments = array_values(array_filter( explode('/', trim(Http::path(), '/')), static fn(string $s): bool => $s !== '' )); $last = strtolower((string)end($segments)); return match ($last) { 'check', 'latest', 'releases', 'publish' => $last, 'publish_release' => 'publish', default => 'check', }; }