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