diff --git a/src/pretix/base/templatetags/rich_text.py b/src/pretix/base/templatetags/rich_text.py index 954e093ffd..2fabad485c 100644 --- a/src/pretix/base/templatetags/rich_text.py +++ b/src/pretix/base/templatetags/rich_text.py @@ -32,13 +32,14 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. +import html import re import urllib.parse import bleach import markdown -from bleach import DEFAULT_CALLBACKS -from bleach.linkifier import build_email_re, build_url_re +from bleach import DEFAULT_CALLBACKS, html5lib_shim +from bleach.linkifier import build_email_re from django import template from django.conf import settings from django.core import signing @@ -124,6 +125,23 @@ ALLOWED_ATTRIBUTES = { ALLOWED_PROTOCOLS = {'http', 'https', 'mailto', 'tel'} + +def build_url_re(tlds=tld_set, protocols=html5lib_shim.allowed_protocols): + # Differs from bleach regex by allowing { and } in URL to allow placeholders in URL parameters + return re.compile( + r"""\(* # Match any opening parentheses. + \b(?"]*)? + # /path/zz (excluding "unsafe" chars from RFC 3986, + # except for # and ~, which happen in practice) + """.format( + "|".join(sorted(protocols)), "|".join(sorted(tlds)) + ), + re.IGNORECASE | re.VERBOSE | re.UNICODE, + ) + + URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, reverse=True))) EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True))) @@ -333,8 +351,14 @@ def markdown_compile_email(source, allowed_tags=None, allowed_attributes=ALLOWED # This is a workaround to fix placeholders in URL targets def context_callback(attrs, new=False): if (None, "href") in attrs and "{" in attrs[None, "href"]: - # Do not use MODE_RICH_TO_HTML to avoid recursive linkification - attrs[None, "href"] = escape(format_map(attrs[None, "href"], context=context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)) + # Do not use MODE_RICH_TO_HTML to avoid recursive linkification. + # We want to esacpe the end result, however, we need to unescape the input to prevent & being turned + # to &amp; because the input is already escaped by the markdown parser. + attrs[None, "href"] = escape(format_map( + html.unescape(attrs[None, "href"]), + context=context, + mode=SafeFormatter.MODE_RICH_TO_PLAIN + )) return attrs context_callbacks.append(context_callback) diff --git a/src/tests/base/test_mail.py b/src/tests/base/test_mail.py index 1e15460a15..edd994815f 100644 --- a/src/tests/base/test_mail.py +++ b/src/tests/base/test_mail.py @@ -227,7 +227,9 @@ def test_placeholder_html_rendering_from_string(env): "Event website: [{event}](https://example.org/{event_slug})\n\n" "Other website: [{event}]({meta_Website})\n\n" "URL: {url}\n\n" - "URL with text: Test" + "URL with text: Test\n\n" + "URL with params: https://example.com/form?action=foo&eventid={event_slug}\n\n" + "URL with params and text: [Link & Text](https://example.com/form?action=foo&eventid={event_slug})\n\n" }) djmail.outbox = [] event, user, organizer = env @@ -249,6 +251,8 @@ def test_placeholder_html_rendering_from_string(env): assert '**Meta**: *Beep*' in djmail.outbox[0].body assert 'URL: https://google.com' in djmail.outbox[0].body assert 'URL with text: Test' in djmail.outbox[0].body + assert 'URL with params: https://example.com/form?action=foo&eventid=dummy' in djmail.outbox[0].body + assert 'URL with params and text: [Link & Text](https://example.com/form?action=foo&eventid=dummy)' in djmail.outbox[0].body assert '<' not in djmail.outbox[0].body assert '&' not in djmail.outbox[0].body html = _extract_html(djmail.outbox[0]) @@ -272,3 +276,13 @@ def test_placeholder_html_rendering_from_string(env): r'URL with text: Test', html ) + assert re.search( + r'URL with params: https://example.com/form\?action=foo&eventid=dummy', + html + ) + assert re.search( + r'URL with params and text: Link & Text', + html + )