git commit -m "feat: sistema de configuración avanzada de IPs administrativas por segmento (v1.5.0)

- Agregar editor visual de configuración avanzada con protección por contraseña
- Implementar rangos de IPs administrativas personalizados por segmento
- Crear AdminRangeHelper.php para procesamiento de JSON por segmento
- Agregar botón ⚙️ de acceso rápido al editor
- Implementar modal de autenticación con validación de contraseña
- Agregar fallback inteligente a rangos globales para segmentos no configurados

Configuración avanzada:
- 3 campos nuevos en manifest.json (checkbox, JSON textarea, password)
- Editor visual completo con formularios dinámicos
- Agregar/eliminar segmentos y rangos en tiempo real
- Tabla de segmentos configurados con acciones
- Validación de cambios sin guardar
- Mensaje de éxito al guardar configuración

Backend:
- AdminRangeHelper.php: getSegmentLimits(), isAdminIpCustom(), validateConfigJson()
- IpSearchService.php: soporte para rangos personalizados con fallback
- Handler POST para guardar configuración JSON

Frontend:
- ~250 líneas de JavaScript para gestión del editor
- CSS responsive con soporte para temas claro/oscuro
- Interfaz amigable para usuarios no técnicos

Lógica de fallback:
- Checkbox desactivado → rangos globales para todos
- Checkbox activado + segmento en JSON → rangos del JSON
- Checkbox activado + segmento NO en JSON → fallback a rangos globales

Archivos creados:
- src/AdminRangeHelper.php

Archivos modificados:
- manifest.json: 3 campos nuevos, versión 1.5.0
- src/IpSearchService.php: lógica de fallback
- public.php: editor completo, modal, JavaScript
- CHANGELOG.md: entrada v1.5.0
- README.md: documentación de nuevos campos"
This commit is contained in:
DANYDHSV 2025-11-27 13:00:40 -06:00
parent 57f9646b71
commit 1b821410c1
9 changed files with 1010 additions and 25 deletions

View File

@ -7,6 +7,38 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/lang/es/).
---
## [1.5.0] - 2025-11-27
### ✨ Añadido
- **Configuración Avanzada de IPs Administrativas por Segmento**: Sistema completo de gestión de rangos administrativos personalizados por segmento.
- Editor visual protegido por contraseña
- Configuración JSON por segmento
- Múltiples rangos iniciales y finales por segmento
- Interfaz amigable para gestionar rangos
- **Botón de Configuración Avanzada** (⚙️): Acceso rápido al editor desde la interfaz principal.
- **Modal de Autenticación**: Protección por contraseña para acceso al editor.
- **Fallback Inteligente**: Segmentos no configurados en JSON usan automáticamente rangos globales.
### 🔧 Mejorado
- **AdminRangeHelper.php**: Nueva clase helper para procesar configuración JSON por segmento.
- **IpSearchService.php**: Lógica mejorada con soporte para rangos personalizados y fallback.
- **Validación de Cambios**: Advertencia al salir del editor con cambios sin guardar.
### 🎨 Interfaz
- Editor visual completo con formularios dinámicos
- Tabla de segmentos configurados
- Agregar/eliminar rangos en tiempo real
- Diseño responsive con soporte para temas claro/oscuro
## [1.4.0] - 2025-11-27
### ✨ Añadido
- **Rangos de IPs Administrativas Configurables**: Ahora puedes configurar qué rangos de IPs se consideran administrativas mediante 4 campos numéricos en la configuración del plugin (inicio/fin de rango inicial y final), en lugar de tener valores fijos (1-30 y 254).
### 🔧 Mejorado
- **Flexibilidad**: Permite adaptar la clasificación de IPs según las necesidades específicas de cada red.
- **Backend y Frontend**: Toda la lógica de clasificación ahora usa configuración dinámica.
## [1.3.3] - 2025-11-27
### ✨ Añadido

View File

