Skip to content

Commit

Permalink
Fixed #18119 -- Added a DomainNameValidator validator.
Browse files Browse the repository at this point in the history
Override __init__ function to add accept IDNA.

Updates to DomainNameValidator to add logic around accept_idna flag

Remove outdated details from docs.

Made changes to domain name validation and tests.

Reused regex in URLValidator.

Added more changes to the DomainNameValidator so all  tests pass.

Removed rogue space.

Blacken.

Update documentation for DomainNameValidator.

Remove unused text.

Tidy docs.

Update docs - fix underline.

Update docs/ref/validators.txt

Remove unnecessary blank lines.

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>

Update docs/releases/5.1.txt

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>

Changes to docs for DomainNameValidator.

Change name of the parameter.

Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>

Update docs for new naming.

Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>

Update django/core/validators.py

Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>

Update comments for validators.

Reorder docs to have DomainNameValidator next to URLNameValidator.

blacken
  • Loading branch information
berkerpeksag authored and Nina Menezes committed May 11, 2024
1 parent 34f329e commit dd9816e
Show file tree
Hide file tree
Showing 4 changed files with 139 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
25 changes: 25 additions & 0 deletions docs/ref/validators.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ 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(schemes=None, regex=None, message=None, code=None, accept_idna=True)

A :class:`RegexValidator` subclass that ensures a value looks like a domain name. Values longer than
255 characters are always considered invalid.

In addition to the optional arguments of its parent :class:`RegexValidator`
class, ``DomainNameValidator`` accepts an extra optional attribute:


.. attribute:: accept_idna
This determines whether or not to accept internationalized domain name, that is,
domain names that contain non-ASCII characters. Default value is true.

``validate_domain_name``
------------------------

.. data:: validate_domain_name

An :class:`DomainNameValidator` instance without any customizations.

``URLValidator``
----------------

Expand Down
3 changes: 2 additions & 1 deletion docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,8 @@ Utilities
Validators
~~~~~~~~~~

* ...
* The new :class:`~django.core.validators.DomainNameValidator` validates domain
names, including internationalized domain names.

.. _backwards-incompatible-5.1:

Expand Down
53 changes: 53 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,35 @@
(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),
(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 +878,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 dd9816e

Please sign in to comment.