{ 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);