Compare commits

..
Author SHA1 Message Date
Raphael Michel 15bfa2b2ce Bump to 2026.5.3 2026-07-01 14:11:01 +02:00
Raphael MichelandMira Weller 43b7e0afe0 [SECURITY] Hardening for user impersonation feature (CVE-2026-13602)
---------

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

---------

Co-authored-by: Mira Weller <weller@pretix.eu>
2026-07-01 14:06:59 +02:00
3730b5473b [SECURITY] Allowlisting and changed salts for safelink and safelink_callback (CVE-2026-13602)
---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-07-01 14:05:17 +02:00
Raphael Michel 7faf6cfd80 Bump to 2026.5.2 2026-06-25 16:40:44 +02:00
Raphael Michel cdeb3609f3 [SECURITY] Properly escape HTML tags in PDF generation (CVE-2026-57535) 2026-06-25 16:40:28 +02:00
Raphael MichelandRaphael Michel 53d8aa5679 [SECURITY] Prevent reading of any local files in reportlab (CVE-2026-57535) 2026-06-25 16:40:00 +02:00
Raphael MichelandRaphael Michel 80601cc013 [SECURITY] Disable outbound and file access for reportlab (CVE-2026-57535) 2026-06-25 16:40:00 +02:00
Mira WellerandRaphael Michel e2661f304d [SECURITY] Fix reflected XSS in redirection page (CVE-2026-57533) 2026-06-25 16:40:00 +02:00
Mira WellerandRaphael Michel 71f6ade5d6 [SECURITY] Fix stored XSS in ticket confirmation page (CVE-2026-13225) 2026-06-25 16:40:00 +02:00
Mira WellerandRaphael Michel e087dd39b8 [SECURITY] Hardening: Don't use |safe on confirm_messages 2026-06-25 16:40:00 +02:00
Mira WellerandRaphael Michel f3a8ce39f1 [SECURITY] Fix XSS in ticket layout JSON (CVE-2026-57532) 2026-06-25 16:40:00 +02:00
Raphael Michel a6c417eb2f Bump version to 2026.5.1 2026-06-09 13:22:17 +02:00
Richard SchreiberandRaphael Michel 5d449ea313 [SECURITY] Reusable media export: Respect giftcard permissions (CVE-2026-11764) (#6261) 2026-06-09 13:21:41 +02:00
58 changed files with 775 additions and 715 deletions
+2 -2
View File
@@ -64,8 +64,8 @@ Backend
.. automodule:: pretix.control.signals
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
order_info, order_approve_info, event_settings_widget, oauth_application_registered,
order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms
order_info, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
item_formsets, order_search_filter_q, order_search_forms
.. automodule:: pretix.base.signals
:no-index:
+2 -2
View File
@@ -93,7 +93,7 @@ dependencies = [
"redis==7.4.*",
"reportlab==4.5.*",
"requests==2.32.*",
"sentry-sdk==2.61.*",
"sentry-sdk==2.60.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -111,7 +111,7 @@ dev = [
"aiohttp==3.13.*",
"coverage",
"coveralls",
"fakeredis==2.36.*",
"fakeredis==2.35.*",
"flake8==7.3.*",
"freezegun",
"isort==8.0.*",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.6.0.dev0"
__version__ = "2026.5.3"
+8 -3
View File
@@ -61,18 +61,23 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
yield headers
yield self.ProgressSetTotal(total=media.count())
can_read_giftcards = self.permission_holder.has_organizer_permission(self.organizer, 'organizer.giftcards:read')
for medium in media.iterator(chunk_size=1000):
row = [
giftcard_secret = medium.linked_giftcard.secret if medium.linked_giftcard_id else ''
if giftcard_secret and not can_read_giftcards:
giftcard_secret = giftcard_secret[:3] + ""
yield [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
giftcard_secret,
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'
+76 -90
View File
@@ -22,9 +22,7 @@
import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
from io import BytesIO
@@ -58,8 +56,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
from pretix.base.signals import register_invoice_renderers
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import (
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
reshaper,
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
normalize_text, register_ttf_font_if_new, reshaper,
)
from pretix.presale.style import get_fonts
@@ -259,18 +257,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
def _normalize(self, text):
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
# alias kept for plugin compatibility
return normalize_text(text)
def _upper(self, val):
# We uppercase labels, but not in every language
@@ -351,10 +339,15 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
return 'invoice.pdf', 'application/pdf', buffer.read()
def _clean_text(self, text, tags=None):
return self._normalize(bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
# tags.
return self._normalize(
bleach.clean(
text,
tags=set(tags) if tags else set()
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
)
class PaidMarker(Flowable):
@@ -405,8 +398,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_to_top = 52 * mm
def _draw_invoice_to(self, canvas):
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
style=self.stylesheet['Normal'])
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
@@ -417,8 +409,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = FontFallbackParagraph(
self._clean_text(self.invoice.full_invoice_from),
p = PlainTextParagraph(
self.invoice.full_invoice_from,
style=self.stylesheet['InvoiceFrom']
)
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
@@ -548,13 +540,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=set()).strip()
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
txt = ' '.join(txt.replace('', '').split()[:-1]) + ''
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
return txt
@@ -572,7 +563,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
else:
p_str = shorten(self.invoice.event.name)
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
p.wrapOn(canvas, self.event_width, self.event_height)
p_size = p.wrap(self.event_width, self.event_height)
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
@@ -645,39 +636,37 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
if type_info_text:
story.append(FontFallbackParagraph(
story.append(PlainTextParagraph(
type_info_text,
self.stylesheet['WarningBlock']
))
if self.invoice.custom_field:
story.append(FontFallbackParagraph(
story.append(PlainTextParagraph(
'{}: {}'.format(
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
self._clean_text(self.invoice.custom_field),
str(self.invoice.event.settings.invoice_address_custom_field),
self.invoice.custom_field,
),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
reference=self._clean_text(self.invoice.internal_reference),
)),
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=self.invoice.internal_reference,
),
self.stylesheet['Normal']
))
if self.invoice.invoice_to_vat_id:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
self._clean_text(self.invoice.invoice_to_vat_id),
story.append(PlainTextParagraph(
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
self.stylesheet['Normal']
))
if self.invoice.invoice_to_beneficiary:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
self._clean_text(self.invoice.invoice_to_beneficiary),
story.append(PlainTextParagraph(
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
self.stylesheet['Normal']
))
@@ -707,11 +696,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = [
NextPageTemplate('FirstPage'),
FontFallbackParagraph(
self._normalize(
PlainTextParagraph(
(
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
else pgettext('invoice', 'Invoice')
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
self.stylesheet['Heading1']
),
Spacer(1, 5 * mm),
@@ -733,17 +722,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
]
if has_taxes:
tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
)]
else:
tdata = [(
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
)]
def _group_key(line):
@@ -780,8 +769,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p = PlainTextParagraph(
curr_description,
p_style
)
h = p.wrap(max_width, doc.height)[1]
@@ -862,7 +851,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
period_line,
self.stylesheet['Fineprint']
))
@@ -874,7 +863,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
@@ -883,11 +872,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
description_p_list.pop(0),
str(len(lines)),
localize(tax_rate) + " %",
FontFallbackParagraph(
PlainTextParagraph(
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
FontFallbackParagraph(
PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -904,14 +893,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description_p_list.append(FontFallbackParagraph(
description_p_list.append(PlainTextParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
description_p_list.pop(0),
str(len(lines)),
FontFallbackParagraph(
PlainTextParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
@@ -944,12 +933,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
if has_taxes:
tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
else:
tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
@@ -958,12 +947,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
pending_sum = self.invoice.order.pending_sum
if pending_sum != total:
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum - total, self.invoice.event.currency)]
)
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(pending_sum, self.invoice.event.currency)]
)
@@ -980,12 +969,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
s=Sum('amount')
)['s'] or Decimal('0.00')
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
[PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(giftcard_sum, self.invoice.event.currency)]
)
tdata.append(
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
(['', '', ''] if has_taxes else ['']) +
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
)
@@ -1008,14 +997,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 10 * mm))
if request_show_service_date:
story.append(FontFallbackParagraph(
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
story.append(PlainTextParagraph(
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
self.stylesheet['Normal']
))
if self.invoice.payment_provider_text:
story.append(FontFallbackParagraph(
self._normalize(self.invoice.payment_provider_text),
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
self.stylesheet['Normal']
))
@@ -1039,10 +1028,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]
thead = [
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
''
]
tdata = [thead]
@@ -1053,7 +1042,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
continue
tax = taxvalue_map[idx]
tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
money_filter(gross - tax, self.invoice.event.currency),
money_filter(gross, self.invoice.event.currency),
money_filter(tax, self.invoice.event.currency),
@@ -1072,7 +1061,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(Spacer(5 * mm, 5 * mm))
story.append(KeepTogether([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
table
]))
@@ -1089,7 +1078,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net = gross - tax
tdata.append([
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
fmt(net), fmt(gross), fmt(tax), ''
])
@@ -1098,13 +1087,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(KeepTogether([
Spacer(1, height=2 * mm),
FontFallbackParagraph(
self._normalize(pgettext(
PlainTextParagraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, this corresponds to:'
).format(rate=localize(self.invoice.foreign_currency_rate),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
self.stylesheet['Fineprint']
),
Spacer(1, height=3 * mm),
@@ -1113,14 +1102,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
story.append(Spacer(1, 5 * mm))
story.append(FontFallbackParagraph(self._normalize(
story.append(PlainTextParagraph(
pgettext(
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
'{date}, the invoice total corresponds to {total}.'
).format(rate=localize(self.invoice.foreign_currency_rate),
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
total=fmt(foreign_total))),
total=fmt(foreign_total)),
self.stylesheet['Fineprint']
))
@@ -1162,11 +1151,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.address_invoice_from:
return
c = [
self._clean_text(l)
for l in self.invoice.address_invoice_from.strip().split('\n')
]
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
c = self.invoice.address_invoice_from.strip().split('\n')
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
super()._draw_invoice_from(canvas)
@@ -1225,8 +1211,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
]
p = FontFallbackParagraph(
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
p = PlainTextParagraph(
date_format(self.invoice.date, "DATE_FORMAT"),
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
)
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
@@ -1283,7 +1269,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
i = []
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
i.append(FontFallbackParagraph(
i.append(PlainTextParagraph(
pgettext('invoice', 'Event date: {date_range}').format(
date_range=self.invoice.event.get_date_range_display(),
),
+1 -1
View File
@@ -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):
@@ -0,0 +1,28 @@
{% extends "error.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
<h1>{% trans "Please continue in a new tab" %}</h1>
<p class="larger">
{% blocktrans trimmed %}
For security reasons, the following step is only possible in a new tab.
{% endblocktrans %}
</p>
<p class="larger">
{% blocktrans trimmed %}
If the new tab did not open automatically, please click the following button:
{% endblocktrans %}
</p>
<div class="text-center">
<a href="{{ url }}"
class="btn btn-primary btn-lg" target="_blank">
<span class="fa fa-external-link-square"></span>
{% trans "Continue in new tab" %}
</a>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% endblock %}
@@ -2,13 +2,14 @@
{% load i18n %}
{% load rich_text %}
{% load static %}
{% load wrap_in %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
{% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
+2 -2
View File
@@ -54,6 +54,7 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map
register = template.Library()
@@ -158,8 +159,7 @@ def safelink_callback(attrs, new=False):
"""
url = html.unescape(attrs.get((None, 'href'), '/'))
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'href'] = safelink(url)
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
+29 -6
View File
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import urllib.parse
from django.core import signing
@@ -26,6 +27,8 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request):
referer = request.headers.get('referer')
@@ -42,11 +45,16 @@ def _is_samesite_referer(request):
def redir_view(request):
signer = signing.Signer(salt='safe-redirect')
framebreak = "framebreak" in request.GET
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
try:
url = signer.unsign(request.GET.get('url', ''))
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
try:
# Backwards-compatibility for a change in 2026-06, remove after a while
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
@@ -55,11 +63,26 @@ def redir_view(request):
'url': url,
})
if framebreak:
r = render(request, 'pretixbase/framebreak.html', {
'url': url,
})
r.xframe_options_exempt = True
return r
r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex'
return r
def safelink(url):
signer = signing.Signer(salt='safe-redirect')
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
def safelink(url, framebreak=False):
url = str(url)
if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
logger.warning('Invalid URL passed to safelink: %r', url)
return '#invalid-url'
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
signer = signing.Signer(salt=salt)
u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
if framebreak:
u += "&framebreak=true"
return u
-28
View File
@@ -461,31 +461,3 @@ class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
**super().create_option(name, value, label, selected, index, subindex, attrs),
"plugin_missing": plugin and plugin not in self.event.get_plugins(),
}
class ModelChoiceIteratorWithNone(forms.models.ModelChoiceIterator):
# see django.forms.models.ModelChoiceIterator for original implementation
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
if self.field.none_label is not None:
yield ("_none", self.field.none_label)
queryset = self.queryset
# Can't use iterator() when queryset uses prefetch_related()
if not queryset._prefetch_related_lookups:
queryset = queryset.iterator()
for obj in queryset:
yield self.choice(obj)
class ModelChoiceFieldWithNone(forms.ModelChoiceField):
iterator = ModelChoiceIteratorWithNone
def __init__(self, *args, **kwargs):
self.none_label = kwargs.pop("none_label", None)
super().__init__(*args, **kwargs)
def to_python(self, value):
if value == "_none":
return value
return super().to_python(value)
+11 -24
View File
@@ -29,30 +29,17 @@ class Select2Mixin:
super().__init__(*args, **kwargs)
def options(self, name, value, attrs=None):
if not value or not value[0]:
return
has_none = "_none" in value
if has_none:
value = [v for v in value if v != "_none"]
yield self.create_option(
None,
"_none",
self.choices.field.none_label,
True,
0,
subindex=None,
attrs=attrs
)
for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)):
yield self.create_option(
None,
self.choices.field.prepare_value(selected),
self.choices.field.label_from_instance(selected),
True,
i + (1 if has_none else 0),
subindex=None,
attrs=attrs
)
if value and value[0]:
for i, selected in enumerate(self.choices.queryset.filter(pk__in=value)):
yield self.create_option(
None,
self.choices.field.prepare_value(selected),
self.choices.field.label_from_instance(selected),
True,
i,
subindex=None,
attrs=attrs
)
return
def optgroups(self, name, value, attrs=None):
+1 -1
View File
@@ -212,7 +212,7 @@ class AuditLogMiddleware:
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0])
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if ss:
ss.logs.create(
-10
View File
@@ -261,16 +261,6 @@ As with all event plugin signals, the ``sender`` keyword argument will contain t
Additionally, the argument ``order`` and ``request`` are available.
"""
order_approve_info = EventPluginSignal()
"""
Arguments: ``order``, ``request``
This signal is sent out to display additional information on the order approve page
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
Additionally, the argument ``order`` and ``request`` are available.
"""
order_position_buttons = EventPluginSignal()
"""
Arguments: ``order``, ``position``, ``request``
+23 -22
View File
@@ -38,6 +38,7 @@ from pretix import __version__
from pretix.base.models import Order, OrderPayment, Transaction
from pretix.base.plugins import get_all_plugins
from pretix.base.templatetags.money import money_filter
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin
from pretix.settings import DATA_DIR
@@ -79,23 +80,23 @@ class SysReport(ReportlabExportMixin):
style_small.fontSize = 6
story = [
Paragraph("System report", headlinestyle),
PlainTextParagraph("System report", headlinestyle),
Spacer(1, 5 * mm),
Paragraph("Usage", subheadlinestyle),
PlainTextParagraph("Usage", subheadlinestyle),
Spacer(1, 5 * mm),
self._usage_table(),
Spacer(1, 5 * mm),
Paragraph("Installed versions", subheadlinestyle),
PlainTextParagraph("Installed versions", subheadlinestyle),
Spacer(1, 5 * mm),
self._tech_table(),
Spacer(1, 5 * mm),
Paragraph("Plugins", subheadlinestyle),
PlainTextParagraph("Plugins", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_plugin_versions(), style_small),
PlainTextParagraph(self._get_plugin_versions(), style_small),
Spacer(1, 5 * mm),
Paragraph("Custom templates", subheadlinestyle),
PlainTextParagraph("Custom templates", subheadlinestyle),
Spacer(1, 5 * mm),
Paragraph(self._get_custom_templates(), style_small),
PlainTextParagraph(self._get_custom_templates(), style_small),
Spacer(1, 5 * mm),
]
@@ -121,13 +122,13 @@ class SysReport(ReportlabExportMixin):
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
]
tdata = [
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
[
Paragraph("Database engine:", style),
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
PlainTextParagraph("Database engine:", style),
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
],
]
table = Table(tdata, colWidths=colwidths, repeatRows=0)
@@ -206,7 +207,7 @@ class SysReport(ReportlabExportMixin):
year_last = now().year
tdata = [
[
Paragraph(l, style_small_head)
PlainTextParagraph(l, style_small_head)
for l in (
"Time frame",
"Currency",
@@ -257,19 +258,19 @@ class SysReport(ReportlabExportMixin):
tdata.append(
(
Paragraph(
PlainTextParagraph(
date_format(first_day, "M Y")
+ " "
+ date_format(after_day - timedelta(days=1), "M Y"),
style_small,
),
Paragraph(c, style_small),
Paragraph(str(orders_count), style_small) if i == 0 else "",
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
Paragraph(str(testmode_count), style_small) if i == 0 else "",
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
Paragraph(str(revenue_data.get("c") or 0), style_small),
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
PlainTextParagraph(c, style_small),
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
)
)
@@ -1,5 +1,4 @@
{% extends "pretixcontrol/event/base.html" %}
{% load eventsignal %}
{% load i18n %}
{% block title %}
{% trans "Approve order" %}
@@ -8,9 +7,6 @@
<h1>
{% trans "Approve order" %}
</h1>
{% eventsignal request.event "pretix.control.signals.order_approve_info" order=order request=request %}
<p>{% blocktrans trimmed %}
Do you really want to approve this order?
{% endblocktrans %}</p>
@@ -19,9 +19,7 @@
{% endif %}
</h1>
<script type="application/json" id="editor-data">
{{ layout|safe }}
</script>
{{ layout|json_script:"editor-data" }}
<div class="row">
<div class="col-md-9">
<div class="panel panel-default panel-pdf-editor">
+1 -2
View File
@@ -401,14 +401,13 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
{
'id': i.pk,
'name': str(i),
'active': i.active,
'variations': [
{
'id': v.pk,
'name': str(v.value)
} for v in i.variations.all()
]
} for i in self.request.event.items.prefetch_related('variations')
} for i in self.request.event.items.filter(active=True).prefetch_related('variations')
],
**super().get_context_data(),
}
+1 -1
View File
@@ -284,7 +284,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables()
ctx['images'] = self.get_images()
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['layout'] = self.get_current_layout()
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
+6 -25
View File
@@ -145,21 +145,11 @@ def event_list(request):
if 'can_copy' in request.GET:
qs = EventWizardCopyForm.copy_from_queryset(request.user, request.session)
else:
permission = request.GET.get('permission')
if permission:
qs = request.user.get_events_with_permission(permission, request)
else:
qs = request.user.get_events_with_any_permission(request)
name_slug_q = Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
organizer = request.GET.get('organizer')
if organizer:
qs = qs.filter(organizer__slug=organizer)
else:
name_slug_q |= Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
qs = request.user.get_events_with_any_permission(request)
qs = qs.filter(
name_slug_q
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) |
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
).annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
@@ -172,19 +162,10 @@ def event_list(request):
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
results = []
if page == 1 and 'include_none' in request.GET and not query:
results.append({
'id': "_none",
'text': _("No event"),
'name': _("No event"),
'type': "event",
})
results += [
serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
]
doc = {
'results': results,
'results': [
serialize_event(e) for e in qs.select_related('organizer')[offset:offset + pagesize]
],
'pagination': {
"more": total >= (offset + pagesize)
}
+29 -5
View File
@@ -19,19 +19,22 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import hmac
import json
from contextlib import contextmanager
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
logout,
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
@@ -230,7 +233,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
hijacked = self.object
hijack_history = request.session.get("hijack_history", [])
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
hijack_history.append({
"user": request.user.pk,
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
# unprivileged user to contain the risk if there is some leak of session data.
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
})
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
@@ -259,8 +270,21 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", [])
hijacked = request.user
user_pk = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
expected_hash = salted_hmac(
key_salt=b"hijack-history-hash",
value=hijacker.get_session_auth_hash(),
algorithm="sha256",
secret=settings.SECRET_KEY
).hexdigest()
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
# that happened during the hijack session
logout(request)
return redirect_to_login(request.get_full_path())
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session):
+5
View File
@@ -29,3 +29,8 @@ class PretixHelpersConfig(AppConfig):
def ready(self):
from .monkeypatching import monkeypatch_all_at_ready
monkeypatch_all_at_ready()
# Ensure reportlab does not make any calls to the internet or the local disk
from reportlab import rl_config
rl_config.trustedHosts = []
rl_config.trustedSchemes = ['data']
+21
View File
@@ -27,6 +27,7 @@ from datetime import datetime
from http import cookies
from django.conf import settings
from django.core.exceptions import SuspiciousFileOperation
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
@@ -40,6 +41,8 @@ from urllib3.util.connection import (
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
from pretix.helpers.reportlab import ThumbnailingImageReader
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
@@ -230,9 +233,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 <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():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
monkeypatch_reportlab_imagereader()
+39
View File
@@ -20,14 +20,19 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
import unicodedata
from arabic_reshaper import ArabicReshaper
from bidi import get_display
from django.conf import settings
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from PIL import Image
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph
from pretix.presale.style import get_fonts
@@ -70,6 +75,20 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
}))
def normalize_text(text: str) -> str:
# reportlab does not support unicode combination characters
# It's important we do this before we use ArabicReshaper
text = unicodedata.normalize("NFKC", text)
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
# to resolve all ligatures and python-bidi to switch RTL texts.
try:
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
except:
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
return text
class FontFallbackParagraph(Paragraph):
def __init__(self, text, style=None, *args, **kwargs):
if style is None:
@@ -87,6 +106,8 @@ class FontFallbackParagraph(Paragraph):
if not text:
return True
font = pdfmetrics.getFont(font_name)
if not isinstance(font, TTFont):
return True
return all(
ord(c) in font.face.charToGlyph or not c.isprintable()
for c in text
@@ -102,6 +123,24 @@ class FontFallbackParagraph(Paragraph):
return family
class PlainTextParagraph(FontFallbackParagraph):
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
if not isinstance(text, str):
if hasattr(text, '__html__'):
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
text = str(text)
# Normalize unicode and apply reshaping
text = normalize_text(text)
# Escape any HTML in the text
text = escape(text)
if linebreaks:
text = text.strip().replace("\n", "<br />\n")
super().__init__(text, style, *args, **kwargs)
def register_ttf_font_if_new(name, path):
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
@@ -0,0 +1,33 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
from django import template
from django.utils.html import format_html
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter
def wrap_in(content, tag_name):
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
+84 -50
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"PO-Revision-Date: 2026-05-01 21:00+0000\n"
"Last-Translator: Paul Berschick <paul@plainschwarz.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.5\n"
"X-Generator: Weblate 5.17\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -620,17 +620,16 @@ msgstr ""
"como variaciones o paquetes."
#: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Quota handling"
msgid "Quota changed"
msgstr "Se ha modificado la cuota"
msgstr "Gestión de cuotas"
#: pretix/api/webhooks.py:414
msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"Esto incluye acciones relacionadas, como la creación, la eliminación, la "
"apertura o el cierre de cuotas. No se envía ningún webhook cuando se "
"producen cambios en la disponibilidad resultante."
#: pretix/api/webhooks.py:419
msgid "Shop taken live"
@@ -3419,13 +3418,11 @@ msgid ""
"The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"."
msgstr ""
"El campo «%(label)s» no puede contener caracteres especiales como «%(chars)s"
"»."
#: pretix/base/forms/questions.py:305
#, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr "El campo «%(label)s» no puede contener una URL (%(url)s)."
msgstr ""
#: pretix/base/forms/questions.py:338
msgctxt "phonenumber"
@@ -8364,14 +8361,19 @@ msgid "Program times"
msgstr "Horarios del programa"
#: pretix/base/pdf.py:503
#, fuzzy
#| msgid ""
#| "2017-05-31 10:00 12:00\n"
#| "2017-05-31 14:00 16:00\n"
#| "2017-05-31 14:00 2017-06-01 14:00"
msgid ""
"2017-05-31 10:00 12:00, Room 1\n"
"2017-05-31 14:00 16:00, Room 2\n"
"2017-05-31 14:00 2017-06-01 14:00, Building A"
msgstr ""
"31 de mayo de 2017, de 10:00 a 12:00, Sala 1\n"
"31 de mayo de 2017, de 14:00 a 16:00, Sala 2\n"
"31 de mayo de 2017, de 14:00 a 1 de junio de 2017, 14:00, Edificio A"
"2017-05-31 10:00 12:00\n"
"2017-05-31 14:00 16:00\n"
"2017-05-31 14:00 2017-06-01 14:00"
#: pretix/base/pdf.py:507
msgid "Reusable Medium ID"
@@ -8901,7 +8903,13 @@ msgid "This voucher code is not known in our database."
msgstr "Este vale de compra no se conoce en nuestra base de datos."
#: pretix/base/services/cart.py:165
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product."
@@ -8909,14 +8917,22 @@ msgid_plural ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching products."
msgstr[0] ""
"El código de descuento «%(voucher)s» solo se puede utilizar si seleccionas "
"al menos%(number)s productos que cumplan los requisitos."
"El vale de compra \"%(voucher)s\" solo se puede utilizar si selecciona al "
"menos %(number)s productos coincidentes."
msgstr[1] ""
"El código de descuento «%(voucher)s» solo se puede utilizar si seleccionas "
"al menos %(number)s productos que cumplan los requisitos."
"Los vales de compra \"%(voucher)s\" solo se pueden utilizar si selecciona al "
"menos %(number)s productos coincidentes."
#: pretix/base/services/cart.py:170
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product. We have therefore removed some positions from "
@@ -8926,15 +8942,13 @@ msgid_plural ""
"%(number)s matching products. We have therefore removed some positions from "
"your cart that can no longer be purchased like this."
msgstr[0] ""
"El código promocional «%(voucher)s» solo se puede utilizar si seleccionas al "
"menos %(number)s producto que cumpla los requisitos. Por lo tanto, hemos "
"eliminado de tu carrito algunos artículos que ya no se pueden comprar de "
"esta forma."
"El vale de compra \"%(voucher)s\" solo se puede utilizar si selecciona al "
"menos %(number)s productos coincidentes. Por lo tanto, hemos eliminado "
"algunas posiciones de su carrito que ya no se pueden comprar así."
msgstr[1] ""
"El código promocional «%(voucher)s» solo se puede utilizar si seleccionas al "
"menos %(number)s productos que cumplan los requisitos. Por lo tanto, hemos "
"eliminado de tu carrito algunos artículos que ya no se pueden comprar de "
"esta forma."
"Los vale de compra \"%(voucher)s\" solo se pueden utilizar si selecciona al "
"menos %(number)s productos coincidentes. Por lo tanto, hemos eliminado "
"algunas posiciones de su carrito que ya no se pueden comprar así."
#: pretix/base/services/cart.py:176
msgid ""
@@ -14240,8 +14254,6 @@ msgid ""
"You entered an URL, which is not allowed. Please remove %(match)s from your "
"input."
msgstr ""
"Ha introducido una URL que no está permitida. Elimina %(match)s de su "
"entrada."
#: pretix/base/views/errors.py:48
msgid ""
@@ -16182,8 +16194,14 @@ msgid "inactive"
msgstr "inactivo"
#: pretix/control/forms/item.py:1414
#, fuzzy
#| msgid ""
#| "Sample Conference Center\n"
#| "Heidelberg, Germany"
msgid "Sample Conference Center, Heidelberg, Germany"
msgstr "Ejemplo de Centro de Conferencia : Heidelberg, Alemania"
msgstr ""
"Ejemplo de Centro de Conferencia \n"
"Heidelberg, Alemania"
#: pretix/control/forms/mailsetup.py:42
msgid "Hostname"
@@ -23641,8 +23659,11 @@ msgid "Quota history"
msgstr "Historial de cuotas"
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:6
#, fuzzy
#| msgctxt "subevent"
#| msgid "Change multiple dates"
msgid "Change multiple quotas"
msgstr "Modificar varias cuotas"
msgstr "Cambiar varias fechas"
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:8
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:8
@@ -23692,15 +23713,18 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:4
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:6
#, fuzzy
#| msgid "Delete quota"
msgid "Delete quotas"
msgstr "Eliminar cuotas"
msgstr "Borrar cuota"
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:10
#, python-format
#, fuzzy, python-format
#| msgid "Are you sure you want to delete the following dates?"
msgid "Are you sure you want to delete the following quota?"
msgid_plural "Are you sure you want to delete the following %(num)s quotas?"
msgstr[0] "¿Está seguro de que desea eliminar la siguiente cuota?"
msgstr[1] "¿Está seguro de que desea eliminar las siguientes %(num)s cuotas?"
msgstr[0] "¿Está seguro de que desea borrar las fechas siguientes?"
msgstr[1] "¿Está seguro de que desea borrar las fechas siguientes?"
#: pretix/control/templates/pretixcontrol/items/quotas.html:9
msgid ""
@@ -24305,15 +24329,12 @@ msgid ""
"generated once the customer pays the invoice or selects a payment method "
"that requires an invoice."
msgstr ""
"Este pedido se modificó después de que se generara la última factura. Aún no "
"se ha generado una nueva factura, ya que las facturas están configuradas "
"para generarse al realizar el pago o si así lo exige la forma de pago. Se "
"generará una nueva factura una vez que el cliente abone la factura o "
"seleccione una forma de pago que requiera una factura."
#: pretix/control/templates/pretixcontrol/order/index.html:152
#, fuzzy
#| msgid "Request invoice"
msgid "Reissue invoice"
msgstr "Reemitir factura"
msgstr "Solicitar factura"
#: pretix/control/templates/pretixcontrol/order/index.html:161
#: pretix/control/templates/pretixcontrol/order/index.html:413
@@ -24744,16 +24765,23 @@ msgid "How should the refund be sent?"
msgstr "¿Cómo se debe de realizar este reembolso?"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:25
#, fuzzy
#| msgid ""
#| "Any payments that you selected for automatical refunds will be "
#| "immediately communicate the refund request to the respective payment "
#| "provider. Manual refunds will be created as pending refunds, you can then "
#| "later mark them as done once you actually transferred the money back to "
#| "the customer."
msgid ""
"Any payments you selected for automatic refunds will have the refund request "
"sent immediately to the respective payment provider. Manual refunds will be "
"created as pending refunds, which you can later mark as done once you have "
"actually transferred the money back to the customer."
msgstr ""
"Los pagos que hayas seleccionado para reembolsos automáticos se enviarán "
"inmediatamente al proveedor de pagos correspondiente. Los reembolsos "
"manuales se crearán como reembolsos pendientes, que podrás marcar como "
"completados más adelante, una vez que hayas devuelto el dinero al cliente."
"Cualquier pago que haya seleccionado de manera automática para reembolso "
"será comunicado inmediatamente a la entidad de pago correspondiente. Los "
"devoluciones manuales se crearán como reembolsos pendientes, podrá marcarlos "
"como hechos una vez que se haya transferido el dinero al cliente."
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:32
msgid "Refund to original payment method"
@@ -29309,8 +29337,11 @@ msgid "The new question has been created."
msgstr "La nueva pregunta ha sido creada."
#: pretix/control/views/item.py:918
#, fuzzy
#| msgctxt "subevent"
#| msgid "The selected dates have been deleted or disabled."
msgid "The selected quotas have been deleted or disabled."
msgstr "Las cuotas seleccionadas se han eliminado o desactivado."
msgstr "Las fechas seleccionadas se han borrado o desactivado."
#: pretix/control/views/item.py:1074
msgid "The new quota has been created."
@@ -30042,9 +30073,11 @@ msgstr ""
"Este plugin no está permitido actualmente para su cuenta de organizador."
#: pretix/control/views/organizer.py:832
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "This plugin can be enabled or disabled for events individually."
msgid "This plugin cannot be activated for event {}."
msgstr "Este complemento no se puede activar para el evento {}."
msgstr ""
"Este plugin se puede activar o desactivar para eventos de forma individual."
#: pretix/control/views/organizer.py:901
msgid "The team has been created. You can now add members to the team."
@@ -31089,9 +31122,10 @@ msgid "{width} x {height} mm label"
msgstr "etiqueta {width} x {height} mm"
#: pretix/plugins/badges/templates.py:265
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "{width} x {height} mm label"
msgid "{width} x {height} inch label"
msgstr "Etiqueta de {width} x {height} pulgadas"
msgstr "etiqueta {width} x {height} mm"
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_info.html:16
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:27
+81 -43
View File
@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"PO-Revision-Date: 2026-05-08 04:00+0000\n"
"Last-Translator: corentin-spec <corentin@spectentaculaire.fr>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 2026.5\n"
"X-Generator: Weblate 5.17.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -618,17 +618,16 @@ msgstr ""
"aux objets imbriqués tels que les variantes ou les lots."
#: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Quota handling"
msgid "Quota changed"
msgstr "Quota modifié"
msgstr "Traitement des quotas"
#: pretix/api/webhooks.py:414
msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"Cela inclut les événements associés, tels que la création, la suppression, "
"l'ouverture ou la suppression de quotas. Aucun webhook n'est envoyé en cas "
"de modification de la disponibilité qui en résulte."
#: pretix/api/webhooks.py:419
msgid "Shop taken live"
@@ -3423,13 +3422,11 @@ msgid ""
"The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"."
msgstr ""
"Le champ « %(label)s » ne doit pas contenir de caractères spéciaux tels que "
"«%(chars)s »."
#: pretix/base/forms/questions.py:305
#, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr "Le champ « %(label)s » ne doit pas contenir d'URL (%(url)s)."
msgstr ""
#: pretix/base/forms/questions.py:338
msgctxt "phonenumber"
@@ -8412,14 +8409,19 @@ msgid "Program times"
msgstr "Horaires du programme"
#: pretix/base/pdf.py:503
#, fuzzy
#| msgid ""
#| "2017-05-31 10:00 12:00\n"
#| "2017-05-31 14:00 16:00\n"
#| "2017-05-31 14:00 2017-06-01 14:00"
msgid ""
"2017-05-31 10:00 12:00, Room 1\n"
"2017-05-31 14:00 16:00, Room 2\n"
"2017-05-31 14:00 2017-06-01 14:00, Building A"
msgstr ""
"31 mai 2017, de 10 h à 12 h, salle 1\n"
"31 mai 2017, de 14 h à 16 h, salle 2\n"
"Du 31 mai 2017 à 1 h du matin au 1er juin 2017 à 14 h, bâtiment A"
"2017-05-31 10:00 12:00\n"
"2017-05-31 14:00 16:00\n"
"2017-05-31 14:00 2017-06-01 14:00"
#: pretix/base/pdf.py:507
msgid "Reusable Medium ID"
@@ -8955,7 +8957,13 @@ msgid "This voucher code is not known in our database."
msgstr "Ce code promotionnel n'est pas connu dans notre base de données."
#: pretix/base/services/cart.py:165
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product."
@@ -8963,14 +8971,22 @@ msgid_plural ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching products."
msgstr[0] ""
"Le code promo « %(voucher)s » ne peut être utilisé que si vous sélectionnez "
"Le code promo \"%(voucher)s\" ne peut être utilisé que si vous sélectionnez "
"au moins %(number)s produit correspondant."
msgstr[1] ""
"Le code promo « %(voucher)s » ne peut être utilisé que si vous sélectionnez "
"Le code promo \"%(voucher)s\" ne peut être utilisé que si vous sélectionnez "
"au moins %(number)s produits correspondants."
#: pretix/base/services/cart.py:170
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product. We have therefore removed some positions from "
@@ -14363,8 +14379,6 @@ msgid ""
"You entered an URL, which is not allowed. Please remove %(match)s from your "
"input."
msgstr ""
"Vous avez saisi une URL, ce qui n'est pas autorisé. Veuillez supprimer %"
"(match)s de votre saisie."
#: pretix/base/views/errors.py:48
msgid ""
@@ -16314,8 +16328,14 @@ msgid "inactive"
msgstr "inactif"
#: pretix/control/forms/item.py:1414
#, fuzzy
#| msgid ""
#| "Sample Conference Center\n"
#| "Heidelberg, Germany"
msgid "Sample Conference Center, Heidelberg, Germany"
msgstr "Centre de conférences d'exemple, Heidelberg, Allemagne"
msgstr ""
"Exemple de centre de conférence\n"
"Centre des Congrès, France"
#: pretix/control/forms/mailsetup.py:42
msgid "Hostname"
@@ -23811,8 +23831,11 @@ msgid "Quota history"
msgstr "Historique des quotas"
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:6
#, fuzzy
#| msgctxt "subevent"
#| msgid "Change multiple dates"
msgid "Change multiple quotas"
msgstr "Modifier plusieurs quotas"
msgstr "Modifier plusieurs dates"
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:8
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:8
@@ -23860,15 +23883,18 @@ msgstr "Les produits suivants pourraient ne plus être disponibles à la vente
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:4
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:6
#, fuzzy
#| msgid "Delete quota"
msgid "Delete quotas"
msgstr "Supprimer les quotas"
msgstr "Supprimer le quota"
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:10
#, python-format
#, fuzzy, python-format
#| msgid "Are you sure you want to delete the following dates?"
msgid "Are you sure you want to delete the following quota?"
msgid_plural "Are you sure you want to delete the following %(num)s quotas?"
msgstr[0] "Êtes-vous sûr de vouloir supprimer le quota suivant?"
msgstr[1] "Êtes-vous sûr de vouloir supprimer les %(num)s quotas suivants?"
msgstr[0] "Voulez-vous vraiment supprimer les dates suivantes ?"
msgstr[1] "Voulez-vous vraiment supprimer les dates suivantes ?"
#: pretix/control/templates/pretixcontrol/items/quotas.html:9
msgid ""
@@ -24477,15 +24503,12 @@ msgid ""
"generated once the customer pays the invoice or selects a payment method "
"that requires an invoice."
msgstr ""
"Cette commande a été modifiée après l'émission de la dernière facture. "
"Aucune nouvelle facture n'a encore été générée, car les factures sont "
"configurées pour être émises lors du paiement ou si le mode de paiement "
"l'exige. Une nouvelle facture sera générée dès que le client aura réglé la "
"facture ou choisi un mode de paiement nécessitant une facture."
#: pretix/control/templates/pretixcontrol/order/index.html:152
#, fuzzy
#| msgid "Request invoice"
msgid "Reissue invoice"
msgstr "Réémettre une facture"
msgstr "Demande de facture"
#: pretix/control/templates/pretixcontrol/order/index.html:161
#: pretix/control/templates/pretixcontrol/order/index.html:413
@@ -24919,17 +24942,25 @@ msgid "How should the refund be sent?"
msgstr "Comment le remboursement doit-il être envoyé ?"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:25
#, fuzzy
#| msgid ""
#| "Any payments that you selected for automatical refunds will be "
#| "immediately communicate the refund request to the respective payment "
#| "provider. Manual refunds will be created as pending refunds, you can then "
#| "later mark them as done once you actually transferred the money back to "
#| "the customer."
msgid ""
"Any payments you selected for automatic refunds will have the refund request "
"sent immediately to the respective payment provider. Manual refunds will be "
"created as pending refunds, which you can later mark as done once you have "
"actually transferred the money back to the customer."
msgstr ""
"Pour tous les paiements que vous avez sélectionnés pour un remboursement "
"automatique, la demande de remboursement sera immédiatement transmise au "
"prestataire de paiement concerné. Les remboursements manuels seront "
"enregistrés comme remboursements en attente; vous pourrez les marquer comme "
"effectués une fois que vous aurez effectivement reversé l'argent au client."
"Tous les paiements que vous avez sélectionnés pour des remboursements "
"automatiques seront immédiatement communiqués à la demande de remboursement "
"au fournisseur de paiement respectif. Les remboursements manuels seront "
"créés en tant que remboursements en attente, vous pourrez ensuite les "
"marquer comme terminés une fois que vous aurez effectivement transféré "
"largent au client."
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:32
msgid "Refund to original payment method"
@@ -29527,8 +29558,11 @@ msgid "The new question has been created."
msgstr "La nouvelle question a été créée."
#: pretix/control/views/item.py:918
#, fuzzy
#| msgctxt "subevent"
#| msgid "The selected dates have been deleted or disabled."
msgid "The selected quotas have been deleted or disabled."
msgstr "Les quotas sélectionnés ont été supprimés ou désactivés."
msgstr "Les dates sélectionnées ont été supprimées ou désactivées."
#: pretix/control/views/item.py:1074
msgid "The new quota has been created."
@@ -30268,9 +30302,12 @@ msgstr ""
"Ce plugin n'est actuellement pas autorisé pour ce compte d'organisateur."
#: pretix/control/views/organizer.py:832
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "This plugin can be enabled or disabled for events individually."
msgid "This plugin cannot be activated for event {}."
msgstr "Ce plugin ne peut pas être activé pour l'événement {}."
msgstr ""
"Ce plugin peut être activé ou désactivé individuellement pour chaque "
"événement."
#: pretix/control/views/organizer.py:901
msgid "The team has been created. You can now add members to the team."
@@ -31325,9 +31362,10 @@ msgid "{width} x {height} mm label"
msgstr "{width} x {height} mm étiquette"
#: pretix/plugins/badges/templates.py:265
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "{width} x {height} mm label"
msgid "{width} x {height} inch label"
msgstr "{width} x {height} pouce étiquette"
msgstr "{width} x {height} mm étiquette"
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_info.html:16
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:27
+71 -34
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-06-01 09:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"PO-Revision-Date: 2026-05-12 06:34+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
"Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.5\n"
"X-Generator: Weblate 5.17.1\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -608,20 +608,20 @@ msgstr ""
"更を含みます。"
#: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Quota handling"
msgid "Quota changed"
msgstr "クォータが変更されました"
msgstr "クォータの処理"
#: pretix/api/webhooks.py:414
msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"これには、クォータの作成、削除、開始または終了といった関連イベントが含まれま"
"す。結果として得られる可用性の変更については、Webhookが送信されません。"
#: pretix/api/webhooks.py:419
msgid "Shop taken live"
msgstr "ショップがオンラインになりました"
msgstr "ショップが公開中になりました"
#: pretix/api/webhooks.py:423
msgid "Shop taken offline"
@@ -3394,13 +3394,11 @@ msgid ""
"The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"."
msgstr ""
"フィールド「%(label)s」には、\"%(chars)s\" のような特殊文字を含めることはでき"
"ません。"
#: pretix/base/forms/questions.py:305
#, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr "フィールド「%(label)s」には URL (%(url)s) を含めることができません。"
msgstr ""
#: pretix/base/forms/questions.py:338
msgctxt "phonenumber"
@@ -8191,14 +8189,19 @@ msgid "Program times"
msgstr "プログラム時間"
#: pretix/base/pdf.py:503
#, fuzzy
#| msgid ""
#| "2017-05-31 10:00 12:00\n"
#| "2017-05-31 14:00 16:00\n"
#| "2017-05-31 14:00 2017-06-01 14:00"
msgid ""
"2017-05-31 10:00 12:00, Room 1\n"
"2017-05-31 14:00 16:00, Room 2\n"
"2017-05-31 14:00 2017-06-01 14:00, Building A"
msgstr ""
"2017-05-31 10:00 12:00、部屋1\n"
"2017-05-31 14:00 16:00、部屋2\n"
"2017-05-31 14:00 2017-06-01 14:00、ビルA"
"2017-05-31 10:00 12:00\n"
"2017-05-31 14:00 16:00\n"
"2017-05-31 14:00 2017-06-01 14:00"
#: pretix/base/pdf.py:507
msgid "Reusable Medium ID"
@@ -8707,7 +8710,13 @@ msgid "This voucher code is not known in our database."
msgstr "このバウチャーコードは、当社のデータベースには登録されていません。"
#: pretix/base/services/cart.py:165
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products."
msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product."
@@ -8719,7 +8728,15 @@ msgstr[0] ""
"した場合にのみ使用できます。"
#: pretix/base/services/cart.py:170
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
#| msgid_plural ""
#| "The voucher code \"%(voucher)s\" can only be used if you select at least "
#| "%(number)s matching products. We have therefore removed some positions "
#| "from your cart that can no longer be purchased like this."
msgid ""
"The voucher code \"%(voucher)s\" can only be used if you select at least "
"%(number)s matching product. We have therefore removed some positions from "
@@ -13820,8 +13837,6 @@ msgid ""
"You entered an URL, which is not allowed. Please remove %(match)s from your "
"input."
msgstr ""
"URL を入力しましたが、許可されていません。入力から %(match)s を削除してくださ"
"い。"
#: pretix/base/views/errors.py:48
msgid ""
@@ -15718,8 +15733,14 @@ msgid "inactive"
msgstr "無効"
#: pretix/control/forms/item.py:1414
#, fuzzy
#| msgid ""
#| "Sample Conference Center\n"
#| "Heidelberg, Germany"
msgid "Sample Conference Center, Heidelberg, Germany"
msgstr "サンプル・カンファレンスセンター, ドイツ, ハイデルベルク"
msgstr ""
"サンプル・カンファレンスセンター\n"
"ドイツ、ハイデルベルク"
#: pretix/control/forms/mailsetup.py:42
msgid "Hostname"
@@ -22960,8 +22981,11 @@ msgid "Quota history"
msgstr "クォータ履歴"
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:6
#, fuzzy
#| msgctxt "subevent"
#| msgid "Change multiple dates"
msgid "Change multiple quotas"
msgstr "複数のクォータを変更"
msgstr "複数の日付を変更"
#: pretix/control/templates/pretixcontrol/items/quota_bulk_edit.html:8
#: pretix/control/templates/pretixcontrol/organizers/device_bulk_edit.html:8
@@ -23007,14 +23031,17 @@ msgstr "以下の製品は販売できなくなる可能性があります:"
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:4
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:6
#, fuzzy
#| msgid "Delete quota"
msgid "Delete quotas"
msgstr "クォータを削除"
#: pretix/control/templates/pretixcontrol/items/quota_delete_bulk.html:10
#, python-format
#, fuzzy, python-format
#| msgid "Are you sure you want to delete the following dates?"
msgid "Are you sure you want to delete the following quota?"
msgid_plural "Are you sure you want to delete the following %(num)s quotas?"
msgstr[0] "以下の%(num)sのクォータを削除してもよろしいですか?"
msgstr[0] "以下の日付を削除してもよろしいですか?"
#: pretix/control/templates/pretixcontrol/items/quotas.html:9
msgid ""
@@ -23607,14 +23634,12 @@ msgid ""
"generated once the customer pays the invoice or selects a payment method "
"that requires an invoice."
msgstr ""
"この注文は、最後の請求書が生成された後に変更されました。新しい請求書はまだ作"
"成されていません。請求書は支払い時に生成されるか、支払方法によって必要とされ"
"る場合に設定されているためです。お客様が請求書を支払うか、請求書が必要な支払"
"方法を選択すると、新しい請求書が生成されます。"
#: pretix/control/templates/pretixcontrol/order/index.html:152
#, fuzzy
#| msgid "Request invoice"
msgid "Reissue invoice"
msgstr "請求書を再発行する"
msgstr "請求書を要求"
#: pretix/control/templates/pretixcontrol/order/index.html:161
#: pretix/control/templates/pretixcontrol/order/index.html:413
@@ -24039,15 +24064,22 @@ msgid "How should the refund be sent?"
msgstr "どのように払い戻しますか?"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:25
#, fuzzy
#| msgid ""
#| "Any payments that you selected for automatical refunds will be "
#| "immediately communicate the refund request to the respective payment "
#| "provider. Manual refunds will be created as pending refunds, you can then "
#| "later mark them as done once you actually transferred the money back to "
#| "the customer."
msgid ""
"Any payments you selected for automatic refunds will have the refund request "
"sent immediately to the respective payment provider. Manual refunds will be "
"created as pending refunds, which you can later mark as done once you have "
"actually transferred the money back to the customer."
msgstr ""
"自動返金をご選択いただいたすべての支払いについては、返金リクエストが直ちに該"
"当する決済プロバイダーへ送信されます。手動返金は保留中の返金として作成され、"
"実際に顧客に返金した後で完了としてマークできます。"
"自動払い戻しに選択した支払いは、該当する決済プロバイダーに払い戻し要求が即座"
"に通知されます。手動払い戻しは保留中の払い戻しとして作成され、実際に顧客に送"
"金した後で完了済みとしてマークできます。"
#: pretix/control/templates/pretixcontrol/order/refund_choose.html:32
msgid "Refund to original payment method"
@@ -28472,8 +28504,11 @@ msgid "The new question has been created."
msgstr "新しい質問が作成されました。"
#: pretix/control/views/item.py:918
#, fuzzy
#| msgctxt "subevent"
#| msgid "The selected dates have been deleted or disabled."
msgid "The selected quotas have been deleted or disabled."
msgstr "選択したクォータは削除されたか無効す。"
msgstr "選択した日付は削除されたか無効になっています。"
#: pretix/control/views/item.py:1074
msgid "The new quota has been created."
@@ -29180,9 +29215,10 @@ msgid "This plugin is currently not allowed for this organizer account."
msgstr "このプラグインは現在、この主催者アカウントでは許可されていません。"
#: pretix/control/views/organizer.py:832
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "This plugin can be enabled or disabled for events individually."
msgid "This plugin cannot be activated for event {}."
msgstr "このプラグインは、イベント{}に対してアクティベートできません。"
msgstr "このプラグインは、イベントごとに個別に有効化または無効化できま。"
#: pretix/control/views/organizer.py:901
msgid "The team has been created. You can now add members to the team."
@@ -30200,9 +30236,10 @@ msgid "{width} x {height} mm label"
msgstr "{width} x {height} mm ラベル"
#: pretix/plugins/badges/templates.py:265
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "{width} x {height} mm label"
msgid "{width} x {height} inch label"
msgstr "{width} x {height} インチラベル"
msgstr "{width} x {height} mm ラベル"
#: pretix/plugins/badges/templates/pretixplugins/badges/control_order_info.html:16
#: pretix/plugins/badges/templates/pretixplugins/badges/index.html:27
+12 -8
View File
@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-06-01 09:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/"
"ko/>\n"
"PO-Revision-Date: 2026-02-01 21:00+0000\n"
"Last-Translator: z3rrry <z3rrry@gmail.com>\n"
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/ko/"
">\n"
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.5\n"
"X-Generator: Weblate 5.15.2\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -48,7 +48,7 @@ msgstr "사전판매 시작하지 않음"
#: pretix/control/templates/pretixcontrol/subevents/index.html:176
#: pretix/control/views/dashboards.py:549
msgid "On sale"
msgstr "세일 중"
msgstr ""
#: pretix/_base_settings.py:89
msgid "English"
@@ -427,8 +427,10 @@ msgstr ""
#: pretix/api/serializers/organizer.py:495
#: pretix/control/views/organizer.py:1035
#, fuzzy
#| msgid "pretix account invitation"
msgid "Account invitation"
msgstr "계정 초대"
msgstr "프레틱스 계정 초대"
#: pretix/api/serializers/organizer.py:516
#: pretix/control/views/organizer.py:1134
@@ -18085,8 +18087,10 @@ msgid "A payment has been performed."
msgstr "수동 거래가 수행되었습니다."
#: pretix/control/logdisplay.py:807
#, fuzzy
#| msgid "A manual transaction has been performed."
msgid "A refund has been performed. "
msgstr "환불이 처리되었습니다. "
msgstr "수동 거래가 수행되었습니다."
#: pretix/control/logdisplay.py:808
#, python-brace-format
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-27 15:47+0000\n"
"PO-Revision-Date: 2026-06-01 09:00+0000\n"
"PO-Revision-Date: 2026-05-21 15:08+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/"
"projects/pretix/pretix/zh_Hant/>\n"
@@ -595,16 +595,16 @@ msgid ""
msgstr "這包括新增或刪除的產品,以及對變體或捆綁等巢狀物件的更改。"
#: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Quota handling"
msgid "Quota changed"
msgstr "配額改變了"
msgstr "額度處理"
#: pretix/api/webhooks.py:414
msgid ""
"This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability."
msgstr ""
"這包括建立、刪除、開啟或關閉配額等相關事件。 沒有傳送webhook來更改結果的可用"
"性。"
#: pretix/api/webhooks.py:419
msgid "Shop taken live"
@@ -650,7 +650,7 @@ msgstr "優惠券已更改"
msgid ""
"Only includes explicit changes to the voucher, not e.g. an increase of the "
"number of redemptions."
msgstr "僅包括對代金券的明確更改,例如不包括兌換次數的增加。"
msgstr ""
#: pretix/api/webhooks.py:460
msgid "Voucher deleted"
@@ -669,16 +669,22 @@ msgid "Customer account anonymized"
msgstr "客戶帳戶已匿名化"
#: pretix/api/webhooks.py:476
#, fuzzy
#| msgid "Gift card code"
msgid "Gift card added"
msgstr "添加了禮品卡"
msgstr "禮品卡代碼"
#: pretix/api/webhooks.py:480
#, fuzzy
#| msgid "Gift card code"
msgid "Gift card modified"
msgstr "禮品卡修改了"
msgstr "禮品卡代碼"
#: pretix/api/webhooks.py:484
#, fuzzy
#| msgid "Gift card transactions"
msgid "Gift card used in transaction"
msgstr "交易中使用的禮品卡"
msgstr "禮品卡交易"
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:1074
+9 -9
View File
@@ -64,7 +64,7 @@ from pretix.base.timeframes import (
from pretix.control.forms.widgets import Select2
from pretix.helpers.filenames import safe_for_filename
from pretix.helpers.iter import chunked_iterable
from pretix.helpers.reportlab import FontFallbackParagraph
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.helpers.templatetags.jsonfield import JSONExtract
from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -343,7 +343,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
]
story = [
FontFallbackParagraph(
PlainTextParagraph(
cl.name,
headlinestyle
),
@@ -351,7 +351,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
if cl.subevent:
story += [
Spacer(1, 3 * mm),
FontFallbackParagraph(
PlainTextParagraph(
'{} ({} {})'.format(
cl.subevent.name,
cl.subevent.get_date_range_display(),
@@ -381,10 +381,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
headrowstyle.fontName = 'OpenSansBd'
for q in questions:
txt = str(q.question)
p = FontFallbackParagraph(txt, headrowstyle)
p = PlainTextParagraph(txt, headrowstyle)
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, headrowstyle)
p = PlainTextParagraph(txt, headrowstyle)
tdata[0].append(p)
qs = self._get_queryset(cl, form_data)
@@ -431,8 +431,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '',
'' if op.order.status != Order.STATUS_PAID else '',
op.order.code,
FontFallbackParagraph(name, self.get_style()),
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
PlainTextParagraph(name, self.get_style()),
PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
]
acache = {}
if op.addon_to:
@@ -443,10 +443,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
for q in questions:
txt = acache.get(q.pk, '')
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>')
p = FontFallbackParagraph(txt, self.get_style())
p = PlainTextParagraph(txt, self.get_style())
while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
txt = txt[:len(txt) - 50] + "..."
p = FontFallbackParagraph(txt, self.get_style())
p = PlainTextParagraph(txt, self.get_style())
row.append(p)
if op.order.status != Order.STATUS_PAID:
tstyledata += [
+2 -7
View File
@@ -34,7 +34,6 @@
import json
import logging
import urllib.parse
from collections import OrderedDict
from decimal import Decimal
@@ -42,7 +41,6 @@ import paypalrestsdk
import paypalrestsdk.exceptions
from django import forms
from django.contrib import messages
from django.core import signing
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
@@ -58,6 +56,7 @@ from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.paypal.api import Api
from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -349,11 +348,7 @@ class Paypal(BasePaymentProvider):
for link in payment.links:
if link.method == "REDIRECT" and link.rel == "approval_url":
if request.session.get('iframe_session', False):
signer = signing.Signer(salt='safe-redirect')
return (
build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' +
urllib.parse.quote(signer.sign(link.href))
)
return safelink(link.href, framebreak=True)
else:
return str(link.href)
else:
@@ -1,33 +0,0 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+1 -2
View File
@@ -21,13 +21,12 @@
#
from django.urls import include, re_path
from .views import abort, oauth_disconnect, redirect_view, success
from .views import abort, oauth_disconnect, success
event_patterns = [
re_path(r'^paypal/', include([
re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'),
re_path(r'^redirect/$', redirect_view, name='redirect'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
+1 -19
View File
@@ -39,13 +39,10 @@ from decimal import Decimal
import paypalrestsdk
import paypalrestsdk.exceptions
from django.contrib import messages
from django.core import signing
from django.db.models import Sum
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render
from django.http import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django_scopes import scopes_disabled
@@ -61,21 +58,6 @@ from pretix.plugins.paypal.payment import Paypal
logger = logging.getLogger('pretix.plugins.paypal')
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
r = render(request, 'pretixplugins/paypal/redirect.html', {
'url': url,
})
r._csp_ignore = True
return r
def success(request, *args, **kwargs):
pid = request.GET.get('paymentId')
token = request.GET.get('token')
@@ -1,33 +0,0 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+1 -3
View File
@@ -22,15 +22,13 @@
from django.urls import include, re_path
from .views import (
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
success, webhook,
PayView, XHRView, abort, isu_disconnect, isu_return, success, webhook,
)
event_patterns = [
re_path(r'^paypal2/', include([
re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'),
re_path(r'^redirect/$', redirect_view, name='redirect'),
re_path(r'^xhr/$', XHRView.as_view(), name='xhr'),
re_path(r'^pay/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'),
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'),
+1 -19
View File
@@ -36,13 +36,10 @@ import logging
from decimal import Decimal
from django.contrib import messages
from django.core import signing
from django.core.cache import cache
from django.db import transaction
from django.db.models import Sum
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
from django.http import Http404, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -104,21 +101,6 @@ class PaypalOrderView:
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
r = render(request, 'pretixplugins/paypal2/redirect.html', {
'url': url,
})
r._csp_ignore = True
return r
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(xframe_options_exempt, 'dispatch')
class XHRView(View):
+78 -78
View File
@@ -36,7 +36,7 @@ from reportlab.lib import colors, pagesizes
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.units import mm
from reportlab.platypus import (
KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle,
KeepTogether, PageTemplate, Spacer, Table, TableStyle,
)
from pretix.base.exporter import BaseExporter
@@ -49,7 +49,7 @@ from pretix.base.timeframes import (
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
)
from pretix.control.forms.filter import get_all_payment_providers
from pretix.helpers.reportlab import FontFallbackParagraph
from pretix.helpers.reportlab import PlainTextParagraph
from pretix.plugins.reports.exporters import ReportlabExportMixin
@@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [
[
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold),
FontFallbackParagraph(_("Price"), tstyle_bold_right),
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right),
FontFallbackParagraph("#", tstyle_bold_right),
FontFallbackParagraph(_("Net total"), tstyle_bold_right),
FontFallbackParagraph(_("Tax total"), tstyle_bold_right),
FontFallbackParagraph(_("Gross total"), tstyle_bold_right),
PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold),
PlainTextParagraph(_("Price"), tstyle_bold_right),
PlainTextParagraph(_("Tax rate"), tstyle_bold_right),
PlainTextParagraph("#", tstyle_bold_right),
PlainTextParagraph(_("Net total"), tstyle_bold_right),
PlainTextParagraph(_("Tax total"), tstyle_bold_right),
PlainTextParagraph(_("Gross total"), tstyle_bold_right),
]
]
@@ -347,12 +347,12 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if e != last_group:
if last_group_head_idx > 0 and e is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata.append(
[
FontFallbackParagraph(
PlainTextParagraph(
e,
tstyle_bold,
),
@@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
text = self._transaction_row_label(r)
tdata.append(
[
FontFallbackParagraph(text, tstyle),
Paragraph(
PlainTextParagraph(text, tstyle),
PlainTextParagraph(
money_filter(r["price"], currency)
if "price" in r and r["price"] is not None
else "",
tstyle_right,
),
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
Paragraph(str(r["sum_cont"]), tstyle_right),
Paragraph(
PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
PlainTextParagraph(
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
),
Paragraph(money_filter(r["sum_tax"], currency), tstyle_right),
Paragraph(money_filter(r["sum_price"], currency), tstyle_right),
PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right),
PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right),
]
)
sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"]
@@ -398,19 +398,19 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
sum_tax_by_group += r["sum_tax"]
if last_group_head_idx > 0 and last_group is not None:
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
if len(sum_tax_by_tax_rate) > 1:
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
tdata.append(
[
FontFallbackParagraph(_("Sum"), tstyle),
Paragraph("", tstyle_right),
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right),
Paragraph("", tstyle_right),
Paragraph(
PlainTextParagraph(_("Sum"), tstyle),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph(
money_filter(
sum_price_by_tax_rate[tax_rate]
- sum_tax_by_tax_rate[tax_rate],
@@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_right,
),
Paragraph(
PlainTextParagraph(
money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right
),
Paragraph(
PlainTextParagraph(
money_filter(sum_price_by_tax_rate[tax_rate], currency),
tstyle_right,
),
@@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append(
[
FontFallbackParagraph(_("Sum"), tstyle_bold),
Paragraph("", tstyle_right),
Paragraph("", tstyle_right),
Paragraph("", tstyle_bold_right),
Paragraph(
PlainTextParagraph(_("Sum"), tstyle_bold),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph("", tstyle_right),
PlainTextParagraph("", tstyle_bold_right),
PlainTextParagraph(
money_filter(
sum(sum_price_by_tax_rate.values())
- sum(sum_tax_by_tax_rate.values()),
@@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(sum(sum_tax_by_tax_rate.values()), currency),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(sum(sum_price_by_tax_rate.values()), currency),
tstyle_bold_right,
),
@@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata = [
[
FontFallbackParagraph(_("Payment method"), tstyle_bold),
FontFallbackParagraph(_("Payments"), tstyle_bold_right),
FontFallbackParagraph(_("Refunds"), tstyle_bold_right),
FontFallbackParagraph(_("Total"), tstyle_bold_right),
PlainTextParagraph(_("Payment method"), tstyle_bold),
PlainTextParagraph(_("Payments"), tstyle_bold_right),
PlainTextParagraph(_("Refunds"), tstyle_bold_right),
PlainTextParagraph(_("Total"), tstyle_bold_right),
]
]
@@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
for p in providers:
tdata.append(
[
Paragraph(provider_names.get(p, p), tstyle),
FontFallbackParagraph(
PlainTextParagraph(provider_names.get(p, p), tstyle),
PlainTextParagraph(
money_filter(payments_by_provider[p], currency)
if p in payments_by_provider
else "",
tstyle_right,
),
Paragraph(
PlainTextParagraph(
money_filter(refunds_by_provider[p], currency)
if p in refunds_by_provider
else "",
tstyle_right,
),
Paragraph(
PlainTextParagraph(
money_filter(
payments_by_provider.get(p, Decimal("0.00"))
- refunds_by_provider.get(p, Decimal("0.00")),
@@ -563,20 +563,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tdata.append(
[
FontFallbackParagraph(_("Sum"), tstyle_bold),
Paragraph(
PlainTextParagraph(_("Sum"), tstyle_bold),
PlainTextParagraph(
money_filter(
sum(payments_by_provider.values(), Decimal("0.00")), currency
),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(
sum(refunds_by_provider.values(), Decimal("0.00")), currency
),
tstyle_bold_right,
),
Paragraph(
PlainTextParagraph(
money_filter(
sum(payments_by_provider.values(), Decimal("0.00"))
- sum(refunds_by_provider.values(), Decimal("0.00")),
@@ -641,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
open_before = tx_before - p_before + r_before
tdata.append(
[
FontFallbackParagraph(
PlainTextParagraph(
_("Pending payments at {datetime}").format(
datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone(
@@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
tstyle,
),
"",
Paragraph(money_filter(open_before, currency), tstyle_right),
PlainTextParagraph(money_filter(open_before, currency), tstyle_right),
]
)
else:
@@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00")
tdata.append(
[
FontFallbackParagraph(_("Orders"), tstyle),
Paragraph("+", tstyle_center),
Paragraph(money_filter(tx_during, currency), tstyle_right),
PlainTextParagraph(_("Orders"), tstyle),
PlainTextParagraph("+", tstyle_center),
PlainTextParagraph(money_filter(tx_during, currency), tstyle_right),
]
)
tdata.append(
[
FontFallbackParagraph(_("Payments"), tstyle),
Paragraph("-", tstyle_center),
Paragraph(money_filter(p_during, currency), tstyle_right),
PlainTextParagraph(_("Payments"), tstyle),
PlainTextParagraph("-", tstyle_center),
PlainTextParagraph(money_filter(p_during, currency), tstyle_right),
]
)
tdata.append(
[
FontFallbackParagraph(_("Refunds"), tstyle),
Paragraph("+", tstyle_center),
Paragraph(money_filter(r_during, currency), tstyle_right),
PlainTextParagraph(_("Refunds"), tstyle),
PlainTextParagraph("+", tstyle_center),
PlainTextParagraph(money_filter(r_during, currency), tstyle_right),
]
)
open_after = open_before + tx_during - p_during + r_during
tdata.append(
[
Paragraph(
PlainTextParagraph(
_("Pending payments at {datetime}").format(
datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_bold,
),
Paragraph("=", tstyle_center),
Paragraph(money_filter(open_after, currency), tstyle_bold_right),
PlainTextParagraph("=", tstyle_center),
PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
]
)
tstyledata += [
@@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
)
tdata.append(
[
Paragraph(
PlainTextParagraph(
_("Total gift card value at {datetime}").format(
datetime=date_format(
(df_start - datetime.timedelta.resolution).astimezone(
@@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle,
),
Paragraph(money_filter(tx_before, currency), tstyle_right),
PlainTextParagraph(money_filter(tx_before, currency), tstyle_right),
]
)
else:
@@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00")
tdata.append(
[
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle),
Paragraph(money_filter(tx_during_pos, currency), tstyle_right),
PlainTextParagraph(_("Gift card transactions (credit)"), tstyle),
PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right),
]
)
@@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
] or Decimal("0.00")
tdata.append(
[
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle),
Paragraph(money_filter(tx_during_neg, currency), tstyle_right),
PlainTextParagraph(_("Gift card transactions (debit)"), tstyle),
PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right),
]
)
open_after = tx_before + tx_during_pos + tx_during_neg
tdata.append(
[
Paragraph(
PlainTextParagraph(
_("Total gift card value at {datetime}").format(
datetime=date_format(
((df_end or now()) - datetime.timedelta.resolution).astimezone(
@@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
),
tstyle_bold,
),
Paragraph(money_filter(open_after, currency), tstyle_bold_right),
PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
]
)
tstyledata += [
@@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
style_small.leading = 10
story = [
FontFallbackParagraph(self.verbose_name, style_h1),
PlainTextParagraph(self.verbose_name, style_h1),
Spacer(0, 3 * mm),
FontFallbackParagraph(
"<br />".join(escape(f) for f in self.describe_filters(form_data)),
PlainTextParagraph(
"\n".join(escape(f) for f in self.describe_filters(form_data)),
style_small,
),
]
@@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s:
story += [
Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2),
PlainTextParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
@@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
if s:
story += [
Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2),
PlainTextParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
@@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Open items") + c_head, style_h2),
PlainTextParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
@@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Gift cards") + c_head, style_h2),
PlainTextParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm),
*s,
]
+13 -13
View File
@@ -70,7 +70,7 @@ from pretix.base.timeframes import (
)
from pretix.control.forms.filter import OverviewFilterForm
from pretix.helpers.reportlab import (
FontFallbackParagraph, register_ttf_font_if_new,
PlainTextParagraph, register_ttf_font_if_new,
)
from pretix.presale.style import get_fonts
@@ -282,7 +282,7 @@ class OverviewReport(Report):
headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd'
story = [
FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
PlainTextParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
Spacer(1, 5 * mm)
]
return story
@@ -292,7 +292,7 @@ class OverviewReport(Report):
if form_data.get('date_axis') and form_data.get('date_range'):
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')],
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '',
@@ -305,13 +305,13 @@ class OverviewReport(Report):
subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
except SubEvent.DoesNotExist:
subevent = self.form_data.get('subevent')
story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
story.append(PlainTextParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
story.append(Spacer(1, 5 * mm))
if form_data.get('subevent_date_range'):
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
story += [
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
PlainTextParagraph(_('{axis} between {start} and {end}').format(
axis=_('Event date'),
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '',
end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '',
@@ -384,13 +384,13 @@ class OverviewReport(Report):
tdata = [
[
_('Product'),
FontFallbackParagraph(_('Canceled'), tstyle_th),
PlainTextParagraph(_('Canceled'), tstyle_th),
'',
FontFallbackParagraph(_('Expired'), tstyle_th),
PlainTextParagraph(_('Expired'), tstyle_th),
'',
FontFallbackParagraph(_('Approval pending'), tstyle_th),
PlainTextParagraph(_('Approval pending'), tstyle_th),
'',
FontFallbackParagraph(_('Purchased'), tstyle_th),
PlainTextParagraph(_('Purchased'), tstyle_th),
'', '', '', '', ''
],
[
@@ -421,14 +421,14 @@ class OverviewReport(Report):
for tup in items_by_category:
if tup[0]:
tdata.append([
FontFallbackParagraph(str(tup[0]), tstyle_bold)
PlainTextParagraph(str(tup[0]), tstyle_bold)
])
for l, s in states:
tdata[-1].append(str(tup[0].num[l][0]))
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
for item in tup[1]:
tdata.append([
FontFallbackParagraph(str(item), tstyle)
PlainTextParagraph(str(item), tstyle)
])
for l, s in states:
tdata[-1].append(str(item.num[l][0]))
@@ -436,7 +436,7 @@ class OverviewReport(Report):
if item.has_variations:
for var in item.all_variations:
tdata.append([
FontFallbackParagraph(" " + str(var), tstyle)
PlainTextParagraph(" " + str(var), tstyle)
])
for l, s in states:
tdata[-1].append(str(var.num[l][0]))
@@ -568,7 +568,7 @@ class OrderTaxListReportPDF(Report):
tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
story = [
FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
PlainTextParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
Spacer(1, 5 * mm)
]
tdata = [
+3 -15
View File
@@ -46,7 +46,6 @@ import stripe
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core import signing
from django.db import transaction
from django.http import HttpRequest
from django.template.loader import get_template
@@ -72,6 +71,7 @@ from pretix.base.payment import (
)
from pretix.base.plugins import get_all_plugins
from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries
from pretix.helpers.http import get_client_ip
@@ -745,15 +745,7 @@ class StripeMethod(BasePaymentProvider):
def redirect(self, request, url):
if request.session.get('iframe_session', False):
return (
build_absolute_uri(request.event, 'plugins:stripe:redirect') +
'?data=' + signing.dumps({
'url': url,
'session': {
'payment_stripe_order_secret': request.session['payment_stripe_order_secret'],
},
}, salt='safe-redirect')
)
return safelink(url, framebreak=True)
else:
return str(url)
@@ -1053,11 +1045,7 @@ class StripeMethod(BasePaymentProvider):
'hash': payment.order.tagged_secret('plugins:stripe'),
})
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False):
return build_absolute_uri(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
'url': url,
'session': {},
}, salt='safe-redirect')
return safelink(url, framebreak=True)
return url
def _confirm_payment_intent(self, request, payment):
@@ -1,33 +0,0 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+1 -2
View File
@@ -25,13 +25,12 @@ from pretix.multidomain import event_url
from .views import (
OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView,
oauth_disconnect, oauth_return, redirect_view, webhook,
oauth_disconnect, oauth_return, webhook,
)
event_patterns = [
re_path(r'^stripe/', include([
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
re_path(r'^redirect/$', redirect_view, name='redirect'),
re_path(r'^return/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ScaView.as_view(), name='sca'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$',
+2 -31
View File
@@ -34,13 +34,11 @@
import json
import logging
import urllib.parse
import requests
from django.contrib import messages
from django.core import signing
from django.db import transaction
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -64,7 +62,7 @@ from pretix.control.views.event import DecoupleMixin
from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.helpers import OF_SELF
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.plugins.stripe.tasks import (
@@ -74,28 +72,6 @@ from pretix.plugins.stripe.tasks import (
logger = logging.getLogger('pretix.plugins.stripe')
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
try:
data = signing.loads(request.GET.get('data', ''), salt='safe-redirect')
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if 'go' in request.GET:
if 'session' in data:
for k, v in data['session'].items():
request.session[k] = v
return redirect(data['url'])
else:
params = request.GET.copy()
params['go'] = '1'
r = render(request, 'pretixplugins/stripe/redirect.html', {
'url': build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params),
})
r._csp_ignore = True
return r
@scopes_disabled()
def oauth_return(request, *args, **kwargs):
import stripe
@@ -514,11 +490,6 @@ class StripeOrderView:
return self.request.event.get_payment_providers()[self.payment.provider]
def _redirect_to_order(self):
if self.request.session.get('payment_stripe_order_secret') != self.order.secret and not self.payment.provider.startswith('stripe'):
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
+2 -1
View File
@@ -51,6 +51,7 @@ from django.http import HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
@@ -1634,7 +1635,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
meta_info = {
'contact_form_data': self.cart_session.get('contact_form_data', {}),
'confirm_messages': [
str(m) for m in self.confirm_messages.values()
conditional_escape(str(m)) for m in self.confirm_messages.values()
]
}
api_meta = {}
+1 -1
View File
@@ -144,7 +144,7 @@ checkout_confirm_messages = EventPluginSignal()
This signal is sent out to retrieve short messages that need to be acknowledged by the user before the
order can be completed. This is typically used for something like "accept the terms and conditions".
Receivers are expected to return a dictionary where the keys are globally unique identifiers for the
message and the values can be arbitrary HTML.
message and the values can be a SafeString containing arbitrary HTML, or a string that will be HTML-escaped.
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -176,7 +176,7 @@
<div class="checkbox">
<label for="input_confirm_{{ key }}">
<input type="checkbox" class="checkbox" value="yes" name="confirm_{{ key }}" id="input_confirm_{{ key }}" required>
{{ desc|safe }}
{{ desc }}
</label>
</div>
{% endfor %}
@@ -2,6 +2,7 @@
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
{% if cart_namespace %}
@@ -23,9 +24,8 @@
class="btn btn-primary btn-lg" target="_blank">
{% trans "Continue in new tab" %}
</a>
<script>
window.open('{{ url|escapejs }}');
</script>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% else %}
<h1>{% trans "Cookies not supported" %}</h1>
@@ -4,6 +4,7 @@
{% load eventsignal %}
{% load money %}
{% load eventurl %}
{% load wrap_in %}
{% block title %}{% trans "Registration details" %}{% endblock %}
{% block content %}
<h2 class="h1">
@@ -48,7 +49,7 @@
</div>
<div class="panel-body">
<p>
{% blocktrans trimmed with email="<strong>"|add:order.email|add:"</strong>"|safe %}
{% blocktrans trimmed with email=order.email|wrap_in:"strong" %}
This order is managed for you by {{ email }}. Please contact them for any questions regarding
payment, cancellation or changes to this order.
{% endblocktrans %}
-1
View File
@@ -536,7 +536,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
**pass_through_url_params,
})
})
r._csp_ignore = True
return r
if not request.event.all_sales_channels and request.sales_channel.identifier not in (s.identifier for s in request.event.limit_sales_channels.all()):
-1
View File
@@ -125,7 +125,6 @@ class WaitingView(EventViewMixin, FormView):
request.event, "presale:event.waitinglist", kwargs={'cart_namespace': kwargs.get('cart_namespace')}
) + '?' + url_replace(request, 'require_cookie', '', 'iframe', '', 'locale', request.GET.get('locale', get_language_without_region()))
})
r._csp_ignore = True
return r
if not self.itemvars:
+4 -16
View File
@@ -58,11 +58,10 @@ from django.utils.translation import gettext_lazy as _ # NOQA
_config = configparser.RawConfigParser()
if 'PRETIX_CONFIG_FILE' in os.environ:
config_files = [os.environ['PRETIX_CONFIG_FILE']]
_config.read_file(open(os.environ.get('PRETIX_CONFIG_FILE'), encoding='utf-8'))
else:
config_files = ['/etc/pretix/pretix.cfg', os.path.expanduser('~/.pretix.cfg'), 'pretix.cfg']
_config.read(config_files, encoding='utf-8')
_config.read(['/etc/pretix/pretix.cfg', os.path.expanduser('~/.pretix.cfg'), 'pretix.cfg'],
encoding='utf-8')
config = EnvOrParserConfig(_config)
CONFIG_FILE = config
@@ -705,7 +704,7 @@ if config.has_option('sentry', 'dsn') and not any(c in sys.argv for c in ('shell
from sentry_sdk.integrations.logging import (
LoggingIntegration, ignore_logger,
)
from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber
from sentry_sdk.scrubber import EventScrubber, DEFAULT_DENYLIST
from .sentry import PretixSentryIntegration, setup_custom_filters
@@ -896,14 +895,3 @@ VITE_DEV_SERVER = f"http://localhost:{VITE_DEV_SERVER_PORT}"
VITE_DEV_MODE = DEBUG
VITE_IGNORE = False # Used to ignore `collectstatic`/`rebuild`
PRETIX_WIDGET_VITE = os.environ.get('PRETIX_WIDGET_VITE', '') not in ('', '0')
if DEBUG:
# Reload if settings file changes
config_files_to_watch = [Path(x).absolute() for x in config_files]
from django.dispatch import receiver
from django.utils.autoreload import BaseReloader, autoreload_started
@receiver(autoreload_started, dispatch_uid="pretix_watch_config_file")
def watch_config_file(sender: BaseReloader, *args, **kwargs):
sender.extra_files.update(config_files_to_watch)
@@ -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,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { rules as rawRules, allItems, activeItems, allProducts, limitProducts } from './django-interop'
import { rules as rawRules, items, allProducts, limitProducts } from './django-interop'
import { convertToDNF } from './jsonlogic-boolalg'
import RulesEditor from './checkin-rules-editor.vue'
@@ -53,7 +53,7 @@ const missingItems = computed(() => {
}
let missing = []
for (const item of activeItems.value) {
for (const item of items.value) {
if (productsSeen[item.id]) continue
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue
if (item.variations.length > 0) {
@@ -87,7 +87,7 @@ const missingItems = computed(() => {
//- Tab panes
.tab-content
#rules-edit.tab-pane.active(v-if="allItems", role="tabpanel")
#rules-edit.tab-pane.active(v-if="items", role="tabpanel")
RulesEditor
#rules-viz.tab-pane(role="tabpanel")
RulesVisualization
@@ -26,13 +26,11 @@ watch(rules, (newVal) => {
rulesInput.value = JSON.stringify(newVal)
}, { deep: true })
export const activeItems = ref<any[]>([])
export const allItems = ref<any[]>([])
export const items = ref<any[]>([])
const itemsEl = document.querySelector('#items')
if (itemsEl?.textContent) {
allItems.value = JSON.parse(itemsEl.textContent || '[]')
activeItems.value = allItems.value.filter(item => item.active)
items.value = JSON.parse(itemsEl.textContent || '[]')
function checkForInvalidIds (validProducts: Record<string, string>, validVariations: Record<string, string>, rule: any) {
if (rule['and']) {
@@ -59,8 +57,8 @@ if (itemsEl?.textContent) {
}
checkForInvalidIds(
Object.fromEntries(allItems.value.map(p => [p.id, p.name])),
Object.fromEntries(allItems.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' ' + v.name]) ?? [])),
Object.fromEntries(items.value.map(p => [p.id, p.name])),
Object.fromEntries(items.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' ' + v.name]) ?? [])),
rules.value
)
}
@@ -639,13 +639,11 @@ var form_handlers = function (el) {
).append(" ").append($("<div>").text(res.organizer).html())
);
}
if (res.date_range) {
$ret.append(
$("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append(res.date_range)
);
}
$ret.append(
$("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append(res.date_range)
);
return $ret;
},
}).on("select2:select", function () {
+1 -1
View File
@@ -119,7 +119,7 @@ def test_linkify_abs(link):
assert markdown_compile_email(input) == f"<p>{output}</p>"
signer = signing.Signer(salt='safe-redirect')
signer = signing.Signer(salt='safelink-url')
@pytest.mark.parametrize(
+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" />',
)