Files
LMS-Estafeta/CHANGELOG.md
T

15 KiB

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/.