1import ipaddress
2import math
3import re
4from pathlib import Path
5from urllib.parse import urlsplit
6
7from django.core.exceptions import ValidationError
8from django.utils.deconstruct import deconstructible
9from django.utils.ipv6 import is_valid_ipv6_address
10from django.utils.regex_helper import _lazy_re_compile
11from django.utils.translation import gettext_lazy as _
12from django.utils.translation import ngettext_lazy
13
14# These values, if given to validate(), will trigger the self.required check.
15EMPTY_VALUES = (None, "", [], (), {})
16
17
18@deconstructible
19class RegexValidator:
20 regex = ""
21 message = _("Enter a valid value.")
22 code = "invalid"
23 inverse_match = False
24 flags = 0
25
26 def __init__(
27 self, regex=None, message=None, code=None, inverse_match=None, flags=None
28 ):
29 if regex is not None:
30 self.regex = regex
31 if message is not None:
32 self.message = message
33 if code is not None:
34 self.code = code
35 if inverse_match is not None:
36 self.inverse_match = inverse_match
37 if flags is not None:
38 self.flags = flags
39 if self.flags and not isinstance(self.regex, str):
40 raise TypeError(
41 "If the flags are set, regex must be a regular expression string."
42 )
43
44 self.regex = _lazy_re_compile(self.regex, self.flags)
45
46 def __call__(self, value):
47 """
48 Validate that the input contains (or does *not* contain, if
49 inverse_match is True) a match for the regular expression.
50 """
51 regex_matches = self.regex.search(str(value))
52 invalid_input = regex_matches if self.inverse_match else not regex_matches
53 if invalid_input:
54 raise ValidationError(self.message, code=self.code, params={"value": value})
55
56 def __eq__(self, other):
57 return (
58 isinstance(other, RegexValidator)
59 and self.regex.pattern == other.regex.pattern
60 and self.regex.flags == other.regex.flags
61 and (self.message == other.message)
62 and (self.code == other.code)
63 and (self.inverse_match == other.inverse_match)
64 )
65
66
67@deconstructible
68class DomainNameValidator(RegexValidator):
69 message = _("Enter a valid domain name.")
70 ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string).
71 # Host patterns.
72 hostname_re = (
73 r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
74 )
75 # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1.
76 domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
77 # Top-level domain.
78 tld_no_fqdn_re = (
79 r"\." # dot
80 r"(?!-)" # can't start with a dash
81 r"(?:[a-z" + ul + "-]{2,63}" # domain label
82 r"|xn--[a-z0-9]{1,59})" # or punycode label
83 r"(?<!-)" # can't end with a dash
84 )
85 tld_re = tld_no_fqdn_re + r"\.?"
86 ascii_only_hostname_re = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
87 ascii_only_domain_re = r"(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*"
88 ascii_only_tld_re = (
89 r"\." # dot
90 r"(?!-)" # can't start with a dash
91 r"(?:[a-zA-Z0-9-]{2,63})" # domain label
92 r"(?<!-)" # can't end with a dash
93 r"\.?" # may have a trailing dot
94 )
95
96 max_length = 255
97
98 def __init__(self, **kwargs):
99 self.accept_idna = kwargs.pop("accept_idna", True)
100
101 if self.accept_idna:
102 self.regex = _lazy_re_compile(
103 r"^" + self.hostname_re + self.domain_re + self.tld_re + r"$",
104 re.IGNORECASE,
105 )
106 else:
107 self.regex = _lazy_re_compile(
108 r"^"
109 + self.ascii_only_hostname_re
110 + self.ascii_only_domain_re
111 + self.ascii_only_tld_re
112 + r"$",
113 re.IGNORECASE,
114 )
115 super().__init__(**kwargs)
116
117 def __call__(self, value):
118 if not isinstance(value, str) or len(value) > self.max_length:
119 raise ValidationError(self.message, code=self.code, params={"value": value})
120 if not self.accept_idna and not value.isascii():
121 raise ValidationError(self.message, code=self.code, params={"value": value})
122 super().__call__(value)
123
124
125validate_domain_name = DomainNameValidator()
126
127
128@deconstructible
129class URLValidator(RegexValidator):
130 # IP patterns
131 ipv4_re = (
132 r"(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)"
133 r"(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}"
134 )
135 ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later)
136
137 hostname_re = DomainNameValidator.hostname_re
138 domain_re = DomainNameValidator.domain_re
139 tld_re = DomainNameValidator.tld_re
140
141 host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
142
143 regex = _lazy_re_compile(
144 r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
145 r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
146 r"(?:" + ipv4_re + "|" + ipv6_re + "|" + host_re + ")"
147 r"(?::[0-9]{1,5})?" # port
148 r"(?:[/?#][^\s]*)?" # resource path
149 r"\Z",
150 re.IGNORECASE,
151 )
152 message = _("Enter a valid URL.")
153 schemes = ["http", "https", "ftp", "ftps"]
154 unsafe_chars = frozenset("\t\r\n")
155 max_length = 2048
156
157 def __init__(self, schemes=None, **kwargs):
158 super().__init__(**kwargs)
159 if schemes is not None:
160 self.schemes = schemes
161
162 def __call__(self, value):
163 if not isinstance(value, str) or len(value) > self.max_length:
164 raise ValidationError(self.message, code=self.code, params={"value": value})
165 if self.unsafe_chars.intersection(value):
166 raise ValidationError(self.message, code=self.code, params={"value": value})
167 # Check if the scheme is valid.
168 scheme = value.split("://")[0].lower()
169 if scheme not in self.schemes:
170 raise ValidationError(self.message, code=self.code, params={"value": value})
171
172 # Then check full URL
173 try:
174 splitted_url = urlsplit(value)
175 except ValueError:
176 raise ValidationError(self.message, code=self.code, params={"value": value})
177 super().__call__(value)
178 # Now verify IPv6 in the netloc part
179 host_match = re.search(r"^\[(.+)\](?::[0-9]{1,5})?$", splitted_url.netloc)
180 if host_match:
181 potential_ip = host_match[1]
182 try:
183 validate_ipv6_address(potential_ip)
184 except ValidationError:
185 raise ValidationError(
186 self.message, code=self.code, params={"value": value}
187 )
188
189 # The maximum length of a full host name is 253 characters per RFC 1034
190 # section 3.1. It's defined to be 255 bytes or less, but this includes
191 # one byte for the length of the name and one byte for the trailing dot
192 # that's used to indicate absolute names in DNS.
193 if splitted_url.hostname is None or len(splitted_url.hostname) > 253:
194 raise ValidationError(self.message, code=self.code, params={"value": value})
195
196
197integer_validator = RegexValidator(
198 _lazy_re_compile(r"^-?\d+\Z"),
199 message=_("Enter a valid integer."),
200 code="invalid",
201)
202
203
204def validate_integer(value):
205 return integer_validator(value)
206
207
208@deconstructible
209class EmailValidator:
210 message = _("Enter a valid email address.")
211 code = "invalid"
212 hostname_re = DomainNameValidator.hostname_re
213 domain_re = DomainNameValidator.domain_re
214 tld_no_fqdn_re = DomainNameValidator.tld_no_fqdn_re
215
216 user_regex = _lazy_re_compile(
217 # dot-atom
218 r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"
219 # quoted-string
220 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])'
221 r'*"\Z)',
222 re.IGNORECASE,
223 )
224 domain_regex = _lazy_re_compile(
225 r"^" + hostname_re + domain_re + tld_no_fqdn_re + r"\Z",
226 re.IGNORECASE,
227 )
228 literal_regex = _lazy_re_compile(
229 # literal form, ipv4 or ipv6 address (SMTP 4.1.3)
230 r"\[([A-F0-9:.]+)\]\Z",
231 re.IGNORECASE,
232 )
233 domain_allowlist = ["localhost"]
234
235 def __init__(self, message=None, code=None, allowlist=None):
236 if message is not None:
237 self.message = message
238 if code is not None:
239 self.code = code
240 if allowlist is not None:
241 self.domain_allowlist = allowlist
242
243 def __call__(self, value):
244 # The maximum length of an email is 320 characters per RFC 3696
245 # section 3.
246 if not value or "@" not in value or len(value) > 320:
247 raise ValidationError(self.message, code=self.code, params={"value": value})
248
249 user_part, domain_part = value.rsplit("@", 1)
250
251 if not self.user_regex.match(user_part):
252 raise ValidationError(self.message, code=self.code, params={"value": value})
253
254 if domain_part not in self.domain_allowlist and not self.validate_domain_part(
255 domain_part
256 ):
257 raise ValidationError(self.message, code=self.code, params={"value": value})
258
259 def validate_domain_part(self, domain_part):
260 if self.domain_regex.match(domain_part):
261 return True
262
263 literal_match = self.literal_regex.match(domain_part)
264 if literal_match:
265 ip_address = literal_match[1]
266 try:
267 validate_ipv46_address(ip_address)
268 return True
269 except ValidationError:
270 pass
271 return False
272
273 def __eq__(self, other):
274 return (
275 isinstance(other, EmailValidator)
276 and (set(self.domain_allowlist) == set(other.domain_allowlist))
277 and (self.message == other.message)
278 and (self.code == other.code)
279 )
280
281
282validate_email = EmailValidator()
283
284slug_re = _lazy_re_compile(r"^[-a-zA-Z0-9_]+\Z")
285validate_slug = RegexValidator(
286 slug_re,
287 # Translators: "letters" means latin letters: a-z and A-Z.
288 _("Enter a valid “slug” consisting of letters, numbers, underscores or hyphens."),
289 "invalid",
290)
291
292slug_unicode_re = _lazy_re_compile(r"^[-\w]+\Z")
293validate_unicode_slug = RegexValidator(
294 slug_unicode_re,
295 _(
296 "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or "
297 "hyphens."
298 ),
299 "invalid",
300)
301
302
303def validate_ipv4_address(value):
304 try:
305 ipaddress.IPv4Address(value)
306 except ValueError:
307 raise ValidationError(
308 _("Enter a valid %(protocol)s address."),
309 code="invalid",
310 params={"protocol": _("IPv4"), "value": value},
311 )
312
313
314def validate_ipv6_address(value):
315 if not is_valid_ipv6_address(value):
316 raise ValidationError(
317 _("Enter a valid %(protocol)s address."),
318 code="invalid",
319 params={"protocol": _("IPv6"), "value": value},
320 )
321
322
323def validate_ipv46_address(value):
324 try:
325 validate_ipv4_address(value)
326 except ValidationError:
327 try:
328 validate_ipv6_address(value)
329 except ValidationError:
330 raise ValidationError(
331 _("Enter a valid %(protocol)s address."),
332 code="invalid",
333 params={"protocol": _("IPv4 or IPv6"), "value": value},
334 )
335
336
337ip_address_validator_map = {
338 "both": [validate_ipv46_address],
339 "ipv4": [validate_ipv4_address],
340 "ipv6": [validate_ipv6_address],
341}
342
343
344def ip_address_validators(protocol, unpack_ipv4):
345 """
346 Depending on the given parameters, return the appropriate validators for
347 the GenericIPAddressField.
348 """
349 if protocol != "both" and unpack_ipv4:
350 raise ValueError(
351 "You can only use `unpack_ipv4` if `protocol` is set to 'both'"
352 )
353 try:
354 return ip_address_validator_map[protocol.lower()]
355 except KeyError:
356 raise ValueError(
357 "The protocol '%s' is unknown. Supported: %s"
358 % (protocol, list(ip_address_validator_map))
359 )
360
361
362def int_list_validator(sep=",", message=None, code="invalid", allow_negative=False):
363 regexp = _lazy_re_compile(
364 r"^%(neg)s\d+(?:%(sep)s%(neg)s\d+)*\Z"
365 % {
366 "neg": "(-)?" if allow_negative else "",
367 "sep": re.escape(sep),
368 }
369 )
370 return RegexValidator(regexp, message=message, code=code)
371
372
373validate_comma_separated_integer_list = int_list_validator(
374 message=_("Enter only digits separated by commas."),
375)
376
377
378@deconstructible
379class BaseValidator:
380 message = _("Ensure this value is %(limit_value)s (it is %(show_value)s).")
381 code = "limit_value"
382
383 def __init__(self, limit_value, message=None):
384 self.limit_value = limit_value
385 if message:
386 self.message = message
387
388 def __call__(self, value):
389 cleaned = self.clean(value)
390 limit_value = (
391 self.limit_value() if callable(self.limit_value) else self.limit_value
392 )
393 params = {"limit_value": limit_value, "show_value": cleaned, "value": value}
394 if self.compare(cleaned, limit_value):
395 raise ValidationError(self.message, code=self.code, params=params)
396
397 def __eq__(self, other):
398 if not isinstance(other, self.__class__):
399 return NotImplemented
400 return (
401 self.limit_value == other.limit_value
402 and self.message == other.message
403 and self.code == other.code
404 )
405
406 def compare(self, a, b):
407 return a is not b
408
409 def clean(self, x):
410 return x
411
412
413@deconstructible
414class MaxValueValidator(BaseValidator):
415 message = _("Ensure this value is less than or equal to %(limit_value)s.")
416 code = "max_value"
417
418 def compare(self, a, b):
419 return a > b
420
421
422@deconstructible
423class MinValueValidator(BaseValidator):
424 message = _("Ensure this value is greater than or equal to %(limit_value)s.")
425 code = "min_value"
426
427 def compare(self, a, b):
428 return a < b
429
430
431@deconstructible
432class StepValueValidator(BaseValidator):
433 message = _("Ensure this value is a multiple of step size %(limit_value)s.")
434 code = "step_size"
435
436 def __init__(self, limit_value, message=None, offset=None):
437 super().__init__(limit_value, message)
438 if offset is not None:
439 self.message = _(
440 "Ensure this value is a multiple of step size %(limit_value)s, "
441 "starting from %(offset)s, e.g. %(offset)s, %(valid_value1)s, "
442 "%(valid_value2)s, and so on."
443 )
444 self.offset = offset
445
446 def __call__(self, value):
447 if self.offset is None:
448 super().__call__(value)
449 else:
450 cleaned = self.clean(value)
451 limit_value = (
452 self.limit_value() if callable(self.limit_value) else self.limit_value
453 )
454 if self.compare(cleaned, limit_value):
455 offset = cleaned.__class__(self.offset)
456 params = {
457 "limit_value": limit_value,
458 "offset": offset,
459 "valid_value1": offset + limit_value,
460 "valid_value2": offset + 2 * limit_value,
461 }
462 raise ValidationError(self.message, code=self.code, params=params)
463
464 def compare(self, a, b):
465 offset = 0 if self.offset is None else self.offset
466 return not math.isclose(math.remainder(a - offset, b), 0, abs_tol=1e-9)
467
468
469@deconstructible
470class MinLengthValidator(BaseValidator):
471 message = ngettext_lazy(
472 "Ensure this value has at least %(limit_value)d character (it has "
473 "%(show_value)d).",
474 "Ensure this value has at least %(limit_value)d characters (it has "
475 "%(show_value)d).",
476 "limit_value",
477 )
478 code = "min_length"
479
480 def compare(self, a, b):
481 return a < b
482
483 def clean(self, x):
484 return len(x)
485
486
487@deconstructible
488class MaxLengthValidator(BaseValidator):
489 message = ngettext_lazy(
490 "Ensure this value has at most %(limit_value)d character (it has "
491 "%(show_value)d).",
492 "Ensure this value has at most %(limit_value)d characters (it has "
493 "%(show_value)d).",
494 "limit_value",
495 )
496 code = "max_length"
497
498 def compare(self, a, b):
499 return a > b
500
501 def clean(self, x):
502 return len(x)
503
504
505@deconstructible
506class DecimalValidator:
507 """
508 Validate that the input does not exceed the maximum number of digits
509 expected, otherwise raise ValidationError.
510 """
511
512 messages = {
513 "invalid": _("Enter a number."),
514 "max_digits": ngettext_lazy(
515 "Ensure that there are no more than %(max)s digit in total.",
516 "Ensure that there are no more than %(max)s digits in total.",
517 "max",
518 ),
519 "max_decimal_places": ngettext_lazy(
520 "Ensure that there are no more than %(max)s decimal place.",
521 "Ensure that there are no more than %(max)s decimal places.",
522 "max",
523 ),
524 "max_whole_digits": ngettext_lazy(
525 "Ensure that there are no more than %(max)s digit before the decimal "
526 "point.",
527 "Ensure that there are no more than %(max)s digits before the decimal "
528 "point.",
529 "max",
530 ),
531 }
532
533 def __init__(self, max_digits, decimal_places):
534 self.max_digits = max_digits
535 self.decimal_places = decimal_places
536
537 def __call__(self, value):
538 digit_tuple, exponent = value.as_tuple()[1:]
539 if exponent in {"F", "n", "N"}:
540 raise ValidationError(
541 self.messages["invalid"], code="invalid", params={"value": value}
542 )
543 if exponent >= 0:
544 digits = len(digit_tuple)
545 if digit_tuple != (0,):
546 # A positive exponent adds that many trailing zeros.
547 digits += exponent
548 decimals = 0
549 else:
550 # If the absolute value of the negative exponent is larger than the
551 # number of digits, then it's the same as the number of digits,
552 # because it'll consume all of the digits in digit_tuple and then
553 # add abs(exponent) - len(digit_tuple) leading zeros after the
554 # decimal point.
555 if abs(exponent) > len(digit_tuple):
556 digits = decimals = abs(exponent)
557 else:
558 digits = len(digit_tuple)
559 decimals = abs(exponent)
560 whole_digits = digits - decimals
561
562 if self.max_digits is not None and digits > self.max_digits:
563 raise ValidationError(
564 self.messages["max_digits"],
565 code="max_digits",
566 params={"max": self.max_digits, "value": value},
567 )
568 if self.decimal_places is not None and decimals > self.decimal_places:
569 raise ValidationError(
570 self.messages["max_decimal_places"],
571 code="max_decimal_places",
572 params={"max": self.decimal_places, "value": value},
573 )
574 if (
575 self.max_digits is not None
576 and self.decimal_places is not None
577 and whole_digits > (self.max_digits - self.decimal_places)
578 ):
579 raise ValidationError(
580 self.messages["max_whole_digits"],
581 code="max_whole_digits",
582 params={"max": (self.max_digits - self.decimal_places), "value": value},
583 )
584
585 def __eq__(self, other):
586 return (
587 isinstance(other, self.__class__)
588 and self.max_digits == other.max_digits
589 and self.decimal_places == other.decimal_places
590 )
591
592
593@deconstructible
594class FileExtensionValidator:
595 message = _(
596 "File extension “%(extension)s” is not allowed. "
597 "Allowed extensions are: %(allowed_extensions)s."
598 )
599 code = "invalid_extension"
600
601 def __init__(self, allowed_extensions=None, message=None, code=None):
602 if allowed_extensions is not None:
603 allowed_extensions = [
604 allowed_extension.lower() for allowed_extension in allowed_extensions
605 ]
606 self.allowed_extensions = allowed_extensions
607 if message is not None:
608 self.message = message
609 if code is not None:
610 self.code = code
611
612 def __call__(self, value):
613 extension = Path(value.name).suffix[1:].lower()
614 if (
615 self.allowed_extensions is not None
616 and extension not in self.allowed_extensions
617 ):
618 raise ValidationError(
619 self.message,
620 code=self.code,
621 params={
622 "extension": extension,
623 "allowed_extensions": ", ".join(self.allowed_extensions),
624 "value": value,
625 },
626 )
627
628 def __eq__(self, other):
629 return (
630 isinstance(other, self.__class__)
631 and set(self.allowed_extensions or [])
632 == set(other.allowed_extensions or [])
633 and self.message == other.message
634 and self.code == other.code
635 )
636
637
638def get_available_image_extensions():
639 try:
640 from PIL import Image
641 except ImportError:
642 return []
643 else:
644 Image.init()
645 return [ext.lower()[1:] for ext in Image.EXTENSION]
646
647
648def validate_image_file_extension(value):
649 return FileExtensionValidator(allowed_extensions=get_available_image_extensions())(
650 value
651 )
652
653
654@deconstructible
655class ProhibitNullCharactersValidator:
656 """Validate that the string doesn't contain the null character."""
657
658 message = _("Null characters are not allowed.")
659 code = "null_characters_not_allowed"
660
661 def __init__(self, message=None, code=None):
662 if message is not None:
663 self.message = message
664 if code is not None:
665 self.code = code
666
667 def __call__(self, value):
668 if "\x00" in str(value):
669 raise ValidationError(self.message, code=self.code, params={"value": value})
670
671 def __eq__(self, other):
672 return (
673 isinstance(other, self.__class__)
674 and self.message == other.message
675 and self.code == other.code
676 )