From f3a8ce39f1c4bbc54fea8b0c2a85ba17bffd8ae2 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Tue, 23 Jun 2026 21:57:05 +0200 Subject: [PATCH 1/9] [SECURITY] Fix XSS in ticket layout JSON (CVE-2026-57532) --- src/pretix/control/templates/pretixcontrol/pdf/index.html | 4 +--- src/pretix/control/views/pdf.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pretix/control/templates/pretixcontrol/pdf/index.html b/src/pretix/control/templates/pretixcontrol/pdf/index.html index 0935a6c12..814c1d37e 100644 --- a/src/pretix/control/templates/pretixcontrol/pdf/index.html +++ b/src/pretix/control/templates/pretixcontrol/pdf/index.html @@ -19,9 +19,7 @@ {% endif %} - + {{ layout|json_script:"editor-data" }}
diff --git a/src/pretix/control/views/pdf.py b/src/pretix/control/views/pdf.py index 03f21c731..e8c5d855d 100644 --- a/src/pretix/control/views/pdf.py +++ b/src/pretix/control/views/pdf.py @@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView): ctx['pdf'] = self.get_current_background() ctx['variables'] = self.get_variables() ctx['images'] = self.get_images() - ctx['layout'] = json.dumps(self.get_current_layout()) + ctx['layout'] = self.get_current_layout() ctx['title'] = self.title ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales] ctx['maxfilesize'] = self.maxfilesize -- 2.52.0 From e087dd39b8abbfddeab3b590b7866b8f3d04ba48 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Fri, 19 Jun 2026 14:13:16 +0200 Subject: [PATCH 2/9] [SECURITY] Hardening: Don't use |safe on confirm_messages --- src/pretix/presale/checkoutflow.py | 3 ++- src/pretix/presale/signals.py | 2 +- .../templates/pretixpresale/event/checkout_confirm.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 76c488624..e6ea9d372 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -51,6 +51,7 @@ from django.http import HttpResponseNotAllowed, JsonResponse from django.shortcuts import redirect from django.utils import translation from django.utils.functional import cached_property +from django.utils.html import conditional_escape from django.utils.translation import ( get_language, gettext_lazy as _, pgettext_lazy, ) @@ -1634,7 +1635,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): meta_info = { 'contact_form_data': self.cart_session.get('contact_form_data', {}), 'confirm_messages': [ - str(m) for m in self.confirm_messages.values() + conditional_escape(str(m)) for m in self.confirm_messages.values() ] } api_meta = {} diff --git a/src/pretix/presale/signals.py b/src/pretix/presale/signals.py index 38ae0dca0..1111f00b2 100644 --- a/src/pretix/presale/signals.py +++ b/src/pretix/presale/signals.py @@ -144,7 +144,7 @@ checkout_confirm_messages = EventPluginSignal() This signal is sent out to retrieve short messages that need to be acknowledged by the user before the order can be completed. This is typically used for something like "accept the terms and conditions". Receivers are expected to return a dictionary where the keys are globally unique identifiers for the -message and the values can be arbitrary HTML. +message and the values can be a SafeString containing arbitrary HTML, or a string that will be HTML-escaped. As with all event plugin signals, the ``sender`` keyword argument will contain the event. """ diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html index 40db91acc..ad47ad429 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html @@ -176,7 +176,7 @@
{% endfor %} -- 2.52.0 From 71f6ade5d6e454e9737aa02738add3b806d88c53 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Fri, 19 Jun 2026 13:31:09 +0200 Subject: [PATCH 3/9] [SECURITY] Fix stored XSS in ticket confirmation page (CVE-2026-13225) --- src/pretix/presale/templates/pretixpresale/event/position.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pretix/presale/templates/pretixpresale/event/position.html b/src/pretix/presale/templates/pretixpresale/event/position.html index a8b735f10..2a89b69ee 100644 --- a/src/pretix/presale/templates/pretixpresale/event/position.html +++ b/src/pretix/presale/templates/pretixpresale/event/position.html @@ -4,6 +4,7 @@ {% load eventsignal %} {% load money %} {% load eventurl %} +{% load wrap_in %} {% block title %}{% trans "Registration details" %}{% endblock %} {% block content %}

@@ -48,7 +49,7 @@

- {% blocktrans trimmed with email=""|add:order.email|add:""|safe %} + {% blocktrans trimmed with email=order.email|wrap_in:"strong" %} This order is managed for you by {{ email }}. Please contact them for any questions regarding payment, cancellation or changes to this order. {% endblocktrans %} -- 2.52.0 From e2661f304d019ac9d4ba1b1d87e93ae3e7681430 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Fri, 19 Jun 2026 13:22:19 +0200 Subject: [PATCH 4/9] [SECURITY] Fix reflected XSS in redirection page (CVE-2026-57533) --- .../base/templates/pretixbase/redirect.html | 3 +- src/pretix/helpers/templatetags/wrap_in.py | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/pretix/helpers/templatetags/wrap_in.py diff --git a/src/pretix/base/templates/pretixbase/redirect.html b/src/pretix/base/templates/pretixbase/redirect.html index 6c8404b4d..62048c122 100644 --- a/src/pretix/base/templates/pretixbase/redirect.html +++ b/src/pretix/base/templates/pretixbase/redirect.html @@ -2,13 +2,14 @@ {% load i18n %} {% load rich_text %} {% load static %} +{% load wrap_in %} {% block title %}{% trans "Redirect" %}{% endblock %} {% block content %}

{% trans "Redirect" %}

