Fixed #21242 -- Allowed more IANA schemes in URLValidator

Thanks Sascha Peilicke for the report and initial patch, and
Tim Graham for the review.
This commit is contained in:
Claude Paroz 2013-12-21 00:15:39 +01:00
parent 9f13c33281
commit 6d66ba5948
4 changed files with 44 additions and 4 deletions

View File

@ -44,7 +44,7 @@ class RegexValidator(object):
@deconstructible @deconstructible
class URLValidator(RegexValidator): class URLValidator(RegexValidator):
regex = re.compile( regex = re.compile(
r'^(?:http|ftp)s?://' # http:// or https:// r'^(?:[a-z0-9\.\-]*)://' # scheme is validated separately
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
r'localhost|' # localhost... r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
@ -52,14 +52,26 @@ class URLValidator(RegexValidator):
r'(?::\d+)?' # optional port r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE) r'(?:/?|[/?]\S+)$', re.IGNORECASE)
message = _('Enter a valid URL.') message = _('Enter a valid URL.')
schemes = ['http', 'https', 'ftp', 'ftps']
def __init__(self, schemes=None, **kwargs):
super(URLValidator, self).__init__(**kwargs)
if schemes is not None:
self.schemes = schemes
def __call__(self, value): def __call__(self, value):
value = force_text(value)
# Check first if the scheme is valid
scheme = value.split('://')[0].lower()
if scheme not in self.schemes:
raise ValidationError(self.message, code=self.code)
# Then check full URL
try: try:
super(URLValidator, self).__call__(value) super(URLValidator, self).__call__(value)
except ValidationError as e: except ValidationError as e:
# Trivial case failed. Try for possible IDN domain # Trivial case failed. Try for possible IDN domain
if value: if value:
value = force_text(value)
scheme, netloc, path, query, fragment = urlsplit(value) scheme, netloc, path, query, fragment = urlsplit(value)
try: try:
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE

View File

@ -87,10 +87,24 @@ to, or in lieu of custom ``field.clean()`` methods.
``URLValidator`` ``URLValidator``
---------------- ----------------
.. class:: URLValidator() .. class:: URLValidator([schemes=None, regex=None, message=None, code=None])
A :class:`RegexValidator` that ensures a value looks like a URL, and raises A :class:`RegexValidator` that ensures a value looks like a URL, and raises
an error code of ``'invalid'`` if it doesn't. an error code of ``'invalid'`` if it doesn't. In addition to the optional
arguments of its parent :class:`RegexValidator` class, ``URLValidator``
accepts an extra optional attribute:
.. attribute:: schemes
URL/URI scheme list to validate against. If not provided, the default
list is ``['http', 'https', 'ftp', 'ftps']``. As a reference, the IANA
Web site provides a full list of `valid URI schemes`_.
.. _valid URI schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
.. versionchanged:: 1.7
The optional ``schemes`` attribute was added.
``validate_email`` ``validate_email``
------------------ ------------------

View File

@ -567,6 +567,13 @@ Tests
* :meth:`~django.test.TransactionTestCase.assertNumQueries` now prints * :meth:`~django.test.TransactionTestCase.assertNumQueries` now prints
out the list of executed queries if the assertion fails. out the list of executed queries if the assertion fails.
Validators
^^^^^^^^^^
* :class:`~django.core.validators.URLValidator` now accepts an optional
``schemes`` argument which allows customization of the accepted URI schemes
(instead of the defaults ``http(s)`` and ``ftp(s)``).
Backwards incompatible changes in 1.7 Backwards incompatible changes in 1.7
===================================== =====================================

View File

@ -18,6 +18,7 @@ from django.test.utils import str_prefix
NOW = datetime.now() NOW = datetime.now()
EXTENDED_SCHEMES = ['http', 'https', 'ftp', 'ftps', 'git', 'file']
TEST_DATA = ( TEST_DATA = (
# (validator, value, expected), # (validator, value, expected),
@ -141,6 +142,7 @@ TEST_DATA = (
(MinLengthValidator(10), '', ValidationError), (MinLengthValidator(10), '', ValidationError),
(URLValidator(), 'http://www.djangoproject.com/', None), (URLValidator(), 'http://www.djangoproject.com/', None),
(URLValidator(), 'HTTP://WWW.DJANGOPROJECT.COM/', None),
(URLValidator(), 'http://localhost/', None), (URLValidator(), 'http://localhost/', None),
(URLValidator(), 'http://example.com/', None), (URLValidator(), 'http://example.com/', None),
(URLValidator(), 'http://www.example.com/', None), (URLValidator(), 'http://www.example.com/', None),
@ -155,6 +157,8 @@ TEST_DATA = (
(URLValidator(), 'https://example.com/', None), (URLValidator(), 'https://example.com/', None),
(URLValidator(), 'ftp://example.com/', None), (URLValidator(), 'ftp://example.com/', None),
(URLValidator(), 'ftps://example.com/', None), (URLValidator(), 'ftps://example.com/', None),
(URLValidator(EXTENDED_SCHEMES), 'file://localhost/path', None),
(URLValidator(EXTENDED_SCHEMES), 'git://example.com/', None),
(URLValidator(), 'foo', ValidationError), (URLValidator(), 'foo', ValidationError),
(URLValidator(), 'http://', ValidationError), (URLValidator(), 'http://', ValidationError),
@ -165,6 +169,9 @@ TEST_DATA = (
(URLValidator(), 'http://-invalid.com', ValidationError), (URLValidator(), 'http://-invalid.com', ValidationError),
(URLValidator(), 'http://inv-.alid-.com', ValidationError), (URLValidator(), 'http://inv-.alid-.com', ValidationError),
(URLValidator(), 'http://inv-.-alid.com', ValidationError), (URLValidator(), 'http://inv-.-alid.com', ValidationError),
(URLValidator(), 'file://localhost/path', ValidationError),
(URLValidator(), 'git://example.com/', ValidationError),
(URLValidator(EXTENDED_SCHEMES), 'git://-invalid.com', ValidationError),
(BaseValidator(True), True, None), (BaseValidator(True), True, None),
(BaseValidator(True), False, ValidationError), (BaseValidator(True), False, ValidationError),