summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerker Peksag <berker.peksag@gmail.com>2016-09-27 04:59:48 +0300
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-05-21 23:11:12 +0200
commit4971a9afe5642569f3dcfcd3972ebb39e88dd457 (patch)
tree56248ceac38e90d6523045eef5c47182dd2f3fa1
parentb9838c65ec2fee36c1fd0d46494ba44da27a9b34 (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.py70
-rw-r--r--docs/ref/validators.txt28
-rw-r--r--docs/releases/5.1.txt5
-rw-r--r--tests/validators/tests.py56
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"),
+ )