From 6d66ba59488bbd0d4f0c2f32b2921f1512301143 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 21 Dec 2013 00:15:39 +0100 Subject: [PATCH] Fixed #21242 -- Allowed more IANA schemes in URLValidator Thanks Sascha Peilicke for the report and initial patch, and Tim Graham for the review. --- django/core/validators.py | 16 ++++++++++++++-- docs/ref/validators.txt | 18 ++++++++++++++++-- docs/releases/1.7.txt | 7 +++++++ tests/validators/tests.py | 7 +++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/django/core/validators.py b/django/core/validators.py index 695dda258b..5559bca28f 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -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 diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 92e257ca85..1b706ab8e3 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -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`` ------------------ diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 0d83c3aa81..a7e7da6d15 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -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 ===================================== diff --git a/tests/validators/tests.py b/tests/validators/tests.py index b0d1376aee..23fa1e3830 100644 --- a/tests/validators/tests.py +++ b/tests/validators/tests.py @@ -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),