Files
whatsapp/core/Router.php
T
Lizandro GuarnizoandClaude Opus 5 645ed677ea Permitir abrir la prueba de voces en el televisor
La página sirve para averiguar qué voces tiene ESE equipo, así que hay que
abrirla en el televisor —donde no hay sesión iniciada, porque es un kiosco—.
Tal como quedó, redirigía al login y era inservible justo donde se necesita.

No expone nada: lista las voces que reporta el navegador y lee una frase de
ejemplo inventada. Ningún dato de paciente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:52:11 -05:00

154 lines
5.2 KiB
PHP

<?php
/**
* core/Router.php
* Enrutador central del ERP.
*
* Mapea la URL entrante a modules/{m}/views/{v}.php.
* Soporta rutas limpias (/turnero/dashboard) y parámetros explícitos (?m=&v=).
*
* RUTAS PÚBLICAS (sin autenticación):
* turnero/display → pantalla TV para sala de espera
* turnero/kiosko → pantalla táctil para tomar turno
*/
class Router
{
/** Módulo por defecto cuando no se especifica ?m= */
private const DEFAULT_MODULE = 'dashboard';
/** Vista por defecto cuando no se especifica ?v= */
private const DEFAULT_VIEW = 'index';
/** Rutas que NO requieren sesión iniciada. Formato: 'modulo/vista' */
private const PUBLIC_ROUTES = [
'turnero/display',
'turnero/kiosko',
// Tablet de firma del paciente: la manipula el público y nadie va a
// iniciar sesión en ella cada mañana. No queda abierta: se identifica
// por la cookie del dispositivo y sin ella no muestra dato alguno.
'turnero/firma',
// Prueba de voces: hay que abrirla EN el televisor para saber qué voces
// tiene ese equipo, y allí no hay sesión iniciada. No expone nada: solo
// lista las voces del navegador y lee una frase de ejemplo inventada.
'turnero/voces',
];
/** Patrón permitido para módulo y vista: solo letras, números y guión bajo */
private const SLUG_PATTERN = '/^[a-zA-Z0-9_]{1,64}$/';
private string $module;
private string $view;
// ─── Constructor ────────────────────────────────────────────────────────
public function __construct()
{
[$this->module, $this->view] = $this->resolveRoute();
}
// ─── API pública ────────────────────────────────────────────────────────
public function getModule(): string { return $this->module; }
public function getView(): string { return $this->view; }
/**
* Indica si la ruta actual es pública (no requiere login).
*/
public function isPublic(): bool
{
return in_array($this->module . '/' . $this->view, self::PUBLIC_ROUTES, true);
}
/**
* Resuelve la ruta y devuelve el archivo de vista correspondiente.
* Lanza una excepción si el archivo no existe.
*
* @return string Ruta absoluta al archivo de vista.
* @throws RuntimeException Si el módulo/vista no se encuentran.
*/
public function resolveFile(): string
{
$file = APP_ROOT . "/modules/{$this->module}/views/{$this->view}.php";
if (!file_exists($file)) {
throw new RuntimeException(
"Vista no encontrada: {$this->module}/{$this->view}",
404
);
}
return $file;
}
// ─── Internos ───────────────────────────────────────────────────────────
/**
* Determina módulo y vista a partir de la URL.
* Prioridad: PATH_INFO limpio → parámetros GET ?m= ?v=
*
* @return array{string, string}
*/
private function resolveRoute(): array
{
// 1. Intentar ruta limpia desde PATH_INFO o REQUEST_URI
// Formato: /modulo/vista → e.g. /turnero/dashboard
$path = $this->cleanPath();
if ($path !== null) {
[$m, $v] = $path;
if ($this->isValidSlug($m) && $this->isValidSlug($v)) {
return [$m, $v];
}
}
// 2. Parámetros GET ?m=module&v=view
$m = $_GET['m'] ?? self::DEFAULT_MODULE;
$v = $_GET['v'] ?? self::DEFAULT_VIEW;
// Sanitizar: solo caracteres seguros
$m = preg_replace('/[^a-zA-Z0-9_]/', '', $m);
$v = preg_replace('/[^a-zA-Z0-9_]/', '', $v);
if (!$this->isValidSlug($m)) $m = self::DEFAULT_MODULE;
if (!$this->isValidSlug($v)) $v = self::DEFAULT_VIEW;
return [$m, $v];
}
/**
* Extrae módulo y vista de la REQUEST_URI limpia.
* Retorna null si la URI no corresponde al formato /m/v.
*
* @return array{string,string}|null
*/
private function cleanPath(): ?array
{
$uri = $_SERVER['REQUEST_URI'] ?? '';
// Eliminar query string
$pos = strpos($uri, '?');
if ($pos !== false) {
$uri = substr($uri, 0, $pos);
}
// Eliminar el subdirectorio base si el proyecto no está en la raíz
$scriptDir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/');
if ($scriptDir !== '' && str_starts_with($uri, $scriptDir)) {
$uri = substr($uri, strlen($scriptDir));
}
// Normalizar y dividir
$parts = array_values(array_filter(explode('/', trim($uri, '/'))));
if (count($parts) >= 2) {
return [$parts[0], $parts[1]];
}
return null;
}
private function isValidSlug(string $slug): bool
{
return (bool) preg_match(self::SLUG_PATTERN, $slug);
}
}