diff --git a/src/pretix/base/templates/pretixbase/framebreak.html b/src/pretix/base/templates/pretixbase/framebreak.html new file mode 100644 index 0000000000..d98061a5ce --- /dev/null +++ b/src/pretix/base/templates/pretixbase/framebreak.html @@ -0,0 +1,28 @@ +{% extends "error.html" %} +{% load i18n %} +{% load eventurl %} +{% load urlreplace %} +{% load static %} + +{% block content %} +

{% trans "Please continue in a new tab" %}

+

+ {% blocktrans trimmed %} + For security reasons, the following step is only possible in a new tab. + {% endblocktrans %} +

+

+ {% blocktrans trimmed %} + If the new tab did not open automatically, please click the following button: + {% endblocktrans %} +

+
+ + + {% trans "Continue in new tab" %} + + {{ url|json_script:"framebreak-url" }} + +
+{% endblock %} diff --git a/src/pretix/base/views/redirect.py b/src/pretix/base/views/redirect.py index 5a282fc3a6..5ecfabee17 100644 --- a/src/pretix/base/views/redirect.py +++ b/src/pretix/base/views/redirect.py @@ -45,8 +45,10 @@ def _is_samesite_referer(request): def redir_view(request): + framebreak = "framebreak" in request.GET + salt = 'framebreak-safelink-url' if framebreak else 'safelink-url' try: - url = signing.Signer(salt='safelink-url').unsign(request.GET.get('url', '')) + url = signing.Signer(salt=salt).unsign(request.GET.get('url', '')) except signing.BadSignature: try: # Backwards-compatibility for a change in 2026-06, remove after a while @@ -61,15 +63,26 @@ def redir_view(request): 'url': url, }) + if framebreak: + r = render(request, 'pretixbase/framebreak.html', { + 'url': url, + }) + r.xframe_options_exempt = True + return r + r = HttpResponseRedirect(url) r['X-Robots-Tag'] = 'noindex' return r -def safelink(url): +def safelink(url, framebreak=False): url = str(url) if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")): logger.warning('Invalid URL passed to safelink: %r', url) return '#invalid-url' - signer = signing.Signer(salt='safelink-url') - return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) + salt = 'framebreak-safelink-url' if framebreak else 'safelink-url' + signer = signing.Signer(salt=salt) + u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) + if framebreak: + u += "&framebreak=true" + return u diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py index 9acc5ae4b2..977535cf17 100644 --- a/src/pretix/plugins/paypal/payment.py +++ b/src/pretix/plugins/paypal/payment.py @@ -34,7 +34,6 @@ import json import logging -import urllib.parse from collections import OrderedDict from decimal import Decimal @@ -42,7 +41,6 @@ import paypalrestsdk import paypalrestsdk.exceptions from django import forms from django.contrib import messages -from django.core import signing from django.http import HttpRequest from django.template.loader import get_template from django.urls import reverse @@ -58,6 +56,7 @@ from pretix.base.forms import SecretKeySettingsField from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.settings import SettingsSandbox +from pretix.base.views.redirect import safelink from pretix.multidomain.urlreverse import build_absolute_uri from pretix.plugins.paypal.api import Api from pretix.plugins.paypal.models import ReferencedPayPalObject @@ -349,11 +348,7 @@ class Paypal(BasePaymentProvider): for link in payment.links: if link.method == "REDIRECT" and link.rel == "approval_url": if request.session.get('iframe_session', False): - signer = signing.Signer(salt='safe-redirect') - return ( - build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' + - urllib.parse.quote(signer.sign(link.href)) - ) + return safelink(link.href, framebreak=True) else: return str(link.href) else: diff --git a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/redirect.html b/src/pretix/plugins/paypal/templates/pretixplugins/paypal/redirect.html deleted file mode 100644 index ca0f472bdc..0000000000 --- a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/redirect.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load compress %} -{% load i18n %} -{% load static %} - - - - {{ settings.PRETIX_INSTANCE_NAME }} - {% compress css %} - - {% endcompress %} - {% compress js %} - - {% endcompress %} - - -
-

