|
1 import time |
|
2 import datetime |
|
3 |
|
4 from django import forms |
|
5 from django.forms.util import ErrorDict |
|
6 from django.conf import settings |
|
7 from django.contrib.contenttypes.models import ContentType |
|
8 from models import Comment |
|
9 from django.utils.encoding import force_unicode |
|
10 from django.utils.hashcompat import sha_constructor |
|
11 from django.utils.text import get_text_list |
|
12 from django.utils.translation import ungettext, ugettext_lazy as _ |
|
13 |
|
14 COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000) |
|
15 |
|
16 class CommentSecurityForm(forms.Form): |
|
17 """ |
|
18 Handles the security aspects (anti-spoofing) for comment forms. |
|
19 """ |
|
20 content_type = forms.CharField(widget=forms.HiddenInput) |
|
21 object_pk = forms.CharField(widget=forms.HiddenInput) |
|
22 timestamp = forms.IntegerField(widget=forms.HiddenInput) |
|
23 security_hash = forms.CharField(min_length=40, max_length=40, widget=forms.HiddenInput) |
|
24 |
|
25 def __init__(self, target_object, data=None, initial=None): |
|
26 self.target_object = target_object |
|
27 if initial is None: |
|
28 initial = {} |
|
29 initial.update(self.generate_security_data()) |
|
30 super(CommentSecurityForm, self).__init__(data=data, initial=initial) |
|
31 |
|
32 def security_errors(self): |
|
33 """Return just those errors associated with security""" |
|
34 errors = ErrorDict() |
|
35 for f in ["honeypot", "timestamp", "security_hash"]: |
|
36 if f in self.errors: |
|
37 errors[f] = self.errors[f] |
|
38 return errors |
|
39 |
|
40 def clean_security_hash(self): |
|
41 """Check the security hash.""" |
|
42 security_hash_dict = { |
|
43 'content_type' : self.data.get("content_type", ""), |
|
44 'object_pk' : self.data.get("object_pk", ""), |
|
45 'timestamp' : self.data.get("timestamp", ""), |
|
46 } |
|
47 expected_hash = self.generate_security_hash(**security_hash_dict) |
|
48 actual_hash = self.cleaned_data["security_hash"] |
|
49 if expected_hash != actual_hash: |
|
50 raise forms.ValidationError("Security hash check failed.") |
|
51 return actual_hash |
|
52 |
|
53 def clean_timestamp(self): |
|
54 """Make sure the timestamp isn't too far (> 2 hours) in the past.""" |
|
55 ts = self.cleaned_data["timestamp"] |
|
56 if time.time() - ts > (2 * 60 * 60): |
|
57 raise forms.ValidationError("Timestamp check failed") |
|
58 return ts |
|
59 |
|
60 def generate_security_data(self): |
|
61 """Generate a dict of security data for "initial" data.""" |
|
62 timestamp = int(time.time()) |
|
63 security_dict = { |
|
64 'content_type' : str(self.target_object._meta), |
|
65 'object_pk' : str(self.target_object._get_pk_val()), |
|
66 'timestamp' : str(timestamp), |
|
67 'security_hash' : self.initial_security_hash(timestamp), |
|
68 } |
|
69 return security_dict |
|
70 |
|
71 def initial_security_hash(self, timestamp): |
|
72 """ |
|
73 Generate the initial security hash from self.content_object |
|
74 and a (unix) timestamp. |
|
75 """ |
|
76 |
|
77 initial_security_dict = { |
|
78 'content_type' : str(self.target_object._meta), |
|
79 'object_pk' : str(self.target_object._get_pk_val()), |
|
80 'timestamp' : str(timestamp), |
|
81 } |
|
82 return self.generate_security_hash(**initial_security_dict) |
|
83 |
|
84 def generate_security_hash(self, content_type, object_pk, timestamp): |
|
85 """Generate a (SHA1) security hash from the provided info.""" |
|
86 info = (content_type, object_pk, timestamp, settings.SECRET_KEY) |
|
87 return sha_constructor("".join(info)).hexdigest() |
|
88 |
|
89 class CommentDetailsForm(CommentSecurityForm): |
|
90 """ |
|
91 Handles the specific details of the comment (name, comment, etc.). |
|
92 """ |
|
93 name = forms.CharField(label=_("Name"), max_length=50) |
|
94 email = forms.EmailField(label=_("Email address")) |
|
95 url = forms.URLField(label=_("URL"), required=False) |
|
96 comment = forms.CharField(label=_('Comment'), widget=forms.Textarea, |
|
97 max_length=COMMENT_MAX_LENGTH) |
|
98 |
|
99 def get_comment_object(self): |
|
100 """ |
|
101 Return a new (unsaved) comment object based on the information in this |
|
102 form. Assumes that the form is already validated and will throw a |
|
103 ValueError if not. |
|
104 |
|
105 Does not set any of the fields that would come from a Request object |
|
106 (i.e. ``user`` or ``ip_address``). |
|
107 """ |
|
108 if not self.is_valid(): |
|
109 raise ValueError("get_comment_object may only be called on valid forms") |
|
110 |
|
111 CommentModel = self.get_comment_model() |
|
112 new = CommentModel(**self.get_comment_create_data()) |
|
113 new = self.check_for_duplicate_comment(new) |
|
114 |
|
115 return new |
|
116 |
|
117 def get_comment_model(self): |
|
118 """ |
|
119 Get the comment model to create with this form. Subclasses in custom |
|
120 comment apps should override this, get_comment_create_data, and perhaps |
|
121 check_for_duplicate_comment to provide custom comment models. |
|
122 """ |
|
123 return Comment |
|
124 |
|
125 def get_comment_create_data(self): |
|
126 """ |
|
127 Returns the dict of data to be used to create a comment. Subclasses in |
|
128 custom comment apps that override get_comment_model can override this |
|
129 method to add extra fields onto a custom comment model. |
|
130 """ |
|
131 return dict( |
|
132 content_type = ContentType.objects.get_for_model(self.target_object), |
|
133 object_pk = force_unicode(self.target_object._get_pk_val()), |
|
134 user_name = self.cleaned_data["name"], |
|
135 user_email = self.cleaned_data["email"], |
|
136 user_url = self.cleaned_data["url"], |
|
137 comment = self.cleaned_data["comment"], |
|
138 submit_date = datetime.datetime.now(), |
|
139 site_id = settings.SITE_ID, |
|
140 is_public = True, |
|
141 is_removed = False, |
|
142 ) |
|
143 |
|
144 def check_for_duplicate_comment(self, new): |
|
145 """ |
|
146 Check that a submitted comment isn't a duplicate. This might be caused |
|
147 by someone posting a comment twice. If it is a dup, silently return the *previous* comment. |
|
148 """ |
|
149 possible_duplicates = self.get_comment_model()._default_manager.filter( |
|
150 content_type = new.content_type, |
|
151 object_pk = new.object_pk, |
|
152 user_name = new.user_name, |
|
153 user_email = new.user_email, |
|
154 user_url = new.user_url, |
|
155 ) |
|
156 for old in possible_duplicates: |
|
157 if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment: |
|
158 return old |
|
159 |
|
160 return new |
|
161 |
|
162 def clean_comment(self): |
|
163 """ |
|
164 If COMMENTS_ALLOW_PROFANITIES is False, check that the comment doesn't |
|
165 contain anything in PROFANITIES_LIST. |
|
166 """ |
|
167 comment = self.cleaned_data["comment"] |
|
168 if settings.COMMENTS_ALLOW_PROFANITIES == False: |
|
169 bad_words = [w for w in settings.PROFANITIES_LIST if w in comment.lower()] |
|
170 if bad_words: |
|
171 plural = len(bad_words) > 1 |
|
172 raise forms.ValidationError(ungettext( |
|
173 "Watch your mouth! The word %s is not allowed here.", |
|
174 "Watch your mouth! The words %s are not allowed here.", plural) % \ |
|
175 get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in bad_words], 'and')) |
|
176 return comment |
|
177 |
|
178 class CommentForm(CommentDetailsForm): |
|
179 honeypot = forms.CharField(required=False, |
|
180 label=_('If you enter anything in this field '\ |
|
181 'your comment will be treated as spam')) |
|
182 |
|
183 def clean_honeypot(self): |
|
184 """Check that nothing's been entered into the honeypot.""" |
|
185 value = self.cleaned_data["honeypot"] |
|
186 if value: |
|
187 raise forms.ValidationError(self.fields["honeypot"].label) |
|
188 return value |