From 231a7e04194ad886b615122583ae7444d6a35443 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sat, 21 Mar 2009 13:45:31 +0000 Subject: [PATCH] Fixed #9958: split the `CommentForm` into a set of smaller forms. This for better encapsulation, but also so that it's easier for subclasses to get at the pieces they might need. Thanks to Thejaswi Puthraya. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10110 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/comments/forms.py | 154 ++++++++++++++------------- docs/ref/contrib/comments/custom.txt | 15 ++- docs/ref/contrib/comments/forms.txt | 48 +++++++++ docs/ref/contrib/comments/index.txt | 1 + 4 files changed, 143 insertions(+), 75 deletions(-) create mode 100644 docs/ref/contrib/comments/forms.txt diff --git a/django/contrib/comments/forms.py b/django/contrib/comments/forms.py index f358b8132b..4ba12efa92 100644 --- a/django/contrib/comments/forms.py +++ b/django/contrib/comments/forms.py @@ -13,15 +13,10 @@ from django.utils.translation import ungettext, ugettext_lazy as _ COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000) -class CommentForm(forms.Form): - name = forms.CharField(label=_("Name"), max_length=50) - email = forms.EmailField(label=_("Email address")) - url = forms.URLField(label=_("URL"), required=False) - comment = forms.CharField(label=_('Comment'), widget=forms.Textarea, - max_length=COMMENT_MAX_LENGTH) - honeypot = forms.CharField(required=False, - label=_('If you enter anything in this field '\ - 'your comment will be treated as spam')) +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) @@ -32,8 +27,75 @@ class CommentForm(forms.Form): if initial is None: initial = {} initial.update(self.generate_security_data()) - super(CommentForm, self).__init__(data=data, initial=initial) + super(CommentSecurityForm, self).__init__(data=data, initial=initial) + 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 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 (> 2 hours) in the past.""" + ts = self.cleaned_data["timestamp"] + if time.time() - ts > (2 * 60 * 60): + raise forms.ValidationError("Timestamp check failed") + 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 (SHA1) security hash from the provided info.""" + info = (content_type, object_pk, timestamp, settings.SECRET_KEY) + return sha_constructor("".join(info)).hexdigest() + +class CommentDetailsForm(CommentSecurityForm): + """ + Handles the specific details of the comment (name, comment, etc.). + """ + name = forms.CharField(label=_("Name"), max_length=50) + email = forms.EmailField(label=_("Email address")) + url = forms.URLField(label=_("URL"), required=False) + comment = forms.CharField(label=_('Comment'), widget=forms.Textarea, + max_length=COMMENT_MAX_LENGTH) + def get_comment_object(self): """ Return a new (unsaved) comment object based on the information in this @@ -97,41 +159,6 @@ class CommentForm(forms.Form): return new - 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_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 - - 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 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 (> 2 hours) in the past.""" - ts = self.cleaned_data["timestamp"] - if time.time() - ts > (2 * 60 * 60): - raise forms.ValidationError("Timestamp check failed") - return ts - def clean_comment(self): """ If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't @@ -148,31 +175,14 @@ class CommentForm(forms.Form): get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and')) return comment - 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 +class CommentForm(CommentDetailsForm): + honeypot = forms.CharField(required=False, + label=_('If you enter anything in this field '\ + 'your comment will be treated as spam')) - 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 (SHA1) security hash from the provided info.""" - info = (content_type, object_pk, timestamp, settings.SECRET_KEY) - return sha_constructor("".join(info)).hexdigest() + 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 diff --git a/docs/ref/contrib/comments/custom.txt b/docs/ref/contrib/comments/custom.txt index 064bbca6d8..d4b175603b 100644 --- a/docs/ref/contrib/comments/custom.txt +++ b/docs/ref/contrib/comments/custom.txt @@ -93,7 +93,12 @@ field:: data['title'] = self.cleaned_data['title'] return data -Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to point Django at these classes we've created:: +Django provides a couple of "helper" classes to make writing certain types of +custom comment forms easier; see :mod:`django.contrib.comments.forms` for +more. + +Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to +point Django at these classes we've created:: from my_comments_app.models import CommentWithTitle from my_comments_app.forms import CommentFormWithTitle @@ -104,14 +109,18 @@ Finally, we'll define a couple of methods in ``my_custom_app/__init__.py`` to po def get_form(): return CommentFormWithTitle -The above process should take care of most common situations. For more advanced usage, there are additional methods you can define. Those are explained in the next section. +The above process should take care of most common situations. For more +advanced usage, there are additional methods you can define. Those are +explained in the next section. .. _custom-comment-app-api: Custom comment app API ====================== -The :mod:`django.contrib.comments` app defines the following methods; any custom comment app must define at least one of them. All are optional, however. +The :mod:`django.contrib.comments` app defines the following methods; any +custom comment app must define at least one of them. All are optional, +however. .. function:: get_model() diff --git a/docs/ref/contrib/comments/forms.txt b/docs/ref/contrib/comments/forms.txt new file mode 100644 index 0000000000..af0c59a27e --- /dev/null +++ b/docs/ref/contrib/comments/forms.txt @@ -0,0 +1,48 @@ +.. _ref-contrib-comments-forms: + +==================== +Comment form classes +==================== + +.. module:: django.contrib.comments.forms + :synopsis: Forms for dealing with the built-in comment model. + +The ``django.contrib.comments.forms`` module contains a handful of forms +you'll use when writing custom views dealing with comments, or when writing +:ref:`custom comment apps `. + +.. class:: CommentForm + + The main comment form representing the standard, built-in way of handling + submitted comments. This is the class used by all the views + :mod:`django.contrib.comments` to handle submitted comments. + + If you want to build custom views that are similar to Django's built-in + comment handling views, you'll probably want to use this form. + +Abstract comment forms for custom comment apps +---------------------------------------------- + +If you're building a :ref:`custom comment app `, +you might want to replace *some* of the form logic but still rely on parts of +the existing form. + +:class:`CommentForm` is actually composed of a couple of abstract base class +forms that you can subclass to reuse pieces of the form handling logic: + +.. class:: CommentSecurityForm + + Handles the anti-spoofing protection aspects of the comment form handling. + + This class contains the ``content_type`` and ``object_pk`` fields pointing + to the object the comment is attached to, along with a ``timestamp`` and a + ``security_hash`` of all the form data. Together, the timestamp and the + security hash ensure that spammers can't "replay" form submissions and + flood you with comments. + +.. class:: CommentDetailsForm + + Handles the details of the comment itself. + + This class contains the ``name``, ``email``, ``url``, and the ``comment`` + field itself, along with the associated valdation logic. \ No newline at end of file diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index b78ac4f36d..f3a59bbbd4 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -215,3 +215,4 @@ More information signals upgrade custom + forms