{% trans "The payment process has started in a new window." %}

- -

- {% trans "The window to enter your payment data was not opened or was closed?" %} -

-

- - - {% trans "Click here in order to open the window." %} - -

- -
- - diff --git a/src/pretix/plugins/paypal/urls.py b/src/pretix/plugins/paypal/urls.py index c13ca1236b..8b69853e10 100644 --- a/src/pretix/plugins/paypal/urls.py +++ b/src/pretix/plugins/paypal/urls.py @@ -21,13 +21,12 @@ # from django.urls import include, re_path -from .views import abort, oauth_disconnect, redirect_view, success +from .views import abort, oauth_disconnect, success event_patterns = [ re_path(r'^paypal/', include([ re_path(r'^abort/$', abort, name='abort'), re_path(r'^return/$', success, name='return'), - re_path(r'^redirect/$', redirect_view, name='redirect'), re_path(r'w/(?P[a-zA-Z0-9]{16})/abort/', abort, name='abort'), re_path(r'w/(?P[a-zA-Z0-9]{16})/return/', success, name='return'), diff --git a/src/pretix/plugins/paypal/views.py b/src/pretix/plugins/paypal/views.py index 1436bdd5f9..3e4049d8bd 100644 --- a/src/pretix/plugins/paypal/views.py +++ b/src/pretix/plugins/paypal/views.py @@ -39,13 +39,10 @@ from decimal import Decimal import paypalrestsdk import paypalrestsdk.exceptions from django.contrib import messages -from django.core import signing from django.db.models import Sum -from django.http import HttpResponse, HttpResponseBadRequest -from django.shortcuts import render +from django.http import HttpResponse from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django_scopes import scopes_disabled @@ -61,21 +58,6 @@ from pretix.plugins.paypal.payment import Paypal logger = logging.getLogger('pretix.plugins.paypal') -@xframe_options_exempt -def redirect_view(request, *args, **kwargs): - signer = signing.Signer(salt='safe-redirect') - try: - url = signer.unsign(request.GET.get('url', '')) - except signing.BadSignature: - return HttpResponseBadRequest('Invalid parameter') - - r = render(request, 'pretixplugins/paypal/redirect.html', { - 'url': url, - }) - r._csp_ignore = True - return r - - def success(request, *args, **kwargs): pid = request.GET.get('paymentId') token = request.GET.get('token') diff --git a/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/redirect.html b/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/redirect.html deleted file mode 100644 index ca0f472bdc..0000000000 --- a/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/redirect.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load compress %} -{% load i18n %} -{% load static %} - - - - {{ settings.PRETIX_INSTANCE_NAME }} - {% compress css %} - - {% endcompress %} - {% compress js %} - - {% endcompress %} - - -
-

{% trans "The payment process has started in a new window." %}

- -

- {% trans "The window to enter your payment data was not opened or was closed?" %} -

-

- - - {% trans "Click here in order to open the window." %} - -

