Compare commits

...
Author SHA1 Message Date
Raphael Michel f7b00f25ed Bump version to 2026.5.6 2026-09-29 20:41:53 +02:00
Raphael Michel ee716497cb Hotfix: Perform validation on the selected step, not on all steps before 2026-09-29 20:41:26 +02:00
Raphael Michel bdfe683146 Bump to 2026.5.5 2026-09-29 14:24:48 +02:00
Raphael Michel 03814bd570 Ignore unimportant testfailure on backport 2026-09-29 14:23:13 +02:00
Raphael Michel 1422ea0fa2 Fix tests after hierarkey update 2026-09-29 14:05:28 +02:00
Raphael Michel 5c761773fd Bump hierarkey to 2.0.2 2026-09-29 14:05:27 +02:00
Raphael Michel abeb615d3d [SECURITY] API: Fix session validation for uploaded files (CVE-2026-101269, Z#23247174) 2026-09-29 13:43:52 +02:00
Mira Weller 1579940503 Block out of bounds image crop dimensions (Z#23245937 / PRT-009) 2026-09-29 13:43:52 +02:00
Mira Weller 463f0d6499 Prevent parsing non-standard-compliant JSON float values (Z#23245937 / PRT-021) 2026-09-29 13:43:52 +02:00
Mira Weller c65e8835f8 Fix potential infinite loop in pdf render (Z#23245937 / PRT-021) 2026-09-29 13:43:52 +02:00
Mira Weller 9eaa6b0e1d Fix inefficient loop in compute_validity (Z#23245937 / PRT-013) 2026-09-29 13:43:52 +02:00
Raphael Michel d5fe5b49df [SECURITY] OAuth: Disable existing tokens when deactivating Application (CVE-2026-101271, Z#23247296) 2026-09-29 13:43:52 +02:00
Mira Weller 9a989f2eef [SECURITY] Escape help texts (CVE-2026-101270) 2026-09-29 13:43:51 +02:00
Raphael Michel 1b69eebbaa [SECURITY] Fix customer session fixation on cross-domain login (CVE-2026-101268, Z#23247268) 2026-09-29 13:42:44 +02:00
Raphael Michel afe87dcda1 [SECURITY] Fix information leak in widgets.json on dashboard (CVE-2026-101267, Z#23247172)
Thanks to Wenhao Wu, Southeast University
2026-09-29 13:41:04 +02:00
Raphael Michel a86f6f295d [SECURITY] Fix checkout validation bypass (CVE-2026-101266, Z#23245008) 2026-09-29 13:41:04 +02:00
Raphael Michel d6d400b5c4 Bump to 2026.5.4 2026-07-28 12:27:00 +02:00
Raphael Michel ad0b5776b6 [SECURITY] Add missing permission check for view (CVE-2026-57532) 2026-07-28 12:26:45 +02:00
Raphael Michel 15bfa2b2ce Bump to 2026.5.3 2026-07-01 14:11:01 +02:00
Raphael MichelandMira Weller 43b7e0afe0 [SECURITY] Hardening for user impersonation feature (CVE-2026-13602)
---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 14:07:28 +02:00
Raphael MichelandMira Weller 6fa7d4289b [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 14:06:59 +02:00
Mira WellerandRaphael Michel 3730b5473b [SECURITY] Allowlisting and changed salts for safelink and safelink_callback (CVE-2026-13602)
---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-01 14:05:17 +02:00
Raphael Michel 7faf6cfd80 Bump to 2026.5.2 2026-06-25 16:40:44 +02:00
Raphael Michel cdeb3609f3 [SECURITY] Properly escape HTML tags in PDF generation (CVE-2026-57535) 2026-06-25 16:40:28 +02:00
Raphael Michel 53d8aa5679 [SECURITY] Prevent reading of any local files in reportlab (CVE-2026-57535) 2026-06-25 16:40:00 +02:00
Raphael Michel 80601cc013 [SECURITY] Disable outbound and file access for reportlab (CVE-2026-57535) 2026-06-25 16:40:00 +02:00
Mira Weller e2661f304d [SECURITY] Fix reflected XSS in redirection page (CVE-2026-57533) 2026-06-25 16:40:00 +02:00
Mira Weller 71f6ade5d6 [SECURITY] Fix stored XSS in ticket confirmation page (CVE-2026-13225) 2026-06-25 16:40:00 +02:00
Mira Weller e087dd39b8 [SECURITY] Hardening: Don't use |safe on confirm_messages 2026-06-25 16:40:00 +02:00
Mira Weller f3a8ce39f1 [SECURITY] Fix XSS in ticket layout JSON (CVE-2026-57532) 2026-06-25 16:40:00 +02:00
Raphael Michel a6c417eb2f Bump version to 2026.5.1 2026-06-09 13:22:17 +02:00
Richard Schreiber 5d449ea313 [SECURITY] Reusable media export: Respect giftcard permissions (CVE-2026-11764) (#6261) 2026-06-09 13:21:41 +02:00
76 changed files with 816 additions and 507 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.5.0"
__version__ = "2026.5.6"
+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,
@@ -327,10 +328,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()
+8 -3
View File
@@ -61,18 +61,23 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
yield headers
yield self.ProgressSetTotal(total=media.count())
can_read_giftcards = self.permission_holder.has_organizer_permission(self.organizer, 'organizer.giftcards:read')
for medium in media.iterator(chunk_size=1000):
row = [
giftcard_secret = medium.linked_giftcard.secret if medium.linked_giftcard_id else ''
if giftcard_secret and not can_read_giftcards:
giftcard_secret = giftcard_secret[:3] + "…"
yield [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
giftcard_secret,
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'
+10 -6
View File
@@ -587,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
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -58,8 +56,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
reshaper,
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
normalize_text, register_ttf_font_if_new, reshaper,
)
from pretix.presale.style import get_fonts
@@ -259,18 +257,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text):
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
# alias kept for plugin compatibility
return normalize_text(text)
def _upper(self, val):
# We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
# tags.
return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
style=self.stylesheet['Normal'])
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph(
self._clean_text(self.invoice.full_invoice_from),
p = PlainTextParagraph(
self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom']
)
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip()
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
p_str = shorten(self.invoice.event.name)
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -645,39 +636,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text:
story.append(FontFallbackParagraph(
story.append(PlainTextParagraph(
type_info_text,
self.stylesheet['WarningBlock']
))
if self.invoice.custom_field:
story.append(FontFallbackParagraph(
story.append(PlainTextParagraph(
'{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
self._clean_text(self.invoice.custom_field),
str(self.invoice.event.settings.invoice_address_custom_field),
self.invoice.custom_field,
),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
)),
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=self.invoice.internal_reference,
),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
story.append(PlainTextParagraph(
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self.stylesheet['Normal']
))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
FontFallbackParagraph(
self._normalize(
PlainTextParagraph(
(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)]
else:
tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)]
def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p = PlainTextParagraph(
curr_description,
p_style
)
h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
period_line,
self.stylesheet['Fineprint']
))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0),
str(len(lines)),
localize(tax_rate) + " %",
FontFallbackParagraph(
PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
FontFallbackParagraph(
PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
description_p_list.pop(0),
str(len(lines)),
FontFallbackParagraph(
PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
else:
tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
[PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm))
if request_show_service_date:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
story.append(PlainTextParagraph(
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal']
))
if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text),
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal']
))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
''
]
tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
FontFallbackParagraph(
self._normalize(pgettext(
PlainTextParagraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize(
story.append(PlainTextParagraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))),
total=fmt(foreign_total)),
self.stylesheet['Fineprint']
))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.address_invoice_from:
return
c = [
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
c = self.invoice.address_invoice_from.strip().split('\n')
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
@@ -1225,8 +1211,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
]
p = FontFallbackParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
p = PlainTextParagraph(
date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(FontFallbackParagraph(
i.append(PlainTextParagraph(
pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(),
),
+2 -4
View File
@@ -1060,10 +1060,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,
+2 -2
View File
@@ -1062,14 +1062,14 @@ class Renderer:
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
p = Paragraph(text, style=style)
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
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,13 +2,14 @@
{% load i18n %}
{% load rich_text %}
{% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
{% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
+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
+4 -3
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):
+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
@@ -355,7 +355,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,
)
@@ -380,7 +380,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(
+23 -22
View File
@@ -38,6 +38,7 @@ from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6
story = [
Paragraph("System report", headlinestyle),
PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle),
PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm),
self._usage_table(),
Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle),
PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm),
self._tech_table(),
Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle),
PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small),
PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle),
PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small),
PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm),
]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
]
tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[
Paragraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
PlainTextParagraph("Database engine:", style),
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
],
]
table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year
tdata = [
[
Paragraph(l, style_small_head)
PlainTextParagraph(l, style_small_head)
for l in (
"Time frame",
"Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append(
(
Paragraph(
PlainTextParagraph(
date_format(first_day, "M Y")
+ " – "
+ date_format(after_day - timedelta(days=1), "M Y"),
style_small,
),
Paragraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
PlainTextParagraph(c, style_small),
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
)
)
@@ -19,9 +19,7 @@
{% endif %}
</h1>
<script type="application/json" id="editor-data">
{{ layout|safe }}
</script>
{{ layout|json_script:"editor-data" }}
<div class="row">
<div class="col-md-9">
<div class="panel panel-default panel-pdf-editor">
+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
@@ -1554,7 +1554,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
+1 -1
View File
@@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images()
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['layout'] = self.get_current_layout()
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
+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):
+5
View File
@@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig):
def ready(self):
from .monkeypatching import monkeypatch_all_at_ready
monkeypatch_all_at_ready()
# Ensure reportlab does not make any calls to the internet or the local disk
from reportlab import rl_config
rl_config.trustedHosts = []
rl_config.trustedSchemes = ['data']
+29
View File
@@ -27,6 +27,7 @@ from datetime import datetime
from http import cookies
from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -40,6 +41,8 @@ from urllib3.util.connection import (
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
@@ -230,9 +233,35 @@ def monkeypatch_cookie_morsel():
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
def monkeypatch_reportlab_imagereader():
from reportlab.lib import utils
old_init = utils.ImageReader.__init__
def new_init(self, fileName, ident=None): # noqa
if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'):
if not isinstance(self, ThumbnailingImageReader):
# ThumbnailingImageReader is only used by us explicitly and not by using <img> in html, so it is safe
raise SuspiciousFileOperation("reportlab should not be reading images from disk")
return types.MethodType(old_init, self)(
fileName, ident
)
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()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
monkeypatch_json_constants()
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts
@@ -70,6 +75,20 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
}))
def normalize_text(text: str) -> str:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs):
if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text:
return True
font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all(
ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
if not isinstance(text, str):
if hasattr(text, '__html__'):
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
text = str(text)
# Normalize unicode and apply reshaping
text = normalize_text(text)
# Escape any HTML in the text
text = escape(text)
if linebreaks:
text = text.strip().replace("\n", "<br />\n")
super().__init__(text, style, *args, **kwargs)
def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
@@ -0,0 +1,33 @@
#
# 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 logging
from django import template
from django.utils.html import format_html
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def wrap_in(content, tag_name):
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
+9 -9
View File
@@ -64,7 +64,7 @@ from pretix.base.timeframes import (
from pretix.control.forms.widgets import Select2
from pretix.helpers.filenames import safe_for_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.reportlab import FontFallbackParagraph
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.helpers.templatetags.jsonfield import JSONExtract
from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -343,7 +343,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
]
story = [
FontFallbackParagraph(
PlainTextParagraph(
cl.name,
headlinestyle
),
@@ -351,7 +351,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
if cl.subevent:
story += [
Spacer(1, 3 * mm),
FontFallbackParagraph(
PlainTextParagraph(
'{} ({} {})'.format(
cl.subevent.name,
cl.subevent.get_date_range_display(),
@@ -381,10 +381,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
headrowstyle.fontName = 'OpenSansBd'
for q in questions:
txt = str(q.question)
p = FontFallbackParagraph(txt, headrowstyle)
p = PlainTextParagraph(txt, headrowstyle)
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, headrowstyle)
p = PlainTextParagraph(txt, headrowstyle)
tdata[0].append(p)
qs = self._get_queryset(cl, form_data)
@@ -431,8 +431,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '—',
'✘' if op.order.status != Order.STATUS_PAID else '✔',
op.order.code,
FontFallbackParagraph(name, self.get_style()),
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
PlainTextParagraph(name, self.get_style()),
PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
]
acache = {}
if op.addon_to:
@@ -443,10 +443,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
for q in questions:
txt = acache.get(q.pk, '')
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>')
p = FontFallbackParagraph(txt, self.get_style())
p = PlainTextParagraph(txt, self.get_style())
while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, self.get_style())
p = PlainTextParagraph(txt, self.get_style())
row.append(p)
if op.order.status != Order.STATUS_PAID:
tstyledata += [
+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 build_absolute_uri
from pretix.plugins.paypal.api import Api
from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -349,11 +348,7 @@ class Paypal(BasePaymentProvider):
for link in payment.links:
if link.method == "REDIRECT" and link.rel == "approval_url":
if request.session.get('iframe_session', False):
signer = signing.Signer(salt='safe-redirect')
return (
build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' +
urllib.parse.quote(signer.sign(link.href))
)
return safelink(link.href, framebreak=True)
else:
return str(link.href)
else:
@@ -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):
+78 -78
View File
@@ -36,7 +36,7 @@ from reportlab.lib import colors, pagesizes
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.units import mm
from reportlab.platypus import (
KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle,
KeepTogether, PageTemplate, Spacer, Table, TableStyle,
)
from pretix.base.exporter import BaseExporter
@@ -49,7 +49,7 @@ from pretix.base.timeframes import (
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
from pretix.control.forms.filter import get_all_payment_providers
from pretix.helpers.reportlab import FontFallbackParagraph
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [
[
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold),
FontFallbackParagraph(_("Price"), tstyle_bold_right),
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right),
FontFallbackParagraph("#", tstyle_bold_right),
FontFallbackParagraph(_("Net total"), tstyle_bold_right),
FontFallbackParagraph(_("Tax total"), tstyle_bold_right),
FontFallbackParagraph(_("Gross total"), tstyle_bold_right),
PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold),
PlainTextParagraph(_("Price"), tstyle_bold_right),
PlainTextParagraph(_("Tax rate"), tstyle_bold_right),
PlainTextParagraph("#", tstyle_bold_right),
PlainTextParagraph(_("Net total"), tstyle_bold_right),
PlainTextParagraph(_("Tax total"), tstyle_bold_right),
PlainTextParagraph(_("Gross total"), tstyle_bold_right),
]
]
@@ -347,12 +347,12 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if e != last_group:
if last_group_head_idx > 0 and e is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata.append(
[
FontFallbackParagraph(
PlainTextParagraph(
e,
tstyle_bold,
),
@@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
text = self._transaction_row_label(r)
tdata.append(
[
FontFallbackParagraph(text, tstyle),
Paragraph(
PlainTextParagraph(text, tstyle),
PlainTextParagraph(
money_filter(r["price"], currency)
if "price" in r and r["price"] is not None
else "",
tstyle_right,
),
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
Paragraph(str(r["sum_cont"]), tstyle_right),
Paragraph(
PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
),
Paragraph(money_filter(r["sum_tax"], currency), tstyle_right),
Paragraph(money_filter(r["sum_price"], currency), tstyle_right),
PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right),
PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right),
]
)
sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"]
@@ -398,19 +398,19 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
sum_tax_by_group += r["sum_tax"]
if last_group_head_idx > 0 and last_group is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
if len(sum_tax_by_tax_rate) > 1:
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
tdata.append(
[
FontFallbackParagraph(_("Sum"), tstyle),
Paragraph("", tstyle_right),
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right),
Paragraph("", tstyle_right),
Paragraph(
PlainTextParagraph(_("Sum"), tstyle),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(
money_filter(
sum_price_by_tax_rate[tax_rate]
- sum_tax_by_tax_rate[tax_rate],
@@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_right,
),
Paragraph(
PlainTextParagraph(
money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right
),
Paragraph(
PlainTextParagraph(
money_filter(sum_price_by_tax_rate[tax_rate], currency),
tstyle_right,
),
@@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append(
[
FontFallbackParagraph(_("Sum"), tstyle_bold),
Paragraph("", tstyle_right),
Paragraph("", tstyle_right),
Paragraph("", tstyle_bold_right),
Paragraph(
PlainTextParagraph(_("Sum"), tstyle_bold),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph("", tstyle_bold_right),
PlainTextParagraph(
money_filter(
sum(sum_price_by_tax_rate.values())
- sum(sum_tax_by_tax_rate.values()),
@@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(sum(sum_tax_by_tax_rate.values()), currency),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(sum(sum_price_by_tax_rate.values()), currency),
tstyle_bold_right,
),
@@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [
[
FontFallbackParagraph(_("Payment method"), tstyle_bold),
FontFallbackParagraph(_("Payments"), tstyle_bold_right),
FontFallbackParagraph(_("Refunds"), tstyle_bold_right),
FontFallbackParagraph(_("Total"), tstyle_bold_right),
PlainTextParagraph(_("Payment method"), tstyle_bold),
PlainTextParagraph(_("Payments"), tstyle_bold_right),
PlainTextParagraph(_("Refunds"), tstyle_bold_right),
PlainTextParagraph(_("Total"), tstyle_bold_right),
]
]
@@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
for p in providers:
tdata.append(
[
Paragraph(provider_names.get(p, p), tstyle),
FontFallbackParagraph(
PlainTextParagraph(provider_names.get(p, p), tstyle),
PlainTextParagraph(
money_filter(payments_by_provider[p], currency)
if p in payments_by_provider
else "",
tstyle_right,
),
Paragraph(
PlainTextParagraph(
money_filter(refunds_by_provider[p], currency)
if p in refunds_by_provider
else "",
tstyle_right,
),
Paragraph(
PlainTextParagraph(
money_filter(
payments_by_provider.get(p, Decimal("0.00"))
- refunds_by_provider.get(p, Decimal("0.00")),
@@ -563,20 +563,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append(
[
FontFallbackParagraph(_("Sum"), tstyle_bold),
Paragraph(
PlainTextParagraph(_("Sum"), tstyle_bold),
PlainTextParagraph(
money_filter(
sum(payments_by_provider.values(), Decimal("0.00")), currency
),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(
sum(refunds_by_provider.values(), Decimal("0.00")), currency
),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(
sum(payments_by_provider.values(), Decimal("0.00"))
- sum(refunds_by_provider.values(), Decimal("0.00")),
@@ -641,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
open_before = tx_before - p_before + r_before
tdata.append(
[
FontFallbackParagraph(
PlainTextParagraph(
_("Pending payments at {datetime}").format(
datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone(
@@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyle,
),
"",
Paragraph(money_filter(open_before, currency), tstyle_right),
PlainTextParagraph(money_filter(open_before, currency), tstyle_right),
]
)
else:
@@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00")
tdata.append(
[
FontFallbackParagraph(_("Orders"), tstyle),
Paragraph("+", tstyle_center),
Paragraph(money_filter(tx_during, currency), tstyle_right),
PlainTextParagraph(_("Orders"), tstyle),
PlainTextParagraph("+", tstyle_center),
PlainTextParagraph(money_filter(tx_during, currency), tstyle_right),
]
)
tdata.append(
[
FontFallbackParagraph(_("Payments"), tstyle),
Paragraph("-", tstyle_center),
Paragraph(money_filter(p_during, currency), tstyle_right),
PlainTextParagraph(_("Payments"), tstyle),
PlainTextParagraph("-", tstyle_center),
PlainTextParagraph(money_filter(p_during, currency), tstyle_right),
]
)
tdata.append(
[
FontFallbackParagraph(_("Refunds"), tstyle),
Paragraph("+", tstyle_center),
Paragraph(money_filter(r_during, currency), tstyle_right),
PlainTextParagraph(_("Refunds"), tstyle),
PlainTextParagraph("+", tstyle_center),
PlainTextParagraph(money_filter(r_during, currency), tstyle_right),
]
)
open_after = open_before + tx_during - p_during + r_during
tdata.append(
[
Paragraph(
PlainTextParagraph(
_("Pending payments at {datetime}").format(
datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_bold,
),
Paragraph("=", tstyle_center),
Paragraph(money_filter(open_after, currency), tstyle_bold_right),
PlainTextParagraph("=", tstyle_center),
PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
]
)
tstyledata += [
@@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
)
tdata.append(
[
Paragraph(
PlainTextParagraph(
_("Total gift card value at {datetime}").format(
datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone(
@@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle,
),
Paragraph(money_filter(tx_before, currency), tstyle_right),
PlainTextParagraph(money_filter(tx_before, currency), tstyle_right),
]
)
else:
@@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00")
tdata.append(
[
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle),
Paragraph(money_filter(tx_during_pos, currency), tstyle_right),
PlainTextParagraph(_("Gift card transactions (credit)"), tstyle),
PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right),
]
)
@@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00")
tdata.append(
[
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle),
Paragraph(money_filter(tx_during_neg, currency), tstyle_right),
PlainTextParagraph(_("Gift card transactions (debit)"), tstyle),
PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right),
]
)
open_after = tx_before + tx_during_pos + tx_during_neg
tdata.append(
[
Paragraph(
PlainTextParagraph(
_("Total gift card value at {datetime}").format(
datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_bold,
),
Paragraph(money_filter(open_after, currency), tstyle_bold_right),
PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
]
)
tstyledata += [
@@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
style_small.leading = 10
story = [
FontFallbackParagraph(self.verbose_name, style_h1),
PlainTextParagraph(self.verbose_name, style_h1),
Spacer(0, 3 * mm),
FontFallbackParagraph(
"<br />".join(escape(f) for f in self.describe_filters(form_data)),
PlainTextParagraph(
"\n".join(escape(f) for f in self.describe_filters(form_data)),
style_small,
),
]
@@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s:
story += [
Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2),
PlainTextParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
@@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s:
story += [
Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2),
PlainTextParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
@@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Open items") + c_head, style_h2),
PlainTextParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
@@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Gift cards") + c_head, style_h2),
PlainTextParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm),
*s,
]
+13 -13
View File
@@ -70,7 +70,7 @@ from pretix.base.timeframes import (
)
from pretix.control.forms.filter import OverviewFilterForm
from pretix.helpers.reportlab import (
FontFallbackParagraph, register_ttf_font_if_new,
PlainTextParagraph, register_ttf_font_if_new,
)
from pretix.presale.style import get_fonts
@@ -282,7 +282,7 @@ class OverviewReport(Report):
headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd'
story = [
FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
PlainTextParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
Spacer(1, 5 * mm)
]
return story
@@ -292,7 +292,7 @@ class OverviewReport(Report):
if form_data.get('date_axis') and form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')],
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–',
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '–',
@@ -305,13 +305,13 @@ class OverviewReport(Report):
subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
except SubEvent.DoesNotExist:
subevent = self.form_data.get('subevent')
story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
story.append(PlainTextParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
story.append(Spacer(1, 5 * mm))
if form_data.get('subevent_date_range'):
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=_('Event date'),
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–',
end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '–',
@@ -384,13 +384,13 @@ class OverviewReport(Report):
tdata = [
[
_('Product'),
FontFallbackParagraph(_('Canceled'), tstyle_th),
PlainTextParagraph(_('Canceled'), tstyle_th),
'',
FontFallbackParagraph(_('Expired'), tstyle_th),
PlainTextParagraph(_('Expired'), tstyle_th),
'',
FontFallbackParagraph(_('Approval pending'), tstyle_th),
PlainTextParagraph(_('Approval pending'), tstyle_th),
'',
FontFallbackParagraph(_('Purchased'), tstyle_th),
PlainTextParagraph(_('Purchased'), tstyle_th),
'', '', '', '', ''
],
[
@@ -421,14 +421,14 @@ class OverviewReport(Report):
for tup in items_by_category:
if tup[0]:
tdata.append([
FontFallbackParagraph(str(tup[0]), tstyle_bold)
PlainTextParagraph(str(tup[0]), tstyle_bold)
])
for l, s in states:
tdata[-1].append(str(tup[0].num[l][0]))
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
for item in tup[1]:
tdata.append([
FontFallbackParagraph(str(item), tstyle)
PlainTextParagraph(str(item), tstyle)
])
for l, s in states:
tdata[-1].append(str(item.num[l][0]))
@@ -436,7 +436,7 @@ class OverviewReport(Report):
if item.has_variations:
for var in item.all_variations:
tdata.append([
FontFallbackParagraph(" " + str(var), tstyle)
PlainTextParagraph(" " + str(var), tstyle)
])
for l, s in states:
tdata[-1].append(str(var.num[l][0]))
@@ -568,7 +568,7 @@ class OrderTaxListReportPDF(Report):
tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
story = [
FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
PlainTextParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
Spacer(1, 5 * mm)
]
tdata = [
+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 (
build_absolute_uri(request.event, 'plugins:stripe:redirect') +
'?data=' + signing.dumps({
'url': url,
'session': {
'payment_stripe_order_secret': request.session['payment_stripe_order_secret'],
},
}, salt='safe-redirect')
)
return safelink(url, framebreak=True)
else:
return str(url)
@@ -1053,11 +1045,7 @@ class StripeMethod(BasePaymentProvider):
'hash': payment.order.tagged_secret('plugins:stripe'),
})
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False):
return build_absolute_uri(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
'url': url,
'session': {},
}, salt='safe-redirect')
return safelink(url, framebreak=True)
return url
def _confirm_payment_intent(self, request, payment):
@@ -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 build_absolute_uri, eventreverse
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.plugins.stripe.tasks import (
@@ -74,28 +72,6 @@ from pretix.plugins.stripe.tasks import (
logger = logging.getLogger('pretix.plugins.stripe')
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
try:
data = signing.loads(request.GET.get('data', ''), salt='safe-redirect')
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if 'go' in request.GET:
if 'session' in data:
for k, v in data['session'].items():
request.session[k] = v
return redirect(data['url'])
else:
params = request.GET.copy()
params['go'] = '1'
r = render(request, 'pretixplugins/stripe/redirect.html', {
'url': build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params),
})
r._csp_ignore = True
return r
@scopes_disabled()
def oauth_return(request, *args, **kwargs):
import stripe
@@ -514,11 +490,6 @@ class StripeOrderView:
return self.request.event.get_payment_providers()[self.payment.provider]
def _redirect_to_order(self):
if self.request.session.get('payment_stripe_order_secret') != self.order.secret and not self.payment.provider.startswith('stripe'):
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
+2 -1
View File
@@ -51,6 +51,7 @@ from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
@@ -1634,7 +1635,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
meta_info = {
'contact_form_data': self.cart_session.get('contact_form_data', {}),
'confirm_messages': [
str(m) for m in self.confirm_messages.values()
conditional_escape(str(m)) for m in self.confirm_messages.values()
]
}
api_meta = {}
+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>",
build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={
'organizer': request.organizer.slug,
}),
+1 -1
View File
@@ -144,7 +144,7 @@ checkout_confirm_messages = EventPluginSignal()
This signal is sent out to retrieve short messages that need to be acknowledged by the user before the
order can be completed. This is typically used for something like "accept the terms and conditions".
Receivers are expected to return a dictionary where the keys are globally unique identifiers for the
message and the values can be arbitrary HTML.
message and the values can be a SafeString containing arbitrary HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -176,7 +176,7 @@
<div class="checkbox">
<label for="input_confirm_{{ key }}">
<input type="checkbox" class="checkbox" value="yes" name="confirm_{{ key }}" id="input_confirm_{{ key }}" required>
{{ desc|safe }}
{{ desc }}
</label>
</div>
{% endfor %}
@@ -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>
@@ -4,6 +4,7 @@
{% load eventsignal %}
{% load money %}
{% load eventurl %}
{% load wrap_in %}
{% block title %}{% trans "Registration details" %}{% endblock %}
{% block content %}
<h2 class="h1">
@@ -48,7 +49,7 @@
</div>
<div class="panel-body">
<p>
{% blocktrans trimmed with email="<strong>"|add:order.email|add:"</strong>"|safe %}
{% blocktrans trimmed with email=order.email|wrap_in:"strong" %}
This order is managed for you by {{ email }}. Please contact them for any questions regarding
payment, cancellation or changes to this order.
{% endblocktrans %}
@@ -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
@@ -703,6 +704,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 = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
'provider': self.provider.pk
})
@@ -953,6 +955,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(
build_absolute_uri(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:
@@ -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";
+6 -6
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,11 +680,11 @@ 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
with django_assert_max_num_queries(30):
with django_assert_max_num_queries(31):
resp = _redeem(token_client, organizer, clist, p)
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
@@ -1346,7 +1346,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)
@@ -1378,7 +1378,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()
+3 -3
View File
@@ -214,11 +214,11 @@ 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()
with django_assert_max_num_queries(29):
with django_assert_max_num_queries(30):
resp = _redeem(token_client, organizer, clist, p.secret)
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
@@ -931,7 +931,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",
@@ -310,6 +311,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),
+39
View File
@@ -0,0 +1,39 @@
#
# 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 json
import pytest
def test_allowed_json():
assert json.loads('{"float":1.5,"int":161,"arr":[]}') == {"float": 1.5, "int": 161, "arr": []}
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]")
+50
View File
@@ -0,0 +1,50 @@
#
# 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 pytest
from django.core.exceptions import SuspiciousFileOperation
from reportlab.platypus import Paragraph
def test_http_access_disabled(monkeypatch):
def guard(*args, **kwargs):
pytest.fail("No internet wanted!")
monkeypatch.setattr('socket.socket', guard)
with pytest.raises(SuspiciousFileOperation, match="should not be reading images from disk"):
Paragraph(
'<img src="https://static.pretix.cloud/static/pretixeu/img/opengraph.png"/>',
)
def test_file_access_disabled_scheme(monkeypatch):
with pytest.raises(SuspiciousFileOperation, match="should not be reading images from disk"):
Paragraph(
'<img src="file:///etc/passwd" />',
)
def test_file_access_disabled_direct(monkeypatch):
with pytest.raises(SuspiciousFileOperation, match="should not be reading images from disk"):
Paragraph(
'<img src="/etc/passwd" />',
)
+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')