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 %}
-
-
-
-
-
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 %}
-
-
-
-
-
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 %}
-
-
-
-
-
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)