diff options
| author | Berker Peksag <berker.peksag@gmail.com> | 2016-09-27 04:59:48 +0300 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2024-05-21 23:11:12 +0200 |
| commit | 4971a9afe5642569f3dcfcd3972ebb39e88dd457 (patch) | |
| tree | 56248ceac38e90d6523045eef5c47182dd2f3fa1 | |
| parent | b9838c65ec2fee36c1fd0d46494ba44da27a9b34 (diff) | |
Fixed #18119 -- Added a DomainNameValidator validator.
Thanks Claude Paroz for the review.
Co-authored-by: Nina Menezes <77671865+nmenezes0@users.noreply.github.com>
| -rw-r--r-- | django/core/validators.py | 70 | ||||
| -rw-r--r-- | docs/ref/validators.txt | 28 | ||||
| -rw-r--r-- | docs/releases/5.1.txt | 5 | ||||
| -rw-r--r-- | tests/validators/tests.py | 56 |
4 files changed, 147 insertions, 12 deletions
diff --git a/django/core/validators.py b/django/core/validators.py index 57940a59da..b1c5c053b8 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -66,22 +66,16 @@ class RegexValidator: @deconstructible -class URLValidator(RegexValidator): +class DomainNameValidator(RegexValidator): + message = _("Enter a valid domain name.") ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string). - - # IP patterns - ipv4_re = ( - r"(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)" - r"(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}" - ) - ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later) - - # Host patterns + # Host patterns. hostname_re = ( r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?" ) - # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1 + # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1. domain_re = r"(?:\.(?!-)[a-z" + ul + r"0-9-]{1,63}(?<!-))*" + # Top-level domain. tld_re = ( r"\." # dot r"(?!-)" # can't start with a dash @@ -90,6 +84,60 @@ class URLValidator(RegexValidator): r"(?<!-)" # can't end with a dash r"\.?" # may have a trailing dot ) + ascii_only_hostname_re = r"[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" + ascii_only_domain_re = r"(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*" + ascii_only_tld_re = ( + r"\." # dot + r"(?!-)" # can't start with a dash + r"(?:[a-zA-Z0-9-]{2,63})" # domain label + r"(?<!-)" # can't end with a dash + r"\.?" # may have a trailing dot + ) + + max_length = 255 + + def __init__(self, **kwargs): + self.accept_idna = kwargs.pop("accept_idna", True) + + if self.accept_idna: + self.regex = _lazy_re_compile( + self.hostname_re + self.domain_re + self.tld_re, re.IGNORECASE + ) + else: + self.regex = _lazy_re_compile( + self.ascii_only_hostname_re + + self.ascii_only_domain_re + + self.ascii_only_tld_re, + re.IGNORECASE, + ) + super().__init__(**kwargs) + + def __call__(self, value): + if not isinstance(value, str) or len(value) > self.max_length: + raise ValidationError(self.message, code=self.code, params={"value": value}) + if not self.accept_idna and not value.isascii(): + raise ValidationError(self.message, code=self.code, params={"value": value}) + super().__call__(value) + + +validate_domain_name = DomainNameValidator() + + +@deconstructible +class URLValidator(RegexValidator): + ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string). + + # IP patterns + ipv4_re = ( + r"(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)" + r"(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}" + ) + ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later) + + hostname_re = DomainNameValidator.hostname_re + domain_re = DomainNameValidator.domain_re + tld_re = DomainNameValidator.tld_re + host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)" regex = _lazy_re_compile( diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index fb69ec8d33..3287d0560e 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -159,6 +159,25 @@ to, or in lieu of custom ``field.clean()`` methods. validation, so you'd need to add them to the ``allowlist`` as necessary. +``DomainNameValidator`` +----------------------- + +.. versionadded:: 5.1 + +.. class:: DomainNameValidator(accept_idna=True, message=None, code=None) + + A :class:`RegexValidator` subclass that ensures a value looks like a domain + name. Values longer than 255 characters are always considered invalid. IP + addresses are not accepted as valid domain names. + + In addition to the optional arguments of its parent :class:`RegexValidator` + class, ``DomainNameValidator`` accepts an extra optional attribute: + + .. attribute:: accept_idna + + Determines whether to accept internationalized domain names, that is, + domain names that contain non-ASCII characters. Defaults to ``True``. + ``URLValidator`` ---------------- @@ -201,6 +220,15 @@ to, or in lieu of custom ``field.clean()`` methods. An :class:`EmailValidator` instance without any customizations. +``validate_domain_name`` +------------------------ + +.. versionadded:: 5.1 + +.. data:: validate_domain_name + + A :class:`DomainNameValidator` instance without any customizations. + ``validate_slug`` ----------------- diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index bb5e4f3fe4..d846788815 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -363,7 +363,10 @@ Utilities Validators ~~~~~~~~~~ -* ... +* The new :class:`~django.core.validators.DomainNameValidator` validates domain + names, including internationalized domain names. The new + :func:`~django.core.validators.validate_domain_name` function returns an + instance of :class:`~django.core.validators.DomainNameValidator`. .. _backwards-incompatible-5.1: diff --git a/tests/validators/tests.py b/tests/validators/tests.py index 5376517a4a..ba1db5ea46 100644 --- a/tests/validators/tests.py +++ b/tests/validators/tests.py @@ -9,6 +9,7 @@ from django.core.files.base import ContentFile from django.core.validators import ( BaseValidator, DecimalValidator, + DomainNameValidator, EmailValidator, FileExtensionValidator, MaxLengthValidator, @@ -21,6 +22,7 @@ from django.core.validators import ( URLValidator, int_list_validator, validate_comma_separated_integer_list, + validate_domain_name, validate_email, validate_image_file_extension, validate_integer, @@ -618,6 +620,38 @@ TEST_DATA = [ (ProhibitNullCharactersValidator(), "\x00something", ValidationError), (ProhibitNullCharactersValidator(), "something", None), (ProhibitNullCharactersValidator(), None, None), + (validate_domain_name, "000000.org", None), + (validate_domain_name, "python.org", None), + (validate_domain_name, "python.co.uk", None), + (validate_domain_name, "python.tk", None), + (validate_domain_name, "domain.with.idn.tld.उदाहरण.परीक्ष", None), + (validate_domain_name, "ıçğü.com", None), + (validate_domain_name, "xn--7ca6byfyc.com", None), + (validate_domain_name, "hg.python.org", None), + (validate_domain_name, "python.xyz", None), + (validate_domain_name, "djangoproject.com", None), + (validate_domain_name, "DJANGOPROJECT.COM", None), + (validate_domain_name, "spam.eggs", None), + (validate_domain_name, "python-python.com", None), + (validate_domain_name, "python.name.uk", None), + (validate_domain_name, "python.tips", None), + (validate_domain_name, "http://例子.测试", None), + (validate_domain_name, "http://dashinpunytld.xn---c", None), + (validate_domain_name, "python..org", ValidationError), + (validate_domain_name, "python-.org", ValidationError), + (validate_domain_name, "too-long-name." * 20 + "com", ValidationError), + (validate_domain_name, "stupid-name试", ValidationError), + (validate_domain_name, "255.0.0.0", ValidationError), + (validate_domain_name, "fe80::1", ValidationError), + (validate_domain_name, "1:2:3:4:5:6:7:8", ValidationError), + (DomainNameValidator(accept_idna=False), "non-idna-domain-name-passes.com", None), + ( + DomainNameValidator(accept_idna=False), + "domain.with.idn.tld.उदाहरण.परीक्ष", + ValidationError, + ), + (DomainNameValidator(accept_idna=False), "ıçğü.com", ValidationError), + (DomainNameValidator(accept_idna=False), "not-domain-name", ValidationError), ] # Add valid and invalid URL tests. @@ -847,3 +881,25 @@ class TestValidatorEquality(TestCase): ProhibitNullCharactersValidator(message="message", code="code1"), ProhibitNullCharactersValidator(message="message", code="code2"), ) + + def test_domain_name_equality(self): + self.assertEqual( + DomainNameValidator(), + DomainNameValidator(), + ) + self.assertNotEqual( + DomainNameValidator(), + EmailValidator(), + ) + self.assertNotEqual( + DomainNameValidator(), + DomainNameValidator(code="custom_code"), + ) + self.assertEqual( + DomainNameValidator(message="custom error message"), + DomainNameValidator(message="custom error message"), + ) + self.assertNotEqual( + DomainNameValidator(message="custom error message"), + DomainNameValidator(message="custom error message", code="custom_code"), + ) |
