Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #18119 -- Added a DomainNameValidator validator. #18037

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schemes is not an accepted parameter. regex shouldn't accepted either, as it is rewritten in all cases in __init__.
BTW, you still have an extra new line just below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - have now updated.


.. 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 @@ -361,7 +361,10 @@ Utilities
Validators
~~~~~~~~~~

* ...
* The new :class:`~django.core.validators.DomainNameValidator` validates domain
nmenezes0 marked this conversation as resolved.
Show resolved Hide resolved
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),
claudep marked this conversation as resolved.
Show resolved Hide resolved
(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"),
)