- -
- - diff --git a/src/pretix/plugins/paypal2/urls.py b/src/pretix/plugins/paypal2/urls.py index ed92dd4733..b8fb2b75b6 100644 --- a/src/pretix/plugins/paypal2/urls.py +++ b/src/pretix/plugins/paypal2/urls.py @@ -22,15 +22,13 @@ from django.urls import include, re_path from .views import ( - PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view, - success, webhook, + PayView, XHRView, abort, isu_disconnect, isu_return, success, webhook, ) event_patterns = [ re_path(r'^paypal2/', include([ re_path(r'^abort/$', abort, name='abort'), re_path(r'^return/$', success, name='return'), - re_path(r'^redirect/$', redirect_view, name='redirect'), re_path(r'^xhr/$', XHRView.as_view(), name='xhr'), re_path(r'^pay/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/$', PayView.as_view(), name='pay'), re_path(r'^(?P[^/][^w]+)/(?P[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'), diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py index b1dac86b65..49c75208f1 100644 --- a/src/pretix/plugins/paypal2/views.py +++ b/src/pretix/plugins/paypal2/views.py @@ -36,13 +36,10 @@ import logging from decimal import Decimal from django.contrib import messages -from django.core import signing from django.core.cache import cache from django.db import transaction from django.db.models import Sum -from django.http import ( - Http404, HttpResponse, HttpResponseBadRequest, JsonResponse, -) +from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator @@ -104,21 +101,6 @@ class PaypalOrderView: }) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else '')) -@xframe_options_exempt -def redirect_view(request, *args, **kwargs): - signer = signing.Signer(salt='safe-redirect') - try: - url = signer.unsign(request.GET.get('url', '')) - except signing.BadSignature: - return HttpResponseBadRequest('Invalid parameter') - - r = render(request, 'pretixplugins/paypal2/redirect.html', { - 'url': url, - }) - r._csp_ignore = True - return r - - @method_decorator(csrf_exempt, name='dispatch') @method_decorator(xframe_options_exempt, 'dispatch') class XHRView(View): diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 9bf3073624..6159b9e5d7 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -46,7 +46,6 @@ import stripe from django import forms from django.conf import settings from django.contrib import messages -from django.core import signing from django.db import transaction from django.http import HttpRequest from django.template.loader import get_template @@ -72,6 +71,7 @@ from pretix.base.payment import ( ) from pretix.base.plugins import get_all_plugins from pretix.base.settings import SettingsSandbox +from pretix.base.views.redirect import safelink from pretix.helpers import OF_SELF from pretix.helpers.countries import CachedCountries from pretix.helpers.http import get_client_ip @@ -745,15 +745,7 @@ class StripeMethod(BasePaymentProvider): def redirect(self, request, url): if request.session.get('iframe_session', False): - return ( - build_absolute_uri(request.event, 'plugins:stripe:redirect') + - '?data=' + signing.dumps({ - 'url': url, - 'session': { - 'payment_stripe_order_secret': request.session['payment_stripe_order_secret'], - }, - }, salt='safe-redirect') - ) + return safelink(url, framebreak=True) else: return str(url) @@ -1053,11 +1045,7 @@ class StripeMethod(BasePaymentProvider): 'hash': payment.order.tagged_secret('plugins:stripe'), }) if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False): - return build_absolute_uri(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({ - 'url': url, - 'session': {}, - }, salt='safe-redirect') - + return safelink(url, framebreak=True) return url def _confirm_payment_intent(self, request, payment): diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/redirect.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/redirect.html deleted file mode 100644 index ca0f472bdc..0000000000 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/redirect.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load compress %} -{% load i18n %} -{% load static %} - - - - {{ settings.PRETIX_INSTANCE_NAME }} - {% compress css %} - - {% endcompress %} - {% compress js %} - - {% endcompress %} - - -
-

{% trans "The payment process has started in a new window." %}

- -

- {% trans "The window to enter your payment data was not opened or was closed?" %} -

-

- - - {% trans "Click here in order to open the window." %} - -

