diff options
author | lxf <sng@luxagraf.net> | 2022-05-14 16:38:07 -0400 |
---|---|---|
committer | lxf <sng@luxagraf.net> | 2022-05-14 16:38:07 -0400 |
commit | bb3973ffb714c932e9ec6dd6a751228dc71fe1d3 (patch) | |
tree | 6fa32f9392ad2ec32271349b86a4c1388fd6ba95 /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.py | 200 |
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 |