mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #18119 -- Added a DomainNameValidator validator.
Thanks Claude Paroz for the review. Co-authored-by: Nina Menezes <77671865+nmenezes0@users.noreply.github.com>
This commit is contained in:
parent
b9838c65ec
commit
4971a9afe5
@ -65,6 +65,64 @@ class RegexValidator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class DomainNameValidator(RegexValidator):
|
||||||
|
message = _("Enter a valid domain name.")
|
||||||
|
ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string).
|
||||||
|
# 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.
|
||||||
|
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
|
||||||
|
r"(?:[a-z" + ul + "-]{2,63}" # domain label
|
||||||
|
r"|xn--[a-z0-9]{1,59})" # or punycode label
|
||||||
|
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
|
@deconstructible
|
||||||
class URLValidator(RegexValidator):
|
class URLValidator(RegexValidator):
|
||||||
ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string).
|
ul = "\u00a1-\uffff" # Unicode letters range (must not be a raw string).
|
||||||
@ -76,20 +134,10 @@ class URLValidator(RegexValidator):
|
|||||||
)
|
)
|
||||||
ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later)
|
ipv6_re = r"\[[0-9a-f:.]+\]" # (simple regex, validated later)
|
||||||
|
|
||||||
# Host patterns
|
hostname_re = DomainNameValidator.hostname_re
|
||||||
hostname_re = (
|
domain_re = DomainNameValidator.domain_re
|
||||||
r"[a-z" + ul + r"0-9](?:[a-z" + ul + r"0-9-]{0,61}[a-z" + ul + r"0-9])?"
|
tld_re = DomainNameValidator.tld_re
|
||||||
)
|
|
||||||
# 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}(?<!-))*"
|
|
||||||
tld_re = (
|
|
||||||
r"\." # dot
|
|
||||||
r"(?!-)" # can't start with a dash
|
|
||||||
r"(?:[a-z" + ul + "-]{2,63}" # domain label
|
|
||||||
r"|xn--[a-z0-9]{1,59})" # or punycode label
|
|
||||||
r"(?<!-)" # can't end with a dash
|
|
||||||
r"\.?" # may have a trailing dot
|
|
||||||
)
|
|
||||||
host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
|
host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
|
||||||
|
|
||||||
regex = _lazy_re_compile(
|
regex = _lazy_re_compile(
|
||||||
|
@ -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
|
validation, so you'd need to add them to the ``allowlist`` as
|
||||||
necessary.
|
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``
|
``URLValidator``
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@ -201,6 +220,15 @@ to, or in lieu of custom ``field.clean()`` methods.
|
|||||||
|
|
||||||
An :class:`EmailValidator` instance without any customizations.
|
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``
|
``validate_slug``
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -363,7 +363,10 @@ Utilities
|
|||||||
Validators
|
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:
|
.. _backwards-incompatible-5.1:
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from django.core.files.base import ContentFile
|
|||||||
from django.core.validators import (
|
from django.core.validators import (
|
||||||
BaseValidator,
|
BaseValidator,
|
||||||
DecimalValidator,
|
DecimalValidator,
|
||||||
|
DomainNameValidator,
|
||||||
EmailValidator,
|
EmailValidator,
|
||||||
FileExtensionValidator,
|
FileExtensionValidator,
|
||||||
MaxLengthValidator,
|
MaxLengthValidator,
|
||||||
@ -21,6 +22,7 @@ from django.core.validators import (
|
|||||||
URLValidator,
|
URLValidator,
|
||||||
int_list_validator,
|
int_list_validator,
|
||||||
validate_comma_separated_integer_list,
|
validate_comma_separated_integer_list,
|
||||||
|
validate_domain_name,
|
||||||
validate_email,
|
validate_email,
|
||||||
validate_image_file_extension,
|
validate_image_file_extension,
|
||||||
validate_integer,
|
validate_integer,
|
||||||
@ -618,6 +620,38 @@ TEST_DATA = [
|
|||||||
(ProhibitNullCharactersValidator(), "\x00something", ValidationError),
|
(ProhibitNullCharactersValidator(), "\x00something", ValidationError),
|
||||||
(ProhibitNullCharactersValidator(), "something", None),
|
(ProhibitNullCharactersValidator(), "something", None),
|
||||||
(ProhibitNullCharactersValidator(), None, 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.
|
# Add valid and invalid URL tests.
|
||||||
@ -847,3 +881,25 @@ class TestValidatorEquality(TestCase):
|
|||||||
ProhibitNullCharactersValidator(message="message", code="code1"),
|
ProhibitNullCharactersValidator(message="message", code="code1"),
|
||||||
ProhibitNullCharactersValidator(message="message", code="code2"),
|
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"),
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user