Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/validators.py: 57%
300 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import ipaddress
2import re
3import warnings
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.deprecation import RemovedInDjango41Warning
10from django.utils.encoding import punycode
11from django.utils.ipv6 import is_valid_ipv6_address
12from django.utils.regex_helper import _lazy_re_compile
13from django.utils.translation import gettext_lazy as _
14from django.utils.translation import ngettext_lazy
16# These values, if given to validate(), will trigger the self.required check.
17EMPTY_VALUES = (None, "", [], (), {})
20@deconstructible
21class RegexValidator:
22 regex = ""
23 message = _("Enter a valid value.")
24 code = "invalid"
25 inverse_match = False
26 flags = 0
28 def __init__(
29 self, regex=None, message=None, code=None, inverse_match=None, flags=None
30 ):
31 if regex is not None:
32 self.regex = regex
33 if message is not None:
34 self.message = message
35 if code is not None:
36 self.code = code
37 if inverse_match is not None: 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true
38 self.inverse_match = inverse_match
39 if flags is not None: 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true
40 self.flags = flags
41 if self.flags and not isinstance(self.regex, str): 41 ↛ 42line 41 didn't jump to line 42, because the condition on line 41 was never true
42 raise TypeError(
43 "If the flags are set, regex must be a regular expression string."
44 )
46 self.regex = _lazy_re_compile(self.regex, self.flags)
48 def __call__(self, value):
49 """
50 Validate that the input contains (or does *not* contain, if
51 inverse_match is True) a match for the regular expression.
52 """
53 regex_matches = self.regex.search(str(value))
54 invalid_input = regex_matches if self.inverse_match else not regex_matches
55 if invalid_input: 55 ↛ 56line 55 didn't jump to line 56, because the condition on line 55 was never true
56 raise ValidationError(self.message, code=self.code, params={"value": value})
58 def __eq__(self, other):
59 return (
60 isinstance(other, RegexValidator)
61 and self.regex.pattern == other.regex.pattern
62 and self.regex.flags == other.regex.flags
63 and (self.message == other.message)
64 and (self.code == other.code)
65 and (self.inverse_match == other.inverse_match)
66 )
69@deconstructible
70class URLValidator(RegexValidator):
71 ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string).
73 # IP patterns
74 ipv4_re = (
75 r"(?:0|25[0-5]|2[0-4]\d|1\d?\d?|[1-9]\d?)"
76 r"(?:\.(?:0|25[0-5]|2[0-4]\d|1\d?\d?|[1-9]\d?)){3}"
77 )
78 ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later)
80 # Host patterns
81 hostname_re = (
82 r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
83 )
84 # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
85 domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*"
86 tld_re = (
87 r"\." # dot
88 r"(?!-)" # can't start with a dash
89 r"(?:[a-z" + ul + "-]{2,63}" # domain label
90 r"|xn--[a-z0-9]{1,59})" # or punycode label
91 r"(?<!-)" # can't end with a dash
92 r"\.?" # may have a trailing dot
93 )
94 host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
96 regex = _lazy_re_compile(
97 r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
98 r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
99 r"(?:" + ipv4_re + "|" + ipv6_re + "|" + host_re + ")"
100 r"(?::\d{1,5})?" # port
101 r"(?:[/?#][^\s]*)?" # resource path
102 r"\Z",
103 re.IGNORECASE,
104 )
105 message = _("Enter a valid URL.")
106 schemes = ["http", "https", "ftp", "ftps"]
107 unsafe_chars = frozenset("\t\r\n")
109 def __init__(self, schemes=None, **kwargs):
110 super().__init__(**kwargs)
111 if schemes is not None: 111 ↛ 112line 111 didn't jump to line 112, because the condition on line 111 was never true
112 self.schemes = schemes
114 def __call__(self, value):
115 if not isinstance(value, str):
116 raise ValidationError(self.message, code=self.code, params={"value": value})
117 if self.unsafe_chars.intersection(value):
118 raise ValidationError(self.message, code=self.code, params={"value": value})
119 # Check if the scheme is valid.
120 scheme = value.split("://")[0].lower()
121 if scheme not in self.schemes:
122 raise ValidationError(self.message, code=self.code, params={"value": value})
124 # Then check full URL
125 try:
126 super().__call__(value)
127 except ValidationError as e:
128 # Trivial case failed. Try for possible IDN domain
129 if value:
130 try:
131 scheme, netloc, path, query, fragment = urlsplit(value)
132 except ValueError: # for example, "Invalid IPv6 URL"
133 raise ValidationError(
134 self.message, code=self.code, params={"value": value}
135 )
136 try:
137 netloc = punycode(netloc) # IDN -> ACE
138 except UnicodeError: # invalid domain part
139 raise e
140 url = urlunsplit((scheme, netloc, path, query, fragment))
141 super().__call__(url)
142 else:
143 raise
144 else:
145 # Now verify IPv6 in the netloc part
146 host_match = re.search(r"^\[(.+)\](?::\d{1,5})?$", urlsplit(value).netloc)
147 if host_match:
148 potential_ip = host_match[1]
149 try:
150 validate_ipv6_address(potential_ip)
151 except ValidationError:
152 raise ValidationError(
153 self.message, code=self.code, params={"value": value}
154 )
156 # The maximum length of a full host name is 253 characters per RFC 1034
157 # section 3.1. It's defined to be 255 bytes or less, but this includes
158 # one byte for the length of the name and one byte for the trailing dot
159 # that's used to indicate absolute names in DNS.
160 if len(urlsplit(value).hostname) > 253:
161 raise ValidationError(self.message, code=self.code, params={"value": value})
164integer_validator = RegexValidator(
165 _lazy_re_compile(r"^-?\d+\Z"),
166 message=_("Enter a valid integer."),
167 code="invalid",
168)
171def validate_integer(value):
172 return integer_validator(value)
175@deconstructible
176class EmailValidator:
177 message = _("Enter a valid email address.")
178 code = "invalid"
179 user_regex = _lazy_re_compile(
180 # dot-atom
181 r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"
182 # quoted-string
183 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])'
184 r'*"\Z)',
185 re.IGNORECASE,
186 )
187 domain_regex = _lazy_re_compile(
188 # max length for domain name labels is 63 characters per RFC 1034
189 r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z",
190 re.IGNORECASE,
191 )
192 literal_regex = _lazy_re_compile(
193 # literal form, ipv4 or ipv6 address (SMTP 4.1.3)
194 r"\[([A-F0-9:.]+)\]\Z",
195 re.IGNORECASE,
196 )
197 domain_allowlist = ["localhost"]
199 @property
200 def domain_whitelist(self):
201 warnings.warn(
202 "The domain_whitelist attribute is deprecated in favor of "
203 "domain_allowlist.",
204 RemovedInDjango41Warning,
205 stacklevel=2,
206 )
207 return self.domain_allowlist
209 @domain_whitelist.setter
210 def domain_whitelist(self, allowlist):
211 warnings.warn(
212 "The domain_whitelist attribute is deprecated in favor of "
213 "domain_allowlist.",
214 RemovedInDjango41Warning,
215 stacklevel=2,
216 )
217 self.domain_allowlist = allowlist
219 def __init__(self, message=None, code=None, allowlist=None, *, whitelist=None):
220 if whitelist is not None: 220 ↛ 221line 220 didn't jump to line 221, because the condition on line 220 was never true
221 allowlist = whitelist
222 warnings.warn(
223 "The whitelist argument is deprecated in favor of allowlist.",
224 RemovedInDjango41Warning,
225 stacklevel=2,
226 )
227 if message is not None:
228 self.message = message
229 if code is not None: 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true
230 self.code = code
231 if allowlist is not None: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true
232 self.domain_allowlist = allowlist
234 def __call__(self, value):
235 if not value or "@" not in value: 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true
236 raise ValidationError(self.message, code=self.code, params={"value": value})
238 user_part, domain_part = value.rsplit("@", 1)
240 if not self.user_regex.match(user_part): 240 ↛ 241line 240 didn't jump to line 241, because the condition on line 240 was never true
241 raise ValidationError(self.message, code=self.code, params={"value": value})
243 if domain_part not in self.domain_allowlist and not self.validate_domain_part( 243 ↛ 247line 243 didn't jump to line 247, because the condition on line 243 was never true
244 domain_part
245 ):
246 # Try for possible IDN domain-part
247 try:
248 domain_part = punycode(domain_part)
249 except UnicodeError:
250 pass
251 else:
252 if self.validate_domain_part(domain_part):
253 return
254 raise ValidationError(self.message, code=self.code, params={"value": value})
256 def validate_domain_part(self, domain_part):
257 if self.domain_regex.match(domain_part): 257 ↛ 260line 257 didn't jump to line 260, because the condition on line 257 was never false
258 return True
260 literal_match = self.literal_regex.match(domain_part)
261 if literal_match:
262 ip_address = literal_match[1]
263 try:
264 validate_ipv46_address(ip_address)
265 return True
266 except ValidationError:
267 pass
268 return False
270 def __eq__(self, other):
271 return (
272 isinstance(other, EmailValidator)
273 and (self.domain_allowlist == other.domain_allowlist)
274 and (self.message == other.message)
275 and (self.code == other.code)
276 )
279validate_email = EmailValidator()
281slug_re = _lazy_re_compile(r"^[-a-zA-Z0-9_]+\Z")
282validate_slug = RegexValidator(
283 slug_re,
284 # Translators: "letters" means latin letters: a-z and A-Z.
285 _("Enter a valid “slug” consisting of letters, numbers, underscores or hyphens."),
286 "invalid",
287)
289slug_unicode_re = _lazy_re_compile(r"^[-\w]+\Z")
290validate_unicode_slug = RegexValidator(
291 slug_unicode_re,
292 _(
293 "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or "
294 "hyphens."
295 ),
296 "invalid",
297)
300def validate_ipv4_address(value):
301 try:
302 ipaddress.IPv4Address(value)
303 except ValueError:
304 raise ValidationError(
305 _("Enter a valid IPv4 address."), code="invalid", params={"value": value}
306 )
307 else:
308 # Leading zeros are forbidden to avoid ambiguity with the octal
309 # notation. This restriction is included in Python 3.9.5+.
310 # TODO: Remove when dropping support for PY39.
311 if any(octet != "0" and octet[0] == "0" for octet in value.split(".")):
312 raise ValidationError(
313 _("Enter a valid IPv4 address."),
314 code="invalid",
315 params={"value": value},
316 )
319def validate_ipv6_address(value):
320 if not is_valid_ipv6_address(value):
321 raise ValidationError(
322 _("Enter a valid IPv6 address."), code="invalid", params={"value": value}
323 )
326def validate_ipv46_address(value):
327 try:
328 validate_ipv4_address(value)
329 except ValidationError:
330 try:
331 validate_ipv6_address(value)
332 except ValidationError:
333 raise ValidationError(
334 _("Enter a valid IPv4 or IPv6 address."),
335 code="invalid",
336 params={"value": value},
337 )
340ip_address_validator_map = {
341 "both": ([validate_ipv46_address], _("Enter a valid IPv4 or IPv6 address.")),
342 "ipv4": ([validate_ipv4_address], _("Enter a valid IPv4 address.")),
343 "ipv6": ([validate_ipv6_address], _("Enter a valid IPv6 address.")),
344}
347def ip_address_validators(protocol, unpack_ipv4):
348 """
349 Depending on the given parameters, return the appropriate validators for
350 the GenericIPAddressField.
351 """
352 if protocol != "both" and unpack_ipv4:
353 raise ValueError(
354 "You can only use `unpack_ipv4` if `protocol` is set to 'both'"
355 )
356 try:
357 return ip_address_validator_map[protocol.lower()]
358 except KeyError:
359 raise ValueError(
360 "The protocol '%s' is unknown. Supported: %s"
361 % (protocol, list(ip_address_validator_map))
362 )
365def int_list_validator(sep=",", message=None, code="invalid", allow_negative=False):
366 regexp = _lazy_re_compile(
367 r"^%(neg)s\d+(?:%(sep)s%(neg)s\d+)*\Z"
368 % {
369 "neg": "(-)?" if allow_negative else "",
370 "sep": re.escape(sep),
371 }
372 )
373 return RegexValidator(regexp, message=message, code=code)
376validate_comma_separated_integer_list = int_list_validator(
377 message=_("Enter only digits separated by commas."),
378)
381@deconstructible
382class BaseValidator:
383 message = _("Ensure this value is %(limit_value)s (it is %(show_value)s).")
384 code = "limit_value"
386 def __init__(self, limit_value, message=None):
387 self.limit_value = limit_value
388 if message:
389 self.message = message
391 def __call__(self, value):
392 cleaned = self.clean(value)
393 limit_value = (
394 self.limit_value() if callable(self.limit_value) else self.limit_value
395 )
396 params = {"limit_value": limit_value, "show_value": cleaned, "value": value}
397 if self.compare(cleaned, limit_value): 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true
398 raise ValidationError(self.message, code=self.code, params=params)
400 def __eq__(self, other):
401 if not isinstance(other, self.__class__):
402 return NotImplemented
403 return (
404 self.limit_value == other.limit_value
405 and self.message == other.message
406 and self.code == other.code
407 )
409 def compare(self, a, b):
410 return a is not b
412 def clean(self, x):
413 return x
416@deconstructible
417class MaxValueValidator(BaseValidator):
418 message = _("Ensure this value is less than or equal to %(limit_value)s.")
419 code = "max_value"
421 def compare(self, a, b):
422 return a > b
425@deconstructible
426class MinValueValidator(BaseValidator):
427 message = _("Ensure this value is greater than or equal to %(limit_value)s.")
428 code = "min_value"
430 def compare(self, a, b):
431 return a < b
434@deconstructible
435class MinLengthValidator(BaseValidator):
436 message = ngettext_lazy(
437 "Ensure this value has at least %(limit_value)d character (it has "
438 "%(show_value)d).",
439 "Ensure this value has at least %(limit_value)d characters (it has "
440 "%(show_value)d).",
441 "limit_value",
442 )
443 code = "min_length"
445 def compare(self, a, b):
446 return a < b
448 def clean(self, x):
449 return len(x)
452@deconstructible
453class MaxLengthValidator(BaseValidator):
454 message = ngettext_lazy(
455 "Ensure this value has at most %(limit_value)d character (it has "
456 "%(show_value)d).",
457 "Ensure this value has at most %(limit_value)d characters (it has "
458 "%(show_value)d).",
459 "limit_value",
460 )
461 code = "max_length"
463 def compare(self, a, b):
464 return a > b
466 def clean(self, x):
467 return len(x)
470@deconstructible
471class DecimalValidator:
472 """
473 Validate that the input does not exceed the maximum number of digits
474 expected, otherwise raise ValidationError.
475 """
477 messages = {
478 "invalid": _("Enter a number."),
479 "max_digits": ngettext_lazy(
480 "Ensure that there are no more than %(max)s digit in total.",
481 "Ensure that there are no more than %(max)s digits in total.",
482 "max",
483 ),
484 "max_decimal_places": ngettext_lazy(
485 "Ensure that there are no more than %(max)s decimal place.",
486 "Ensure that there are no more than %(max)s decimal places.",
487 "max",
488 ),
489 "max_whole_digits": ngettext_lazy(
490 "Ensure that there are no more than %(max)s digit before the decimal "
491 "point.",
492 "Ensure that there are no more than %(max)s digits before the decimal "
493 "point.",
494 "max",
495 ),
496 }
498 def __init__(self, max_digits, decimal_places):
499 self.max_digits = max_digits
500 self.decimal_places = decimal_places
502 def __call__(self, value):
503 digit_tuple, exponent = value.as_tuple()[1:]
504 if exponent in {"F", "n", "N"}:
505 raise ValidationError(
506 self.messages["invalid"], code="invalid", params={"value": value}
507 )
508 if exponent >= 0:
509 # A positive exponent adds that many trailing zeros.
510 digits = len(digit_tuple) + exponent
511 decimals = 0
512 else:
513 # If the absolute value of the negative exponent is larger than the
514 # number of digits, then it's the same as the number of digits,
515 # because it'll consume all of the digits in digit_tuple and then
516 # add abs(exponent) - len(digit_tuple) leading zeros after the
517 # decimal point.
518 if abs(exponent) > len(digit_tuple):
519 digits = decimals = abs(exponent)
520 else:
521 digits = len(digit_tuple)
522 decimals = abs(exponent)
523 whole_digits = digits - decimals
525 if self.max_digits is not None and digits > self.max_digits:
526 raise ValidationError(
527 self.messages["max_digits"],
528 code="max_digits",
529 params={"max": self.max_digits, "value": value},
530 )
531 if self.decimal_places is not None and decimals > self.decimal_places:
532 raise ValidationError(
533 self.messages["max_decimal_places"],
534 code="max_decimal_places",
535 params={"max": self.decimal_places, "value": value},
536 )
537 if (
538 self.max_digits is not None
539 and self.decimal_places is not None
540 and whole_digits > (self.max_digits - self.decimal_places)
541 ):
542 raise ValidationError(
543 self.messages["max_whole_digits"],
544 code="max_whole_digits",
545 params={"max": (self.max_digits - self.decimal_places), "value": value},
546 )
548 def __eq__(self, other):
549 return (
550 isinstance(other, self.__class__)
551 and self.max_digits == other.max_digits
552 and self.decimal_places == other.decimal_places
553 )
556@deconstructible
557class FileExtensionValidator:
558 message = _(
559 "File extension “%(extension)s” is not allowed. "
560 "Allowed extensions are: %(allowed_extensions)s."
561 )
562 code = "invalid_extension"
564 def __init__(self, allowed_extensions=None, message=None, code=None):
565 if allowed_extensions is not None: 565 ↛ 569line 565 didn't jump to line 569, because the condition on line 565 was never false
566 allowed_extensions = [
567 allowed_extension.lower() for allowed_extension in allowed_extensions
568 ]
569 self.allowed_extensions = allowed_extensions
570 if message is not None: 570 ↛ 571line 570 didn't jump to line 571, because the condition on line 570 was never true
571 self.message = message
572 if code is not None: 572 ↛ 573line 572 didn't jump to line 573, because the condition on line 572 was never true
573 self.code = code
575 def __call__(self, value):
576 extension = Path(value.name).suffix[1:].lower()
577 if ( 577 ↛ 581line 577 didn't jump to line 581
578 self.allowed_extensions is not None
579 and extension not in self.allowed_extensions
580 ):
581 raise ValidationError(
582 self.message,
583 code=self.code,
584 params={
585 "extension": extension,
586 "allowed_extensions": ", ".join(self.allowed_extensions),
587 "value": value,
588 },
589 )
591 def __eq__(self, other):
592 return (
593 isinstance(other, self.__class__)
594 and self.allowed_extensions == other.allowed_extensions
595 and self.message == other.message
596 and self.code == other.code
597 )
600def get_available_image_extensions():
601 try:
602 from PIL import Image
603 except ImportError:
604 return []
605 else:
606 Image.init()
607 return [ext.lower()[1:] for ext in Image.EXTENSION]
610def validate_image_file_extension(value):
611 return FileExtensionValidator(allowed_extensions=get_available_image_extensions())(
612 value
613 )
616@deconstructible
617class ProhibitNullCharactersValidator:
618 """Validate that the string doesn't contain the null character."""
620 message = _("Null characters are not allowed.")
621 code = "null_characters_not_allowed"
623 def __init__(self, message=None, code=None):
624 if message is not None: 624 ↛ 625line 624 didn't jump to line 625, because the condition on line 624 was never true
625 self.message = message
626 if code is not None: 626 ↛ 627line 626 didn't jump to line 627, because the condition on line 626 was never true
627 self.code = code
629 def __call__(self, value):
630 if "\x00" in str(value): 630 ↛ 631line 630 didn't jump to line 631, because the condition on line 630 was never true
631 raise ValidationError(self.message, code=self.code, params={"value": value})
633 def __eq__(self, other):
634 return (
635 isinstance(other, self.__class__)
636 and self.message == other.message
637 and self.code == other.code
638 )