import ipaddress import re from pathlib import Path from urllib.parse import urlsplit, urlunsplit from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.encoding import punycode from django.utils.ipv6 import is_valid_ipv6_address from django.utils.regex_helper import _lazy_re_compile from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext_lazy # These values, if given to validate(), will trigger the self.required check. EMPTY_VALUES = (None, "", [], (), {}) @deconstructible class RegexValidator: regex = "" message = _("Enter a valid value.") code = "invalid" inverse_match = False flags = 0 def __init__( self, regex=None, message=None, code=None, inverse_match=None, flags=None ): if regex is not None: self.regex = regex if message is not None: self.message = message if code is not None: self.code = code if inverse_match is not None: self.inverse_match = inverse_match if flags is not None: self.flags = flags if self.flags and not isinstance(self.regex, str): raise TypeError( "If the flags are set, regex must be a regular expression string." ) self.regex = _lazy_re_compile(self.regex, self.flags) def __call__(self, value): """ Validate that the input contains (or does *not* contain, if inverse_match is True) a match for the regular expression. """ regex_matches = self.regex.search(str(value)) invalid_input = regex_matches if self.inverse_match else not regex_matches if invalid_input: raise ValidationError(self.message, code=self.code, params={"value": value}) def __eq__(self, other): return ( isinstance(other, RegexValidator) and self.regex.pattern == other.regex.pattern and self.regex.flags == other.regex.flags and (self.message == other.message) and (self.code == other.code) and (self.inverse_match == other.inverse_match) ) @deconstructible class URLValidator(RegexValidator): ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string). # IP patterns ipv4_re = ( r"(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)" r"(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}" ) ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later) # Host patterns hostname_re = ( r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?" ) # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1 domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(? ACE except UnicodeError: # invalid domain part raise e url = urlunsplit((scheme, netloc, path, query, fragment)) super().__call__(url) else: raise else: # Now verify IPv6 in the netloc part host_match = re.search(r"^\[(.+)\](?::[0-9]{1,5})?$", splitted_url.netloc) if host_match: potential_ip = host_match[1] try: validate_ipv6_address(potential_ip) except ValidationError: raise ValidationError( self.message, code=self.code, params={"value": value} ) # The maximum length of a full host name is 253 characters per RFC 1034 # section 3.1. It's defined to be 255 bytes or less, but this includes # one byte for the length of the name and one byte for the trailing dot # that's used to indicate absolute names in DNS. if splitted_url.hostname is None or len(splitted_url.hostname) > 253: raise ValidationError(self.message, code=self.code, params={"value": value}) integer_validator = RegexValidator( _lazy_re_compile(r"^-?\d+\Z"), message=_("Enter a valid integer."), code="invalid", ) def validate_integer(value): return integer_validator(value) @deconstructible class EmailValidator: message = _("Enter a valid email address.") code = "invalid" user_regex = _lazy_re_compile( r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # dot-atom r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)', # quoted-string re.IGNORECASE, ) domain_regex = _lazy_re_compile( # max length for domain name labels is 63 characters per RFC 1034 r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(? b @deconstructible class MinValueValidator(BaseValidator): message = _("Ensure this value is greater than or equal to %(limit_value)s.") code = "min_value" def compare(self, a, b): return a < b @deconstructible class MinLengthValidator(BaseValidator): message = ngettext_lazy( "Ensure this value has at least %(limit_value)d character (it has %(show_value)d).", "Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).", "limit_value", ) code = "min_length" def compare(self, a, b): return a < b def clean(self, x): return len(x) @deconstructible class MaxLengthValidator(BaseValidator): message = ngettext_lazy( "Ensure this value has at most %(limit_value)d character (it has %(show_value)d).", "Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).", "limit_value", ) code = "max_length" def compare(self, a, b): return a > b def clean(self, x): return len(x) @deconstructible class DecimalValidator: """ Validate that the input does not exceed the maximum number of digits expected, otherwise raise ValidationError. """ messages = { "invalid": _("Enter a number."), "max_digits": ngettext_lazy( "Ensure that there are no more than %(max)s digit in total.", "Ensure that there are no more than %(max)s digits in total.", "max", ), "max_decimal_places": ngettext_lazy( "Ensure that there are no more than %(max)s decimal place.", "Ensure that there are no more than %(max)s decimal places.", "max", ), "max_whole_digits": ngettext_lazy( "Ensure that there are no more than %(max)s digit before the decimal point.", "Ensure that there are no more than %(max)s digits before the decimal point.", "max", ), } def __init__(self, max_digits, decimal_places): self.max_digits = max_digits self.decimal_places = decimal_places def __call__(self, value): digit_tuple, exponent = value.as_tuple()[1:] if exponent in {"F", "n", "N"}: raise ValidationError( self.messages["invalid"], code="invalid", params={"value": value} ) if exponent >= 0: # A positive exponent adds that many trailing zeros. digits = len(digit_tuple) + exponent decimals = 0 else: # If the absolute value of the negative exponent is larger than the # number of digits, then it's the same as the number of digits, # because it'll consume all of the digits in digit_tuple and then # add abs(exponent) - len(digit_tuple) leading zeros after the # decimal point. if abs(exponent) > len(digit_tuple): digits = decimals = abs(exponent) else: digits = len(digit_tuple) decimals = abs(exponent) whole_digits = digits - decimals if self.max_digits is not None and digits > self.max_digits: raise ValidationError( self.messages["max_digits"], code="max_digits", params={"max": self.max_digits, "value": value}, ) if self.decimal_places is not None and decimals > self.decimal_places: raise ValidationError( self.messages["max_decimal_places"], code="max_decimal_places", params={"max": self.decimal_places, "value": value}, ) if ( self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places) ): raise ValidationError( self.messages["max_whole_digits"], code="max_whole_digits", params={"max": (self.max_digits - self.decimal_places), "value": value}, ) def __eq__(self, other): return ( isinstance(other, self.__class__) and self.max_digits == other.max_digits and self.decimal_places == other.decimal_places ) @deconstructible class FileExtensionValidator: message = _( "File extension “%(extension)s” is not allowed. " "Allowed extensions are: %(allowed_extensions)s." ) code = "invalid_extension" def __init__(self, allowed_extensions=None, message=None, code=None): if allowed_extensions is not None: allowed_extensions = [ allowed_extension.lower() for allowed_extension in allowed_extensions ] self.allowed_extensions = allowed_extensions if message is not None: self.message = message if code is not None: self.code = code def __call__(self, value): extension = Path(value.name).suffix[1:].lower() if ( self.allowed_extensions is not None and extension not in self.allowed_extensions ): raise ValidationError( self.message, code=self.code, params={ "extension": extension, "allowed_extensions": ", ".join(self.allowed_extensions), "value": value, }, ) def __eq__(self, other): return ( isinstance(other, self.__class__) and self.allowed_extensions == other.allowed_extensions and self.message == other.message and self.code == other.code ) def get_available_image_extensions(): try: from PIL import Image except ImportError: return [] else: Image.init() return [ext.lower()[1:] for ext in Image.EXTENSION] def validate_image_file_extension(value): return FileExtensionValidator(allowed_extensions=get_available_image_extensions())( value ) @deconstructible class ProhibitNullCharactersValidator: """Validate that the string doesn't contain the null character.""" message = _("Null characters are not allowed.") code = "null_characters_not_allowed" def __init__(self, message=None, code=None): if message is not None: self.message = message if code is not None: self.code = code def __call__(self, value): if "\x00" in str(value): raise ValidationError(self.message, code=self.code, params={"value": value}) def __eq__(self, other): return ( isinstance(other, self.__class__) and self.message == other.message and self.code == other.code )