'Invalid action'], 400); } } catch (Exception $e) { jsonResponse(['error' => $e->getMessage()], 500); } /** * List entries with pagination and filtering */ function handleList($db) { $page = max(1, intval($_GET['page'] ?? 1)); $limit = min(100, max(10, intval($_GET['limit'] ?? ITEMS_PER_PAGE))); $offset = ($page - 1) * $limit; $where = ['1=1']; $params = []; // Filtering if (!empty($_GET['country'])) { $where[] = 'country_code = :country'; $params[':country'] = strtoupper($_GET['country']); } if (!empty($_GET['search'])) { $where[] = '(ip_prefix LIKE :search OR city LIKE :search2 OR region_code LIKE :search3)'; $searchTerm = '%' . $_GET['search'] . '%'; $params[':search'] = $searchTerm; $params[':search2'] = $searchTerm; $params[':search3'] = $searchTerm; } $whereClause = implode(' AND ', $where); // Get total count $countStmt = $db->prepare("SELECT COUNT(*) as total FROM geofeed_entries WHERE $whereClause"); $countStmt->execute($params); $total = $countStmt->fetch()['total']; // Get entries $sql = "SELECT * FROM geofeed_entries WHERE $whereClause ORDER BY created_at DESC LIMIT :limit OFFSET :offset"; $stmt = $db->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $entries = $stmt->fetchAll(); jsonResponse([ 'success' => true, 'data' => $entries, 'pagination' => [ 'page' => $page, 'limit' => $limit, 'total' => $total, 'pages' => ceil($total / $limit) ] ]); } /** * Get single entry */ function handleGet($db) { $id = intval($_GET['id'] ?? 0); if (!$id) { jsonResponse(['error' => 'Invalid ID'], 400); } $stmt = $db->prepare("SELECT * FROM geofeed_entries WHERE id = :id"); $stmt->execute([':id' => $id]); $entry = $stmt->fetch(); if (!$entry) { jsonResponse(['error' => 'Entry not found'], 404); } jsonResponse(['success' => true, 'data' => $entry]); } /** * Create new entry */ function handleCreate($db) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['error' => 'Method not allowed'], 405); } $input = json_decode(file_get_contents('php://input'), true); // Validate CSRF if (!validateCSRFToken($input['csrf_token'] ?? '')) { jsonResponse(['error' => 'Invalid CSRF token'], 403); } // Validate required fields if (empty($input['ip_prefix'])) { jsonResponse(['error' => 'IP prefix is required'], 400); } if (!isValidIpPrefix($input['ip_prefix'])) { jsonResponse(['error' => 'Invalid IP prefix format'], 400); } // Validate optional fields $countryCode = strtoupper(trim($input['country_code'] ?? '')); if (!empty($countryCode) && !isValidCountryCode($countryCode)) { jsonResponse(['error' => 'Invalid country code (must be 2 letters)'], 400); } $regionCode = strtoupper(trim($input['region_code'] ?? '')); if (!empty($regionCode) && !isValidRegionCode($regionCode)) { jsonResponse(['error' => 'Invalid region code (format: XX-YYY)'], 400); } // Check for duplicate $checkStmt = $db->prepare("SELECT id FROM geofeed_entries WHERE ip_prefix = :prefix"); $checkStmt->execute([':prefix' => $input['ip_prefix']]); if ($checkStmt->fetch()) { jsonResponse(['error' => 'An entry with this IP prefix already exists'], 409); } // Insert entry $stmt = $db->prepare(" INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code, notes) VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code, :notes) "); $stmt->execute([ ':ip_prefix' => trim($input['ip_prefix']), ':country_code' => $countryCode ?: null, ':region_code' => $regionCode ?: null, ':city' => trim($input['city'] ?? '') ?: null, ':postal_code' => trim($input['postal_code'] ?? '') ?: null, ':notes' => trim($input['notes'] ?? '') ?: null ]); $id = $db->lastInsertId(); // Log the action logAction($db, $id, 'INSERT', null, $input); jsonResponse(['success' => true, 'id' => $id, 'message' => 'Entry created successfully'], 201); } /** * Update existing entry */ function handleUpdate($db) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['error' => 'Method not allowed'], 405); } $input = json_decode(file_get_contents('php://input'), true); $id = intval($input['id'] ?? 0); if (!$id) { jsonResponse(['error' => 'Invalid ID'], 400); } // Validate CSRF if (!validateCSRFToken($input['csrf_token'] ?? '')) { jsonResponse(['error' => 'Invalid CSRF token'], 403); } // Get existing entry $checkStmt = $db->prepare("SELECT * FROM geofeed_entries WHERE id = :id"); $checkStmt->execute([':id' => $id]); $oldEntry = $checkStmt->fetch(); if (!$oldEntry) { jsonResponse(['error' => 'Entry not found'], 404); } // Validate fields if (empty($input['ip_prefix'])) { jsonResponse(['error' => 'IP prefix is required'], 400); } if (!isValidIpPrefix($input['ip_prefix'])) { jsonResponse(['error' => 'Invalid IP prefix format'], 400); } $countryCode = strtoupper(trim($input['country_code'] ?? '')); if (!empty($countryCode) && !isValidCountryCode($countryCode)) { jsonResponse(['error' => 'Invalid country code'], 400); } $regionCode = strtoupper(trim($input['region_code'] ?? '')); if (!empty($regionCode) && !isValidRegionCode($regionCode)) { jsonResponse(['error' => 'Invalid region code'], 400); } // Check for duplicate (excluding current entry) $dupStmt = $db->prepare("SELECT id FROM geofeed_entries WHERE ip_prefix = :prefix AND id != :id"); $dupStmt->execute([':prefix' => $input['ip_prefix'], ':id' => $id]); if ($dupStmt->fetch()) { jsonResponse(['error' => 'Another entry with this IP prefix already exists'], 409); } // Update entry $stmt = $db->prepare(" UPDATE geofeed_entries SET ip_prefix = :ip_prefix, country_code = :country_code, region_code = :region_code, city = :city, postal_code = :postal_code, notes = :notes WHERE id = :id "); $stmt->execute([ ':id' => $id, ':ip_prefix' => trim($input['ip_prefix']), ':country_code' => $countryCode ?: null, ':region_code' => $regionCode ?: null, ':city' => trim($input['city'] ?? '') ?: null, ':postal_code' => trim($input['postal_code'] ?? '') ?: null, ':notes' => trim($input['notes'] ?? '') ?: null ]); // Log the action logAction($db, $id, 'UPDATE', $oldEntry, $input); jsonResponse(['success' => true, 'message' => 'Entry updated successfully']); } /** * Delete entry */ function handleDelete($db) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { jsonResponse(['error' => 'Method not allowed'], 405); } $input = json_decode(file_get_contents('php://input'), true); $id = intval($input['id'] ?? 0); if (!$id) { jsonResponse(['error' => 'Invalid ID'], 400); } // Validate CSRF if (!validateCSRFToken($input['csrf_token'] ?? '')) { jsonResponse(['error' => 'Invalid CSRF token'], 403); } // Get existing entry for logging $checkStmt = $db->prepare("SELECT * FROM geofeed_entries WHERE id = :id"); $checkStmt->execute([':id' => $id]); $oldEntry = $checkStmt->fetch(); if (!$oldEntry) { jsonResponse(['error' => 'Entry not found'], 404); } // Delete entry $stmt = $db->prepare("DELETE FROM geofeed_entries WHERE id = :id"); $stmt->execute([':id' => $id]); // Log the action logAction($db, $id, 'DELETE', $oldEntry, null); jsonResponse(['success' => true, 'message' => 'Entry deleted successfully']); } /** * Export entries as CSV */ function handleExport($db) { $stmt = $db->query("SELECT ip_prefix, country_code, region_code, city, postal_code FROM geofeed_entries ORDER BY ip_prefix"); $entries = $stmt->fetchAll(); // Build CSV content (RFC 8805 format) $csv = "# Geofeed - Generated by Geofeed Manager\r\n"; $csv .= "# Format: ip_prefix,country_code,region_code,city,postal_code\r\n"; $csv .= "# Generated: " . date('c') . "\r\n"; foreach ($entries as $entry) { $csv .= implode(',', [ $entry['ip_prefix'], $entry['country_code'] ?? '', $entry['region_code'] ?? '', $entry['city'] ?? '', $entry['postal_code'] ?? '' ]) . "\r\n"; } // Return as downloadable or as JSON based on format param if (($_GET['format'] ?? '') === 'download') { header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="geofeed.csv"'); echo $csv; exit; } jsonResponse([ 'success' => true, 'csv' => $csv, 'count' => count($entries) ]); } /** * Get statistics */ function handleStats($db) { $stats = []; // Total entries $stmt = $db->query("SELECT COUNT(*) as count FROM geofeed_entries"); $stats['total_entries'] = $stmt->fetch()['count']; // Entries by country (top 10) $stmt = $db->query(" SELECT country_code, COUNT(*) as count FROM geofeed_entries WHERE country_code IS NOT NULL AND country_code != '' GROUP BY country_code ORDER BY count DESC LIMIT 10 "); $stats['by_country'] = $stmt->fetchAll(); // Recent changes $stmt = $db->query(" SELECT action, COUNT(*) as count, DATE(changed_at) as date FROM geofeed_audit_log WHERE changed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY action, DATE(changed_at) ORDER BY date DESC "); $stats['recent_changes'] = $stmt->fetchAll(); // IPv4 vs IPv6 $stmt = $db->query(" SELECT SUM(CASE WHEN ip_prefix LIKE '%:%' THEN 1 ELSE 0 END) as ipv6, SUM(CASE WHEN ip_prefix NOT LIKE '%:%' THEN 1 ELSE 0 END) as ipv4 FROM geofeed_entries "); $stats['ip_versions'] = $stmt->fetch(); jsonResponse(['success' => true, 'data' => $stats]); } /** * Search entries */ function handleSearch($db) { $query = trim($_GET['q'] ?? ''); if (strlen($query) < 2) { jsonResponse(['error' => 'Search query too short'], 400); } $searchTerm = '%' . $query . '%'; $stmt = $db->prepare(" SELECT * FROM geofeed_entries WHERE ip_prefix LIKE :q1 OR city LIKE :q2 OR region_code LIKE :q3 OR country_code LIKE :q4 ORDER BY ip_prefix LIMIT 50 "); $stmt->execute([ ':q1' => $searchTerm, ':q2' => $searchTerm, ':q3' => $searchTerm, ':q4' => $searchTerm ]); jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]); } /** * Log action to audit table */ function logAction($db, $entryId, $action, $oldValues, $newValues) { $stmt = $db->prepare(" INSERT INTO geofeed_audit_log (entry_id, action, old_values, new_values, changed_by) VALUES (:entry_id, :action, :old_values, :new_values, :changed_by) "); $stmt->execute([ ':entry_id' => $entryId, ':action' => $action, ':old_values' => $oldValues ? json_encode($oldValues) : null, ':new_values' => $newValues ? json_encode($newValues) : null, ':changed_by' => $_SERVER['REMOTE_ADDR'] ?? 'system' ]); }