diff --git a/src/pretix/control/templates/pretixcontrol/event/fragment_qr_dropdown.html b/src/pretix/control/templates/pretixcontrol/event/fragment_qr_dropdown.html
new file mode 100644
index 0000000000..b139d6dfd3
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/event/fragment_qr_dropdown.html
@@ -0,0 +1,27 @@
+{% load i18n %}
+
diff --git a/src/pretix/control/templates/pretixcontrol/event/index.html b/src/pretix/control/templates/pretixcontrol/event/index.html
index 88f5fd3748..d89bb08efa 100644
--- a/src/pretix/control/templates/pretixcontrol/event/index.html
+++ b/src/pretix/control/templates/pretixcontrol/event/index.html
@@ -27,28 +27,7 @@
-
+ {% include "pretixcontrol/event/fragment_qr_dropdown.html" with url=0 %}
diff --git a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
index 612f216b63..7e3542fab1 100644
--- a/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
+++ b/src/pretix/control/templates/pretixcontrol/vouchers/detail.html
@@ -42,10 +42,18 @@
{% endif %}
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index 35bf3c8116..b81ea68efb 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -40,7 +40,7 @@ from collections import OrderedDict
from decimal import Decimal
from io import BytesIO
from itertools import groupby
-from urllib.parse import urlsplit
+from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo
import bleach
@@ -50,6 +50,7 @@ from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.db import transaction
from django.db.models import ProtectedError
@@ -61,6 +62,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
+from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
from django.views.generic import FormView, ListView
@@ -1530,6 +1532,12 @@ class EventQRCode(EventPermissionRequiredMixin, View):
def get(self, request, *args, filetype, **kwargs):
url = build_absolute_uri(request.event, 'presale:event.index')
+ if "url" in request.GET:
+ if url_has_allowed_host_and_scheme(request.GET["url"], allowed_hosts=[urlparse(url).netloc]):
+ url = request.GET["url"]
+ else:
+ raise PermissionDenied("Untrusted URL")
+
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_M,
diff --git a/src/pretix/control/views/vouchers.py b/src/pretix/control/views/vouchers.py
index d354a7fc36..1574a55587 100644
--- a/src/pretix/control/views/vouchers.py
+++ b/src/pretix/control/views/vouchers.py
@@ -34,6 +34,7 @@
# License for the specific language governing permissions and limitations under the License.
import io
+from urllib.parse import urlencode
import bleach
from defusedcsv import csv
@@ -75,6 +76,7 @@ from pretix.control.views import PaginationMixin
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.format import format_map
from pretix.helpers.models import modelcopy
+from pretix.multidomain.urlreverse import build_absolute_uri
class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
@@ -315,6 +317,13 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
expires__gte=now()
).count()
ctx['redeemed_in_carts'] = redeemed_in_carts
+
+ url_params = {
+ 'voucher': self.object.code
+ }
+ if self.object.subevent_id:
+ url_params['subevent'] = self.object.subevent_id
+ ctx['url'] = build_absolute_uri(self.request.event, "presale:event.redeem") + "?" + urlencode(url_params)
return ctx