- {% blocktrans trimmed with host=""|add:hostname|add:""|safe %} + {% blocktrans trimmed with host=hostname|wrap_in:'strong' %} The link you clicked on wants to redirect you to a destination on the website {{ host }}. {% endblocktrans %} {% blocktrans trimmed %} diff --git a/src/pretix/helpers/templatetags/wrap_in.py b/src/pretix/helpers/templatetags/wrap_in.py new file mode 100644 index 000000000..e08bd2c90 --- /dev/null +++ b/src/pretix/helpers/templatetags/wrap_in.py @@ -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 . +# +# 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 +# . +# +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}>{{}}', content) -- 2.52.0 From 80601cc013c49969fefc35bfd18ca6bcfb21ccbd Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 9 Jun 2026 19:32:56 +0200 Subject: [PATCH 5/9] [SECURITY] Disable outbound and file access for reportlab (CVE-2026-57535) --- src/pretix/helpers/apps.py | 5 +++ src/tests/helpers/test_reportlab.py | 52 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/tests/helpers/test_reportlab.py diff --git a/src/pretix/helpers/apps.py b/src/pretix/helpers/apps.py index c51aa3ed3..a2ef2d7d9 100644 --- a/src/pretix/helpers/apps.py +++ b/src/pretix/helpers/apps.py @@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig): def ready(self): from .monkeypatching import monkeypatch_all_at_ready monkeypatch_all_at_ready() + + # Ensure reportlab does not make any calls to the internet + from reportlab import rl_config + rl_config.trustedHosts = [] + rl_config.trustedSchemes = ['data'] diff --git a/src/tests/helpers/test_reportlab.py b/src/tests/helpers/test_reportlab.py new file mode 100644 index 000000000..dcd8f0f5a --- /dev/null +++ b/src/tests/helpers/test_reportlab.py @@ -0,0 +1,52 @@ +# +# 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 . +# +# 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 +# . +# +import pytest +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(OSError, match="Cannot open resource"): + Paragraph( + '', + ) + + +def test_file_access_disabled_scheme(monkeypatch): + with pytest.raises(OSError, match="Cannot open resource"): + Paragraph( + '', + ) + + +@pytest.mark.xfail +def test_file_access_disabled_direct(monkeypatch): + # Unfortunately this is not prevented by the reprotlab config, but the risk is low since only valid images + # can be used. + with pytest.raises(OSError, match="Cannot open resource"): + Paragraph( + '', + ) -- 2.52.0 From 53d8aa567997fd3d7c62e9d1c23da4e69295de9f Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 9 Jun 2026 19:46:04 +0200 Subject: [PATCH 6/9] [SECURITY] Prevent reading of any local files in reportlab (CVE-2026-57535) --- src/pretix/helpers/apps.py | 2 +- src/pretix/helpers/monkeypatching.py | 21 +++++++++++++++++++++ src/tests/helpers/test_reportlab.py | 10 ++++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/pretix/helpers/apps.py b/src/pretix/helpers/apps.py index a2ef2d7d9..847fccb6d 100644 --- a/src/pretix/helpers/apps.py +++ b/src/pretix/helpers/apps.py @@ -30,7 +30,7 @@ class PretixHelpersConfig(AppConfig): from .monkeypatching import monkeypatch_all_at_ready monkeypatch_all_at_ready() - # Ensure reportlab does not make any calls to the internet + # 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'] diff --git a/src/pretix/helpers/monkeypatching.py b/src/pretix/helpers/monkeypatching.py index 1eb46fb5f..2e309255d 100644 --- a/src/pretix/helpers/monkeypatching.py +++ b/src/pretix/helpers/monkeypatching.py @@ -27,6 +27,7 @@ from datetime import datetime from http import cookies from django.conf import settings +from django.core.exceptions import SuspiciousFileOperation from PIL import Image from requests.adapters import HTTPAdapter from urllib3.connection import HTTPConnection, HTTPSConnection @@ -40,6 +41,8 @@ from urllib3.util.connection import ( ) from urllib3.util.timeout import _DEFAULT_TIMEOUT +from pretix.helpers.reportlab import ThumbnailingImageReader + _cgnat_net = ipaddress.ip_network('100.64.0.0/10') @@ -230,9 +233,27 @@ def monkeypatch_cookie_morsel(): cookies.Morsel._reserved.setdefault("partitioned", "Partitioned") +def monkeypatch_reportlab_imagereader(): + from reportlab.lib import utils + old_init = utils.ImageReader.__init__ + + def new_init(self, fileName, ident=None): # noqa + if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'): + if not isinstance(self, ThumbnailingImageReader): + # ThumbnailingImageReader is only used by us explicitly and not by using 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(): monkeypatch_vobject_performance() monkeypatch_pillow_safer() monkeypatch_requests_timeout() monkeypatch_urllib3_ssrf_protection() monkeypatch_cookie_morsel() + monkeypatch_reportlab_imagereader() diff --git a/src/tests/helpers/test_reportlab.py b/src/tests/helpers/test_reportlab.py index dcd8f0f5a..354e2cbd1 100644 --- a/src/tests/helpers/test_reportlab.py +++ b/src/tests/helpers/test_reportlab.py @@ -20,6 +20,7 @@ # . # import pytest +from django.core.exceptions import SuspiciousFileOperation from reportlab.platypus import Paragraph @@ -29,24 +30,21 @@ def test_http_access_disabled(monkeypatch): monkeypatch.setattr('socket.socket', guard) - with pytest.raises(OSError, match="Cannot open resource"): + with pytest.raises(SuspiciousFileOperation, match="should not be reading images from disk"): Paragraph( '', ) def test_file_access_disabled_scheme(monkeypatch): - with pytest.raises(OSError, match="Cannot open resource"): + with pytest.raises(SuspiciousFileOperation, match="should not be reading images from disk"): Paragraph( '', ) -@pytest.mark.xfail def test_file_access_disabled_direct(monkeypatch): - # Unfortunately this is not prevented by the reprotlab config, but the risk is low since only valid images - # can be used. - with pytest.raises(OSError, match="Cannot open resource"): + with pytest.raises(SuspiciousFileOperation, match="should not be reading images from disk"): Paragraph( '', ) -- 2.52.0 From cdeb3609f351d793c6be54c985b66305cc99faae Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 12 Jun 2026 13:56:29 +0200 Subject: [PATCH 7/9] [SECURITY] Properly escape HTML tags in PDF generation (CVE-2026-57535) --- src/pretix/base/invoicing/pdf.py | 166 ++++++++---------- src/pretix/base/pdf.py | 2 +- src/pretix/control/sysreport.py | 45 ++--- src/pretix/helpers/reportlab.py | 39 ++++ src/pretix/plugins/checkinlists/exporters.py | 18 +- .../plugins/reports/accountingreport.py | 156 ++++++++-------- src/pretix/plugins/reports/exporters.py | 26 +-- 7 files changed, 239 insertions(+), 213 deletions(-) diff --git a/src/pretix/base/invoicing/pdf.py b/src/pretix/base/invoicing/pdf.py index b47b376bd..ca464cb50 100644 --- a/src/pretix/base/invoicing/pdf.py +++ b/src/pretix/base/invoicing/pdf.py @@ -22,9 +22,7 @@ import datetime import logging import math -import re import textwrap -import unicodedata from collections import defaultdict from decimal import Decimal from io import BytesIO @@ -58,8 +56,8 @@ from pretix.base.services.currencies import SOURCE_NAMES from pretix.base.signals import register_invoice_renderers from pretix.base.templatetags.money import money_filter from pretix.helpers.reportlab import ( - FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new, - reshaper, + FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader, + normalize_text, register_ttf_font_if_new, reshaper, ) from pretix.presale.style import get_fonts @@ -259,18 +257,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype'])) def _normalize(self, text): - # reportlab does not support unicode combination characters - # It's important we do this before we use ArabicReshaper - text = unicodedata.normalize("NFKC", text) - - # reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper - # to resolve all ligatures and python-bidi to switch RTL texts. - try: - text = "
".join(get_display(reshaper.reshape(l)) for l in re.split("
", text)) - except: - logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) - - return text + # alias kept for plugin compatibility + return normalize_text(text) def _upper(self, val): # We uppercase labels, but not in every language @@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): return 'invoice.pdf', 'application/pdf', buffer.read() def _clean_text(self, text, tags=None): - return self._normalize(bleach.clean( - text, - tags=set(tags) if tags else set() - ).strip().replace('
', '
').replace('\n', '
\n')) + # For backwards compatibility with customer content, we need to support tags like
and in a few text + # fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed + # tags. + return self._normalize( + bleach.clean( + text, + tags=set(tags) if tags else set() + ).strip().replace('
', '
').replace('\n', '
\n') + ) class PaidMarker(Flowable): @@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): invoice_to_top = 52 * mm def _draw_invoice_to(self, canvas): - p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to), - style=self.stylesheet['Normal']) + p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal']) p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height) p_size = p.wrap(self.invoice_to_width, self.invoice_to_height) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top) @@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): invoice_from_top = 17 * mm def _draw_invoice_from(self, canvas): - p = FontFallbackParagraph( - self._clean_text(self.invoice.full_invoice_from), + p = PlainTextParagraph( + self.invoice.full_invoice_from, style=self.stylesheet['InvoiceFrom'] ) p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height) @@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): def _draw_event(self, canvas): def shorten(txt): txt = str(txt) - txt = bleach.clean(txt, tags=set()).strip() - p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '
\n')), style=self.stylesheet['Normal']) + p = PlainTextParagraph(txt, style=self.stylesheet['Normal']) p_size = p.wrap(self.event_width, self.event_height) while p_size[1] > 2 * self.stylesheet['Normal'].leading: txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…' - p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '
\n')), style=self.stylesheet['Normal']) + p = PlainTextParagraph(txt, style=self.stylesheet['Normal']) p_size = p.wrap(self.event_width, self.event_height) return txt @@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): else: p_str = shorten(self.invoice.event.name) - p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '
\n')), style=self.stylesheet['Normal']) + p = PlainTextParagraph(p_str, style=self.stylesheet['Normal']) p.wrapOn(canvas, self.event_width, self.event_height) p_size = p.wrap(self.event_width, self.event_height) p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1]) @@ -645,39 +636,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): type_info_text = self.invoice.transmission_type_instance.pdf_info_text() if type_info_text: - story.append(FontFallbackParagraph( + story.append(PlainTextParagraph( type_info_text, self.stylesheet['WarningBlock'] )) if self.invoice.custom_field: - story.append(FontFallbackParagraph( + story.append(PlainTextParagraph( '{}: {}'.format( - self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)), - self._clean_text(self.invoice.custom_field), + str(self.invoice.event.settings.invoice_address_custom_field), + self.invoice.custom_field, ), self.stylesheet['Normal'] )) if self.invoice.internal_reference: - story.append(FontFallbackParagraph( - self._normalize(pgettext('invoice', 'Customer reference: {reference}').format( - reference=self._clean_text(self.invoice.internal_reference), - )), + story.append(PlainTextParagraph( + pgettext('invoice', 'Customer reference: {reference}').format( + reference=self.invoice.internal_reference, + ), self.stylesheet['Normal'] )) if self.invoice.invoice_to_vat_id: - story.append(FontFallbackParagraph( - self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' + - self._clean_text(self.invoice.invoice_to_vat_id), + story.append(PlainTextParagraph( + pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id, self.stylesheet['Normal'] )) if self.invoice.invoice_to_beneficiary: - story.append(FontFallbackParagraph( - self._normalize(pgettext('invoice', 'Beneficiary')) + ':
' + - self._clean_text(self.invoice.invoice_to_beneficiary), + story.append(PlainTextParagraph( + pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary, self.stylesheet['Normal'] )) @@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): story = [ NextPageTemplate('FirstPage'), - FontFallbackParagraph( - self._normalize( + PlainTextParagraph( + ( pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU' else pgettext('invoice', 'Invoice') - ) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')), + ) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'), self.stylesheet['Heading1'] ), Spacer(1, 5 * mm), @@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): ] if has_taxes: tdata = [( - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']), + PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']), + PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']), + PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']), + PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']), + PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']), )] else: tdata = [( - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']), + PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']), + PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']), + PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']), )] def _group_key(line): @@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): max_height = self.stylesheet['Normal'].leading * 5 p_style = self.stylesheet['Normal'] for __ in range(1000): - p = FontFallbackParagraph( - self._clean_text(curr_description, tags=['br']), + p = PlainTextParagraph( + curr_description, p_style ) h = p.wrap(max_width, doc.height)[1] @@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): # Group together at the end of the invoice request_show_service_date = period_line elif period_line: - description_p_list.append(FontFallbackParagraph( + description_p_list.append(PlainTextParagraph( period_line, self.stylesheet['Fineprint'] )) @@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): net_price=money_filter(net_value, self.invoice.event.currency), gross_price=money_filter(gross_value, self.invoice.event.currency), ) - description_p_list.append(FontFallbackParagraph( + description_p_list.append(PlainTextParagraph( single_price_line, self.stylesheet['Fineprint'] )) @@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): description_p_list.pop(0), str(len(lines)), localize(tax_rate) + " %", - FontFallbackParagraph( + PlainTextParagraph( money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight'] ), - FontFallbackParagraph( + PlainTextParagraph( money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight'] ), @@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): single_price_line = pgettext('invoice', 'Single price: {price}').format( price=money_filter(gross_value, self.invoice.event.currency), ) - description_p_list.append(FontFallbackParagraph( + description_p_list.append(PlainTextParagraph( single_price_line, self.stylesheet['Fineprint'] )) tdata.append(( description_p_list.pop(0), str(len(lines)), - FontFallbackParagraph( + PlainTextParagraph( money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), self.stylesheet['NormalRight'] ), @@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): if has_taxes: tdata.append([ - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '', + PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '', money_filter(total, self.invoice.event.currency) ]) else: tdata.append([ - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', + PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', money_filter(total, self.invoice.event.currency) ]) @@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): pending_sum = self.invoice.order.pending_sum if pending_sum != total: tdata.append( - [FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] + + [PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] + (['', '', ''] if has_taxes else ['']) + [money_filter(pending_sum - total, self.invoice.event.currency)] ) tdata.append( - [FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] + + [PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] + (['', '', ''] if has_taxes else ['']) + [money_filter(pending_sum, self.invoice.event.currency)] ) @@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): s=Sum('amount') )['s'] or Decimal('0.00') tdata.append( - [FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] + + [PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] + (['', '', ''] if has_taxes else ['']) + [money_filter(giftcard_sum, self.invoice.event.currency)] ) tdata.append( - [FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] + + [PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] + (['', '', ''] if has_taxes else ['']) + [money_filter(total - giftcard_sum, self.invoice.event.currency)] ) @@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): story.append(Spacer(1, 10 * mm)) if request_show_service_date: - story.append(FontFallbackParagraph( - self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)), + story.append(PlainTextParagraph( + pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date), self.stylesheet['Normal'] )) if self.invoice.payment_provider_text: story.append(FontFallbackParagraph( - self._normalize(self.invoice.payment_provider_text), + self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']), self.stylesheet['Normal'] )) @@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): ('FONTNAME', (0, 0), (-1, -1), self.font_regular), ] thead = [ - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']), - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']), + PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']), + PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']), + PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']), + PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']), '' ] tdata = [thead] @@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): continue tax = taxvalue_map[idx] tdata.append([ - FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), + PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']), money_filter(gross - tax, self.invoice.event.currency), money_filter(gross, self.invoice.event.currency), money_filter(tax, self.invoice.event.currency), @@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): table.setStyle(TableStyle(tstyledata)) story.append(Spacer(5 * mm, 5 * mm)) story.append(KeepTogether([ - FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']), + PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']), table ])) @@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): net = gross - tax tdata.append([ - FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']), + PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']), fmt(net), fmt(gross), fmt(tax), '' ]) @@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): story.append(KeepTogether([ Spacer(1, height=2 * mm), - FontFallbackParagraph( - self._normalize(pgettext( + PlainTextParagraph( + pgettext( 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' '{date}, this corresponds to:' ).format(rate=localize(self.invoice.foreign_currency_rate), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), - date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))), + date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")), self.stylesheet['Fineprint'] ), Spacer(1, height=3 * mm), @@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate: foreign_total = round_decimal(total * self.invoice.foreign_currency_rate) story.append(Spacer(1, 5 * mm)) - story.append(FontFallbackParagraph(self._normalize( + story.append(PlainTextParagraph( pgettext( 'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on ' '{date}, the invoice total corresponds to {total}.' ).format(rate=localize(self.invoice.foreign_currency_rate), date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"), authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"), - total=fmt(foreign_total))), + total=fmt(foreign_total)), self.stylesheet['Fineprint'] )) @@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer): def _draw_invoice_from(self, canvas): if not self.invoice.address_invoice_from: return - c = [ - self._clean_text(l) - for l in self.invoice.address_invoice_from.strip().split('\n') - ] - p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender']) + c = self.invoice.address_invoice_from.strip().split('\n') + p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender']) p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm) p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm) super()._draw_invoice_from(canvas) @@ -1225,8 +1211,8 @@ class Modern1Renderer(ClassicInvoiceRenderer): _draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs) ] - p = FontFallbackParagraph( - self._normalize(date_format(self.invoice.date, "DATE_FORMAT")), + p = PlainTextParagraph( + date_format(self.invoice.date, "DATE_FORMAT"), style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2) ) w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize) @@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer): i = [] if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage: - i.append(FontFallbackParagraph( + i.append(PlainTextParagraph( pgettext('invoice', 'Event date: {date_range}').format( date_range=self.invoice.event.get_date_range_display(), ), diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index a8cb2fc2c..a63c8f316 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -1062,7 +1062,7 @@ class Renderer: except: logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) - p = Paragraph(text, style=style) + p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above return p, ad, lineheight def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict): diff --git a/src/pretix/control/sysreport.py b/src/pretix/control/sysreport.py index 6beb0e4fc..e2a908cb0 100644 --- a/src/pretix/control/sysreport.py +++ b/src/pretix/control/sysreport.py @@ -38,6 +38,7 @@ from pretix import __version__ from pretix.base.models import Order, OrderPayment, Transaction from pretix.base.plugins import get_all_plugins from pretix.base.templatetags.money import money_filter +from pretix.helpers.reportlab import PlainTextParagraph from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.settings import DATA_DIR @@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin): style_small.fontSize = 6 story = [ - Paragraph("System report", headlinestyle), + PlainTextParagraph("System report", headlinestyle), Spacer(1, 5 * mm), - Paragraph("Usage", subheadlinestyle), + PlainTextParagraph("Usage", subheadlinestyle), Spacer(1, 5 * mm), self._usage_table(), Spacer(1, 5 * mm), - Paragraph("Installed versions", subheadlinestyle), + PlainTextParagraph("Installed versions", subheadlinestyle), Spacer(1, 5 * mm), self._tech_table(), Spacer(1, 5 * mm), - Paragraph("Plugins", subheadlinestyle), + PlainTextParagraph("Plugins", subheadlinestyle), Spacer(1, 5 * mm), - Paragraph(self._get_plugin_versions(), style_small), + PlainTextParagraph(self._get_plugin_versions(), style_small), Spacer(1, 5 * mm), - Paragraph("Custom templates", subheadlinestyle), + PlainTextParagraph("Custom templates", subheadlinestyle), Spacer(1, 5 * mm), - Paragraph(self._get_custom_templates(), style_small), + PlainTextParagraph(self._get_custom_templates(), style_small), Spacer(1, 5 * mm), ] @@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin): ("RIGHTPADDING", (-1, 0), (-1, -1), 0), ] tdata = [ - [Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], - [Paragraph("pretix version:", style), Paragraph(__version__, style)], - [Paragraph("Python version:", style), Paragraph(sys.version, style)], - [Paragraph("Platform:", style), Paragraph(platform.platform(), style)], + [PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], + [PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)], + [PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)], + [PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)], [ - Paragraph("Database engine:", style), - Paragraph(settings.DATABASES["default"]["ENGINE"], style), + PlainTextParagraph("Database engine:", style), + PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style), ], ] table = Table(tdata, colWidths=colwidths, repeatRows=0) @@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin): year_last = now().year tdata = [ [ - Paragraph(l, style_small_head) + PlainTextParagraph(l, style_small_head) for l in ( "Time frame", "Currency", @@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin): tdata.append( ( - Paragraph( + PlainTextParagraph( date_format(first_day, "M Y") + " – " + date_format(after_day - timedelta(days=1), "M Y"), style_small, ), - Paragraph(c, style_small), - Paragraph(str(orders_count), style_small) if i == 0 else "", - Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), - Paragraph(str(testmode_count), style_small) if i == 0 else "", - Paragraph(str(unconfirmed_count), style_small) if i == 0 else "", - Paragraph(str(revenue_data.get("c") or 0), style_small), - Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), + PlainTextParagraph(c, style_small), + PlainTextParagraph(str(orders_count), style_small) if i == 0 else "", + PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), + PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "", + PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "", + PlainTextParagraph(str(revenue_data.get("c") or 0), style_small), + PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), ) ) diff --git a/src/pretix/helpers/reportlab.py b/src/pretix/helpers/reportlab.py index 5a39ac4a5..618dbb87e 100644 --- a/src/pretix/helpers/reportlab.py +++ b/src/pretix/helpers/reportlab.py @@ -20,14 +20,19 @@ # . # import logging +import re +import unicodedata from arabic_reshaper import ArabicReshaper +from bidi import get_display from django.conf import settings from django.utils.functional import SimpleLazyObject +from django.utils.html import escape from PIL import Image from reportlab.lib.styles import ParagraphStyle from reportlab.lib.utils import ImageReader from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import Paragraph from pretix.presale.style import get_fonts @@ -70,6 +75,20 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={ })) +def normalize_text(text: str) -> str: + # reportlab does not support unicode combination characters + # It's important we do this before we use ArabicReshaper + text = unicodedata.normalize("NFKC", text) + + # reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper + # to resolve all ligatures and python-bidi to switch RTL texts. + try: + text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text)) + except: + logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text))) + return text + + class FontFallbackParagraph(Paragraph): def __init__(self, text, style=None, *args, **kwargs): if style is None: @@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph): if not text: return True font = pdfmetrics.getFont(font_name) + if not isinstance(font, TTFont): + return True return all( ord(c) in font.face.charToGlyph or not c.isprintable() for c in text @@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph): return family +class PlainTextParagraph(FontFallbackParagraph): + def __init__(self, text, style=None, linebreaks=True, *args, **kwargs): + if not isinstance(text, str): + if hasattr(text, '__html__'): + raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph") + text = str(text) + + # Normalize unicode and apply reshaping + text = normalize_text(text) + + # Escape any HTML in the text + text = escape(text) + + if linebreaks: + text = text.strip().replace("\n", "
\n") + super().__init__(text, style, *args, **kwargs) + + def register_ttf_font_if_new(name, path): from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py index 8bd8763fc..40d98add1 100644 --- a/src/pretix/plugins/checkinlists/exporters.py +++ b/src/pretix/plugins/checkinlists/exporters.py @@ -64,7 +64,7 @@ from pretix.base.timeframes import ( from pretix.control.forms.widgets import Select2 from pretix.helpers.filenames import safe_for_filename from pretix.helpers.iter import chunked_iterable -from pretix.helpers.reportlab import FontFallbackParagraph +from pretix.helpers.reportlab import PlainTextParagraph from pretix.helpers.templatetags.jsonfield import JSONExtract from pretix.plugins.reports.exporters import ReportlabExportMixin @@ -343,7 +343,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter): ] story = [ - FontFallbackParagraph( + PlainTextParagraph( cl.name, headlinestyle ), @@ -351,7 +351,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter): if cl.subevent: story += [ Spacer(1, 3 * mm), - FontFallbackParagraph( + PlainTextParagraph( '{} ({} {})'.format( cl.subevent.name, cl.subevent.get_date_range_display(), @@ -381,10 +381,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter): headrowstyle.fontName = 'OpenSansBd' for q in questions: txt = str(q.question) - p = FontFallbackParagraph(txt, headrowstyle) + p = PlainTextParagraph(txt, headrowstyle) while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm: txt = txt[:len(txt) - 50] + "..." - p = FontFallbackParagraph(txt, headrowstyle) + p = PlainTextParagraph(txt, headrowstyle) tdata[0].append(p) qs = self._get_queryset(cl, form_data) @@ -431,8 +431,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter): CBFlowable(bool(op.last_checked_in)) if not op.blocked else '—', '✘' if op.order.status != Order.STATUS_PAID else '✔', op.order.code, - FontFallbackParagraph(name, self.get_style()), - FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('
', '
'), self.get_style()), + PlainTextParagraph(name, self.get_style()), + PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('
', '
'), self.get_style()), ] acache = {} if op.addon_to: @@ -443,10 +443,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter): for q in questions: txt = acache.get(q.pk, '') txt = bleach.clean(txt, tags={'br'}).strip().replace('
', '
') - p = FontFallbackParagraph(txt, self.get_style()) + p = PlainTextParagraph(txt, self.get_style()) while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm: txt = txt[:len(txt) - 50] + "..." - p = FontFallbackParagraph(txt, self.get_style()) + p = PlainTextParagraph(txt, self.get_style()) row.append(p) if op.order.status != Order.STATUS_PAID: tstyledata += [ diff --git a/src/pretix/plugins/reports/accountingreport.py b/src/pretix/plugins/reports/accountingreport.py index 17315eb02..6c09ad141 100644 --- a/src/pretix/plugins/reports/accountingreport.py +++ b/src/pretix/plugins/reports/accountingreport.py @@ -36,7 +36,7 @@ from reportlab.lib import colors, pagesizes from reportlab.lib.enums import TA_CENTER, TA_RIGHT from reportlab.lib.units import mm from reportlab.platypus import ( - KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle, + KeepTogether, PageTemplate, Spacer, Table, TableStyle, ) from pretix.base.exporter import BaseExporter @@ -49,7 +49,7 @@ from pretix.base.timeframes import ( resolve_timeframe_to_datetime_start_inclusive_end_exclusive, ) from pretix.control.forms.filter import get_all_payment_providers -from pretix.helpers.reportlab import FontFallbackParagraph +from pretix.helpers.reportlab import PlainTextParagraph from pretix.plugins.reports.exporters import ReportlabExportMixin @@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): tdata = [ [ - FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold), - FontFallbackParagraph(_("Price"), tstyle_bold_right), - FontFallbackParagraph(_("Tax rate"), tstyle_bold_right), - FontFallbackParagraph("#", tstyle_bold_right), - FontFallbackParagraph(_("Net total"), tstyle_bold_right), - FontFallbackParagraph(_("Tax total"), tstyle_bold_right), - FontFallbackParagraph(_("Gross total"), tstyle_bold_right), + PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold), + PlainTextParagraph(_("Price"), tstyle_bold_right), + PlainTextParagraph(_("Tax rate"), tstyle_bold_right), + PlainTextParagraph("#", tstyle_bold_right), + PlainTextParagraph(_("Net total"), tstyle_bold_right), + PlainTextParagraph(_("Tax total"), tstyle_bold_right), + PlainTextParagraph(_("Gross total"), tstyle_bold_right), ] ] @@ -347,12 +347,12 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): if e != last_group: if last_group_head_idx > 0 and e is not None: - tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), - tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), - tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), + tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), + tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), + tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), tdata.append( [ - FontFallbackParagraph( + PlainTextParagraph( e, tstyle_bold, ), @@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): text = self._transaction_row_label(r) tdata.append( [ - FontFallbackParagraph(text, tstyle), - Paragraph( + PlainTextParagraph(text, tstyle), + PlainTextParagraph( money_filter(r["price"], currency) if "price" in r and r["price"] is not None else "", tstyle_right, ), - Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right), - Paragraph(str(r["sum_cont"]), tstyle_right), - Paragraph( + PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right), + PlainTextParagraph(str(r["sum_cont"]), tstyle_right), + PlainTextParagraph( money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right ), - Paragraph(money_filter(r["sum_tax"], currency), tstyle_right), - Paragraph(money_filter(r["sum_price"], currency), tstyle_right), + PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right), + PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right), ] ) sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"] @@ -398,19 +398,19 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): sum_tax_by_group += r["sum_tax"] if last_group_head_idx > 0 and last_group is not None: - tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), - tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), - tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), + tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right), + tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right), + tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right), if len(sum_tax_by_tax_rate) > 1: for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True): tdata.append( [ - FontFallbackParagraph(_("Sum"), tstyle), - Paragraph("", tstyle_right), - Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right), - Paragraph("", tstyle_right), - Paragraph( + PlainTextParagraph(_("Sum"), tstyle), + PlainTextParagraph("", tstyle_right), + PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right), + PlainTextParagraph("", tstyle_right), + PlainTextParagraph( money_filter( sum_price_by_tax_rate[tax_rate] - sum_tax_by_tax_rate[tax_rate], @@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ), tstyle_right, ), - Paragraph( + PlainTextParagraph( money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right ), - Paragraph( + PlainTextParagraph( money_filter(sum_price_by_tax_rate[tax_rate], currency), tstyle_right, ), @@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): tdata.append( [ - FontFallbackParagraph(_("Sum"), tstyle_bold), - Paragraph("", tstyle_right), - Paragraph("", tstyle_right), - Paragraph("", tstyle_bold_right), - Paragraph( + PlainTextParagraph(_("Sum"), tstyle_bold), + PlainTextParagraph("", tstyle_right), + PlainTextParagraph("", tstyle_right), + PlainTextParagraph("", tstyle_bold_right), + PlainTextParagraph( money_filter( sum(sum_price_by_tax_rate.values()) - sum(sum_tax_by_tax_rate.values()), @@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ), tstyle_bold_right, ), - Paragraph( + PlainTextParagraph( money_filter(sum(sum_tax_by_tax_rate.values()), currency), tstyle_bold_right, ), - Paragraph( + PlainTextParagraph( money_filter(sum(sum_price_by_tax_rate.values()), currency), tstyle_bold_right, ), @@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): tdata = [ [ - FontFallbackParagraph(_("Payment method"), tstyle_bold), - FontFallbackParagraph(_("Payments"), tstyle_bold_right), - FontFallbackParagraph(_("Refunds"), tstyle_bold_right), - FontFallbackParagraph(_("Total"), tstyle_bold_right), + PlainTextParagraph(_("Payment method"), tstyle_bold), + PlainTextParagraph(_("Payments"), tstyle_bold_right), + PlainTextParagraph(_("Refunds"), tstyle_bold_right), + PlainTextParagraph(_("Total"), tstyle_bold_right), ] ] @@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): for p in providers: tdata.append( [ - Paragraph(provider_names.get(p, p), tstyle), - FontFallbackParagraph( + PlainTextParagraph(provider_names.get(p, p), tstyle), + PlainTextParagraph( money_filter(payments_by_provider[p], currency) if p in payments_by_provider else "", tstyle_right, ), - Paragraph( + PlainTextParagraph( money_filter(refunds_by_provider[p], currency) if p in refunds_by_provider else "", tstyle_right, ), - Paragraph( + PlainTextParagraph( money_filter( payments_by_provider.get(p, Decimal("0.00")) - refunds_by_provider.get(p, Decimal("0.00")), @@ -563,20 +563,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): tdata.append( [ - FontFallbackParagraph(_("Sum"), tstyle_bold), - Paragraph( + PlainTextParagraph(_("Sum"), tstyle_bold), + PlainTextParagraph( money_filter( sum(payments_by_provider.values(), Decimal("0.00")), currency ), tstyle_bold_right, ), - Paragraph( + PlainTextParagraph( money_filter( sum(refunds_by_provider.values(), Decimal("0.00")), currency ), tstyle_bold_right, ), - Paragraph( + PlainTextParagraph( money_filter( sum(payments_by_provider.values(), Decimal("0.00")) - sum(refunds_by_provider.values(), Decimal("0.00")), @@ -641,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): open_before = tx_before - p_before + r_before tdata.append( [ - FontFallbackParagraph( + PlainTextParagraph( _("Pending payments at {datetime}").format( datetime=date_format( (df_start - datetime.timedelta.resolution).astimezone( @@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): tstyle, ), "", - Paragraph(money_filter(open_before, currency), tstyle_right), + PlainTextParagraph(money_filter(open_before, currency), tstyle_right), ] ) else: @@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ] or Decimal("0.00") tdata.append( [ - FontFallbackParagraph(_("Orders"), tstyle), - Paragraph("+", tstyle_center), - Paragraph(money_filter(tx_during, currency), tstyle_right), + PlainTextParagraph(_("Orders"), tstyle), + PlainTextParagraph("+", tstyle_center), + PlainTextParagraph(money_filter(tx_during, currency), tstyle_right), ] ) tdata.append( [ - FontFallbackParagraph(_("Payments"), tstyle), - Paragraph("-", tstyle_center), - Paragraph(money_filter(p_during, currency), tstyle_right), + PlainTextParagraph(_("Payments"), tstyle), + PlainTextParagraph("-", tstyle_center), + PlainTextParagraph(money_filter(p_during, currency), tstyle_right), ] ) tdata.append( [ - FontFallbackParagraph(_("Refunds"), tstyle), - Paragraph("+", tstyle_center), - Paragraph(money_filter(r_during, currency), tstyle_right), + PlainTextParagraph(_("Refunds"), tstyle), + PlainTextParagraph("+", tstyle_center), + PlainTextParagraph(money_filter(r_during, currency), tstyle_right), ] ) open_after = open_before + tx_during - p_during + r_during tdata.append( [ - Paragraph( + PlainTextParagraph( _("Pending payments at {datetime}").format( datetime=date_format( ((df_end or now()) - datetime.timedelta.resolution).astimezone( @@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ), tstyle_bold, ), - Paragraph("=", tstyle_center), - Paragraph(money_filter(open_after, currency), tstyle_bold_right), + PlainTextParagraph("=", tstyle_center), + PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right), ] ) tstyledata += [ @@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ) tdata.append( [ - Paragraph( + PlainTextParagraph( _("Total gift card value at {datetime}").format( datetime=date_format( (df_start - datetime.timedelta.resolution).astimezone( @@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ), tstyle, ), - Paragraph(money_filter(tx_before, currency), tstyle_right), + PlainTextParagraph(money_filter(tx_before, currency), tstyle_right), ] ) else: @@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ] or Decimal("0.00") tdata.append( [ - FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle), - Paragraph(money_filter(tx_during_pos, currency), tstyle_right), + PlainTextParagraph(_("Gift card transactions (credit)"), tstyle), + PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right), ] ) @@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ] or Decimal("0.00") tdata.append( [ - FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle), - Paragraph(money_filter(tx_during_neg, currency), tstyle_right), + PlainTextParagraph(_("Gift card transactions (debit)"), tstyle), + PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right), ] ) open_after = tx_before + tx_during_pos + tx_during_neg tdata.append( [ - Paragraph( + PlainTextParagraph( _("Total gift card value at {datetime}").format( datetime=date_format( ((df_end or now()) - datetime.timedelta.resolution).astimezone( @@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): ), tstyle_bold, ), - Paragraph(money_filter(open_after, currency), tstyle_bold_right), + PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right), ] ) tstyledata += [ @@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): style_small.leading = 10 story = [ - FontFallbackParagraph(self.verbose_name, style_h1), + PlainTextParagraph(self.verbose_name, style_h1), Spacer(0, 3 * mm), - FontFallbackParagraph( - "
".join(escape(f) for f in self.describe_filters(form_data)), + PlainTextParagraph( + "\n".join(escape(f) for f in self.describe_filters(form_data)), style_small, ), ] @@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): if s: story += [ Spacer(0, 3 * mm), - FontFallbackParagraph(_("Orders") + c_head, style_h2), + PlainTextParagraph(_("Orders") + c_head, style_h2), Spacer(0, 3 * mm), *s ] @@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): if s: story += [ Spacer(0, 8 * mm), - FontFallbackParagraph(_("Payments") + c_head, style_h2), + PlainTextParagraph(_("Payments") + c_head, style_h2), Spacer(0, 3 * mm), *s ] @@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): Spacer(0, 8 * mm), KeepTogether( [ - FontFallbackParagraph(_("Open items") + c_head, style_h2), + PlainTextParagraph(_("Open items") + c_head, style_h2), Spacer(0, 3 * mm), *s ] @@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter): Spacer(0, 8 * mm), KeepTogether( [ - FontFallbackParagraph(_("Gift cards") + c_head, style_h2), + PlainTextParagraph(_("Gift cards") + c_head, style_h2), Spacer(0, 3 * mm), *s, ] diff --git a/src/pretix/plugins/reports/exporters.py b/src/pretix/plugins/reports/exporters.py index 6957ab396..98241865e 100644 --- a/src/pretix/plugins/reports/exporters.py +++ b/src/pretix/plugins/reports/exporters.py @@ -70,7 +70,7 @@ from pretix.base.timeframes import ( ) from pretix.control.forms.filter import OverviewFilterForm from pretix.helpers.reportlab import ( - FontFallbackParagraph, register_ttf_font_if_new, + PlainTextParagraph, register_ttf_font_if_new, ) from pretix.presale.style import get_fonts @@ -282,7 +282,7 @@ class OverviewReport(Report): headlinestyle.fontSize = 15 headlinestyle.fontName = 'OpenSansBd' story = [ - FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle), + PlainTextParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle), Spacer(1, 5 * mm) ] return story @@ -292,7 +292,7 @@ class OverviewReport(Report): if form_data.get('date_axis') and form_data.get('date_range'): d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone) story += [ - FontFallbackParagraph(_('{axis} between {start} and {end}').format( + PlainTextParagraph(_('{axis} between {start} and {end}').format( axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')], start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–', end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '–', @@ -305,13 +305,13 @@ class OverviewReport(Report): subevent = self.event.subevents.get(pk=self.form_data.get('subevent')) except SubEvent.DoesNotExist: subevent = self.form_data.get('subevent') - story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style())) + story.append(PlainTextParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style())) story.append(Spacer(1, 5 * mm)) if form_data.get('subevent_date_range'): d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone) story += [ - FontFallbackParagraph(_('{axis} between {start} and {end}').format( + PlainTextParagraph(_('{axis} between {start} and {end}').format( axis=_('Event date'), start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–', end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '–', @@ -384,13 +384,13 @@ class OverviewReport(Report): tdata = [ [ _('Product'), - FontFallbackParagraph(_('Canceled'), tstyle_th), + PlainTextParagraph(_('Canceled'), tstyle_th), '', - FontFallbackParagraph(_('Expired'), tstyle_th), + PlainTextParagraph(_('Expired'), tstyle_th), '', - FontFallbackParagraph(_('Approval pending'), tstyle_th), + PlainTextParagraph(_('Approval pending'), tstyle_th), '', - FontFallbackParagraph(_('Purchased'), tstyle_th), + PlainTextParagraph(_('Purchased'), tstyle_th), '', '', '', '', '' ], [ @@ -421,14 +421,14 @@ class OverviewReport(Report): for tup in items_by_category: if tup[0]: tdata.append([ - FontFallbackParagraph(str(tup[0]), tstyle_bold) + PlainTextParagraph(str(tup[0]), tstyle_bold) ]) for l, s in states: tdata[-1].append(str(tup[0].num[l][0])) tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places)) for item in tup[1]: tdata.append([ - FontFallbackParagraph(str(item), tstyle) + PlainTextParagraph(str(item), tstyle) ]) for l, s in states: tdata[-1].append(str(item.num[l][0])) @@ -436,7 +436,7 @@ class OverviewReport(Report): if item.has_variations: for var in item.all_variations: tdata.append([ - FontFallbackParagraph(" " + str(var), tstyle) + PlainTextParagraph(" " + str(var), tstyle) ]) for l, s in states: tdata[-1].append(str(var.num[l][0])) @@ -568,7 +568,7 @@ class OrderTaxListReportPDF(Report): tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0))) story = [ - FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle), + PlainTextParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle), Spacer(1, 5 * mm) ] tdata = [ -- 2.52.0 From 7faf6cfd80002d52f4fcec55add989cfbc3576a7 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 25 Jun 2026 16:40:44 +0200 Subject: [PATCH 8/9] Bump to 2026.5.2 --- src/pretix/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pretix/__init__.py b/src/pretix/__init__.py index 7807542d8..f46c131b1 100644 --- a/src/pretix/__init__.py +++ b/src/pretix/__init__.py @@ -19,4 +19,4 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -__version__ = "2026.5.1" +__version__ = "2026.5.2" -- 2.52.0 From ce2704f6fda18db5649262a31742e61dd3ab592d Mon Sep 17 00:00:00 2001 From: monsi Date: Wed, 1 Jul 2026 21:26:32 +0200 Subject: [PATCH 9/9] Remove gendered language from de/de_Informal translations --- src/pretix/locale/de/LC_MESSAGES/djangojs.po | 4 ++-- src/pretix/locale/de_Informal/LC_MESSAGES/djangojs.po | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pretix/locale/de/LC_MESSAGES/djangojs.po b/src/pretix/locale/de/LC_MESSAGES/djangojs.po index ca79c1705..40b6b84e8 100644 --- a/src/pretix/locale/de/LC_MESSAGES/djangojs.po +++ b/src/pretix/locale/de/LC_MESSAGES/djangojs.po @@ -161,12 +161,12 @@ msgstr "Bezahlte Bestellungen" #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 msgid "Attendees (ordered)" -msgstr "Teilnehmende (bestellt)" +msgstr "Teilnehmer (bestellt)" #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 msgid "Attendees (paid)" -msgstr "Teilnehmende (bezahlt)" +msgstr "Teilnehmer (bezahlt)" #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 msgid "Total revenue" diff --git a/src/pretix/locale/de_Informal/LC_MESSAGES/djangojs.po b/src/pretix/locale/de_Informal/LC_MESSAGES/djangojs.po index ba1788508..94c6174ac 100644 --- a/src/pretix/locale/de_Informal/LC_MESSAGES/djangojs.po +++ b/src/pretix/locale/de_Informal/LC_MESSAGES/djangojs.po @@ -161,12 +161,12 @@ msgstr "Bezahlte Bestellungen" #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 msgid "Attendees (ordered)" -msgstr "Teilnehmende (bestellt)" +msgstr "Teilnehmer (bestellt)" #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27 #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39 msgid "Attendees (paid)" -msgstr "Teilnehmende (bezahlt)" +msgstr "Teilnehmer (bezahlt)" #: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51 msgid "Total revenue" -- 2.52.0