diff --git a/src/MANIFEST.in b/src/MANIFEST.in
index db6262f81f..a789f22ac2 100644
--- a/src/MANIFEST.in
+++ b/src/MANIFEST.in
@@ -22,3 +22,5 @@ recursive-include pretix/plugins/ticketoutputpdf/templates *
recursive-include pretix/plugins/ticketoutputpdf/static *
recursive-include pretix/plugins/badges/templates *
recursive-include pretix/plugins/badges/static *
+recursive-include pretix/plugins/returnurl/templates *
+recursive-include pretix/plugins/returnurl/static *
diff --git a/src/pretix/plugins/returnurl/__init__.py b/src/pretix/plugins/returnurl/__init__.py
new file mode 100644
index 0000000000..ecaac9c956
--- /dev/null
+++ b/src/pretix/plugins/returnurl/__init__.py
@@ -0,0 +1,21 @@
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+from pretix import __version__ as version
+
+
+class ReturnURLApp(AppConfig):
+ name = 'pretix.plugins.returnurl'
+ verbose_name = _("Redirection from order page")
+
+ class PretixPluginMeta:
+ name = _("Redirection from order page")
+ author = _("the pretix team")
+ version = version
+ description = _("This plugin allows to link to payments and redirect back afterwards.")
+
+ def ready(self):
+ from . import signals # NOQA
+
+
+default_app_config = 'pretix.plugins.returnurl.ReturnURLApp'
diff --git a/src/pretix/plugins/returnurl/signals.py b/src/pretix/plugins/returnurl/signals.py
new file mode 100644
index 0000000000..9108ebabf4
--- /dev/null
+++ b/src/pretix/plugins/returnurl/signals.py
@@ -0,0 +1,50 @@
+from django.core.exceptions import PermissionDenied
+from django.dispatch import receiver
+from django.shortcuts import redirect
+from django.urls import resolve, reverse
+from django.utils.translation import ugettext_lazy as _
+
+from pretix.control.signals import nav_event_settings
+from pretix.presale.signals import process_request
+
+
+@receiver(process_request, dispatch_uid="returnurl_process_request")
+def returnurl_process_request(sender, request, **kwargs):
+ try:
+ r = resolve(request.path_info)
+ except:
+ return
+
+ urlname = r.url_name
+ urlkwargs = r.kwargs
+
+ if urlname.startswith('event.order'):
+ key = 'order_{}_{}_{}_return_url'.format(urlkwargs.get('organizer', '-'), urlkwargs['event'],
+ urlkwargs['order'])
+ if urlname == 'event.order' and key in request.session:
+ r = redirect(request.session.get(key))
+ del request.session[key]
+ return r
+ elif urlname != 'event.order' and 'return_url' in request.GET:
+ u = request.GET.get('return_url')
+ if not sender.settings.returnurl_prefix:
+ raise PermissionDenied('No return URL prefix set.')
+ elif not u.startswith(sender.settings.returnurl_prefix):
+ raise PermissionDenied('Invalid return URL.')
+ request.session[key] = u
+
+
+@receiver(nav_event_settings, dispatch_uid='returnurl_nav')
+def navbar_info(sender, request, **kwargs):
+ url = resolve(request.path_info)
+ if not request.user.has_event_permission(request.organizer, request.event, 'can_change_event_settings',
+ request=request):
+ return []
+ return [{
+ 'label': _('Redirection'),
+ 'url': reverse('plugins:returnurl:settings', kwargs={
+ 'event': request.event.slug,
+ 'organizer': request.organizer.slug,
+ }),
+ 'active': url.namespace == 'plugins:returnurl',
+ }]
diff --git a/src/pretix/plugins/returnurl/templates/returnurl/settings.html b/src/pretix/plugins/returnurl/templates/returnurl/settings.html
new file mode 100644
index 0000000000..19157262c7
--- /dev/null
+++ b/src/pretix/plugins/returnurl/templates/returnurl/settings.html
@@ -0,0 +1,16 @@
+{% extends "pretixcontrol/event/settings_base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block inside %}
+
{% trans "Redirection from order page" %}
+
+{% endblock %}
diff --git a/src/pretix/plugins/returnurl/urls.py b/src/pretix/plugins/returnurl/urls.py
new file mode 100644
index 0000000000..173498db72
--- /dev/null
+++ b/src/pretix/plugins/returnurl/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls import url
+
+from .views import ReturnSettings
+
+urlpatterns = [
+ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/returnurl/settings$',
+ ReturnSettings.as_view(), name='settings'),
+]
diff --git a/src/pretix/plugins/returnurl/views.py b/src/pretix/plugins/returnurl/views.py
new file mode 100644
index 0000000000..1079d086b7
--- /dev/null
+++ b/src/pretix/plugins/returnurl/views.py
@@ -0,0 +1,30 @@
+from django import forms
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from pretix.base.forms import SettingsForm
+from pretix.base.models import Event
+from pretix.control.views.event import (
+ EventSettingsFormView, EventSettingsViewMixin,
+)
+
+
+class ReturnSettingsForm(SettingsForm):
+ returnurl_prefix = forms.URLField(
+ label=_("Base redirection URL"),
+ help_text=_("Redirection will only be allowed to URLs that start with this prefix."),
+ required=False,
+ )
+
+
+class ReturnSettings(EventSettingsViewMixin, EventSettingsFormView):
+ model = Event
+ form_class = ReturnSettingsForm
+ template_name = 'returnurl/settings.html'
+ permission = 'can_change_settings'
+
+ def get_success_url(self) -> str:
+ return reverse('plugins:returnurl:settings', kwargs={
+ 'organizer': self.request.event.organizer.slug,
+ 'event': self.request.event.slug
+ })
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index f94ca10849..ead3d74ac8 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -46,6 +46,7 @@ from pretix.presale.views.robots import NoSearchIndexViewMixin
class OrderDetailMixin(NoSearchIndexViewMixin):
+
@cached_property
def order(self):
order = self.request.event.orders.filter(code=self.kwargs['order']).select_related('event').first()
diff --git a/src/pretix/settings.py b/src/pretix/settings.py
index aad80a825c..c69b98b335 100644
--- a/src/pretix/settings.py
+++ b/src/pretix/settings.py
@@ -279,6 +279,7 @@ INSTALLED_APPS = [
'pretix.plugins.pretixdroid',
'pretix.plugins.badges',
'pretix.plugins.manualpayment',
+ 'pretix.plugins.returnurl',
'django_markup',
'django_otp',
'django_otp.plugins.otp_totp',
diff --git a/src/tests/plugins/test_returnurl.py b/src/tests/plugins/test_returnurl.py
new file mode 100644
index 0000000000..10b792fd05
--- /dev/null
+++ b/src/tests/plugins/test_returnurl.py
@@ -0,0 +1,74 @@
+from decimal import Decimal
+
+from django_scopes import scopes_disabled
+
+from pretix.base.models import OrderPayment
+
+from ..presale.test_orders import BaseOrdersTest
+
+
+class ReturnURLTest(BaseOrdersTest):
+ @scopes_disabled()
+ def setUp(self):
+ super().setUp()
+ self.event.enable_plugin("pretix.plugins.returnurl")
+ self.event.save()
+ self.event.settings.returnurl_prefix = 'https://example.com'
+ self.event.settings.set('payment_banktransfer__enabled', True)
+ self.event.settings.set('payment_testdummy__enabled', True)
+ self.order.payments.create(
+ provider='manual',
+ state=OrderPayment.PAYMENT_STATE_CONFIRMED,
+ amount=Decimal('10.00'),
+ )
+
+ def test_redirect_once(self):
+ r = self.client.get(
+ '/%s/%s/order/%s/%s/pay/change?return_url=https://example.com/foo/var/' % (
+ self.orga.slug, self.event.slug, self.order.code, self.order.secret
+ )
+ )
+ assert r.status_code == 200
+ r = self.client.post(
+ '/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
+ {
+ 'payment': 'banktransfer'
+ },
+ follow=False
+ )
+ assert r['Location'].endswith('/confirm')
+ r = self.client.post(
+ r['Location'],
+ follow=False
+ )
+ assert r['Location'] == '/%s/%s/order/%s/%s/' % (
+ self.orga.slug, self.event.slug, self.order.code, self.order.secret
+ )
+ r = self.client.get(
+ r['Location'],
+ follow=False
+ )
+ assert r['Location'] == 'https://example.com/foo/var/'
+ r = self.client.get(
+ '/%s/%s/order/%s/%s/' % (
+ self.orga.slug, self.event.slug, self.order.code, self.order.secret
+ )
+ )
+ assert r.status_code == 200
+
+ def test_redirect_enforce_prefix_match(self):
+ r = self.client.get(
+ '/%s/%s/order/%s/%s/pay/change?return_url=https://example.org/foo/var/' % (
+ self.orga.slug, self.event.slug, self.order.code, self.order.secret
+ )
+ )
+ assert r.status_code == 403
+
+ def test_redirect_enforce_prefix_set(self):
+ del self.event.settings.returnurl_prefix
+ r = self.client.get(
+ '/%s/%s/order/%s/%s/pay/change?return_url=https://example.org/foo/var/' % (
+ self.orga.slug, self.event.slug, self.order.code, self.order.secret
+ )
+ )
+ assert r.status_code == 403
diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py
index 90e01ca6d9..0f98674b5b 100644
--- a/src/tests/presale/test_orders.py
+++ b/src/tests/presale/test_orders.py
@@ -17,7 +17,8 @@ from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
-class OrdersTest(TestCase):
+class BaseOrdersTest(TestCase):
+
@scopes_disabled()
def setUp(self):
super().setUp()
@@ -82,6 +83,8 @@ class OrdersTest(TestCase):
total=Decimal("23")
)
+
+class OrdersTest(BaseOrdersTest):
def test_unknown_order(self):
response = self.client.get(
'/%s/%s/order/ABCDE/123/' % (self.orga.slug, self.event.slug)