diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html index cc728cd105b16391f5a0ccaae8528e240c8e25c0..06c8b523f80afebface6a12584dbaae35f7131c9 100644 --- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html +++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html @@ -221,13 +221,55 @@ {% if conference.support_assembly_physical %} <div class="card mb-3"> <div class="card-header">Position</div> - <div class="card-body"> - {{ object.location_point }} - <a role="button" - class="btn btn-sm btn-{% if assembly.is_public %}primary{% else %}secondary{% endif %}" - href="{% url 'backoffice:assemblyteam-editposition' pk=assembly.id %}"> - <i class="bi bi-pencil"></i> - </a> + <div class="card-body d-flex flex-row"> + <div> + <label for="pos_public"> + <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "public" %}: + </label> + <br> + <label for="pos_point"> + <i class="bi bi-pin-map"></i> POI: + </label> + <br> + <label for="pos_boundary"> + <i class="bi bi-map"></i> Boundary: + </label> + </div> + <div class="flex-grow-1 ps-3"> + {% if object.is_placed %} + <span id="pos_public" class="text-success"><i class="bi bi-check-square"></i> {% trans "yes" %}</span> + {% else %} + <span id="pos_public" class="text-warning"><i class="bi bi-x-square"></i> {% trans "no" %}</span> + {% endif %} + <br> + {% if object.location_data.point %} + <span id="pos_point" + class="text-success" + title="{{ object.location_data.point }}"> + <i class="bi bi-check-square"></i> {% trans "yes" %} + </span> + {% else %} + <span id="pos_poi" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span> + {% endif %} + <br> + {% if object.location_data.boundaries %} + <span id="pos_boundary" + class="text-success" + title="{{ object.location_data.boundaries }}"> + <i class="bi bi-check-square"></i> {% trans "yes" %} + </span> + {% else %} + <span id="pos_boundary" class="text-danger"><i class="bi bi-x-square"></i> {% trans "no" %}</span> + {% endif %} + </div> + <div> + <a role="button" + class="btn btn-sm btn-{% if assembly.is_public %}primary{% else %}secondary{% endif %}" + href="{% url 'backoffice:assemblyteam-editposition' pk=assembly.id %}"> + <i class="bi bi-pencil"></i> + {% trans "edit" %} + </a> + </div> </div> </div> {% endif %} diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 132a7c3d75cee8685229d149a32de88b100ae0d7..5a321506f61299e167a4741fdac614935b3dcab1 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -1224,18 +1224,15 @@ msgstr "Video-Stream" msgid "RoomLink__type-audio" msgstr "Audio-Stream" +msgid "RoomLink__type-nav" +msgstr "Navigation" + msgid "RoomLink__type__help" msgstr "Was wird hier verlinkt?" msgid "RoomLink__type" msgstr "Typ" -msgid "RoomLink__conference_internal__help" -msgstr "Es handelt sich um einen Link der keine Vorschaltseite benötigt." - -msgid "RoomLink__conference_internal" -msgstr "interner Link" - msgid "Link__link__must_be_url" msgstr "Das Link-Ziel muss eine gültige URL sein." diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 8370a3dbb5d6f67610e75f78b319f9552a1609b9..9d671ff7feb9acda6418267b0a0a41ccb74c8373 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -1224,18 +1224,15 @@ msgstr "video stream" msgid "RoomLink__type-audio" msgstr "audio stream" +msgid "RoomLink__type-nav" +msgstr "navigation" + msgid "RoomLink__type__help" msgstr "What is being linked here?" msgid "RoomLink__type" msgstr "type" -msgid "RoomLink__conference_internal__help" -msgstr "This is an internal link which does not require a disclaimer page." - -msgid "RoomLink__conference_internal" -msgstr "internal link" - msgid "Link__link__must_be_url" msgstr "The link target must be a URL." diff --git a/src/core/markdown.py b/src/core/markdown.py index 06e3365b4e0f1f3949aed2a5aad7faad5eaba16d..ad3ca64719ad01fef3342754fdabca3b273d799e 100644 --- a/src/core/markdown.py +++ b/src/core/markdown.py @@ -1,6 +1,5 @@ import html import re -from urllib.parse import quote, urlparse import bleach import mistletoe @@ -8,7 +7,6 @@ from mistletoe.block_token import BlockToken from mistletoe.html_renderer import HTMLRenderer from mistletoe.span_token import AutoLink, Link, SpanToken, tokenize_inner -from django.conf import settings from django.db.models import Model from django.urls import NoReverseMatch, reverse from django.utils.safestring import mark_safe @@ -17,24 +15,13 @@ from modeltranslation.fields import build_localized_fieldname from modeltranslation.settings import AVAILABLE_LANGUAGES from .models import conference -from .utils import scheme_and_netloc_from_url, url_in_allowlist +from .utils import resolve_link def markdown_header_slugify(value: str, separator: str) -> str: return 'md-' + slugify(value) -def is_trusted_dst(url: str): - url = urlparse(url) - is_local_domain = url.netloc in settings.ALLOWED_HOSTS - is_external = (url.scheme or url.netloc) and not is_local_domain - return not is_external or url.scheme not in {'http', 'https', 'ftp', 'ftps'} - - -def redirect_via_dereferer(url: str): - return settings.PLAINUI_DEREFERER_URL.format(quoted_target=quote(url)) - - class PageLink(SpanToken): pattern = re.compile(r'\[\[ *([^|\]]+?) *(?:\| *(.*))? *\]\]') parse_group = 2 @@ -109,46 +96,17 @@ class MyHtmlRenderer(HTMLRenderer): result += '</div>\n' return result - def __init__(self, conf: 'conference.Conference', result: 'RenderResult', *extras, derefer_allowlist: bool = True, **kwargs): + def __init__(self, conf: 'conference.Conference', result: 'RenderResult', *extras, use_derefer_allowlist: bool = True, **kwargs): self.conf = conf self.result = result - self.derefer_allowlist = derefer_allowlist + self.use_derefer_allowlist = use_derefer_allowlist super().__init__(PageLink, ProfileLink, Tag, AlertBlock, *extras, **kwargs) - def derive_link_target(self, url): - """rewrite given URL unless it is trusted or in dereferrer-allowlist while those shall not be dereferred""" - - do_derefer = True - if not self.derefer_allowlist: - try: - scheme_and_netloc = scheme_and_netloc_from_url(url) - if url_in_allowlist(scheme_and_netloc, settings.DEREFERRER_GLOBAL_ALLOWLIST): - do_derefer = False - except ValueError: - # ignore URL parsing error - pass - - return redirect_via_dereferer(url) if do_derefer else url - - def handle_link(self, url: str) -> tuple[str, str]: - from .utils import resolve_internal_url - - # attempt resolving an internal URL - if resolved_internal_url := resolve_internal_url(url, fallback_as_is=False): - url = resolved_internal_url - - # derive external link (i.e. apply dereferer), if its not an internal or trusted location - if resolved_internal_url is None and not is_trusted_dst(url): - return 'external', self.derive_link_target(url) - - # otherwise, it's an internal link - return 'internal', url - def render_link(self, token: Link) -> str: if token.target.startswith(('javascript:', 'data:')): token.target = '' - link_type, url = self.handle_link(token.target) + link_type, url = resolve_link(token.target, self.use_derefer_allowlist) self.result.linked_urls.add(url) template = '<a href="{target}"{title} class="{link_type}">{inner}</a>' @@ -269,7 +227,7 @@ def render_markdown_ex( result = RenderResult() with renderer(conf, result) as renderer: - renderer.derefer_allowlist = not dont_derefer_allowlist + renderer.use_derefer_allowlist = dont_derefer_allowlist rendered_markup = renderer.render(mistletoe.Document(markup)) if sanitize_html: diff --git a/src/core/migrations/0167_roomlink_add_type_nav_remove_internal_link.py b/src/core/migrations/0167_roomlink_add_type_nav_remove_internal_link.py new file mode 100644 index 0000000000000000000000000000000000000000..92d338bd3fe5c7647907b32e0ad44675ab56c2eb --- /dev/null +++ b/src/core/migrations/0167_roomlink_add_type_nav_remove_internal_link.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.3 on 2024-12-21 01:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0166_alter_room_official_room_order'), + ] + + operations = [ + migrations.RemoveField( + model_name='link', + name='conference_internal', + ), + migrations.RemoveField( + model_name='roomlink', + name='conference_internal', + ), + migrations.AlterField( + model_name='link', + name='link_type', + field=models.CharField(choices=[('website', 'RoomLink__type-website'), ('chat', 'RoomLink__type-chat'), ('bbb', 'RoomLink__type-bbb'), ('jitsi', 'RoomLink__type-jitsi'), ('pad', 'RoomLink__type-pad'), ('media.ccc.de', 'RoomLink__type-media_ccc_de'), ('video', 'RoomLink__type-video'), ('audio', 'RoomLink__type-audio'), ('nav', 'RoomLink__type-nav')], help_text='RoomLink__type__help', max_length=20, verbose_name='RoomLink__type'), + ), + migrations.AlterField( + model_name='roomlink', + name='link_type', + field=models.CharField(choices=[('website', 'RoomLink__type-website'), ('chat', 'RoomLink__type-chat'), ('bbb', 'RoomLink__type-bbb'), ('jitsi', 'RoomLink__type-jitsi'), ('pad', 'RoomLink__type-pad'), ('media.ccc.de', 'RoomLink__type-media_ccc_de'), ('video', 'RoomLink__type-video'), ('audio', 'RoomLink__type-audio'), ('nav', 'RoomLink__type-nav')], help_text='RoomLink__type__help', max_length=20, verbose_name='RoomLink__type'), + ), + ] diff --git a/src/core/models/links.py b/src/core/models/links.py index 2b7ae3e5fbf87b7a6eb93f93fd579ecfae5b4878..86764e7e409e67adef65819949d2b20d5600766f 100644 --- a/src/core/models/links.py +++ b/src/core/models/links.py @@ -5,6 +5,8 @@ from django.core.validators import URLValidator from django.db import models from django.utils.translation import gettext_lazy as _ +from core.utils import resolve_link + class Link(models.Model): class Meta: @@ -19,8 +21,9 @@ class Link(models.Model): MEDIA_CCC_DE = 'media.ccc.de', _('RoomLink__type-media_ccc_de') VIDEO = 'video', _('RoomLink__type-video') AUDIO = 'audio', _('RoomLink__type-audio') + NAV = 'nav', _('RoomLink__type-nav') - URL_LINK_TYPES = [LinkType.WEBSITE, LinkType.CHAT, LinkType.BBB, LinkType.JITSI, LinkType.PAD, LinkType.VIDEO, LinkType.AUDIO] + URL_LINK_TYPES = [LinkType.WEBSITE, LinkType.CHAT, LinkType.BBB, LinkType.JITSI, LinkType.PAD, LinkType.VIDEO, LinkType.AUDIO, LinkType.NAV] """All LinkTypes which require the link to be a valid URL. The notable exception is media_ccc_de where only a global id is required.""" content_types = models.Q(app_label='core', model='project') @@ -32,9 +35,24 @@ class Link(models.Model): link_type = models.CharField(max_length=20, choices=LinkType.choices, help_text=_('RoomLink__type__help'), verbose_name=_('RoomLink__type')) link = models.CharField(max_length=255) - conference_internal = models.BooleanField( - default=False, help_text=_('RoomLink__conference_internal__help'), verbose_name=_('RoomLink__conference_internal') - ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._resolved_url = None + + def _cache_resolved_url_if_necessary(self): + if self._resolved_url: + return + self._resolved_url = resolve_link(self.link) + + @property + def resolved_url(self) -> str: + self._cache_resolved_url_if_necessary() + return self._resolved_url[1] + + @property + def resolved_url_internal(self) -> bool: + self._cache_resolved_url_if_necessary() + return self._resolved_url[0] == 'internal' def clean(self): if self.link_type in Link.URL_LINK_TYPES: @@ -48,7 +66,6 @@ class Link(models.Model): return self.name def __repr__(self) -> str: - repr_str = f'<{self.__class__.__name__}: {self.name} ({self.link_type}' - repr_str += ':internal)' if self.conference_internal else ')' + repr_str = f'<{self.__class__.__name__}: {self.name} ({self.link_type})' repr_str += f': {self.link}>' if self.link != self.name else '>' return repr_str diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py index 21e0c1ee2991d1e131403f2a6a1f902e6f1580d7..b54819c259eb740fe27d5810a9ede9ef16a73cac 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, resolve_link, str2bool from core.validators import FileSizeValidator, ImageDimensionValidator @@ -465,10 +464,11 @@ class RoomLink(models.Model): MEDIA_CCC_DE = 'media.ccc.de', _('RoomLink__type-media_ccc_de') VIDEO = 'video', _('RoomLink__type-video') AUDIO = 'audio', _('RoomLink__type-audio') + NAV = 'nav', _('RoomLink__type-nav') objects = RoomLinkManager() - URL_LINKTYPES = [LinkType.WEBSITE, LinkType.CHAT, LinkType.BBB, LinkType.JITSI, LinkType.PAD, LinkType.VIDEO, LinkType.AUDIO] + URL_LINKTYPES = [LinkType.WEBSITE, LinkType.CHAT, LinkType.BBB, LinkType.JITSI, LinkType.PAD, LinkType.VIDEO, LinkType.AUDIO, LinkType.NAV] """All LinkTypes which require the link to be a valid URL. The notable exception is media_ccc_de where only a global id is required.""" room = models.ForeignKey(Room, related_name='links', on_delete=models.CASCADE) @@ -477,16 +477,28 @@ class RoomLink(models.Model): link_type = models.CharField(max_length=20, choices=LinkType.choices, help_text=_('RoomLink__type__help'), verbose_name=_('RoomLink__type')) link = models.CharField(max_length=255) - conference_internal = models.BooleanField( - default=False, help_text=_('RoomLink__conference_internal__help'), verbose_name=_('RoomLink__conference_internal') - ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._resolved_url = None + + def _cache_resolved_url_if_necessary(self): + if self._resolved_url: + return + self._resolved_url = resolve_link(self.link) + + @property + def resolved_url(self) -> str: + self._cache_resolved_url_if_necessary() + return self._resolved_url[1] + + @property + def resolved_url_internal(self) -> bool: + self._cache_resolved_url_if_necessary() + return self._resolved_url[0] == 'internal' 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/markdown.py b/src/core/tests/markdown.py index d30892ab8ce3ecd58025077a29198c312c93ca9f..0ab5b990e0ca4172ed7c90e3eb65d1fd24645e7d 100644 --- a/src/core/tests/markdown.py +++ b/src/core/tests/markdown.py @@ -19,6 +19,7 @@ class MarkdownTest(TestCase): conf = Conference(name='foo', id=TEST_CONF_ID) conf.save() + # TODO: consider moving this test into tests/utils.py as this is basically testing resolve_link() only (except the footnote/anchored links part) tests = [ ('https://localhost/', False), ('https://localhost/foo', False), diff --git a/src/core/tests/utils.py b/src/core/tests/utils.py index b39677a3372b26cdc7bc28b18ced6fabc03143d1..489713310d72750ffc6b17b852ba942608059780 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,30 @@ 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 = [ + ('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 3ec4c560b60f602ff686d5680be68db9a6368dcd..a96d9e4bd5cc717fa8671048608725984ee0587d 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -8,10 +8,11 @@ import uuid from datetime import UTC, datetime, timedelta from pathlib import Path from string import ascii_letters, digits -from urllib.parse import parse_qs, urlparse, urlunparse +from urllib.parse import parse_qs, quote, 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 @@ -155,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 @@ -163,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) @@ -174,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 @@ -189,6 +194,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 @@ -197,6 +213,49 @@ def resolve_internal_url(url: str, fallback_as_is: bool = True) -> str | None: return url if fallback_as_is else None +def get_dereferred_url(url: str, use_derefer_allowlist: bool = True): + """rewrite given URL unless it is trusted or in dereferrer-allowlist while those shall not be dereferred""" + + do_derefer = True + if use_derefer_allowlist: + try: + scheme_and_netloc = scheme_and_netloc_from_url(url) + if url_in_allowlist(scheme_and_netloc, settings.DEREFERRER_GLOBAL_ALLOWLIST): + do_derefer = False + except ValueError: + # ignore URL parsing error + pass + + return settings.PLAINUI_DEREFERER_URL.format(quoted_target=quote(url)) if do_derefer else url + + +def is_trusted_link_destination(url: str): + url = urlparse(url) + is_local_domain = url.netloc in settings.ALLOWED_HOSTS + is_external = (url.scheme or url.netloc) and not is_local_domain + return not is_external or url.scheme not in {'http', 'https', 'ftp', 'ftps'} + + +def resolve_link(url: str, use_derefer_allowlist: bool = True) -> tuple[str, str]: + """ + Resolves a given URL, classifies it as internal or external and optionally rewrites it to use the dereferrer. + :param url: the original URL to resolve + :param use_derefer_allowlist: controls if the global allowlist (see settings.DEREFERRER_GLOBAL_ALLOWLIST) shall be used + :return: tuple with two values, the first being either 'internal' or 'external', the second being the resolved link (might be to the dereferrer) + """ + + # attempt resolving an internal URL + 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 + if resolved_internal_url is None and not is_trusted_link_destination(url): + return 'external', get_dereferred_url(url, use_derefer_allowlist=use_derefer_allowlist) + + # otherwise, it's an internal link + return 'internal', url + + def download_from_url(url: str) -> tuple[str, bytes]: # let requests library fetch the URL r = requests.get(url, timeout=30) diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py index edfd10e2b2705e9be4fe4d3ebf12fe7d87044826..087ef7e96c24c12b9076da56f62d5c4532c78f00 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 # ---------------------------------- diff --git a/src/plainui/jinja2.py b/src/plainui/jinja2.py index 7365c3a327e7084da0485e6ecd2c0b994b94e0ca..1094fa4df85caea4f2565d4446a33207e934cfca 100644 --- a/src/plainui/jinja2.py +++ b/src/plainui/jinja2.py @@ -159,6 +159,7 @@ link_icon_map = { 'media.ccc.de': 'play-circle-fill', 'video': 'camera-reels-fill', 'audio': 'volume-up-fill', + 'nav': 'map', } diff --git a/src/plainui/jinja2/plainui/assembly.html.j2 b/src/plainui/jinja2/plainui/assembly.html.j2 index 81942bd6ef808f2adff41b266bf42cdb7573dd1d..c8b116d105ea99cbbce14bc41139b2cfb81795bc 100644 --- a/src/plainui/jinja2/plainui/assembly.html.j2 +++ b/src/plainui/jinja2/plainui/assembly.html.j2 @@ -47,14 +47,13 @@ {% endif %} {% if assembly.assembly_link %} - {% set assembly_url = '/' + assembly.assembly_link if assembly.assembly_link.conference_internal else url('plainui:dereferrer', assembly.assembly_link) %} <div class="hub-tags"> {{ tagboxMacro.tag(_("Website") , icon="globe", - link=assembly_url, + link=assembly.assembly_link.resolved_url, style="secondary", - target=('_self' if assembly.assembly_link.conference_internal else '_blank'), - rel=('' if assembly.assembly_link.conference_internal else 'external') + target=('_self' if assembly.assembly_link.resolved_url_internal else '_blank'), + rel=('' if assembly.assembly_link.resolved_url_internal else 'external') ) }} </div> {% endif %} diff --git a/src/plainui/jinja2/plainui/projects/detail.html.j2 b/src/plainui/jinja2/plainui/projects/detail.html.j2 index 461b7a2e6c38f7b280f33cd14b3db3367d2cad27..d12fc39a5aac8b03bd33d2fe07cde3cd2d8c6835 100644 --- a/src/plainui/jinja2/plainui/projects/detail.html.j2 +++ b/src/plainui/jinja2/plainui/projects/detail.html.j2 @@ -68,13 +68,12 @@ {% if project.links %} <div class="hub-tags" role="list"> {% for link in project.links.all() %} - {% set url = '/' + link.link if link.conference_internal else url('plainui:dereferrer', dst=link.link) %} {{ tagMacros.tag(link.name, - link=url, + link=link.resolved_url, icon=link_icon(link) , style='secondary', - target=('_self' if link.conference_internal else '_blank'), - rel=('' if link.conference_internal else 'external, noreferrer') + target=('_self' if link.resolved_url_internal else '_blank'), + rel=('' if link.resolved_url_internal else 'external, noreferrer') ) }} {% endfor %} </div> diff --git a/src/plainui/jinja2/plainui/room.html.j2 b/src/plainui/jinja2/plainui/room.html.j2 index d37cae1bb1de2067cc59e4d85aeffbbc59f145cd..de12e4b29d711a3c6c4fdcce37a41d166b3fc63a 100644 --- a/src/plainui/jinja2/plainui/room.html.j2 +++ b/src/plainui/jinja2/plainui/room.html.j2 @@ -41,13 +41,12 @@ {% if links %} <div class="hub-tags" role="list"> {% for link in links %} - {% set url = '/' + link.link if link.conference_internal else url('plainui:dereferrer', dst=link.link) %} {{ tagMacros.tag(link.name, - link=url, + link=link.resolved_url, icon=link_icon(link) , style='secondary', - target=('_self' if link.conference_internal else '_blank'), - rel=('' if link.conference_internal else 'external, noreferrer') + target=('_self' if link.resolved_url_internal else '_blank'), + rel=('' if link.resolved_url_internal else 'external, noreferrer') ) }} {% endfor %} </div> diff --git a/src/plainui/views/rooms.py b/src/plainui/views/rooms.py index ae1b7ce7155e6b6e01183103d25fb0d183486233..0de04f39d117cdaa539c16bb96e97a33741968eb 100644 --- a/src/plainui/views/rooms.py +++ b/src/plainui/views/rooms.py @@ -56,7 +56,7 @@ class RoomView(ConferenceRequiredMixin, DetailView): for link in self.room.links.all(): if link.link_type == RoomLink.LinkType.MEDIA_CCC_DE: media_link = link.link - elif link.link_type in {RoomLink.LinkType.WEBSITE, RoomLink.LinkType.CHAT, RoomLink.LinkType.JITSI, RoomLink.LinkType.PAD}: + elif link.link_type in [RoomLink.LinkType.WEBSITE, RoomLink.LinkType.CHAT, RoomLink.LinkType.JITSI, RoomLink.LinkType.PAD, RoomLink.LinkType.NAV]: linking_links.append(link) context['voc_stream'] = media_link