Browse Source

Fixed #36000 -- Deprecated HTTP as the default protocol in urlize and urlizetrunc.

Ahmed Nassar 2 weeks ago
parent
commit
ec7044c706

+ 5 - 0
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

+ 21 - 1
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

+ 5 - 0
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

+ 16 - 0
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

+ 34 - 6
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 <a href="http://www.djangoproject.com"
-rel="nofollow">www.djangoproject.com</a>"``.
+If ``value`` is ``"Check out www.djangoproject.com"``, the output will be:
+
+.. code-block:: html+django
+
+    Check out <a href="http://www.djangoproject.com" rel="nofollow">www.djangoproject.com</a>
+
+.. 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 <a href="https://www.djangoproject.com" rel="nofollow">www.djangoproject.com</a>
+
+    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 <a href="http://www.djangoproject.com"
-rel="nofollow">www.djangoproj…</a>'``.
+If ``value`` is ``"Check out www.djangoproject.com"``, the output would be:
+
+.. code-block:: html+django
+
+    Check out <a href="http://www.djangoproject.com" rel="nofollow">www.djangoproj…</a>
+
+.. 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 <a href="https://www.djangoproject.com" rel="nofollow">www.djangoproj…</a>
+
+    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.
 

+ 7 - 0
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
 =======================
 

+ 28 - 24
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"),
-            '<a href="http://www.google.com" rel="nofollow">www.google.com</a>',
+            '<a href="https://www.google.com" rel="nofollow">www.google.com</a>',
         )
         self.assertEqual(
             urlize("djangoproject.org"),
-            '<a href="http://djangoproject.org" rel="nofollow">djangoproject.org</a>',
+            '<a href="https://djangoproject.org" rel="nofollow">djangoproject.org</a>',
         )
         self.assertEqual(
             urlize("djangoproject.org/"),
-            '<a href="http://djangoproject.org/" rel="nofollow">djangoproject.org/</a>',
+            '<a href="https://djangoproject.org/" rel="nofollow">'
+            "djangoproject.org/</a>",
         )
 
     def test_url_split_chars(self):
@@ -137,21 +140,21 @@ class FunctionTests(SimpleTestCase):
         # part of URLs.
         self.assertEqual(
             urlize('www.server.com"abc'),
-            '<a href="http://www.server.com" rel="nofollow">www.server.com</a>&quot;'
+            '<a href="https://www.server.com" rel="nofollow">www.server.com</a>&quot;'
             "abc",
         )
         self.assertEqual(
             urlize("www.server.com'abc"),
-            '<a href="http://www.server.com" rel="nofollow">www.server.com</a>&#x27;'
+            '<a href="https://www.server.com" rel="nofollow">www.server.com</a>&#x27;'
             "abc",
         )
         self.assertEqual(
             urlize("www.server.com<abc"),
-            '<a href="http://www.server.com" rel="nofollow">www.server.com</a>&lt;abc',
+            '<a href="https://www.server.com" rel="nofollow">www.server.com</a>&lt;abc',
         )
         self.assertEqual(
             urlize("www.server.com>abc"),
-            '<a href="http://www.server.com" rel="nofollow">www.server.com</a>&gt;abc',
+            '<a href="https://www.server.com" rel="nofollow">www.server.com</a>&gt;abc',
         )
 
     def test_email(self):
