From a4f00e6242344cdd398ed05eb38fd92da1739193 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Wed, 13 Jul 2022 21:28:07 -0700 Subject: [PATCH] Add user post deletion Users can delete their posts as long as they don't clear their cookies, and as long as server-side user sessions are persistent. Signed-off-by: Alek Ratzloff --- board/forms.py | 3 ++- board/models.py | 4 ++++ board/utils.py | 11 +++++++++++ board/views.py | 17 ++++++++++++++++- threadchat/settings.py | 6 ++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/board/forms.py b/board/forms.py index 20dd71f..07391b6 100644 --- a/board/forms.py +++ b/board/forms.py @@ -25,11 +25,12 @@ class PostForm(ModelForm): model = Post fields = ["subject", "name", "text", "capcode", "image"] - def __init__(self, *args, user, board, ip, **kwargs): + def __init__(self, *args, user, board, ip, user_token, **kwargs): super(PostForm, self).__init__(*args, **kwargs) self.user = user self.instance.board = board self.instance.ip = ip + self.instance.user_token = user_token self.fields["capcode"].queryset = get_objects_for_user( self.user, "board.use_capcode" ) diff --git a/board/models.py b/board/models.py index d76260a..112383e 100644 --- a/board/models.py +++ b/board/models.py @@ -124,6 +124,10 @@ class Post(models.Model): # Image width and height image_width = models.IntegerField(null=True, blank=True) image_height = models.IntegerField(null=True, blank=True) + # The user token that was used to make this post. This is used for + # verification when a user wants to delete their post. It is not + # particularly secret and does not need to be hashed. + user_token = models.CharField(max_length=30, null=True) class Meta: permissions = [ diff --git a/board/utils.py b/board/utils.py index 268be2b..8bc7a63 100644 --- a/board/utils.py +++ b/board/utils.py @@ -1,6 +1,9 @@ from typing import Optional, TYPE_CHECKING from django.utils import timezone +from django.conf import settings import ipaddress +import random +import string from board.models import Ban, RangeBan @@ -45,3 +48,11 @@ def is_banned(ip: str, board: Optional["Board"]) -> bool: return bool(active) else: return False + + +def generate_user_token() -> str: + """ + Generates a non-secure user token. + User tokens need not be secure so this is a simple implementation. + """ + return "".join(random.choices(string.ascii_letters, k=settings.USER_TOKEN_LENGTH)) diff --git a/board/views.py b/board/views.py index face8da..4f7a52a 100644 --- a/board/views.py +++ b/board/views.py @@ -8,7 +8,7 @@ from django.http import Http404, HttpResponseRedirect from django.http.request import QueryDict from django.shortcuts import get_object_or_404 from django.views.generic.base import TemplateView -from django.views.generic import detail, edit +from django.views.generic import edit from django.urls import reverse, reverse_lazy from django.utils import timezone @@ -147,6 +147,10 @@ class PostCreateView(CreateView): def get_form_kwargs(self): kwargs = super(PostCreateView, self).get_form_kwargs() kwargs["user"] = self.request.user + if "user_token" not in self.request.session: + # Generate a user token + self.request.session["user_token"] = generate_user_token() + kwargs["user_token"] = self.request.session["user_token"] return kwargs def get_success_url(self) -> str: @@ -213,6 +217,10 @@ class ReplyCreateView(CreateView): post = get_object_or_404(Post, id=post_id) kwargs["op"] = post kwargs["user"] = self.request.user + if "user_token" not in self.request.session: + # Generate a user token + self.request.session["user_token"] = generate_user_token() + kwargs["user_token"] = self.request.session["user_token"] return kwargs def get_success_url(self) -> str: @@ -256,6 +264,13 @@ class PostDeleteView(PermissionRequiredMixin, edit.DeleteView): success_url = reverse_lazy("board:post_delete_success") raise_exception = True + def has_permission(self) -> bool: + object = self.get_object() + user_token = self.request.session.get("user_token", None) + return self.request.user.has_perm("board.delete_post") or ( + user_token and object.user_token == user_token + ) + def form_valid(self, form): success_url = self.get_success_url() if form["image_only"].value() != "0": diff --git a/threadchat/settings.py b/threadchat/settings.py index c02ad90..ef6f68e 100644 --- a/threadchat/settings.py +++ b/threadchat/settings.py @@ -174,3 +174,9 @@ BAN_WINDOW_CLOSE_TIMEOUT = 0 # By default, wait 0 seconds. If there is an error, the window won't close # because it will be redirected elsewhere. POST_WINDOW_CLOSE_TIMEOUT = 0 + +# This is the length of a user's token, which is held in a server-side session. +# This token is used to identify a user's individual web client, outside of IP +# address. You probably don't need to change this. +# It should be at most 30, and probably at least 10. +USER_TOKEN_LENGTH = 30