Compare commits

..
Author SHA1 Message Date
Raphael Michel e2b28dd81e Bump to 2026.6.3 2026-09-29 20:43:41 +02:00
Raphael Michel 169110e741 Hotfix: Perform validation on the selected step, not on all steps before 2026-09-29 20:43:33 +02:00
Raphael Michel 91d7459ecd Bump to 2026.6.2 2026-09-29 14:23:42 +02:00
Raphael Michel 686e4d54b1 Fix tests after hierarkey update 2026-09-29 13:52:12 +02:00
Raphael Michel 6687d475f2 Bump hierarkey to 2.0.2 2026-09-29 13:52:11 +02:00
Raphael Michel 7ef22971c6 [SECURITY] API: Fix session validation for uploaded files (CVE-2026-101269, Z#23247174) 2026-09-29 13:40:38 +02:00
Mira Weller d2ba4b7733 Block out of bounds image crop dimensions (Z#23245937 / PRT-009) 2026-09-29 13:40:18 +02:00
Mira Weller 81e004e6c8 Prevent parsing non-standard-compliant JSON float values (Z#23245937 / PRT-021) 2026-09-29 13:40:18 +02:00
Mira Weller d8fe665798 Fix potential infinite loop in pdf render (Z#23245937 / PRT-021) 2026-09-29 13:40:18 +02:00
Mira Weller b7117bb3a3 Fix inefficient loop in compute_validity (Z#23245937 / PRT-013) 2026-09-29 13:40:18 +02:00
Raphael Michel 912851aa87 [SECURITY] OAuth: Disable existing tokens when deactivating Application (CVE-2026-101271, Z#23247296) 2026-09-29 13:40:18 +02:00
Mira Weller 5440508be0 [SECURITY] Escape help texts (CVE-2026-101270) 2026-09-29 13:40:17 +02:00
Raphael Michel 7025159f6b [SECURITY] Fix customer session fixation on cross-domain login (CVE-2026-101268, Z#23247268) 2026-09-29 13:39:39 +02:00
Raphael Michel 42cedc7306 [SECURITY] Fix information leak in widgets.json on dashboard (CVE-2026-101267, Z#23247172)
Thanks to Wenhao Wu, Southeast University
2026-09-29 13:39:38 +02:00
Raphael Michel 11ac2cc91c [SECURITY] Fix checkout validation bypass (CVE-2026-101266, Z#23245008) 2026-09-29 13:38:58 +02:00
Raphael Michel 2bd0a341d6 Bump to 2026.6.1 2026-07-28 12:25:38 +02:00
Raphael Michel 6bec15d12a [SECURITY] Add missing permission check for view (CVE-2026-57532) 2026-07-28 12:25:31 +02:00
Raphael Michel fd565ecdb2 Bump version to 2026.6.0 2026-07-01 16:33:07 +02:00
Nikita Mitasov f35b13b686 Translations: Update Russian
Currently translated at 18.9% (1200 of 6343 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ru/

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-es 550bb675f5 Translations: Update Spanish
Currently translated at 100.0% (260 of 260 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/es/

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-es adc9c9d514 Translations: Update Spanish
Currently translated at 100.0% (6343 of 6343 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-es 8441c4bc7a Translations: Update French
Currently translated at 100.0% (260 of 260 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/fr/

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-es 97ff252c09 Translations: Update French
Currently translated at 100.0% (6343 of 6343 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/fr/

powered by weblate
2026-07-01 16:32:06 +02:00
CVZ-es d3ca2ac1e5 Translations: Update German
Currently translated at 100.0% (6343 of 6343 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2026-07-01 16:32:06 +02:00
Raphael MichelandMira Weller 6eebaaa563 [SECURITY] Hardening for user impersonation feature (CVE-2026-13602)
---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 15:15:43 +02:00
Raphael MichelandMira Weller c9781f012b [SECURITY] Centralize framebreaking logic from payment plugins to core (CVE-2026-13602)
- Add central framebreaker page via safelink helper
- Update paypal, paypal2 and stripe plugins to use central framebreaker
- Add CSP header to cookies.html

---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 15:15:43 +02:00
Mira WellerandRaphael Michel 000bf54105 [SECURITY] Allowlisting and changed salts for safelink and safelink_callback (CVE-2026-13602)
---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-01 15:15:43 +02:00
Lukas Bockstaller e42d3d632f filter out the 404 log records from django.request (#6324) 2026-07-01 11:33:04 +02:00
Lukas Bockstaller 3bf5a5e478 include settings attribute during type checking (#6323)
* include settings attribute during type checking

* isort
2026-07-01 11:32:54 +02:00
Raphael Michel a6f31df0d4 Do not assign domain across organizers when copying events 2026-06-30 18:51:49 +02:00
70 changed files with 623 additions and 587 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ dependencies = [
"django-filter==25.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.6.1",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hierarkey==2.0.*,>=2.0.2",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
"django-libsass==0.9",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.6.0.dev0"
__version__ = "2026.6.3"
+9
View File
@@ -0,0 +1,9 @@
def get_session_key_for_api_auth(user, auth):
if user.is_authenticated:
return f'api-upload-User-{user.pk}'
else:
return f'api-upload-{str(type(auth))}-{auth.pk}'
def get_session_key_for_api_request(request):
return get_session_key_for_api_auth(request.user, request.auth)
+4
View File
@@ -101,6 +101,10 @@ class OAuthAccessToken(AbstractAccessToken):
self.expires = now() - timedelta(hours=1)
self.save(update_fields=['expires'])
def is_valid(self, scopes=None):
# Can maybe be removed after upgrading django-oauth-toolkit to 3.4.1
return super().is_valid(scopes) and self.application.is_usable(None)
class OAuthRefreshToken(AbstractRefreshToken):
application = models.ForeignKey(
+9 -1
View File
@@ -37,6 +37,8 @@ from collections import OrderedDict
from django.core.exceptions import ValidationError
from rest_framework import serializers
from pretix.api.auth.utils import get_session_key_for_api_request
def remove_duplicates_from_list(data):
return list(OrderedDict.fromkeys(data))
@@ -83,10 +85,16 @@ class UploadedFileField(serializers.Field):
request = self.context.get('request', None)
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}',
file__isnull=False,
pk=data[len("file:"):],
)
if cf.session_key == "api-upload-<class 'django.contrib.auth.models.AnonymousUser'>-None":
# OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should
# also be harmless because all files with this key are expired one day after deployment of this fix
# and no new files with this key are created
pass
elif cf.session_key != get_session_key_for_api_request(request):
self.fail('not_found')
except (ValidationError, IndexError): # invalid uuid
self.fail('not_found')
except CachedFile.DoesNotExist:
+10 -4
View File
@@ -41,6 +41,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.auth.utils import get_session_key_for_api_request
from pretix.api.serializers import CompatibleJSONField
from pretix.api.serializers.event import SubEventSerializer
from pretix.api.serializers.forms import form_field_to_serializer_field
@@ -257,16 +258,21 @@ class AnswerSerializer(I18nAwareModelSerializer):
if data['answer'] == 'file:keep':
return data
try:
ao = self.context["request"].user or self.context["request"].auth
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(ao))}-{ao.pk}',
file__isnull=False,
pk=data['answer'][len("file:"):],
)
if cf.session_key == "api-upload-<class 'django.contrib.auth.models.AnonymousUser'>-None":
# OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should
# also be harmless because all files with this key are expired one day after deployment of this fix
# and no new files with this key are created
pass
elif cf.session_key != get_session_key_for_api_request(self.context["request"]):
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer']))
except (ValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer']))
except CachedFile.DoesNotExist:
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer']))
allowed_types = (
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
+8 -1
View File
@@ -50,6 +50,7 @@ from rest_framework.generics import ListAPIView
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from pretix.api.auth.utils import get_session_key_for_api_auth
from pretix.api.serializers.checkin import (
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
@@ -329,10 +330,16 @@ with scopes_disabled():
def _handle_file_upload(data, user, auth):
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}',
file__isnull=False,
pk=data[len("file:"):],
)
if cf.session_key == "api-upload-<class 'django.contrib.auth.models.AnonymousUser'>-None":
# OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should
# also be harmless because all files with this key are expired one day after deployment of this fix
# and no new files with this key are created
pass
elif cf.session_key != get_session_key_for_api_auth(user, auth):
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except (ValidationError, BaseValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except CachedFile.DoesNotExist:
+2 -1
View File
@@ -33,6 +33,7 @@ from rest_framework.views import APIView
from pretix.api.auth.device import DeviceTokenAuthentication
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
from pretix.api.auth.token import TeamTokenAuthentication
from pretix.api.auth.utils import get_session_key_for_api_request
from pretix.base.models import CachedFile
from pretix.helpers.images import (
IMAGE_TYPES, validate_uploaded_file_for_valid_image,
@@ -78,7 +79,7 @@ class UploadView(APIView):
web_download=False,
filename=file_obj.name,
type=content_type,
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
session_key=get_session_key_for_api_request(request)
)
cf.file.save(file_obj.name, file_obj)
cf.save()
+19 -21
View File
@@ -53,7 +53,6 @@ from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
@@ -325,21 +324,16 @@ class WrappedPhonePrefixSelect(Select):
initial = None
def __init__(self, initial=None):
def _get_choices():
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
return choices
choices = lazy(_get_choices, list)()
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code',
@@ -593,12 +587,16 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
image = ImageOps.exif_transpose(image)
if f._cropdata:
image = image.crop((
f._cropdata.get('x', 0),
f._cropdata.get('y', 0),
f._cropdata.get('x', 0) + f._cropdata.get('width', image.width),
f._cropdata.get('y', 0) + f._cropdata.get('height', image.height),
))
left = int(f._cropdata.get('x', 0))
top = int(f._cropdata.get('y', 0))
right = left + int(f._cropdata.get('width', image.width))
bottom = top + int(f._cropdata.get('height', image.height))
if left >= image.width or top >= image.height or right > image.width or bottom > image.height:
raise ValidationError(
self.error_messages['max_dimension'],
code='max_dimension',
)
image = image.crop((left, top, right, bottom))
with BytesIO() as output:
# This might use a lot of memory, but temporary files are not a good option since
# we don't control the cleanup
+8 -1
View File
@@ -40,6 +40,7 @@ import warnings
from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
from typing import TYPE_CHECKING
from urllib.parse import urljoin
from zoneinfo import ZoneInfo
@@ -79,10 +80,16 @@ from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
from .organizer import Organizer, Team
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
logger = logging.getLogger(__name__)
class EventMixin:
if TYPE_CHECKING:
settings: HierarkeyProxy
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
@@ -899,7 +906,7 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment'):
if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels:
+2 -4
View File
@@ -1070,10 +1070,8 @@ class Item(LoggedModel):
replace_year = valid_until.year
replace_month = valid_until.month + self.validity_dynamic_duration_months
while replace_month > 12:
replace_month -= 12
replace_year += 1
replace_year += (replace_month - 1) // 12
replace_month = ((replace_month - 1) % 12) + 1
max_day = calendar.monthrange(replace_year, replace_month)[1]
replace_date = date(
year=replace_year,
+7
View File
@@ -35,6 +35,7 @@ import operator
import string
from datetime import date, datetime, time
from functools import reduce
from typing import TYPE_CHECKING
import pytz_deprecation_shim
from django.conf import settings
@@ -61,6 +62,9 @@ from ...helpers.permission_migration import (
from ..settings import settings_hierarkey
from .auth import User
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
@settings_hierarkey.add(cache_namespace='organizer')
class Organizer(LoggedModel):
@@ -78,6 +82,9 @@ class Organizer(LoggedModel):
"""
settings_namespace = 'organizer'
if TYPE_CHECKING:
settings: HierarkeyProxy
name = models.CharField(max_length=200,
verbose_name=_("Name"))
slug = models.CharField(
+1 -1
View File
@@ -1074,7 +1074,7 @@ class Renderer:
fontsize = float(o['fontsize'])
height = float(o['height']) * mm
width = float(o['width']) * mm
while True:
for _i in range(25): # try adapting the font size at most 25 times
p, ad, lineheight = self._text_paragraph(op, order, o, override_fontsize=fontsize)
w, h = p.wrapOn(canvas, width, 1000 * mm)
widths = p.getActualLineWidths0()
@@ -0,0 +1,28 @@
{% extends "error.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
<h1>{% trans "Please continue in a new tab" %}</h1>
<p class="larger">
{% blocktrans trimmed %}
For security reasons, the following step is only possible in a new tab.
{% endblocktrans %}
</p>
<p class="larger">
{% blocktrans trimmed %}
If the new tab did not open automatically, please click the following button:
{% endblocktrans %}
</p>
<div class="text-center">
<a href="{{ url }}"
class="btn btn-primary btn-lg" target="_blank">
<span class="fa fa-external-link-square"></span>
{% trans "Continue in new tab" %}
</a>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% endblock %}
+2 -2
View File
@@ -54,6 +54,7 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map
register = template.Library()
@@ -158,8 +159,7 @@ def safelink_callback(attrs, new=False):
"""
url = html.unescape(attrs.get((None, 'href'), '/'))
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'href'] = safelink(url)
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
+29 -6
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import urllib.parse
from django.core import signing
@@ -26,6 +27,8 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request):
referer = request.headers.get('referer')
@@ -42,11 +45,16 @@ def _is_samesite_referer(request):
def redir_view(request):
signer = signing.Signer(salt='safe-redirect')
framebreak = "framebreak" in request.GET
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
try:
url = signer.unsign(request.GET.get('url', ''))
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
try:
# Backwards-compatibility for a change in 2026-06, remove after a while
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
@@ -55,11 +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):
signer = signing.Signer(salt='safe-redirect')
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(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'
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
+5 -4
View File
@@ -830,9 +830,10 @@ class CancelSettingsForm(SettingsForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.obj.settings.giftcard_expiry_years is not None:
self.fields['cancel_allow_user_paid_refund_as_giftcard'].help_text = gettext(
'You have configured gift cards to be valid {} years plus the year the gift card is issued in.'
).format(self.obj.settings.giftcard_expiry_years)
self.fields['cancel_allow_user_paid_refund_as_giftcard'].help_text = format_html(
gettext('You have configured gift cards to be valid {} years plus the year the gift card is issued in.'),
self.obj.settings.giftcard_expiry_years
)
class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
@@ -1673,7 +1674,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
choices=CountriesAndEUAndStates(),
required=False
)
address_type = forms.ChoiceField(
+2 -2
View File
@@ -22,7 +22,7 @@
from django import forms
from django.core.exceptions import ValidationError
from django.utils.functional import lazy
from django.utils.html import format_html
from django.utils.html import conditional_escape, format_html
from django.utils.translation import gettext_lazy as _
from pretix.base.modelimport_orders import get_order_import_columns
@@ -66,7 +66,7 @@ class ProcessForm(forms.Form):
widget=forms.Select(
attrs={'data-static': 'true'}
),
help_text=c.help_text,
help_text=conditional_escape(c.help_text),
)
def get_columns(self):
+2 -2
View File
@@ -364,7 +364,7 @@ class TeamForm(forms.ModelForm):
for opt in pg.options
],
label=pg.label,
help_text=pg.help_text,
help_text=conditional_escape(pg.help_text),
initial=initial,
widget=forms.RadioSelect,
)
@@ -389,7 +389,7 @@ class TeamForm(forms.ModelForm):
for opt in pg.options
],
label=pg.label,
help_text=pg.help_text,
help_text=conditional_escape(pg.help_text),
initial=initial,
widget=forms.RadioSelect,
)
+2 -1
View File
@@ -41,6 +41,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator
from django.db.models.functions import Upper
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
@@ -161,7 +162,7 @@ class VoucherForm(I18nModelForm):
required=False,
widget=forms.TextInput(attrs={'data-seat-guid-field': '1'}),
initial=self.instance.seat.seat_guid if self.instance.seat else '',
help_text=str(self.instance.seat) if self.instance.seat else '',
help_text=escape(str(self.instance.seat) if self.instance.seat else ''),
)
def clean(self):
+1 -1
View File
@@ -212,7 +212,7 @@ class AuditLogMiddleware:
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0])
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if ss:
ss.logs.create(
+5 -2
View File
@@ -414,9 +414,12 @@ def event_index_widgets_lazy(request, organizer, event):
except SubEvent.DoesNotExist:
pass
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
request=request)
widgets = []
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
if can_view_orders:
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
widgets.extend(result)
return JsonResponse({'widgets': widgets})
+1 -1
View File
@@ -1558,7 +1558,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
return ctx
class QuickSetupView(FormView):
class QuickSetupView(EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/event/quick_setup.html'
permission = 'event.settings.general:write'
form_class = QuickSetupForm
+29 -5
View File
@@ -19,19 +19,22 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import hmac
import json
from contextlib import contextmanager
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
logout,
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
@@ -230,7 +233,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
hijacked = self.object
hijack_history = request.session.get("hijack_history", [])
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
hijack_history.append({
"user": request.user.pk,
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
# unprivileged user to contain the risk if there is some leak of session data.
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
})
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
@@ -259,8 +270,21 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", [])
hijacked = request.user
user_pk = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
expected_hash = salted_hmac(
key_salt=b"hijack-history-hash",
value=hijacker.get_session_auth_hash(),
algorithm="sha256",
secret=settings.SECRET_KEY
).hexdigest()
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
# that happened during the hijack session
logout(request)
return redirect_to_login(request.get_full_path())
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session):
+7
View File
@@ -46,6 +46,13 @@ class RequestIdFilter(logging.Filter):
return True
class SkipNotFoundFilter(logging.Filter):
# Drop the WARNING "Not Found: ..." records django.request emits for 404s
# We have different access logs for that
def filter(self, record):
return getattr(record, 'status_code', None) != 404
class RequestIdMiddleware:
def __init__(self, get_response):
self.get_response = get_response
+8
View File
@@ -251,6 +251,13 @@ def monkeypatch_reportlab_imagereader():
utils.ImageReader.__init__ = new_init
def monkeypatch_json_constants():
from json.decoder import _CONSTANTS # noqa
del _CONSTANTS['-Infinity']
del _CONSTANTS['Infinity']
del _CONSTANTS['NaN']
def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
@@ -258,3 +265,4 @@ def monkeypatch_all_at_ready():
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
monkeypatch_json_constants()
+2 -2
View File
@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-28 15:19+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
+71 -112
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.5\n"
"X-Generator: Weblate 2026.6.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -467,10 +467,8 @@ msgid "Medium connected to other event"
msgstr "Medio conectado a otro evento"
#: pretix/api/views/checkin.py:814
#, fuzzy
#| msgid "You cannot change this order."
msgid "You cannot exchange a medium for a medium."
msgstr "No puedes cambiar este pedido."
msgstr "No se puede cambiar un medio por otro."
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
#, python-brace-format
@@ -4730,22 +4728,16 @@ msgid "Check-in annulled"
msgstr "Check-in anulado"
#: pretix/base/models/checkin.py:372
#, fuzzy
#| msgid "Ticket already used"
msgid "Ticket already exchanged"
msgstr "Esta entrada ya fue utilizada"
msgstr "Entrada ya canjeada"
#: pretix/base/models/checkin.py:373
#, fuzzy
#| msgid "Reusable media"
msgid "Reusable medium invalid"
msgstr "Medios reutilizables"
msgstr "Soporte reutilizable no válido"
#: pretix/base/models/checkin.py:374
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists"
msgstr "Tipo de medio reusable"
msgstr "El soporte reutilizable ya existe"
#: pretix/base/models/customers.py:63
msgid "Provider name"
@@ -5330,11 +5322,8 @@ msgstr ""
"ambas cosas."
#: pretix/base/models/event.py:1884
#, fuzzy
#| msgid "The bundled item must belong to the same event as the item."
msgid "Property and event must belong to the same organizer."
msgstr ""
"La agrupación de artículos debe pertenecer al mismo evento que el artículo."
msgstr "La propiedad y el evento deben pertenecer al mismo organizador."
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
msgid "Link text"
@@ -5586,42 +5575,41 @@ msgid "Show product with info on why it’s unavailable"
msgstr "Mostrar el producto con la razón que no está disponible"
#: pretix/base/models/items.py:458 pretix/base/models/items.py:786
#, fuzzy
#| msgid "Don't use re-usable media, use regular one-off tickets"
msgid "Don't use reusable media, use regular one-off tickets"
msgstr "No usar medios reutilizables, usar entradas de un solo uso"
msgstr ""
"No utilizar soportes reutilizables, sino billetes normales de un solo uso"
#: pretix/base/models/items.py:459
msgid "Require a previously unknown medium to be newly added"
msgstr "Exigir que un medio desconocido anteriormente se adicione de nuevo"
#: pretix/base/models/items.py:460
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, replacing any previous tickets"
msgstr "Exigir que se reuse un medio ya existente"
msgstr ""
"Necesita que se reutilice un soporte ya existente, sustituyendo cualquier "
"billete anterior"
#: pretix/base/models/items.py:461
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, replacing any "
"previous tickets"
msgstr "Exigir que se use un medio existente o uno nuevo"
msgstr ""
"Se debe utilizar un soporte ya existente o uno nuevo, sustituyendo cualquier "
"billete anterior"
#: pretix/base/models/items.py:462
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, adding to any previous tickets"
msgstr "Exigir que se reuse un medio ya existente"
msgstr ""
"Exigir que se reutilice un soporte ya existente, añadiendo esta información "
"a cualquier entrada anterior"
#: pretix/base/models/items.py:464
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, adding to any "
"previous tickets"
msgstr "Exigir que se use un medio existente o uno nuevo"
msgstr ""
"Es necesario utilizar un medio ya existente o uno nuevo, que se sumará a los "
"billetes anteriores"
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
msgid "Category"
@@ -5981,14 +5969,6 @@ msgid "Reusable media policy"
msgstr "Condiciones de utilización de medios"
#: pretix/base/models/items.py:777
#, fuzzy
#| msgid ""
#| "If this product should be stored on a re-usable physical medium, you can "
#| "attach a physical media policy. This is not required for regular tickets, "
#| "which just use a one-time barcode, but only for products like renewable "
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
#| "feature that also requires specific configuration of ticketing and "
#| "printing settings."
msgid ""
"If this product should be stored on a reusable physical medium, you can "
"attach a physical media policy. This is not required for regular tickets, "
@@ -6052,6 +6032,9 @@ msgid ""
"prior to their usage. Therefore, the selected media policy does not make "
"sense for this media type."
msgstr ""
"El tipo de soporte seleccionado requiere que todos los soportes se registren "
"en el sistema antes de su uso. Por lo tanto, la política de soportes "
"seleccionada no es aplicable a este tipo de soporte."
#: pretix/base/models/items.py:1009
msgid ""
@@ -6626,18 +6609,16 @@ msgstr "rebotado"
#: pretix/base/models/media.py:77
msgctxt "reusable_medium"
msgid "Claim token"
msgstr ""
msgstr "Canjear el token"
#: pretix/base/models/media.py:82
msgctxt "reusable_medium"
msgid "Label"
msgstr ""
msgstr "Designación"
#: pretix/base/models/media.py:105
#, fuzzy
#| msgid "Linked ticket"
msgid "Linked tickets"
msgstr "Entrada vinculada"
msgstr "Entradas vinculadas"
#: pretix/base/models/media.py:107
msgid ""
@@ -6645,6 +6626,9 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Si enlaza más de un billete, asegúrese de que no haya solapamiento en la "
"validez. Si varios billetes son válidos al mismo tiempo, esto provocará que "
"no se puedan realizar el check-in."
#: pretix/base/models/memberships.py:44
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html:28
@@ -8390,14 +8374,10 @@ msgid "Atlantis"
msgstr "Atlántida"
#: pretix/base/pdf.py:376
#, fuzzy
#| msgid "Invoice recipient email"
msgid "Invoice custom recipient field"
msgstr "Correo electrónico del destinatario de la factura"
msgstr "Campo personalizado de destinatario en la factura"
#: pretix/base/pdf.py:377
#, fuzzy
#| msgid "Custom recipient field label"
msgid "Custom recipient field"
msgstr "Campo de destinatario personalizado"
@@ -9415,13 +9395,15 @@ msgstr "Necesitas responder preguntas para terminar el check-in."
#: pretix/base/services/checkin.py:1121
msgid "Ticket needs to be exchanged to a suitable medium."
msgstr ""
msgstr "El billete debe canjearse por un soporte adecuado."
#: pretix/base/services/checkin.py:1128
msgid ""
"This ticket has already been exchanged for a reusable medium that now needs "
"to be used instead."
msgstr ""
"Esta entrada ya se ha canjeado por un soporte reutilizable que ahora hay que "
"utilizar en su lugar."
#: pretix/base/services/checkin.py:1180
msgid "This ticket has already been redeemed."
@@ -9587,64 +9569,46 @@ msgstr ""
"{event}."
#: pretix/base/services/media.py:93 pretix/base/services/media.py:95
#, fuzzy
#| msgid "Invalid input type."
msgid "Invalid medium type."
msgstr "Tipo de entrada no válido."
msgstr "Tipo de soporte no válido."
#: pretix/base/services/media.py:100 pretix/base/services/media.py:102
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid "Medium type is not enabled for organizer."
msgstr ""
"El tipo de medio seleccionado no está activo en tus ajustes de organizador/a/"
"e."
msgstr "Este tipo de medio no está habilitado para el organizador."
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
msgid "Incorrect medium type for product."
msgstr ""
msgstr "El tipo de soporte no es el adecuado para este producto."
#: pretix/base/services/media.py:114 pretix/base/services/media.py:116
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "Ticket is already exchanged for reusable medium."
msgstr "Esta entrada ya ha sido canjeada."
msgstr "La entrada ya se ha canjeado por un soporte reutilizable."
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
#, fuzzy
#| msgid "Reusable Medium ID"
msgid "Reusable medium not found."
msgstr "ID mediana reutilizable"
msgstr "No se ha encontrado el soporte reutilizable."
#: pretix/base/services/media.py:140 pretix/base/services/media.py:142
#: pretix/base/services/media.py:168 pretix/base/services/media.py:170
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium is inactive or expired."
msgstr "Se ha creado el medio reutilizable."
msgstr "El medio reutilizable está inactivo o caducado."
#: pretix/base/services/media.py:155 pretix/base/services/media.py:162
#: pretix/base/services/media.py:176
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium not found and could not be created."
msgstr "Se ha creado el medio reutilizable."
msgstr "No se ha encontrado el soporte reutilizable y no se ha podido crear."
#: pretix/base/services/media.py:183
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists."
msgstr "Tipo de medio reusable"
msgstr "Ya existe un soporte reutilizable."
#: pretix/base/services/media.py:189
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium could not be created."
msgstr "Se ha creado el medio reutilizable."
msgstr "No se ha podido crear el soporte reutilizable."
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
msgid "Product does not support medium exchange."
msgstr ""
msgstr "Este producto no admite el cambio de medio."
#: pretix/base/services/memberships.py:108
#, python-brace-format
@@ -10368,17 +10332,10 @@ msgstr ""
"inició sesión durante la compra."
#: pretix/base/settings.py:214
#, fuzzy
#| msgid "Activate re-usable media"
msgid "Activate reusable media"
msgstr "Activar medios reutilizables"
#: pretix/base/settings.py:215
#, fuzzy
#| msgid ""
#| "The re-usable media feature allows you to connect tickets and gift cards "
#| "with physical media such as wristbands or chip cards that may be re-used "
#| "for different tickets or gift cards later."
msgid ""
"The reusable media feature allows you to connect tickets and gift cards with "
"physical media such as wristbands or chip cards that may be reused for "
@@ -10390,7 +10347,7 @@ msgstr ""
#: pretix/base/settings.py:226
msgid "Enforce the usage of issued reusable media for check-in"
msgstr ""
msgstr "Exigir el uso de los soportes reutilizables para el check-in"
#: pretix/base/settings.py:227
msgid ""
@@ -10398,6 +10355,10 @@ msgid ""
"medium has been created and linked to a ticket. Keeping this option turned "
"off will treat the reusable medium and ticket as equals."
msgstr ""
"Si se activa esta opción, ya no se aceptarán los códigos de barras de los "
"billetes cuando se haya creado un soporte reutilizable y se haya vinculado a "
"un billete. Si se mantiene desactivada esta opción, el soporte reutilizable "
"y el billete se tratarán como iguales."
#: pretix/base/settings.py:254
msgid "Length of barcodes"
@@ -18548,16 +18509,12 @@ msgid "The reusable medium has been changed."
msgstr "El medio reutilizable ha sido modificado."
#: pretix/control/logdisplay.py:746
#, fuzzy
#| msgid "The new member has been added to the team."
msgid "A new ticket has been added to the medium."
msgstr "El nuevo miembro ha sido añadido al equipo."
msgstr "Se ha añadido un nuevo billete al medio."
#: pretix/control/logdisplay.py:747
#, fuzzy
#| msgid "{user} has been removed from the team."
msgid "A ticket has been removed from the medium."
msgstr "{user} ha sido removido del equipo."
msgstr "Se ha eliminado un billete del medio."
#: pretix/control/logdisplay.py:748
msgid "The medium has been connected to a new ticket."
@@ -18569,6 +18526,8 @@ msgid ""
"The ticket #{positionid} was exchanged for reusable medium "
"{medium_identifier}."
msgstr ""
"El billete n.º {positionid} se ha canjeado por un medio reutilizable "
"{medium_identifier}."
#: pretix/control/logdisplay.py:750
msgid "The medium has been connected to a new gift card."
@@ -20483,10 +20442,8 @@ msgstr ""
"check-in:"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
#, fuzzy
#| msgid "Special attention required"
msgid "Media exchange required"
msgstr "Atención especial requerida"
msgstr "Es necesario intercambiar medios"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
#, python-format
@@ -20494,6 +20451,8 @@ msgid ""
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
"reusable medium. <strong>%(media_policy)s</strong>."
msgstr ""
"Esta entrada debe canjearse por un soporte reutilizable <strong>%(media_type)"
"s</strong>. <strong>%(media_policy)s</strong>."
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:103
msgid "Special attention required"
@@ -27038,6 +26997,9 @@ msgid ""
"Even if a team has no access to a certain category of data, they might still "
"be able to see parts of this data when it is linked to data they can see."
msgstr ""
"Aunque un equipo no tenga acceso a una determinada categoría de datos, es "
"posible que pueda ver parte de esos datos cuando estén vinculados a datos a "
"los que sí tiene acceso."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:35
msgid ""
@@ -27045,6 +27007,10 @@ msgid ""
"some information about gift cards linked to a customer account, even if they "
"generally can't see gift cards directly."
msgstr ""
"Por ejemplo, una persona con acceso a las cuentas de los clientes podrá ver "
"cierta información sobre las tarjetas regalo vinculadas a una cuenta de "
"cliente, aunque, por lo general, no pueda ver las tarjetas regalo "
"directamente."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:59
msgid ""
@@ -27052,6 +27018,9 @@ msgid ""
"information about vouchers used to create an order, even if they generally "
"can't see vouchers directly."
msgstr ""
"Por ejemplo, una persona con acceso a los pedidos podrá ver cierta "
"información sobre los vales utilizados para crear un pedido, aunque "
"normalmente no pueda ver los vales directamente."
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:21
msgid "Member"
@@ -27912,30 +27881,22 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
#, fuzzy, python-format
#| msgid "Quota: %(name)s"
#, python-format
msgctxt "subevent"
msgid "Date: %(name)s"
msgstr "Cuota: %(name)s"
msgstr "Fecha: %(name)s"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
#, fuzzy
#| msgid "Partially paid"
msgid "partially canceled"
msgstr "Pagado parcialmente"
msgstr "cancelado parcialmente"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:282
#, fuzzy
#| msgctxt "permission_level"
#| msgid "View all"
msgid "View all"
msgstr "Ver todo"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:289
#, fuzzy
#| msgid "No archived events found."
msgid "No orders found."
msgstr "No se han encontrado eventos archivados."
msgstr "No se han encontrado pedidos."
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
@@ -30708,10 +30669,8 @@ msgid "Voucher {}"
msgstr "Vale de compra {}"
#: pretix/control/views/typeahead.py:179 pretix/control/views/typeahead.py:180
#, fuzzy
#| msgid "Go to event"
msgid "No event"
msgstr "Ir al evento"
msgstr "No hay eventos"
#: pretix/control/views/user.py:169
msgid "The password you entered was invalid, please try again."
+8 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 2026.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -442,11 +442,11 @@ msgstr "¡Presione Control+C para copiar!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr ""
msgstr "Editar"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr ""
msgstr "Visualizar"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
@@ -454,10 +454,13 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"Su regla siempre filtra por producto o variante, pero los siguientes "
"productos o variantes no figuran en ninguna de las partes de su regla, por "
"lo que las personas con estos tickets no podrán acceder:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr ""
msgstr "Por favor, comprueba bien si esto ha sido a propósito."
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
+71 -111
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-08 17:00+0000\n"
"Last-Translator: Sébastien BRUNEAU <s.bruneau@beauvaisis.fr>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -466,10 +466,8 @@ msgid "Medium connected to other event"
msgstr "Média connecté à un autre événement"
#: pretix/api/views/checkin.py:814
#, fuzzy
#| msgid "You cannot change this order."
msgid "You cannot exchange a medium for a medium."
msgstr "Vous ne pouvez pas modifier cette commande."
msgstr "Il n'est pas possible d'échanger un support contre un autre support."
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
#, python-brace-format
@@ -4736,22 +4734,16 @@ msgid "Check-in annulled"
msgstr "Enregistrement annulé"
#: pretix/base/models/checkin.py:372
#, fuzzy
#| msgid "Ticket already used"
msgid "Ticket already exchanged"
msgstr "Billet déjà utilisé"
msgstr "Billet déjà échangé"
#: pretix/base/models/checkin.py:373
#, fuzzy
#| msgid "Reusable media"
msgid "Reusable medium invalid"
msgstr "Support réutilisable"
msgstr "Support réutilisable non valide"
#: pretix/base/models/checkin.py:374
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists"
msgstr "Type de support réutilisable"
msgstr "Il existe déjà un support réutilisable"
#: pretix/base/models/customers.py:63
msgid "Provider name"
@@ -5343,10 +5335,8 @@ msgstr ""
"deux."
#: pretix/base/models/event.py:1884
#, fuzzy
#| msgid "The bundled item must belong to the same event as the item."
msgid "Property and event must belong to the same organizer."
msgstr "L’élément groupé doit appartenir au même événement que l’élément."
msgstr "La propriété et l'événement doivent appartenir au même organisateur."
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
msgid "Link text"
@@ -5601,8 +5591,6 @@ msgstr ""
"indisponibilité"
#: pretix/base/models/items.py:458 pretix/base/models/items.py:786
#, fuzzy
#| msgid "Don't use re-usable media, use regular one-off tickets"
msgid "Don't use reusable media, use regular one-off tickets"
msgstr ""
"N'utilisez pas de supports réutilisables, mais plutôt des tickets uniques "
@@ -5613,32 +5601,30 @@ msgid "Require a previously unknown medium to be newly added"
msgstr "Exiger l'ajout d'un support inconnu jusqu'alors"
#: pretix/base/models/items.py:460
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, replacing any previous tickets"
msgstr "Exiger la réutilisation d'un support existant"
#: pretix/base/models/items.py:461
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, replacing any "
"previous tickets"
msgstr "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
msgstr ""
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
"remplacement de tout billet antérieur"
#: pretix/base/models/items.py:462
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, adding to any previous tickets"
msgstr "Exiger la réutilisation d'un support existant"
msgstr ""
"Exiger la réutilisation d'un support existant, en ajoutant cette demande à "
"tout ticket précédent"
#: pretix/base/models/items.py:464
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, adding to any "
"previous tickets"
msgstr "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
msgstr ""
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
"complément des tickets précédents"
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
msgid "Category"
@@ -6001,14 +5987,6 @@ msgid "Reusable media policy"
msgstr "Politique relative aux médias réutilisables"
#: pretix/base/models/items.py:777
#, fuzzy
#| msgid ""
#| "If this product should be stored on a re-usable physical medium, you can "
#| "attach a physical media policy. This is not required for regular tickets, "
#| "which just use a one-time barcode, but only for products like renewable "
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
#| "feature that also requires specific configuration of ticketing and "
#| "printing settings."
msgid ""
"If this product should be stored on a reusable physical medium, you can "
"attach a physical media policy. This is not required for regular tickets, "
@@ -6073,6 +6051,10 @@ msgid ""
"prior to their usage. Therefore, the selected media policy does not make "
"sense for this media type."
msgstr ""
"Le type de support sélectionné exige que tous les supports soient "
"enregistrés dans le système avant leur utilisation. Par conséquent, la "
"politique relative aux supports sélectionnée n'est pas applicable à ce type "
"de support."
#: pretix/base/models/items.py:1009
msgid ""
@@ -6649,18 +6631,16 @@ msgstr "Non distribué"
#: pretix/base/models/media.py:77
msgctxt "reusable_medium"
msgid "Claim token"
msgstr ""
msgstr "Réclamer un jeton"
#: pretix/base/models/media.py:82
msgctxt "reusable_medium"
msgid "Label"
msgstr ""
msgstr "Descriptif"
#: pretix/base/models/media.py:105
#, fuzzy
#| msgid "Linked ticket"
msgid "Linked tickets"
msgstr "Billet lié"
msgstr "Billets liés"
#: pretix/base/models/media.py:107
msgid ""
@@ -6668,6 +6648,9 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Si vous associez plusieurs billets, assurez-vous qu'il n'y ait pas de "
"chevauchement entre leurs périodes de validité. Si plusieurs billets sont "
"valables en même temps, cela entraînera l'échec de l'enregistrement."
#: pretix/base/models/memberships.py:44
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html:28
@@ -8437,16 +8420,12 @@ msgid "Atlantis"
msgstr "Atlantide"
#: pretix/base/pdf.py:376
#, fuzzy
#| msgid "Invoice recipient email"
msgid "Invoice custom recipient field"
msgstr "E-mail du destinataire de la facture"
msgstr "Champ personnalisé destinataire de la facture"
#: pretix/base/pdf.py:377
#, fuzzy
#| msgid "Custom recipient field label"
msgid "Custom recipient field"
msgstr "Libellé personnalisé du champ destinataire"
msgstr "Champ de destinataire personnalisé"
#: pretix/base/pdf.py:381
msgid "List of Add-Ons"
@@ -9471,13 +9450,15 @@ msgstr ""
#: pretix/base/services/checkin.py:1121
msgid "Ticket needs to be exchanged to a suitable medium."
msgstr ""
msgstr "Le billet doit être échangé contre un support adapté."
#: pretix/base/services/checkin.py:1128
msgid ""
"This ticket has already been exchanged for a reusable medium that now needs "
"to be used instead."
msgstr ""
"Ce billet a déjà été échangé contre un support réutilisable qui doit "
"désormais être utilisé à sa place."
#: pretix/base/services/checkin.py:1180
msgid "This ticket has already been redeemed."
@@ -9643,64 +9624,46 @@ msgstr ""
"Vous recevez cet e-mail parce que vous avez passé une commande pour {event}."
#: pretix/base/services/media.py:93 pretix/base/services/media.py:95
#, fuzzy
#| msgid "Invalid input type."
msgid "Invalid medium type."
msgstr "Type d’entrée non valide."
msgstr "Type de support non valide."
#: pretix/base/services/media.py:100 pretix/base/services/media.py:102
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid "Medium type is not enabled for organizer."
msgstr ""
"Le type de média sélectionné n’est pas activé dans les paramètres de votre "
"organisateur."
msgstr "Ce type de média n’est pas activé par l'organisateur."
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
msgid "Incorrect medium type for product."
msgstr ""
msgstr "Type de support incorrect pour ce produit."
#: pretix/base/services/media.py:114 pretix/base/services/media.py:116
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "Ticket is already exchanged for reusable medium."
msgstr "Ce billet a déjà été échangé."
msgstr "Le billet a déjà été échangé contre un support réutilisable."
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
#, fuzzy
#| msgid "Reusable Medium ID"
msgid "Reusable medium not found."
msgstr "Identification de support réutilisable"
msgstr "Support réutilisable introuvable."
#: pretix/base/services/media.py:140 pretix/base/services/media.py:142
#: pretix/base/services/media.py:168 pretix/base/services/media.py:170
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium is inactive or expired."
msgstr "Le support réutilisable a été créé."
msgstr "Le support réutilisable est inactif ou a expiré."
#: pretix/base/services/media.py:155 pretix/base/services/media.py:162
#: pretix/base/services/media.py:176
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium not found and could not be created."
msgstr "Le support réutilisable a été créé."
msgstr "Support réutilisable introuvable et impossible à créer."
#: pretix/base/services/media.py:183
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists."
msgstr "Type de support réutilisable"
msgstr "Le type de support réutilisable existe déjà."
#: pretix/base/services/media.py:189
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium could not be created."
msgstr "Le support réutilisable a été créé."
msgstr "Impossible de créer le support réutilisable."
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
msgid "Product does not support medium exchange."
msgstr ""
msgstr "Ce produit ne permet pas de changer de support."
#: pretix/base/services/memberships.py:108
#, python-brace-format
@@ -10426,17 +10389,10 @@ msgstr ""
"l’achat."
#: pretix/base/settings.py:214
#, fuzzy
#| msgid "Activate re-usable media"
msgid "Activate reusable media"
msgstr "Activer les supports réutilisables"
#: pretix/base/settings.py:215
#, fuzzy
#| msgid ""
#| "The re-usable media feature allows you to connect tickets and gift cards "
#| "with physical media such as wristbands or chip cards that may be re-used "
#| "for different tickets or gift cards later."
msgid ""
"The reusable media feature allows you to connect tickets and gift cards with "
"physical media such as wristbands or chip cards that may be reused for "
@@ -10450,6 +10406,8 @@ msgstr ""
#: pretix/base/settings.py:226
msgid "Enforce the usage of issued reusable media for check-in"
msgstr ""
"Imposer l'utilisation de supports réutilisables fournis lors de "
"l'enregistrement"
#: pretix/base/settings.py:227
msgid ""
@@ -10457,6 +10415,10 @@ msgid ""
"medium has been created and linked to a ticket. Keeping this option turned "
"off will treat the reusable medium and ticket as equals."
msgstr ""
"Si cette option est activée, le code-barres d'un billet ne sera plus accepté "
"dès lors qu'un support réutilisable a été créé et associé à ce billet. Si "
"cette option reste désactivée, le support réutilisable et le billet seront "
"considérés comme équivalents."
#: pretix/base/settings.py:254
msgid "Length of barcodes"
@@ -18691,16 +18653,12 @@ msgid "The reusable medium has been changed."
msgstr "Le support réutilisable a été changé."
#: pretix/control/logdisplay.py:746
#, fuzzy
#| msgid "The new member has been added to the team."
msgid "A new ticket has been added to the medium."
msgstr "Le nouveau membre a été ajouté à l'équipe."
msgstr "Un nouveau billet a été ajouté sur le support."
#: pretix/control/logdisplay.py:747
#, fuzzy
#| msgid "{user} has been removed from the team."
msgid "A ticket has been removed from the medium."
msgstr "{user} a été retiré de l'équipe."
msgstr "Un billet a été retiré du support."
#: pretix/control/logdisplay.py:748
msgid "The medium has been connected to a new ticket."
@@ -18712,6 +18670,8 @@ msgid ""
"The ticket #{positionid} was exchanged for reusable medium "
"{medium_identifier}."
msgstr ""
"Le ticket n° {positionid} a été échangé contre un support réutilisable "
"{medium_identifier}."
#: pretix/control/logdisplay.py:750
msgid "The medium has been connected to a new gift card."
@@ -20627,10 +20587,8 @@ msgstr ""
"l’enregistrement :"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
#, fuzzy
#| msgid "Special attention required"
msgid "Media exchange required"
msgstr "Une attention particulière est requise"
msgstr "Échange de supports requis"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
#, python-format
@@ -20638,6 +20596,8 @@ msgid ""
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
"reusable medium. <strong>%(media_policy)s</strong>."
msgstr ""
"Ce billet doit être échangé contre un support réutilisable <strong>%"
"(media_type)s</strong>. <strong>%(media_policy)s</strong>."
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:103
msgid "Special attention required"
@@ -27232,6 +27192,9 @@ msgid ""
"Even if a team has no access to a certain category of data, they might still "
"be able to see parts of this data when it is linked to data they can see."
msgstr ""
"Même si une équipe n'a pas accès à une certaine catégorie de données, elle "
"peut néanmoins être en mesure de consulter certaines parties de ces données "
"lorsque celles-ci sont liées à des données auxquelles elle a accès."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:35
msgid ""
@@ -27239,6 +27202,10 @@ msgid ""
"some information about gift cards linked to a customer account, even if they "
"generally can't see gift cards directly."
msgstr ""
"Par exemple, une personne ayant accès aux comptes clients pourra consulter "
"certaines informations concernant les cartes cadeaux associées à un compte "
"client, même si, en règle générale, elle ne peut pas voir directement ces "
"cartes cadeaux."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:59
msgid ""
@@ -27246,6 +27213,9 @@ msgid ""
"information about vouchers used to create an order, even if they generally "
"can't see vouchers directly."
msgstr ""
"Par exemple, une personne ayant accès aux commandes pourra consulter "
"certaines informations concernant les bons utilisés pour créer une commande, "
"même si, en règle générale, elle ne peut pas consulter directement ces bons."
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:21
msgid "Member"
@@ -28117,30 +28087,22 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
#, fuzzy, python-format
#| msgid "Quota: %(name)s"
#, python-format
msgctxt "subevent"
msgid "Date: %(name)s"
msgstr "Quota : %(name)s"
msgstr "Date : %(name)s"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
#, fuzzy
#| msgid "Partially paid"
msgid "partially canceled"
msgstr "Partiellement payé"
msgstr "partiellement annulé"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:282
#, fuzzy
#| msgctxt "permission_level"
#| msgid "View all"
msgid "View all"
msgstr "Tout afficher"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:289
#, fuzzy
#| msgid "No archived events found."
msgid "No orders found."
msgstr "Aucun événement archivé trouvé."
msgstr "Aucune commande trouvée."
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
@@ -30938,10 +30900,8 @@ msgid "Voucher {}"
msgstr "Bon {}"
#: pretix/control/views/typeahead.py:179 pretix/control/views/typeahead.py:180
#, fuzzy
#| msgid "Go to event"
msgid "No event"
msgstr "Aller à l'événement"
msgstr "Aucun événement"
#: pretix/control/views/user.py:169
msgid "The password you entered was invalid, please try again."
+9 -5
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 2026.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -443,11 +443,11 @@ msgstr "Appuyez sur Ctrl-C pour copier !"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr ""
msgstr "Éditer"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr ""
msgstr "Visualiser"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
@@ -455,10 +455,14 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"Votre règle effectue toujours un filtrage par produit ou variante, mais les "
"produits ou variantes suivants ne figurent dans aucune des parties de votre "
"règle ; par conséquent, les personnes détenant ces billets ne seront pas "
"admises :"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr ""
msgstr "Veuillez vérifier si cela était intentionnel."
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
+2 -4
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-20 17:00+0000\n"
"PO-Revision-Date: 2026-06-30 16:52+0000\n"
"Last-Translator: Nikita Mitasov <me@ch4og.com>\n"
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/"
"ru/>\n"
@@ -35488,10 +35488,8 @@ msgstr "Введите промокод ниже, чтобы купить эт
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:10
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:14
#, fuzzy
#| msgid "Quota availabilities"
msgid "Not available yet."
msgstr "Наличие квот"
msgstr "Еще недоступно."
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:18
msgid "Not available any more."
+2 -7
View File
@@ -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 eventreverse_absolute
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 (
eventreverse_absolute(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:
@@ -1,33 +0,0 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+1 -2
View File
@@ -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<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
+1 -19
View File
@@ -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')
@@ -1,33 +0,0 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+1 -3
View File
@@ -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<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'),
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'),
+1 -19
View File
@@ -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):
+3 -15
View File
@@ -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 (
eventreverse_absolute(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 eventreverse_absolute(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):
@@ -1,33 +0,0 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+1 -2
View File
@@ -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<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ScaView.as_view(), name='sca'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$',
+2 -31
View File
@@ -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 eventreverse, eventreverse_absolute
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': eventreverse_absolute(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
+3 -2
View File
@@ -33,7 +33,7 @@ from django.contrib.auth.password_validation import (
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import signing
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.html import escape, format_html
from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import PhoneNumberField
@@ -83,7 +83,8 @@ class AuthenticationForm(forms.Form):
self.request = request
self.customer_cache = None
super().__init__(*args, **kwargs)
self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format(
self.fields['password'].help_text = format_html(
"<a target='_blank' href='{}'>{}</a>",
eventreverse_absolute(False, 'presale:organizer.customer.resetpw', kwargs={
'organizer': request.organizer.slug,
}),
@@ -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" %}
</a>
<script>
window.open('{{ url|escapejs }}');
</script>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% else %}
<h1>{% trans "Cookies not supported" %}</h1>
@@ -14,10 +14,23 @@
{% trans "Log out" %}
</a>
{% else %}
<a href="{% abseventurl request.organizer "presale:organizer.customer.login" %}{% if request.resolver_match.url_name != "organizer.customer.login" %}?next={% if request.event_domain %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.path|urlencode }}%3F{{ request.META.QUERY_STRING|urlencode }}{% endif %}{% if request.event_domain %}&request_cross_domain_customer_auth=true{% endif %}">
<span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Log in" %}</a>
<form
{% if request.event_domain %}
action="{% abseventurl request.event "presale:event.customer.loginstart" %}" method="post"
{% else %}
action="{% abseventurl request.organizer "presale:organizer.customer.login" %}" method="get"
{% endif %}
class="helper-display-inline">
{% if request.event_domain %}
{% csrf_token %}
{% endif %}
{% if request.resolver_match.url_name != "organizer.customer.login" %}
<input type="hidden" name="next" value="{% if request.event_domain %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.path }}?{{ request.META.QUERY_STRING }}">
{% endif %}
<button class="btn btn-link" type="submit">
<span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Log in" %}</button>
</form>
{% endif %}
</nav>
{% endif %}
+1
View File
@@ -98,6 +98,7 @@ event_patterns = [
re_path(r'unlock/(?P<hash>[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(),
name='event.payment.unlock'),
re_path(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
re_path(r'^account/loginstart$', pretix.presale.views.customer.LoginStartView.as_view(), name='event.customer.loginstart'),
re_path(r'^favicon.ico/?$',
pretix.presale.views.organizer.OrganizerFavicon.as_view(),
+3 -1
View File
@@ -151,7 +151,9 @@ def add_customer_to_request(request):
else:
parent_session_key = otpstore.get(f'customer_cross_domain_auth_{request.organizer.pk}')
if parent_session_key: # not already invalidated, expired, …
expected_nonce = request.session.pop('cross_domain_customer_auth_nonce', None)
found_nonce = request.GET.get("cross_domain_customer_auth_nonce")
if parent_session_key and expected_nonce and expected_nonce == found_nonce: # not already invalidated, expired, …
# Make sure the OTP can't be used again
otpstore.delete()
+14 -2
View File
@@ -21,7 +21,9 @@
#
from urllib.parse import quote, urlencode
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import SuspiciousOperation
from django.http import Http404
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
@@ -29,6 +31,7 @@ from django.views.generic import View
from pretix.base.services.cart import CartError
from pretix.base.signals import validate_cart
from pretix.base.views.tasks import AsyncAction
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.checkoutflow import get_checkout_flow
@@ -51,7 +54,12 @@ class CheckoutView(View):
def dispatch(self, request, *args, **kwargs):
self.request = request
if not cart_exists(request) and "async_id" not in request.GET:
is_asyncaction_call = (
request.method == "GET" and
'async_id' in request.GET and
settings.HAS_CELERY
)
if not cart_exists(request) and not is_asyncaction_call:
messages.error(request, _("Your cart is empty"))
return self.redirect(self.get_index_url(self.request))
@@ -78,9 +86,13 @@ class CheckoutView(View):
utm_params = {k: v for k, v in request.GET.items() if k.startswith("utm_")}
return self.redirect(step.get_step_url(request) + '?' + urlencode(utm_params))
is_selected = (step.identifier == kwargs.get('step', ''))
if "async_id" not in request.GET and not is_selected and not step.is_completed(request, warn=not is_selected):
if not is_asyncaction_call and not is_selected and not step.is_completed(request, warn=not is_selected):
return self.redirect(step.get_step_url(request))
if is_selected:
if is_asyncaction_call and not isinstance(step, AsyncAction):
# This could be used to circumvent validation otherwise
raise SuspiciousOperation("Received ?async_id for a step that is not an AsyncAction")
if request.method.lower() in self.http_method_names:
handler = getattr(step, request.method.lower(), self.http_method_not_allowed)
else:
+24
View File
@@ -146,6 +146,7 @@ class LoginView(RedirectBackMixin, FormView):
u = urlparse(url)
qsl = parse_qs(u.query)
qsl['cross_domain_customer_auth'] = otp
qsl['cross_domain_customer_auth_nonce'] = self.request.GET.get("request_cross_domain_customer_auth_nonce", "")
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
return url
@@ -705,6 +706,7 @@ class SSOLoginView(RedirectBackMixin, View):
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_nonce'] = self.request.GET.get("request_cross_domain_customer_auth_nonce")
redirect_uri = eventreverse_absolute(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
'provider': self.provider.pk
})
@@ -955,6 +957,28 @@ class SSOLoginReturnView(RedirectBackMixin, View):
u = urlparse(url)
qsl = parse_qs(u.query)
qsl['cross_domain_customer_auth'] = otp
qsl['cross_domain_customer_auth_nonce'] = self.request.session.get(f'pretix_customerauth_{self.provider.pk}_cross_domain_nonce', '')
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
return url
class LoginStartView(View):
# When a login is initiated on a event-domain-level view, we need to carry the user to the organizer domain through
# this POST request to be able to set a nonce on their current session. We can't just use a link, since then we'd
# need to create sessions for every anonymous user of the ticketshop, which is too expensive.
def post(self, request, *args, **kwargs):
if getattr(self.request, 'domain_mode', 'system') not in (KnownDomain.MODE_ORG_ALT_DOMAIN, KnownDomain.MODE_EVENT_DOMAIN):
raise Http404("Only active on event-level domains")
nonce = get_random_string(32)
request.session['cross_domain_customer_auth_nonce'] = nonce
query = {
"next": request.POST.get("next", ""),
"request_cross_domain_customer_auth_nonce": nonce,
"request_cross_domain_customer_auth": "true",
}
return redirect_to_url(
eventreverse_absolute(self.request.organizer, "presale:organizer.customer.login") + "?" + urlencode(query)
)
-1
View File
@@ -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()):
+10 -3
View File
@@ -42,7 +42,7 @@ import os
import re
from collections import Counter, OrderedDict, defaultdict
from decimal import Decimal
from urllib.parse import quote
from urllib.parse import quote, urlencode
from django import forms
from django.conf import settings
@@ -55,6 +55,7 @@ from django.http import (
FileResponse, Http404, HttpResponseRedirect, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -117,8 +118,14 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
login_url = eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
if hasattr(self.request, "event_domain") and self.request.event_domain:
next_url = quote(self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path())
return redirect_to_url(f'{login_url}?next={next_url}&request_cross_domain_customer_auth=true')
nonce = get_random_string(32)
self.request.session['cross_domain_customer_auth_nonce'] = nonce
query = {
"next": self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path(),
"request_cross_domain_customer_auth_nonce": nonce,
"request_cross_domain_customer_auth": "true",
}
return redirect_to_url(f'{login_url}?{urlencode(query)}')
else:
next_url = quote(self.request.get_full_path())
-1
View File
@@ -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:
+4
View File
@@ -624,6 +624,9 @@ LOGGING = {
'request_id': {
'()': 'pretix.helpers.logs.RequestIdFilter'
},
'skip_not_found': {
'()': 'pretix.helpers.logs.SkipNotFoundFilter',
}
},
'handlers': {
'console': {
@@ -665,6 +668,7 @@ LOGGING = {
'handlers': ['file', 'console', 'mail_admins'],
'level': loglevel,
'propagate': True,
'filters': ['skip_not_found'],
},
'pretix.security.csp': {
'handlers': ['csp_file'],
@@ -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)
@@ -221,6 +221,11 @@ footer nav .btn-link {
/*border-bottom: 2px solid $brand-primary;*/
font-weight: bold;
}
.btn-link {
padding: 0;
margin-left: 5px;
vertical-align: top;
}
img {
vertical-align: baseline;
}
@@ -653,6 +658,35 @@ h2 .label {
}
.helper-position-relative {
position: relative;
}
.helper-display-block {
display: block !important;
}
.helper-display-inline {
display: inline !important;
}
.helper-display-inline-block {
display: inline-block !important;
}
.helper-display-none-soft {
display: none;
}
.helper-display-none {
display: none !important;
}
.helper-width-auto {
width: auto;
}
.helper-width-100 {
width: 100%;
}
.helper-space-below {
margin-bottom: 10px;
}
@import "_iframe.scss";
@import "_a11y.scss";
@import "_print.scss";
+5 -5
View File
@@ -252,7 +252,7 @@ TEST_HISTORY_RES = {
}
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_list_list(token_client, organizer, event, clist, item, subevent, django_assert_num_queries):
res = dict(TEST_LIST_RES)
res["id"] = clist.pk
@@ -422,7 +422,7 @@ def test_list_update(token_client, organizer, event, clist):
assert cl.name == "VIP"
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_num_queries):
with scopes_disabled():
p1 = dict(TEST_ORDERPOSITION1_RES)
@@ -680,7 +680,7 @@ def _redeem(token_client, org, clist, p, body=None):
), body or {}, format='json')
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
with scopes_disabled():
p = order.positions.first().pk
@@ -1367,7 +1367,7 @@ def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist,
assert ci.position == p
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
with scopes_disabled():
p1 = dict(TEST_ORDERPOSITION1_RES)
@@ -1399,7 +1399,7 @@ def test_checkin_pdf_data_requires_permission(token_client, event, team, organiz
assert not resp.data['results'][0].get('pdf_data')
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_expand(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
with scopes_disabled():
op = order.positions.first()
+2 -2
View File
@@ -214,7 +214,7 @@ def _redeem(token_client, org, clist, p, body=None, query='', headers={}):
), body, format='json', headers={})
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
with scopes_disabled():
p = order.positions.first()
@@ -996,7 +996,7 @@ def test_redeem_conflicting_lists(token_client, organizer, clist, clist_all, eve
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order,
django_assert_max_num_queries):
with scopes_disabled():
+1 -1
View File
@@ -1826,7 +1826,7 @@ def test_event_block_unblock_seat_bulk(token_client, organizer, event, seatingpl
assert not s2.blocked
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item):
event.settings.seating_minimal_distance = 2
+56 -2
View File
@@ -70,14 +70,27 @@ def admin_user(admin_team):
@pytest.fixture
def application():
def app_developer():
return User.objects.create_user('app-developer@example.org', 'app-developer')
@pytest.fixture
def client2():
# We need a second test client instance to log in as the app developer user
from django.test import Client
return Client()
@pytest.fixture
def application(app_developer):
secret = get_random_string(32)
a = OAuthApplication.objects.create(
name="pretalx",
redirect_uris="https://pretalx.com",
client_type='confidential',
client_secret=secret,
authorization_grant_type='authorization-code'
authorization_grant_type='authorization-code',
user=app_developer,
)
a._cached_secret = secret
a.save()
@@ -703,6 +716,47 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O
assert list(grant.organizers.all()) == [organizer]
@pytest.mark.django_db
def test_token_app_disabled(client, client2, admin_user, organizer, application: OAuthApplication, app_developer):
client.login(email='dummy@dummy.dummy', password='dummy')
session = client.session
session['pretix_auth_login_time'] = int(time.time())
session.save()
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % (
application.client_id, quote(application.redirect_uris)
))
assert resp.status_code == 200
resp = client.post('/api/v1/oauth/authorize', data={
'organizers': str(organizer.pk),
'redirect_uri': application.redirect_uris,
'scope': 'read write',
'client_id': application.client_id,
'response_type': 'code',
'allow': 'Authorize',
})
assert resp.status_code == 302
assert resp['Location'].startswith('https://pretalx.com?code=')
code = resp['Location'].split("=")[1]
client.logout()
resp = client.post('/api/v1/oauth/token', data={
'code': code,
'redirect_uri': application.redirect_uris,
'grant_type': 'authorization_code',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
assert resp.status_code == 200
data = json.loads(resp.content.decode())
access_token = data['access_token']
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 200
client2.login(email='app-developer@example.org', password='app-developer')
client2.post(f'/control/settings/oauth/apps/{application.pk}/disable', {})
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 401
@pytest.mark.django_db
def test_user_revoke(client, admin_user, organizer, application: OAuthApplication):
client.login(email='dummy@dummy.dummy', password='dummy')
+2 -2
View File
@@ -1058,7 +1058,7 @@ def test_orderposition_list_limited_read(
('/api/v1/organizers/{}/orderpositions/', "organizer")
],
)
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_orderposition_list(
endpoint_template,
endpoint_type,
@@ -2036,7 +2036,7 @@ def test_blocked_secret_list(token_client, organizer, event):
assert [res] == resp.data['results']
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_queries):
# order detail
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?pdf_data=true'.format(
+3 -3
View File
@@ -732,7 +732,7 @@ def test_five_tickets_one_free(event):
@scopes_disabled()
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("itemcount", [3, 10, 50])
def test_query_count_many_items(event, itemcount):
setup_items(event, 'Tickets', 'both', 'discounts',
@@ -784,7 +784,7 @@ def test_query_count_many_items(event, itemcount):
@scopes_disabled()
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("catcount", [1, 10, 50])
def test_query_count_many_categories_and_discounts(event, catcount):
for n in range(1, catcount + 1):
@@ -838,7 +838,7 @@ def test_query_count_many_categories_and_discounts(event, catcount):
@scopes_disabled()
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("catcount", [2, 10, 50])
def test_query_count_many_cartpos(event, catcount):
for n in range(1, catcount + 1):
+1 -1
View File
@@ -210,7 +210,7 @@ def test_validate_membership_required(event, customer, membership, requiring_tic
assert "requires an active" in str(excinfo.value)
@pytest.mark.django_db
@pytest.mark.django_db(transaction=True)
def test_validate_membership_ensure_locking(event, customer, membership, requiring_ticket, membership_type, django_assert_num_queries):
with django_assert_num_queries(4) as captured:
validate_memberships_in_order(
+1 -1
View File
@@ -119,7 +119,7 @@ def test_linkify_abs(link):
assert markdown_compile_email(input) == f"<p>{output}</p>"
signer = signing.Signer(salt='safe-redirect')
signer = signing.Signer(salt='safelink-url')
@pytest.mark.parametrize(
+6
View File
@@ -28,6 +28,7 @@ from django.test import override_settings
from django.utils import translation
from django_scopes import scopes_disabled
from fakeredis import FakeRedisConnection
from hierarkey.proxy import dirty_cache_keys
from xdist.dsession import DSession
from pretix.testutils.mock import get_redis_connection
@@ -82,6 +83,11 @@ def reset_locale():
translation.activate("en")
@pytest.fixture(autouse=True)
def reset_hierarkey_cache_state():
dirty_cache_keys.set(set())
@pytest.fixture
def fakeredis_client(monkeypatch):
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
+2
View File
@@ -90,6 +90,7 @@ event_urls = [
"delete/",
"dangerzone/",
"cancel/",
"quickstart/",
"settings/",
"settings/plugins",
"settings/payment",
@@ -311,6 +312,7 @@ event_permission_urls = [
("event.settings.general:write", "live/", 200, HTTP_GET),
("event.settings.general:write", "delete/", 200, HTTP_GET),
("event.settings.general:write", "dangerzone/", 200, HTTP_GET),
("event.settings.general:write", "quickstart/", 200, HTTP_GET),
("event.settings.general:write", "settings/", 200, HTTP_GET),
# ("event.settings.payment:write", "settings/payment", 200, HTTP_GET), GET allowed also with other permissions
("event.settings.payment:write", "settings/payment", 200, HTTP_POST),
@@ -19,11 +19,21 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.core.management.base import BaseCommand
import json
import pytest
class Command(BaseCommand):
help = "Do nothing. Useful for startup performance testing."
def test_allowed_json():
assert json.loads('{"float":1.5,"int":161,"arr":[]}') == {"float": 1.5, "int": 161, "arr": []}
def handle(self, *args, **options):
pass
def test_disallowed_json_float_consts():
with pytest.raises(KeyError):
json.loads("Infinity")
with pytest.raises(KeyError):
json.loads("-Infinity")
with pytest.raises(KeyError):
json.loads("NaN")
with pytest.raises(KeyError):
json.loads("[123, NaN, Infinity, -Infinity]")
+11
View File
@@ -5959,3 +5959,14 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
assert 'Gift card' in response.content.decode()
assert '(1 available)' in response.content.decode()
def test_validation_bypass_error_async_id_is_fixed(self):
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=0, listed_price=0, price_after_voucher=0, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/confirm/?async_id=1' % (self.orga.slug, self.event.slug), follow=False)
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
target_status_code=200)
+25 -4
View File
@@ -721,9 +721,21 @@ def _cross_domain_login(env, client, client2, org_alt=False):
else:
KnownDomain.objects.create(domainname='event.test', organizer=env[0], event=env[1])
# Log in on org domain
# Start session on event domain
path = '/conf/' if org_alt else '/'
r = client.post(f'/account/login?next=https://event.test{path}redeem&request_cross_domain_customer_auth=true', {
r = client2.post(f'{path}account/loginstart', {
'next': f'https://event.test{path}redeem',
}, HTTP_HOST='event.test')
assert r.status_code == 302
u = urlparse(r.headers['Location'])
assert u.netloc == 'org.test'
assert u.path == '/account/login'
assert 'request_cross_domain_customer_auth=' in u.query
assert 'request_cross_domain_customer_auth_nonce=' in u.query
assert 'next=' in u.query
# Log in on org domain
r = client.post(f'{u.path}?{u.query}', {
'email': 'john@example.org',
'password': 'foo',
}, HTTP_HOST='org.test')
@@ -734,6 +746,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
assert u.path == path + 'redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
assert 'cross_domain_customer_auth_nonce' in q
# Take session over to event domain
r = client2.get(f'{path}?{u.query}', HTTP_HOST='event.test')
@@ -745,7 +758,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
def test_cross_domain_login(env, client, client2):
_cross_domain_login(env, client, client2)
# Logged in on evnet domain
# Logged in on event domain
r = client.get('/', HTTP_HOST='event.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
@@ -896,7 +909,14 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
},
)
url = f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true'
r = client2.post('/account/loginstart', {"next": "https://event.test/redeem"}, follow=False, HTTP_HOST='event.test')
assert r.status_code == 302
assert "/account/login" in r['Location']
u = urlparse(r.headers['Location'])
nonce = parse_qs(u.query)['request_cross_domain_customer_auth_nonce'][0]
url = (f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true&'
f'request_cross_domain_customer_auth_nonce={nonce}')
r = client.get(url, follow=False, HTTP_HOST='org.test')
assert r.status_code == 302
assert "/authorize" in r['Location']
@@ -910,6 +930,7 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
assert u.path == '/redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
assert 'cross_domain_customer_auth_nonce' in q
# Take session over to event domain
r = client2.get(f'/?{u.query}', HTTP_HOST='event.test')
-46
View File
@@ -1,46 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import os
import subprocess
import sys
import tempfile
def test_start_with_redis_down():
"""
This is a test that ensures that pretix is able to start without a running redis server,
even if one is configured.
"""
with tempfile.NamedTemporaryFile(suffix="cfg") as f:
f.write(b"[redis]\nlocation=redis://127.0.0.99:65534/2\n")
f.flush()
assert subprocess.check_call(
[
sys.executable,
os.path.join(os.path.dirname(__file__), '../manage.py'),
"noop",
],
env={
"PRETIX_CONFIG_FILE": f.name,
}
) == 0