- -
- - diff --git a/src/pretix/plugins/stripe/urls.py b/src/pretix/plugins/stripe/urls.py index 51106622ed..d03fafa101 100644 --- a/src/pretix/plugins/stripe/urls.py +++ b/src/pretix/plugins/stripe/urls.py @@ -25,13 +25,12 @@ from pretix.multidomain import event_url from .views import ( OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView, - oauth_disconnect, oauth_return, redirect_view, webhook, + oauth_disconnect, oauth_return, webhook, ) event_patterns = [ re_path(r'^stripe/', include([ event_url(r'^webhook/$', webhook, name='webhook', require_live=False), - re_path(r'^redirect/$', redirect_view, name='redirect'), re_path(r'^return/(?P[^/]+)/(?P[^/]+)/(?P[0-9]+)/$', ReturnView.as_view(), name='return'), re_path(r'^sca/(?P[^/]+)/(?P[^/]+)/(?P[0-9]+)/$', ScaView.as_view(), name='sca'), re_path(r'^sca/(?P[^/]+)/(?P[^/]+)/(?P[0-9]+)/return/$', diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index a9590f49ab..c8adfdf374 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -34,13 +34,11 @@ import json import logging -import urllib.parse import requests from django.contrib import messages -from django.core import signing from django.db import transaction -from django.http import Http404, HttpResponse, HttpResponseBadRequest +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator @@ -64,7 +62,7 @@ from pretix.control.views.event import DecoupleMixin from pretix.control.views.organizer import OrganizerDetailViewMixin from pretix.helpers import OF_SELF from pretix.helpers.http import redirect_to_url -from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse +from pretix.multidomain.urlreverse import eventreverse from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.tasks import ( @@ -74,28 +72,6 @@ from pretix.plugins.stripe.tasks import ( logger = logging.getLogger('pretix.plugins.stripe') -@xframe_options_exempt -def redirect_view(request, *args, **kwargs): - try: - data = signing.loads(request.GET.get('data', ''), salt='safe-redirect') - except signing.BadSignature: - return HttpResponseBadRequest('Invalid parameter') - - if 'go' in request.GET: - if 'session' in data: - for k, v in data['session'].items(): - request.session[k] = v - return redirect(data['url']) - else: - params = request.GET.copy() - params['go'] = '1' - r = render(request, 'pretixplugins/stripe/redirect.html', { - 'url': build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params), - }) - r._csp_ignore = True - return r - - @scopes_disabled() def oauth_return(request, *args, **kwargs): import stripe @@ -514,11 +490,6 @@ class StripeOrderView: return self.request.event.get_payment_providers()[self.payment.provider] def _redirect_to_order(self): - if self.request.session.get('payment_stripe_order_secret') != self.order.secret and not self.payment.provider.startswith('stripe'): - messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' - 'in your emails to continue.')) - return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) - return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={ 'order': self.order.code, 'secret': self.order.secret diff --git a/src/pretix/presale/templates/pretixpresale/event/cookies.html b/src/pretix/presale/templates/pretixpresale/event/cookies.html index 7e10261034..e5bc8e4eda 100644 --- a/src/pretix/presale/templates/pretixpresale/event/cookies.html +++ b/src/pretix/presale/templates/pretixpresale/event/cookies.html @@ -2,6 +2,7 @@ {% load i18n %} {% load eventurl %} {% load urlreplace %} +{% load static %} {% block content %} {% if cart_namespace %} @@ -23,9 +24,8 @@ class="btn btn-primary btn-lg" target="_blank"> {% trans "Continue in new tab" %} - + {{ url|json_script:"framebreak-url" }} + {% else %}

{% trans "Cookies not supported" %}

diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index 786377e3e3..55f91e42b5 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -536,7 +536,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): **pass_through_url_params, }) }) - r._csp_ignore = True return r if not request.event.all_sales_channels and request.sales_channel.identifier not in (s.identifier for s in request.event.limit_sales_channels.all()): diff --git a/src/pretix/presale/views/waiting.py b/src/pretix/presale/views/waiting.py index b2b1019796..e135e3f338 100644 --- a/src/pretix/presale/views/waiting.py +++ b/src/pretix/presale/views/waiting.py @@ -125,7 +125,6 @@ class WaitingView(EventViewMixin, FormView): request.event, "presale:event.waitinglist", kwargs={'cart_namespace': kwargs.get('cart_namespace')} ) + '?' + url_replace(request, 'require_cookie', '', 'iframe', '', 'locale', request.GET.get('locale', get_language_without_region())) }) - r._csp_ignore = True return r if not self.itemvars: diff --git a/src/pretix/static/pretixbase/js/framebreak.js b/src/pretix/static/pretixbase/js/framebreak.js new file mode 100644 index 0000000000..1beab5d7a4 --- /dev/null +++ b/src/pretix/static/pretixbase/js/framebreak.js @@ -0,0 +1,3 @@ +// Attempt to auto-open page in new tab. Will be ignored by most browser's popup blockers anyways, though. +var url = JSON.parse(document.getElementById('framebreak-url').innerText) +window.open(url)