123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- import ipaddress
- import os
- import re
- from urllib.parse import urlsplit, urlunsplit
- from django.core.exceptions import ValidationError
- from django.utils.deconstruct import deconstructible
- from django.utils.functional import SimpleLazyObject
- from django.utils.ipv6 import is_valid_ipv6_address
- from django.utils.translation import gettext_lazy as _, ngettext_lazy
- # These values, if given to validate(), will trigger the self.required check.
- EMPTY_VALUES = (None, '', [], (), {})
- def _lazy_re_compile(regex, flags=0):
- """Lazily compile a regex with flags."""
- def _compile():
- # Compile the regex if it was not passed pre-compiled.
- if isinstance(regex, str):
- return re.compile(regex, flags)
- else:
- assert not flags, "flags must be empty if regex is passed pre-compiled"
- return regex
- return SimpleLazyObject(_compile)
- @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 = bool(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)
- 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'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){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}(?<!-))*'
- tld_re = (
- r'\.' # dot
- r'(?!-)' # can't start with a dash
- r'(?:[a-z' + ul + '-]{2,63}' # domain label
- r'|xn--[a-z0-9]{1,59})' # or punycode label
- r'(?<!-)' # can't end with a dash
- r'\.?' # may have a trailing dot
- )
- 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'(?::\d{2,5})?' # port
- r'(?:[/?#][^\s]*)?' # resource path
- r'\Z', re.IGNORECASE)
- message = _('Enter a valid URL.')
- schemes = ['http', 'https', 'ftp', 'ftps']
- def __init__(self, schemes=None, **kwargs):
- super().__init__(**kwargs)
- if schemes is not None:
- self.schemes = schemes
- def __call__(self, value):
- # Check first if the scheme is valid
- scheme = value.split('://')[0].lower()
- if scheme not in self.schemes:
- raise ValidationError(self.message, code=self.code)
- # Then check full URL
- try:
- super().__call__(value)
- except ValidationError as e:
- # Trivial case failed. Try for possible IDN domain
- if value:
- try:
- scheme, netloc, path, query, fragment = urlsplit(value)
- except ValueError: # for example, "Invalid IPv6 URL"
- raise ValidationError(self.message, code=self.code)
- try:
- netloc = netloc.encode('idna').decode('ascii') # IDN -> 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'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
- if host_match:
- potential_ip = host_match.groups()[0]
- try:
- validate_ipv6_address(potential_ip)
- except ValidationError:
- raise ValidationError(self.message, code=self.code)
- # 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 len(urlsplit(value).netloc) > 253:
- raise ValidationError(self.message, code=self.code)
- 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}(?<!-))\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_whitelist = ['localhost']
- def __init__(self, message=None, code=None, whitelist=None):
- if message is not None:
- self.message = message
- if code is not None:
- self.code = code
- if whitelist is not None:
- self.domain_whitelist = whitelist
- def __call__(self, value):
- if not value or '@' not in value:
- raise ValidationError(self.message, code=self.code)
- user_part, domain_part = value.rsplit('@', 1)
- if not self.user_regex.match(user_part):
- raise ValidationError(self.message, code=self.code)
- if (domain_part not in self.domain_whitelist and
- not self.validate_domain_part(domain_part)):
- # Try for possible IDN domain-part
- try:
- domain_part = domain_part.encode('idna').decode('ascii')
- except UnicodeError:
- pass
- else:
- if self.validate_domain_part(domain_part):
- return
- raise ValidationError(self.message, code=self.code)
- 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.group(1)
- try:
- validate_ipv46_address(ip_address)
- return True
- except ValidationError:
- pass
- return False
- def __eq__(self, other):
- return (
- isinstance(other, EmailValidator) and
- (self.domain_whitelist == other.domain_whitelist) 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 IPv4 address.'), code='invalid')
- def validate_ipv6_address(value):
- if not is_valid_ipv6_address(value):
- raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid')
- def validate_ipv46_address(value):
- try:
- validate_ipv4_address(value)
- except ValidationError:
- try:
- validate_ipv6_address(value)
- except ValidationError:
- raise ValidationError(_('Enter a valid IPv4 or IPv6 address.'), code='invalid')
- ip_address_validator_map = {
- 'both': ([validate_ipv46_address], _('Enter a valid IPv4 or IPv6 address.')),
- 'ipv4': ([validate_ipv4_address], _('Enter a valid IPv4 address.')),
- 'ipv6': ([validate_ipv6_address], _('Enter a valid 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)
- params = {'limit_value': self.limit_value, 'show_value': cleaned, 'value': value}
- if self.compare(cleaned, self.limit_value):
- raise ValidationError(self.message, code=self.code, params=params)
- def __eq__(self, other):
- return (
- isinstance(other, self.__class__) and
- 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 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 = {
- '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:]
- decimals = abs(exponent)
- # digit_tuple doesn't include any leading zeros.
- digits = len(digit_tuple)
- if decimals > digits:
- # We have leading zeros up to or past the decimal point. Count
- # everything past the decimal point as a digit. We do not count
- # 0 before the decimal point as a digit since that would mean
- # we would not allow max_digits = decimal_places.
- digits = decimals
- 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},
- )
- 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},
- )
- 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)},
- )
- 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 = os.path.splitext(value.name)[1][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)
- }
- )
- 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]
- validate_image_file_extension = FileExtensionValidator(
- allowed_extensions=get_available_image_extensions(),
- )
- @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)
- def __eq__(self, other):
- return (
- isinstance(other, self.__class__) and
- self.message == other.message and
- self.code == other.code
- )
|