@ -1,6 +1,6 @@
# SIIP - Buscador de IP's Disponibles UISP
[![Version](https://img.shields.io/badge/version-1.3.3-blue.svg)](manifest.json)
[![Version](https://img.shields.io/badge/version-1.5.0-blue.svg)](manifest.json)
[![UCRM](https://img.shields.io/badge/UCRM-Compatible-green.svg)](https://uisp.com/)
[![UNMS](https://img.shields.io/badge/UNMS-Compatible-green.svg)](https://uisp.com/)
@ -93,6 +93,13 @@ Después de instalar el plugin, configura los siguientes parámetros:
|-----------|-------------|-----|
| **Token de la API UNMS** | Token de UNMS (34 caracteres) | Para búsqueda de IPs en dispositivos de red |
| **Habilitar verificación por Ping** | Activa modo de verificación por ping | Permite verificar disponibilidad real de IPs |
| **Inicio primer rango admin** | Primera IP del rango administrativo inicial (default: 1) | Define el inicio del primer rango de IPs administrativas |
| **Fin primer rango admin** | Última IP del rango administrativo inicial (default: 30) | Define el fin del primer rango de IPs administrativas |
| **Inicio rango admin final** | Primera IP del rango administrativo final (default: 254) | Define el inicio del rango final de IPs administrativas |
| **Fin rango admin final** | Última IP del rango administrativo final (default: 254) | Define el fin del rango final de IPs administrativas |
| **Usar rangos personalizados por segmento** | Habilita configuración avanzada por segmento | Permite definir rangos específicos para cada segmento de red |
| **Configuración JSON de rangos** | JSON con rangos por segmento | Configuración detallada de rangos administrativos personalizados |
| **Contraseña de administrador** | Contraseña para editor avanzado | Protege el acceso al editor de configuración avanzada |
| **Debug Mode** | Modo de depuración | Habilita logs más detallados |
| **Enable debug logs** | Logs verbosos | Información adicional en logs |
@ -600,5 +607,5 @@ Este plugin es propiedad de **SIIP Internet**. Todos los derechos reservados.
---
**Versión**: 1.3.3
**Versión**: 1.5.0
**Última actualización**: 27 de noviembre de 2025

View File

@ -1 +1 @@
{"ipserver":"sistema.siip.mx","apitoken":"393eb3d0-9b46-4a47-b9b4-473e4e24a89c","unmsApiToken":"393eb3d0-9b46-4a47-b9b4-473e4e24a89c","debugMode":true,"logging_level":true,"enablePingVerification":true}
{"ipserver":"sistema.siip.mx","apitoken":"393eb3d0-9b46-4a47-b9b4-473e4e24a89c","unmsApiToken":"393eb3d0-9b46-4a47-b9b4-473e4e24a89c","debugMode":true,"logging_level":true,"enablePingVerification":true,"adminRangeStart":"2","adminRangeEnd":"50","adminRangeFinalStart":"254","adminRangeFinalEnd":"255"}

View File

@ -3581,3 +3581,168 @@ Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: GET
POST data: []
GET data: []
Content-Type:
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
Acceso a la interfaz pública de búsqueda de IPs
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"search","segment":"13","verify_ping":"false"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundaryd9c9bd779b06b9bab23d3ccee85be660
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de búsqueda AJAX
Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true}
Buscando IPs en segmento: 13 (Búsqueda inicial rápida)
URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true
Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true
Respuesta HTTP: 200
Longitud de respuesta: 48923 bytes
IPs obtenidas exitosamente: 3106 direcciones
Búsqueda de IPs en segmento 172.16.13.x - Disponibles: 153, En uso: 101
Resultado de búsqueda: {"success":true,"ipsDisponibles":153,"ipsEnUso":101}
<<< Finalizando handler de búsqueda AJAX
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.13.31\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundary6eaecbebd5f8b555bb94571735b05c99
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.13.33\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundaryf35d820ad41ba3e6d10728762ee63d9
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.13.34\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundaryee78e7f8a9ba81f13ea3e610be045399
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.13.35\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundary8e80bfbb0ef83ea63233d967489cc33c
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.13.36\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundary59280d458af805620af084daa0eaea4
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: GET
POST data: []
GET data: []
Content-Type:
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
Acceso a la interfaz pública de búsqueda de IPs
=== NUEVA PETICIÓN ===
Método: GET
POST data: []
GET data: []
Content-Type:
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
Acceso a la interfaz pública de búsqueda de IPs
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"search","segment":"100","verify_ping":"false"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundary5ddd3ee57a052d31763847c17daef50
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de búsqueda AJAX
Configuración cargada: {"ipserver":"sistema.siip.mx","hasUnmsToken":true,"hasApiToken":true}
Buscando IPs en segmento: 100 (Búsqueda inicial rápida)
URL de API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true
Iniciando conexión a API: https://sistema.siip.mx/nms/api/v2.1/devices/ips?suspended=false&management=true&includeObsolete=true
Respuesta HTTP: 200
Longitud de respuesta: 48923 bytes
IPs obtenidas exitosamente: 3106 direcciones
Búsqueda de IPs en segmento 172.16.100.x - Disponibles: 123, En uso: 131
Resultado de búsqueda: {"success":true,"ipsDisponibles":123,"ipsEnUso":131}
<<< Finalizando handler de búsqueda AJAX
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.100.55\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundary8344b24563663e659e7e20943555f60
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.100.57\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundary4d513f2e427d730558bc8fe000c66683
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.100.58\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundaryfeb931394f33d483c38cb8d49c83bab1
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.12s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.100.59\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundarye70755c6fad06c208f6ad0d8651cb811
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.12s)
=== NUEVA PETICIÓN ===
Método: POST
POST data: {"action":"verify_batch","ips":"[\"172.16.100.69\"]"}
GET data: []
Content-Type: multipart/form-data; boundary=----geckoformboundaryc8832b02f5f8eb4a670c6cc46e34ce90
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0
>>> Entrando al handler de verificación por lotes (verify_batch)
Verificando lote de 1 IPs
Iniciando verificación por ping de 1 IPs (lotes de 1)
Procesando lote 1/1 (1 IPs)
Lote completado: 0/1 IPs responden (1.10s)

View File

@ -5,7 +5,7 @@
"displayName": "SIIP - Buscador de IP's Disponibles UISP",
"description": "Este plugin permite buscar IP's disponibles en UISP (UNMS) y asignarlas a los clientes en UCRM. Evitando así la asignación de IP's duplicadas y mejorando la gestión de direcciones IP en la red.",
"url": "https://siip.mx",
"version": "1.3.3",
"version": "1.5.0",
"ucrmVersionCompliancy": {
"min": "1.0.0",
"max": null
@ -58,6 +58,60 @@
"description": "Permite verificar la disponibilidad real de las IPs mediante ping. Cuando está habilitado, el usuario puede elegir si desea verificar las IPs con ping antes de mostrarlas como disponibles. Esto ayuda a detectar dispositivos no registrados en UISP.",
"required": 0,
"type": "checkbox"
},
{
"key": "adminRangeStart",
"label": "Inicio primer rango admin",
"description": "Primera IP del rango administrativo inicial (ej: 1). Las IPs en este rango se marcarán como administrativas.",
"required": 0,
"type": "number",
"default": 1
},
{
"key": "adminRangeEnd",
"label": "Fin primer rango admin",
"description": "Última IP del rango administrativo inicial (ej: 30). Las IPs desde 'Inicio primer rango' hasta este valor se marcarán como administrativas.",
"required": 0,
"type": "number",
"default": 30
},
{
"key": "adminRangeFinalStart",
"label": "Inicio rango admin final",
"description": "Primera IP del rango administrativo final (ej: 254). Útil para reservar las últimas IPs del segmento.",
"required": 0,
"type": "number",
"default": 254
},
{
"key": "adminRangeFinalEnd",
"label": "Fin rango admin final",
"description": "Última IP del rango administrativo final (ej: 254). Las IPs desde 'Inicio rango final' hasta este valor se marcarán como administrativas.",
"required": 0,
"type": "number",
"default": 254
},
{
"key": "useCustomAdminRanges",
"label": "Usar rangos de IPs administrativas personalizados por segmento",
"description": "Habilita configuración personalizada por segmento en lugar de rangos globales. Permite definir rangos específicos para cada segmento de red.",
"required": 0,
"type": "checkbox"
},
{
"key": "customAdminRangesJson",
"label": "Configuración JSON de rangos administrativos",
"description": "JSON con rangos administrativos por segmento. Formato: {\"segmentos\": [{\"segmento\": \"18\", \"administrativas_iniciales\": [{\"inicio\": 1, \"hasta\": 50}], \"administrativas_finales\": [{\"inicio\": 253, \"hasta\": 255}]}]}",
"required": 1,
"type": "textarea",
"default": "{\"segmentos\":[]}"
},
{
"key": "adminPassword",
"label": "Contraseña de administrador para configuración avanzada",
"description": "Contraseña requerida para acceder al editor visual de configuración avanzada de rangos administrativos.",
"required": 1,
"type": "password"
}
],
"menu": [

View File

@ -30,12 +30,43 @@ require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/IpSearchService.php';
require_once __DIR__ . '/src/PingService.php';
require_once __DIR__ . '/src/ApiHandlers.php';
require_once __DIR__ . '/src/AdminRangeHelper.php';
use Ubnt\UcrmPluginSdk\Service\PluginLogManager;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use SiipAvailableIps\IpSearchService;
use SiipAvailableIps\PingService;
// Manejo de errores para el handler de configuración avanzada
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Handler para guardar configuración avanzada
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_advanced_config'])) {
try {
$jsonData = $_POST['save_advanced_config'];
// Validar JSON
$validation = \SiipAvailableIps\AdminRangeHelper::validateConfigJson($jsonData);
if (!$validation['valid']) {
die('Error: ' . $validation['error']);
}
// Guardar en config.json
$configManager = PluginConfigManager::create();
$config = $configManager->loadConfig();
$config['customAdminRangesJson'] = $jsonData;
$configManager->saveConfig($config);
// Redirigir con mensaje de éxito
header('Location: ?success=1');
exit;
} catch (Exception $e) {
die('Error al guardar configuración: ' . $e->getMessage());
}
}
// Get UCRM log manager
$log = PluginLogManager::create();
@ -843,18 +874,313 @@ $pingEnabled = !empty($config['enablePingVerification']) && ($config['enablePing
right: 15px;
}
}
/* Botón de configuración avanzada */
.config-toggle {
position: fixed;
top: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--card-bg);
border: 2px solid var(--card-border);
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: var(--shadow-lg);
z-index: 1001;
}
.config-toggle:hover {
transform: scale(1.1) rotate(90deg);
box-shadow: var(--shadow-xl);
}
/* Ajustar posición del botón de tema */
.theme-toggle {
top: 80px; /* Debajo del botón de configuración */
}
/* Modal de contraseña */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 2000;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 16px;
padding: 40px;
max-width: 400px;
width: 90%;
box-shadow: var(--shadow-xl);
text-align: center;
}
.modal-content h3 {
margin-bottom: 10px;
color: var(--text-primary);
}
.modal-content p {
color: var(--text-secondary);
margin-bottom: 20px;
}
.modal-input {
width: 100%;
padding: 12px;
border: 2px solid var(--card-border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
font-size: 1rem;
margin-bottom: 20px;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: center;
}
/* Editor Visual */
.advanced-editor {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--dark-bg);
z-index: 1500;
overflow-y: auto;
padding: 20px;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
}
.editor-header h2 {
margin: 0;
color: var(--text-primary);
}
.editor-content {
max-width: 1200px;
margin: 0 auto;
}
.add-segment-form {
padding: 20px 0;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.range-input-group {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.range-input-group input {
width: 100px;
padding: 8px;
border: 2px solid var(--card-border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
}
.range-input-group span {
color: var(--text-secondary);
}
.btn-small {
padding: 6px 12px;
font-size: 0.85rem;
background: rgba(102, 126, 234, 0.2);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
color: #667eea;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-small:hover {
background: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.editor-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
@media (max-width: 768px) {
.config-toggle {
width: 45px;
height: 45px;
font-size: 1.3rem;
top: 15px;
right: 15px;
}
.theme-toggle {
top: 70px;
}
.modal-content {
padding: 30px 20px;
}
.editor-header {
flex-direction: column;
gap: 15px;
}
.form-row {
flex-direction: column;
}
.range-input-group {
flex-wrap: wrap;
}
}
</style>
</head>
<body data-theme="dark">
<!-- Botón de configuración avanzada -->
<button id="advancedConfigBtn" class="config-toggle" title="Configuración avanzada">
<span>⚙️</span>
</button>
<!-- Botón de cambio de tema -->
<button id="themeToggle" class="theme-toggle" title="Cambiar tema">
<span id="themeIcon">🌙</span>
</button>
<!-- Modal de contraseña -->
<div id="passwordModal" class="modal" style="display: none;">
<div class="modal-content">
<h3>🔒 Acceso Restringido</h3>
<p>Ingresa la contraseña de administrador para acceder a la configuración avanzada</p>
<input type="password" id="adminPasswordInput" class="modal-input" placeholder="Contraseña" />
<div class="modal-actions">
<button class="btn btn-primary" onclick="validatePassword()">Acceder</button>
<button class="btn" onclick="closePasswordModal()">Cancelar</button>
</div>
</div>
</div>
<!-- Editor Visual de Configuración Avanzada -->
<div id="advancedEditor" class="advanced-editor" style="display: none;">
<div class="editor-header">
<h2>⚙️ Editor de Rangos Administrativos por Segmento</h2>
<button class="btn" onclick="returnToMain()"> Volver</button>
</div>
<div class="editor-content">
<!-- Formulario para agregar segmento -->
<div class="card">
<h3>Agregar Nuevo Segmento</h3>
<div class="add-segment-form">
<div class="form-row">
<div class="input-group">
<label>Segmento</label>
<input type="number" id="newSegment" placeholder="ej: 18" min="0" max="255" />
</div>
</div>
<h4>Rangos Iniciales (IPs al inicio del segmento)</h4>
<div id="initialRangesContainer">
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="addInitialRange()">+ Agregar Rango</button>
</div>
</div>
<h4>Rangos Finales (IPs al final del segmento)</h4>
<div id="finalRangesContainer">
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="addFinalRange()">+ Agregar Rango</button>
</div>
</div>
<button class="btn btn-primary" onclick="addSegmentToTable()"> Agregar Segmento</button>
</div>
</div>
<!-- Tabla de segmentos configurados -->
<div class="card">
<h3>Segmentos Configurados</h3>
<div class="table-container">
<table>
<thead>
<tr>
<th>Segmento</th>
<th>Rangos Iniciales</th>
<th>Rangos Finales</th>
<th>Acciones</th>
</tr>
</thead>
<tbody id="segmentsTableBody">
<!-- Filas dinámicas -->
</tbody>
</table>
</div>
</div>
<div class="editor-actions">
<button class="btn btn-primary" onclick="saveConfiguration()">💾 Guardar Configuración</button>
<button class="btn" onclick="returnToMain()">Cancelar</button>
</div>
</div>
</div>
<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>
<?php if (isset($_GET['success']) && $_GET['success'] == '1'): ?>
<div style="background: rgba(79, 209, 197, 0.2); border: 1px solid rgba(79, 209, 197, 0.4); color: #4fd1c5; padding: 15px; border-radius: 12px; margin-top: 20px; text-align: center;">
Configuración avanzada guardada exitosamente
</div>
<?php endif; ?>
</div>
<div class="card">
@ -957,6 +1283,14 @@ $pingEnabled = !empty($config['enablePingVerification']) && ($config['enablePing
</div>
<script>
// Configuración de rangos de IPs administrativas
const adminConfig = {
rangeStart: <?php echo isset($config['adminRangeStart']) ? (int)$config['adminRangeStart'] : 1; ?>,
rangeEnd: <?php echo isset($config['adminRangeEnd']) ? (int)$config['adminRangeEnd'] : 30; ?>,
finalStart: <?php echo isset($config['adminRangeFinalStart']) ? (int)$config['adminRangeFinalStart'] : 254; ?>,
finalEnd: <?php echo isset($config['adminRangeFinalEnd']) ? (int)$config['adminRangeFinalEnd'] : 254; ?>
};
const searchForm = document.getElementById('searchForm');
const searchBtn = document.getElementById('searchBtn');
const loading = document.getElementById('loading');
@ -969,6 +1303,249 @@ $pingEnabled = !empty($config['enablePingVerification']) && ($config['enablePing
const cancelBtn = document.getElementById('cancelBtn');
let verificationCancelled = false;
// ========== ADVANCED CONFIGURATION EDITOR ==========
// Variables del editor
let editorData = { segmentos: [] };
let hasUnsavedChanges = false;
const adminPassword = '<?php echo addslashes($config['adminPassword'] ?? ''); ?>';
const customRangesJson = '<?php echo addslashes($config['customAdminRangesJson'] ?? '{"segmentos":[]}'); ?>';
// Event listener para botón de configuración avanzada
document.getElementById('advancedConfigBtn').addEventListener('click', openAdvancedConfig);
function openAdvancedConfig() {
document.getElementById('passwordModal').style.display = 'flex';
document.getElementById('adminPasswordInput').value = '';
document.getElementById('adminPasswordInput').focus();
}
function closePasswordModal() {
document.getElementById('passwordModal').style.display = 'none';
}
function validatePassword() {
const inputPassword = document.getElementById('adminPasswordInput').value;
if (inputPassword === adminPassword) {
closePasswordModal();
showEditor();
} else {
alert('❌ Contraseña incorrecta');
document.getElementById('adminPasswordInput').value = '';
document.getElementById('adminPasswordInput').focus();
}
}
// Permitir Enter en el campo de contraseña
document.getElementById('adminPasswordInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
validatePassword();
}
});
function showEditor() {
document.querySelector('.container').style.display = 'none';
document.getElementById('advancedEditor').style.display = 'block';
loadEditorData();
hasUnsavedChanges = false;
}
function returnToMain() {
if (hasUnsavedChanges) {
if (!confirm('⚠️ Tienes cambios sin guardar. ¿Deseas salir de todos modos?')) {
return;
}
}
document.getElementById('advancedEditor').style.display = 'none';
document.querySelector('.container').style.display = 'block';
hasUnsavedChanges = false;
}
function loadEditorData() {
try {
editorData = JSON.parse(customRangesJson);
if (!editorData.segmentos) {
editorData = { segmentos: [] };
}
} catch (e) {
console.error('Error parsing JSON:', e);
editorData = { segmentos: [] };
}
renderSegmentsTable();
}
function renderSegmentsTable() {
const tbody = document.getElementById('segmentsTableBody');
tbody.innerHTML = '';
if (editorData.segmentos.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: var(--text-secondary);">No hay segmentos configurados</td></tr>';
return;
}
editorData.segmentos.forEach((seg, index) => {
const row = document.createElement('tr');
// Formatear rangos iniciales
const iniciales = seg.administrativas_iniciales.map(r => `${r.inicio}-${r.hasta}`).join(', ');
// Formatear rangos finales
const finales = seg.administrativas_finales.map(r => `${r.inicio}-${r.hasta}`).join(', ');
row.innerHTML = `
<td><strong>172.16.${seg.segmento}.x</strong></td>
<td>${iniciales || '-'}</td>
<td>${finales || '-'}</td>
<td>
<button class="btn-small" onclick="deleteSegment(${index})" style="background: rgba(245, 87, 108, 0.2); color: #ff6b6b; border-color: rgba(245, 87, 108, 0.3);">
🗑️ Eliminar
</button>
</td>
`;
tbody.appendChild(row);
});
}
function addInitialRange() {
const container = document.getElementById('initialRangesContainer');
const newRange = document.createElement('div');
newRange.className = 'range-input-group';
newRange.innerHTML = `
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="this.parentElement.remove()" style="background: rgba(245, 87, 108, 0.2); color: #ff6b6b;">
Quitar
</button>
`;
container.appendChild(newRange);
}
function addFinalRange() {
const container = document.getElementById('finalRangesContainer');
const newRange = document.createElement('div');
newRange.className = 'range-input-group';
newRange.innerHTML = `
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="this.parentElement.remove()" style="background: rgba(245, 87, 108, 0.2); color: #ff6b6b;">
Quitar
</button>
`;
container.appendChild(newRange);
}
function addSegmentToTable() {
const segmentInput = document.getElementById('newSegment');
const segment = segmentInput.value;
if (!segment || segment < 0 || segment > 255) {
alert('❌ Por favor ingresa un segmento válido (0-255)');
return;
}
// Verificar si el segmento ya existe
if (editorData.segmentos.some(s => s.segmento == segment)) {
alert('⚠️ Este segmento ya está configurado');
return;
}
// Obtener rangos iniciales
const initialRanges = [];
const initialContainer = document.getElementById('initialRangesContainer');
const initialGroups = initialContainer.querySelectorAll('.range-input-group');
initialGroups.forEach(group => {
const start = parseInt(group.querySelector('.range-start').value);
const end = parseInt(group.querySelector('.range-end').value);
if (start && end && start <= end) {
initialRanges.push({ inicio: start, hasta: end });
}
});
// Obtener rangos finales
const finalRanges = [];
const finalContainer = document.getElementById('finalRangesContainer');
const finalGroups = finalContainer.querySelectorAll('.range-input-group');
finalGroups.forEach(group => {
const start = parseInt(group.querySelector('.range-start').value);
const end = parseInt(group.querySelector('.range-end').value);
if (start && end && start <= end) {
finalRanges.push({ inicio: start, hasta: end });
}
});
// Agregar segmento
editorData.segmentos.push({
segmento: segment,
administrativas_iniciales: initialRanges,
administrativas_finales: finalRanges
});
// Limpiar formulario
segmentInput.value = '';
initialContainer.innerHTML = `
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="addInitialRange()">+ Agregar Rango</button>
</div>
`;
finalContainer.innerHTML = `
<div class="range-input-group">
<input type="number" class="range-start" placeholder="Desde" min="1" max="254" />
<span>hasta</span>
<input type="number" class="range-end" placeholder="Hasta" min="1" max="254" />
<button class="btn-small" onclick="addFinalRange()">+ Agregar Rango</button>
</div>
`;
hasUnsavedChanges = true;
renderSegmentsTable();
alert('✅ Segmento agregado correctamente');
}
function deleteSegment(index) {
if (confirm('¿Estás seguro de eliminar este segmento?')) {
editorData.segmentos.splice(index, 1);
hasUnsavedChanges = true;
renderSegmentsTable();
}
}
function saveConfiguration() {
if (!confirm('¿Deseas guardar la configuración? Esto actualizará el archivo de configuración del plugin.')) {
return;
}
const jsonData = JSON.stringify(editorData);
// Crear formulario para enviar datos
const form = document.createElement('form');
form.method = 'POST';
form.action = '';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'save_advanced_config';
input.value = jsonData;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
// ========== FIN ADVANCED CONFIGURATION EDITOR ==========
// Theme toggle functionality
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
@ -1202,12 +1779,14 @@ $pingEnabled = !empty($config['enablePingVerification']) && ($config['enablePing
// Función para clasificar el tipo de IP
function getIpType(ip) {
const parts = ip.split('.');
const lastOctet = parseInt(parts[3]);
const lastOctet = parseInt(ip.split('.')[3]);
if ((lastOctet >= 1 && lastOctet <= 30) || lastOctet === 254) {
// Verificar si está en alguno de los rangos administrativos configurados
if ((lastOctet >= adminConfig.rangeStart && lastOctet <= adminConfig.rangeEnd) ||
(lastOctet >= adminConfig.finalStart && lastOctet <= adminConfig.finalEnd)) {
return 'Administración';
}
return 'Válida para cliente';
}

130
src/AdminRangeHelper.php Normal file
View File

@ -0,0 +1,130 @@
<?php
namespace SiipAvailableIps;
/**
* Helper class para manejar rangos de IPs administrativas personalizados por segmento
*/
class AdminRangeHelper
{
/**
* Obtiene los límites de IPs administrativas para un segmento específico
*
* @param string|int $segmentNumber Número del segmento (ej: 18 para 172.16.18.x)
* @param string $jsonData JSON con configuración de rangos
* @return array|null Array con límites o null si el segmento no existe
*/
public static function getSegmentLimits($segmentNumber, $jsonData)
{
if (empty($jsonData)) {
return null;
}
$data = json_decode($jsonData, true);
if (!isset($data['segmentos']) || !is_array($data['segmentos'])) {
return null;
}
foreach ($data['segmentos'] as $segmento) {
if ($segmento['segmento'] == $segmentNumber) {
// Última IP del bloque inicial
$ultimoInicial = 0;
if (isset($segmento['administrativas_iniciales']) && is_array($segmento['administrativas_iniciales'])) {
foreach ($segmento['administrativas_iniciales'] as $rango) {
if (isset($rango['hasta']) && $rango['hasta'] > $ultimoInicial) {
$ultimoInicial = $rango['hasta'];
}
}
}
// Primera IP del bloque final
$primerFinal = 999;
if (isset($segmento['administrativas_finales']) && is_array($segmento['administrativas_finales'])) {
foreach ($segmento['administrativas_finales'] as $rango) {
if (isset($rango['inicio']) && $rango['inicio'] < $primerFinal) {
$primerFinal = $rango['inicio'];
}
}
}
return [
'ultimo_inicial_reservado' => $ultimoInicial,
'primer_final_reservado' => $primerFinal,
'rangos_iniciales' => $segmento['administrativas_iniciales'] ?? [],
'rangos_finales' => $segmento['administrativas_finales'] ?? []
];
}
}
return null;
}
/**
* Verifica si una IP es administrativa según configuración JSON personalizada
*
* @param string $ip Dirección IP completa (ej: 172.16.18.45)
* @param string $jsonData JSON con configuración de rangos
* @return bool True si es IP administrativa
*/
public static function isAdminIpCustom($ip, $jsonData)
{
$parts = explode('.', $ip);
if (count($parts) !== 4) {
return false;
}
$segment = $parts[2];
$lastOctet = (int)$parts[3];
$limits = self::getSegmentLimits($segment, $jsonData);
if (!$limits) {
return false;
}
// Verificar rangos iniciales
if (isset($limits['rangos_iniciales']) && is_array($limits['rangos_iniciales'])) {
foreach ($limits['rangos_iniciales'] as $rango) {
if ($lastOctet >= $rango['inicio'] && $lastOctet <= $rango['hasta']) {
return true;
}
}
}
// Verificar rangos finales
if (isset($limits['rangos_finales']) && is_array($limits['rangos_finales'])) {
foreach ($limits['rangos_finales'] as $rango) {
if ($lastOctet >= $rango['inicio'] && $lastOctet <= $rango['hasta']) {
return true;
}
}
}
return false;
}
/**
* Valida el formato del JSON de configuración
*
* @param string $jsonData JSON a validar
* @return array Array con 'valid' (bool) y 'error' (string|null)
*/
public static function validateConfigJson($jsonData)
{
if (empty($jsonData)) {
return ['valid' => false, 'error' => 'JSON vacío'];
}
$data = json_decode($jsonData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ['valid' => false, 'error' => 'JSON inválido: ' . json_last_error_msg()];
}
if (!isset($data['segmentos']) || !is_array($data['segmentos'])) {
return ['valid' => false, 'error' => 'Falta el array "segmentos"'];
}
return ['valid' => true, 'error' => null];
}
}

View File

@ -102,9 +102,9 @@ function handleIpRequest($data, $log) {
// Formatear respuesta para API
if ($resultado['success']) {
// Filtrar IPs administrativas - Solo devolver IPs aptas para clientes (31-253)
$clientAvailableIps = array_filter($resultado['data'], function($ip) {
return !\SiipAvailableIps\IpSearchService::isAdminIp($ip);
// Filtrar IPs administrativas - Solo devolver IPs aptas para clientes
$clientAvailableIps = array_filter($resultado['data'], function($ip) use ($config) {
return !\SiipAvailableIps\IpSearchService::isAdminIp($ip, $config);
});
$clientAvailableIps = array_values($clientAvailableIps); // Reindexar array
@ -138,8 +138,8 @@ function handleIpRequest($data, $log) {
// Filtrar IPs que responden de las IPs de cliente
if (isset($resultado['ping_responding'])) {
$clientPingResponding = array_filter($resultado['ping_responding'], function($ip) {
return !\SiipAvailableIps\IpSearchService::isAdminIp($ip);
$clientPingResponding = array_filter($resultado['ping_responding'], function($ip) use ($config) {
return !\SiipAvailableIps\IpSearchService::isAdminIp($ip, $config);
});
$response['ping_responding'] = array_values($clientPingResponding);
}
@ -210,7 +210,7 @@ function handleIpCheck($data, $log) {
if ($resultado['success']) {
$isAvailable = in_array($ipToCheck, $resultado['data']);
$isUsed = in_array($ipToCheck, $resultado['used']);
$ipType = \SiipAvailableIps\IpSearchService::getIpType($ipToCheck);
$ipType = \SiipAvailableIps\IpSearchService::getIpType($ipToCheck, $config);
// Determinar status con información de tipo de IP
$status = 'unknown';

View File

@ -8,13 +8,6 @@ class IpSearchService
private $apiToken;
private $logger;
// Rangos de IPs administrativas (no recomendadas para clientes)
const ADMIN_IP_RANGES = [
'start' => 1,
'end' => 30,
'broadcast' => 254
];
public function __construct($apiUrl, $apiToken, $logger = null)
{
$this->apiUrl = $apiUrl;
@ -26,10 +19,28 @@ class IpSearchService
* Determina si una IP es administrativa (no recomendada para clientes)
*
* @param string $ip Dirección IP completa (ej: 172.16.13.5)
* @param array $config Configuración del plugin con rangos admin
* @return bool True si es IP administrativa
*/
public static function isAdminIp($ip)
public static function isAdminIp($ip, $config = [])
{
// Si está habilitada la configuración personalizada por segmento
if (!empty($config['useCustomAdminRanges']) && !empty($config['customAdminRangesJson'])) {
// Intentar obtener límites del segmento desde JSON
$parts = explode('.', $ip);
if (count($parts) === 4) {
$segment = $parts[2];
$limits = AdminRangeHelper::getSegmentLimits($segment, $config['customAdminRangesJson']);
// Si el segmento está configurado en JSON, usar esos rangos
if ($limits !== null) {
return AdminRangeHelper::isAdminIpCustom($ip, $config['customAdminRangesJson']);
}
// Si el segmento NO está en JSON, usar rangos globales como fallback
}
}
// Usar configuración estándar (rangos globales)
$parts = explode('.', $ip);
if (count($parts) !== 4) {
return false;
@ -37,19 +48,26 @@ class IpSearchService
$lastOctet = (int)$parts[3];
return ($lastOctet >= self::ADMIN_IP_RANGES['start'] && $lastOctet <= self::ADMIN_IP_RANGES['end'])
|| $lastOctet === self::ADMIN_IP_RANGES['broadcast'];
// Obtener rangos de configuración o usar valores por defecto
$rangeStart = isset($config['adminRangeStart']) ? (int)$config['adminRangeStart'] : 1;
$rangeEnd = isset($config['adminRangeEnd']) ? (int)$config['adminRangeEnd'] : 30;
$finalStart = isset($config['adminRangeFinalStart']) ? (int)$config['adminRangeFinalStart'] : 254;
$finalEnd = isset($config['adminRangeFinalEnd']) ? (int)$config['adminRangeFinalEnd'] : 254;
return ($lastOctet >= $rangeStart && $lastOctet <= $rangeEnd)
|| ($lastOctet >= $finalStart && $lastOctet <= $finalEnd);
}
/**
* Obtiene el tipo de IP (administrativa o cliente)
*
* @param string $ip Dirección IP completa
* @param array $config Configuración del plugin con rangos admin
* @return array ['type' => 'admin'|'client', 'label' => string, 'recommended' => bool]
*/
public static function getIpType($ip)
public static function getIpType($ip, $config = [])
{
if (self::isAdminIp($ip)) {
if (self::isAdminIp($ip, $config)) {
return [
'type' => 'admin',
'label' => 'Administración',