diff --git a/src/pretix/base/views/tasks.py b/src/pretix/base/views/tasks.py index f7c78098ae..bed6862bb6 100644 --- a/src/pretix/base/views/tasks.py +++ b/src/pretix/base/views/tasks.py @@ -34,7 +34,7 @@ from django.core.exceptions import PermissionDenied, ValidationError from django.core.files.uploadedfile import UploadedFile from django.db import transaction from django.http import HttpResponse, JsonResponse, QueryDict -from django.shortcuts import redirect, render +from django.shortcuts import render from django.test import RequestFactory from django.utils import timezone, translation from django.utils.datastructures import MultiValueDict @@ -47,6 +47,7 @@ from redis import ResponseError from pretix.base.models import CachedFile, User from pretix.base.services.tasks import ProfiledEventTask from pretix.celery_app import app +from pretix.helpers.http import redirect_to_url logger = logging.getLogger('pretix.base.tasks') @@ -152,7 +153,7 @@ class AsyncMixin: 'redirect': self.get_success_url(value), 'message': str(self.get_success_message(value)) }) - return redirect(self.get_success_url(value)) + return redirect_to_url(self.get_success_url(value)) def error(self, exception): if isinstance(exception, PermissionDenied): @@ -165,7 +166,7 @@ class AsyncMixin: 'redirect': self.get_error_url(), 'message': str(self.get_error_message(exception)) }) - return redirect(self.get_error_url()) + return redirect_to_url(self.get_error_url()) def get_error_message(self, exception): if isinstance(exception, dict) and exception['exc_type'] in self.known_errortypes: @@ -203,7 +204,7 @@ class AsyncAction(AsyncMixin): return self.success(res.info) else: return self.error(res.info) - return redirect(self.get_check_url(res.id, False)) + return redirect_to_url(self.get_check_url(res.id, False)) def get(self, request, *args, **kwargs): if 'async_id' in request.GET and settings.HAS_CELERY: @@ -375,7 +376,7 @@ class AsyncFormView(AsyncMixin, FormView): return self.success(res.info) else: return self.error(res.info) - return redirect(self.get_check_url(res.id, False)) + return redirect_to_url(self.get_check_url(res.id, False)) class AsyncPostView(AsyncMixin, View): @@ -478,4 +479,4 @@ class AsyncPostView(AsyncMixin, View): return self.success(res.info) else: return self.error(res.info) - return redirect(self.get_check_url(res.id, False)) + return redirect_to_url(self.get_check_url(res.id, False)) diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index 53903d17ed..b5d995c28f 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -37,7 +37,7 @@ from urllib.parse import quote, urljoin, urlparse from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, logout from django.http import Http404 -from django.shortcuts import get_object_or_404, redirect, resolve_url +from django.shortcuts import get_object_or_404, resolve_url from django.template.response import TemplateResponse from django.urls import get_script_prefix, resolve, reverse from django.utils.encoding import force_str @@ -46,6 +46,7 @@ from django_scopes import scope from pretix.base.models import Event, Organizer from pretix.base.models.auth import SuperuserPermissionSet, User +from pretix.helpers.http import redirect_to_url from pretix.helpers.security import ( SessionInvalid, SessionReauthRequired, assert_session_valid, ) @@ -118,7 +119,7 @@ class PermissionMiddleware: if hasattr(request, 'organizer'): # If the user is on a organizer's subdomain, he should be redirected to pretix - return redirect(urljoin(settings.SITE_URL, request.get_full_path())) + return redirect_to_url(urljoin(settings.SITE_URL, request.get_full_path())) if url_name in self.EXCEPTIONS: return self.get_response(request) if not request.user.is_authenticated: @@ -132,14 +133,14 @@ class PermissionMiddleware: return self._login_redirect(request) except SessionReauthRequired: if url_name not in ('user.reauth', 'auth.logout'): - return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) + return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE: - return redirect(reverse('control:user.settings') + '?next=' + quote(request.get_full_path())) + return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path())) if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \ and url_name not in self.EXCEPTIONS_2FA: - return redirect(reverse('control:user.settings.2fa')) + return redirect_to_url(reverse('control:user.settings.2fa')) if 'event' in url.kwargs and 'organizer' in url.kwargs: if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-': @@ -152,7 +153,7 @@ class PermissionMiddleware: k = dict(url.kwargs) k['organizer'] = ev.organizer.slug k['event'] = ev.slug - return redirect(reverse(url.view_name, kwargs=k, args=url.args)) + return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args)) with scope(organizer=None): request.event = Event.objects.filter( @@ -178,7 +179,7 @@ class PermissionMiddleware: "have no permission to administrate it.")) k = dict(url.kwargs) k['organizer'] = org.slug - return redirect(reverse(url.view_name, kwargs=k, args=url.args)) + return redirect_to_url(reverse(url.view_name, kwargs=k, args=url.args)) request.organizer = Organizer.objects.filter( slug=url.kwargs['organizer'], diff --git a/src/pretix/control/permissions.py b/src/pretix/control/permissions.py index a3e9bb9c68..afc677ff22 100644 --- a/src/pretix/control/permissions.py +++ b/src/pretix/control/permissions.py @@ -35,10 +35,11 @@ from urllib.parse import quote from django.core.exceptions import PermissionDenied -from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext as _ +from pretix.helpers.http import redirect_to_url + def current_url(request): if request.GET: @@ -135,7 +136,7 @@ def administrator_permission_required(): raise PermissionDenied() if not request.user.has_active_staff_session(request.session.session_key): if request.user.is_staff: - return redirect(reverse('control:user.sudo') + '?next=' + quote(current_url(request))) + return redirect_to_url(reverse('control:user.sudo') + '?next=' + quote(current_url(request))) raise PermissionDenied(_('You do not have permission to view this content.')) return function(request, *args, **kw) return wrapper diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index 925d4edd8c..0c33e93bc6 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -87,8 +87,8 @@ def process_login(request, user, keep_logged_in): auth_login(request, user) request.session['pretix_auth_login_time'] = int(time.time()) if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None): - return redirect(next_url) - return redirect(reverse('control:index')) + return redirect_to_url(next_url) + return redirect('control:index') def login(request): @@ -149,7 +149,10 @@ def register(request): raise PermissionDenied('Registration is disabled') ctx = {} if request.user.is_authenticated: - return redirect(request.GET.get("next", 'control:index')) + next_url = request.GET.get("next") or reverse("control:index") + if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None): + return redirect_to_url(next_url) + return redirect("control:index") if request.method == 'POST': form = RegistrationForm(data=request.POST) if form.is_valid(): @@ -256,7 +259,10 @@ class Forgot(TemplateView): def get(self, request, *args, **kwargs): if request.user.is_authenticated: - return redirect(request.GET.get("next", 'control:index')) + next_url = request.GET.get("next") or reverse("control:index") + if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None): + return redirect_to_url(next_url) + return redirect("control:index") return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): @@ -329,7 +335,10 @@ class Recover(TemplateView): def get(self, request, *args, **kwargs): if request.user.is_authenticated: - return redirect(request.GET.get("next", 'control:index')) + next_url = request.GET.get("next") or reverse("control:index") + if next_url and url_has_allowed_host_and_scheme(next_url, allowed_hosts=None): + return redirect_to_url(next_url) + return redirect("control:index") try: user = User.objects.get(id=self.request.GET.get('id'), is_active=True, auth_backend='native') except User.DoesNotExist: @@ -453,7 +462,7 @@ class Login2FAView(TemplateView): del request.session['pretix_auth_2fa_time'] if "next" in request.GET and url_has_allowed_host_and_scheme(request.GET.get("next"), allowed_hosts=None): return redirect_to_url(request.GET.get("next")) - return redirect(reverse('control:index')) + return redirect('control:index') else: messages.error(request, _('Invalid code, please try again.')) return redirect('control:auth.login.2fa') diff --git a/src/pretix/control/views/orderimport.py b/src/pretix/control/views/orderimport.py index 48abe174f4..1912886468 100644 --- a/src/pretix/control/views/orderimport.py +++ b/src/pretix/control/views/orderimport.py @@ -50,6 +50,7 @@ from pretix.base.services.orderimport import import_orders, parse_csv from pretix.base.views.tasks import AsyncAction from pretix.control.forms.orderimport import ProcessForm from pretix.control.permissions import EventPermissionRequiredMixin +from pretix.helpers.http import redirect_to_url logger = logging.getLogger(__name__) ENCODINGS = ( @@ -69,19 +70,19 @@ class ImportView(EventPermissionRequiredMixin, TemplateView): def post(self, request, *args, **kwargs): if 'file' not in request.FILES: - return redirect(reverse('control:event.orders.import', kwargs={ + return redirect_to_url(reverse('control:event.orders.import', kwargs={ 'event': request.event.slug, 'organizer': request.organizer.slug, })) if not request.FILES['file'].name.lower().endswith('.csv'): messages.error(request, _('Please only upload CSV files.')) - return redirect(reverse('control:event.orders.import', kwargs={ + return redirect_to_url(reverse('control:event.orders.import', kwargs={ 'event': request.event.slug, 'organizer': request.organizer.slug, })) if request.FILES['file'].size > settings.FILE_UPLOAD_MAX_SIZE_OTHER: messages.error(request, _('Please do not upload files larger than 10 MB.')) - return redirect(reverse('control:event.orders.import', kwargs={ + return redirect_to_url(reverse('control:event.orders.import', kwargs={ 'event': request.event.slug, 'organizer': request.organizer.slug, })) diff --git a/src/pretix/plugins/paypal/views.py b/src/pretix/plugins/paypal/views.py index 62b41fcb9d..414b377892 100644 --- a/src/pretix/plugins/paypal/views.py +++ b/src/pretix/plugins/paypal/views.py @@ -42,7 +42,7 @@ 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 redirect, render +from django.shortcuts import render from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.decorators.clickjacking import xframe_options_exempt @@ -53,6 +53,7 @@ from django_scopes import scopes_disabled from pretix.base.models import Order, OrderPayment, OrderRefund, Quota from pretix.base.payment import PaymentException from pretix.control.permissions import event_permission_required +from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.plugins.paypal.models import ReferencedPayPalObject from pretix.plugins.paypal.payment import Paypal @@ -99,23 +100,23 @@ def success(request, *args, **kwargs): except PaymentException as e: messages.error(request, str(e)) urlkwargs['step'] = 'payment' - return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs)) + return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs)) if resp: return resp else: messages.error(request, _('Invalid response from PayPal received.')) logger.error('Session did not contain payment_paypal_id') urlkwargs['step'] = 'payment' - return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs)) + return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs)) if payment: - return redirect(eventreverse(request.event, 'presale:event.order', kwargs={ + return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={ 'order': payment.order.code, 'secret': payment.order.secret }) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else '')) else: urlkwargs['step'] = 'confirm' - return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs)) + return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs)) def abort(request, *args, **kwargs): @@ -127,12 +128,12 @@ def abort(request, *args, **kwargs): payment = None if payment: - return redirect(eventreverse(request.event, 'presale:event.order', kwargs={ + return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={ 'order': payment.order.code, 'secret': payment.order.secret }) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else '')) else: - return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'})) + return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'})) @csrf_exempt @@ -259,7 +260,7 @@ def oauth_disconnect(request, **kwargs): event.enable_plugin("pretix.plugins.paypal2") event.save() - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': request.event.organizer.slug, 'event': request.event.slug, 'provider': 'paypal_settings' diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py index 7651d79a44..c89dd5c687 100644 --- a/src/pretix/plugins/paypal2/views.py +++ b/src/pretix/plugins/paypal2/views.py @@ -63,6 +63,7 @@ from pretix.base.payment import PaymentException from pretix.base.services.cart import add_payment_to_cart, get_fees from pretix.base.settings import GlobalSettingsObject from pretix.control.permissions import event_permission_required +from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_request import ( PartnersMerchantIntegrationsGetRequest, @@ -222,7 +223,7 @@ def isu_return(request, *args, **kwargs): missing_getparams = set(getparams) - set(request.GET) missing_sessionparams = {p for p in sessionparams if p not in request.session} logger.exception('PayPal2 - Missing params in GET {} and/or Session {}'.format(missing_getparams, missing_sessionparams)) - return redirect(reverse('control:index')) + return redirect('control:index') event = get_object_or_404(Event, pk=request.session['payment_paypal_isu_event']) @@ -282,7 +283,7 @@ def isu_return(request, *args, **kwargs): if third_party.partner_client_id == prov.client.environment.client_id: event.settings.payment_paypal_isu_scopes = third_party.scopes - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': event.organizer.slug, 'event': event.slug, 'provider': 'paypal_settings' @@ -345,12 +346,12 @@ def abort(request, *args, **kwargs): payment = None if payment: - return redirect(eventreverse(request.event, 'presale:event.order', kwargs={ + return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={ 'order': payment.order.code, 'secret': payment.order.secret }) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else '')) else: - return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'})) + return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'})) @csrf_exempt @@ -518,7 +519,7 @@ def isu_disconnect(request, **kwargs): request.event.settings.payment_paypal__enabled = False messages.success(request, _('Your PayPal account has been disconnected.')) - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': request.event.organizer.slug, 'event': request.event.slug, 'provider': 'paypal_settings' diff --git a/src/pretix/plugins/returnurl/signals.py b/src/pretix/plugins/returnurl/signals.py index 252d2f49c7..6ad8c22321 100644 --- a/src/pretix/plugins/returnurl/signals.py +++ b/src/pretix/plugins/returnurl/signals.py @@ -24,11 +24,11 @@ from urllib.parse import urlencode from django.contrib.messages import constants as messages, get_messages 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 gettext_lazy as _ from pretix.control.signals import nav_event_settings +from pretix.helpers.http import redirect_to_url from pretix.presale.signals import process_request @@ -64,7 +64,7 @@ def returnurl_process_request(sender, request, **kwargs): url += '&' + urlencode(query) else: url += '?' + urlencode(query) - r = redirect(url) + r = redirect_to_url(url) del request.session[key] return r elif urlname != 'event.order' and 'return_url' in request.GET: diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index cc86ad721f..3d479cdc0a 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -65,6 +65,7 @@ from pretix.control.permissions import ( 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.plugins.stripe.forms import OrganizerStripeSettingsForm from pretix.plugins.stripe.models import ReferencedStripeObject @@ -102,13 +103,13 @@ def redirect_view(request, *args, **kwargs): def oauth_return(request, *args, **kwargs): if 'payment_stripe_oauth_event' not in request.session: messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) - return redirect(reverse('control:index')) + return redirect('control:index') event = get_object_or_404(Event, pk=request.session['payment_stripe_oauth_event']) if request.GET.get('state') != request.session['payment_stripe_oauth_token']: messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': event.organizer.slug, 'event': event.slug, 'provider': 'stripe_settings' @@ -147,7 +148,7 @@ def oauth_return(request, *args, **kwargs): except: logger.exception('Failed to obtain OAuth token') messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': event.organizer.slug, 'event': event.slug, 'provider': 'stripe_settings' @@ -188,7 +189,7 @@ def oauth_return(request, *args, **kwargs): stripe_verify_domain.apply_async(args=(event.pk, get_domain_for_event(event))) - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': event.organizer.slug, 'event': event.slug, 'provider': 'stripe_settings' @@ -469,7 +470,7 @@ def oauth_disconnect(request, **kwargs): request.event.settings.payment_stripe__enabled = False messages.success(request, _('Your Stripe account has been disconnected.')) - return redirect(reverse('control:event.settings.payment.provider', kwargs={ + return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': request.event.organizer.slug, 'event': request.event.slug, 'provider': 'stripe_settings' @@ -503,9 +504,9 @@ class StripeOrderView: 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(eventreverse(self.request.event, 'presale:event.index')) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) - return redirect(eventreverse(self.request.event, 'presale:event.order', kwargs={ + return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={ 'order': self.order.code, 'secret': self.order.secret }) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else '')) @@ -522,12 +523,12 @@ class ReturnView(StripeOrderView, View): logger.exception('Could not retrieve source') messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' 'in your emails to continue.')) - return redirect(eventreverse(self.request.event, 'presale:event.index')) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) if src.client_secret != request.GET.get('client_secret'): messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' 'in your emails to continue.')) - return redirect(eventreverse(self.request.event, 'presale:event.index')) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) with transaction.atomic(): self.order.refresh_from_db() @@ -664,7 +665,7 @@ class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, Adminis } ) messages.success(self.request, _('Your changes have been saved.')) - return redirect(self.get_success_url()) + return redirect_to_url(self.get_success_url()) else: messages.error(self.request, _('We could not save your changes. See below for details.')) return self.get(request) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index f4ae38b80a..ef85266532 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -47,7 +47,6 @@ from django.db import models from django.db.models import Count, F, Q, Sum from django.db.models.functions import Cast from django.http import HttpResponseNotAllowed, JsonResponse -from django.shortcuts import redirect from django.utils import translation from django.utils.functional import cached_property from django.utils.translation import ( @@ -73,6 +72,7 @@ from pretix.base.templatetags.phone_format import phone_format from pretix.base.templatetags.rich_text import rich_text_snippet from pretix.base.views.tasks import AsyncAction from pretix.celery_app import app +from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.presale.forms.checkout import ( ContactForm, InvoiceAddressForm, InvoiceNameForm, MembershipForm, @@ -320,23 +320,23 @@ class CustomerStep(CartMixin, TemplateFlowStep): if request.POST.get("customer_mode") == 'login': if self.cart_session.get('customer'): - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) elif request.customer: self.cart_session['customer_mode'] = 'login' self.cart_session['customer'] = request.customer.pk self.cart_session['customer_cart_tied_to_login'] = True - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) elif self.request.POST.get("login-sso-data"): if not self._handle_sso_login(): messages.error(request, _('We failed to process your authentication request, please try again.')) return self.render() - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) elif self.event.settings.customer_accounts_native and self.login_form.is_valid(): customer_login(self.request, self.login_form.get_customer()) self.cart_session['customer_mode'] = 'login' self.cart_session['customer'] = self.login_form.get_customer().pk self.cart_session['customer_cart_tied_to_login'] = True - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) else: return self.render() elif request.POST.get("customer_mode") == 'register' and self.signup_allowed: @@ -345,13 +345,13 @@ class CustomerStep(CartMixin, TemplateFlowStep): self.cart_session['customer_mode'] = 'login' self.cart_session['customer'] = customer.pk self.cart_session['customer_cart_tied_to_login'] = False - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) else: return self.render() elif request.POST.get("customer_mode") == 'guest' and self.guest_allowed: self.cart_session['customer'] = None self.cart_session['customer_mode'] = 'guest' - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) else: return self.render() @@ -453,7 +453,7 @@ class MembershipStep(CartMixin, TemplateFlowStep): for f in self.forms: f.position.save(update_fields=['used_membership']) - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) def is_completed(self, request, warn=False): self.request = request @@ -932,9 +932,9 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): messages.info(request, _('Due to the invoice address you entered, we need to apply a different tax ' 'rate to your purchase and the price of the products in your cart has ' 'changed accordingly.')) - return redirect(self.get_next_url(request) + '?open_cart=true') + return redirect_to_url(self.get_next_url(request) + '?open_cart=true') - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) def is_completed(self, request, warn=False): self.request = request @@ -1237,7 +1237,7 @@ class PaymentStep(CartMixin, TemplateFlowStep): if "remove_payment" in request.POST: self._remove_payment(request.POST["remove_payment"]) - return redirect(self.get_step_url(request)) + return redirect_to_url(self.get_step_url(request)) for p in self.provider_forms: pprov = p['provider'] @@ -1277,7 +1277,7 @@ class PaymentStep(CartMixin, TemplateFlowStep): cart = self.get_cart() valid, remainder = self.current_payments_valid(cart['total']) if valid: - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) else: # Show payment step again to select another method messages.success( @@ -1287,9 +1287,9 @@ class PaymentStep(CartMixin, TemplateFlowStep): money_filter(remainder, self.event.currency) ) ) - return redirect(self.get_step_url(request)) + return redirect_to_url(self.get_step_url(request)) elif isinstance(resp, str): - return redirect(resp) + return redirect_to_url(resp) else: if resp is True or isinstance(resp, str): # There can only be one payment method that does not have multi_use_supported, remove all @@ -1298,14 +1298,14 @@ class PaymentStep(CartMixin, TemplateFlowStep): add_payment_to_cart(request, pprov, None, None, None) if isinstance(resp, str): - return redirect(resp) + return redirect_to_url(resp) else: - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) return self.render() if self.is_completed(request, warn=False): # All payments already accounted for, no need to select one - return redirect(self.get_next_url(request)) + return redirect_to_url(self.get_next_url(request)) messages.error(self.request, _("Please select a payment method.")) return self.render() @@ -1507,7 +1507,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): 'redirect': self.get_error_url(), 'message': msg }) - return redirect(self.get_error_url()) + return redirect_to_url(self.get_error_url()) meta_info = { 'contact_form_data': self.cart_session.get('contact_form_data', {}), diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py index dc2aa2f238..ecfe4d5969 100644 --- a/src/pretix/presale/utils.py +++ b/src/pretix/presale/utils.py @@ -42,7 +42,6 @@ from django.core.exceptions import PermissionDenied from django.db.models import Q from django.http import Http404 from django.middleware.csrf import rotate_token -from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import resolve from django.utils.crypto import constant_time_compare @@ -54,6 +53,7 @@ from django_scopes import scope from pretix.base.middleware import LocaleMiddleware from pretix.base.models import Customer, Event, Organizer +from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import ( get_event_domain, get_organizer_domain, ) @@ -241,7 +241,7 @@ def _detect_event(request, require_live=True, require_plugin=None): if url.kwargs['organizer'] != request.organizer.slug: raise Http404(_('The selected event was not found.')) path = "/" + request.get_full_path().split("/", 2)[-1] - return redirect(path) + return redirect_to_url(path) request.organizer = request.organizer if 'event' in url.kwargs: @@ -256,7 +256,7 @@ def _detect_event(request, require_live=True, require_plugin=None): if request.port and request.port not in (80, 443): domain = '%s:%d' % (domain, request.port) path = request.get_full_path().split("/", 2)[-1] - r = redirect(urljoin('%s://%s' % (request.scheme, domain), path)) + r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path)) r['Access-Control-Allow-Origin'] = '*' return r else: @@ -277,7 +277,7 @@ def _detect_event(request, require_live=True, require_plugin=None): if request.port and request.port not in (80, 443): domain = '%s:%d' % (domain, request.port) path = request.get_full_path().split("/", 3)[-1] - r = redirect(urljoin('%s://%s' % (request.scheme, domain), path)) + r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path)) r['Access-Control-Allow-Origin'] = '*' return r elif 'organizer' in url.kwargs: @@ -293,7 +293,7 @@ def _detect_event(request, require_live=True, require_plugin=None): if request.port and request.port not in (80, 443): domain = '%s:%d' % (domain, request.port) path = request.get_full_path().split("/", 2)[-1] - r = redirect(urljoin('%s://%s' % (request.scheme, domain), path)) + r = redirect_to_url(urljoin('%s://%s' % (request.scheme, domain), path)) r['Access-Control-Allow-Origin'] = '*' return r @@ -350,7 +350,7 @@ def _detect_event(request, require_live=True, require_plugin=None): ) pathparts = request.get_full_path().split('/') pathparts[1] = event.slug - r = redirect('/'.join(pathparts)) + r = redirect_to_url('/'.join(pathparts)) r['Access-Control-Allow-Origin'] = '*' return r else: @@ -362,7 +362,7 @@ def _detect_event(request, require_live=True, require_plugin=None): pathparts = request.get_full_path().split('/') pathparts[1] = event.organizer.slug pathparts[2] = event.slug - r = redirect('/'.join(pathparts)) + r = redirect_to_url('/'.join(pathparts)) r['Access-Control-Allow-Origin'] = '*' return r except Event.DoesNotExist: @@ -378,7 +378,7 @@ def _detect_event(request, require_live=True, require_plugin=None): raise Http404(_('The selected organizer was not found.')) pathparts = request.get_full_path().split('/') pathparts[1] = organizer.slug - r = redirect('/'.join(pathparts)) + r = redirect_to_url('/'.join(pathparts)) r['Access-Control-Allow-Origin'] = '*' return r raise Http404(_('The selected organizer was not found.')) diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py index 557935135c..f58a115db0 100644 --- a/src/pretix/presale/views/cart.py +++ b/src/pretix/presale/views/cart.py @@ -44,7 +44,7 @@ from django.contrib import messages from django.core.cache import caches from django.db.models import Q from django.http import FileResponse, Http404, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, render from django.utils import translation from django.utils.crypto import get_random_string from django.utils.decorators import method_decorator @@ -430,7 +430,7 @@ class CartApplyVoucher(EventViewMixin, CartActionMixin, AsyncAction, View): 'redirect': self.get_error_url() }) else: - return redirect(self.get_error_url()) + return redirect_to_url(self.get_error_url()) @method_decorator(allow_frame_if_namespaced, 'dispatch') @@ -451,14 +451,14 @@ class CartRemove(EventViewMixin, CartActionMixin, AsyncAction, View): return self.do(self.request.event.id, int(request.POST.get('id')), get_or_create_cart_id(self.request), translation.get_language(), request.sales_channel.identifier) except ValueError: - return redirect(self.get_error_url()) + return redirect_to_url(self.get_error_url()) else: if 'ajax' in self.request.GET or 'ajax' in self.request.POST: return JsonResponse({ 'redirect': self.get_error_url() }) else: - return redirect(self.get_error_url()) + return redirect_to_url(self.get_error_url()) @method_decorator(allow_frame_if_namespaced, 'dispatch') @@ -537,7 +537,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View): 'message': str(error_messages['empty']) }) else: - return redirect(self.get_error_url()) + return redirect_to_url(self.get_error_url()) @method_decorator(allow_frame_if_namespaced, 'dispatch') @@ -660,8 +660,12 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView self.subevent = self.voucher.subevent if not err and not self.subevent: - return redirect(eventreverse(self.request.event, 'presale:event.index', - kwargs={'cart_namespace': kwargs.get('cart_namespace') or ''}) + '?voucher=' + quote(self.voucher.code)) + return redirect_to_url( + eventreverse( + self.request.event, 'presale:event.index', + kwargs={'cart_namespace': kwargs.get('cart_namespace') or ''} + ) + '?voucher=' + quote(self.voucher.code) + ) else: pass @@ -678,7 +682,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView def get(self, request, *args, **kwargs): if 'iframe' in request.GET and 'require_cookie' not in request.GET: - return redirect(request.get_full_path() + '&require_cookie=1') + return redirect_to_url(request.get_full_path() + '&require_cookie=1') if len(self.request.GET.get('widget_data', '{}')) > 3: # We've been passed data from a widget, we need to create a cart session to store it. diff --git a/src/pretix/presale/views/checkout.py b/src/pretix/presale/views/checkout.py index 1f55833ffa..f61bb0c363 100644 --- a/src/pretix/presale/views/checkout.py +++ b/src/pretix/presale/views/checkout.py @@ -23,13 +23,13 @@ from urllib.parse import quote from django.contrib import messages from django.http import Http404 -from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.views.generic import View from pretix.base.services.cart import CartError from pretix.base.signals import validate_cart +from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.presale.checkoutflow import get_checkout_flow from pretix.presale.views import ( @@ -94,4 +94,4 @@ class CheckoutView(View): def redirect(self, url): if 'cart_id' in self.request.GET: url += ('&' if '?' in url else '?') + 'cart_id=' + quote(self.request.GET.get('cart_id')) - return redirect(url) + return redirect_to_url(url) diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index 665f4ffdce..2b59b3cb9d 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -35,7 +35,7 @@ from django.db.models import ( Count, IntegerField, OuterRef, Prefetch, Q, Subquery, ) from django.http import Http404, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, render from django.utils.crypto import get_random_string from django.utils.decorators import method_decorator from django.utils.functional import cached_property @@ -343,7 +343,7 @@ class CustomerRequiredMixin: if not request.organizer.settings.customer_accounts: raise Http404('Feature not enabled') if not getattr(request, 'customer', None): - return redirect( + return redirect_to_url( eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={}) + '?next=' + quote(self.request.path_info + '?' + self.request.GET.urlencode()) ) @@ -848,7 +848,7 @@ class SSOLoginReturnView(RedirectBackMixin, View): self.request, message, ) - return redirect(eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})) + return redirect_to_url(eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})) else: return render(self.request, 'pretixpresale/postmessage.html', { 'message': { diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py index e2e5a8ab52..1500ac70f2 100644 --- a/src/pretix/presale/views/event.py +++ b/src/pretix/presale/views/event.py @@ -49,7 +49,7 @@ from django.db.models import ( Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value, ) from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404, render from django.utils.decorators import method_decorator from django.utils.formats import get_format from django.utils.functional import SimpleLazyObject @@ -78,6 +78,7 @@ from pretix.presale.views.organizer import ( ) from ...helpers.formats.en.formats import SHORT_MONTH_DAY_FORMAT, WEEK_FORMAT +from ...helpers.http import redirect_to_url from . import ( CartMixin, EventViewMixin, allow_frame_if_namespaced, get_cart, iframe_entry_view_wrapper, @@ -456,14 +457,14 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): if all(k in request.GET for k in keys): get_params = {k: v for k, v in request.GET.items() if k not in keys} get_params["date"] = "%s-%s" % (request.GET.get("year"), request.GET.get("month")) - return redirect(self.request.path + "?" + urlencode(get_params)) + return redirect_to_url(self.request.path + "?" + urlencode(get_params)) # redirect old week-year-URLs to new date-URLs keys = ("week", "year") if all(k in request.GET for k in keys): get_params = {k: v for k, v in request.GET.items() if k not in keys} get_params["date"] = "%s-W%s" % (request.GET.get("year"), request.GET.get("week")) - return redirect(self.request.path + "?" + urlencode(get_params)) + return redirect_to_url(self.request.path + "?" + urlencode(get_params)) from pretix.presale.views.cart import get_or_create_cart_id @@ -471,11 +472,11 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): if request.GET.get('src', '') == 'widget' and 'take_cart_id' in request.GET: # User has clicked "Open in a new tab" link in widget get_or_create_cart_id(request) - return redirect(eventreverse(request.event, 'presale:event.index', kwargs=kwargs)) + return redirect_to_url(eventreverse(request.event, 'presale:event.index', kwargs=kwargs)) elif request.GET.get('iframe', '') == '1' and 'take_cart_id' in request.GET: # Widget just opened, a cart already exists. Let's to a stupid redirect to check if cookies are disabled get_or_create_cart_id(request) - return redirect(eventreverse(request.event, 'presale:event.index', kwargs=kwargs) + '?' + urllib.parse.urlencode({ + return redirect_to_url(eventreverse(request.event, 'presale:event.index', kwargs=kwargs) + '?' + urllib.parse.urlencode({ 'require_cookie': 'true', 'cart_id': request.GET.get('take_cart_id'), **({"locale": request.GET.get('locale')} if request.GET.get('locale') else {}), @@ -510,7 +511,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView): return super().get(request, *args, **kwargs) else: if 'subevent' in kwargs: - return redirect(self.get_index_url()) + return redirect_to_url(self.get_index_url()) else: return super().get(request, *args, **kwargs) @@ -787,11 +788,11 @@ class SeatingPlanView(EventViewMixin, TemplateView): if request.GET.get('src', '') == 'widget' and 'take_cart_id' in request.GET: # User has clicked "Open in a new tab" link in widget get_or_create_cart_id(request) - return redirect(eventreverse(request.event, 'presale:event.seatingplan', kwargs=kwargs)) + return redirect_to_url(eventreverse(request.event, 'presale:event.seatingplan', kwargs=kwargs)) elif request.GET.get('iframe', '') == '1' and 'take_cart_id' in request.GET: # Widget just opened, a cart already exists. Let's to a stupid redirect to check if cookies are disabled get_or_create_cart_id(request) - return redirect(eventreverse(request.event, 'presale:event.seatingplan', kwargs=kwargs) + '?require_cookie=true&cart_id={}'.format( + return redirect_to_url(eventreverse(request.event, 'presale:event.seatingplan', kwargs=kwargs) + '?require_cookie=true&cart_id={}'.format( request.GET.get('take_cart_id') )) elif request.GET.get('iframe', '') == '1' and len(self.request.GET.get('widget_data', '{}')) > 3: @@ -890,4 +891,4 @@ class EventAuth(View): raise PermissionDenied(_('Please go back and try again.')) request.session['pretix_event_access_{}'.format(request.event.pk)] = parent - return redirect(eventreverse(request.event, 'presale:event.index')) + return redirect_to_url(eventreverse(request.event, 'presale:event.index')) diff --git a/src/pretix/presale/views/oidc_op.py b/src/pretix/presale/views/oidc_op.py index 5a388be170..cb6a66ec64 100644 --- a/src/pretix/presale/views/oidc_op.py +++ b/src/pretix/presale/views/oidc_op.py @@ -44,6 +44,7 @@ from pretix.base.customersso.oidc import ( from pretix.base.models.customers import ( CustomerSSOAccessToken, CustomerSSOClient, CustomerSSOGrant, ) +from pretix.helpers.http import redirect_to_url from pretix.multidomain.middlewares import CsrfViewMiddleware from pretix.multidomain.urlreverse import build_absolute_uri from pretix.presale.forms.customer import AuthenticationForm @@ -106,7 +107,7 @@ class AuthorizeView(View): CsrfViewMiddleware(lambda: None)._check_token(request) except: # External request, we prefer GET and will redirect to prevent confusion with our login form - return redirect(request.path + '?' + request.POST.urlencode()) + return redirect_to_url(request.path + '?' + request.POST.urlencode()) return self._process_auth_request(request, request.GET) def _final_error(self, error, error_description): diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 54715eaad3..a7d3a6cd2e 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -86,6 +86,7 @@ from pretix.base.signals import ( from pretix.base.templatetags.money import money_filter from pretix.base.views.mixins import OrderQuestionsViewMixin from pretix.base.views.tasks import AsyncAction +from pretix.helpers.http import redirect_to_url from pretix.helpers.safedownload import check_token from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.presale.forms.checkout import InvoiceAddressForm, QuestionsForm @@ -420,9 +421,9 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView): if 'payment_change_{}'.format(self.order.pk) in request.session: del request.session['payment_change_{}'.format(self.order.pk)] if isinstance(resp, str): - return redirect(resp) + return redirect_to_url(resp) elif resp is True: - return redirect(self.get_confirm_url()) + return redirect_to_url(self.get_confirm_url()) else: return self.get(request, *args, **kwargs) @@ -574,9 +575,9 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View): return redirect(self.get_order_url()) if self.order.status == Order.STATUS_PAID: - return redirect(resp or self.get_order_url() + '?paid=yes') + return redirect_to_url(resp or self.get_order_url() + '?paid=yes') else: - return redirect(resp or self.get_order_url() + '?thanks=yes') + return redirect_to_url(resp or self.get_order_url() + '?thanks=yes') def get_payment_url(self): return eventreverse(self.request.event, 'presale:event.order.pay', kwargs={ diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py index 260c5cd002..a43372d04c 100644 --- a/src/pretix/presale/views/organizer.py +++ b/src/pretix/presale/views/organizer.py @@ -48,7 +48,6 @@ from django.core.cache import caches from django.db.models import Exists, Max, Min, OuterRef, Prefetch, Q from django.db.models.functions import Coalesce, Greatest from django.http import Http404, HttpResponse, QueryDict -from django.shortcuts import redirect from django.templatetags.static import static from django.utils.decorators import method_decorator from django.utils.formats import date_format, get_format @@ -68,6 +67,7 @@ from pretix.helpers.daterange import daterange from pretix.helpers.formats.en.formats import ( SHORT_MONTH_DAY_FORMAT, WEEK_FORMAT, ) +from pretix.helpers.http import redirect_to_url from pretix.helpers.thumb import get_thumbnail from pretix.multidomain.urlreverse import eventreverse from pretix.presale.forms.organizer import EventListFilterForm @@ -655,7 +655,7 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView): if all(k in request.GET for k in keys): get_params = {k: v for k, v in request.GET.items() if k not in keys} get_params["date"] = "%s-%s" % (request.GET.get("year"), request.GET.get("month")) - return redirect(self.request.path + "?" + urlencode(get_params)) + return redirect_to_url(self.request.path + "?" + urlencode(get_params)) self._set_month_year() return super().get(request, *args, **kwargs) @@ -736,7 +736,7 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView): if all(k in request.GET for k in keys): get_params = {k: v for k, v in request.GET.items() if k not in keys} get_params["date"] = "%s-W%s" % (request.GET.get("year"), request.GET.get("week")) - return redirect(self.request.path + "?" + urlencode(get_params)) + return redirect_to_url(self.request.path + "?" + urlencode(get_params)) self._set_week_year() return super().get(request, *args, **kwargs) @@ -1230,6 +1230,6 @@ class OrganizerIcalDownload(OrganizerViewMixin, View): class OrganizerFavicon(View): def get(self, *args, **kwargs): if self.request.organizer.settings.favicon: - return redirect(get_thumbnail(self.request.organizer.settings.favicon, '32x32^').thumb.url) + return redirect_to_url(get_thumbnail(self.request.organizer.settings.favicon, '32x32^').thumb.url) else: - return redirect(static("pretixbase/img/favicon.ico")) + return redirect_to_url(static("pretixbase/img/favicon.ico")) diff --git a/src/pretix/presale/views/user.py b/src/pretix/presale/views/user.py index 3543bf4205..926be4786f 100644 --- a/src/pretix/presale/views/user.py +++ b/src/pretix/presale/views/user.py @@ -36,7 +36,6 @@ import logging from django.conf import settings from django.contrib import messages -from django.shortcuts import redirect from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views import View @@ -44,6 +43,7 @@ from django.views.generic import TemplateView from pretix.base.email import get_email_context from pretix.base.services.mail import INVALID_ADDRESS, SendMailException, mail +from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.presale.forms.user import ResendLinkForm from pretix.presale.views import EventViewMixin @@ -71,7 +71,7 @@ class ResendLinkView(EventViewMixin, TemplateView): 'already sent you an email with a link to your ticket in the past {number} hours. ' 'If the email did not arrive, please check your spam folder and also double check ' 'that you used the correct email address.').format(number=24)) - return redirect(eventreverse(self.request.event, 'presale:event.resend_link')) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link')) else: rc.setex('pretix_resend_{}_{}'.format(request.event.pk, user), 3600 * 24, '1') @@ -92,7 +92,7 @@ class ResendLinkView(EventViewMixin, TemplateView): return self.get(request, *args, **kwargs) messages.success(self.request, _('If there were any orders by this user, they will receive an email with their order codes.')) - return redirect(eventreverse(self.request.event, 'presale:event.index')) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -107,4 +107,4 @@ class UnlockHashView(EventViewMixin, View): hashes = request.session.get('pretix_unlock_hashes', []) hashes.append(kwargs.get('hash')) request.session['pretix_unlock_hashes'] = hashes - return redirect(eventreverse(self.request.event, 'presale:event.index')) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))