From 9cdff1b51c35154864873ee6268dde3987d926f2 Mon Sep 17 00:00:00 2001
From: Helge Jung <hej@c3pb.de>
Date: Sat, 21 Dec 2024 00:38:49 +0100
Subject: [PATCH] extend core.utils.resolve_internal_url() to allow any HTTP
 url

This is done to allow internal URLs in RoomLinks without having to
handle non-internal URLs in a different way.
---
 src/core/markdown.py     |  2 +-
 src/core/models/rooms.py |  8 ++------
 src/core/tests/utils.py  | 12 ++++++++++++
 src/core/utils.py        |  8 ++++++--
 4 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/src/core/markdown.py b/src/core/markdown.py
index 06e3365b4..b6011d3c2 100644
--- a/src/core/markdown.py
+++ b/src/core/markdown.py
@@ -134,7 +134,7 @@ class MyHtmlRenderer(HTMLRenderer):
         from .utils import resolve_internal_url
 
         # attempt resolving an internal URL
-        if resolved_internal_url := resolve_internal_url(url, fallback_as_is=False):
+        if resolved_internal_url := resolve_internal_url(url, accept_http_https=False, fallback_as_is=False):
             url = resolved_internal_url
 
         # derive external link (i.e. apply dereferer), if its not an internal or trusted location
diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py
index 21e0c1ee2..9a4c275f2 100644
--- a/src/core/models/rooms.py
+++ b/src/core/models/rooms.py
@@ -8,7 +8,6 @@ from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.postgres.fields import DateTimeRangeField
 from django.core.exceptions import ValidationError
-from django.core.validators import URLValidator
 from django.db import models
 from django.db.models import Q, QuerySet
 from django.utils.text import slugify
@@ -21,7 +20,7 @@ from core.models.base_managers import ConferenceManagerMixin
 from core.models.conference import Conference, ConferenceMember
 from core.models.shared import BackendMixin
 from core.models.tags import TagItem
-from core.utils import str2bool
+from core.utils import resolve_internal_url, str2bool
 from core.validators import FileSizeValidator, ImageDimensionValidator
 
 
@@ -483,10 +482,7 @@ class RoomLink(models.Model):
 
     def clean(self):
         if self.link_type in RoomLink.URL_LINKTYPES:
-            validator = URLValidator()
-            try:
-                validator(self.link)
-            except ValidationError:
+            if not resolve_internal_url(self.link, fallback_as_is=False):
                 raise ValidationError({'link': _('RoomLink__link__must_be_url')})
 
     def __str__(self):
diff --git a/src/core/tests/utils.py b/src/core/tests/utils.py
index b4d4ca33b..489713310 100644
--- a/src/core/tests/utils.py
+++ b/src/core/tests/utils.py
@@ -90,6 +90,18 @@ class InternalUrlTests(TestCase):
         for check in checks:
             self.assertEqual(check[1], resolve_internal_url(check[0]))
 
+        # combinations of accept_http_https and fallback_as_is with a https URL
+        self.assertEqual('https://ccc.de/', resolve_internal_url('https://ccc.de/', accept_http_https=True, fallback_as_is=True))
+        self.assertEqual('https://ccc.de/', resolve_internal_url('https://ccc.de/', accept_http_https=True, fallback_as_is=False))
+        self.assertEqual('https://ccc.de/', resolve_internal_url('https://ccc.de/', accept_http_https=False, fallback_as_is=True))
+        self.assertIsNone(resolve_internal_url('https://ccc.de/', accept_http_https=False, fallback_as_is=False))
+
+        # combinations of accept_http_https and fallback_as_is with an invalid URL
+        self.assertEqual('foo', resolve_internal_url('foo', accept_http_https=True, fallback_as_is=True))
+        self.assertIsNone(resolve_internal_url('foo', accept_http_https=True, fallback_as_is=False))
+        self.assertEqual('foo', resolve_internal_url('foo', accept_http_https=False, fallback_as_is=True))
+        self.assertIsNone(resolve_internal_url('foo', accept_http_https=False, fallback_as_is=False))
+
     @override_settings(ADDITIONAL_LINK_PROTOCOLS={'c3nav': 'https://test.c3nav.de/l/VALUE', 'ccc': 'https://ccc.de/?goto=VALUE'})
     def test_additional_link_protocols(self):
         checks = [
diff --git a/src/core/utils.py b/src/core/utils.py
index c445d70d3..cd3c151c2 100644
--- a/src/core/utils.py
+++ b/src/core/utils.py
@@ -156,7 +156,7 @@ def mask_url(url):
     return urlunparse(masked)
 
 
-def resolve_internal_url(url: str, fallback_as_is: bool = True) -> str | None:
+def resolve_internal_url(url: str, accept_http_https: bool = True, fallback_as_is: bool = True) -> str | None:
     """
     Resolves special URLs like
      - conference://url_resolver_name
@@ -164,7 +164,8 @@ def resolve_internal_url(url: str, fallback_as_is: bool = True) -> str | None:
      - event://slug
      - wiki://slug
 
-    Regular URLs (i.e. https://...) are returned as-is by default but may be resolved as None (i.e. no match).
+    Regular URLs (i.e. https://...) are accepted by default, too.
+    Everything else (unparseable, unknown protocl) are returned as-is by default but may be resolved as None (i.e. no match).
     """
     try:
         protocol, rhs = url.split('://', maxsplit=1)
@@ -175,6 +176,9 @@ def resolve_internal_url(url: str, fallback_as_is: bool = True) -> str | None:
     remainder = rhs_splitted[0]
     query_string = '' if len(rhs_splitted) == 1 else rhs_splitted[1]
 
+    if accept_http_https and protocol in ['http', 'https']:
+        return url
+
     try:
         from core.templatetags.hub_absolute import hub_absolute  # pylint: disable=import-outside-toplevel
 
-- 
GitLab