/tcc/forms.py

https://github.com/WoLpH/django-tcc · Python · 163 lines · 148 code · 10 blank · 5 comment · 6 complexity · b42f7842752d2b5aeaae9ad9c6f5a799 MD5 · raw file

  1. import time
  2. from django import forms
  3. from django.conf import settings
  4. from django.utils.crypto import salted_hmac, constant_time_compare
  5. from django.utils.hashcompat import sha_constructor
  6. from django.utils.translation import ugettext_lazy as _
  7. from tcc.models import Comment
  8. class CommentForm(forms.ModelForm):
  9. """
  10. Handles the security aspects (anti-spoofing) for comment forms.
  11. """
  12. timestamp = forms.IntegerField(widget=forms.HiddenInput)
  13. security_hash = forms.CharField(min_length=40, max_length=40,
  14. widget=forms.HiddenInput)
  15. next = forms.CharField(widget=forms.HiddenInput, required=False)
  16. honeypot = forms.CharField(
  17. required=False,
  18. label=_('If you enter anything in this field '\
  19. 'your comment will be treated as spam'),
  20. widget=forms.HiddenInput,
  21. )
  22. def __init__(self, data=None, initial=None, ip=None):
  23. self.data = data
  24. self.initial = initial or {}
  25. self.initial.update(self.generate_security_data())
  26. self.ip = ip
  27. super(CommentForm, self).__init__(data=data, initial=self.initial)
  28. def save(self, commit=True):
  29. instance = forms.ModelForm.save(self, commit=False)
  30. assert self.ip, 'Unable to save without an IP address'
  31. instance.ip_address = self.ip
  32. if commit:
  33. instance.save()
  34. return instance
  35. @property
  36. def content_type(self):
  37. content_type = self.initial.get('content_type')
  38. if not content_type:
  39. content_type = self.data['content_type']
  40. return content_type
  41. @property
  42. def object_pk(self):
  43. object_pk = self.initial.get('object_pk')
  44. if not object_pk:
  45. object_pk = self.data['object_pk']
  46. return object_pk
  47. def clean(self):
  48. """ Check that the user has not posted too many comments today.
  49. """
  50. data = self.cleaned_data
  51. user = data['user']
  52. if not user.profile.check_comment_limits():
  53. raise forms.ValidationError(_("You have posted too many messages today."))
  54. return data
  55. def clean_honeypot(self):
  56. """Check that nothing's been entered into the honeypot."""
  57. value = self.cleaned_data["honeypot"]
  58. if value:
  59. raise forms.ValidationError(self.fields["honeypot"].label)
  60. return value
  61. def security_errors(self):
  62. """Return just those errors associated with security"""
  63. errors = forms.ErrorDict()
  64. for f in ["honeypot", "timestamp", "security_hash"]:
  65. if f in self.errors:
  66. errors[f] = self.errors[f]
  67. return errors
  68. def clean_security_hash(self):
  69. """Check the security hash."""
  70. security_hash_dict = {
  71. 'content_type' : self.data.get("content_type", ""),
  72. 'object_pk' : self.data.get("object_pk", ""),
  73. 'timestamp' : self.data.get("timestamp", ""),
  74. }
  75. expected_hash = self.generate_security_hash(**security_hash_dict)
  76. actual_hash = self.cleaned_data["security_hash"]
  77. if not constant_time_compare(expected_hash, actual_hash):
  78. # Fallback to Django 1.2 method for compatibility
  79. # PendingDeprecationWarning <- here to remind us to remove this
  80. # fallback in Django 1.5
  81. expected_hash_old = self._generate_security_hash_old(**security_hash_dict)
  82. if not constant_time_compare(expected_hash_old, actual_hash):
  83. raise forms.ValidationError("Security hash check failed.")
  84. return actual_hash
  85. def clean_timestamp(self):
  86. """Make sure the timestamp isn't too far (> 2 hours) in the past."""
  87. ts = self.cleaned_data["timestamp"]
  88. if time.time() - ts > (2 * 60 * 60):
  89. raise forms.ValidationError("Timestamp check failed")
  90. return ts
  91. def generate_security_data(self):
  92. """Generate a dict of security data for "initial" data."""
  93. timestamp = int(time.time())
  94. security_dict = {
  95. 'content_type' : str(self.content_type),
  96. 'object_pk' : str(self.object_pk),
  97. 'timestamp' : str(timestamp),
  98. 'security_hash' : self.initial_security_hash(timestamp),
  99. }
  100. return security_dict
  101. def initial_security_hash(self, timestamp):
  102. """
  103. Generate the initial security hash from self.content_object
  104. and a (unix) timestamp.
  105. """
  106. initial_security_dict = {
  107. 'content_type' : str(self.content_type),
  108. 'object_pk' : str(self.object_pk),
  109. 'timestamp' : str(timestamp),
  110. }
  111. return self.generate_security_hash(**initial_security_dict)
  112. def generate_security_hash(self, content_type, object_pk, timestamp):
  113. """
  114. Generate a HMAC security hash from the provided info.
  115. """
  116. info = (content_type, object_pk, timestamp)
  117. key_salt = "django.contrib.forms.CommentSecurityForm"
  118. value = "-".join(info)
  119. return salted_hmac(key_salt, value).hexdigest()
  120. def _generate_security_hash_old(self, content_type, object_pk, timestamp):
  121. """Generate a (SHA1) security hash from the provided info."""
  122. # Django 1.2 compatibility
  123. info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
  124. return sha_constructor("".join(info)).hexdigest()
  125. class Meta:
  126. model = Comment
  127. fields = [
  128. 'content_type',
  129. 'object_pk',
  130. 'parent',
  131. 'user',
  132. 'comment',
  133. 'timestamp',
  134. 'security_hash',
  135. 'next',
  136. 'honeypot',
  137. ]
  138. widgets = {
  139. 'content_type': forms.HiddenInput,
  140. 'object_pk': forms.HiddenInput,
  141. 'user': forms.HiddenInput,
  142. 'parent': forms.HiddenInput,
  143. }