From d972cd4c49a1c5dc379b4bf3d4e5e81d208e0c39 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 13 Aug 2019 11:56:39 +0200 Subject: [PATCH] Add new bundled plugin "returnurl" --- src/MANIFEST.in | 2 + src/pretix/plugins/returnurl/__init__.py | 21 ++++++ src/pretix/plugins/returnurl/signals.py | 50 +++++++++++++ .../templates/returnurl/settings.html | 16 ++++ src/pretix/plugins/returnurl/urls.py | 8 ++ src/pretix/plugins/returnurl/views.py | 30 ++++++++ src/pretix/presale/views/order.py | 1 + src/pretix/settings.py | 1 + src/tests/plugins/test_returnurl.py | 74 +++++++++++++++++++ src/tests/presale/test_orders.py | 5 +- 10 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/pretix/plugins/returnurl/__init__.py create mode 100644 src/pretix/plugins/returnurl/signals.py create mode 100644 src/pretix/plugins/returnurl/templates/returnurl/settings.html create mode 100644 src/pretix/plugins/returnurl/urls.py create mode 100644 src/pretix/plugins/returnurl/views.py create mode 100644 src/tests/plugins/test_returnurl.py 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" %}

+
+ {% csrf_token %} + {% bootstrap_form_errors form %} + {% bootstrap_field form.returnurl_prefix layout="horizontal" %} +
+ +
+
+{% 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)