OLD | NEW |
(Empty) | |
| 1 from __future__ import unicode_literals |
| 2 |
| 3 import os |
| 4 import re |
| 5 |
| 6 from django.core.exceptions import ValidationError |
| 7 from django.utils import six |
| 8 from django.utils.deconstruct import deconstructible |
| 9 from django.utils.encoding import force_text |
| 10 from django.utils.functional import SimpleLazyObject |
| 11 from django.utils.ipv6 import is_valid_ipv6_address |
| 12 from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit |
| 13 from django.utils.translation import ugettext_lazy as _, ungettext_lazy |
| 14 |
| 15 # These values, if given to validate(), will trigger the self.required check. |
| 16 EMPTY_VALUES = (None, '', [], (), {}) |
| 17 |
| 18 |
| 19 def _lazy_re_compile(regex, flags=0): |
| 20 """Lazily compile a regex with flags.""" |
| 21 def _compile(): |
| 22 # Compile the regex if it was not passed pre-compiled. |
| 23 if isinstance(regex, six.string_types): |
| 24 return re.compile(regex, flags) |
| 25 else: |
| 26 assert not flags, "flags must be empty if regex is passed pre-compil
ed" |
| 27 return regex |
| 28 return SimpleLazyObject(_compile) |
| 29 |
| 30 |
| 31 @deconstructible |
| 32 class RegexValidator(object): |
| 33 regex = '' |
| 34 message = _('Enter a valid value.') |
| 35 code = 'invalid' |
| 36 inverse_match = False |
| 37 flags = 0 |
| 38 |
| 39 def __init__(self, regex=None, message=None, code=None, inverse_match=None,
flags=None): |
| 40 if regex is not None: |
| 41 self.regex = regex |
| 42 if message is not None: |
| 43 self.message = message |
| 44 if code is not None: |
| 45 self.code = code |
| 46 if inverse_match is not None: |
| 47 self.inverse_match = inverse_match |
| 48 if flags is not None: |
| 49 self.flags = flags |
| 50 if self.flags and not isinstance(self.regex, six.string_types): |
| 51 raise TypeError("If the flags are set, regex must be a regular expre
ssion string.") |
| 52 |
| 53 self.regex = _lazy_re_compile(self.regex, self.flags) |
| 54 |
| 55 def __call__(self, value): |
| 56 """ |
| 57 Validate that the input contains a match for the regular expression |
| 58 if inverse_match is False, otherwise raise ValidationError. |
| 59 """ |
| 60 if not (self.inverse_match is not bool(self.regex.search( |
| 61 force_text(value)))): |
| 62 raise ValidationError(self.message, code=self.code) |
| 63 |
| 64 def __eq__(self, other): |
| 65 return ( |
| 66 isinstance(other, RegexValidator) and |
| 67 self.regex.pattern == other.regex.pattern and |
| 68 self.regex.flags == other.regex.flags and |
| 69 (self.message == other.message) and |
| 70 (self.code == other.code) and |
| 71 (self.inverse_match == other.inverse_match) |
| 72 ) |
| 73 |
| 74 def __ne__(self, other): |
| 75 return not (self == other) |
| 76 |
| 77 |
| 78 @deconstructible |
| 79 class URLValidator(RegexValidator): |
| 80 ul = '\u00a1-\uffff' # unicode letters range (must be a unicode string, not
a raw string) |
| 81 |
| 82 # IP patterns |
| 83 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}' |
| 84 ipv6_re = r'\[[0-9a-f:\.]+\]' # (simple regex, validated later) |
| 85 |
| 86 # Host patterns |
| 87 hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul +
r'0-9])?' |
| 88 # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1 |
| 89 domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*' |
| 90 tld_re = ( |
| 91 r'\.' # dot |
| 92 r'(?!-)' # can't start with a dash |
| 93 r'(?:[a-z' + ul + '-]{2,63}' # domain label |
| 94 r'|xn--[a-z0-9]{1,59})' # or punycode label |
| 95 r'(?<!-)' # can't end with a dash |
| 96 r'\.?' # may have a trailing dot |
| 97 ) |
| 98 host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)' |
| 99 |
| 100 regex = _lazy_re_compile( |
| 101 r'^(?:[a-z0-9\.\-\+]*)://' # scheme is validated separately |
| 102 r'(?:\S+(?::\S*)?@)?' # user:pass authentication |
| 103 r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')' |
| 104 r'(?::\d{2,5})?' # port |
| 105 r'(?:[/?#][^\s]*)?' # resource path |
| 106 r'\Z', re.IGNORECASE) |
| 107 message = _('Enter a valid URL.') |
| 108 schemes = ['http', 'https', 'ftp', 'ftps'] |
| 109 |
| 110 def __init__(self, schemes=None, **kwargs): |
| 111 super(URLValidator, self).__init__(**kwargs) |
| 112 if schemes is not None: |
| 113 self.schemes = schemes |
| 114 |
| 115 def __call__(self, value): |
| 116 value = force_text(value) |
| 117 # Check first if the scheme is valid |
| 118 scheme = value.split('://')[0].lower() |
| 119 if scheme not in self.schemes: |
| 120 raise ValidationError(self.message, code=self.code) |
| 121 |
| 122 # Then check full URL |
| 123 try: |
| 124 super(URLValidator, self).__call__(value) |
| 125 except ValidationError as e: |
| 126 # Trivial case failed. Try for possible IDN domain |
| 127 if value: |
| 128 try: |
| 129 scheme, netloc, path, query, fragment = urlsplit(value) |
| 130 except ValueError: # for example, "Invalid IPv6 URL" |
| 131 raise ValidationError(self.message, code=self.code) |
| 132 try: |
| 133 netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE |
| 134 except UnicodeError: # invalid domain part |
| 135 raise e |
| 136 url = urlunsplit((scheme, netloc, path, query, fragment)) |
| 137 super(URLValidator, self).__call__(url) |
| 138 else: |
| 139 raise |
| 140 else: |
| 141 # Now verify IPv6 in the netloc part |
| 142 host_match = re.search(r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).n
etloc) |
| 143 if host_match: |
| 144 potential_ip = host_match.groups()[0] |
| 145 try: |
| 146 validate_ipv6_address(potential_ip) |
| 147 except ValidationError: |
| 148 raise ValidationError(self.message, code=self.code) |
| 149 |
| 150 # The maximum length of a full host name is 253 characters per RFC 1034 |
| 151 # section 3.1. It's defined to be 255 bytes or less, but this includes |
| 152 # one byte for the length of the name and one byte for the trailing dot |
| 153 # that's used to indicate absolute names in DNS. |
| 154 if len(urlsplit(value).netloc) > 253: |
| 155 raise ValidationError(self.message, code=self.code) |
| 156 |
| 157 |
| 158 integer_validator = RegexValidator( |
| 159 _lazy_re_compile(r'^-?\d+\Z'), |
| 160 message=_('Enter a valid integer.'), |
| 161 code='invalid', |
| 162 ) |
| 163 |
| 164 |
| 165 def validate_integer(value): |
| 166 return integer_validator(value) |
| 167 |
| 168 |
| 169 @deconstructible |
| 170 class EmailValidator(object): |
| 171 message = _('Enter a valid email address.') |
| 172 code = 'invalid' |
| 173 user_regex = _lazy_re_compile( |
| 174 r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" #
dot-atom |
| 175 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-
\177])*"\Z)', # quoted-string |
| 176 re.IGNORECASE) |
| 177 domain_regex = _lazy_re_compile( |
| 178 # max length for domain name labels is 63 characters per RFC 1034 |
| 179 r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))
\Z', |
| 180 re.IGNORECASE) |
| 181 literal_regex = _lazy_re_compile( |
| 182 # literal form, ipv4 or ipv6 address (SMTP 4.1.3) |
| 183 r'\[([A-f0-9:\.]+)\]\Z', |
| 184 re.IGNORECASE) |
| 185 domain_whitelist = ['localhost'] |
| 186 |
| 187 def __init__(self, message=None, code=None, whitelist=None): |
| 188 if message is not None: |
| 189 self.message = message |
| 190 if code is not None: |
| 191 self.code = code |
| 192 if whitelist is not None: |
| 193 self.domain_whitelist = whitelist |
| 194 |
| 195 def __call__(self, value): |
| 196 value = force_text(value) |
| 197 |
| 198 if not value or '@' not in value: |
| 199 raise ValidationError(self.message, code=self.code) |
| 200 |
| 201 user_part, domain_part = value.rsplit('@', 1) |
| 202 |
| 203 if not self.user_regex.match(user_part): |
| 204 raise ValidationError(self.message, code=self.code) |
| 205 |
| 206 if (domain_part not in self.domain_whitelist and |
| 207 not self.validate_domain_part(domain_part)): |
| 208 # Try for possible IDN domain-part |
| 209 try: |
| 210 domain_part = domain_part.encode('idna').decode('ascii') |
| 211 if self.validate_domain_part(domain_part): |
| 212 return |
| 213 except UnicodeError: |
| 214 pass |
| 215 raise ValidationError(self.message, code=self.code) |
| 216 |
| 217 def validate_domain_part(self, domain_part): |
| 218 if self.domain_regex.match(domain_part): |
| 219 return True |
| 220 |
| 221 literal_match = self.literal_regex.match(domain_part) |
| 222 if literal_match: |
| 223 ip_address = literal_match.group(1) |
| 224 try: |
| 225 validate_ipv46_address(ip_address) |
| 226 return True |
| 227 except ValidationError: |
| 228 pass |
| 229 return False |
| 230 |
| 231 def __eq__(self, other): |
| 232 return ( |
| 233 isinstance(other, EmailValidator) and |
| 234 (self.domain_whitelist == other.domain_whitelist) and |
| 235 (self.message == other.message) and |
| 236 (self.code == other.code) |
| 237 ) |
| 238 |
| 239 |
| 240 validate_email = EmailValidator() |
| 241 |
| 242 slug_re = _lazy_re_compile(r'^[-a-zA-Z0-9_]+\Z') |
| 243 validate_slug = RegexValidator( |
| 244 slug_re, |
| 245 _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphe
ns."), |
| 246 'invalid' |
| 247 ) |
| 248 |
| 249 slug_unicode_re = _lazy_re_compile(r'^[-\w]+\Z', re.U) |
| 250 validate_unicode_slug = RegexValidator( |
| 251 slug_unicode_re, |
| 252 _("Enter a valid 'slug' consisting of Unicode letters, numbers, underscores,
or hyphens."), |
| 253 'invalid' |
| 254 ) |
| 255 |
| 256 ipv4_re = _lazy_re_compile(r'^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])(\.(25[0-5]
|2[0-4][0-9]|[0-1]?[0-9]?[0-9])){3}\Z') |
| 257 validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.')
, 'invalid') |
| 258 |
| 259 |
| 260 def validate_ipv6_address(value): |
| 261 if not is_valid_ipv6_address(value): |
| 262 raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid') |
| 263 |
| 264 |
| 265 def validate_ipv46_address(value): |
| 266 try: |
| 267 validate_ipv4_address(value) |
| 268 except ValidationError: |
| 269 try: |
| 270 validate_ipv6_address(value) |
| 271 except ValidationError: |
| 272 raise ValidationError(_('Enter a valid IPv4 or IPv6 address.'), code
='invalid') |
| 273 |
| 274 |
| 275 ip_address_validator_map = { |
| 276 'both': ([validate_ipv46_address], _('Enter a valid IPv4 or IPv6 address.'))
, |
| 277 'ipv4': ([validate_ipv4_address], _('Enter a valid IPv4 address.')), |
| 278 'ipv6': ([validate_ipv6_address], _('Enter a valid IPv6 address.')), |
| 279 } |
| 280 |
| 281 |
| 282 def ip_address_validators(protocol, unpack_ipv4): |
| 283 """ |
| 284 Depending on the given parameters returns the appropriate validators for |
| 285 the GenericIPAddressField. |
| 286 |
| 287 This code is here, because it is exactly the same for the model and the form
field. |
| 288 """ |
| 289 if protocol != 'both' and unpack_ipv4: |
| 290 raise ValueError( |
| 291 "You can only use `unpack_ipv4` if `protocol` is set to 'both'") |
| 292 try: |
| 293 return ip_address_validator_map[protocol.lower()] |
| 294 except KeyError: |
| 295 raise ValueError("The protocol '%s' is unknown. Supported: %s" |
| 296 % (protocol, list(ip_address_validator_map))) |
| 297 |
| 298 |
| 299 def int_list_validator(sep=',', message=None, code='invalid', allow_negative=Fal
se): |
| 300 regexp = _lazy_re_compile(r'^%(neg)s\d+(?:%(sep)s%(neg)s\d+)*\Z' % { |
| 301 'neg': '(-)?' if allow_negative else '', |
| 302 'sep': re.escape(sep), |
| 303 }) |
| 304 return RegexValidator(regexp, message=message, code=code) |
| 305 |
| 306 |
| 307 validate_comma_separated_integer_list = int_list_validator( |
| 308 message=_('Enter only digits separated by commas.'), |
| 309 ) |
| 310 |
| 311 |
| 312 @deconstructible |
| 313 class BaseValidator(object): |
| 314 message = _('Ensure this value is %(limit_value)s (it is %(show_value)s).') |
| 315 code = 'limit_value' |
| 316 |
| 317 def __init__(self, limit_value, message=None): |
| 318 self.limit_value = limit_value |
| 319 if message: |
| 320 self.message = message |
| 321 |
| 322 def __call__(self, value): |
| 323 cleaned = self.clean(value) |
| 324 params = {'limit_value': self.limit_value, 'show_value': cleaned, 'value
': value} |
| 325 if self.compare(cleaned, self.limit_value): |
| 326 raise ValidationError(self.message, code=self.code, params=params) |
| 327 |
| 328 def __eq__(self, other): |
| 329 return ( |
| 330 isinstance(other, self.__class__) and |
| 331 self.limit_value == other.limit_value and |
| 332 self.message == other.message and |
| 333 self.code == other.code |
| 334 ) |
| 335 |
| 336 def compare(self, a, b): |
| 337 return a is not b |
| 338 |
| 339 def clean(self, x): |
| 340 return x |
| 341 |
| 342 |
| 343 @deconstructible |
| 344 class MaxValueValidator(BaseValidator): |
| 345 message = _('Ensure this value is less than or equal to %(limit_value)s.') |
| 346 code = 'max_value' |
| 347 |
| 348 def compare(self, a, b): |
| 349 return a > b |
| 350 |
| 351 |
| 352 @deconstructible |
| 353 class MinValueValidator(BaseValidator): |
| 354 message = _('Ensure this value is greater than or equal to %(limit_value)s.'
) |
| 355 code = 'min_value' |
| 356 |
| 357 def compare(self, a, b): |
| 358 return a < b |
| 359 |
| 360 |
| 361 @deconstructible |
| 362 class MinLengthValidator(BaseValidator): |
| 363 message = ungettext_lazy( |
| 364 'Ensure this value has at least %(limit_value)d character (it has %(show
_value)d).', |
| 365 'Ensure this value has at least %(limit_value)d characters (it has %(sho
w_value)d).', |
| 366 'limit_value') |
| 367 code = 'min_length' |
| 368 |
| 369 def compare(self, a, b): |
| 370 return a < b |
| 371 |
| 372 def clean(self, x): |
| 373 return len(x) |
| 374 |
| 375 |
| 376 @deconstructible |
| 377 class MaxLengthValidator(BaseValidator): |
| 378 message = ungettext_lazy( |
| 379 'Ensure this value has at most %(limit_value)d character (it has %(show_
value)d).', |
| 380 'Ensure this value has at most %(limit_value)d characters (it has %(show
_value)d).', |
| 381 'limit_value') |
| 382 code = 'max_length' |
| 383 |
| 384 def compare(self, a, b): |
| 385 return a > b |
| 386 |
| 387 def clean(self, x): |
| 388 return len(x) |
| 389 |
| 390 |
| 391 @deconstructible |
| 392 class DecimalValidator(object): |
| 393 """ |
| 394 Validate that the input does not exceed the maximum number of digits |
| 395 expected, otherwise raise ValidationError. |
| 396 """ |
| 397 messages = { |
| 398 'max_digits': ungettext_lazy( |
| 399 'Ensure that there are no more than %(max)s digit in total.', |
| 400 'Ensure that there are no more than %(max)s digits in total.', |
| 401 'max' |
| 402 ), |
| 403 'max_decimal_places': ungettext_lazy( |
| 404 'Ensure that there are no more than %(max)s decimal place.', |
| 405 'Ensure that there are no more than %(max)s decimal places.', |
| 406 'max' |
| 407 ), |
| 408 'max_whole_digits': ungettext_lazy( |
| 409 'Ensure that there are no more than %(max)s digit before the decimal
point.', |
| 410 'Ensure that there are no more than %(max)s digits before the decima
l point.', |
| 411 'max' |
| 412 ), |
| 413 } |
| 414 |
| 415 def __init__(self, max_digits, decimal_places): |
| 416 self.max_digits = max_digits |
| 417 self.decimal_places = decimal_places |
| 418 |
| 419 def __call__(self, value): |
| 420 digit_tuple, exponent = value.as_tuple()[1:] |
| 421 decimals = abs(exponent) |
| 422 # digit_tuple doesn't include any leading zeros. |
| 423 digits = len(digit_tuple) |
| 424 if decimals > digits: |
| 425 # We have leading zeros up to or past the decimal point. Count |
| 426 # everything past the decimal point as a digit. We do not count |
| 427 # 0 before the decimal point as a digit since that would mean |
| 428 # we would not allow max_digits = decimal_places. |
| 429 digits = decimals |
| 430 whole_digits = digits - decimals |
| 431 |
| 432 if self.max_digits is not None and digits > self.max_digits: |
| 433 raise ValidationError( |
| 434 self.messages['max_digits'], |
| 435 code='max_digits', |
| 436 params={'max': self.max_digits}, |
| 437 ) |
| 438 if self.decimal_places is not None and decimals > self.decimal_places: |
| 439 raise ValidationError( |
| 440 self.messages['max_decimal_places'], |
| 441 code='max_decimal_places', |
| 442 params={'max': self.decimal_places}, |
| 443 ) |
| 444 if (self.max_digits is not None and self.decimal_places is not None and |
| 445 whole_digits > (self.max_digits - self.decimal_places)): |
| 446 raise ValidationError( |
| 447 self.messages['max_whole_digits'], |
| 448 code='max_whole_digits', |
| 449 params={'max': (self.max_digits - self.decimal_places)}, |
| 450 ) |
| 451 |
| 452 def __eq__(self, other): |
| 453 return ( |
| 454 isinstance(other, self.__class__) and |
| 455 self.max_digits == other.max_digits and |
| 456 self.decimal_places == other.decimal_places |
| 457 ) |
| 458 |
| 459 |
| 460 @deconstructible |
| 461 class FileExtensionValidator(object): |
| 462 message = _( |
| 463 "File extension '%(extension)s' is not allowed. " |
| 464 "Allowed extensions are: '%(allowed_extensions)s'." |
| 465 ) |
| 466 code = 'invalid_extension' |
| 467 |
| 468 def __init__(self, allowed_extensions=None, message=None, code=None): |
| 469 self.allowed_extensions = allowed_extensions |
| 470 if message is not None: |
| 471 self.message = message |
| 472 if code is not None: |
| 473 self.code = code |
| 474 |
| 475 def __call__(self, value): |
| 476 extension = os.path.splitext(value.name)[1][1:].lower() |
| 477 if self.allowed_extensions is not None and extension not in self.allowed
_extensions: |
| 478 raise ValidationError( |
| 479 self.message, |
| 480 code=self.code, |
| 481 params={ |
| 482 'extension': extension, |
| 483 'allowed_extensions': ', '.join(self.allowed_extensions) |
| 484 } |
| 485 ) |
| 486 |
| 487 def __eq__(self, other): |
| 488 return ( |
| 489 isinstance(other, self.__class__) and |
| 490 self.allowed_extensions == other.allowed_extensions and |
| 491 self.message == other.message and |
| 492 self.code == other.code |
| 493 ) |
| 494 |
| 495 |
| 496 def get_available_image_extensions(): |
| 497 try: |
| 498 from PIL import Image |
| 499 except ImportError: |
| 500 return [] |
| 501 else: |
| 502 Image.init() |
| 503 return [ext.lower()[1:] for ext in Image.EXTENSION.keys()] |
| 504 |
| 505 |
| 506 validate_image_file_extension = FileExtensionValidator( |
| 507 allowed_extensions=get_available_image_extensions(), |
| 508 ) |
OLD | NEW |