Commit 6d66ba59 authored by Claude Paroz's avatar Claude Paroz
Browse files

Fixed #21242 -- Allowed more IANA schemes in URLValidator

Thanks Sascha Peilicke for the report and initial patch, and
Tim Graham for the review.
parent 9f13c332
Loading
Loading
Loading
Loading
+14 −2
Original line number Diff line number Diff line
@@ -44,7 +44,7 @@ class RegexValidator(object):
@deconstructible
class URLValidator(RegexValidator):
    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'localhost|'  # localhost...
        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'(?:/?|[/?]\S+)$', re.IGNORECASE)
    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):
        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:
            super(URLValidator, self).__call__(value)
        except ValidationError as e:
            # Trivial case failed. Try for possible IDN domain
            if value:
                value = force_text(value)
                scheme, netloc, path, query, fragment = urlsplit(value)
                try:
                    netloc = netloc.encode('idna').decode('ascii')  # IDN -> ACE
+16 −2
Original line number Diff line number Diff line
@@ -87,10 +87,24 @@ to, or in lieu of custom ``field.clean()`` methods.

``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
    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``
------------------
+7 −0
Original line number Diff line number Diff line
@@ -567,6 +567,13 @@ Tests
* :meth:`~django.test.TransactionTestCase.assertNumQueries` now prints
  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
=====================================

+7 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ from django.test.utils import str_prefix


NOW = datetime.now()
EXTENDED_SCHEMES = ['http', 'https', 'ftp', 'ftps', 'git', 'file']

TEST_DATA = (
    # (validator, value, expected),
@@ -141,6 +142,7 @@ TEST_DATA = (
    (MinLengthValidator(10), '', ValidationError),

    (URLValidator(), 'http://www.djangoproject.com/', None),
    (URLValidator(), 'HTTP://WWW.DJANGOPROJECT.COM/', None),
    (URLValidator(), 'http://localhost/', None),
    (URLValidator(), 'http://example.com/', None),
    (URLValidator(), 'http://www.example.com/', None),
@@ -155,6 +157,8 @@ TEST_DATA = (
    (URLValidator(), 'https://example.com/', None),
    (URLValidator(), 'ftp://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(), 'http://', ValidationError),
@@ -165,6 +169,9 @@ TEST_DATA = (
    (URLValidator(), 'http://-invalid.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), False, ValidationError),