Skip to content

Commit

Permalink
Fixed django#18119 -- Added a DomainNameValidator validator.
Browse files Browse the repository at this point in the history
Thanks Claude Paroz for the review.

Co-authored-by: Nina Menezes <[email protected]>
  • Loading branch information
2 people authored and sarahboyce committed May 21, 2024
1 parent b9838c6 commit 4971a9a
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 12 deletions.
70 changes: 59 additions & 11 deletions django/core/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,16 @@ def __eq__(self, other):


@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
Expand All @@ -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(
Expand Down
28 changes: 28 additions & 0 deletions docs/ref/validators.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
----------------

Expand Down Expand Up @@ -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``
-----------------

Expand Down
5 changes: 4 additions & 1 deletion docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
56 changes: 56 additions & 0 deletions tests/validators/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.core.validators import (
BaseValidator,
DecimalValidator,
DomainNameValidator,
EmailValidator,
FileExtensionValidator,
MaxLengthValidator,
Expand All @@ -21,6 +22,7 @@
URLValidator,
int_list_validator,
validate_comma_separated_integer_list,
validate_domain_name,
validate_email,
validate_image_file_extension,
validate_integer,
Expand Down Expand Up @@ -618,6 +620,38 @@
(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.
Expand Down Expand Up @@ -847,3 +881,25 @@ def test_prohibit_null_characters_validator_equality(self):
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"),
)

0 comments on commit 4971a9a

Please sign in to comment.