validators.py 20 KB

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