diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 536bbb7c45..672c46b88f 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -662,3 +662,8 @@ SECURE_REDIRECT_EXEMPT = [] SECURE_REFERRER_POLICY = "same-origin" SECURE_SSL_HOST = None SECURE_SSL_REDIRECT = False + +# RemovedInDjango70Warning: A transitional setting helpful in early adoption of +# HTTPS as the default protocol in urlize and urlizetrunc when no protocol is +# provided. Set to True to assume HTTPS during the Django 6.x release cycle. +URLIZE_ASSUME_HTTPS = False diff --git a/django/utils/html.py b/django/utils/html.py index 182b7d4cec..a9d029165f 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -3,12 +3,15 @@ import html import json import re +import warnings from collections.abc import Mapping from html.parser import HTMLParser from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit +from django.conf import settings from django.core.exceptions import SuspiciousOperation, ValidationError from django.core.validators import EmailValidator +from django.utils.deprecation import RemovedInDjango70Warning from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS from django.utils.regex_helper import _lazy_re_compile @@ -343,7 +346,24 @@ class Urlizer: if len(middle) <= MAX_URL_LENGTH and self.simple_url_re.match(middle): url = smart_urlquote(html.unescape(middle)) elif len(middle) <= MAX_URL_LENGTH and self.simple_url_2_re.match(middle): - url = smart_urlquote("http://%s" % html.unescape(middle)) + unescaped_middle = html.unescape(middle) + # RemovedInDjango70Warning: When the deprecation ends, replace with: + # url = smart_urlquote(f"https://{unescaped_middle}") + protocol = ( + "https" + if getattr(settings, "URLIZE_ASSUME_HTTPS", False) + else "http" + ) + if not settings.URLIZE_ASSUME_HTTPS: + warnings.warn( + "The default protocol will be changed from HTTP to " + "HTTPS in Django 7.0. Set the URLIZE_ASSUME_HTTPS " + "transitional setting to True to opt into using HTTPS as the " + "new default protocol.", + RemovedInDjango70Warning, + stacklevel=2, + ) + url = smart_urlquote(f"{protocol}://{unescaped_middle}") elif ":" not in middle and self.is_email_simple(middle): local, domain = middle.rsplit("@", 1) # Encode per RFC 6068 Section 2 (items 1, 4, 5). Defer any IDNA diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 15a44e78e4..1ac2c291e0 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -23,6 +23,11 @@ details on these changes. * The ``django.contrib.postgres.aggregates.mixins.OrderableAggMixin`` class will be removed. +* The default protocol in ``urlize`` and ``urlizetrunc`` template filters will + change from HTTP to HTTPS. + +* The ``URLIZE_ASSUME_HTTPS`` transitional setting will be removed. + .. _deprecation-removed-in-6.1: 6.1 diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 556cbb4b1e..ab82b539b0 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2955,6 +2955,21 @@ enabled if a proxy which sets this header is in use. :setting:`USE_X_FORWARDED_HOST` takes priority over this setting. +.. setting:: URLIZE_ASSUME_HTTPS + +``URLIZE_ASSUME_HTTPS`` +----------------------- + +.. versionadded:: 6.0 +.. deprecated:: 6.0 + +Default: ``False`` + +Set this transitional setting to ``True`` to opt into using HTTPS as the +default protocol when none is provided in URLs processed by the +:tfilter:`urlize` and :tfilter:`urlizetrunc` template filters during the Django +6.x release cycle. + .. setting:: WSGI_APPLICATION ``WSGI_APPLICATION`` @@ -3766,6 +3781,7 @@ Security * :setting:`SECRET_KEY` * :setting:`SECRET_KEY_FALLBACKS` +* :setting:`URLIZE_ASSUME_HTTPS` * :setting:`X_FRAME_OPTIONS` Serialization diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 247f5e9890..f5470ad0eb 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -2905,9 +2905,23 @@ For example: {{ value|urlize }} -If ``value`` is ``"Check out www.djangoproject.com"``, the output will be -``"Check out www.djangoproject.com"``. +If ``value`` is ``"Check out www.djangoproject.com"``, the output will be: + +.. code-block:: html+django + + Check out www.djangoproject.com + +.. deprecated:: 6.0 + + The default protocol when none is provided will change from HTTP to HTTPS + in Django 7.0. Hence, the output will become: + + .. code-block:: html+django + + Check out www.djangoproject.com + + Set the transitional setting :setting:`URLIZE_ASSUME_HTTPS` to ``True`` to + opt into using HTTPS during the Django 6.x release cycle. In addition to web links, ``urlize`` also converts email addresses into ``mailto:`` links. If ``value`` is @@ -2942,9 +2956,23 @@ For example: {{ value|urlizetrunc:15 }} -If ``value`` is ``"Check out www.djangoproject.com"``, the output would be -``'Check out www.djangoproj…'``. +If ``value`` is ``"Check out www.djangoproject.com"``, the output would be: + +.. code-block:: html+django + + Check out www.djangoproj… + +.. deprecated:: 6.0 + + The default protocol when none is provided will change from HTTP to HTTPS + in Django 7.0. Hence, the output will become: + + .. code-block:: html+django + + Check out www.djangoproj… + + Set the transitional setting :setting:`URLIZE_ASSUME_HTTPS` to ``True`` to + opt into using HTTPS during the Django 6.x release cycle. As with urlize_, this filter should only be applied to plain text. diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index c604638deb..2cbab2dace 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -321,6 +321,13 @@ Miscellaneous * The PostgreSQL ``OrderableAggMixin`` is deprecated in favor of the ``order_by`` attribute now available on the ``Aggregate`` class. +* The default protocol in :tfilter:`urlize` and :tfilter:`urlizetrunc` will + change from HTTP to HTTPS in Django 7.0. Set the transitional setting + ``URLIZE_ASSUME_HTTPS`` to ``True`` to opt into assuming HTTPS during the + Django 6.x release cycle. + +* ``URLIZE_ASSUME_HTTPS`` transitional setting is deprecated. + Features removed in 6.0 ======================= diff --git a/tests/template_tests/filter_tests/test_urlize.py b/tests/template_tests/filter_tests/test_urlize.py index c186acd948..043029c8dc 100644 --- a/tests/template_tests/filter_tests/test_urlize.py +++ b/tests/template_tests/filter_tests/test_urlize.py @@ -2,6 +2,7 @@ from unittest import mock from django.template.defaultfilters import urlize from django.test import SimpleTestCase +from django.test.utils import override_settings from django.utils.functional import lazy from django.utils.html import Urlizer from django.utils.safestring import mark_safe @@ -109,6 +110,7 @@ class UrlizeTests(SimpleTestCase): ) +@override_settings(URLIZE_ASSUME_HTTPS=True) class FunctionTests(SimpleTestCase): def test_urls(self): self.assertEqual( @@ -121,15 +123,16 @@ class FunctionTests(SimpleTestCase): ) self.assertEqual( urlize("www.google.com"), - 'www.google.com', + 'www.google.com', ) self.assertEqual( urlize("djangoproject.org"), - 'djangoproject.org', + 'djangoproject.org', ) self.assertEqual( urlize("djangoproject.org/"), - 'djangoproject.org/', + '' + "djangoproject.org/", ) def test_url_split_chars(self): @@ -137,21 +140,21 @@ class FunctionTests(SimpleTestCase): # part of URLs. self.assertEqual( urlize('www.server.com"abc'), - 'www.server.com"' + 'www.server.com"' "abc", ) self.assertEqual( urlize("www.server.com'abc"), - 'www.server.com'' + 'www.server.com'' "abc", ) self.assertEqual( urlize("www.server.comwww.server.com<abc', + 'www.server.com<abc', ) self.assertEqual( urlize("www.server.com>abc"), - 'www.server.com>abc', + 'www.server.com>abc', ) def test_email(self): @@ -184,7 +187,7 @@ class FunctionTests(SimpleTestCase): def test_urlencoded(self): self.assertEqual( urlize("www.mystore.com/30%OffCoupons!"), - '' + '' "www.mystore.com/30%OffCoupons!", ) self.assertEqual( @@ -222,7 +225,7 @@ class FunctionTests(SimpleTestCase): self.assertEqual( urlize("foo@bar.com or www.bar.com"), 'foo@bar.com or ' - 'www.bar.com', + 'www.bar.com', ) def test_idn(self): @@ -236,11 +239,11 @@ class FunctionTests(SimpleTestCase): ) self.assertEqual( urlize("www.c✶.ws"), - 'www.c✶.ws', + 'www.c✶.ws', ) self.assertEqual( urlize("c✶.org"), - 'c✶.org', + 'c✶.org', ) self.assertEqual( urlize("info@c✶.org"), @@ -250,12 +253,12 @@ class FunctionTests(SimpleTestCase): # Pre-encoded IDNA is urlized but not re-encoded. self.assertEqual( urlize("www.xn--iny-zx5a.com/idna2003"), - 'www.xn--iny-zx5a.com/idna2003', ) self.assertEqual( urlize("www.xn--fa-hia.com/idna2008"), - 'www.xn--fa-hia.com/idna2008', ) @@ -272,7 +275,7 @@ class FunctionTests(SimpleTestCase): #16656 - Check urlize accepts more TLDs """ self.assertEqual( - urlize("usa.gov"), 'usa.gov' + urlize("usa.gov"), 'usa.gov' ) def test_invalid_email(self): @@ -351,11 +354,12 @@ class FunctionTests(SimpleTestCase): """ self.assertEqual( urlize("[see www.example.com]"), - '[see www.example.com]', + '[see ' + "www.example.com]", ) self.assertEqual( urlize("see test[at[example.com"), - 'see ' + 'see ' "test[at[example.com", ) self.assertEqual( @@ -443,22 +447,22 @@ class FunctionTests(SimpleTestCase): """ self.assertEqual( urlize("Go to djangoproject.com! and enjoy."), - 'Go to djangoproject.com' + 'Go to djangoproject.com' "! and enjoy.", ) self.assertEqual( urlize("Search for google.com/?q=! and see."), - 'Search for google.com/?q=' + 'Search for google.com/?q=' "! and see.", ) self.assertEqual( urlize("Search for google.com/?q=dj!`? and see."), - 'Search for ' + 'Search for ' "google.com/?q=dj!`? and see.", ) self.assertEqual( urlize("Search for google.com/?q=dj!`?! and see."), - 'Search for ' + 'Search for ' "google.com/?q=dj!`?! and see.", ) @@ -468,14 +472,14 @@ class FunctionTests(SimpleTestCase): def test_autoescape(self): self.assertEqual( urlize('foobarbuz'), - 'foo<a href=" google.com' - " ">bar</a>buz", + 'foo<a href=" ' + "google.com ">bar</a>buz", ) def test_autoescape_off(self): self.assertEqual( urlize('foobarbuz', autoescape=False), - 'foogoogle.com ">' + 'foogoogle.com ">' "barbuz", ) @@ -483,7 +487,7 @@ class FunctionTests(SimpleTestCase): prepend_www = lazy(lambda url: "www." + url, str) self.assertEqual( urlize(prepend_www("google.com")), - 'www.google.com', + 'www.google.com', ) @mock.patch.object(Urlizer, "handle_word", return_value="test") diff --git a/tests/template_tests/filter_tests/test_urlizetrunc.py b/tests/template_tests/filter_tests/test_urlizetrunc.py index 752ee3571e..df8c4efa79 100644 --- a/tests/template_tests/filter_tests/test_urlizetrunc.py +++ b/tests/template_tests/filter_tests/test_urlizetrunc.py @@ -1,5 +1,6 @@ from django.template.defaultfilters import urlizetrunc from django.test import SimpleTestCase +from django.test.utils import override_settings from django.utils.safestring import mark_safe from ..utils import setup @@ -48,6 +49,7 @@ class UrlizetruncTests(SimpleTestCase): ) +@override_settings(URLIZE_ASSUME_HTTPS=True) class FunctionTests(SimpleTestCase): def test_truncate(self): uri = "http://31characteruri.com/test/" @@ -93,13 +95,13 @@ class FunctionTests(SimpleTestCase): def test_autoescape(self): self.assertEqual( urlizetrunc('foobarbuz', 10), - 'foo<a href=" google.com' - " ">bar</a>buz", + 'foo<a href=" ' + "google.com ">bar</a>buz", ) def test_autoescape_off(self): self.assertEqual( urlizetrunc('foobarbuz', 9, autoescape=False), - 'foogoogle.c… ">' + 'foogoogle.c… ">' "barbuz", ) diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index 4db3816c72..88e77a3c82 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -4,6 +4,8 @@ from datetime import datetime from django.core.exceptions import SuspiciousOperation from django.core.serializers.json import DjangoJSONEncoder from django.test import SimpleTestCase +from django.test.utils import override_settings +from django.utils.deprecation import RemovedInDjango70Warning from django.utils.functional import lazystr from django.utils.html import ( conditional_escape, @@ -22,6 +24,7 @@ from django.utils.html import ( from django.utils.safestring import mark_safe +@override_settings(URLIZE_ASSUME_HTTPS=True) class TestUtilsHtml(SimpleTestCase): def check_output(self, function, value, output=None): """ @@ -369,17 +372,17 @@ class TestUtilsHtml(SimpleTestCase): tests = ( ( "Search for google.com/?q=! and see.", - 'Search for google.com/?q=! and ' + 'Search for google.com/?q=! and ' "see.", ), ( "Search for google.com/?q=1<! and see.", - 'Search for google.com/?q=1<' + 'Search for google.com/?q=1<' "! and see.", ), ( lazystr("Search for google.com/?q=!"), - 'Search for google.com/?q=!', + 'Search for google.com/?q=!', ), ( "http://www.foo.bar/", @@ -388,7 +391,7 @@ class TestUtilsHtml(SimpleTestCase): ( "Look on www.نامه‌ای.com.", "Look on www.نامه‌ای.com.", ), ("foo@example.com", 'foo@example.com'), @@ -422,6 +425,19 @@ class TestUtilsHtml(SimpleTestCase): with self.subTest(value=value): self.assertEqual(urlize(value), output) + @override_settings(URLIZE_ASSUME_HTTPS=False) + def test_urlize_http_default_warning(self): + msg = ( + "The default protocol will be changed from HTTP to HTTPS in Django 7.0. " + "Set the URLIZE_ASSUME_HTTPS transitional setting to True to opt into " + "using HTTPS as the new default protocol." + ) + with self.assertWarnsMessage(RemovedInDjango70Warning, msg): + self.assertEqual( + urlize("Visit example.com"), + 'Visit example.com', + ) + def test_urlize_unchanged_inputs(self): tests = ( ("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test