PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, ] ); } catch (PDOException $e) { http_response_code(500); header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'success' => false, 'data' => null, 'message' => 'Koneksi database gagal: ' . $e->getMessage() ]); exit; } // ── CORS Headers ───────────────────────────────────────────── header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); // Handle preflight request if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } // ============================================================ // FUNGSI UTILITAS // ============================================================ /** * Kirim respon JSON standar lalu hentikan eksekusi. * Format: {"success": true/false, "data": ..., "message": "..."} */ function jsonResponse($data = null, string $message = '', bool $success = true, int $httpCode = 200, array $extra = []): void { http_response_code($httpCode); header('Content-Type: application/json; charset=utf-8'); $response = [ 'success' => $success, 'data' => $data, 'message' => $message ]; // Gabungkan field tambahan (misal: duplikasi_warning) if (!empty($extra)) { $response = array_merge($response, $extra); } echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } /** * Bersihkan input string dari tag HTML. */ function clean(string $value): string { return htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8'); } /** * Hitung jarak Haversine antara dua koordinat dalam kilometer. */ function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float { $R = 6371; // Radius bumi dalam km $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) * sin($dLng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return round($R * $c, 4); } /** * Auto-assign: cari rumah ibadah terdekat yang radius-nya mencakup lokasi KK. * Mengembalikan ['rumah_ibadah_id' => int|null, 'jarak_km' => float|null] */ function autoAssign(PDO $pdo, float $lat, float $lng): array { $result = ['rumah_ibadah_id' => null, 'jarak_km' => null]; $stmt = $pdo->query("SELECT id, lat, lng, radius_km FROM rumah_ibadah"); $rows = $stmt->fetchAll(); $bestDist = PHP_FLOAT_MAX; $bestId = null; foreach ($rows as $row) { $dist = haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']); // Hanya assign jika KK berada dalam radius RI dan merupakan yang terdekat if ($dist <= (float)$row['radius_km'] && $dist < $bestDist) { $bestDist = $dist; $bestId = (int)$row['id']; } } if ($bestId !== null) { $result['rumah_ibadah_id'] = $bestId; $result['jarak_km'] = $bestDist; } return $result; } /** * Cek apakah user sudah login. Jika belum, kirim 401. * Mengembalikan data session user. */ function requireLogin(): array { if (empty($_SESSION['user'])) { jsonResponse(null, 'Tidak diizinkan. Silakan login terlebih dahulu.', false, 401); } return $_SESSION['user']; } /** * Cek apakah role user termasuk dalam daftar yang diizinkan. * Jika tidak, kirim 403. */ function requireRole(array $allowedRoles): array { $user = requireLogin(); if (!in_array($user['role'], $allowedRoles)) { jsonResponse(null, 'Akses ditolak. Role Anda tidak memiliki izin.', false, 403); } return $user; } /** * Blokir aksi tulis (POST/PUT/DELETE) untuk role walikota. * Walikota hanya boleh GET. */ function blockWalikotaWrite(): void { if (!empty($_SESSION['user']) && $_SESSION['user']['role'] === 'walikota') { $method = $_SERVER['REQUEST_METHOD']; if (in_array($method, ['POST', 'PUT', 'DELETE'])) { jsonResponse(null, 'Walikota hanya memiliki akses baca (read-only).', false, 403); } } } /** * Generate kode pengurus dengan format: RI-[JENIS 3 HURUF]-[RANDOM 4 DIGIT] * Contoh: RI-MSJ-4821, RI-GRJ-7153 */ function generateKodePengurus(PDO $pdo, string $jenis): string { // Mapping jenis ke 3 huruf $map = [ 'Masjid' => 'MSJ', 'Gereja' => 'GRJ', 'Pura' => 'PRA', 'Vihara' => 'VHR', 'Klenteng' => 'KLT', 'Lainnya' => 'LNY', ]; $prefix = $map[$jenis] ?? 'LNY'; // Coba generate kode unik (maks 100 percobaan) for ($i = 0; $i < 100; $i++) { $angka = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT); $kode = "RI-{$prefix}-{$angka}"; $stmt = $pdo->prepare("SELECT id FROM rumah_ibadah WHERE kode_pengurus = ? LIMIT 1"); $stmt->execute([$kode]); if ($stmt->rowCount() === 0) { return $kode; } } // Fallback: tambahkan timestamp return "RI-{$prefix}-" . substr(time(), -4); } /** * Cek rate limit untuk IP tertentu. Maks 5 submit per jam. * Mengembalikan true jika masih diizinkan, false jika melebihi batas. */ function checkRateLimit(PDO $pdo, string $ip): bool { // Hapus record yang lebih dari 1 jam $pdo->prepare("DELETE FROM rate_limit WHERE created_at < NOW() - INTERVAL 1 HOUR")->execute(); // Hitung jumlah submit dalam 1 jam terakhir $stmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM rate_limit WHERE ip_address = ? AND created_at >= NOW() - INTERVAL 1 HOUR"); $stmt->execute([$ip]); $count = (int)$stmt->fetch()['cnt']; if ($count >= 5) { return false; // Sudah melebihi batas } // Catat submit baru $stmt = $pdo->prepare("INSERT INTO rate_limit (ip_address) VALUES (?)"); $stmt->execute([$ip]); return true; } /** * Handle upload file foto. Mengembalikan path relatif file atau null. */ function handleUpload(string $fieldName): ?string { if (!isset($_FILES[$fieldName]) || $_FILES[$fieldName]['error'] !== UPLOAD_ERR_OK) { return null; } $file = $_FILES[$fieldName]; $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $maxSize = 5 * 1024 * 1024; // 5MB if (!in_array($file['type'], $allowedTypes)) { jsonResponse(null, 'Tipe file tidak diizinkan. Gunakan JPG, PNG, GIF, atau WebP.', false, 400); } if ($file['size'] > $maxSize) { jsonResponse(null, 'Ukuran file melebihi batas 5MB.', false, 400); } // Buat direktori upload jika belum ada if (!is_dir(UPLOAD_DIR)) { mkdir(UPLOAD_DIR, 0755, true); } // Generate nama file unik $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = 'laporan_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext; $dest = UPLOAD_DIR . $filename; if (!move_uploaded_file($file['tmp_name'], $dest)) { jsonResponse(null, 'Gagal menyimpan file foto.', false, 500); } return 'uploads/' . $filename; } /** * Dapatkan IP address client (mendukung proxy). */ function getClientIP(): string { if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; }