fields.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import warnings
  2. import django
  3. from django.core.exceptions import ImproperlyConfigured
  4. from django.forms import ValidationError
  5. from django.forms.fields import CharField, MultiValueField
  6. from django.forms.widgets import HiddenInput, MultiWidget, TextInput
  7. from django.template.loader import render_to_string
  8. from django.urls import NoReverseMatch, reverse
  9. from django.utils import timezone
  10. from django.utils.safestring import mark_safe
  11. from django.utils.translation import gettext_lazy
  12. from captcha.conf import settings
  13. from captcha.models import CaptchaStore
  14. class CaptchaHiddenInput(HiddenInput):
  15. """Hidden input for the captcha key."""
  16. # Use *args and **kwargs because signature changed in Django 1.11
  17. def build_attrs(self, *args, **kwargs):
  18. """Disable autocomplete to prevent problems on page reload."""
  19. attrs = super().build_attrs(*args, **kwargs)
  20. attrs["autocomplete"] = "off"
  21. return attrs
  22. class CaptchaAnswerInput(TextInput):
  23. """Text input for captcha answer."""
  24. # Use *args and **kwargs because signature changed in Django 1.11
  25. def build_attrs(self, *args, **kwargs):
  26. """Disable automatic corrections and completions."""
  27. attrs = super().build_attrs(*args, **kwargs)
  28. attrs["autocapitalize"] = "off"
  29. attrs["autocomplete"] = "off"
  30. attrs["autocorrect"] = "off"
  31. attrs["spellcheck"] = "false"
  32. return attrs
  33. class BaseCaptchaTextInput(MultiWidget):
  34. """
  35. Base class for Captcha widgets
  36. """
  37. def __init__(self, attrs=None):
  38. widgets = (CaptchaHiddenInput(attrs), CaptchaAnswerInput(attrs))
  39. super(BaseCaptchaTextInput, self).__init__(widgets, attrs)
  40. def decompress(self, value):
  41. if value:
  42. return value.split(",")
  43. return [None, None]
  44. def fetch_captcha_store(self, name, value, attrs=None, generator=None):
  45. """
  46. Fetches a new CaptchaStore
  47. This has to be called inside render
  48. """
  49. try:
  50. reverse("captcha-image", args=("dummy",))
  51. except NoReverseMatch:
  52. raise ImproperlyConfigured(
  53. "Make sure you've included captcha.urls as explained in the INSTALLATION section on http://readthedocs.org/docs/django-simple-captcha/en/latest/usage.html#installation"
  54. )
  55. if settings.CAPTCHA_GET_FROM_POOL:
  56. key = CaptchaStore.pick()
  57. else:
  58. key = CaptchaStore.generate_key(generator)
  59. # these can be used by format_output and render
  60. self._value = [key, ""]
  61. self._key = key
  62. self.id_ = self.build_attrs(attrs).get("id", None)
  63. def id_for_label(self, id_):
  64. if id_:
  65. return id_ + "_1"
  66. return id_
  67. def image_url(self):
  68. return reverse("captcha-image", kwargs={"key": self._key})
  69. def audio_url(self):
  70. return (
  71. reverse("captcha-audio", kwargs={"key": self._key})
  72. if settings.CAPTCHA_FLITE_PATH
  73. else None
  74. )
  75. def refresh_url(self):
  76. return reverse("captcha-refresh")
  77. class CaptchaTextInput(BaseCaptchaTextInput):
  78. template_name = "captcha/widgets/captcha.html"
  79. def __init__(
  80. self,
  81. attrs=None,
  82. field_template=None,
  83. id_prefix=None,
  84. generator=None,
  85. output_format=None,
  86. ):
  87. self.id_prefix = id_prefix
  88. self.generator = generator
  89. if field_template is not None:
  90. msg = "CaptchaTextInput's field_template argument is deprecated in favor of widget's template_name."
  91. warnings.warn(msg, DeprecationWarning)
  92. self.field_template = field_template or settings.CAPTCHA_FIELD_TEMPLATE
  93. if output_format is not None:
  94. msg = "CaptchaTextInput's output_format argument is deprecated in favor of widget's template_name."
  95. warnings.warn(msg, DeprecationWarning)
  96. self.output_format = output_format or settings.CAPTCHA_OUTPUT_FORMAT
  97. # Fallback to custom rendering in Django < 1.11
  98. if (
  99. not hasattr(self, "_render")
  100. and self.field_template is None
  101. and self.output_format is None
  102. ):
  103. self.field_template = "captcha/field.html"
  104. if self.output_format:
  105. for key in ("image", "hidden_field", "text_field"):
  106. if "%%(%s)s" % key not in self.output_format:
  107. raise ImproperlyConfigured(
  108. "All of %s must be present in your CAPTCHA_OUTPUT_FORMAT setting. Could not find %s"
  109. % (
  110. ", ".join(
  111. [
  112. "%%(%s)s" % k
  113. for k in ("image", "hidden_field", "text_field")
  114. ]
  115. ),
  116. "%%(%s)s" % key,
  117. )
  118. )
  119. super(CaptchaTextInput, self).__init__(attrs)
  120. def build_attrs(self, *args, **kwargs):
  121. ret = super(CaptchaTextInput, self).build_attrs(*args, **kwargs)
  122. if self.id_prefix and "id" in ret:
  123. ret["id"] = "%s_%s" % (self.id_prefix, ret["id"])
  124. return ret
  125. def id_for_label(self, id_):
  126. ret = super(CaptchaTextInput, self).id_for_label(id_)
  127. if self.id_prefix and "id" in ret:
  128. ret = "%s_%s" % (self.id_prefix, ret)
  129. return ret
  130. def get_context(self, name, value, attrs):
  131. """Add captcha specific variables to context."""
  132. context = super(CaptchaTextInput, self).get_context(name, value, attrs)
  133. context["image"] = self.image_url()
  134. context["audio"] = self.audio_url()
  135. return context
  136. def format_output(self, rendered_widgets):
  137. # hidden_field, text_field = rendered_widgets
  138. if self.output_format:
  139. ret = self.output_format % {
  140. "image": self.image_and_audio,
  141. "hidden_field": self.hidden_field,
  142. "text_field": self.text_field,
  143. }
  144. return ret
  145. elif self.field_template:
  146. context = {
  147. "image": mark_safe(self.image_and_audio),
  148. "hidden_field": mark_safe(self.hidden_field),
  149. "text_field": mark_safe(self.text_field),
  150. }
  151. return render_to_string(self.field_template, context)
  152. def _direct_render(self, name, attrs):
  153. """Render the widget the old way - using field_template or output_format."""
  154. context = {
  155. "image": self.image_url(),
  156. "name": name,
  157. "key": self._key,
  158. "id": "%s_%s" % (self.id_prefix, attrs.get("id"))
  159. if self.id_prefix
  160. else attrs.get("id"),
  161. "audio": self.audio_url(),
  162. }
  163. self.image_and_audio = render_to_string(
  164. settings.CAPTCHA_IMAGE_TEMPLATE, context
  165. )
  166. self.hidden_field = render_to_string(
  167. settings.CAPTCHA_HIDDEN_FIELD_TEMPLATE, context
  168. )
  169. self.text_field = render_to_string(
  170. settings.CAPTCHA_TEXT_FIELD_TEMPLATE, context
  171. )
  172. return self.format_output(None)
  173. def render(self, name, value, attrs=None, renderer=None):
  174. self.fetch_captcha_store(name, value, attrs, self.generator)
  175. if self.field_template or self.output_format:
  176. return self._direct_render(name, attrs)
  177. extra_kwargs = {}
  178. if django.VERSION >= (1, 11):
  179. # https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#django.forms.Widget.render
  180. extra_kwargs["renderer"] = renderer
  181. return super(CaptchaTextInput, self).render(
  182. name, self._value, attrs=attrs, **extra_kwargs
  183. )
  184. class CaptchaField(MultiValueField):
  185. def __init__(self, *args, **kwargs):
  186. fields = (CharField(show_hidden_initial=True), CharField())
  187. if "error_messages" not in kwargs or "invalid" not in kwargs.get(
  188. "error_messages"
  189. ):
  190. if "error_messages" not in kwargs:
  191. kwargs["error_messages"] = {}
  192. kwargs["error_messages"].update(
  193. {"invalid": gettext_lazy("Invalid CAPTCHA")}
  194. )
  195. kwargs["widget"] = kwargs.pop(
  196. "widget",
  197. CaptchaTextInput(
  198. output_format=kwargs.pop("output_format", None),
  199. id_prefix=kwargs.pop("id_prefix", None),
  200. generator=kwargs.pop("generator", None),
  201. ),
  202. )
  203. super(CaptchaField, self).__init__(fields, *args, **kwargs)
  204. def compress(self, data_list):
  205. if data_list:
  206. return ",".join(data_list)
  207. return None
  208. def clean(self, value):
  209. super(CaptchaField, self).clean(value)
  210. response, value[1] = (value[1] or "").strip().lower(), ""
  211. if not settings.CAPTCHA_GET_FROM_POOL:
  212. CaptchaStore.remove_expired()
  213. if settings.CAPTCHA_TEST_MODE and response.lower() == "passed":
  214. # automatically pass the test
  215. try:
  216. # try to delete the captcha based on its hash
  217. CaptchaStore.objects.get(hashkey=value[0]).delete()
  218. except CaptchaStore.DoesNotExist:
  219. # ignore errors
  220. pass
  221. elif not self.required and not response:
  222. pass
  223. else:
  224. try:
  225. CaptchaStore.objects.get(
  226. response=response, hashkey=value[0], expiration__gt=timezone.now()
  227. ).delete()
  228. except CaptchaStore.DoesNotExist:
  229. raise ValidationError(
  230. getattr(self, "error_messages", {}).get(
  231. "invalid", gettext_lazy("Invalid CAPTCHA")
  232. )
  233. )
  234. return value