// 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;
}