// Suppress raw HTML PHP errors from contaminating API JSON responses error_reporting(0); ini_set('display_errors', '0'); ob_start(); set_exception_handler(function ($e) { if (ob_get_length()) ob_clean(); http_response_code(500); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['error' => 'Server exception: ' . $e->getMessage()]); exit; }); require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/jwt.php'; // Dynamic CORS Header $allowedOrigins = [ 'https://land.hinduvishwa.com', 'http://localhost', 'http://localhost:8080', 'http://localhost:58887', ]; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, $allowedOrigins) || preg_match('/^http:\/\/localhost(:\d+)?$/', $origin)) { header("Access-Control-Allow-Origin: $origin"); } else { header('Access-Control-Allow-Origin: https://land.hinduvishwa.com'); } header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); // Handle preflight if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } // ─── DDoS Protection & Rate Limiting ───────────────────────────── function applyRateLimit(int $maxRequests = 120, int $windowSeconds = 60, string $bucketPrefix = 'api'): void { $ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; $ipKey = md5($ip . '_' . $bucketPrefix); $tmpFile = sys_get_temp_dir() . '/rate_limit_' . $ipKey . '.json'; $now = time(); $data = ['requests' => [], 'first_request' => $now]; if (file_exists($tmpFile)) { $raw = @file_get_contents($tmpFile); if ($raw) { $parsed = json_decode($raw, true); if (is_array($parsed) && isset($parsed['requests'])) { $data = $parsed; } } } // Filter requests within time window $data['requests'] = array_filter($data['requests'], function($ts) use ($now, $windowSeconds) { return ($now - $ts) < $windowSeconds; }); if (count($data['requests']) >= $maxRequests) { jsonResponse([ 'error' => 'Rate limit exceeded. Too many requests. Please try again shortly.', 'retryAfter' => $windowSeconds ], 429); } $data['requests'][] = $now; @file_put_contents($tmpFile, json_encode($data), LOCK_EX); } // Apply default API rate limit (120 req / minute) applyRateLimit(120, 60, 'global'); function getRequestBody(): array { $raw = file_get_contents('php://input'); if ($raw) { $raw = preg_replace('/^\xEF\xBB\xBF/', '', trim($raw)); $data = json_decode($raw, true); if (is_array($data) && !empty($data)) { return $data; } } return $_POST; } function jsonResponse(mixed $data, int $status = 200): void { if (ob_get_length()) ob_clean(); http_response_code($status); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; } function addAuditLog(string $action, string $details, ?array $user = null): void { try { $db = getDB(); $logId = 'LOG-' . round(microtime(true) * 1000); $stmt = $db->prepare( "INSERT INTO audit_logs (id, action, performed_by, uid, role, details, device_fingerprint) VALUES (?, ?, ?, ?, ?, ?, ?)" ); $stmt->execute([ $logId, $action, $user['name'] ?? 'System', $user['uid'] ?? '', $user['role'] ?? 'System', $details, $_SERVER['REMOTE_ADDR'] ?? '', ]); } catch (Exception $e) { error_log("Audit log error: " . $e->getMessage()); } } /** * Generate a UUID v4 string */ function generateUUID(): string { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } /** * Build a scoped SELECT query filtered by role and jilla * Includes strict SQL table & order column whitelisting to eliminate SQL injection risks */ function buildScopedQuery(PDO $db, string $table, array $user, string $orderColumn = 'created_at', int $limit = 500): \PDOStatement { // Whitelist tables to prevent SQL injection $allowedTables = ['land_records', 'tax_records', 'encroachment_cases', 'property_owners', 'audit_logs']; if (!in_array($table, $allowedTables)) { jsonResponse(['error' => 'Invalid resource table.'], 400); } // Whitelist order columns $allowedColumns = ['created_at', 'date_reported', 'timestamp', 'owner_name', 'id']; if (!in_array($orderColumn, $allowedColumns)) { $orderColumn = 'created_at'; } $roleLevel = getRoleLevel($user['role']); $filterJilla = $_GET['jilla'] ?? ''; $scopeJilla = $user['scope_jilla'] ?? ''; $limit = min(max(1, (int)$limit), 500); if ($roleLevel <= 1 && empty($filterJilla)) { // Global readers — see everything $stmt = $db->prepare("SELECT * FROM `$table` ORDER BY `$orderColumn` DESC LIMIT :limit"); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); } else { $effectiveJilla = !empty($filterJilla) ? $filterJilla : $scopeJilla; if (!empty($effectiveJilla)) { $stmt = $db->prepare("SELECT * FROM `$table` WHERE jilla = :jilla ORDER BY `$orderColumn` DESC LIMIT :limit"); $stmt->bindValue(':jilla', $effectiveJilla, PDO::PARAM_STR); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); } else { $stmt = $db->prepare("SELECT * FROM `$table` ORDER BY `$orderColumn` DESC LIMIT :limit"); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); } } return $stmt; }