import ipaddress import math import re from pathlib import Path from urllib.parse import urlsplit from django.core.exceptions import ValidationError from django.utils.deconstruct import deconstructible from django.utils.http import MAX_URL_LENGTH 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 DomainNameValidator(RegexValidator): message = _("Enter a valid domain name.") ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string). # 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}(? self.max_length: raise ValidationError(self.message, code=self.code, params={"value": value}) if not self.accept_idna and not value.isascii(): raise ValidationError(self.message, code=self.code, params={"value": value}) super().__call__(value) validate_domain_name = DomainNameValidator() @deconstructible class URLValidator(RegexValidator): # 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) hostname_re = DomainNameValidator.hostname_re domain_re = DomainNameValidator.domain_re tld_re = DomainNameValidator.tld_re host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)" regex = _lazy_re_compile( r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication r"(?:" + ipv4_re + "|" + ipv6_re + "|" + host_re + ")" r"(?::[0-9]{1,5})?" # port r"(?:[/?#][^\s]*)?" # resource path r"\Z", re.IGNORECASE, ) message = _("Enter a valid URL.") schemes = ["http", "https", "ftp", "ftps"] unsafe_chars = frozenset("\t\r\n") max_length = MAX_URL_LENGTH def __init__(self, schemes=None, **kwargs): super().__init__(**kwargs) if schemes is not None: self.schemes = schemes def __call__(self, value): if not isinstance(value, str) or len(value) > self.max_length: raise ValidationError(self.message, code=self.code, params={"value": value}) if self.unsafe_chars.intersection(value): raise ValidationError(self.message, code=self.code, params={"value": value}) # Check if the scheme is valid. scheme = value.split("://")[0].lower() if scheme not in self.schemes: raise ValidationError(self.message, code=self.code, params={"value": value}) # Then check full URL try: splitted_url = urlsplit(value) except ValueError: raise ValidationError(self.message, code=self.code, params={"value": value}) super().__call__(value) # 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" hostname_re = DomainNameValidator.hostname_re domain_re = DomainNameValidator.domain_re tld_no_fqdn_re = DomainNameValidator.tld_no_fqdn_re user_regex = _lazy_re_compile( # dot-atom r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # quoted-string r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])' r'*"\Z)', re.IGNORECASE, ) domain_regex = _lazy_re_compile( r"^" + hostname_re + domain_re + tld_no_fqdn_re + r"\Z", re.IGNORECASE, ) literal_regex = _lazy_re_compile( # literal form, ipv4 or ipv6 address (SMTP 4.1.3) r"\[([A-F0-9:.]+)\]\Z", re.IGNORECASE, ) domain_allowlist = ["localhost"] def __init__(self, message=None, code=None, allowlist=None): if message is not None: self.message = message if code is not None: self.code = code if allowlist is not None: self.domain_allowlist = allowlist def __call__(self, value): # The maximum length of an email is 320 characters per RFC 3696 # section 3. if not value or "@" not in value or len(value) > 320: raise ValidationError(self.message, code=self.code, params={"value": value}) user_part, domain_part = value.rsplit("@", 1) if not self.user_regex.match(user_part): raise ValidationError(self.message, code=self.code, params={"value": value}) if domain_part not in self.domain_allowlist and not self.validate_domain_part( domain_part ): raise ValidationError(self.message, code=self.code, params={"value": value}) def validate_domain_part(self, domain_part): if self.domain_regex.match(domain_part): return True literal_match = self.literal_regex.match(domain_part) if literal_match: ip_address = literal_match[1] try: validate_ipv46_address(ip_address) return True except ValidationError: pass return False def __eq__(self, other): return ( isinstance(other, EmailValidator) and (set(self.domain_allowlist) == set(other.domain_allowlist)) and (self.message == other.message) and (self.code == other.code) ) validate_email = EmailValidator() slug_re = _lazy_re_compile(r"^[-a-zA-Z0-9_]+\Z") validate_slug = RegexValidator( slug_re, # Translators: "letters" means latin letters: a-z and A-Z. _("Enter a valid “slug” consisting of letters, numbers, underscores or hyphens."), "invalid", ) slug_unicode_re = _lazy_re_compile(r"^[-\w]+\Z") validate_unicode_slug = RegexValidator( slug_unicode_re, _( "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " "hyphens." ), "invalid", ) def validate_ipv4_address(value): try: ipaddress.IPv4Address(value) except ValueError: raise ValidationError( _("Enter a valid %(protocol)s address."), code="invalid", params={"protocol": _("IPv4"), "value": value}, ) def validate_ipv6_address(value): if not is_valid_ipv6_address(value): raise ValidationError( _("Enter a valid %(protocol)s address."), code="invalid", params={"protocol": _("IPv6"), "value": value}, ) def validate_ipv46_address(value): try: validate_ipv4_address(value) except ValidationError: try: validate_ipv6_address(value) except ValidationError: raise ValidationError( _("Enter a valid %(protocol)s address."), code="invalid", params={"protocol": _("IPv4 or IPv6"), "value": value}, ) ip_address_validator_map = { "both": [validate_ipv46_address], "ipv4": [validate_ipv4_address], "ipv6": [validate_ipv6_address], } def ip_address_validators(protocol, unpack_ipv4): """ Depending on the given parameters, return the appropriate validators for the GenericIPAddressField. """ if protocol != "both" and unpack_ipv4: raise ValueError( "You can only use `unpack_ipv4` if `protocol` is set to 'both'" ) try: return ip_address_validator_map[protocol.lower()] except KeyError: raise ValueError( "The protocol '%s' is unknown. Supported: %s" % (protocol, list(ip_address_validator_map)) ) def int_list_validator(sep=",", message=None, code="invalid", allow_negative=False): regexp = _lazy_re_compile( r"^%(neg)s\d+(?:%(sep)s%(neg)s\d+)*\Z" % { "neg": "(-)?" if allow_negative else "", "sep": re.escape(sep), } ) return RegexValidator(regexp, message=message, code=code) validate_comma_separated_integer_list = int_list_validator( message=_("Enter only digits separated by commas."), ) @deconstructible class BaseValidator: message = _("Ensure this value is %(limit_value)s (it is %(show_value)s).") code = "limit_value" def __init__(self, limit_value, message=None): self.limit_value = limit_value if message: self.message = message def __call__(self, value): cleaned = self.clean(value) limit_value = ( self.limit_value() if callable(self.limit_value) else self.limit_value ) params = {"limit_value": limit_value, "show_value": cleaned, "value": value} if self.compare(cleaned, limit_value): raise ValidationError(self.message, code=self.code, params=params) def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented return ( self.limit_value == other.limit_value and self.message == other.message and self.code == other.code ) def compare(self, a, b): return a is not b def clean(self, x): return x @deconstructible class MaxValueValidator(BaseValidator): message = _("Ensure this value is less than or equal to %(limit_value)s.") code = "max_value" def compare(self, a, b): return a > 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 StepValueValidator(BaseValidator): message = _("Ensure this value is a multiple of step size %(limit_value)s.") code = "step_size" def __init__(self, limit_value, message=None, offset=None): super().__init__(limit_value, message) if offset is not None: self.message = _( "Ensure this value is a multiple of step size %(limit_value)s, " "starting from %(offset)s, e.g. %(offset)s, %(valid_value1)s, " "%(valid_value2)s, and so on." ) self.offset = offset def __call__(self, value): if self.offset is None: super().__call__(value) else: cleaned = self.clean(value) limit_value = ( self.limit_value() if callable(self.limit_value) else self.limit_value ) if self.compare(cleaned, limit_value): offset = cleaned.__class__(self.offset) params = { "limit_value": limit_value, "offset": offset, "valid_value1": offset + limit_value, "valid_value2": offset + 2 * limit_value, } raise ValidationError(self.message, code=self.code, params=params) def compare(self, a, b): offset = 0 if self.offset is None else self.offset return not math.isclose(math.remainder(a - offset, b), 0, abs_tol=1e-9) @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: digits = len(digit_tuple) if digit_tuple != (0,): # A positive exponent adds that many trailing zeros. digits += 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 set(self.allowed_extensions or []) == set(other.allowed_extensions or []) 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 )