Files
LMS-Estafeta/CHANGELOG.md
T

229 lines
15 KiB
Markdown

# CHANGELOG / Registro de cambios — Plataforma LMS Estafeta
> Documento de seguimiento para agentes. Describe **todos los cambios de esta iteración**, el estado actual verificado del repositorio, archivos a subir a producción y pendientes.
>
> **Ruta local trabajada:** `C:\backup\htdocs\gitlab\Estafeta`
> **Ruta de ejecución del usuario (dev):** `C:\xampp\htdocs\git\Estafeta-plataform`
> **Producción:** `https://induccion-estafeta.com.mx/`
> **Rama/base:** sin control de versiones (copia de htdocs).
---
## 0. Contexto y decisiones
1. **Login:** por decisión del negocio se ingresa **solo con número de empleado**. No se implementó verificación de contraseña (omitido a propósito). Aun así se endureció la sesión y se agregó rate-limit.
2. **RTE:** se decide usar **únicamente `runtime/`** y **eliminar `runtimev2/`**.
3. **SCORM:** el RTE central `runtime/sco.php` sirve un contenedor con barra superior + botón "Regresar" e **iframe** que carga el paquete, con el API SCORM en la página padre (así Storyline/Rise lo encuentran subiendo por los frames).
4. Los endpoints de `admin/` (y los del front que devuelven JSON) **siempre responden JSON**; nunca deben emitir HTML de avisos/errores.
---
## 1. Resumen por fases
### Fase 1 — Seguridad base
- **`admin/auth_guard.php` (nuevo):** valida sesión + rol admin; responde `{"code":"403","msg":"No autorizado"}` si no. Fuerza `display_errors=0` y descarta salida previa (buffering) para no contaminar el JSON.
- **Guard añadido** a **113** endpoints `admin/*.php` con `require_once __DIR__.'/auth_guard.php';` (se excluyen `admin/index.php` y `admin/audit.php`, que son apoyo, no endpoints).
- **`data/.htaccess` (nuevo):** `Options -Indexes` + bloqueo de ejecución de scripts (los SCORM son estáticos → evita RCE/XSS por archivos subidos).
- **`runtime/sco.php`:** `display_errors`/`display_startup_errors` en `0`; se quitó `allow_url_include`.
- **Backups eliminados:** `admin/index.php.back`, `admin/usuarios.php.back`, `admin/usuarios.bak.php`, `admin/usuarios.bak.html`, `admin/usuarios_manager.bak.php`, `runtime/getUserDashBoard.php.back`, `runtime/runtime.js.back`, carpeta `admin/backup100322/`.
### Fase 2 — Endurecimiento
- **`config.php`:** endurecimiento de sesión (`session.use_strict_mode`, `use_only_cookies`, `cookie_httponly`, `cookie_samesite=Lax`, `cookie_secure` si HTTPS). Solo aplica si no hay sesión activa.
- **`runtime/newlogin.php`:** reescrito con **consultas preparadas**, `session_regenerate_id(true)` (anti-fijación) y **rate-limit por IP** (60/10 min). Mantiene login por número de empleado.
- **`admin/usuarios_add.php` / `admin/usuarios_upadate.php`:** convertidos a **sentencias preparadas**.
- **CSRF:** token en `admin/index.php` (`<meta name="csrf-token">`), envío global en `js/admin.js` (`X-CSRF-Token` + `window.CSRF_TOKEN`), verificación en `auth_guard.php` para POST/PUT/DELETE, y cabecera `X-CSRF-Token` añadida a todos los uploads Uppy.
- **`runtimev2/api.php`:** corregido bug `=` → `==` en `LMSInitialize` (comparación de `lesson_status`/`score.raw`).
### Fase 3 — Mantenibilidad y calidad
- **N+1 eliminado** en `runtime/getUserDashBoard.php`: los hijos se traen con una sola consulta `WHERE padre IN (...)`.
- **Auditoría:** `admin/audit.php` (nuevo) + `logs/.htaccess` (nuevo). Registra en `logs/admin.log`: fecha, IP, userid, numempl, archivo, acción, detalle.
- **Sentencias preparadas + auditoría** en: `admin/categorias_add.php`, `categorias_upadate.php`, `categorias_delete.php`, `recursos_add.php`, `recursos_update.php`, `recursos_delete.php`, `gamification_add.php`, `gamification_delete.php`. Los `*_delete` sanitizan ids con `intval`.
- **`runtimev2/` eliminada** (sin referencias en el proyecto).
### Correcciones posteriores (vistas admin + dashboard)
- **polyfill.io eliminado** de `footer.php` y `runtime/rte.php` (dominio comprometido en 2024; causaba 404/CORS y rompía DataTables).
- **`JSON.parse` inseguro:** se añadió helper `parseJSON(resp)` (try/catch) y se reemplazaron todos los `JSON.parse(response)` en: `admin/usuarios.html`, `categorias.html`, `asignaciones.html`, `constancias.html`, `encuestas.html`, `evaluaciones.html`, `gamification.html`, `licencia.html`, `recursos.html`.
- **`admin/usuarios.php` y `admin/usuarios_manager.php`:** reescritos como DataTables server-side limpio y **con sentencias preparadas**; se eliminó el cifrado roto de la consulta y la propiedad `"query"` que rompía el `drawCallback` (`Cannot read properties of null (reading 'query')`).
- **`admin/usuarios.html`:** quitadas asignaciones `query = json.query`; reload con `ajax.reload(null, false)`.
- **`admin/usuarios_*_list.php` (8):** `mysqli_fetch_array` → `mysqli_fetch_assoc` (evita claves numéricas duplicadas en el JSON).
- **`admin/exportUsers.php`:** exporta según rol (`roladmin`), sin depender del `query` cifrado; `exportar()` envía `?roladmin=`.
- **`runtime/getUserDashBoard.php`:** blindado como endpoint JSON (`display_errors=0`, `Content-Type: application/json`, valida sesión y responde `{"code":"0","msg":"Sesión no válida"}` en lugar de HTML).
- **`dashboard.php`:** parseo tolerante de la respuesta (try/catch) con mensaje amigable.
- **`config.php`:** `ini_set` de sesión solo si no hay sesión activa (evita warnings).
---
## 2. Archivos NUEVOS
| Archivo | Propósito |
|---|---|
| `admin/auth_guard.php` | Guard de autenticación admin + verificación CSRF |
| `admin/audit.php` | Función `audit_log()` → escribe `logs/admin.log` |
| `admin/recursos_scorm_upload.php` | Subida de `.zip` SCORM: valida, descomprime en `data/scormpkg/<categoria>/` y devuelve la URL de inicio |
| `data/.htaccess` | `Options -Indexes` + bloqueo de ejecución de scripts en `data/` |
| `logs/.htaccess` | Protección de la carpeta de auditoría |
| `README.md` | Documentación general del proyecto (stack, funcionalidades, instalación, seguridad) |
| `CHANGELOG.md` | Este documento |
---
## 3. Archivos MODIFICADOS (por área)
### Configuración
- `config.php` — hardening de sesión (condicional).
### Runtime / RTE
- `runtime/gotorte.php` — lanzador central; carga `runtime/sco.php` con `require` (sin redirect) y respaldo al `sco.php` del paquete.
- `runtime/sco.php` — contenedor con barra + botón Regresar, API en el padre, iframe con cache-busting, cierre de BD, `display_errors=0`.
- `runtime/commit.php` — `mysqli_close($link)`.
- `runtime/finish.php` — `mysqli_close($link)`.
- `runtime/runtime.js` — `pagehide` en lugar de `window.onunload`.
- `runtime/getUserDashBoard.php` — N+1 resuelto + endpoint JSON blindado.
- `runtime/newlogin.php` — consultas preparadas + anti-fijación + rate-limit.
- `runtime/rte.php` — removido polyfill.io.
### Front (alumno)
- `dashboard.php` — scroll/resaltado de categoría al volver (`?idcat=`), parseo tolerante del dashboard.
- `recursos.php` — botón "Regresa al menú anterior" → `dashboard.php?idcat=<cat>`.
- `footer.php` — removido polyfill.io.
### Admin — endpoints
- `admin/usuarios.php`, `admin/usuarios_manager.php` — server-side limpio + preparadas.
- `admin/usuarios_add.php`, `admin/usuarios_upadate.php` — preparadas + auditoría.
- `admin/categorias_add.php`, `admin/categorias_upadate.php`, `admin/categorias_delete.php` — preparadas + auditoría.
- `admin/recursos_add.php`, `admin/recursos_update.php`, `admin/recursos_delete.php` — preparadas + auditoría.
- `admin/gamification_add.php`, `admin/gamification_delete.php` — preparadas + auditoría.
- `admin/exportUsers.php` — export por rol sin query cifrado.
- `admin/usuarios_{centros,distritos,posiciones,sociedades,divisiones,unidades,ceco,supervisores}_list.php` — `fetch_assoc`.
- `admin/categorias.php` — normalización de imagen (`assetUrl` inline) + `'Raíz'`.
- `admin/usuarios.php`/`recursos.php` (endpoints de tabla) — columnas/normalizaciones.
- **113** `admin/*.php` — línea del guard añadida.
### Admin — vistas (HTML)
- `admin/usuarios.html`, `categorias.html`, `asignaciones.html`, `constancias.html`, `encuestas.html`, `evaluaciones.html`, `gamification.html`, `licencia.html`, `recursos.html` — helper `parseJSON`, CSRF en Uppy, preselección por id en selects.
- `admin/index.php` — meta CSRF.
### JS
- `js/admin.js` — `window.CSRF_TOKEN` + `$.ajaxSetup({ headers: { 'X-CSRF-Token': csrf } })`.
### Data (paquete de ejemplo)
- `data/scormpkg/Premio_Nacional/index.html` — busca API subiendo al padre y redirige a Genially.
- `data/scormpkg/Premio_Nacional/sco.php`, `imsmanifest.xml`, XSDs — paquete creado.
---
## 4. Archivos ELIMINADOS
- Carpeta `runtimev2/` (completa).
- `admin/index.php.back`, `admin/usuarios.php.back`, `admin/usuarios.bak.php`, `admin/usuarios.bak.html`, `admin/usuarios_manager.bak.php`.
- `runtime/getUserDashBoard.php.back`, `runtime/runtime.js.back`.
- Carpeta `admin/backup100322/`.
---
## 5. Estado verificado del repositorio
- `runtimev2/` → **no existe**. Sin referencias en el proyecto.
- `admin/auth_guard.php`, `admin/audit.php`, `data/.htaccess`, `logs/.htaccess`, `README.md` → **existen**.
- Endpoints `admin/*.php` con guard: **113**. Sin guard: solo `audit.php` (helper) e `index.php` (layout).
- No quedan archivos `*.back`/`*.bak` (fuera de `vendor/` y `data/`).
- `php -l` sin errores en los archivos tocados.
- Pruebas con BD temporal (`estafeta_lms2`) realizadas y **eliminadas** al terminar: login, dashboard (con/sin sesión), usuarios server-side, listas, CRUD categorías/recursos con auditoría.
---
## 6. Instrucciones para subir a producción (resumen)
**Obligatorio subir:**
1. `config.php`, `footer.php`, `dashboard.php`, `recursos.php`
2. `runtime/` completo (gotorte, sco, commit, finish, runtime.js, getUserDashBoard, newlogin, rte)
3. `admin/` completo (incluye `auth_guard.php`, `audit.php`, `recursos_scorm_upload.php`, todos los `.php` con guard y todos los `.html`)
4. `js/admin.js`
5. `data/.htaccess`
6. `logs/` (carpeta con `.htaccess`)
7. `data/scormpkg/Premio_Nacional/`
**Requisitos del servidor:**
- Permisos de escritura en `data/`, `data/scormpkg/`, `logs/`.
- `ZipArchive` habilitado en PHP.
- `upload_max_filesize` / `post_max_size` acordes al tamaño de los SCORM.
**NO subir / ya no aplica:**
- `runtimev2/` (eliminada).
- `config.php` ya no contiene `assetUrl()` (se quitó).
---
## 7. Pendientes / recomendaciones (no bloqueantes)
1. **Credenciales:** mover `config.php` a variables de entorno y **rotar** la clave `wwserv_estafeta` (aparece comentada en el archivo). Hoy sigue en texto plano.
2. **SQL restante:** migrar a sentencias preparadas los endpoints de `constancias`, `encuestas`, `evaluaciones`, `licencia` y otros CRUD que aún interpolan SQL.
3. **Contraseñas:** la columna `password` usa `getEncrypt()` (no-op). Solo hashear si algún día se habilita login con contraseña.
4. **`session.save_path`** del `.htaccess` apunta a `ea-php73`; revisar con el PHP real en producción.
5. **polyfill.io dentro de `data/scormpkg/**`:** los paquetes antiguos aún lo referencian, pero no se usan (los sirve el RTE central). Limpiar opcionalmente.
6. **Tests automatizados** de flujos críticos (login, SCORM, alta de recursos).
7. **Consolidación de RTE** ya resuelta con la eliminación de `runtimev2/`; validar en producción el lanzamiento de Storyline/Rise/NOM-035.
---
## 8. Notas de troubleshooting
- **`Fatal error: Unknown database 'estafeta_lms2'`** (local): es solo del entorno local (falta la BD). En producción existe `wwserv_estafeta`. Opciones: crear/importar la BD local o hacer `dbConnect()` tolerante.
- **`Unexpected token '<' ... is not valid JSON`:** indica que un endpoint devolvió HTML (error PHP). Con los cambios, los endpoints JSON ya no deben emitir HTML; si reaparece, revisar en DevTools → Network la respuesta del endpoint y confirmar que el archivo subido es el nuevo.
- **`polyfill.io` CORS/404:** dominio comprometido; ya removido del proyecto.
- **Rutas RTE:** `runtime/sco.php` usa rutas raíz (`$_SERVER['DOCUMENT_ROOT']."/sessionManager.php"`). Si el LMS se sirve en subcarpeta, ajustar a `/gitlab/estafeta/...`.
---
## 9. Corrección: vista de Usuarios (500 por columnas faltantes)
**Síntoma:** `POST admin/usuarios.php`, `usuarios_{centros,distritos,posiciones,sociedades,divisiones,unidades,ceco,supervisores}_list.php` → `500 (Internal Server Error)`.
**Causa raíz:** la BD local `estafeta_lms2` conserva el esquema **antiguo** de `user`
(solo `centrotrabajoid` / `distritoid` con FK a `centrotrabajo` / `distrito`).
El código actual lee/escribe columnas **denormalizadas** en `user`
(`centrotrabajonom`, `distritonom`, `posicion`, `sociedad`, `division`,
`unidad_organizativa`, `ceco`, `supervisor`). En PHP 8.1+ `mysqli` lanza
`mysqli_sql_exception` ante `Unknown column`, que antes terminaba en un fatal
sin control (HTTP 500). En PHP 7.4 solo devolvía resultados vacíos.
**Cambios:**
1. `bd/migration_usuarios_org_fields.sql` (nuevo): agrega las columnas faltantes
a `user` (idempotente) y rellena `centrotrabajonom` / `distritonom` desde las
FK `centrotrabajo` / `distrito`. **Aplicar en cada entorno.**
2. `admin/auth_guard.php`: `set_exception_handler` que descarta la salida previa
y responde JSON (`{"code":"500",...}`) en cualquier excepción no controlada,
en lugar de HTML; registra el detalle en `error_log`.
**Verificación local:** los 10 endpoints de usuarios + `usuarios.php` /
`usuarios_manager.php` responden JSON con datos; centros y distritos se
rellenan desde las FK.
**Pendiente del entorno:** completar la BD local (faltan tablas `licencia`,
`evaluaciones`, `encuestas`, `constancias`, etc.); `usuarios_statistical.php`
falla por `Table 'estafeta_lms2.licencia' doesn't exist` (ahora responde JSON 500
en vez de fatal).
---
## 10. Corrección: traducción de DataTables (español)
**Síntoma:** las tablas de `recursos`, `categorias`, `gamification` (y otras del
panel) mostraban la interfaz de DataTables en **inglés** (Search, Show entries,
Previous/Next, "No data available", etc.).
**Causa raíz:** las vistas cargaban el idioma desde un CDN externo
(`https://cdn.datatables.net/plug-ins/.../i18n/Spanish.json`). Cuando el CDN no
está disponible (sin internet, bloqueado o CORS), DataTables cae a los textos por
defecto en inglés. `usuarios.html` ya usaba la copia local y sí se traducía.
**Cambios:** se reemplazó la URL del CDN por la copia local `../json/Spanish.json`
(compatible con DataTables 1.10.20 incluida en `js/datatables.min.js`) en:
- `admin/categorias.html`, `gamification.html`, `recursos.html`, `constancias.html`,
`encuestas.html`, `evaluaciones.html`, `reporte_categoria.html`, `reporte_curso.html`
- `admin/reporte_categoria.php`, `reporte_curso.php`, `reporte_distrito.php`,
`reporte_categoria_completadav2.php`, `reporte_categoria_visitadosv2.php`
**Verificación:** `http://localhost/git/LMS-Estafeta/json/Spanish.json` responde
HTTP 200; ya no quedan referencias a `cdn.datatables.net/plug-ins` en `admin/`.