@@ -184,7 +187,7 @@ class FunctionTests(SimpleTestCase):
     def test_urlencoded(self):
         self.assertEqual(
             urlize("www.mystore.com/30%OffCoupons!"),
-            '<a href="http://www.mystore.com/30%25OffCoupons" rel="nofollow">'
+            '<a href="https://www.mystore.com/30%25OffCoupons" rel="nofollow">'
             "www.mystore.com/30%OffCoupons</a>!",
         )
         self.assertEqual(
@@ -222,7 +225,7 @@ class FunctionTests(SimpleTestCase):
         self.assertEqual(
             urlize("foo@bar.com or www.bar.com"),
             '<a href="mailto:foo@bar.com">foo@bar.com</a> or '
-            '<a href="http://www.bar.com" rel="nofollow">www.bar.com</a>',
+            '<a href="https://www.bar.com" rel="nofollow">www.bar.com</a>',
         )
 
     def test_idn(self):
@@ -236,11 +239,11 @@ class FunctionTests(SimpleTestCase):
         )
         self.assertEqual(
             urlize("www.c✶.ws"),
-            '<a href="http://www.c%E2%9C%B6.ws" rel="nofollow">www.c✶.ws</a>',
+            '<a href="https://www.c%E2%9C%B6.ws" rel="nofollow">www.c✶.ws</a>',
         )
         self.assertEqual(
             urlize("c✶.org"),
-            '<a href="http://c%E2%9C%B6.org" rel="nofollow">c✶.org</a>',
+            '<a href="https://c%E2%9C%B6.org" rel="nofollow">c✶.org</a>',
         )
         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"),
-            '<a href="http://www.xn--iny-zx5a.com/idna2003"'
+            '<a href="https://www.xn--iny-zx5a.com/idna2003"'
             ' rel="nofollow">www.xn--iny-zx5a.com/idna2003</a>',
         )
         self.assertEqual(
             urlize("www.xn--fa-hia.com/idna2008"),
-            '<a href="http://www.xn--fa-hia.com/idna2008"'
+            '<a href="https://www.xn--fa-hia.com/idna2008"'
             ' rel="nofollow">www.xn--fa-hia.com/idna2008</a>',
         )
 
