/django/core/validators.py
Python | 425 lines | 343 code | 55 blank | 27 comment | 48 complexity | 62f8e8d65b849c60cd79835a566aca7d MD5 | raw file
- from __future__ import unicode_literals
- import re
- from django.core.exceptions import ValidationError
- from django.utils import six
- from django.utils.deconstruct import deconstructible
- from django.utils.encoding import force_text
- from django.utils.functional import SimpleLazyObject
- from django.utils.ipv6 import is_valid_ipv6_address
- from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit
- from django.utils.translation import ugettext_lazy as _, ungettext_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, six.string_types):
- 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(object):
- 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, six.string_types):
- 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):
- """
- Validates that the input matches the regular expression
- if inverse_match is False, otherwise raises ValidationError.
- """
- if not (self.inverse_match is not bool(self.regex.search(
- force_text(value)))):
- 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)
- )
- def __ne__(self, other):
- return not (self == other)
- @deconstructible
- class URLValidator(RegexValidator):
- ul = '\u00a1-\uffff' # unicode letters range (must be a unicode string, not 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'\.(?:[a-z' + ul + r']{2,63}|xn--[a-z0-9]{1,59})\.?'
- 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(URLValidator, self).__init__(**kwargs)
- if schemes is not None:
- self.schemes = schemes
- def __call__(self, value):
- value = force_text(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(URLValidator, self).__call__(value)
- except ValidationError as e:
- # Trivial case failed. Try for possible IDN domain
- if value:
- scheme, netloc, path, query, fragment = urlsplit(value)
- try:
- netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
- except UnicodeError: # invalid domain part
- raise e
- url = urlunsplit((scheme, netloc, path, query, fragment))
- super(URLValidator, self).__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)
- url = 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 len(urlsplit(value).netloc) > 253:
- raise ValidationError(self.message, code=self.code)
- integer_validator = RegexValidator(
- _lazy_re_compile('^-?\d+\Z'),
- message=_('Enter a valid integer.'),
- code='invalid',
- )
- def validate_integer(value):
- return integer_validator(value)
- @deconstructible
- class EmailValidator(object):
- 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):
- value = force_text(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')
- if self.validate_domain_part(domain_part):
- return
- except UnicodeError:
- pass
- 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,
- _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."),
- 'invalid'
- )
- slug_unicode_re = _lazy_re_compile(r'^[-\w]+\Z', re.U)
- validate_unicode_slug = RegexValidator(
- slug_unicode_re,
- _("Enter a valid 'slug' consisting of Unicode letters, numbers, underscores, or hyphens."),
- 'invalid'
- )
- ipv4_re = _lazy_re_compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z')
- validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), '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 returns the appropriate validators for
- the GenericIPAddressField.
- This code is here, because it is exactly the same for the model and the form field.
- """
- 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'):
- regexp = _lazy_re_compile('^\d+(?:%s\d+)*\Z' % 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(object):
- compare = lambda self, a, b: a is not b
- clean = lambda self, x: x
- 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)
- )
- @deconstructible
- class MaxValueValidator(BaseValidator):
- compare = lambda self, a, b: a > b
- message = _('Ensure this value is less than or equal to %(limit_value)s.')
- code = 'max_value'
- @deconstructible
- class MinValueValidator(BaseValidator):
- compare = lambda self, a, b: a < b
- message = _('Ensure this value is greater than or equal to %(limit_value)s.')
- code = 'min_value'
- @deconstructible
- class MinLengthValidator(BaseValidator):
- compare = lambda self, a, b: a < b
- clean = lambda self, x: len(x)
- message = ungettext_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'
- @deconstructible
- class MaxLengthValidator(BaseValidator):
- compare = lambda self, a, b: a > b
- clean = lambda self, x: len(x)
- message = ungettext_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'
- @deconstructible
- class DecimalValidator(object):
- """
- Validate that the input does not exceed the maximum number of digits
- expected, otherwise raise ValidationError.
- """
- messages = {
- 'max_digits': ungettext_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': ungettext_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': ungettext_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
- )