$s !== '' )); $last = strtolower((string)end($segments)); return match ($last) { 'observe' => 'observe', 'summary' => 'summary', 'catalog' => 'catalog', 'status' => 'status', default => 'findings', }; } Http::beginJson(['GET', 'POST', 'OPTIONS'], true); $action = resolveSupervisorAction(); if (!in_array($action, READ_ACTIONS, true) && !in_array($action, WRITE_ACTIONS, true)) { Http::fail(404, 'unknown_action', sprintf('Unbekannte Aktion "%s".', $action), null, [ 'available' => array_merge(READ_ACTIONS, WRITE_ACTIONS), ]); } $isWrite = in_array($action, WRITE_ACTIONS, true); if ($isWrite && Http::method() !== 'POST') { Http::fail(405, 'method_not_allowed', sprintf('Die Aktion "%s" erwartet POST.', $action)); } $db = Db::init(); // Ein Beobachter meldet regelmaessig. Bricht seine Zustandsverwaltung, sendet // er im Sekundentakt - dann soll er gedrosselt werden und nicht die Tabelle // fuellen. if ($action === 'observe') { $limiter = new RateLimiter( $db, (int)Config::get('supervisor.observe_rate', 120), 60, 'sv_observe' ); if (!$limiter->check(Http::clientIp())) { Http::fail(429, 'rate_limited', 'Zu viele Befunde. Bitte Sendefrequenz reduzieren.'); } } // Drei Rechte, drei Rollen: // observe - der Beobachter auf der Kundenmaschine, meldet nur // status - die Sichtung, entscheidet ueber Befunde // Rest - Lesen // // Beim Einliefern ist die Sitzung bewusst nicht erlaubt: das ist die // Maschinenschnittstelle. $scope = match ($action) { 'observe' => 'supervisor:report', 'status' => 'supervisor:manage', default => 'supervisor:read', }; $context = ApiAuth::requireScope($db, $scope, null, $action !== 'observe'); $repo = new FindingRepo($db); switch ($action) { // ---------------------------------------------------------------- lesen case 'catalog': Http::ok([ 'count' => count(Catalog::DETECTORS), 'detectors' => Catalog::DETECTORS, ]); // no break - Http::ok beendet die Anfrage case 'summary': $slug = ApiAuth::projectFilter($context, true) ?? Http::str('project_slug'); Http::ok([ 'project_slug' => $slug, 'by_detector' => $repo->byDetector($slug), ]); case 'findings': $bound = ApiAuth::projectFilter($context, true); $result = $repo->search([ 'project_slug' => $bound ?? Http::str('project_slug') ?? 'all', 'detector' => Http::str('detector'), 'status' => Http::str('status'), 'instance' => Http::str('instance'), 'build' => Http::str('build'), 'environment' => Http::str('environment'), 'since' => Http::str('since'), 'limit' => Http::int('limit', 50), 'offset' => Http::int('offset', 0), ]); Http::ok($result); // ------------------------------------------------------------ schreiben case 'observe': $data = Http::body(); if ($data === []) { Http::fail(400, 'empty_body', 'Der Request-Body ist leer.'); } $projectSlug = is_string($data['project_slug'] ?? null) ? trim($data['project_slug']) : null; ApiAuth::enforceProject($context, $projectSlug); // Ein projektgebundenes Token meldet immer fuer sein eigenes Projekt. // Anders als beim Bugtracker gibt es hier keine Ausnahme fuer // Werkbank-Token: ein Befund ist eine Beobachtung an einer laufenden // Installation, und die macht man nicht stellvertretend fuer ein // fremdes Projekt. $home = ApiAuth::homeProject($context); if ($home !== null) { $data['project_slug'] = $home; } // Die Herkunft wird abgeleitet, nicht uebernommen - ein Client darf // seine Befunde nicht als serverseitig ermittelt ausgeben. $data['origin'] = Catalog::ORIGIN_CLIENT; try { $result = $repo->record($data); } catch (\InvalidArgumentException $e) { Http::fail(400, 'invalid_finding', $e->getMessage()); } Http::ok([ 'finding_id' => $result['id'], 'is_new' => $result['is_new'], 'occurrence_count' => $result['occurrence_count'], 'detector' => $result['detector'], // NICHT 'status'. Http::ok() legt ['status' => 'success'] als // Umschlag darum und merged dieses Feld darueber - der Zustand des // Befundes ueberschrieb also die Erfolgsmeldung. Jeder Client, der // wie dokumentiert auf status === "success" prueft, brach // ausgerechnet an dem Endpunkt, den ein meldender Client am // haeufigsten aufruft. 'finding_status' => $result['status'], 'client_ref' => $result['client_ref'], // Ein unbekannter Detektorname wird angenommen, aber gemeldet. // Still verwerfen waere schlimmer: dann suchte jemand tagelang // nach Befunden, die nie ankamen. 'known_detector' => $result['known_detector'], 'message' => $result['known_detector'] ? ($result['is_new'] ? 'Befund aufgenommen.' : 'Bekannter Befund - Zaehler erhoeht.') : sprintf( 'Befund aufgenommen. Achtung: "%s" steht nicht im Katalog - Tippfehler, ' . 'oder der Katalog muss erweitert werden.', $result['detector'] ), ], $result['is_new'] ? 201 : 200); case 'status': $id = Http::int('id', 0); if ($id <= 0) { Http::fail(400, 'missing_id', 'Es wurde keine Befund-ID uebergeben.'); } $finding = $repo->get($id); if ($finding === null) { Http::fail(404, 'not_found', sprintf('Befund #%d existiert nicht.', $id)); } ApiAuth::enforceProject($context, (string)$finding['project_slug'], true); $status = Http::str('status'); if ($status === null || !in_array($status, FindingRepo::STATUSES, true)) { Http::fail(400, 'invalid_status', 'Ungueltiger Status.', null, [ 'allowed' => FindingRepo::STATUSES, ]); } if (!$repo->setStatus($id, $status, $context['actor'])) { Http::fail(500, 'update_failed', sprintf('Befund #%d konnte nicht geaendert werden.', $id)); } Http::ok([ 'finding_id' => $id, // Aus demselben Grund wie bei 'observe' nicht 'status': Der // gesetzte Zustand ueberschriebe sonst die Erfolgsmeldung des // Umschlags. Hier faellt es weniger auf, weil ein Mensch die // Einstufung ausloest - der Fehler ist derselbe. 'finding_status' => $status, 'message' => sprintf('Befund #%d auf "%s" gesetzt.', $id, $status), ]); }