import warnings import django from django.core.exceptions import ImproperlyConfigured from django.forms import ValidationError from django.forms.fields import CharField, MultiValueField from django.forms.widgets import HiddenInput, MultiWidget, TextInput from django.template.loader import render_to_string from django.urls import NoReverseMatch, reverse from django.utils import timezone from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy from captcha.conf import settings from captcha.models import CaptchaStore class CaptchaHiddenInput(HiddenInput): """Hidden input for the captcha key.""" # Use *args and **kwargs because signature changed in Django 1.11 def build_attrs(self, *args, **kwargs): """Disable autocomplete to prevent problems on page reload.""" attrs = super().build_attrs(*args, **kwargs) attrs["autocomplete"] = "off" return attrs class CaptchaAnswerInput(TextInput): """Text input for captcha answer.""" # Use *args and **kwargs because signature changed in Django 1.11 def build_attrs(self, *args, **kwargs): """Disable automatic corrections and completions.""" attrs = super().build_attrs(*args, **kwargs) attrs["autocapitalize"] = "off" attrs["autocomplete"] = "off" attrs["autocorrect"] = "off" attrs["spellcheck"] = "false" return attrs class BaseCaptchaTextInput(MultiWidget): """ Base class for Captcha widgets """ def __init__(self, attrs=None): widgets = (CaptchaHiddenInput(attrs), CaptchaAnswerInput(attrs)) super(BaseCaptchaTextInput, self).__init__(widgets, attrs) def decompress(self, value): if value: return value.split(",") return [None, None] def fetch_captcha_store(self, name, value, attrs=None, generator=None): """ Fetches a new CaptchaStore This has to be called inside render """ try: reverse("captcha-image", args=("dummy",)) except NoReverseMatch: raise ImproperlyConfigured( "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" ) if settings.CAPTCHA_GET_FROM_POOL: key = CaptchaStore.pick() else: key = CaptchaStore.generate_key(generator) # these can be used by format_output and render self._value = [key, ""] self._key = key self.id_ = self.build_attrs(attrs).get("id", None) def id_for_label(self, id_): if id_: return id_ + "_1" return id_ def image_url(self): return reverse("captcha-image", kwargs={"key": self._key}) def audio_url(self): return ( reverse("captcha-audio", kwargs={"key": self._key}) if settings.CAPTCHA_FLITE_PATH else None ) def refresh_url(self): return reverse("captcha-refresh") class CaptchaTextInput(BaseCaptchaTextInput): template_name = "captcha/widgets/captcha.html" def __init__( self, attrs=None, field_template=None, id_prefix=None, generator=None, output_format=None, ): self.id_prefix = id_prefix self.generator = generator if field_template is not None: msg = "CaptchaTextInput's field_template argument is deprecated in favor of widget's template_name." warnings.warn(msg, DeprecationWarning) self.field_template = field_template or settings.CAPTCHA_FIELD_TEMPLATE if output_format is not None: msg = "CaptchaTextInput's output_format argument is deprecated in favor of widget's template_name." warnings.warn(msg, DeprecationWarning) self.output_format = output_format or settings.CAPTCHA_OUTPUT_FORMAT # Fallback to custom rendering in Django < 1.11 if ( not hasattr(self, "_render") and self.field_template is None and self.output_format is None ): self.field_template = "captcha/field.html" if self.output_format: for key in ("image", "hidden_field", "text_field"): if "%%(%s)s" % key not in self.output_format: raise ImproperlyConfigured( "All of %s must be present in your CAPTCHA_OUTPUT_FORMAT setting. Could not find %s" % ( ", ".join( [ "%%(%s)s" % k for k in ("image", "hidden_field", "text_field") ] ), "%%(%s)s" % key, ) ) super(CaptchaTextInput, self).__init__(attrs) def build_attrs(self, *args, **kwargs): ret = super(CaptchaTextInput, self).build_attrs(*args, **kwargs) if self.id_prefix and "id" in ret: ret["id"] = "%s_%s" % (self.id_prefix, ret["id"]) return ret def id_for_label(self, id_): ret = super(CaptchaTextInput, self).id_for_label(id_) if self.id_prefix and "id" in ret: ret = "%s_%s" % (self.id_prefix, ret) return ret def get_context(self, name, value, attrs): """Add captcha specific variables to context.""" context = super(CaptchaTextInput, self).get_context(name, value, attrs) context["image"] = self.image_url() context["audio"] = self.audio_url() return context def format_output(self, rendered_widgets): # hidden_field, text_field = rendered_widgets if self.output_format: ret = self.output_format % { "image": self.image_and_audio, "hidden_field": self.hidden_field, "text_field": self.text_field, } return ret elif self.field_template: context = { "image": mark_safe(self.image_and_audio), "hidden_field": mark_safe(self.hidden_field), "text_field": mark_safe(self.text_field), } return render_to_string(self.field_template, context) def _direct_render(self, name, attrs): """Render the widget the old way - using field_template or output_format.""" context = { "image": self.image_url(), "name": name, "key": self._key, "id": "%s_%s" % (self.id_prefix, attrs.get("id")) if self.id_prefix else attrs.get("id"), "audio": self.audio_url(), } self.image_and_audio = render_to_string( settings.CAPTCHA_IMAGE_TEMPLATE, context ) self.hidden_field = render_to_string( settings.CAPTCHA_HIDDEN_FIELD_TEMPLATE, context ) self.text_field = render_to_string( settings.CAPTCHA_TEXT_FIELD_TEMPLATE, context ) return self.format_output(None) def render(self, name, value, attrs=None, renderer=None): self.fetch_captcha_store(name, value, attrs, self.generator) if self.field_template or self.output_format: return self._direct_render(name, attrs) extra_kwargs = {} if django.VERSION >= (1, 11): # https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#django.forms.Widget.render extra_kwargs["renderer"] = renderer return super(CaptchaTextInput, self).render( name, self._value, attrs=attrs, **extra_kwargs ) class CaptchaField(MultiValueField): def __init__(self, *args, **kwargs): fields = (CharField(show_hidden_initial=True), CharField()) if "error_messages" not in kwargs or "invalid" not in kwargs.get( "error_messages" ): if "error_messages" not in kwargs: kwargs["error_messages"] = {} kwargs["error_messages"].update( {"invalid": gettext_lazy("Invalid CAPTCHA")} ) kwargs["widget"] = kwargs.pop( "widget", CaptchaTextInput( output_format=kwargs.pop("output_format", None), id_prefix=kwargs.pop("id_prefix", None), generator=kwargs.pop("generator", None), ), ) super(CaptchaField, self).__init__(fields, *args, **kwargs) def compress(self, data_list): if data_list: return ",".join(data_list) return None def clean(self, value): super(CaptchaField, self).clean(value) response, value[1] = (value[1] or "").strip().lower(), "" if not settings.CAPTCHA_GET_FROM_POOL: CaptchaStore.remove_expired() if settings.CAPTCHA_TEST_MODE and response.lower() == "passed": # automatically pass the test try: # try to delete the captcha based on its hash CaptchaStore.objects.get(hashkey=value[0]).delete() except CaptchaStore.DoesNotExist: # ignore errors pass elif not self.required and not response: pass else: try: CaptchaStore.objects.get( response=response, hashkey=value[0], expiration__gt=timezone.now() ).delete() except CaptchaStore.DoesNotExist: raise ValidationError( getattr(self, "error_messages", {}).get( "invalid", gettext_lazy("Invalid CAPTCHA") ) ) return value