""" Form mixins for bot protection and validation. """ from django import forms from django.core.cache import cache from django.utils import timezone from datetime import timedelta import time class HoneypotMixin: """ Honeypot field mixin - adds a hidden field that bots will fill but humans won't. """ # This field should be left empty by real users website = forms.CharField( required=False, widget=forms.HiddenInput(attrs={'tabindex': '-1', 'autocomplete': 'off'}), label='', # Empty label so screen readers skip it ) def clean_website(self): """If this field is filled, it's likely a bot.""" website = self.cleaned_data.get('website') if website: raise forms.ValidationError('Bot detected. Please try again.') return website class TimeBasedValidationMixin: """ Time-based validation - prevents forms from being submitted too quickly (bot behavior). """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add a hidden timestamp field self.fields['form_timestamp'] = forms.CharField( required=False, widget=forms.HiddenInput(), initial=str(time.time()) ) def clean_form_timestamp(self): """Validate that form wasn't submitted too quickly.""" timestamp = self.cleaned_data.get('form_timestamp') if not timestamp: # If timestamp is missing, it might be a bot raise forms.ValidationError('Invalid form submission.') try: submit_time = float(timestamp) current_time = time.time() elapsed = current_time - submit_time # Forms submitted in less than 2 seconds are likely bots if elapsed < 2: raise forms.ValidationError('Form submitted too quickly. Please take your time.') # Forms submitted after 1 hour are likely stale if elapsed > 3600: raise forms.ValidationError('Form session expired. Please refresh and try again.') except (ValueError, TypeError): raise forms.ValidationError('Invalid form submission.') return timestamp class RateLimitMixin: """ Rate limiting mixin - prevents too many form submissions from the same IP/user. """ def __init__(self, *args, **kwargs): self.request = kwargs.pop('request', None) super().__init__(*args, **kwargs) def clean(self): cleaned_data = super().clean() if self.request: # Get client IP ip = self.get_client_ip(self.request) # Create a unique key for this form type form_name = self.__class__.__name__ cache_key = f'form_submission_{form_name}_{ip}' # Check submission count submissions = cache.get(cache_key, 0) # Limit: 10 submissions per hour per IP if submissions >= 10: raise forms.ValidationError( 'Too many submissions. Please wait before submitting again.' ) # Increment counter cache.set(cache_key, submissions + 1, 3600) # 1 hour return cleaned_data def get_client_ip(self, request): """Get client IP address.""" x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip class BrowserFingerprintMixin: """ Browser fingerprint validation - ensures form is submitted from a real browser. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['user_agent_hash'] = forms.CharField( required=False, widget=forms.HiddenInput() ) def clean_user_agent_hash(self): """Validate user agent is present and reasonable.""" ua_hash = self.cleaned_data.get('user_agent_hash') # If no user agent hash, it might be a bot if not ua_hash: raise forms.ValidationError('Invalid browser signature.') return ua_hash class BotProtectionMixin(HoneypotMixin, TimeBasedValidationMixin, RateLimitMixin): """ Combined bot protection mixin that includes: - Honeypot field - Time-based validation - Rate limiting """ pass