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