JezK
Edit File: api.php
<?php /** * NyayKutumb (न्याय कुटुंब) - High Performance Civic Telemetry & Anonymous Community API * Scalable backend for crowdsourced bribe reporting, multi-media threads & civic discussions */ // Explicitly set Indian Standard Time (IST - UTC+5:30) for all timestamps date_default_timezone_set('Asia/Kolkata'); header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); } require_once __DIR__ . '/admin/config.php'; $dataDir = __DIR__ . '/data'; if (!is_dir($dataDir)) { @mkdir($dataDir, 0777, true); } $dataFile = $dataDir . '/records.json'; $customDataFile = $dataDir . '/user_submissions.json'; $commentsDataFile = $dataDir . '/thread_comments.json'; // Helper to load all records (MySQL Primary with JSON Fallback) function loadAllRecords($dataFile, $customDataFile, $commentsDataFile = null) { $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->query("SELECT * FROM `records` ORDER BY `created_at` DESC"); $records = $stmt->fetchAll(PDO::FETCH_ASSOC); $stmtCmt = $pdo->query("SELECT * FROM `thread_comments` ORDER BY `created_at` ASC"); $allCmts = $stmtCmt->fetchAll(PDO::FETCH_ASSOC); $commentsByRec = []; foreach ($allCmts as $c) { $rid = $c['record_id'] ?? ''; if ($rid) { if (!isset($commentsByRec[$rid])) $commentsByRec[$rid] = []; $commentsByRec[$rid][] = $c; } } foreach ($records as &$r) { if (isset($r['images']) && is_string($r['images'])) $r['images'] = json_decode($r['images'], true); if (isset($r['videos']) && is_string($r['videos'])) $r['videos'] = json_decode($r['videos'], true); if (isset($r['risk_flags']) && is_string($r['risk_flags'])) $r['risk_flags'] = json_decode($r['risk_flags'], true); $r['has_proof'] = !empty($r['has_proof']); $r['amount'] = floatval($r['amount']); $r['days_delayed'] = intval($r['days_delayed']); $r['upvotes'] = intval($r['upvotes']); $r['views'] = intval($r['views']); $rid = $r['id'] ?? ''; $r['comments'] = $commentsByRec[$rid] ?? []; } return $records; } catch (Exception $e) { error_log("loadAllRecords MySQL Exception: " . $e->getMessage()); } } // JSON Fallback $records = []; if (file_exists($dataFile)) { $json = @file_get_contents($dataFile); $records = json_decode($json, true) ?: []; } if (file_exists($customDataFile)) { $customJson = @file_get_contents($customDataFile); $customRecords = json_decode($customJson, true) ?: []; $records = array_merge($customRecords, $records); } if (!$commentsDataFile) { $commentsDataFile = __DIR__ . '/data/thread_comments.json'; } if (file_exists($commentsDataFile)) { $commentsLog = json_decode(file_get_contents($commentsDataFile), true) ?: []; $commentsByRec = []; foreach ($commentsLog as $c) { $recId = $c['record_id'] ?? ''; if ($recId) { if (!isset($commentsByRec[$recId])) $commentsByRec[$recId] = []; $commentsByRec[$recId][] = $c; } } $validCommentIds = array_column($commentsLog, 'id'); foreach ($records as &$r) { $rid = $r['id'] ?? ''; if (isset($commentsByRec[$rid])) { $r['comments'] = $commentsByRec[$rid]; } else { if (isset($r['comments']) && is_array($r['comments'])) { $r['comments'] = array_values(array_filter($r['comments'], function($c) use ($validCommentIds) { return in_array($c['id'] ?? '', $validCommentIds); })); } } } } return $records; } // Helper to save user submission (MySQL Primary + Dual JSON Persistence) function saveUserSubmission($customDataFile, $record) { $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare(" INSERT INTO `records` ( `id`, `state`, `city`, `department`, `icon`, `service`, `designation`, `amount`, `status`, `outcome_text`, `mode`, `days_delayed`, `story`, `created_at`, `upvotes`, `views`, `has_proof`, `images`, `videos`, `token_hash`, `post_type`, `risk_score`, `risk_tier`, `risk_flags`, `auto_action`, `moderation_status` ) VALUES ( :id, :state, :city, :department, :icon, :service, :designation, :amount, :status, :outcome_text, :mode, :days_delayed, :story, :created_at, :upvotes, :views, :has_proof, :images, :videos, :token_hash, :post_type, :risk_score, :risk_tier, :risk_flags, :auto_action, :moderation_status ) ON DUPLICATE KEY UPDATE `state` = VALUES(`state`), `city` = VALUES(`city`), `department` = VALUES(`department`), `amount` = VALUES(`amount`), `status` = VALUES(`status`), `story` = VALUES(`story`), `moderation_status` = VALUES(`moderation_status`) "); $stmt->execute([ ':id' => $record['id'], ':state' => $record['state'] ?? '', ':city' => $record['city'] ?? '', ':department' => $record['department'] ?? '', ':icon' => $record['icon'] ?? 'building', ':service' => $record['service'] ?? '', ':designation' => $record['designation'] ?? '', ':amount' => floatval($record['amount'] ?? 0), ':status' => $record['status'] ?? 'demanded_and_paid', ':outcome_text' => $record['outcome_text'] ?? '', ':mode' => $record['mode'] ?? 'Direct Cash Demand', ':days_delayed' => intval($record['days_delayed'] ?? 0), ':story' => $record['story'] ?? '', ':created_at' => $record['created_at'] ?? date('Y-m-d H:i:s'), ':upvotes' => intval($record['upvotes'] ?? 0), ':views' => intval($record['views'] ?? 0), ':has_proof' => !empty($record['has_proof']) ? 1 : 0, ':images' => isset($record['images']) ? json_encode($record['images']) : NULL, ':videos' => isset($record['videos']) ? json_encode($record['videos']) : NULL, ':token_hash' => $record['token_hash'] ?? '', ':post_type' => $record['post_type'] ?? 'incident', ':risk_score' => intval($record['risk_score'] ?? 0), ':risk_tier' => $record['risk_tier'] ?? 'LOW', ':risk_flags' => isset($record['risk_flags']) ? json_encode($record['risk_flags']) : NULL, ':auto_action' => $record['auto_action'] ?? 'ALLOW_PUBLISH', ':moderation_status' => $record['moderation_status'] ?? 'APPROVED_PUBLISHED' ]); } catch (Exception $e) { error_log("saveUserSubmission MySQL Exception: " . $e->getMessage()); } } $customRecords = []; if (file_exists($customDataFile)) { $json = @file_get_contents($customDataFile); $customRecords = json_decode($json, true) ?: []; } array_unshift($customRecords, $record); file_put_contents($customDataFile, json_encode($customRecords, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); return true; } // Helper to update a record function updateRecord($customDataFile, $recordId, $updaterFunc) { if (!file_exists($customDataFile)) return false; $customRecords = json_decode(file_get_contents($customDataFile), true) ?: []; $updated = false; foreach ($customRecords as &$rec) { if (($rec['id'] ?? '') === $recordId) { $rec = $updaterFunc($rec); $updated = true; break; } } if ($updated) { file_put_contents($customDataFile, json_encode($customRecords, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); } return $updated; } // PII Redactor for Indian compliance and defamation protection (BNS 2023 / IPC 499) function sanitizeAndRedactPII($text) { if (!$text) return ''; // Redact 10-digit mobile numbers $text = preg_replace('/(\+?91[-.\s]?)?[6-9]\d{9}/', '[REDACTED_PHONE]', $text); // Redact email addresses $text = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', '[REDACTED_EMAIL]', $text); // Redact Aadhaar-like 12 digit sequences $text = preg_replace('/\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b/', '[REDACTED_AADHAAR]', $text); // Redact PAN $text = preg_replace('/\b[A-Z]{5}[0-9]{4}[A-Z]{1}\b/i', '[REDACTED_PAN]', $text); // Redact GSTIN $text = preg_replace('/\b[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}\b/i', '[REDACTED_GSTIN]', $text); // Redact Indian Vehicle Number Plates (e.g. DL 01 AB 1234, MH12DE1234) $text = preg_replace('/\b[A-Z]{2}[0-9]{1,2}[A-Z]{1,3}[0-9]{4}\b/i', '[REDACTED_VEHICLE_NO]', $text); // Redact UPI handles $text = preg_replace('/\b[a-zA-Z0-9.\-_]{2,40}@(okaxis|oksbi|okhdfcbank|okicici|paytm|ybl|apl|upi)\b/i', '[REDACTED_UPI_ID]', $text); // Redact explicit honorific officer names to protect whistleblowers & platform under BNS 356 / IPC 499 $text = preg_replace('/\b(Shri|Shrimati|Smt|Mr|Mrs|Ms|Officer|Inspector|Constable|Tehsildar|Patwari|Engineer|Doctor|Advocate|Collector|Commissioner|Lineman|Clerk|Peshkar|RTO|JE|AE|EE|SI|ASI|DSP|ACP)\.?\s+([A-Z][a-z]{1,20}(?:\s+[A-Z][a-z]{1,20}){1,2})/u', '$1 [REDACTED_OFFICIAL_NAME]', $text); // Redact 2 or 3 capitalized consecutive proper names (e.g. "Sham Bahudar", "Ramesh Kumar") excluding safe system terms $exclusionPattern = '/\b(Driving License|Commercial Permit|Vehicle Registration|Regional Transport|Transport Office|Traffic Police|Municipal Corporation|Urban Local|Electricity Board|Water Supply|Sewerage Board|Civil Supplies|Food Safety|Subordinate Court|Court Registry|Higher Education|University Registrar|Excise Customs|Tax Department|Public Works|Revenue Land|Land Administration|State Government|District Collectorate|Panchayat Samiti|Zilla Parishad|Vigilance Bureau|Anti Corruption|Corruption Bureau|Central Public|Indian Police|Resident Welfare|NyayKutumb|Paid Under|Refused Stood|File Illegally|Direct Cash|Agent Intermediary|Verified Proof|Legal Guidance|Action Plan|Community Support|Andhra Pradesh|Arunachal Pradesh|Himachal Pradesh|Madhya Pradesh|Uttar Pradesh|West Bengal|Tamil Nadu|Jammu Kashmir|New Delhi|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday|January|February|March|April|May|June|July|August|September|October|November|December)\b/i'; $text = preg_replace_callback('/\b([A-Z][a-z]{1,20}\s+[A-Z][a-z]{1,20}(?:\s+[A-Z][a-z]{1,20})?)\b/u', function($matches) use ($exclusionPattern) { $phrase = $matches[1]; if (preg_match($exclusionPattern, $phrase)) { return $phrase; } return '[REDACTED_OFFICIAL_NAME]'; }, $text); // Strip malicious tags $text = htmlspecialchars(strip_tags($text), ENT_QUOTES, 'UTF-8'); return $text; } // Autonomous Content Risk Scoring Engine function calculateServerRiskScore($record) { $score = 0; $flags = []; $story = $record['story'] ?? ''; $amount = floatval($record['amount'] ?? 0); // 1. Monetary Anomaly (> ₹10L or > ₹2L) if ($amount > 1000000) { $score += 35; $flags[] = "Monetary Anomaly (> ₹10 Lakhs)"; } elseif ($amount > 200000) { $score += 15; $flags[] = "High Extortion Demand (> ₹2 Lakhs)"; } // 2. Unredacted PII Detection if (preg_match('/(\+?91[\-\s]?)?[6-9]\d{9}/', $story)) { $score += 25; $flags[] = "Unredacted Phone Number"; } if (preg_match('/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/', $story)) { $score += 20; $flags[] = "Unredacted Email"; } if (preg_match('/\b[A-Z]{5}[0-9]{4}[A-Z]{1}\b/i', $story)) { $score += 25; $flags[] = "Income Tax PAN Sequence"; } if (preg_match('/\b[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}\b/i', $story)) { $score += 25; $flags[] = "GSTIN Sequence"; } // 3. Defamation & Accusatory Aggression if (preg_match('/\b(thief|criminal|corrupt|fraud|scammer|looter|bribe-taker)\b/i', $story)) { $score += 20; $flags[] = "Accusatory Defamatory Language"; } // 4. Excessive Capitalization $len = strlen($story); if ($len > 30) { preg_match_all('/[A-Z]/', $story, $caps); $capRatio = count($caps[0]) / $len; if ($capRatio > 0.45) { $score += 15; $flags[] = "Aggressive Capitalization (>45%)"; } } $tier = 'LOW'; $autoAction = 'ALLOW_PUBLISH'; if ($score >= 45) { $tier = 'HIGH'; $autoAction = 'QUARANTINE_PENDING_REVIEW'; } elseif ($score >= 20) { $tier = 'MEDIUM'; $autoAction = 'FLAG_FOR_AUDIT'; } return [ 'risk_score' => $score, 'risk_tier' => $tier, 'auto_action' => $autoAction, 'risk_flags' => $flags ]; } // Autonomous AI Comment Risk Scoring & Cluster Purge Engine function calculateCommentRiskScore($text) { $score = 0; $flags = []; // Exclude safe legal advice phrases (e.g. "contact with lawyer", "file with vigilance") $cleanText = preg_replace('/\b(contact|meet|consult|file|complain|speak|write)\s+(with\s+)?(a\s+)?(lawyer|advocate|attorney|legal\s+counsel|police|vigilance|cvo|ombudsman|tribunal|commissioner|authority|helpdesk|portal)\b/i', '', $text); // 1. Named Individual / Official Accusation Patterns (e.g. "Suraj sir is involved", "clerk Rajesh") if (preg_match('/\b([A-Z][a-z]+)\s+(sir|ji|officer|clerk|agent|dalal|peshkar|inspector|tehsildar|patwari)\b/i', $cleanText) || preg_match('/\b(officer|clerk|agent|dalal|peshkar|inspector|tehsildar|patwari|sir|ji)\s+([A-Z][a-z]+)\b/i', $cleanText) || preg_match('/\b([A-Z][a-z]+)\s+(is|was)\s+involved\b/i', $cleanText) || preg_match('/\b(meet|contact|pay|give)\s+([A-Z][a-z]{2,})\b/i', $cleanText)) { $score += 45; $flags[] = 'NAMED_ACCUSATION_CLUSTER'; } // 2. Defamatory / Allegational Accusation Language if (preg_match('/\b(chor|corrupt|briber|fraud|thief|scammer|criminal|looter|taking bribe|took bribe|demanded bribe)\b/i', $text)) { $score += 30; $flags[] = 'DEFAMATORY_LANGUAGE_CLUSTER'; } // 3. Unredacted Phone / Email / Account Numbers if (preg_match('/\b\d{10}\b|\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/', $text)) { $score += 40; $flags[] = 'PII_LEAK_CLUSTER'; } return [ 'score' => $score, 'flags' => $flags, 'is_defamation_cluster' => ($score >= 40) ]; } // Batch Purge Execution Worker function executeBatchCommentPurge($customDataFile, $commentsDataFile, $dataFile) { $startTime = microtime(true); $purgedCount = 0; $retainedCount = 0; $clustersRemoved = []; // Purge from MySQL thread_comments table $pdo = getDbConnection(); if ($pdo) { try { $stmtC = $pdo->query("SELECT `id`, `text` FROM `thread_comments`"); $dbComments = $stmtC->fetchAll(PDO::FETCH_ASSOC); $delStmt = $pdo->prepare("DELETE FROM `thread_comments` WHERE `id` = ?"); foreach ($dbComments as $dc) { $eval = calculateCommentRiskScore($dc['text'] ?? ''); if ($eval['is_defamation_cluster']) { $delStmt->execute([$dc['id']]); } } } catch (Exception $e) { error_log("executeBatchCommentPurge MySQL Exception: " . $e->getMessage()); } } $commentsLog = file_exists($commentsDataFile) ? (json_decode(file_get_contents($commentsDataFile), true) ?: []) : []; $purgedLog = []; foreach ($commentsLog as $c) { $eval = calculateCommentRiskScore($c['text'] ?? ''); if ($eval['is_defamation_cluster']) { $purgedCount++; foreach ($eval['flags'] as $flg) { $clustersRemoved[$flg] = ($clustersRemoved[$flg] ?? 0) + 1; } } else { $purgedLog[] = $c; $retainedCount++; } } // Write updated commentsLog atomically file_put_contents($commentsDataFile, json_encode($purgedLog, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); // Also purge flagged comments inside user_submissions.json if (file_exists($customDataFile)) { $userSubs = json_decode(file_get_contents($customDataFile), true) ?: []; foreach ($userSubs as &$rec) { if (isset($rec['comments']) && is_array($rec['comments'])) { $rec['comments'] = array_values(array_filter($rec['comments'], function($c) { $eval = calculateCommentRiskScore($c['text'] ?? ''); return !$eval['is_defamation_cluster']; })); } } file_put_contents($customDataFile, json_encode($userSubs, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); } $executionTimeMs = round((microtime(true) - $startTime) * 1000, 2); return [ 'success' => true, 'purged_count' => $purgedCount, 'retained_count' => $retainedCount, 'clusters_removed' => $clustersRemoved, 'execution_time_ms' => $executionTimeMs, 'timestamp' => date('Y-m-d H:i:s') ]; } $action = $_GET['action'] ?? 'get_records'; switch ($action) { case 'get_records': $allRecords = loadAllRecords($dataFile, $customDataFile); // Exclude quarantined records from public feed $records = array_values(array_filter($allRecords, function($r) { return ($r['moderation_status'] ?? '') !== 'QUARANTINED_PENDING_REVIEW'; })); // Optional filtering parameters $state = $_GET['state'] ?? ''; $city = $_GET['city'] ?? ''; $department = $_GET['department'] ?? ''; $status = $_GET['status'] ?? ''; $postType = $_GET['post_type'] ?? ''; $hasMedia = isset($_GET['has_media']) && $_GET['has_media'] === 'true'; $search = strtolower($_GET['search'] ?? ''); $minAmount = isset($_GET['min_amount']) ? floatval($_GET['min_amount']) : 0; $maxAmount = isset($_GET['max_amount']) ? floatval($_GET['max_amount']) : 0; $sort = $_GET['sort'] ?? 'newest'; $page = max(1, intval($_GET['page'] ?? 1)); $limit = min(100, max(1, intval($_GET['limit'] ?? 24))); $filtered = array_filter($records, function ($r) use ($state, $city, $department, $status, $postType, $hasMedia, $search, $minAmount, $maxAmount) { if ($state && strcasecmp($r['state'], $state) !== 0) return false; if ($city && strcasecmp($r['city'], $city) !== 0) return false; if ($department && strcasecmp($r['department'], $department) !== 0) return false; if ($status && $r['status'] !== $status) return false; if ($postType && ($r['post_type'] ?? 'incident') !== $postType) return false; if ($hasMedia && empty($r['images']) && empty($r['videos'])) return false; if ($minAmount > 0 && ($r['amount'] ?? 0) < $minAmount) return false; if ($maxAmount > 0 && ($r['amount'] ?? 0) > $maxAmount) return false; if ($search) { $haystack = strtolower(($r['service'] ?? '') . ' ' . ($r['department'] ?? '') . ' ' . ($r['city'] ?? '') . ' ' . ($r['state'] ?? '') . ' ' . ($r['designation'] ?? '') . ' ' . ($r['story'] ?? '')); if (strpos($haystack, $search) === false) return false; } return true; }); // Sort usort($filtered, function ($a, $b) use ($sort) { if ($sort === 'amount_high') return ($b['amount'] ?? 0) <=> ($a['amount'] ?? 0); if ($sort === 'amount_low') return ($a['amount'] ?? 0) <=> ($b['amount'] ?? 0); if ($sort === 'upvotes') return ($b['upvotes'] ?? 0) <=> ($a['upvotes'] ?? 0); if ($sort === 'delay') return ($b['days_delayed'] ?? 0) <=> ($a['days_delayed'] ?? 0); if ($sort === 'replies') return count($b['comments'] ?? []) <=> count($a['comments'] ?? []); // Default newest return strtotime($b['created_at'] ?? 'now') <=> strtotime($a['created_at'] ?? 'now'); }); $totalCount = count($filtered); $totalPages = ceil($totalCount / $limit); $offset = ($page - 1) * $limit; $pagedRecords = array_slice($filtered, $offset, $limit); echo json_encode([ 'success' => true, 'total_records' => $totalCount, 'page' => $page, 'total_pages' => $totalPages, 'limit' => $limit, 'data' => array_values($pagedRecords) ], JSON_UNESCAPED_UNICODE); break; case 'get_stats': $records = loadAllRecords($dataFile, $customDataFile); $totalAmountDemanded = 0; $totalRefusedCount = 0; $totalPaidCount = 0; $totalStalledCount = 0; $deptStats = []; $stateStats = []; $cityStats = []; foreach ($records as $r) { $amt = $r['amount'] ?? 0; $totalAmountDemanded += $amt; if ($r['status'] === 'demanded_and_refused') { $totalRefusedCount++; } elseif ($r['status'] === 'demanded_and_paid') { $totalPaidCount++; } else { $totalStalledCount++; } // Department aggregation $dept = $r['department'] ?? 'Other'; if (!isset($deptStats[$dept])) { $deptStats[$dept] = ['count' => 0, 'total_amount' => 0, 'refused_count' => 0, 'icon' => $r['icon'] ?? 'building']; } $deptStats[$dept]['count']++; $deptStats[$dept]['total_amount'] += $amt; if (($r['status'] ?? '') === 'demanded_and_refused') { $deptStats[$dept]['refused_count']++; } // State aggregation $st = $r['state'] ?? 'Unknown'; if (!isset($stateStats[$st])) { $stateStats[$st] = ['count' => 0, 'total_amount' => 0]; } $stateStats[$st]['count']++; $stateStats[$st]['total_amount'] += $amt; // City aggregation $ct = $r['city'] ?? 'Unknown'; if (!isset($cityStats[$ct])) { $cityStats[$ct] = ['count' => 0, 'total_amount' => 0, 'state' => $st]; } $cityStats[$ct]['count']++; $cityStats[$ct]['total_amount'] += $amt; } uasort($deptStats, function ($a, $b) { return $b['count'] <=> $a['count']; }); uasort($stateStats, function ($a, $b) { return $b['count'] <=> $a['count']; }); uasort($cityStats, function ($a, $b) { return $b['count'] <=> $a['count']; }); $totalCount = count($records); $resistanceRate = $totalCount > 0 ? round(($totalRefusedCount / $totalCount) * 100, 1) : 0; echo json_encode([ 'success' => true, 'summary' => [ 'total_incidents' => $totalCount, 'total_amount_demanded' => $totalAmountDemanded, 'paid_count' => $totalPaidCount, 'refused_count' => $totalRefusedCount, 'stalled_count' => $totalStalledCount, 'resistance_rate_percent' => $resistanceRate, 'top_state' => array_key_first($stateStats), 'top_department' => array_key_first($deptStats) ], 'departments' => array_slice($deptStats, 0, 10, true), 'states' => array_slice($stateStats, 0, 10, true), 'cities' => array_slice($cityStats, 0, 10, true) ], JSON_UNESCAPED_UNICODE); break; case 'submit_record': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); if (!$input) { echo json_encode(['success' => false, 'error' => 'Invalid JSON input']); exit; } // Validation & extraction $state = trim($input['state'] ?? ''); $city = trim($input['city'] ?? ''); $department = trim($input['department'] ?? ''); $service = trim($input['service'] ?? ''); $amount = floatval($input['amount'] ?? 0); $status = in_array($input['status'] ?? '', ['demanded_and_paid', 'demanded_and_refused', 'work_stalled_no_bribe']) ? $input['status'] : 'demanded_and_paid'; $postType = in_array($input['post_type'] ?? '', ['incident', 'refusal_victory', 'delayed_file', 'community_request', 'whistleblower_alert']) ? $input['post_type'] : 'incident'; $designation = trim($input['designation'] ?? 'Public Official'); $mode = trim($input['mode'] ?? 'Direct Cash Demand'); $daysDelayed = intval($input['days_delayed'] ?? 0); $story = trim($input['story'] ?? ''); $hasProof = !empty($input['has_proof']); // Media attachments (multiple images and videos) $images = is_array($input['images'] ?? null) ? $input['images'] : []; $videos = is_array($input['videos'] ?? null) ? $input['videos'] : []; if (empty($state) || empty($city) || empty($department) || empty($service) || empty($story)) { echo json_encode(['success' => false, 'error' => 'Missing required fields (State, City, Department, Service, and Story are mandatory)']); exit; } // Redact PII to protect platform & whistleblower legally $cleanStory = sanitizeAndRedactPII($story); $cleanDesignation = sanitizeAndRedactPII($designation); $cleanService = sanitizeAndRedactPII($service); // Determine outcome label $outcomeText = 'Paid Under Duress'; if ($status === 'demanded_and_refused') { $outcomeText = 'Refused & Stood Firm'; } elseif ($status === 'work_stalled_no_bribe') { $outcomeText = 'Refused / File Illegally Stalled'; } // Map icons $icons = [ 'Regional Transport Office (RTO)' => 'car', 'Revenue & Land Administration' => 'map-pin', 'Traffic Police & Law Enforcement' => 'shield', 'Municipal Corporation & Urban Local Body' => 'building', 'Electricity Board (DISCOM)' => 'zap', 'Water Supply & Sewerage Board' => 'droplet', 'Judiciary & Subordinate Court Registry' => 'scale', 'Civil Supplies & Food Safety' => 'shopping-bag', 'Higher Education & University Registrar' => 'graduation-cap', 'Excise, Customs & Tax Department' => 'receipt' ]; $icon = $icons[$department] ?? 'file-text'; $anonId = 'anon_' . substr(md5(uniqid('', true)), 0, 8); $recordId = 'NS-' . (rand(50000, 99999)); $newRecord = [ 'id' => $recordId, 'state' => $state, 'city' => $city, 'department' => $department, 'icon' => $icon, 'service' => $cleanService, 'designation' => $cleanDesignation, 'amount' => $amount, 'status' => $status, 'outcome_text' => $outcomeText, 'post_type' => $postType, 'mode' => $mode, 'days_delayed' => $daysDelayed, 'story' => $cleanStory, 'images' => $images, 'videos' => $videos, 'has_media' => (!empty($images) || !empty($videos)), 'created_at' => date('Y-m-d H:i:s'), 'upvotes' => 1, 'views' => 1, 'has_proof' => $hasProof, 'token_hash' => $anonId, 'comments' => [] ]; // Evaluate Autonomous Content Risk Score $riskEval = calculateServerRiskScore($newRecord); $newRecord['risk_score'] = $riskEval['risk_score']; $newRecord['risk_tier'] = $riskEval['risk_tier']; $newRecord['auto_action'] = $riskEval['auto_action']; $newRecord['risk_flags'] = $riskEval['risk_flags']; $newRecord['moderation_status'] = ($riskEval['auto_action'] === 'QUARANTINE_PENDING_REVIEW') ? 'QUARANTINED_PENDING_REVIEW' : 'APPROVED_PUBLISHED'; saveUserSubmission($customDataFile, $newRecord); $msg = ($newRecord['moderation_status'] === 'QUARANTINED_PENDING_REVIEW') ? 'Report received safely. Auto-held in AI quarantine desk for compliance verification.' : 'Anonymous thread published safely with EXIF metadata stripped.'; echo json_encode([ 'success' => true, 'message' => $msg, 'record' => $newRecord ], JSON_UNESCAPED_UNICODE); break; case 'add_comment': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['record_id'] ?? ''); $commentText = trim($input['comment_text'] ?? ''); $adviceCategory = trim($input['category'] ?? 'Community Advice'); if (empty($recordId) || empty($commentText)) { echo json_encode(['success' => false, 'error' => 'Record ID and comment text are required']); exit; } $cleanComment = sanitizeAndRedactPII($commentText); $newComment = [ 'id' => 'c_' . substr(md5(uniqid('', true)), 0, 8), 'author' => 'Anonymous Citizen #' . rand(100, 999), 'category' => $adviceCategory, 'text' => $cleanComment, 'created_at' => date('Y-m-d H:i:s'), 'upvotes' => 1 ]; // Insert comment into MySQL DB $pdo = getDbConnection(); if ($pdo) { try { $stmtCmt = $pdo->prepare(" INSERT INTO `thread_comments` (`id`, `record_id`, `author`, `category`, `text`, `created_at`, `upvotes`) VALUES (?, ?, ?, ?, ?, ?, 1) ON DUPLICATE KEY UPDATE `text` = VALUES(`text`); "); $stmtCmt->execute([ $newComment['id'], $recordId, $newComment['author'], $newComment['category'], $newComment['text'], $newComment['created_at'] ]); } catch (Exception $e) { error_log("add_comment MySQL Exception: " . $e->getMessage()); } } // 1. Try updating in customDataFile $updated = updateRecord($customDataFile, $recordId, function($rec) use ($newComment) { if (!isset($rec['comments']) || !is_array($rec['comments'])) { $rec['comments'] = []; } $rec['comments'][] = $newComment; return $rec; }); // 2. If not found in customDataFile, find it from all records (e.g. seed data) and save to customDataFile! if (!$updated) { $allRecords = loadAllRecords($dataFile, $customDataFile); $foundRec = null; foreach ($allRecords as $r) { if (($r['id'] ?? '') === $recordId) { $foundRec = $r; break; } } if ($foundRec) { if (!isset($foundRec['comments']) || !is_array($foundRec['comments'])) { $foundRec['comments'] = []; } $foundRec['comments'][] = $newComment; saveUserSubmission($customDataFile, $foundRec); $updated = true; } } // 3. Also log to thread_comments.json for independent audit telemetry $allComments = file_exists($commentsDataFile) ? (json_decode(file_get_contents($commentsDataFile), true) ?: []) : []; $allComments[] = array_merge($newComment, ['record_id' => $recordId]); file_put_contents($commentsDataFile, json_encode($allComments, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); echo json_encode([ 'success' => true, 'message' => 'Anonymous reply posted successfully.', 'comment' => $newComment ], JSON_UNESCAPED_UNICODE); break; case 'vote_record': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['id'] ?? ''); $voteType = $input['type'] ?? 'up'; if (empty($recordId)) { echo json_encode(['success' => false, 'error' => 'Record ID required']); exit; } // MySQL DB Upvote update $pdo = getDbConnection(); if ($pdo) { try { $delta = ($voteType === 'up') ? 1 : -1; $stmt = $pdo->prepare("UPDATE `records` SET `upvotes` = GREATEST(0, `upvotes` + ?) WHERE `id` = ?"); $stmt->execute([$delta, $recordId]); } catch (Exception $e) { error_log("vote_record MySQL Exception: " . $e->getMessage()); } } updateRecord($customDataFile, $recordId, function($rec) use ($voteType) { $current = intval($rec['upvotes'] ?? 1); $rec['upvotes'] = ($voteType === 'up') ? ($current + 1) : max(0, $current - 1); return $rec; }); echo json_encode(['success' => true, 'message' => 'Vote recorded']); break; case 'export_csv': $records = loadAllRecords($dataFile, $customDataFile); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="nyayasetu_civic_bribe_data_' . date('Y-m-d') . '.csv"'); $output = fopen('php://output', 'w'); fputcsv($output, ['ID', 'State', 'City', 'Department', 'Service', 'Designation', 'Amount_INR', 'Status', 'Outcome', 'Mode', 'Days_Delayed', 'Has_Media', 'Created_At', 'Story']); foreach ($records as $r) { fputcsv($output, [ $r['id'] ?? '', $r['state'] ?? '', $r['city'] ?? '', $r['department'] ?? '', $r['service'] ?? '', $r['designation'] ?? '', $r['amount'] ?? 0, $r['status'] ?? '', $r['outcome_text'] ?? '', $r['mode'] ?? '', $r['days_delayed'] ?? 0, !empty($r['images']) || !empty($r['videos']) ? 'YES' : 'NO', $r['created_at'] ?? '', $r['story'] ?? '' ]); } fclose($output); exit; case 'get_statutes': echo json_encode([ 'success' => true, 'statutes' => [ [ 'act' => 'Prevention of Corruption Act, 1988 (Amended 2018)', 'short_code' => 'PC_ACT_1988', 'key_sections' => [ ['section' => 'Section 7', 'title' => 'Offence relating to public servant being bribed', 'penalty' => '3 to 7 years rigorous imprisonment + Fine'], ['section' => 'Section 7A', 'title' => 'Taking undue advantage to influence public servant by corrupt means / Dalals', 'penalty' => '3 to 7 years imprisonment + Fine'], ['section' => 'Section 8 Proviso', 'title' => 'Compelled bribe-giver safe harbor immunity', 'timeline' => 'Mandatory report within 7 calendar days to ACB / Police for 100% legal protection.'], ['section' => 'Section 13', 'title' => 'Criminal misconduct by a public servant', 'penalty' => '4 to 10 years imprisonment'] ], 'source' => 'India Code / Gazette of India' ], [ 'act' => 'Right to Information Act, 2005', 'short_code' => 'RTI_ACT_2005', 'key_sections' => [ ['section' => 'Section 6(1)', 'title' => 'Application for obtaining information / file inspection', 'timeline' => '30 calendar days (48 hours for life/liberty)'], ['section' => 'Section 7(6)', 'title' => 'Free information after timeline breach', 'rule' => 'If PIO fails to give reply within 30 days, info must be given 100% free of cost.'], ['section' => 'Section 19(1)', 'title' => 'First Appeal to FAA', 'timeline' => 'Within 30 days from expiry of response limit.'], ['section' => 'Section 20(1)', 'title' => 'Personal penalty on delinquent PIO', 'penalty' => '₹250 per day up to maximum ₹25,000 deducted directly from salary.'] ], 'source' => 'Ministry of Personnel, Public Grievances and Pensions' ], [ 'act' => 'Bharatiya Nyaya Sanhita, 2023 (BNS)', 'short_code' => 'BNS_2023', 'key_sections' => [ ['section' => 'Section 356 Excep. 1 & 2', 'title' => 'Public Good Imputation & Conduct of Public Servants', 'protection' => 'Absolute statutory defense against frivolous defamation suits for civic corruption reports.'] ], 'source' => 'Ministry of Law and Justice' ] ], 'source_attributions' => [ ['name' => 'India Code', 'url' => 'https://indiacode.nic.in'], ['name' => 'Open Government Data India', 'url' => 'https://data.gov.in'], ['name' => 'Central Vigilance Commission', 'url' => 'https://cvc.gov.in'], ['name' => 'OpenNyAI Community', 'url' => 'https://github.com/OpenNyAI/opennyai'] ] ], JSON_UNESCAPED_UNICODE); break; case 'get_open_datasets': $records = loadAllRecords($dataFile, $customDataFile); $totalReports = count($records); $totalBribeAmount = 0; $deptCounts = []; $cityCounts = []; $refusalCount = 0; $totalDelayDays = 0; $delayReportCount = 0; foreach ($records as $r) { $amt = intval($r['amount'] ?? 0); $totalBribeAmount += $amt; $dept = $r['department'] ?? 'Other'; $city = $r['city'] ?? 'Other'; $deptCounts[$dept] = ($deptCounts[$dept] ?? 0) + 1; $cityCounts[$city] = ($cityCounts[$city] ?? 0) + 1; if (($r['status'] ?? '') === 'demanded_and_refused') { $refusalCount++; } if (!empty($r['days_delayed'])) { $totalDelayDays += intval($r['days_delayed']); $delayReportCount++; } } arsort($deptCounts); arsort($cityCounts); // Load OGD telemetry cache (generated by sync_ogd_datasets.py from data.gov.in NDSAP public domain data) $ogdCache = []; $ogdCacheFile = $dataDir . '/ogd_telemetry_cache.json'; if (file_exists($ogdCacheFile)) { $ogdCache = json_decode(file_get_contents($ogdCacheFile), true) ?: []; } echo json_encode([ 'success' => true, 'source' => 'NyayKutumb Civic Telemetry + Open Government Data (data.gov.in) NDSAP', 'license' => 'Open Data Commons Open Database License (ODbL) + NDSAP Public Domain', 'citizen_charters' => $ogdCache['citizen_charters'] ?? [], 'landmark_precedents' => $ogdCache['landmark_precedents'] ?? [], 'ogd_synchronized_at' => $ogdCache['synchronized_at'] ?? null, 'summary' => [ 'total_records' => $totalReports, 'total_bribes_reported_inr' => $totalBribeAmount, 'refusal_count' => $refusalCount, 'refusal_rate_pct' => $totalReports > 0 ? round(($refusalCount / $totalReports) * 100, 1) : 0, 'avg_delay_days' => $delayReportCount > 0 ? round($totalDelayDays / $delayReportCount, 1) : 0, 'top_departments' => array_slice($deptCounts, 0, 5, true), 'top_cities' => array_slice($cityCounts, 0, 5, true), 'timestamp' => date('Y-m-d H:i:s') ] ], JSON_UNESCAPED_UNICODE); break; // ═══════════════════════════════════════════════════════ // ADMIN API ENDPOINTS (AUTHENTICATED) // ═══════════════════════════════════════════════════════ case 'admin_login': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $username = trim($input['username'] ?? ''); $password = trim($input['password'] ?? ''); if (verifyAdminCredentials($username, $password)) { $_SESSION['nyaya_admin_logged_in'] = true; $_SESSION['nyaya_admin_user'] = 'iamadmin'; echo json_encode(['success' => true, 'message' => 'Admin login successful', 'username' => 'iamadmin']); } else { echo json_encode(['success' => false, 'error' => 'Invalid username or password']); } break; case 'admin_logout': $_SESSION['nyaya_admin_logged_in'] = false; unset($_SESSION['nyaya_admin_logged_in']); unset($_SESSION['nyaya_admin_user']); session_destroy(); echo json_encode(['success' => true, 'message' => 'Logged out successfully']); break; case 'admin_check_auth': echo json_encode([ 'success' => true, 'authenticated' => isAdminLoggedIn(), 'username' => isAdminLoggedIn() ? ($_SESSION['nyaya_admin_user'] ?? 'iamadmin') : null ]); break; case 'admin_get_records': requireAdminAuth(); $records = loadAllRecords($dataFile, $customDataFile); $commentsLog = file_exists($commentsDataFile) ? (json_decode(file_get_contents($commentsDataFile), true) ?: []) : []; if (!empty($commentsLog)) { $commentsByRec = []; foreach ($commentsLog as $c) { $recId = $c['record_id'] ?? ''; if ($recId) { if (!isset($commentsByRec[$recId])) $commentsByRec[$recId] = []; $commentsByRec[$recId][] = $c; } } foreach ($records as &$r) { $rid = $r['id'] ?? ''; if (isset($commentsByRec[$rid])) { $existingComments = is_array($r['comments'] ?? null) ? $r['comments'] : []; $existingIds = array_column($existingComments, 'id'); foreach ($commentsByRec[$rid] as $c) { if (!in_array($c['id'] ?? '', $existingIds)) { $existingComments[] = $c; } } $r['comments'] = $existingComments; } } } echo json_encode([ 'success' => true, 'total_records' => count($records), 'data' => $records ], JSON_UNESCAPED_UNICODE); break; case 'admin_get_takedown_notices': requireAdminAuth(); $takedownFile = $dataDir . '/takedown_notices.json'; $notices = file_exists($takedownFile) ? (json_decode(file_get_contents($takedownFile), true) ?: []) : []; echo json_encode([ 'success' => true, 'total_notices' => count($notices), 'data' => $notices ], JSON_UNESCAPED_UNICODE); break; case 'process_batch_comment_purge': requireAdminAuth(); $result = executeBatchCommentPurge($customDataFile, $commentsDataFile, $dataFile); echo json_encode($result, JSON_UNESCAPED_UNICODE); break; case 'get_batch_purge_status': requireAdminAuth(); $commentsLog = file_exists($commentsDataFile) ? (json_decode(file_get_contents($commentsDataFile), true) ?: []) : []; $flaggedCount = 0; $clusters = []; foreach ($commentsLog as $c) { $eval = calculateCommentRiskScore($c['text'] ?? ''); if ($eval['is_defamation_cluster']) { $flaggedCount++; foreach ($eval['flags'] as $flg) { $clusters[$flg] = ($clusters[$flg] ?? 0) + 1; } } } echo json_encode([ 'success' => true, 'total_pending_comments' => count($commentsLog), 'flagged_clusters_count' => $flaggedCount, 'clusters' => $clusters ], JSON_UNESCAPED_UNICODE); break; case 'admin_delete_comment': requireAdminAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['record_id'] ?? ''); $commentId = trim($input['comment_id'] ?? $input['id'] ?? ''); if (empty($commentId)) { echo json_encode(['success' => false, 'error' => 'Comment ID is required']); exit; } $deleted = false; // 1. Delete from MySQL Database $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare("DELETE FROM `thread_comments` WHERE `id` = ? OR (`record_id` = ? AND `id` = ?)"); $stmt->execute([$commentId, $recordId, $commentId]); if ($stmt->rowCount() > 0) { $deleted = true; } } catch (Exception $e) { error_log("admin_delete_comment MySQL Exception: " . $e->getMessage()); } } // 2. Remove from user_submissions.json if (file_exists($customDataFile)) { updateRecord($customDataFile, $recordId, function($rec) use ($commentId) { if (isset($rec['comments']) && is_array($rec['comments'])) { $rec['comments'] = array_values(array_filter($rec['comments'], function($c) use ($commentId) { return ($c['id'] ?? '') !== $commentId; })); } return $rec; }); $deleted = true; } // 3. Remove from records.json if present if (file_exists($dataFile)) { updateRecord($dataFile, $recordId, function($rec) use ($commentId) { if (isset($rec['comments']) && is_array($rec['comments'])) { $rec['comments'] = array_values(array_filter($rec['comments'], function($c) use ($commentId) { return ($c['id'] ?? '') !== $commentId; })); } return $rec; }); $deleted = true; } // 4. Remove from thread_comments.json if (file_exists($commentsDataFile)) { $commentsLog = json_decode(file_get_contents($commentsDataFile), true) ?: []; $initialCount = count($commentsLog); $commentsLog = array_values(array_filter($commentsLog, function($c) use ($commentId) { return ($c['id'] ?? '') !== $commentId; })); if (count($commentsLog) !== $initialCount) { file_put_contents($commentsDataFile, json_encode($commentsLog, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); $deleted = true; } } if ($deleted) { echo json_encode(['success' => true, 'message' => 'Comment deleted successfully']); } else { echo json_encode(['success' => false, 'error' => 'Comment not found']); } break; case 'admin_update_takedown_status': requireAdminAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $ticketId = trim($input['ticket_id'] ?? ''); $newStatus = trim($input['status'] ?? 'RESOLVED_TAKEN_DOWN'); if (empty($ticketId)) { echo json_encode(['success' => false, 'error' => 'Ticket ID required']); exit; } $updated = false; // MySQL DB Update $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare("UPDATE `takedown_notices` SET `status` = ? WHERE `ticket_id` = ?"); $stmt->execute([$newStatus, $ticketId]); if ($stmt->rowCount() > 0) { $updated = true; } } catch (Exception $e) { error_log("admin_update_takedown_status MySQL Exception: " . $e->getMessage()); } } // JSON file update $takedownFile = $dataDir . '/takedown_notices.json'; if (file_exists($takedownFile)) { $notices = json_decode(file_get_contents($takedownFile), true) ?: []; foreach ($notices as &$n) { if (($n['ticket_id'] ?? '') === $ticketId) { $n['status'] = $newStatus; $n['resolved_at'] = date('Y-m-d H:i:s'); $updated = true; break; } } if ($updated) { file_put_contents($takedownFile, json_encode($notices, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); } } if ($updated) { echo json_encode(['success' => true, 'message' => 'Takedown ticket status updated successfully']); } else { echo json_encode(['success' => false, 'error' => 'Ticket ID not found']); } break; case 'admin_approve_quarantine': requireAdminAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['id'] ?? ''); if (empty($recordId)) { echo json_encode(['success' => false, 'error' => 'Record ID required']); exit; } $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare("UPDATE `records` SET `moderation_status` = 'APPROVED_PUBLISHED' WHERE `id` = ?"); $stmt->execute([$recordId]); } catch (Exception $e) { error_log("admin_approve_quarantine MySQL Exception: " . $e->getMessage()); } } $ok = updateRecord($customDataFile, $recordId, function($rec) { $rec['moderation_status'] = 'APPROVED_PUBLISHED'; $rec['approved_by_admin_at'] = date('Y-m-d H:i:s'); return $rec; }); if (file_exists($dataFile)) { updateRecord($dataFile, $recordId, function($rec) { $rec['moderation_status'] = 'APPROVED_PUBLISHED'; $rec['approved_by_admin_at'] = date('Y-m-d H:i:s'); return $rec; }); } echo json_encode(['success' => true, 'message' => 'Record approved and published to public feed successfully.']); break; case 'admin_reject_quarantine': requireAdminAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['id'] ?? ''); if (empty($recordId)) { echo json_encode(['success' => false, 'error' => 'Record ID required']); exit; } $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare("UPDATE `records` SET `moderation_status` = 'REJECTED_PERMANENTLY' WHERE `id` = ?"); $stmt->execute([$recordId]); } catch (Exception $e) { error_log("admin_reject_quarantine MySQL Exception: " . $e->getMessage()); } } $ok = updateRecord($customDataFile, $recordId, function($rec) { $rec['moderation_status'] = 'REJECTED_PERMANENTLY'; $rec['rejected_by_admin_at'] = date('Y-m-d H:i:s'); return $rec; }); if (file_exists($dataFile)) { updateRecord($dataFile, $recordId, function($rec) { $rec['moderation_status'] = 'REJECTED_PERMANENTLY'; $rec['rejected_by_admin_at'] = date('Y-m-d H:i:s'); return $rec; }); } echo json_encode(['success' => true, 'message' => 'Record rejected and permanently withheld from public feed.']); break; case 'admin_update_record': requireAdminAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['id'] ?? ''); if (empty($recordId)) { echo json_encode(['success' => false, 'error' => 'Record ID is required']); exit; } $service = isset($input['service']) ? sanitizeAndRedactPII(trim($input['service'])) : null; $designation = isset($input['designation']) ? sanitizeAndRedactPII(trim($input['designation'])) : null; $story = isset($input['story']) ? sanitizeAndRedactPII(trim($input['story'])) : null; $amount = isset($input['amount']) ? floatval($input['amount']) : null; $status = isset($input['status']) ? trim($input['status']) : null; $hasProof = isset($input['has_proof']) ? (bool)$input['has_proof'] : null; $outcomeText = 'Paid Under Duress'; if ($status === 'demanded_and_refused') { $outcomeText = 'Refused & Stood Firm'; } elseif ($status === 'work_stalled_no_bribe') { $outcomeText = 'Refused / File Illegally Stalled'; } $updated = false; $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare(" UPDATE `records` SET `service` = COALESCE(:service, `service`), `designation` = COALESCE(:designation, `designation`), `story` = COALESCE(:story, `story`), `amount` = COALESCE(:amount, `amount`), `status` = COALESCE(:status, `status`), `outcome_text` = COALESCE(:outcome_text, `outcome_text`), `has_proof` = COALESCE(:has_proof, `has_proof`) WHERE `id` = :id "); $stmt->execute([ ':service' => $service, ':designation' => $designation, ':story' => $story, ':amount' => $amount, ':status' => $status, ':outcome_text' => $outcomeText, ':has_proof' => $hasProof !== null ? ($hasProof ? 1 : 0) : null, ':id' => $recordId ]); if ($stmt->rowCount() > 0) { $updated = true; } } catch (Exception $e) { error_log("admin_update_record MySQL Exception: " . $e->getMessage()); } } $jsonOk = updateRecord($customDataFile, $recordId, function($rec) use ($service, $designation, $story, $amount, $status, $hasProof, $outcomeText) { if ($service !== null) $rec['service'] = $service; if ($designation !== null) $rec['designation'] = $designation; if ($story !== null) $rec['story'] = $story; if ($amount !== null) $rec['amount'] = $amount; if ($status !== null) $rec['status'] = $status; if ($hasProof !== null) $rec['has_proof'] = $hasProof; $rec['outcome_text'] = $outcomeText; return $rec; }); if (file_exists($dataFile)) { updateRecord($dataFile, $recordId, function($rec) use ($service, $designation, $story, $amount, $status, $hasProof, $outcomeText) { if ($service !== null) $rec['service'] = $service; if ($designation !== null) $rec['designation'] = $designation; if ($story !== null) $rec['story'] = $story; if ($amount !== null) $rec['amount'] = $amount; if ($status !== null) $rec['status'] = $status; if ($hasProof !== null) $rec['has_proof'] = $hasProof; $rec['outcome_text'] = $outcomeText; return $rec; }); } if ($updated || $jsonOk) { echo json_encode(['success' => true, 'message' => 'Record updated successfully by admin']); } else { echo json_encode(['success' => false, 'error' => 'Record not found or modification failed']); } break; case 'admin_delete_record': requireAdminAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $recordId = trim($input['id'] ?? ''); if (empty($recordId)) { echo json_encode(['success' => false, 'error' => 'Record ID required']); exit; } $deleted = false; // 1. Delete from MySQL DB $pdo = getDbConnection(); if ($pdo) { try { $pdo->prepare("DELETE FROM `thread_comments` WHERE `record_id` = ?")->execute([$recordId]); $stmt = $pdo->prepare("DELETE FROM `records` WHERE `id` = ?"); $stmt->execute([$recordId]); if ($stmt->rowCount() > 0) { $deleted = true; } } catch (Exception $e) { error_log("admin_delete_record MySQL Exception: " . $e->getMessage()); } } // 2. Delete from user_submissions.json if (file_exists($customDataFile)) { $customRecords = json_decode(file_get_contents($customDataFile), true) ?: []; $initialCount = count($customRecords); $filtered = array_values(array_filter($customRecords, function($r) use ($recordId) { return ($r['id'] ?? '') !== $recordId; })); if (count($filtered) !== $initialCount) { file_put_contents($customDataFile, json_encode($filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); $deleted = true; } } // 3. Delete from records.json if (file_exists($dataFile)) { $records = json_decode(file_get_contents($dataFile), true) ?: []; $initialCount = count($records); $filtered = array_values(array_filter($records, function($r) use ($recordId) { return ($r['id'] ?? '') !== $recordId; })); if (count($filtered) !== $initialCount) { file_put_contents($dataFile, json_encode($filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); $deleted = true; } } if ($deleted) { echo json_encode(['success' => true, 'message' => 'Record deleted successfully']); } else { echo json_encode(['success' => false, 'error' => 'Record not found']); } break; case 'submit_takedown_notice': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success' => false, 'error' => 'Method not allowed'], 405); exit; } $input = json_decode(file_get_contents('php://input'), true); $complainantName = trim($input['complainant_name'] ?? ''); $email = trim($input['email'] ?? ''); $targetUrl = trim($input['target_url'] ?? ''); $reason = trim($input['reason'] ?? ''); $statement = trim($input['statement'] ?? ''); if (empty($complainantName) || empty($email) || empty($reason)) { echo json_encode(['success' => false, 'error' => 'Complainant Name, Email, and Statutory Reason are required under IT Rules 2021']); exit; } $newNotice = [ 'ticket_id' => 'TKD-' . date('Ymd') . '-' . rand(1000, 9999), 'complainant_name' => sanitizeAndRedactPII($complainantName), 'email' => filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : '[INVALID_EMAIL]', 'target_url' => htmlspecialchars($targetUrl, ENT_QUOTES, 'UTF-8'), 'reason' => htmlspecialchars($reason, ENT_QUOTES, 'UTF-8'), 'statement' => htmlspecialchars($statement, ENT_QUOTES, 'UTF-8'), 'submitted_at' => date('Y-m-d H:i:s'), 'status' => 'PENDING_REVIEW_36H_SLA' ]; // MySQL DB Insert $pdo = getDbConnection(); if ($pdo) { try { $stmt = $pdo->prepare(" INSERT INTO `takedown_notices` (`ticket_id`, `complainant_name`, `email`, `target_url`, `reason`, `status`, `submitted_at`) VALUES (?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([ $newNotice['ticket_id'], $newNotice['complainant_name'], $newNotice['email'], $newNotice['target_url'], $newNotice['reason'], $newNotice['status'], $newNotice['submitted_at'] ]); } catch (Exception $e) { error_log("submit_takedown_notice MySQL Exception: " . $e->getMessage()); } } // JSON file insert $takedownFile = $dataDir . '/takedown_notices.json'; $notices = file_exists($takedownFile) ? (json_decode(file_get_contents($takedownFile), true) ?: []) : []; array_unshift($notices, $newNotice); file_put_contents($takedownFile, json_encode($notices, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); echo json_encode([ 'success' => true, 'message' => 'Formal IT Rules 2021 Grievance Ticket logged successfully. Grievance Desk SLA: 36 Hours.', 'ticket_id' => $newNotice['ticket_id'] ], JSON_UNESCAPED_UNICODE); break; default: echo json_encode(['success' => false, 'error' => 'Unknown action']); break; }