diff --git a/src/pretix/helpers/payment.py b/src/pretix/helpers/payment.py index 8d19d56fd3..9950e85f2c 100644 --- a/src/pretix/helpers/payment.py +++ b/src/pretix/helpers/payment.py @@ -25,6 +25,19 @@ import text_unidecode from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ +EPC_QR_ALLOWED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789/-?:().,'+ " +) + + +def epc_qr_field(value): + return ''.join( + char for char in text_unidecode.unidecode(str(value or '')) + if char in EPC_QR_ALLOWED_CHARS + ) + def dotdecimal(value): return str(value).replace(",", ".") @@ -77,7 +90,7 @@ def euro_epc_qr( return { "id": "girocode", "label": "EPC-QR", - "qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [ + "qr_data": "\n".join(epc_qr_field(d) for d in [ "BCD", # Service Tag: ‘BCD’ "002", # Version: V2 "2", # Character set: ISO 8859-1 diff --git a/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html b/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html index 9c1500f082..f45394c23f 100644 --- a/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html +++ b/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html @@ -1,5 +1,6 @@ {% load i18n %} {% load static %} +{% load escapejson %} {% if payment_qr_codes %} @@ -41,4 +42,4 @@ {% endif %} {% endfor %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 9b48016b53..9ec38e311f 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -666,10 +666,12 @@ var form_handlers = function (el) { el.find("script[data-replace-with-qr]").each(function () { var $div = $("
"); + var qrText = this.getAttribute("type") === "application/json" && this.textContent.startsWith('"') ? + JSON.parse(this.textContent) : $(this).html(); $div.insertBefore($(this)); $div.qrcode( { - text: $(this).html(), + text: qrText, correctLevel: 0, // M width: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256, height: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256, diff --git a/src/pretix/static/pretixpresale/js/ui/main.js b/src/pretix/static/pretixpresale/js/ui/main.js index f4f66f70c4..f3eb4067fd 100644 --- a/src/pretix/static/pretixpresale/js/ui/main.js +++ b/src/pretix/static/pretixpresale/js/ui/main.js @@ -131,10 +131,12 @@ var form_handlers = function (el) { el.find("script[data-replace-with-qr]").each(function () { var $div = $("
"); + var qrText = this.getAttribute("type") === "application/json" && this.textContent.startsWith('"') ? + JSON.parse(this.textContent) : $(this).html(); $div.insertBefore($(this)); $div.qrcode( { - text: $(this).html(), + text: qrText, correctLevel: 0, // M width: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256, height: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256, @@ -345,7 +347,7 @@ function setup_basics(el) { }).on('click', function (event) { setCurrentTab(this); }); - + var firstTab = tabs.first().get(0); var lastTab = tabs.last().get(0); setCurrentTab(tabs.filter('[aria-selected=true]').get(0)); @@ -658,7 +660,7 @@ $(function () { var currentTimeDisplayParts = []; timeFormatParts.forEach(function(format) { currentTimeDisplayParts.push([format, $("").appendTo(currentTimeDisplay)]) - }); + }); var duration = this.getAttribute("data-duration").split(":").reduce(function(previousValue, currentValue, currentIndex) { return previousValue + (currentIndex ? parseInt(currentValue, 10) * 60 : parseInt(currentValue, 10) * 60 * 60); }, 0); @@ -671,7 +673,7 @@ $(function () { currentTimeBar.remove(); return; } - + var offset = thisCalendar.querySelector("h3").getBoundingClientRect().width; var dx = Math.round(offset + (thisCalendar.scrollWidth-offset)*(currentTimeDelta/duration)); currentTimeDisplayParts.forEach(function(part) { diff --git a/src/tests/helpers/test_payment.py b/src/tests/helpers/test_payment.py index 82ab754d69..ea99faa2d6 100644 --- a/src/tests/helpers/test_payment.py +++ b/src/tests/helpers/test_payment.py @@ -23,6 +23,8 @@ from datetime import timedelta from decimal import Decimal import pytest +from django.template.loader import render_to_string +from django.utils.safestring import SafeData from django.utils.timezone import now from pretix.base.models import Event, Organizer @@ -77,6 +79,31 @@ TESTVERANST-12345 '&bic=BYLADEM1MIL&amount=123%2C00&reason=TESTVERANST-12345¤cy=EUR') +@pytest.mark.django_db +def test_payment_qr_codes_euro_keeps_allowed_apostrophe_unescaped(env): + o, event = env + codes = generate_payment_qr_codes( + event=event, + code='TESTVERANST-12345', + amount=Decimal('123.00'), + bank_details_sepa_bic='BYLADEM1MIL', + bank_details_sepa_iban='DE37796500000069799047', + bank_details_sepa_name='Bits\'n"Bugs', + ) + + qr_data = codes[0]['qr_data'] + assert '\nBits\'nBugs\n' in qr_data + assert not isinstance(qr_data, SafeData) + + html = render_to_string( + 'pretixpresale/event/payment_qr_codes.html', + {'payment_qr_codes': codes}, + ) + assert 'type="application/json"' in html + assert "\\nBits'nBugs\\n" in html + assert ''' not in html + + @pytest.mark.django_db def test_payment_qr_codes_swiss(env): o, event = env