added hostname option

This commit is contained in:
Purple
2026-01-17 22:27:05 +00:00
parent b0e3f4c385
commit 58b91fb2d3
2 changed files with 505 additions and 0 deletions

View File

@@ -142,6 +142,18 @@ try {
handleLogout();
break;
case 'database_backup':
handleDatabaseBackup($db);
break;
case 'database_import':
handleDatabaseImport($db);
break;
case 'system_info':
handleSystemInfo($db);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
@@ -1429,3 +1441,254 @@ function handleLogout() {
logoutUser();
jsonResponse(['success' => true, 'redirect' => 'login.php']);
}
/**
* Export full database backup as JSON
*/
function handleDatabaseBackup($db) {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
try {
// Get all geofeed entries
$entries = $db->query("SELECT * FROM geofeed_entries ORDER BY id")->fetchAll();
// Get all settings
$settings = $db->query("SELECT * FROM settings ORDER BY setting_key")->fetchAll();
// Get audit log (last 1000 entries)
$auditLog = $db->query("SELECT * FROM geofeed_audit_log ORDER BY id DESC LIMIT 1000")->fetchAll();
// Get client logos
$logos = $db->query("SELECT * FROM client_logos ORDER BY short_name")->fetchAll();
$backup = [
'backup_info' => [
'created_at' => date('c'),
'app_version' => APP_VERSION,
'app_name' => APP_NAME,
'entry_count' => count($entries),
'settings_count' => count($settings),
'audit_log_count' => count($auditLog),
'logos_count' => count($logos)
],
'geofeed_entries' => $entries,
'settings' => $settings,
'audit_log' => $auditLog,
'client_logos' => $logos
];
// Set headers for file download
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="geofeed_backup_' . date('Y-m-d_His') . '.json"');
header('Cache-Control: no-cache, must-revalidate');
echo json_encode($backup, JSON_PRETTY_PRINT);
exit;
} catch (Exception $e) {
jsonResponse(['error' => 'Backup failed: ' . $e->getMessage()], 500);
}
}
/**
* Import database from JSON backup
*/
function handleDatabaseImport($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);
}
if (empty($input['backup_data'])) {
jsonResponse(['error' => 'No backup data provided'], 400);
}
$backup = $input['backup_data'];
// Validate backup structure
if (!isset($backup['backup_info']) || !isset($backup['geofeed_entries'])) {
jsonResponse(['error' => 'Invalid backup file format'], 400);
}
try {
$db->beginTransaction();
$importedEntries = 0;
$importedSettings = 0;
$importedLogos = 0;
// Clear existing entries if backup contains entries
if (!empty($backup['geofeed_entries'])) {
$db->exec("DELETE FROM geofeed_entries");
// Re-insert entries
$stmt = $db->prepare("
INSERT INTO geofeed_entries
(ip_prefix, country_code, region_code, city, postal_code, client_short_name, notes, sort_order,
ipr_enriched_at, ipr_hostname, ipr_isp, ipr_org, ipr_asn, ipr_asn_name, ipr_connection_type,
ipr_country_name, ipr_region_name, ipr_timezone, ipr_latitude, ipr_longitude,
flag_abuser, flag_attacker, flag_bogon, flag_cloud_provider, flag_proxy,
flag_relay, flag_tor, flag_tor_exit, flag_vpn, flag_anonymous, flag_threat,
created_at, updated_at)
VALUES
(:ip_prefix, :country_code, :region_code, :city, :postal_code, :client_short_name, :notes, :sort_order,
:ipr_enriched_at, :ipr_hostname, :ipr_isp, :ipr_org, :ipr_asn, :ipr_asn_name, :ipr_connection_type,
:ipr_country_name, :ipr_region_name, :ipr_timezone, :ipr_latitude, :ipr_longitude,
:flag_abuser, :flag_attacker, :flag_bogon, :flag_cloud_provider, :flag_proxy,
:flag_relay, :flag_tor, :flag_tor_exit, :flag_vpn, :flag_anonymous, :flag_threat,
:created_at, :updated_at)
");
foreach ($backup['geofeed_entries'] as $entry) {
$stmt->execute([
':ip_prefix' => $entry['ip_prefix'] ?? null,
':country_code' => $entry['country_code'] ?? null,
':region_code' => $entry['region_code'] ?? null,
':city' => $entry['city'] ?? null,
':postal_code' => $entry['postal_code'] ?? null,
':client_short_name' => $entry['client_short_name'] ?? null,
':notes' => $entry['notes'] ?? null,
':sort_order' => $entry['sort_order'] ?? 0,
':ipr_enriched_at' => $entry['ipr_enriched_at'] ?? null,
':ipr_hostname' => $entry['ipr_hostname'] ?? null,
':ipr_isp' => $entry['ipr_isp'] ?? null,
':ipr_org' => $entry['ipr_org'] ?? null,
':ipr_asn' => $entry['ipr_asn'] ?? null,
':ipr_asn_name' => $entry['ipr_asn_name'] ?? null,
':ipr_connection_type' => $entry['ipr_connection_type'] ?? null,
':ipr_country_name' => $entry['ipr_country_name'] ?? null,
':ipr_region_name' => $entry['ipr_region_name'] ?? null,
':ipr_timezone' => $entry['ipr_timezone'] ?? null,
':ipr_latitude' => $entry['ipr_latitude'] ?? null,
':ipr_longitude' => $entry['ipr_longitude'] ?? null,
':flag_abuser' => $entry['flag_abuser'] ?? 0,
':flag_attacker' => $entry['flag_attacker'] ?? 0,
':flag_bogon' => $entry['flag_bogon'] ?? 0,
':flag_cloud_provider' => $entry['flag_cloud_provider'] ?? 0,
':flag_proxy' => $entry['flag_proxy'] ?? 0,
':flag_relay' => $entry['flag_relay'] ?? 0,
':flag_tor' => $entry['flag_tor'] ?? 0,
':flag_tor_exit' => $entry['flag_tor_exit'] ?? 0,
':flag_vpn' => $entry['flag_vpn'] ?? 0,
':flag_anonymous' => $entry['flag_anonymous'] ?? 0,
':flag_threat' => $entry['flag_threat'] ?? 0,
':created_at' => $entry['created_at'] ?? date('Y-m-d H:i:s'),
':updated_at' => $entry['updated_at'] ?? date('Y-m-d H:i:s')
]);
$importedEntries++;
}
}
// Import settings
if (!empty($backup['settings'])) {
foreach ($backup['settings'] as $setting) {
saveSetting($db, $setting['setting_key'], $setting['setting_value']);
$importedSettings++;
}
}
// Import client logos
if (!empty($backup['client_logos'])) {
$db->exec("DELETE FROM client_logos");
$stmt = $db->prepare("
INSERT INTO client_logos (short_name, logo_data, mime_type, created_at, updated_at)
VALUES (:short_name, :logo_data, :mime_type, :created_at, :updated_at)
");
foreach ($backup['client_logos'] as $logo) {
$stmt->execute([
':short_name' => $logo['short_name'],
':logo_data' => $logo['logo_data'],
':mime_type' => $logo['mime_type'] ?? 'image/png',
':created_at' => $logo['created_at'] ?? date('Y-m-d H:i:s'),
':updated_at' => $logo['updated_at'] ?? date('Y-m-d H:i:s')
]);
$importedLogos++;
}
}
// Log the import action
logAudit($db, null, 'database_import', null, [
'entries_imported' => $importedEntries,
'settings_imported' => $importedSettings,
'logos_imported' => $importedLogos,
'backup_date' => $backup['backup_info']['created_at'] ?? 'unknown'
]);
$db->commit();
jsonResponse([
'success' => true,
'message' => 'Database restored successfully',
'imported' => [
'entries' => $importedEntries,
'settings' => $importedSettings,
'logos' => $importedLogos
]
]);
} catch (Exception $e) {
$db->rollBack();
jsonResponse(['error' => 'Import failed: ' . $e->getMessage()], 500);
}
}
/**
* Get system information for developer tab
*/
function handleSystemInfo($db) {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
try {
// Get database stats
$entryCount = $db->query("SELECT COUNT(*) FROM geofeed_entries")->fetchColumn();
$enrichedCount = $db->query("SELECT COUNT(*) FROM geofeed_entries WHERE ipr_enriched_at IS NOT NULL")->fetchColumn();
$settingsCount = $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
$auditCount = $db->query("SELECT COUNT(*) FROM geofeed_audit_log")->fetchColumn();
$logosCount = $db->query("SELECT COUNT(*) FROM client_logos")->fetchColumn();
// Get database size
$dbSize = $db->query("
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) as size_mb
FROM information_schema.tables
WHERE table_schema = '" . DB_NAME . "'
")->fetchColumn();
jsonResponse([
'success' => true,
'data' => [
'app_version' => APP_VERSION,
'php_version' => PHP_VERSION,
'database' => [
'name' => DB_NAME,
'host' => DB_HOST,
'size_mb' => $dbSize ?: 0,
'entries' => (int)$entryCount,
'enriched_entries' => (int)$enrichedCount,
'settings' => (int)$settingsCount,
'audit_log_entries' => (int)$auditCount,
'client_logos' => (int)$logosCount
],
'server' => [
'software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'time' => date('c'),
'timezone' => date_default_timezone_get()
]
]
]);
} catch (Exception $e) {
jsonResponse(['error' => 'Failed to get system info: ' . $e->getMessage()], 500);
}
}

View File

@@ -1556,6 +1556,13 @@ if (function_exists('requireAuth')) {
</svg>
Advanced
</button>
<button class="tab" onclick="switchTab('developer')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/>
<polyline points="8 6 2 12 8 18"/>
</svg>
Developer
</button>
</div>
<!-- Entries Tab -->
@@ -1900,6 +1907,89 @@ if (function_exists('requireAuth')) {
</button>
</div>
</div>
<!-- Developer Tab -->
<div class="tab-content" id="tab-developer">
<!-- Database Backup Section -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Database Backup
</h2>
<p class="advanced-section-desc">Export a full backup of the database including all entries, settings, and audit logs as a JSON file.</p>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
<button class="btn btn-primary" onclick="downloadDatabaseBackup()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Download Full Backup
</button>
</div>
</div>
<!-- Database Import Section -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Database Import
</h2>
<p class="advanced-section-desc">Restore the database from a previously exported backup file. This will replace all existing data.</p>
<div style="margin-top: 16px;">
<div class="form-group">
<label class="form-label">Select Backup File (JSON)</label>
<input type="file" id="dbImportFile" accept=".json" class="form-input" style="padding: 8px;">
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 12px;">
<button class="btn btn-warning" onclick="importDatabaseBackup()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Import Backup
</button>
</div>
</div>
<div class="alert alert-warning" style="margin-top: 16px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span>Warning: Importing a backup will permanently replace all existing data. Make sure to download a backup first.</span>
</div>
</div>
<!-- Debug Info Section -->
<div class="advanced-section">
<h2 class="advanced-section-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
System Information
</h2>
<p class="advanced-section-desc">Current system and database information for debugging purposes.</p>
<div id="systemInfoContent" style="margin-top: 16px;">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
</div>
</main>
<footer class="footer">
@@ -2068,6 +2158,11 @@ if (function_exists('requireAuth')) {
loadWebhookQueueStatus();
loadIpRegistrySettings();
}
// Load data for developer tab
if (tab === 'developer') {
loadSystemInfo();
}
}
// API Helper
@@ -3226,6 +3321,153 @@ if (function_exists('requireAuth')) {
}
}
// Developer Tab Functions
// Download database backup
function downloadDatabaseBackup() {
const url = `api.php?action=database_backup`;
window.location.href = url;
showToast('Downloading backup...', 'success');
}
// Import database backup
async function importDatabaseBackup() {
const fileInput = document.getElementById('dbImportFile');
const file = fileInput.files[0];
if (!file) {
showToast('Please select a backup file', 'error');
return;
}
if (!file.name.endsWith('.json')) {
showToast('Please select a valid JSON backup file', 'error');
return;
}
if (!confirm('WARNING: This will replace ALL existing data with the backup. Are you sure you want to continue?')) {
return;
}
try {
const text = await file.text();
const backupData = JSON.parse(text);
// Validate basic structure
if (!backupData.backup_info || !backupData.geofeed_entries) {
showToast('Invalid backup file format', 'error');
return;
}
const result = await api('database_import', {}, 'POST', { backup_data: backupData });
if (result.success) {
showToast(`Import successful: ${result.imported.entries} entries, ${result.imported.settings} settings, ${result.imported.logos} logos`, 'success');
fileInput.value = '';
loadSystemInfo();
loadEntries();
loadStats();
} else {
showToast(result.error || 'Import failed', 'error');
}
} catch (e) {
if (e instanceof SyntaxError) {
showToast('Invalid JSON file', 'error');
} else {
showToast('Import failed: ' + e.message, 'error');
}
}
}
// Load system information
async function loadSystemInfo() {
const container = document.getElementById('systemInfoContent');
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
try {
const result = await api('system_info');
if (result.success) {
const data = result.data;
container.innerHTML = `
<div style="display: grid; gap: 16px;">
<div class="table-container" style="margin: 0;">
<table>
<tbody>
<tr>
<td style="width: 200px; font-weight: 600;">App Version</td>
<td>${escapeHtml(data.app_version)}</td>
</tr>
<tr>
<td style="font-weight: 600;">PHP Version</td>
<td>${escapeHtml(data.php_version)}</td>
</tr>
<tr>
<td style="font-weight: 600;">Server Software</td>
<td>${escapeHtml(data.server.software)}</td>
</tr>
<tr>
<td style="font-weight: 600;">Server Time</td>
<td>${escapeHtml(data.server.time)}</td>
</tr>
<tr>
<td style="font-weight: 600;">Timezone</td>
<td>${escapeHtml(data.server.timezone)}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container" style="margin: 0;">
<div class="table-header">
<h3 class="table-title">Database Statistics</h3>
</div>
<table>
<tbody>
<tr>
<td style="width: 200px; font-weight: 600;">Database Name</td>
<td>${escapeHtml(data.database.name)}</td>
</tr>
<tr>
<td style="font-weight: 600;">Database Host</td>
<td>${escapeHtml(data.database.host)}</td>
</tr>
<tr>
<td style="font-weight: 600;">Database Size</td>
<td>${data.database.size_mb} MB</td>
</tr>
<tr>
<td style="font-weight: 600;">Geofeed Entries</td>
<td>${data.database.entries.toLocaleString()}</td>
</tr>
<tr>
<td style="font-weight: 600;">Enriched Entries</td>
<td>${data.database.enriched_entries.toLocaleString()}</td>
</tr>
<tr>
<td style="font-weight: 600;">Settings</td>
<td>${data.database.settings.toLocaleString()}</td>
</tr>
<tr>
<td style="font-weight: 600;">Audit Log Entries</td>
<td>${data.database.audit_log_entries.toLocaleString()}</td>
</tr>
<tr>
<td style="font-weight: 600;">Client Logos</td>
<td>${data.database.client_logos.toLocaleString()}</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
} else {
container.innerHTML = `<div class="alert alert-danger">Failed to load system info: ${escapeHtml(result.error)}</div>`;
}
} catch (error) {
container.innerHTML = `<div class="alert alert-danger">Failed to load system info</div>`;
}
}
// Logout function
async function logout() {
try {