# 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` (``), 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//` 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=`. - `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/`.