summaryrefslogtreecommitdiff
path: root/app/lib/django_comments/forms.py
diff options
context:
space:
mode:
authorlxf <sng@luxagraf.net>2022-05-14 16:38:07 -0400
committerlxf <sng@luxagraf.net>2022-05-14 16:38:07 -0400
commitbb3973ffb714c932e9ec6dd6a751228dc71fe1d3 (patch)
tree6fa32f9392ad2ec32271349b86a4c1388fd6ba95 /app/lib/django_comments/forms.py
initial commit
Diffstat (limited to 'app/lib/django_comments/forms.py')
-rw-r--r--app/lib/django_comments/forms.py200
1 files changed, 200 insertions, 0 deletions
diff --git a/app/lib/django_comments/forms.py b/app/lib/django_comments/forms.py
new file mode 100644
index 0000000..7b7eafd
--- /dev/null
+++ b/app/lib/django_comments/forms.py
@@ -0,0 +1,200 @@
+import time
+
+from django import forms
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.forms.utils import ErrorDict
+from django.utils.crypto import salted_hmac, constant_time_compare
+from django.utils.encoding import force_text
+from django.utils.text import get_text_list
+from django.utils import timezone
+from django.utils.translation import pgettext_lazy, ungettext, ugettext, ugettext_lazy as _
+
+from . import get_model
+
+COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000)
+DEFAULT_COMMENTS_TIMEOUT = getattr(settings, 'COMMENTS_TIMEOUT', (2 * 60 * 60)) # 2h
+
+
+class CommentSecurityForm(forms.Form):
+ """
+ Handles the security aspects (anti-spoofing) for comment forms.
+ """
+ content_type = forms.CharField(widget=forms.HiddenInput)
+ object_pk = forms.CharField(widget=forms.HiddenInput)
+ timestamp = forms.IntegerField(widget=forms.HiddenInput)
+ security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput)
+
+ def __init__(self, target_object, data=None, initial=None, **kwargs):
+ self.target_object = target_object
+ if initial is None:
+ initial = {}
+ initial.update(self.generate_security_data())
+ super(CommentSecurityForm, self).__init__(data=data, initial=initial, **kwargs)
+
+ def security_errors(self):
+ """Return just those errors associated with security"""
+ errors = ErrorDict()
+ for f in ["honeypot", "timestamp", "security_hash"]:
+ if f in self.errors:
+ errors[f] = self.errors[f]
+ return errors
+
+ def clean_security_hash(self):
+ """Check the security hash."""
+ security_hash_dict = {
+ 'content_type': self.data.get("content_type", ""),
+ 'object_pk': self.data.get("object_pk", ""),
+ 'timestamp': self.data.get("timestamp", ""),
+ }
+ expected_hash = self.generate_security_hash(**security_hash_dict)
+ actual_hash = self.cleaned_data["security_hash"]
+ if not constant_time_compare(expected_hash, actual_hash):
+ raise forms.ValidationError("Security hash check failed.")
+ return actual_hash
+
+ def clean_timestamp(self):
+ """Make sure the timestamp isn't too far (default is > 2 hours) in the past."""
+ ts = self.cleaned_data["timestamp"]
+ return ts
+
+ def generate_security_data(self):
+ """Generate a dict of security data for "initial" data."""
+ timestamp = int(time.time())
+ security_dict = {
+ 'content_type': str(self.target_object._meta),
+ 'object_pk': str(self.target_object._get_pk_val()),
+ 'timestamp': str(timestamp),
+ 'security_hash': self.initial_security_hash(timestamp),
+ }
+ return security_dict
+
+ def initial_security_hash(self, timestamp):
+ """
+ Generate the initial security hash from self.content_object
+ and a (unix) timestamp.
+ """
+
+ initial_security_dict = {
+ 'content_type': str(self.target_object._meta),
+ 'object_pk': str(self.target_object._get_pk_val()),
+ 'timestamp': str(timestamp),
+ }
+ return self.generate_security_hash(**initial_security_dict)
+
+ def generate_security_hash(self, content_type, object_pk, timestamp):
+ """
+ Generate a HMAC security hash from the provided info.
+ """
+ info = (content_type, object_pk, timestamp)
+ key_salt = "django.contrib.forms.CommentSecurityForm"
+ value = "-".join(info)
+ return salted_hmac(key_salt, value).hexdigest()
+
+
+class CommentDetailsForm(CommentSecurityForm):
+ """
+ Handles the specific details of the comment (name, comment, etc.).
+ """
+ name = forms.CharField(label=pgettext_lazy("Person name", "Name"), max_length=50)
+ email = forms.EmailField(label=_("Email address"))
+ url = forms.URLField(label=_("URL"), required=False)
+ # Translators: 'Comment' is a noun here.
+ comment = forms.CharField(label=_('Comment'), widget=forms.Textarea,
+ max_length=COMMENT_MAX_LENGTH)
+
+ def get_comment_object(self, site_id=None):
+ """
+ Return a new (unsaved) comment object based on the information in this
+ form. Assumes that the form is already validated and will throw a
+ ValueError if not.
+
+ Does not set any of the fields that would come from a Request object
+ (i.e. ``user`` or ``ip_address``).
+ """
+ if not self.is_valid():
+ raise ValueError("get_comment_object may only be called on valid forms")
+
+ CommentModel = self.get_comment_model()
+ new = CommentModel(**self.get_comment_create_data(site_id=site_id))
+ new = self.check_for_duplicate_comment(new)
+
+ return new
+
+ def get_comment_model(self):
+ """
+ Get the comment model to create with this form. Subclasses in custom
+ comment apps should override this, get_comment_create_data, and perhaps
+ check_for_duplicate_comment to provide custom comment models.
+ """
+ return get_model()
+
+ def get_comment_create_data(self, site_id=None):
+ """
+ Returns the dict of data to be used to create a comment. Subclasses in
+ custom comment apps that override get_comment_model can override this
+ method to add extra fields onto a custom comment model.
+ """
+ return dict(
+ content_type=ContentType.objects.get_for_model(self.target_object),
+ object_pk=force_text(self.target_object._get_pk_val()),
+ user_name=self.cleaned_data["name"],
+ user_email=self.cleaned_data["email"],
+ user_url=self.cleaned_data["url"],
+ comment=self.cleaned_data["comment"],
+ submit_date=timezone.now(),
+ site_id=site_id or getattr(settings, "SITE_ID", None),
+ is_public=True,
+ is_removed=False,
+ )
+
+ def check_for_duplicate_comment(self, new):
+ """
+ Check that a submitted comment isn't a duplicate. This might be caused
+ by someone posting a comment twice. If it is a dup, silently return the *previous* comment.
+ """
+ possible_duplicates = self.get_comment_model()._default_manager.using(
+ self.target_object._state.db
+ ).filter(
+ content_type=new.content_type,
+ object_pk=new.object_pk,
+ user_name=new.user_name,
+ user_email=new.user_email,
+ user_url=new.user_url,
+ )
+ for old in possible_duplicates:
+ if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment:
+ return old
+
+ return new
+
+ def clean_comment(self):
+ """
+ If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't
+ contain anything in PROFANITIES_LIST.
+ """
+ comment = self.cleaned_data["comment"]
+ if (not getattr(settings, 'COMMENTS_ALLOW_PROFANITIES', False) and
+ getattr(settings, 'PROFANITIES_LIST', False)):
+ bad_words = [w for w in settings.PROFANITIES_LIST if w in comment.lower()]
+ if bad_words:
+ raise forms.ValidationError(ungettext(
+ "Watch your mouth! The word %s is not allowed here.",
+ "Watch your mouth! The words %s are not allowed here.",
+ len(bad_words)) % get_text_list(
+ ['"%s%s%s"' % (i[0], '-' * (len(i) - 2), i[-1])
+ for i in bad_words], ugettext('and')))
+ return comment
+
+
+class CommentForm(CommentDetailsForm):
+ honeypot = forms.CharField(required=False,
+ label=_('If you enter anything in this field '
+ 'your comment will be treated as spam'))
+
+ def clean_honeypot(self):
+ """Check that nothing's been entered into the honeypot."""
+ value = self.cleaned_data["honeypot"]
+ if value:
+ raise forms.ValidationError(self.fields["honeypot"].label)
+ return value