siip-available-ips/public.php

769 lines
24 KiB
PHP

<?php
// Configurar manejo de errores para devolver JSON
error_reporting(E_ALL);
ini_set('display_errors', 0); // No mostrar errores en HTML
// Handler global de errores
set_error_handler(function($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
// Handler global de excepciones no capturadas
set_exception_handler(function($exception) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Error fatal: ' . $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine()
]);
exit;
});
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/IpSearchService.php';
use Ubnt\UcrmPluginSdk\Service\PluginLogManager;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use SiipAvailableIps\IpSearchService;
// Get UCRM log manager
$log = PluginLogManager::create();
// Log TODAS las peticiones
$log->appendLog('=== NUEVA PETICIÓN ===' );
$log->appendLog('Método: ' . $_SERVER['REQUEST_METHOD']);
$log->appendLog('POST data: ' . json_encode($_POST));
$log->appendLog('GET data: ' . json_encode($_GET));
$log->appendLog('Content-Type: ' . ($_SERVER['CONTENT_TYPE'] ?? 'no definido'));
$log->appendLog('User Agent: ' . ($_SERVER['HTTP_USER_AGENT'] ?? 'no definido'));
// Manejar peticiones AJAX
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'search') {
$log->appendLog('>>> Entrando al handler de búsqueda AJAX');
try {
// Obtener configuración del plugin desde data/config.json
$configManager = PluginConfigManager::create();
$config = $configManager->loadConfig();
$log->appendLog('Configuración cargada: ' . json_encode([
'ipserver' => $config['ipserver'] ?? 'NO CONFIGURADO',
'hasUnmsToken' => !empty($config['unmsApiToken']),
'hasApiToken' => !empty($config['apitoken'])
]));
// Validar que exista la configuración necesaria
if (empty($config['ipserver'])) {
$log->appendLog('ERROR: No se ha configurado ipserver');
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'El plugin no está configurado correctamente. Falta la dirección del servidor.'
]);
exit;
}
if (empty($config['unmsApiToken'])) {
$log->appendLog('ERROR: No se ha configurado unmsApiToken');
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'El plugin no está configurado correctamente. Falta el token de API de UNMS.'
]);
exit;
}
$segmento = $_POST['segment'] ?? '';
$log->appendLog("Buscando IPs en segmento: $segmento");
// URL de la API de UISP - Usar HTTPS
$apiUrl = "https://{$config['ipserver']}/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true";
$apiToken = $config['unmsApiToken'];
$log->appendLog("URL de API: $apiUrl");
// Crear instancia del servicio y buscar IPs
$ipService = new IpSearchService($apiUrl, $apiToken, $log);
$resultado = $ipService->buscarIpsDisponibles($segmento);
$log->appendLog('Resultado de búsqueda: ' . json_encode([
'success' => $resultado['success'],
'ipsDisponibles' => count($resultado['data'] ?? []),
'ipsEnUso' => count($resultado['used'] ?? [])
]));
header('Content-Type: application/json');
echo json_encode($resultado);
} catch (Exception $e) {
$log->appendLog('EXCEPCIÓN en búsqueda de IPs: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Error al procesar la solicitud: ' . $e->getMessage(),
'error_file' => $e->getFile(),
'error_line' => $e->getLine()
]);
} catch (Error $e) {
$log->appendLog('ERROR FATAL en búsqueda de IPs: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Error fatal: ' . $e->getMessage(),
'error_file' => $e->getFile(),
'error_line' => $e->getLine()
]);
}
$log->appendLog('<<< Finalizando handler de búsqueda AJAX');
exit;
} else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$log->appendLog('Petición POST recibida pero sin action=search o action no válida');
$log->appendLog('Action recibida: ' . ($_POST['action'] ?? 'NO DEFINIDA'));
}
// Log de acceso público
$log->appendLog('Acceso a la interfaz pública de búsqueda de IPs');
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Buscador de IPs Disponibles - SIIP</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--dark-bg: #0f0f23;
--card-bg: rgba(255, 255, 255, 0.05);
--card-border: rgba(255, 255, 255, 0.1);
--text-primary: #ffffff;
--text-secondary: #a0aec0;
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.3);
--shadow-xl: 0 25px 80px rgba(0, 0, 0, 0.4);
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--dark-bg);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background-image:
radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(245, 87, 108, 0.15) 0%, transparent 50%);
background-attachment: fixed;
}
.container {
width: 100%;
max-width: 1200px;
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
text-align: center;
margin-bottom: 40px;
}
.logo {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: var(--primary-gradient);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
box-shadow: var(--shadow-lg);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
h1 {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #f5576c 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
font-weight: 400;
}
.card {
background: var(--card-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--card-border);
border-radius: 24px;
padding: 40px;
box-shadow: var(--shadow-xl);
margin-bottom: 30px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.5);
}
.search-form {
display: flex;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.input-group {
flex: 1;
min-width: 250px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 0.9rem;
color: var(--text-secondary);
}
.input-wrapper {
position: relative;
}
.input-prefix {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
font-weight: 500;
pointer-events: none;
}
input[type="text"] {
width: 100%;
padding: 16px 20px 16px 90px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid var(--card-border);
border-radius: 12px;
color: var(--text-primary);
font-size: 1rem;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}
.btn {
padding: 16px 32px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Inter', sans-serif;
display: inline-flex;
align-items: center;
gap: 10px;
align-self: flex-end;
}
.btn-primary {
background: var(--primary-gradient);
color: white;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.loading.active {
display: block;
}
.spinner {
width: 50px;
height: 50px;
margin: 0 auto 15px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.results {
display: none;
}
.results.active {
display: block;
animation: fadeInUp 0.5s ease-out;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 20px;
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
background: var(--success-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 5px;
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
.table-container {
overflow-x: auto;
border-radius: 16px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--card-border);
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: rgba(255, 255, 255, 0.05);
}
th {
padding: 16px;
text-align: left;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 16px;
border-top: 1px solid var(--card-border);
}
tbody tr {
transition: background 0.2s ease;
}
tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
.ip-address {
font-family: 'Courier New', monospace;
font-weight: 600;
font-size: 1rem;
color: #4facfe;
}
.btn-copy {
padding: 8px 16px;
background: rgba(102, 126, 234, 0.2);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
color: #667eea;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-copy:hover {
background: rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.5);
transform: scale(1.05);
}
.btn-copy.copied {
background: rgba(79, 172, 254, 0.2);
border-color: rgba(79, 172, 254, 0.3);
color: #4facfe;
}
.alert {
padding: 16px 20px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
animation: fadeInUp 0.4s ease-out;
}
.alert-error {
background: rgba(245, 87, 108, 0.1);
border: 1px solid rgba(245, 87, 108, 0.3);
color: #f5576c;
}
.alert-success {
background: rgba(79, 172, 254, 0.1);
border: 1px solid rgba(79, 172, 254, 0.3);
color: #4facfe;
}
.footer {
text-align: center;
color: var(--text-secondary);
font-size: 0.9rem;
margin-top: 40px;
}
.footer a {
color: #667eea;
text-decoration: none;
transition: color 0.2s ease;
}
.footer a:hover {
color: #f5576c;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.card {
padding: 25px;
}
.search-form {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
.stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">🌐</div>
<h1>Buscador de IPs Disponibles</h1>
<p class="subtitle">Sistema de gestión de direcciones IP para UISP</p>
</div>
<div class="card">
<form class="search-form" id="searchForm">
<div class="input-group">
<label for="segment">Segmento de Red</label>
<div class="input-wrapper">
<span class="input-prefix">172.16.</span>
<input
type="text"
id="segment"
name="segment"
placeholder="0-255"
required
pattern="[0-9]{1,3}"
maxlength="3"
>
</div>
</div>
<button type="submit" class="btn btn-primary" id="searchBtn">
<span>🔍</span>
<span>Buscar IPs</span>
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Consultando IPs disponibles...</p>
</div>
<div id="errorContainer"></div>
<div class="results" id="results">
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="availableCount">0</div>
<div class="stat-label">IPs Disponibles</div>
</div>
<div class="stat-card">
<div class="stat-value" id="usedCount">0</div>
<div class="stat-label">IPs en Uso</div>
</div>
<div class="stat-card">
<div class="stat-value" id="segmentDisplay">-</div>
<div class="stat-label">Segmento</div>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>#</th>
<th>Dirección IP</th>
<th>Acción</th>
</tr>
</thead>
<tbody id="ipTableBody">
</tbody>
</table>
</div>
</div>
</div>
<div class="footer">
<p>Desarrollado por <a href="https://siip.mx" target="_blank">SIIP Internet</a></p>
</div>
</div>
<script>
const searchForm = document.getElementById('searchForm');
const searchBtn = document.getElementById('searchBtn');
const loading = document.getElementById('loading');
const results = document.getElementById('results');
const errorContainer = document.getElementById('errorContainer');
const ipTableBody = document.getElementById('ipTableBody');
const availableCount = document.getElementById('availableCount');
const usedCount = document.getElementById('usedCount');
const segmentDisplay = document.getElementById('segmentDisplay');
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const segment = document.getElementById('segment').value;
// Validar segmento
const segmentNum = parseInt(segment);
if (isNaN(segmentNum) || segmentNum < 0 || segmentNum > 255) {
showError('El segmento debe ser un número entre 0 y 255');
return;
}
// Mostrar loading
loading.classList.add('active');
results.classList.remove('active');
errorContainer.innerHTML = '';
searchBtn.disabled = true;
try {
const formData = new FormData();
formData.append('action', 'search');
formData.append('segment', segment);
console.log('Enviando petición AJAX...');
console.log('Segmento:', segment);
const response = await fetch('', {
method: 'POST',
body: formData
});
console.log('Respuesta recibida:', response.status, response.statusText);
// Verificar si la respuesta es JSON
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);
if (!contentType || !contentType.includes('application/json')) {
const textResponse = await response.text();
console.error('Respuesta no es JSON:', textResponse.substring(0, 500));
throw new Error('El servidor no devolvió una respuesta JSON válida');
}
const data = await response.json();
console.log('Datos recibidos:', data);
loading.classList.remove('active');
searchBtn.disabled = false;
if (data.success) {
displayResults(data);
} else {
showError(data.message || 'Error al buscar IPs disponibles');
}
} catch (error) {
loading.classList.remove('active');
searchBtn.disabled = false;
console.error('Error completo:', error);
console.error('Tipo de error:', error.name);
console.error('Mensaje:', error.message);
console.error('Stack:', error.stack);
showError('Error de conexión: ' + error.message + '. Revise la consola del navegador para más detalles.');
}
});
function displayResults(data) {
// Actualizar estadísticas
availableCount.textContent = data.data.length;
usedCount.textContent = data.used ? data.used.length : 0;
segmentDisplay.textContent = data.segment || '-';
// Limpiar tabla
ipTableBody.innerHTML = '';
// Mostrar mensaje si no hay IPs disponibles
if (data.data.length === 0) {
showError('No hay IPs disponibles en este segmento', 'warning');
return;
}
// Llenar tabla
data.data.forEach((ip, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td><span class="ip-address">${ip}</span></td>
<td>
<button class="btn-copy" onclick="copyToClipboard('${ip}', this)">
📋 Copiar
</button>
</td>
`;
ipTableBody.appendChild(row);
});
// Mostrar resultados
results.classList.add('active');
// Mostrar mensaje de éxito
showError(data.message, 'success');
}
function showError(message, type = 'error') {
const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
const icon = type === 'success' ? '✓' : '⚠';
errorContainer.innerHTML = `
<div class="alert ${alertClass}">
<span style="font-size: 1.5rem;">${icon}</span>
<span>${message}</span>
</div>
`;
// Auto-ocultar mensajes de éxito después de 5 segundos
if (type === 'success') {
setTimeout(() => {
errorContainer.innerHTML = '';
}, 5000);
}
}
async function copyToClipboard(text, button) {
try {
await navigator.clipboard.writeText(text);
const originalText = button.innerHTML;
button.innerHTML = '✓ Copiado';
button.classList.add('copied');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('copied');
}, 2000);
} catch (error) {
console.error('Error al copiar:', error);
alert('Error al copiar al portapapeles');
}
}
// Auto-focus en el campo de segmento
document.getElementById('segment').focus();
</script>
</body>
</html>