From 874a0c1ad9f10a9e6ef152e4a76e2652f59f5532 Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 17:15:35 +0000 Subject: [PATCH] =?UTF-8?q?Foro:=20editor=20CKEditor=205=20en=20los=20mens?= =?UTF-8?q?ajes=20y=20moderaci=C3=B3n=20por=20POST+CSRF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - forum/forms.py: TopicForm/ReplyForm/EditPostForm con CKEditor5Widget (rich text) - vistas crear/responder/editar pasan el form; el HTML del editor se sanea con nh3 - acciones de moderación (bloquear, fijar, mover, borrar/restaurar temas y posts) ahora exigen POST y van con CSRF; las plantillas usan formularios en vez de enlaces GET - docs/FORO.md actualizado Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/FORO.md | 17 ++++++++------- forum/forms.py | 23 ++++++++++++++++++++ forum/templates/forum/_style.html | 3 +++ forum/templates/forum/create_topic.html | 5 +++-- forum/templates/forum/edit_post.html | 3 ++- forum/templates/forum/topic.html | 17 ++++++++------- forum/views.py | 28 ++++++++++++++++++++----- 7 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 forum/forms.py diff --git a/docs/FORO.md b/docs/FORO.md index a852ca0..cce3480 100644 --- a/docs/FORO.md +++ b/docs/FORO.md @@ -46,10 +46,16 @@ en `forum/permissions.py`: - El texto de los posts se **sanea con `nh3`** (allowlist de etiquetas) al guardar — `forum/sanitize.py` — evitando el XSS que tenía el original (que renderizaba HTML del editor sin filtrar). Se renderiza con `|safe` ya saneado. -- Formularios de publicar/responder/editar/mover van por **POST con CSRF**. -- ⚠️ Pendiente de endurecer: las acciones de moderación (bloquear, borrar, - restaurar, fijar) son enlaces **GET** (como en el original). Para producción se - recomienda pasarlas a POST con CSRF. +- **Todas** las acciones que modifican datos (publicar, responder, editar, mover, + bloquear/desbloquear, fijar, borrar/restaurar temas y posts) van por **POST con + CSRF**. Las vistas de moderación rechazan GET. + +## Editor + +Los mensajes se escriben con **CKEditor 5** (el mismo editor que usa el resto de +NovaWoW), vía `forum/forms.py` (`TopicForm`, `ReplyForm`, `EditPostForm`). El +HTML que produce el editor se **sanea con nh3** al guardar, así que el rich text +es seguro. ## Despliegue @@ -70,8 +76,5 @@ en `forum/permissions.py`: ## Notas / posibles mejoras -- Editor enriquecido (CKEditor/TinyMCE) en el textarea de posts (ahora es - texto/HTML saneado; los saltos de línea se respetan por CSS `pre-wrap`). - El autor se muestra como el nombre de cuenta de juego (`#1`); se podría mapear a un nick o al email como en «Mi cuenta». -- Endurecer las acciones de moderación a POST+CSRF. diff --git a/forum/forms.py b/forum/forms.py new file mode 100644 index 0000000..7e4a34a --- /dev/null +++ b/forum/forms.py @@ -0,0 +1,23 @@ +""" +Formularios del foro con el editor enriquecido CKEditor 5 (el mismo que usa el +resto de NovaWoW). El HTML resultante se sanea con nh3 al guardar (ver views). +""" + +from django import forms +from django_ckeditor_5.widgets import CKEditor5Widget + + +class TopicForm(forms.Form): + name = forms.CharField( + max_length=45, + widget=forms.TextInput(attrs={'placeholder': 'Título del tema', 'maxlength': 45}), + ) + text = forms.CharField(widget=CKEditor5Widget(config_name='default')) + + +class ReplyForm(forms.Form): + text = forms.CharField(widget=CKEditor5Widget(config_name='default')) + + +class EditPostForm(forms.Form): + text = forms.CharField(widget=CKEditor5Widget(config_name='default')) diff --git a/forum/templates/forum/_style.html b/forum/templates/forum/_style.html index 83e96a3..67faf4a 100644 --- a/forum/templates/forum/_style.html +++ b/forum/templates/forum/_style.html @@ -32,6 +32,9 @@ .forum-msg.error { background: rgba(196,30,58,.2); color: #ff9a9a; } .forum-msg.success { background: rgba(46,160,67,.2); color: #9affb0; } .forum-deleted { opacity: .55; } +.forum-inline { display: inline; } +.link-btn { background: none; border: none; color: #d79602; cursor: pointer; padding: 0; font: inherit; text-decoration: underline; } +.link-btn.danger { color: #ff9a9a; } {% if messages %}
diff --git a/forum/templates/forum/create_topic.html b/forum/templates/forum/create_topic.html index b9ae3a3..f9a126b 100644 --- a/forum/templates/forum/create_topic.html +++ b/forum/templates/forum/create_topic.html @@ -15,13 +15,14 @@

Foro: {{ forum.name }}

