281 lines
8.6 KiB
PHP
281 lines
8.6 KiB
PHP
<?php
|
|
// ============================================================
|
|
// Konfigurasi Koneksi Database & Fungsi Utilitas
|
|
// WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah
|
|
// ============================================================
|
|
|
|
// Konfigurasi database
|
|
define('DB_HOST', 'vg4so4ccs0444o4cgkssgos8');
|
|
define('DB_PORT', 3306);
|
|
define('DB_NAME', 'default');
|
|
define('DB_USER', 'mysql');
|
|
define('DB_PASS', 'tg9lvPlr4hgUhgtT7aPq9XelkCEvvMGtsoV8eB4dD5FAkz891PwkJT340xmaEWxL');
|
|
define('DB_CHARSET', 'utf8mb4');
|
|
|
|
// Direktori upload foto laporan
|
|
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
|
|
|
// Mulai session
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
// ── Koneksi PDO ──────────────────────────────────────────────
|
|
try {
|
|
$pdo = new PDO(
|
|
"mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET,
|
|
DB_USER,
|
|
DB_PASS,
|
|
[
|
|
PDO::ATTR_ERRMODE => 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';
|
|
}
|