@@ -272,7 +275,7 @@ class FunctionTests(SimpleTestCase):
         #16656 - Check urlize accepts more TLDs
         """
         self.assertEqual(
-            urlize("usa.gov"), '<a href="http://usa.gov" rel="nofollow">usa.gov</a>'
+            urlize("usa.gov"), '<a href="https://usa.gov" rel="nofollow">usa.gov</a>'
         )
 
     def test_invalid_email(self):
@@ -351,11 +354,12 @@ class FunctionTests(SimpleTestCase):
         """
         self.assertEqual(
             urlize("[see www.example.com]"),
-            '[see <a href="http://www.example.com" rel="nofollow">www.example.com</a>]',
+            '[see <a href="https://www.example.com" rel="nofollow">'
+            "www.example.com</a>]",
         )
         self.assertEqual(
             urlize("see test[at[example.com"),
-            'see <a href="http://test[at[example.com" rel="nofollow">'
+            'see <a href="https://test[at[example.com" rel="nofollow">'
             "test[at[example.com</a>",
         )
         self.assertEqual(
@@ -443,22 +447,22 @@ class FunctionTests(SimpleTestCase):
         """
         self.assertEqual(
             urlize("Go to djangoproject.com! and enjoy."),
-            'Go to <a href="http://djangoproject.com" rel="nofollow">djangoproject.com'
+            'Go to <a href="https://djangoproject.com" rel="nofollow">djangoproject.com'
             "</a>! and enjoy.",
         )
         self.assertEqual(
             urlize("Search for google.com/?q=! and see."),
-            'Search for <a href="http://google.com/?q=" rel="nofollow">google.com/?q='
+            'Search for <a href="https://google.com/?q=" rel="nofollow">google.com/?q='
             "</a>! and see.",
         )
         self.assertEqual(
             urlize("Search for google.com/?q=dj!`? and see."),
-            'Search for <a href="http://google.com/?q=dj%21%60%3F" rel="nofollow">'
+            'Search for <a href="https://google.com/?q=dj%21%60%3F" rel="nofollow">'
             "google.com/?q=dj!`?</a> and see.",
         )
         self.assertEqual(
             urlize("Search for google.com/?q=dj!`?! and see."),
-            'Search for <a href="http://google.com/?q=dj%21%60%3F" rel="nofollow">'
+            'Search for <a href="https://google.com/?q=dj%21%60%3F" rel="nofollow">'
             "google.com/?q=dj!`?</a>! and see.",
         )
 
@@ -468,14 +472,14 @@ class FunctionTests(SimpleTestCase):
     def test_autoescape(self):
         self.assertEqual(
             urlize('foo<a href=" google.com ">bar</a>buz'),
-            'foo&lt;a href=&quot; <a href="http://google.com" rel="nofollow">google.com'
-            "</a> &quot;&gt;bar&lt;/a&gt;buz",
+            'foo&lt;a href=&quot; <a href="https://google.com" rel="nofollow">'
+            "google.com</a> &quot;&gt;bar&lt;/a&gt;buz",
         )
 
     def test_autoescape_off(self):
         self.assertEqual(
             urlize('foo<a href=" google.com ">bar</a>buz', autoescape=False),
-            'foo<a href=" <a href="http://google.com" rel="nofollow">google.com</a> ">'
+            'foo<a href=" <a href="https://google.com" rel="nofollow">google.com</a> ">'
             "bar</a>buz",
         )
 
@@ -483,7 +487,7 @@ class FunctionTests(SimpleTestCase):
         prepend_www = lazy(lambda url: "www." + url, str)
         self.assertEqual(
             urlize(prepend_www("google.com")),
-            '<a href="http://www.google.com" rel="nofollow">www.google.com</a>',
+            '<a href="https://www.google.com" rel="nofollow">www.google.com</a>',
         )
 
     @mock.patch.object(Urlizer, "handle_word", return_value="test")

+ 5 - 3
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('foo<a href=" google.com ">bar</a>buz', 10),
-            'foo&lt;a href=&quot; <a href="http://google.com" rel="nofollow">google.com'
-            "</a> &quot;&gt;bar&lt;/a&gt;buz",
+            'foo&lt;a href=&quot; <a href="https://google.com" rel="nofollow">'
+            "google.com</a> &quot;&gt;bar&lt;/a&gt;buz",
         )
 
     def test_autoescape_off(self):
         self.assertEqual(
             urlizetrunc('foo<a href=" google.com ">bar</a>buz', 9, autoescape=False),
-            'foo<a href=" <a href="http://google.com" rel="nofollow">google.c…</a> ">'
+            'foo<a href=" <a href="https://google.com" rel="nofollow">google.c…</a> ">'
             "bar</a>buz",
         )

+ 20 - 4
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 <a href="http://google.com/?q=">google.com/?q=</a>! and '
+                'Search for <a href="https://google.com/?q=">google.com/?q=</a>! and '
                 "see.",
             ),
             (
                 "Search for google.com/?q=1&lt! and see.",
-                'Search for <a href="http://google.com/?q=1%3C">google.com/?q=1&lt'
+                'Search for <a href="https://google.com/?q=1%3C">google.com/?q=1&lt'
                 "</a>! and see.",
             ),
             (
                 lazystr("Search for google.com/?q=!"),
-                'Search for <a href="http://google.com/?q=">google.com/?q=</a>!',
+                'Search for <a href="https://google.com/?q=">google.com/?q=</a>!',
             ),
             (
                 "http://www.foo.bar/",
@@ -388,7 +391,7 @@ class TestUtilsHtml(SimpleTestCase):
             (
                 "Look on www.نامه‌ای.com.",
                 "Look on <a "
-                'href="http://www.%D9%86%D8%A7%D9%85%D9%87%E2%80%8C%D8%A7%DB%8C.com"'
+                'href="https://www.%D9%86%D8%A7%D9%85%D9%87%E2%80%8C%D8%A7%DB%8C.com"'
                 ">www.نامه‌ای.com</a>.",
             ),
             ("foo@example.com", '<a href="mailto:foo@example.com">foo@example.com</a>'),
@@ -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 <a href="http://example.com">example.com</a>',
+            )
+
     def test_urlize_unchanged_inputs(self):
         tests = (
             ("a" + "@a" * 50000) + "a",  # simple_email_re catastrophic test