{% csrf_token %} - - + {{ form.name }} + {{ form.text }}
Cancelar
+ {{ form.media }}
diff --git a/forum/templates/forum/edit_post.html b/forum/templates/forum/edit_post.html index 994ecf5..1a421f4 100644 --- a/forum/templates/forum/edit_post.html +++ b/forum/templates/forum/edit_post.html @@ -14,12 +14,13 @@
{% csrf_token %} - + {{ form.text }}
Cancelar
+ {{ form.media }}
diff --git a/forum/templates/forum/topic.html b/forum/templates/forum/topic.html index 7c97af0..7e25682 100644 --- a/forum/templates/forum/topic.html +++ b/forum/templates/forum/topic.html @@ -40,10 +40,10 @@ {% if post.can_edit and not post.deleted %} Editar - · Borrar + ·
{% csrf_token %}
{% endif %} {% if post.deleted and forum_is_moderator %} - Restaurar +
{% csrf_token %}
{% endif %}
@@ -69,11 +69,12 @@

Responder

{% csrf_token %} - + {{ reply_form.text }}
+ {{ reply_form.media }} {% elif topic.locked %}

Este tema está bloqueado. No se admiten nuevas respuestas.

@@ -86,15 +87,15 @@

Moderación

{% if topic.locked %} - Desbloquear +
{% csrf_token %}
{% else %} - Bloquear +
{% csrf_token %}
{% endif %} - {% if topic.sticky %}Quitar fijado{% else %}Fijar{% endif %} +
{% csrf_token %}
{% if topic.deleted %} - Restaurar tema +
{% csrf_token %}
{% else %} - Borrar tema +
{% csrf_token %}
{% endif %}
{% if move_forums %} diff --git a/forum/views.py b/forum/views.py index 8c864a3..21f6c45 100644 --- a/forum/views.py +++ b/forum/views.py @@ -16,6 +16,7 @@ from django.urls import reverse from . import db from . import permissions as perm from . import sanitize +from .forms import TopicForm, ReplyForm, EditPostForm MIN_TEXT_LEN = 5 @@ -110,6 +111,7 @@ def view_topic(request, topic_id, page=1): 'pagination': pag, 'can_reply': perm.can_post(request) and not topic['locked'], 'move_forums': db.list_forums_for_move(topic['forum_id']) if perm.is_moderator(request) else [], + 'reply_form': ReplyForm(), })) @@ -127,6 +129,7 @@ def create_topic(request, forum_id): return redirect('app_forum') return render(request, 'forum/create_topic.html', _base_context(request, { 'forum': forum, + 'form': TopicForm(), })) @@ -204,6 +207,7 @@ def edit_post(request, post_id): return redirect('forum_topic_p1', topic_id=post['topic_id']) return render(request, 'forum/edit_post.html', _base_context(request, { 'post': post, + 'form': EditPostForm(initial={'text': post['text']}), })) @@ -242,6 +246,8 @@ def delete_post(request, post_id): if not perm.can_edit_post(request, post): messages.error(request, 'No tienes permiso.') return redirect('forum_topic_p1', topic_id=post['topic_id']) + if request.method != 'POST': + return redirect('forum_topic_p1', topic_id=post['topic_id']) db.set_post_deleted(post_id, True) messages.success(request, 'Post borrado.') return redirect('forum_topic_p1', topic_id=post['topic_id']) @@ -253,6 +259,8 @@ def restore_post(request, post_id): post = db.get_post(post_id) if not post: return redirect('app_forum') + if request.method != 'POST': + return redirect('forum_topic_p1', topic_id=post['topic_id']) db.set_post_deleted(post_id, False) messages.success(request, 'Post restaurado.') return redirect('forum_topic_p1', topic_id=post['topic_id']) @@ -269,8 +277,18 @@ def _mod_only(request): return None -def lock_topic(request, topic_id): +def _mod_post_only(request): + """Exige moderador y método POST (CSRF).""" block = _mod_only(request) + if block: + return block + if request.method != 'POST': + return redirect('app_forum') + return None + + +def lock_topic(request, topic_id): + block = _mod_post_only(request) if block: return block db.set_topic_flag(topic_id, 'locked', True) @@ -279,7 +297,7 @@ def lock_topic(request, topic_id): def unlock_topic(request, topic_id): - block = _mod_only(request) + block = _mod_post_only(request) if block: return block db.set_topic_flag(topic_id, 'locked', False) @@ -288,7 +306,7 @@ def unlock_topic(request, topic_id): def toggle_sticky(request, topic_id): - block = _mod_only(request) + block = _mod_post_only(request) if block: return block topic = db.get_topic(topic_id, include_hidden=True) @@ -299,7 +317,7 @@ def toggle_sticky(request, topic_id): def delete_topic(request, topic_id): - block = _mod_only(request) + block = _mod_post_only(request) if block: return block topic = db.get_topic(topic_id, include_hidden=True) @@ -311,7 +329,7 @@ def delete_topic(request, topic_id): def restore_topic(request, topic_id): - block = _mod_only(request) + block = _mod_post_only(request) if block: return block db.set_topic_flag(topic_id, 'deleted', False)