From f468fb0a07682f8b8e67f90611170cc289702fa7 Mon Sep 17 00:00:00 2001
From: Helge Jung <hej@c3pb.de>
Date: Fri, 20 Dec 2024 23:29:25 +0100
Subject: [PATCH] internal_urls: allow arbitrary additional protocols

This shall be used for e.g. c3nav to support c3nav://location URLs which
are remapped to https://38c3.c3nav.de/l/VALUE (with "VALUE" being the
placeholder).

See the unittest for additional usage examples.
---
 src/core/tests/utils.py  | 14 +++++++++++++-
 src/core/utils.py        | 12 ++++++++++++
 src/hub/settings/base.py |  7 +++++++
 3 files changed, 32 insertions(+), 1 deletion(-)

diff --git a/src/core/tests/utils.py b/src/core/tests/utils.py
index b39677a33..b4d4ca33b 100644
--- a/src/core/tests/utils.py
+++ b/src/core/tests/utils.py
@@ -2,7 +2,7 @@ import uuid
 from datetime import timedelta
 
 from django.conf import settings
-from django.test import TestCase
+from django.test import TestCase, override_settings
 from django.urls import reverse
 from django.utils.timezone import now
 
@@ -90,6 +90,18 @@ class InternalUrlTests(TestCase):
         for check in checks:
             self.assertEqual(check[1], resolve_internal_url(check[0]))
 
+    @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 = [
+            ('c3nav://unittest', 'https://test.c3nav.de/l/unittest'),
+            ('c3nav://unittest?a=b', 'https://test.c3nav.de/l/unittest?a=b'),
+            ('ccc://unittest', 'https://ccc.de/?goto=unittest'),
+            ('ccc://unittest?foo=bar', 'https://ccc.de/?goto=unittest&foo=bar'),
+        ]
+
+        for check in checks:
+            self.assertEqual(check[1], resolve_internal_url(check[0]))
+
 
 class GitRepoOfflineTests(TestCase):
     def test_invalid_url_local_path(self):
diff --git a/src/core/utils.py b/src/core/utils.py
index 3ec4c560b..c445d70d3 100644
--- a/src/core/utils.py
+++ b/src/core/utils.py
@@ -12,6 +12,7 @@ from urllib.parse import parse_qs, urlparse, urlunparse
 
 import requests
 
+from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.core.files.base import ContentFile
 from django.core.validators import validate_slug
@@ -189,6 +190,17 @@ def resolve_internal_url(url: str, fallback_as_is: bool = True) -> str | None:
         if protocol == 'wiki':
             return hub_absolute('plainui:static_page', query_string=query_string, page_slug=remainder)
 
+        if link := settings.ADDITIONAL_LINK_PROTOCOLS.get(protocol):
+            link = link.replace('VALUE', remainder)
+            if query_string:
+                result = urlparse(link)
+                if q := result.query:
+                    q += '&' + query_string
+                else:
+                    q = query_string
+                link = urlunparse((result[0], result[1], result[2], result[3], q, result[5]))
+            return link
+
     except NoReverseMatch:
         # we matched a protocol but the remainder was something bogus
         return None
diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py
index edfd10e2b..087ef7e96 100644
--- a/src/hub/settings/base.py
+++ b/src/hub/settings/base.py
@@ -115,6 +115,7 @@ env = environ.FileAwareEnv(
     CSP_FORM_ACTION=(list, ["'self'"]),
     CSP_BASE_URI=(list, ["'self'"]),
     CSP_INCLUDE_NONCE_IN=(list, ['script-src']),
+    ADDITIONAL_LINK_PROTOCOLS=(dict, {}),
 )
 
 
@@ -536,6 +537,12 @@ PRETIX_SECRET_KEY = env('PRETIX_SECRET')  # the JWT shared secret with Pretix
 METRICS_SERVER_IPS = env('METRICS_SERVER_IPS')
 
 
+# ----------------------------------
+# additional protocols supported by core.utils.resolve_internal_url() and thus, all markdown in the hub
+# ----------------------------------
+
+ADDITIONAL_LINK_PROTOCOLS = env.dict('ADDITIONAL_LINK_PROTOCOLS', cast={'value': str})
+
 # ----------------------------------
 # External Schedule Support
 # ----------------------------------
-- 
GitLab