From 7c03cc01de94a4b64e3c4de39f6d44e401b7db60 Mon Sep 17 00:00:00 2001 From: Agando Date: Sun, 18 Oct 2026 17:01:52 +0200 Subject: newOS4 v4.6.37: bug-report pipeline (report.php, bug command, HTML generator, telemetry-loop fix) --- api/debug/report.php | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 api/debug/report.php (limited to 'api/debug/report.php') diff --git a/api/debug/report.php b/api/debug/report.php new file mode 100644 index 0000000..a4cf9f1 --- /dev/null +++ b/api/debug/report.php @@ -0,0 +1,99 @@ + { success, id, duplicate, url } +require_once __DIR__ . '/config.php'; + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + error_response('Method not allowed', 405); +} + +define('REPORTS_DIR', __DIR__ . '/../../data/reports'); +define('RATE_FILE', DEBUG_DATA_DIR . '/report_rate.json'); +define('MAX_REPORT_KB', 512); +define('RATE_MAX', 5); // reports per IP … +define('RATE_WINDOW', 600); // … per 10 minutes + +$raw = file_get_contents('php://input'); +if (!$raw) error_response('Empty request body'); +if (strlen($raw) > MAX_REPORT_KB * 1024) { + error_response('Payload too large (max ' . MAX_REPORT_KB . 'KB)', 413); +} + +$payload = json_decode($raw, true); +if (!is_array($payload)) error_response('Invalid JSON'); + +$title = trim((string)($payload['title'] ?? '')); +if ($title === '') error_response('title is required'); +if (mb_strlen($title) > 160) $title = mb_substr($title, 0, 160); +$body = (string)($payload['body'] ?? ''); +if (mb_strlen($body) > 4000) $body = mb_substr($body, 0, 4000); +$diag = (isset($payload['diagnostics']) && is_array($payload['diagnostics'])) ? $payload['diagnostics'] : []; + +// Storage guard: data/ is not writable by php-fpm, so never pretend we wrote a report. +if (!is_dir(REPORTS_DIR)) @mkdir(REPORTS_DIR, 0775, true); +if (!is_dir(REPORTS_DIR) || !is_writable(REPORTS_DIR)) { + error_response('Report storage unavailable', 503); +} + +// Per-IP rate limit +$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; +$now = time(); +$rate = load_json(RATE_FILE); +$hits = array_values(array_filter($rate[$ip] ?? [], function ($t) use ($now) { + return $t > $now - RATE_WINDOW; +})); +if (count($hits) >= RATE_MAX) { + header('Retry-After: ' . RATE_WINDOW); + error_response('Rate limit exceeded (max ' . RATE_MAX . ' reports per 10 minutes)', 429); +} +$hits[] = $now; +$rate[$ip] = $hits; +foreach ($rate as $k => $v) { + $v = array_values(array_filter($v, function ($t) use ($now) { return $t > $now - 86400; })); + if ($v) $rate[$k] = $v; else unset($rate[$k]); +} +save_json(RATE_FILE, $rate); + +// Dedupe: same title + error set + version within 24h collapses onto the existing report. +$fingerprint = sha1($title . '|' . json_encode($diag['errors'] ?? []) . '|' . (string)($diag['version'] ?? '')); +$existing = glob(REPORTS_DIR . '/RPT-*.json') ?: []; +foreach (array_slice(array_reverse($existing), 0, 100) as $f) { + $d = json_decode(@file_get_contents($f), true); + if (!is_array($d)) continue; + if (($d['fingerprint'] ?? '') === $fingerprint && strtotime($d['created_at'] ?? '') > $now - 86400) { + json_response([ + 'success' => true, + 'id' => $d['id'], + 'duplicate' => true, + 'url' => '/spaces/newos4/downloads/bugs/BUG-REPORT.html', + ]); + } +} + +$id = 'RPT-' . gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(4)), 0, 8); +$record = [ + 'id' => $id, + 'created_at' => gmdate('c'), + 'fingerprint' => $fingerprint, + 'title' => $title, + 'body' => $body, + 'diagnostics' => $diag, + 'meta' => [ + 'ip' => $ip, + 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', + ], +]; + +$file = REPORTS_DIR . '/' . $id . '.json'; +$json = json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +if ($json === false || file_put_contents($file, $json) === false) { + error_response('Failed to persist report', 500); +} +@chmod($file, 0664); + +json_response([ + 'success' => true, + 'id' => $id, + 'duplicate' => false, + 'url' => '/spaces/newos4/downloads/bugs/BUG-REPORT.html', +], 201); -- cgit v1.2.3