1) Resumen ejecutivo
Este documento define todo lo necesario para que un desarrollador implemente el módulo Manual Editor sin consultas adicionales, respetando el flujo de trabajo y estándares del equipo.
- Lectura y edición en vivo del campo
manual
(LONGTEXT) de la tablacursos_sence
, identificado por$curso_id = cursos_sence.id
. - Interfaz en dos columnas: galería de imágenes arrastrables (izquierda) + editor TinyMCE (derecha).
- Inserción por drag & drop de imágenes en el contenido; actualización persistente del HTML.
- Subida de imágenes con validaciones, límite y optimización automática (Imagick → GD).
- Registro de historial en tabla
manual_historial
previo a cada actualización. - Monitoreo por consola (cliente) y en error_log (servidor).
2) Decisiones confirmadas
- Editor: TinyMCE Community (se dispone de carpeta local:
/manual_editor/tinymce/js/tinymce/
tal como la imagen proporcionada; también se admite CDN si se prefiere). - Raíz del módulo: /manual_editor/
- Carpeta de imágenes (configurable en duro): /manual_editor/img/
- Formatos permitidos:
jpg, jpeg, png, gif
. - Límites: 4 MB por archivo, 2560 px lado mayor (reeescala/optimiza si los excede).
- Rutas de imágenes en contenido: relativas bajo el propio módulo.
- Acceso: “cualquiera puede editar” (no hay control de roles en esta fase).
- Historial: tabla
manual_historial
, sin interfaz de administración por ahora. - CSRF: activado desde el inicio.
type="file"
) que permite escoger archivos o una carpeta (atributo webkitdirectory
/ directory
en navegadores compatibles). El sistema mostrará en la galería las imágenes elegidas por el usuario. Si se requiere recorrido automático del disco, debe desarrollarse una app nativa (p. ej. Electron/desktop) o un helper fuera del navegador.
3) Objetivos funcionales
- Cargar
manual
por$curso_id
y mostrarlo en el editor. - Listar imágenes desde
$carpeta_img
como miniaturas arrastrables. - Permitir subir imágenes (validar, optimizar, guardar, exponer URL relativa).
- Insertar imágenes por drag&drop en el contenido.
- Actualizar el campo
manual
con el HTML resultante (texto + <img>). - Crear registro en
manual_historial
previo a la actualización. - Mostrar notificaciones y logs claros de cada acción.
4) Arquitectura de archivos
Todos los archivos residen bajo /manual_editor/:
Ruta | Rol | Notas |
---|---|---|
/manual_editor/manual_editor.php | Vista principal (UI 2 columnas). Orquesta solicitudes AJAX y TinyMCE. | Incluye configuración en duro y recursos (jQuery, Bootstrap, TinyMCE). |
/manual_editor/api_get_manual.php | GET: devuelve HTML del campo manual por curso_id . | Usa include de conexión BD. |
/manual_editor/api_update_manual.php | POST JSON: sanea y actualiza cursos_sence.manual . Inserta en manual_historial antes del UPDATE. | Incluye protección CSRF. |
/manual_editor/api_listar_imagenes.php | GET: lista de imágenes bajo $carpeta_img . | Incluye metadatos (ancho, alto, peso). |
/manual_editor/api_subir_imagen.php | POST multipart/form-data: valida, optimiza y guarda imagen. | Devuelve ruta relativa. |
/manual_editor/helpers_imagenes.php | Funciones de validación, normalización y optimización (Imagick → GD). | Control de límites y nombres de archivo. |
/manual_editor/helpers_html.php | Saneado del HTML con whitelist de etiquetas/atributos y normalización de URLs relativas. | Bloquea scripts/estilos en línea y eventos. |
/manual_editor/README_NOTAS.txt | Notas operativas y troubleshooting. | Opcional. |
/manual_editor/tinymce/… | Distribución local de TinyMCE (como en la captura). | Se referencia /manual_editor/tinymce/js/tinymce/tinymce.min.js . |
/manual_editor/img/ | Carpeta de imágenes del módulo. | Se puede subdividir por curso en el futuro. |
5) Configuración (en duro, reutilizable)
Las siguientes variables se definirán al inicio de manual_editor.php y se reutilizarán (vía include) en los endpoints:
Variable | Valor por defecto | Descripción |
---|---|---|
$carpeta_img | "/manual_editor/img/" | Ruta relativa pública donde se listan/guardan imágenes. |
$permitidos_ext | ['jpg','jpeg','png','gif'] | Extensiones permitidas. |
$limite_peso_mb | 4 | Tamaño máximo de subida (MB) antes de optimización/reeescala. |
$max_dim_px | 2560 | Dimensión máxima por lado. Si se excede, se reescala. |
$usar_imagick | true si disponible | Fallback automático a GD si Imagick no está. |
$csrf_activo | true | Protege los POST con token. |
Todos los endpoints deben include el mismo archivo de configuración para mantener consistencia.
6) UI / UX
Columna izquierda – Galería
- Encabezado con ruta activa (
$carpeta_img
) y conteo de imágenes. - Grid de miniaturas con “drag handle” para arrastrar hacia el editor.
- Botón Subir imagen (abre input file); valida y envía a
api_subir_imagen.php
. - Botón Buscar en…:
- Abre el selector de archivos del navegador (permite elegir archivos o carpeta completa con webkitdirectory).
- Las imágenes seleccionadas se previsualizan en galería local y pueden subirse individualmente o por lotes (enviando cada una).
- Filtro por nombre (simple, lado cliente) y paginación si hay muchas imágenes.
Columna derecha – Editor
- Instancia de TinyMCE con plugins:
link
,lists
,image
,code
,paste
. - Barra de herramientas mínima: formato (negrita/itálica), listas, encabezados, enlace, imagen, código.
- Drag&Drop: al soltar una miniatura dentro del editor, insertar
<img src="RUTA_REL" alt="" />
en el caret. - Previsualización inmediata: el editor es WYSIWYG (lo que se ve es lo que se guarda).
- Botón Actualizar:
- Obtiene HTML del editor → saneado servidor → inserta en
manual_historial
→ actualizacursos_sence.manual
. - Feedback con toast/alert y logs de consola.
- Obtiene HTML del editor → saneado servidor → inserta en
7) Arrastrar y soltar (comportamiento)
- La miniatura establece en
dataTransfer
una carga con la ruta relativa (p. ej. /manual_editor/img/curso_123_1717400000.jpg). - El área del editor acepta el drop y usa la API de TinyMCE para insertar un nodo
<img>
en la posición del cursor. - Antes de insertar, se valida que la ruta:
- No contenga ../ (path traversal).
- Empiece por
$carpeta_img
. - Termine en una extensión permitida.
8) API – Contratos
8.1. GET /manual_editor/api_get_manual.php?curso_id={ID}
Descripción | Devuelve el HTML actual del campo manual de cursos_sence . |
---|---|
Parámetros | curso_id (entero, requerido) |
Respuesta 200 | { "ok": true, "curso_id": 123, "html": "<p>...</p>" } |
Errores | { "ok": false, "error_code": "NOT_FOUND" | "INVALID_ID" | "DB_ERROR", "message": "..." } |
8.2. GET /manual_editor/api_listar_imagenes.php
Descripción | Lista imágenes disponibles bajo $carpeta_img . |
---|---|
Parámetros | Opcionales: page (entero ≥1), q (string para filtrar por nombre) |
Respuesta 200 | { "ok": true, "base": "/manual_editor/img/", "page": 1, "total": 42, "imagenes": [ {"nombre":"curso_123_1717400000.jpg","ruta_rel":"/manual_editor/img/curso_123_1717400000.jpg","ancho":1200,"alto":800,"peso":340212}, ... ] } |
8.3. POST /manual_editor/api_subir_imagen.php
Descripción | Sube una imagen, valida, optimiza y la guarda bajo $carpeta_img . |
---|---|
Headers | Content-Type: multipart/form-data |
Campos | archivo (file, requerido), curso_id (entero, opcional para nombre), csrf_token (string, requerido si CSRF activo) |
Respuesta 200 | { "ok": true, "ruta_rel": "/manual_editor/img/curso_123_1717400000.jpg", "width": 1200, "height": 800, "size": 240132 } |
Errores | { "ok": false, "error_code": "NO_FILE" | "EXT_NOT_ALLOWED" | "MIME_NOT_ALLOWED" | "TOO_LARGE" | "OPTIMIZE_FAIL" | "SAVE_FAIL" | "CSRF_FAIL", "message": "..." } |
8.4. POST /manual_editor/api_update_manual.php
Descripción | Sanea HTML, inserta en manual_historial y actualiza el campo manual . |
---|---|
Headers | Content-Type: application/json |
Body | { "curso_id": 123, "html": "<p>...<img src="/manual_editor/img/....jpg" alt="">...</p>", "csrf_token": "..." } |
Respuesta 200 | { "ok": true, "updated": true, "historial_id": 9876 } |
Errores | { "ok": false, "error_code": "INVALID_ID" | "EMPTY_HTML" | "SANITIZE_FAIL" | "DB_ERROR" | "CSRF_FAIL", "message": "..." } |
9) Base de datos
9.1. Tabla existente
- cursos_sence
id
(PK, entero) — se usa como$curso_id
.manual
(LONGTEXT) — contenido HTML.
9.2. Tabla de historial (nueva)
Definición funcional (sin entregar DDL):
Campo | Tipo sugerido | Descripción |
---|---|---|
id | INT AUTO_INCREMENT | PK |
curso_id | INT | Referencia a cursos_sence.id |
html | LONGTEXT | Snapshot previo al UPDATE |
creado_en | DATETIME | Fecha/hora del registro |
ip_origen | VARCHAR(45) | IPv4/IPv6 del cliente |
user_agent | VARCHAR(255) | Navegador/OS |
10) TinyMCE
Se utilizará la distribución local provista (ver captura): /manual_editor/tinymce/js/tinymce/tinymce.min.js. Alternativamente puede emplearse CDN oficial si se desea.
- Plugins:
link
,lists
,image
,code
,paste
. - Política de pegado: limpiar estilos/formatos agresivos; helpers_html.php hará el saneado final del lado servidor.
- Imágenes: se insertan por drop con
src
relativo, nuncadata:
.
11) Seguridad y saneado
11.1. Saneado HTML (servidor)
- Etiquetas permitidas:
p, br, h1, h2, h3, h4, h5, h6, ul, ol, li, a, strong, em, u, blockquote, code, pre, hr, img, figure, figcaption, div, span
. - Atributos permitidos:
a
:href
,title
,target
(si_blank
→ añadirrel="noopener"
)img
:src
(relativo bajo$carpeta_img
),alt
,title
,width
,height
class
opcional para estilos Bootstrap
- Bloqueado:
style
inline, eventoson*
,script
,iframe
, URLs externas enimg
, protocolodata:
.
11.2. Subida de imágenes
- Validación de extensión y MIME real con finfo.
- Rechazo si excede límites → intentar optimización (reescalar/recomprimir) → guardar.
- Nomenclatura: curso_{ID}_{timestamp}_{rand}.{ext}
- Prevención de path traversal: limpiar nombres, prohibir ../.
11.3. CSRF
- Token generado en manual_editor.php, almacenado en $_SESSION.
- Incluido en POST de subida y actualización.
- Validado server-side; error CSRF_FAIL si no coincide.
12) Monitoreo y logging
12.1. Cliente (consola)
Todos los eventos relevantes deben registrarse con prefijo MANUAL_EDITOR:
Evento | Payload |
---|---|
LOAD_OK | {curso_id, bytes_html} |
LOAD_FAIL | {error_code, message} |
IMAGES_LIST_OK | {count, page} |
UPLOAD_OK | {ruta_rel, size, width, height} |
UPLOAD_FAIL | {error_code, message} |
DROP_OK | {ruta_rel} |
DROP_BLOCKED | {reason} |
UPDATE_OK | {historial_id, bytes_html} |
UPDATE_FAIL | {error_code, message} |
12.2. Servidor
- Usar error_log con contexto: [curso_id] [endpoint] [ip] [ua] [detalle].
- Loggear fallos de validación, sanitización, optimización y SQL.
13) Catálogo de errores (códigos estándar)
Código | Descripción | Endpoint |
---|---|---|
INVALID_ID | ID ausente o no numérico | todos |
NOT_FOUND | No existe el curso o no tiene manual | get_manual |
DB_ERROR | Error de base de datos | todos |
NO_FILE | No se recibió archivo | subir_imagen |
EXT_NOT_ALLOWED | Extensión prohibida | subir_imagen |
MIME_NOT_ALLOWED | MIME real no es imagen válida | subir_imagen |
TOO_LARGE | Supera límite y no posible optimizar | subir_imagen |
OPTIMIZE_FAIL | Fallo al optimizar | subir_imagen |
SAVE_FAIL | Fallo al guardar archivo | subir_imagen |
EMPTY_HTML | Contenido vacío o solo espacios | update_manual |
SANITIZE_FAIL | El saneado detectó contenido no permitido | update_manual |
CSRF_FAIL | Token inválido o ausente | subir_imagen, update_manual |
14) Rendimiento
- Lazy loading/paginación en
api_listar_imagenes.php
(p. ej. 60 miniaturas por página). - Miniaturas: mostrar versión reducida en cliente (usar
loading="lazy"
en <img>). - Optimización en servidor con calidad adecuada (JPEG 75–82).
15) Cabeceras y endurecimiento
- X-Content-Type-Options: nosniff
- Referrer-Policy: same-origin
- Content-Security-Policy restrictiva (sin
unsafe-inline
si es posible; permitir la ruta local de TinyMCE).
16) Recursos obligatorios (front)
- jQuery 3.7.1 (CDN): https://code.jquery.com/jquery-3.7.1.js
- Bootstrap 5.3.3 JS (CDN): https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js
- Bootstrap 5.3 CSS (si no está global): CDN oficial.
- TinyMCE local: /manual_editor/tinymce/js/tinymce/tinymce.min.js
17) Plan de pruebas
- Carga inicial: con
curso_id
válido/ inválido; manual vacío / con contenido pesado. - Listado imágenes: sin archivos / con 1000+ archivos (paginación); filtro por nombre.
- Subida:
- Archivos válidos en cada formato permitido.
- Archivo > 4 MB y > 2560 px (verificar optimización).
- MIME falso / extensión cambiada.
- Drag&Drop: insertar 1, 10 y 100 imágenes; verificar rutas relativas y saneado.
- Actualización: guardar cambios, confirmar inserción en
manual_historial
y UPDATE encursos_sence
. - CSRF: token ausente/incorrecto → rechazar.
- Accesibilidad: foco, teclas de navegación, textos alternativos en <img>.
18) Criterios de aceptación
- El editor muestra exactamente el contenido del campo
manual
y los cambios se ven al instante. - Se puede arrastrar cualquier miniatura válida y se inserta en el caret sin errores.
- Subidas fuera de límites son optimizadas y aceptadas; si fallan, se reporta con código y mensaje.
- Las rutas de <img> guardadas son relativas bajo
$carpeta_img
. - Previo a cada UPDATE, se genera un registro en
manual_historial
. - La consola muestra MANUAL_EDITOR logs consistentes para todas las acciones.
19) Plan de implementación por etapas (sin escribir código aún)
- Esqueleto: crear estructura de archivos y bloque de configuración único.
- UI base: layout Bootstrap 2 columnas, carga de TinyMCE y botones.
- GET manual: endpoint + render en editor + logs LOAD_OK/FAIL.
- Galería: endpoint listar + grid + dragstart.
- Drop en TinyMCE: insertar <img> seguro (validaciones ruta).
- Subida: endpoint multipart + validaciones + optimización + refresco galería.
- Update: saneado server, historial, UPDATE BD + toasts + logs.
- CSRF: integrar en subir y update.
- Pulido: paginación, filtro, lazy images, mensajes UX.
- QA: ejecutar plan de pruebas y checklist de aceptación.
20) Estándares y encabezados obligatorios de código
Cuando autorices la codificación, todos los archivos PHP del módulo iniciarán con el bloque de depuración y el include a la conexión que definiste:
- Config de errores activada (desactivable en producción).
- include("include/conecta_mysql_otec.php");
- Código organizado en secciones numeradas con comentarios claros.
21) Anexos
A) Glosario rápido
- Rutas relativas: comienzan en /manual_editor/ y no contienen ../.
- TinyMCE local: /manual_editor/tinymce/js/tinymce/tinymce.min.js
- Carpeta imágenes: /manual_editor/img/
B) Compatibilidad del botón “Buscar…”
Para seleccionar carpetas completas, usar el atributo webkitdirectory (Chrome/Edge/Opera/Brave). En Firefox no está soportado: usar selección múltiple de archivos. En todos los casos, la lectura del disco solo ocurre después de que el usuario selecciona explícitamente la carpeta/archivos.