Compare commits

...
Author SHA1 Message Date
Raphael Michel 916951aa68 Bump to 2026.3.5 2026-07-01 14:13:39 +02:00
Raphael Michel 3adb2eaacb Update GitLab CI instructions 2026-07-01 14:13:39 +02:00
Raphael MichelandMira Weller 436a8b0c24 [SECURITY] Hardening for user impersonation feature (CVE-2026-13602)
---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 14:02:48 +02:00
Raphael MichelandMira Weller deee2c9f66 [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:02:48 +02:00
cf87fa1039 [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:02:48 +02:00
Raphael Michel 7ed69a8ef7 Bump version to 2026.3.4 2026-06-25 16:36:43 +02:00
Mira WellerandRaphael Michel ba37f2eb47 [SECURITY] Fix XSS in ticket layout JSON (CVE-2026-57532) 2026-06-25 16:22:02 +02:00
Raphael Michel 8ec81f80e8 [SECURITY] Properly escape HTML tags in PDF generation (CVE-2026-57535) 2026-06-25 16:21:44 +02:00
Raphael MichelandRaphael Michel 257bb2e4b7 [SECURITY] Prevent reading of any local files in reportlab (CVE-2026-57535) 2026-06-25 16:19:38 +02:00
Raphael MichelandRaphael Michel 3953d57c2e [SECURITY] Disable outbound and file access for reportlab (CVE-2026-57535) 2026-06-25 16:17:49 +02:00
Mira WellerandRaphael Michel 2f1025c3a6 [SECURITY] Fix reflected XSS in redirection page (CVE-2026-57533) 2026-06-25 16:17:49 +02:00
Mira WellerandRaphael Michel 19f4dce267 [SECURITY] Fix stored XSS in ticket confirmation page (CVE-2026-13225) 2026-06-25 16:17:49 +02:00
Mira WellerandRaphael Michel e44f91530a [SECURITY] Hardening: Don't use |safe on confirm_messages 2026-06-25 16:17:49 +02:00
Raphael Michel d985fd61a1 Bump to 2026.3.3 2026-06-09 13:23:50 +02:00
Richard SchreiberandGitHub 803964da0e [SECURITY] Reusable media export: Hide giftcard secret (CVE-2026-11764 backport) (#6262) 2026-06-09 13:20:27 +02:00
Raphael Michel 7e6df3d427 Bump to 2026.3.2 2026-05-27 16:29:35 +02:00
Raphael Michel 7b93cc57db [SECURITY] Add missing session check for cached files (CVE-2026-9712) 2026-05-27 16:29:26 +02:00
Raphael Michel 21d62c5078 Bump version to 2026.3.1 2026-04-08 13:58:36 +02:00
Raphael MichelandRaphael Michel 988dc112ac [SECURITY] API: Add missing event filter for check-ins 2026-04-08 13:58:23 +02:00
44 changed files with 482 additions and 449 deletions
+3 -2
View File
@@ -10,9 +10,10 @@ tests:
- cd src - cd src
- python manage.py check - python manage.py check
- make all compress - make all compress
- playwright install
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100 - PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
except: except:
- pypi - '/^v.*$/'
pypi: pypi:
stage: release stage: release
image: image:
@@ -35,7 +36,7 @@ pypi:
- twine check dist/* - twine check dist/*
- twine upload dist/* - twine upload dist/*
only: only:
- pypi - '/^v.*$/'
artifacts: artifacts:
paths: paths:
- src/dist/ - src/dist/
+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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2026.3.0" __version__ = "2026.3.5"
+1 -1
View File
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read' permission = 'event.orders:read'
def get_queryset(self): def get_queryset(self):
qs = Checkin.all.filter().select_related( qs = Checkin.all.filter(list__event=self.request.event).select_related(
"position", "position",
"device", "device",
) )
+3 -1
View File
@@ -69,7 +69,9 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '', date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id 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 '', 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 '', # we cannot determine here whether user has permission organizer.giftcards:read
# so default to not showing giftcard secret
medium.linked_giftcard.secret[:3] + "" if medium.linked_giftcard_id else '',
medium.notes, medium.notes,
] ]
yield row yield row
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime import datetime
import logging import logging
import math import math
import re
import textwrap import textwrap
import unicodedata
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from io import BytesIO 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.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new, FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
reshaper, normalize_text, register_ttf_font_if_new, reshaper,
) )
from pretix.presale.style import get_fonts 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'])) register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text): def _normalize(self, text):
# reportlab does not support unicode combination characters # alias kept for plugin compatibility
# It's important we do this before we use ArabicReshaper return normalize_text(text)
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
def _upper(self, val): def _upper(self, val):
# We uppercase labels, but not in every language # We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read() return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None): def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean( # For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
text, # fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
tags=set(tags) if tags else set() # tags.
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')) return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable): class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas): def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to), p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height) p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(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) 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 invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(self.invoice.full_invoice_from), self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom'] style=self.stylesheet['InvoiceFrom']
) )
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height) p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas): def _draw_event(self, canvas):
def shorten(txt): def shorten(txt):
txt = str(txt) txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip() p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading: while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + '' 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) p_size = p.wrap(self.event_width, self.event_height)
return txt return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else: else:
p_str = shorten(self.invoice.event.name) 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.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(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]) 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() type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text: if type_info_text:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
type_info_text, type_info_text,
self.stylesheet['WarningBlock'] self.stylesheet['WarningBlock']
)) ))
if self.invoice.custom_field: if self.invoice.custom_field:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
'{}: {}'.format( '{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)), str(self.invoice.event.settings.invoice_address_custom_field),
self._clean_text(self.invoice.custom_field), self.invoice.custom_field,
), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.internal_reference: if self.invoice.internal_reference:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format( pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference), reference=self.invoice.internal_reference,
)), ),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_vat_id: if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' + pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self._clean_text(self.invoice.invoice_to_vat_id),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.invoice_to_beneficiary: if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' + pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self._clean_text(self.invoice.invoice_to_beneficiary),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [ story = [
NextPageTemplate('FirstPage'), NextPageTemplate('FirstPage'),
FontFallbackParagraph( PlainTextParagraph(
self._normalize( (
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU' pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice') 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'] self.stylesheet['Heading1']
), ),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
] ]
if has_taxes: if has_taxes:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)] )]
else: else:
tdata = [( tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']), PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)] )]
def _group_key(line): def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5 max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal'] p_style = self.stylesheet['Normal']
for __ in range(1000): for __ in range(1000):
p = FontFallbackParagraph( p = PlainTextParagraph(
self._clean_text(curr_description, tags=['br']), curr_description,
p_style p_style
) )
h = p.wrap(max_width, doc.height)[1] h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice # Group together at the end of the invoice
request_show_service_date = period_line request_show_service_date = period_line
elif period_line: elif period_line:
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
period_line, period_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency), net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_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, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
localize(tax_rate) + " %", localize(tax_rate) + " %",
FontFallbackParagraph( PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format( single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency), price=money_filter(gross_value, self.invoice.event.currency),
) )
description_p_list.append(FontFallbackParagraph( description_p_list.append(PlainTextParagraph(
single_price_line, single_price_line,
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
tdata.append(( tdata.append((
description_p_list.pop(0), description_p_list.pop(0),
str(len(lines)), str(len(lines)),
FontFallbackParagraph( PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes: if has_taxes:
tdata.append([ 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) money_filter(total, self.invoice.event.currency)
]) ])
else: else:
tdata.append([ 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) money_filter(total, self.invoice.event.currency)
]) ])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum pending_sum = self.invoice.order.pending_sum
if pending_sum != total: if pending_sum != total:
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] + [PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)] [money_filter(pending_sum - total, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)] [money_filter(pending_sum, self.invoice.event.currency)]
) )
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount') s=Sum('amount')
)['s'] or Decimal('0.00') )['s'] or Decimal('0.00')
tdata.append( 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 ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)] [money_filter(giftcard_sum, self.invoice.event.currency)]
) )
tdata.append( tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] + [PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) + (['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)] [money_filter(total - giftcard_sum, self.invoice.event.currency)]
) )
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm)) story.append(Spacer(1, 10 * mm))
if request_show_service_date: if request_show_service_date:
story.append(FontFallbackParagraph( story.append(PlainTextParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)), pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
if self.invoice.payment_provider_text: if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph( story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text), self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal'] self.stylesheet['Normal']
)) ))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular), ('FONTNAME', (0, 0), (-1, -1), self.font_regular),
] ]
thead = [ thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']), PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']), PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
'' ''
] ]
tdata = [thead] tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue continue
tax = taxvalue_map[idx] tax = taxvalue_map[idx]
tdata.append([ 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 - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency), money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency), money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata)) table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm)) story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([ story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']), PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table table
])) ]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax net = gross - tax
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), '' fmt(net), fmt(gross), fmt(tax), ''
]) ])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([ story.append(KeepTogether([
Spacer(1, height=2 * mm), Spacer(1, height=2 * mm),
FontFallbackParagraph( PlainTextParagraph(
self._normalize(pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:' '{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), 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'] self.stylesheet['Fineprint']
), ),
Spacer(1, height=3 * mm), Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate: elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate) foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm)) story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize( story.append(PlainTextParagraph(
pgettext( pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.' '{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate), ).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))), total=fmt(foreign_total)),
self.stylesheet['Fineprint'] self.stylesheet['Fineprint']
)) ))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas): def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from: if not self.invoice.invoice_from:
return return
c = [ c = self.invoice.address_invoice_from.strip().split('\n')
self._clean_text(l) p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm) 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) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas) 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) _draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
] ]
p = FontFallbackParagraph( p = PlainTextParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")), 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) 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) w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = [] i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage: 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( pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(), date_range=self.invoice.event.get_date_range_display(),
), ),
+1 -1
View File
@@ -1045,7 +1045,7 @@ class Renderer:
except: except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) 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 return p, ad, lineheight
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict): def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
@@ -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 i18n %}
{% load rich_text %} {% load rich_text %}
{% load static %} {% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %} {% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %} {% block content %}
<i class="fa fa-link fa-fw big-icon"></i> <i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details"> <div class="error-details">
<h1>{% trans "Redirect" %}</h1> <h1>{% trans "Redirect" %}</h1>
<h3> <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 }}. The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %} {% endblocktrans %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
+2 -4
View File
@@ -42,8 +42,6 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
from bleach.linkifier import build_email_re from bleach.linkifier import build_email_re
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
@@ -54,6 +52,7 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
register = template.Library() register = template.Library()
@@ -158,8 +157,7 @@ def safelink_callback(attrs, new=False):
""" """
url = html.unescape(attrs.get((None, 'href'), '/')) 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:'): 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'] = safelink(url)
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank' attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener' attrs[None, 'rel'] = 'noopener'
return attrs 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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
import logging
import urllib.parse import urllib.parse
from django.core import signing from django.core import signing
@@ -26,6 +27,8 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request): def _is_samesite_referer(request):
referer = request.headers.get('referer') referer = request.headers.get('referer')
@@ -42,11 +45,16 @@ def _is_samesite_referer(request):
def redir_view(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: try:
url = signer.unsign(request.GET.get('url', '')) url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
except signing.BadSignature: 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): if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url) u = urllib.parse.urlparse(url)
@@ -55,11 +63,26 @@ def redir_view(request):
'url': url, 'url': url,
}) })
if framebreak:
r = render(request, 'pretixbase/framebreak.html', {
'url': url,
})
r.xframe_options_exempt = True
return r
r = HttpResponseRedirect(url) r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex' r['X-Robots-Tag'] = 'noindex'
return r return r
def safelink(url): def safelink(url, framebreak=False):
signer = signing.Signer(salt='safe-redirect') url = str(url)
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(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
+1 -1
View File
@@ -212,7 +212,7 @@ class AuditLogMiddleware:
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated: if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False): if getattr(request.user, "is_hijacked", False):
hijack_history = request.session.get('hijack_history', 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')) ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if ss: if ss:
ss.logs.create( 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.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6 style_small.fontSize = 6
story = [ story = [
Paragraph("System report", headlinestyle), PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle), PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._usage_table(), self._usage_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle), PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
self._tech_table(), self._tech_table(),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle), PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small), PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle), PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small), PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm), Spacer(1, 5 * mm),
] ]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0), ("RIGHTPADDING", (-1, 0), (-1, -1), 0),
] ]
tdata = [ tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], [PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)], [PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)], [PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)], [PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[ [
Paragraph("Database engine:", style), PlainTextParagraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style), PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
], ],
] ]
table = Table(tdata, colWidths=colwidths, repeatRows=0) table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year year_last = now().year
tdata = [ tdata = [
[ [
Paragraph(l, style_small_head) PlainTextParagraph(l, style_small_head)
for l in ( for l in (
"Time frame", "Time frame",
"Currency", "Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append( tdata.append(
( (
Paragraph( PlainTextParagraph(
date_format(first_day, "M Y") date_format(first_day, "M Y")
+ " " + " "
+ date_format(after_day - timedelta(days=1), "M Y"), + date_format(after_day - timedelta(days=1), "M Y"),
style_small, style_small,
), ),
Paragraph(c, style_small), PlainTextParagraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "", PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "", PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "", PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small), PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
) )
) )
@@ -19,9 +19,7 @@
{% endif %} {% endif %}
</h1> </h1>
<script type="application/json" id="editor-data"> {{ layout|json_script:"editor-data" }}
{{ layout|safe }}
</script>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
<div class="panel panel-default panel-pdf-editor"> <div class="panel panel-default panel-pdf-editor">
+1 -1
View File
@@ -289,7 +289,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['pdf'] = self.get_current_background() ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables() ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images() ctx['images'] = self.get_images()
ctx['layout'] = json.dumps(self.get_current_layout()) ctx['layout'] = self.get_current_layout()
ctx['title'] = self.title ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales] ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize 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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
import hmac
import json import json
from contextlib import contextmanager from contextlib import contextmanager
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import ( 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.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse 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.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
@@ -230,7 +233,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
hijacked = self.object hijacked = self.object
hijack_history = request.session.get("hijack_history", []) 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 = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}" backend = f"{backend.__module__}.{backend.__class__.__name__}"
@@ -259,8 +270,21 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijs = request.session['hijacker_session'] hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", []) hijack_history = request.session.get("hijack_history", [])
hijacked = request.user hijacked = request.user
user_pk = hijack_history.pop() prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk) 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 = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}" backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session): with signals.no_update_last_login(), keep_session_age(request.session):
+5
View File
@@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig):
def ready(self): def ready(self):
from .monkeypatching import monkeypatch_all_at_ready from .monkeypatching import monkeypatch_all_at_ready
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']
+21
View File
@@ -23,9 +23,12 @@ import types
from datetime import datetime from datetime import datetime
from http import cookies from http import cookies
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image from PIL import Image
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from pretix.helpers.reportlab import ThumbnailingImageReader
def monkeypatch_vobject_performance(): def monkeypatch_vobject_performance():
""" """
@@ -95,8 +98,26 @@ def monkeypatch_cookie_morsel():
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned") 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_all_at_ready(): def monkeypatch_all_at_ready():
monkeypatch_vobject_performance() monkeypatch_vobject_performance()
monkeypatch_pillow_safer() monkeypatch_pillow_safer()
monkeypatch_requests_timeout() monkeypatch_requests_timeout()
monkeypatch_cookie_morsel() monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings from django.conf import settings
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image from PIL import Image
from reportlab.lib.styles import ParagraphStyle from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts 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): class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs): def __init__(self, text, style=None, *args, **kwargs):
if style is None: if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text: if not text:
return True return True
font = pdfmetrics.getFont(font_name) font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all( return all(
ord(c) in font.face.charToGlyph or not c.isprintable() ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
return family 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): def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont 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.control.forms.widgets import Select2
from pretix.helpers.filenames import safe_for_filename from pretix.helpers.filenames import safe_for_filename
from pretix.helpers.iter import chunked_iterable 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.helpers.templatetags.jsonfield import JSONExtract
from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -344,7 +344,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
] ]
story = [ story = [
FontFallbackParagraph( PlainTextParagraph(
cl.name, cl.name,
headlinestyle headlinestyle
), ),
@@ -352,7 +352,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
if cl.subevent: if cl.subevent:
story += [ story += [
Spacer(1, 3 * mm), Spacer(1, 3 * mm),
FontFallbackParagraph( PlainTextParagraph(
'{} ({} {})'.format( '{} ({} {})'.format(
cl.subevent.name, cl.subevent.name,
cl.subevent.get_date_range_display(), cl.subevent.get_date_range_display(),
@@ -382,10 +382,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
headrowstyle.fontName = 'OpenSansBd' headrowstyle.fontName = 'OpenSansBd'
for q in questions: for q in questions:
txt = str(q.question) txt = str(q.question)
p = FontFallbackParagraph(txt, headrowstyle) p = PlainTextParagraph(txt, headrowstyle)
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm: while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
txt = txt[:len(txt) - 50] + "..." txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, headrowstyle) p = PlainTextParagraph(txt, headrowstyle)
tdata[0].append(p) tdata[0].append(p)
qs = self._get_queryset(cl, form_data) qs = self._get_queryset(cl, form_data)
@@ -432,8 +432,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '', CBFlowable(bool(op.last_checked_in)) if not op.blocked else '',
'' if op.order.status != Order.STATUS_PAID else '', '' if op.order.status != Order.STATUS_PAID else '',
op.order.code, op.order.code,
FontFallbackParagraph(name, self.get_style()), PlainTextParagraph(name, self.get_style()),
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()), PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
] ]
acache = {} acache = {}
if op.addon_to: if op.addon_to:
@@ -444,10 +444,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
for q in questions: for q in questions:
txt = acache.get(q.pk, '') txt = acache.get(q.pk, '')
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>') 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: while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
txt = txt[:len(txt) - 50] + "..." txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, self.get_style()) p = PlainTextParagraph(txt, self.get_style())
row.append(p) row.append(p)
if op.order.status != Order.STATUS_PAID: if op.order.status != Order.STATUS_PAID:
tstyledata += [ tstyledata += [
+4 -10
View File
@@ -34,7 +34,6 @@
import json import json
import logging import logging
import urllib.parse
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
@@ -42,7 +41,6 @@ import paypalrestsdk
import paypalrestsdk.exceptions import paypalrestsdk.exceptions
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse 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.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.paypal.api import Api from pretix.plugins.paypal.api import Api
from pretix.plugins.paypal.models import ReferencedPayPalObject from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -348,14 +347,9 @@ class Paypal(BasePaymentProvider):
request.session['payment_paypal_id'] = payment.id request.session['payment_paypal_id'] = payment.id
for link in payment.links: for link in payment.links:
if link.method == "REDIRECT" and link.rel == "approval_url": if link.method == "REDIRECT" and link.rel == "approval_url":
if request.session.get('iframe_session', False): return safelink(link.href, framebreak=True)
signer = signing.Signer(salt='safe-redirect') else:
return ( return str(link.href)
build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' +
urllib.parse.quote(signer.sign(link.href))
)
else:
return str(link.href)
else: else:
messages.error(request, _('We had trouble communicating with PayPal')) messages.error(request, _('We had trouble communicating with PayPal'))
logger.error('Error on creating payment: ' + str(payment.error)) logger.error('Error on creating payment: ' + str(payment.error))
@@ -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 django.urls import include, re_path
from .views import abort, oauth_disconnect, redirect_view, success from .views import abort, oauth_disconnect, success
event_patterns = [ event_patterns = [
re_path(r'^paypal/', include([ re_path(r'^paypal/', include([
re_path(r'^abort/$', abort, name='abort'), re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'), 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})/abort/', abort, name='abort'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'), 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
import paypalrestsdk.exceptions import paypalrestsdk.exceptions
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ 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.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -61,21 +58,6 @@ from pretix.plugins.paypal.payment import Paypal
logger = logging.getLogger('pretix.plugins.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): def success(request, *args, **kwargs):
pid = request.GET.get('paymentId') pid = request.GET.get('paymentId')
token = request.GET.get('token') 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 django.urls import include, re_path
from .views import ( from .views import (
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view, PayView, XHRView, abort, isu_disconnect, isu_return, success, webhook,
success, webhook,
) )
event_patterns = [ event_patterns = [
re_path(r'^paypal2/', include([ re_path(r'^paypal2/', include([
re_path(r'^abort/$', abort, name='abort'), re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'), 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'^xhr/$', XHRView.as_view(), name='xhr'),
re_path(r'^pay/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'), 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'), 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 decimal import Decimal
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.core.cache import cache from django.core.cache import cache
from django.db import transaction from django.db import transaction
from django.db.models import Sum from django.db.models import Sum
from django.http import ( from django.http import Http404, HttpResponse, JsonResponse
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@@ -104,21 +101,6 @@ class PaypalOrderView:
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else '')) }) + ('?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(csrf_exempt, name='dispatch')
@method_decorator(xframe_options_exempt, 'dispatch') @method_decorator(xframe_options_exempt, 'dispatch')
class XHRView(View): 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.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.platypus import ( from reportlab.platypus import (
KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle, KeepTogether, PageTemplate, Spacer, Table, TableStyle,
) )
from pretix.base.exporter import BaseExporter from pretix.base.exporter import BaseExporter
@@ -49,7 +49,7 @@ from pretix.base.timeframes import (
resolve_timeframe_to_datetime_start_inclusive_end_exclusive, resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
) )
from pretix.control.forms.filter import get_all_payment_providers 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 from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [ tdata = [
[ [
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold), PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold),
FontFallbackParagraph(_("Price"), tstyle_bold_right), PlainTextParagraph(_("Price"), tstyle_bold_right),
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right), PlainTextParagraph(_("Tax rate"), tstyle_bold_right),
FontFallbackParagraph("#", tstyle_bold_right), PlainTextParagraph("#", tstyle_bold_right),
FontFallbackParagraph(_("Net total"), tstyle_bold_right), PlainTextParagraph(_("Net total"), tstyle_bold_right),
FontFallbackParagraph(_("Tax total"), tstyle_bold_right), PlainTextParagraph(_("Tax total"), tstyle_bold_right),
FontFallbackParagraph(_("Gross total"), tstyle_bold_right), PlainTextParagraph(_("Gross total"), tstyle_bold_right),
] ]
] ]
@@ -347,12 +347,12 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if e != last_group: if e != last_group:
if last_group_head_idx > 0 and e is not None: 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][4] = PlainTextParagraph(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][5] = PlainTextParagraph(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][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata.append( tdata.append(
[ [
FontFallbackParagraph( PlainTextParagraph(
e, e,
tstyle_bold, tstyle_bold,
), ),
@@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
text = self._transaction_row_label(r) text = self._transaction_row_label(r)
tdata.append( tdata.append(
[ [
FontFallbackParagraph(text, tstyle), PlainTextParagraph(text, tstyle),
Paragraph( PlainTextParagraph(
money_filter(r["price"], currency) money_filter(r["price"], currency)
if "price" in r and r["price"] is not None if "price" in r and r["price"] is not None
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right), PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
Paragraph(str(r["sum_cont"]), tstyle_right), PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
Paragraph( PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
), ),
Paragraph(money_filter(r["sum_tax"], currency), tstyle_right), PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right),
Paragraph(money_filter(r["sum_price"], currency), tstyle_right), PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right),
] ]
) )
sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"] 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"] sum_tax_by_group += r["sum_tax"]
if last_group_head_idx > 0 and last_group is not None: 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][4] = PlainTextParagraph(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][5] = PlainTextParagraph(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][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
if len(sum_tax_by_tax_rate) > 1: if len(sum_tax_by_tax_rate) > 1:
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True): for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle), PlainTextParagraph(_("Sum"), tstyle),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right), PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum_price_by_tax_rate[tax_rate] sum_price_by_tax_rate[tax_rate]
- sum_tax_by_tax_rate[tax_rate], - sum_tax_by_tax_rate[tax_rate],
@@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right
), ),
Paragraph( PlainTextParagraph(
money_filter(sum_price_by_tax_rate[tax_rate], currency), money_filter(sum_price_by_tax_rate[tax_rate], currency),
tstyle_right, tstyle_right,
), ),
@@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle_bold), PlainTextParagraph(_("Sum"), tstyle_bold),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph("", tstyle_right), PlainTextParagraph("", tstyle_right),
Paragraph("", tstyle_bold_right), PlainTextParagraph("", tstyle_bold_right),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(sum_price_by_tax_rate.values()) sum(sum_price_by_tax_rate.values())
- sum(sum_tax_by_tax_rate.values()), - sum(sum_tax_by_tax_rate.values()),
@@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum(sum_tax_by_tax_rate.values()), currency), money_filter(sum(sum_tax_by_tax_rate.values()), currency),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(sum(sum_price_by_tax_rate.values()), currency), money_filter(sum(sum_price_by_tax_rate.values()), currency),
tstyle_bold_right, tstyle_bold_right,
), ),
@@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [ tdata = [
[ [
FontFallbackParagraph(_("Payment method"), tstyle_bold), PlainTextParagraph(_("Payment method"), tstyle_bold),
FontFallbackParagraph(_("Payments"), tstyle_bold_right), PlainTextParagraph(_("Payments"), tstyle_bold_right),
FontFallbackParagraph(_("Refunds"), tstyle_bold_right), PlainTextParagraph(_("Refunds"), tstyle_bold_right),
FontFallbackParagraph(_("Total"), tstyle_bold_right), PlainTextParagraph(_("Total"), tstyle_bold_right),
] ]
] ]
@@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
for p in providers: for p in providers:
tdata.append( tdata.append(
[ [
Paragraph(provider_names.get(p, p), tstyle), PlainTextParagraph(provider_names.get(p, p), tstyle),
FontFallbackParagraph( PlainTextParagraph(
money_filter(payments_by_provider[p], currency) money_filter(payments_by_provider[p], currency)
if p in payments_by_provider if p in payments_by_provider
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter(refunds_by_provider[p], currency) money_filter(refunds_by_provider[p], currency)
if p in refunds_by_provider if p in refunds_by_provider
else "", else "",
tstyle_right, tstyle_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
payments_by_provider.get(p, Decimal("0.00")) payments_by_provider.get(p, Decimal("0.00"))
- refunds_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( tdata.append(
[ [
FontFallbackParagraph(_("Sum"), tstyle_bold), PlainTextParagraph(_("Sum"), tstyle_bold),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(payments_by_provider.values(), Decimal("0.00")), currency sum(payments_by_provider.values(), Decimal("0.00")), currency
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(refunds_by_provider.values(), Decimal("0.00")), currency sum(refunds_by_provider.values(), Decimal("0.00")), currency
), ),
tstyle_bold_right, tstyle_bold_right,
), ),
Paragraph( PlainTextParagraph(
money_filter( money_filter(
sum(payments_by_provider.values(), Decimal("0.00")) sum(payments_by_provider.values(), Decimal("0.00"))
- sum(refunds_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 open_before = tx_before - p_before + r_before
tdata.append( tdata.append(
[ [
FontFallbackParagraph( PlainTextParagraph(
_("Pending payments at {datetime}").format( _("Pending payments at {datetime}").format(
datetime=date_format( datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone( (df_start - datetime.timedelta.resolution).astimezone(
@@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyle, tstyle,
), ),
"", "",
Paragraph(money_filter(open_before, currency), tstyle_right), PlainTextParagraph(money_filter(open_before, currency), tstyle_right),
] ]
) )
else: else:
@@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Orders"), tstyle), PlainTextParagraph(_("Orders"), tstyle),
Paragraph("+", tstyle_center), PlainTextParagraph("+", tstyle_center),
Paragraph(money_filter(tx_during, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during, currency), tstyle_right),
] ]
) )
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Payments"), tstyle), PlainTextParagraph(_("Payments"), tstyle),
Paragraph("-", tstyle_center), PlainTextParagraph("-", tstyle_center),
Paragraph(money_filter(p_during, currency), tstyle_right), PlainTextParagraph(money_filter(p_during, currency), tstyle_right),
] ]
) )
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Refunds"), tstyle), PlainTextParagraph(_("Refunds"), tstyle),
Paragraph("+", tstyle_center), PlainTextParagraph("+", tstyle_center),
Paragraph(money_filter(r_during, currency), tstyle_right), PlainTextParagraph(money_filter(r_during, currency), tstyle_right),
] ]
) )
open_after = open_before + tx_during - p_during + r_during open_after = open_before + tx_during - p_during + r_during
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Pending payments at {datetime}").format( _("Pending payments at {datetime}").format(
datetime=date_format( datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone( ((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold, tstyle_bold,
), ),
Paragraph("=", tstyle_center), PlainTextParagraph("=", tstyle_center),
Paragraph(money_filter(open_after, currency), tstyle_bold_right), PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
] ]
) )
tstyledata += [ tstyledata += [
@@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
) )
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Total gift card value at {datetime}").format( _("Total gift card value at {datetime}").format(
datetime=date_format( datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone( (df_start - datetime.timedelta.resolution).astimezone(
@@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle, tstyle,
), ),
Paragraph(money_filter(tx_before, currency), tstyle_right), PlainTextParagraph(money_filter(tx_before, currency), tstyle_right),
] ]
) )
else: else:
@@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle), PlainTextParagraph(_("Gift card transactions (credit)"), tstyle),
Paragraph(money_filter(tx_during_pos, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right),
] ]
) )
@@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00") ] or Decimal("0.00")
tdata.append( tdata.append(
[ [
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle), PlainTextParagraph(_("Gift card transactions (debit)"), tstyle),
Paragraph(money_filter(tx_during_neg, currency), tstyle_right), PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right),
] ]
) )
open_after = tx_before + tx_during_pos + tx_during_neg open_after = tx_before + tx_during_pos + tx_during_neg
tdata.append( tdata.append(
[ [
Paragraph( PlainTextParagraph(
_("Total gift card value at {datetime}").format( _("Total gift card value at {datetime}").format(
datetime=date_format( datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone( ((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
), ),
tstyle_bold, tstyle_bold,
), ),
Paragraph(money_filter(open_after, currency), tstyle_bold_right), PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
] ]
) )
tstyledata += [ tstyledata += [
@@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
style_small.leading = 10 style_small.leading = 10
story = [ story = [
FontFallbackParagraph(self.verbose_name, style_h1), PlainTextParagraph(self.verbose_name, style_h1),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
FontFallbackParagraph( PlainTextParagraph(
"<br />".join(escape(f) for f in self.describe_filters(form_data)), "\n".join(escape(f) for f in self.describe_filters(form_data)),
style_small, style_small,
), ),
] ]
@@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s: if s:
story += [ story += [
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2), PlainTextParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s: if s:
story += [ story += [
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2), PlainTextParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
KeepTogether( KeepTogether(
[ [
FontFallbackParagraph(_("Open items") + c_head, style_h2), PlainTextParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s *s
] ]
@@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm), Spacer(0, 8 * mm),
KeepTogether( KeepTogether(
[ [
FontFallbackParagraph(_("Gift cards") + c_head, style_h2), PlainTextParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm), Spacer(0, 3 * mm),
*s, *s,
] ]
+13 -13
View File
@@ -70,7 +70,7 @@ from pretix.base.timeframes import (
) )
from pretix.control.forms.filter import OverviewFilterForm from pretix.control.forms.filter import OverviewFilterForm
from pretix.helpers.reportlab import ( from pretix.helpers.reportlab import (
FontFallbackParagraph, register_ttf_font_if_new, PlainTextParagraph, register_ttf_font_if_new,
) )
from pretix.presale.style import get_fonts from pretix.presale.style import get_fonts
@@ -282,7 +282,7 @@ class OverviewReport(Report):
headlinestyle.fontSize = 15 headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd' headlinestyle.fontName = 'OpenSansBd'
story = [ 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) Spacer(1, 5 * mm)
] ]
return story return story
@@ -292,7 +292,7 @@ class OverviewReport(Report):
if form_data.get('date_axis') and form_data.get('date_range'): 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) d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
story += [ 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')], 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 '', start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end 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')) subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
except SubEvent.DoesNotExist: except SubEvent.DoesNotExist:
subevent = self.form_data.get('subevent') 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)) story.append(Spacer(1, 5 * mm))
if form_data.get('subevent_date_range'): 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) d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
story += [ story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format( PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=_('Event date'), axis=_('Event date'),
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '', 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 '', end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '',
@@ -384,13 +384,13 @@ class OverviewReport(Report):
tdata = [ tdata = [
[ [
_('Product'), _('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: for tup in items_by_category:
if tup[0]: if tup[0]:
tdata.append([ tdata.append([
FontFallbackParagraph(str(tup[0]), tstyle_bold) PlainTextParagraph(str(tup[0]), tstyle_bold)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(tup[0].num[l][0])) tdata[-1].append(str(tup[0].num[l][0]))
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places)) tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
for item in tup[1]: for item in tup[1]:
tdata.append([ tdata.append([
FontFallbackParagraph(str(item), tstyle) PlainTextParagraph(str(item), tstyle)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(item.num[l][0])) tdata[-1].append(str(item.num[l][0]))
@@ -436,7 +436,7 @@ class OverviewReport(Report):
if item.has_variations: if item.has_variations:
for var in item.all_variations: for var in item.all_variations:
tdata.append([ tdata.append([
FontFallbackParagraph(" " + str(var), tstyle) PlainTextParagraph(" " + str(var), tstyle)
]) ])
for l, s in states: for l, s in states:
tdata[-1].append(str(var.num[l][0])) 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))) tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
story = [ 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) Spacer(1, 5 * mm)
] ]
tdata = [ tdata = [
+3 -15
View File
@@ -46,7 +46,6 @@ import stripe
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.db import transaction from django.db import transaction
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template 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.plugins import get_all_plugins
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries from pretix.helpers.countries import CachedCountries
from pretix.helpers.http import get_client_ip from pretix.helpers.http import get_client_ip
@@ -745,15 +745,7 @@ class StripeMethod(BasePaymentProvider):
def redirect(self, request, url): def redirect(self, request, url):
if request.session.get('iframe_session', False): if request.session.get('iframe_session', False):
return ( return safelink(url, framebreak=True)
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')
)
else: else:
return str(url) return str(url)
@@ -1053,11 +1045,7 @@ class StripeMethod(BasePaymentProvider):
'hash': payment.order.tagged_secret('plugins:stripe'), 'hash': payment.order.tagged_secret('plugins:stripe'),
}) })
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False): 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({ return safelink(url, framebreak=True)
'url': url,
'session': {},
}, salt='safe-redirect')
return url return url
def _confirm_payment_intent(self, request, payment): 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 ( from .views import (
OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView, OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView,
oauth_disconnect, oauth_return, redirect_view, webhook, oauth_disconnect, oauth_return, webhook,
) )
event_patterns = [ event_patterns = [
re_path(r'^stripe/', include([ re_path(r'^stripe/', include([
event_url(r'^webhook/$', webhook, name='webhook', require_live=False), 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'^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]+)/$', ScaView.as_view(), name='sca'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$', re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$',
+2 -31
View File
@@ -34,13 +34,11 @@
import json import json
import logging import logging
import urllib.parse
import requests import requests
from django.contrib import messages from django.contrib import messages
from django.core import signing
from django.db import transaction 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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator 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.control.views.organizer import OrganizerDetailViewMixin
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.http import redirect_to_url 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.forms import OrganizerStripeSettingsForm
from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.plugins.stripe.tasks import ( from pretix.plugins.stripe.tasks import (
@@ -74,28 +72,6 @@ from pretix.plugins.stripe.tasks import (
logger = logging.getLogger('pretix.plugins.stripe') 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() @scopes_disabled()
def oauth_return(request, *args, **kwargs): def oauth_return(request, *args, **kwargs):
import stripe import stripe
@@ -514,11 +490,6 @@ class StripeOrderView:
return self.request.event.get_payment_providers()[self.payment.provider] return self.request.event.get_payment_providers()[self.payment.provider]
def _redirect_to_order(self): 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={ return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={
'order': self.order.code, 'order': self.order.code,
'secret': self.order.secret 'secret': self.order.secret
@@ -229,6 +229,11 @@ class TicketRendererViewSet(viewsets.ViewSet):
@action(detail=False, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)') @action(detail=False, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def download(self, *args, **kwargs): def download(self, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs['cfid']) cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if not cf.allowed_for_session(self.request, "ticketoutputpdf-api"):
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
if cf.file: if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type) resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore") resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
@@ -265,6 +270,7 @@ class TicketRendererViewSet(viewsets.ViewSet):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=False) cf = CachedFile(web_download=False)
cf.bind_to_session(self.request, "ticketoutputpdf-api")
cf.date = now() cf.date = now()
cf.expires = now() + timedelta(hours=24) cf.expires = now() + timedelta(hours=24)
cf.save() cf.save()
+2 -1
View File
@@ -50,6 +50,7 @@ from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils import translation from django.utils import translation
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.translation import ( from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy, get_language, gettext_lazy as _, pgettext_lazy,
) )
@@ -1614,7 +1615,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
meta_info = { meta_info = {
'contact_form_data': self.cart_session.get('contact_form_data', {}), 'contact_form_data': self.cart_session.get('contact_form_data', {}),
'confirm_messages': [ 'confirm_messages': [
str(m) for m in self.confirm_messages.values() conditional_escape(str(m)) for m in self.confirm_messages.values()
] ]
} }
api_meta = {} api_meta = {}
+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 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". 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 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. As with all event plugin signals, the ``sender`` keyword argument will contain the event.
""" """
@@ -176,7 +176,7 @@
<div class="checkbox"> <div class="checkbox">
<label for="input_confirm_{{ key }}"> <label for="input_confirm_{{ key }}">
<input type="checkbox" class="checkbox" value="yes" name="confirm_{{ key }}" id="input_confirm_{{ key }}" required> <input type="checkbox" class="checkbox" value="yes" name="confirm_{{ key }}" id="input_confirm_{{ key }}" required>
{{ desc|safe }} {{ desc }}
</label> </label>
</div> </div>
{% endfor %} {% endfor %}
@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load eventurl %} {% load eventurl %}
{% load urlreplace %} {% load urlreplace %}
{% load static %}
{% block content %} {% block content %}
{% if cart_namespace %} {% if cart_namespace %}
@@ -23,9 +24,8 @@
class="btn btn-primary btn-lg" target="_blank"> class="btn btn-primary btn-lg" target="_blank">
{% trans "Continue in new tab" %} {% trans "Continue in new tab" %}
</a> </a>
<script> {{ url|json_script:"framebreak-url" }}
window.open('{{ url|escapejs }}'); <script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</script>
</div> </div>
{% else %} {% else %}
<h1>{% trans "Cookies not supported" %}</h1> <h1>{% trans "Cookies not supported" %}</h1>
@@ -4,6 +4,7 @@
{% load eventsignal %} {% load eventsignal %}
{% load money %} {% load money %}
{% load eventurl %} {% load eventurl %}
{% load wrap_in %}
{% block title %}{% trans "Registration details" %}{% endblock %} {% block title %}{% trans "Registration details" %}{% endblock %}
{% block content %} {% block content %}
<h2 class="h1"> <h2 class="h1">
@@ -48,7 +49,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <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 This order is managed for you by {{ email }}. Please contact them for any questions regarding
payment, cancellation or changes to this order. payment, cancellation or changes to this order.
{% endblocktrans %} {% endblocktrans %}
-1
View File
@@ -536,7 +536,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
**pass_through_url_params, **pass_through_url_params,
}) })
}) })
r._csp_ignore = True
return r 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()): 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()):
-1
View File
@@ -120,7 +120,6 @@ class WaitingView(EventViewMixin, FormView):
request.event, "presale:event.waitinglist", kwargs={'cart_namespace': kwargs.get('cart_namespace')} 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())) ) + '?' + url_replace(request, 'require_cookie', '', 'iframe', '', 'locale', request.GET.get('locale', get_language_without_region()))
}) })
r._csp_ignore = True
return r return r
if not self.itemvars: 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)
+1 -1
View File
@@ -119,7 +119,7 @@ def test_linkify_abs(link):
assert markdown_compile_email(input) == f"<p>{output}</p>" assert markdown_compile_email(input) == f"<p>{output}</p>"
signer = signing.Signer(salt='safe-redirect') signer = signing.Signer(salt='safelink-url')
@pytest.mark.parametrize( @pytest.mark.parametrize(
+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" />',
)