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