check(Http::clientIp())) { Http::fail(429, 'rate_limited', 'Zu viele Reports. Bitte Sendefrequenz reduzieren.'); } $data = Http::body(); if ($data === []) { Http::fail(400, 'empty_body', 'Der Request-Body ist leer.'); } $environment = is_string($data['environment'] ?? null) ? $data['environment'] : null; // Session ist hier bewusst nicht erlaubt: dieser Endpunkt ist die // Maschinenschnittstelle. Das WebUI legt Items ueber index.php an. $context = ApiAuth::requireScope($db, 'bugtracker:report', $environment, false); $projectSlug = is_string($data['project_slug'] ?? null) ? trim($data['project_slug']) : null; // Werkbank-Token duerfen in ein fremdes Projekt melden - ein Agent, der das SDK // in Projekt X integriert und dabei einen Fehler im Deploymentcenter findet, // muss ihn dort ablegen koennen. Laufzeit-Token duerfen das nicht: sie stecken // im ausgelieferten Client und sind extrahierbar. ApiAuth::enforceProject($context, $projectSlug, true); $homeProject = ApiAuth::homeProject($context); // Ein Laufzeit-Token schreibt immer in sein eigenes Projekt. if ($homeProject !== null && !ApiAuth::isWorkbench($context)) { $data['project_slug'] = $homeProject; } // --- Fremdmeldung? ---------------------------------------------------------- $target = is_string($data['project_slug'] ?? null) ? trim($data['project_slug']) : null; $isForeign = $homeProject !== null && $target !== null && $target !== '' && $target !== $homeProject; // Die Herkunft wird aus der Token-Bindung abgeleitet und niemals aus dem Body // uebernommen. Nur so ist sie bezeugt statt behauptet - und nur dann traegt das // Signal "drei Projekte melden dasselbe" ueberhaupt etwas. $rawEvidence = $data['evidence'] ?? null; unset($data['reported_from'], $data['evidence']); if ($isForeign) { $evidence = is_string($rawEvidence) ? trim($rawEvidence) : null; // Begruendungspflicht. Ein Agent in Projekt X kann kaum unterscheiden, ob // das fremde Projekt kaputt ist oder ob X es falsch benutzt; die // Voreinstellung eines hilfsbereiten Modells ist im Zweifel "melde es". // Etwas ausformulieren zu muessen kostet einen Reflex fast immer den Kopf. if ($evidence === null || mb_strlen($evidence) < MIN_EVIDENCE_LENGTH) { Http::fail( 400, 'missing_evidence', sprintf( 'Eine Meldung gegen das fremde Projekt "%s" verlangt das Feld "evidence" ' . '(mindestens %d Zeichen): was wurde beobachtet, und warum liegt die ' . 'Ursache dort und nicht in "%s"?', $target, MIN_EVIDENCE_LENGTH, $homeProject ), null, ['home_project' => $homeProject, 'target_project' => $target] ); } $data['reported_from'] = $homeProject; $data['evidence'] = $evidence; } // Der Absender wird aus dem Token abgeleitet und kann nicht frei gewaehlt // werden - sonst koennte sich ein Agent als ein anderer ausgeben. $data['created_by'] = $context['actor']; // Idempotenz-Schluessel darf auch als Header kommen. if (empty($data['client_ref'])) { $headerRef = Http::header('idempotency-key'); if ($headerRef !== null) { $data['client_ref'] = $headerRef; } } $repo = new BugRepo($db); $result = $repo->reportItem($data); // Bekannt und in einer spaeteren Fassung behoben. Diese Antwort geht jeder // anderen vor: Sie beendet die Frage, statt sie einzusortieren. Der Melder // prueft eine Fassung, in der der Fix noch nicht steckt - er soll aufhoeren zu // suchen und aktualisieren, nicht ein weiteres Item anlegen. // Der Fix hat nicht getragen. Kein neues Item - das bestehende ist zurueck in // der Arbeitsschlange, und der Beleg haengt daran. if (!empty($result['fix_insufficient'])) { $message = sprintf( 'Der Fix traegt nicht. Item #%d steht wieder auf "fix_insufficient" und liegt in der ' . 'Arbeitsschlange - kein neues Item angelegt, der Beleg haengt am bestehenden.', $result['id'] ); } elseif (isset($result['known_fixed_in'])) { $message = sprintf( 'Bekannt und behoben in Build "%s" - du hast "%s" geprueft. Kein neues Item angelegt. ' . 'Aktualisiere und pruefe erneut; tritt es dann immer noch auf, ist es eine Regression ' . 'und gehoert gemeldet.', $result['known_fixed_in'], $result['reported_build'] ); } elseif ($isForeign) { $message = $result['is_new'] ? sprintf( 'Im Eingang von "%s" hinterlegt. Wird dort gesichtet und zaehlt erst nach ' . 'Bestaetigung in die Statistik.', $target ) : sprintf('Bereits bekannt - "%s" ist als betroffen vermerkt.', $homeProject); } else { $message = $result['idempotent_hit'] ? 'Bereits erfasst (identische client_ref) - kein Duplikat angelegt.' : ($result['is_new'] ? ($result['type'] === 'bug' ? 'Bug erfasst.' : 'Feature Request erfasst.') : 'Wiederkehrendes Vorkommnis - Zaehler erhoeht.'); } Http::ok([ 'item_id' => $result['id'], 'is_new' => $result['is_new'], 'idempotent_hit' => $result['idempotent_hit'], 'occurrence_count' => $result['occurrence_count'], 'dedup_key' => $result['dedup_key'], 'error_hash' => $result['error_hash'], 'type' => $result['type'], 'item_status' => $result['status'], 'environment' => $result['environment'], 'push_id' => $result['push_id'] ?? null, 'regression_of' => $result['regression_of'] ?? null, // Gesetzt, wenn der Fehler bereits in einer spaeteren Fassung behoben ist // als der, die gemeldet wurde. Dann wurde NICHTS angelegt und nichts // hochgezaehlt - die Antwort ist eine Auskunft. 'known_fixed_in' => $result['known_fixed_in'] ?? null, 'reported_build' => $result['reported_build'] ?? null, // Gesetzt, wenn der gemeldete Fehler ein Item betrifft, dessen Fix noch // nicht bestaetigt war. Dann wurde kein neues Item angelegt - das // bestehende wechselte auf fix_insufficient. 'fix_insufficient' => !empty($result['fix_insufficient']), 'reported_from' => $result['reported_from'] ?? null, 'affects' => $result['affects'] ?? [], 'url' => Http::baseUrl() . '/index.php#tab-bugtracker', 'message' => $message, ], $result['is_new'] ? 201 : 200);