Compare commits

...

20 Commits

Author SHA1 Message Date
Raphael Michel
dd3821983f Remove back link from 404 error page (#23222967)
I've kept it for 400/403/500/csrffail for now, because they also have a
"try again" link. Yes, both things have browser buttons, but they make
it a *little* clearer to technical users what one could to next, and
especially on csrffail, "step back" is always possible and possibly actually
helpful.
2026-02-17 09:31:24 +01:00
Raphael Michel
119cc50897 Fix inconsistent singular/plural use in text (Z#23223585) 2026-02-17 09:31:08 +01:00
Raphael Michel
61f9cf13b4 Order change: Fix list of unchangeable add-ons not filtered to category (Z#23223330) (#5876) 2026-02-16 15:13:24 +01:00
Raphael Michel
f24429a7c5 Fix tests on Python <3.11 2026-02-16 13:40:00 +01:00
Raphael Michel
29ed07ccce Merge branch 'pajowu/security-plaintext-placeholder' into 'master'
SECURITY: Prevent placeholder injection in plaintext emails

See merge request pretix/pretix!21
2026-02-16 10:59:44 +01:00
Nate Horst
dd0cd7ab0b Translations: Update Thai
Currently translated at 36.0% (2237 of 6207 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Nate Horst
d7df906995 Translations: Update Thai
Currently translated at 36.0% (2237 of 6207 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
839f4b4657 Translations: Update Dutch (Belgium)
Currently translated at 0.1% (12 of 6207 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
74f7e1f61c Translations: Add Dutch (Belgium) 2026-02-16 10:44:21 +01:00
Yasunobu YesNo Kawaguchi
47919afab0 Translations: Update Japanese
Currently translated at 100.0% (256 of 256 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Yasunobu YesNo Kawaguchi
819daa99f7 Translations: Update Japanese
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
8512e79d68 Translations: Update Dutch (informal) (nl_Informal)
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Ruud Hendrickx
52672ae25b Translations: Update Dutch
Currently translated at 100.0% (6207 of 6207 strings)

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

powered by weblate
2026-02-16 10:44:21 +01:00
Raphael Michel
ad752dc617 Fix placeholder injection with django templates 2026-02-13 13:36:12 +01:00
Raphael Michel
43c6c33bd8 SafeFormatter: Ignore conversion spec 2026-02-13 12:35:49 +01:00
Raphael Michel
88c9f8c047 Remove duplicate rendering of plain content without variables 2026-02-13 12:30:01 +01:00
Raphael Michel
2d2663f15f Mark strings as formatted to prevent double-formatting 2026-02-13 12:28:32 +01:00
Kara Engelhardt
ae6014708b SECURITY: Prevent placeholder injcetion in plaintext emails 2026-02-13 12:28:32 +01:00
Richard Schreiber
d1686df07c Move request.GET.items to ctx (#5889) 2026-02-12 12:05:08 +01:00
Richard Schreiber
4d60d7bfbc Fix widget quantity prefill (#5886) 2026-02-12 12:04:11 +01:00
24 changed files with 33367 additions and 151 deletions

View File

@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_callback,
)
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -141,6 +141,7 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
apply_format_map = not isinstance(plain_body, FormattedString)
body_md = self.compile_markdown(plain_body, context)
if context:
linker = bleach.Linker(
@@ -149,12 +150,13 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
body_md = format_map(
body_md,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
if apply_format_map:
body_md = format_map(
body_md,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,

View File

@@ -87,7 +87,7 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ...helpers.format import FormattedString, format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -1178,7 +1178,8 @@ class Order(LockModel, LoggedModel):
recipient = position.attendee_email
email_content = render_mail(template, context)
subject = format_map(subject, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
@@ -2907,7 +2908,8 @@ class OrderPosition(AbstractPosition):
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
email_content = render_mail(template, context)
subject = format_map(subject, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,

View File

@@ -81,7 +81,9 @@ from pretix.base.signals import (
)
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import SafeFormatter, format_map
from pretix.helpers.format import (
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals
@@ -218,6 +220,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if email == INVALID_ADDRESS:
return
if isinstance(template, FormattedString):
raise TypeError("Cannot pass an already formatted body template")
if no_order_links and not plain_text_only:
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
@@ -251,23 +256,28 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if event and attach_tickets and not event.settings.mail_attach_tickets:
attach_tickets = False
with language(locale):
with language(locale), override(timezone):
if isinstance(context, dict) and order:
_autoextend_context(context, order)
# Build raw content
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
content_plain = render_mail(template, context, placeholder_mode=None)
if settings_holder:
signature = str(settings_holder.settings.get('mail_text_signature'))
else:
signature = ""
# Build full plain-text body
if not isinstance(content_plain, FormattedString):
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
else:
body_plain = content_plain
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
# Build subject
subject = str(subject).format_map(TolerantDict(context))
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
if settings_holder:
subject = prefix_subject(settings_holder, subject)
@@ -286,26 +296,24 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
else:
renderer = ClassicMailRenderer(None, organizer)
with override(timezone):
content_plain = render_mail(template, context, placeholder_mode=None)
try:
if 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else:
# Backwards compatibility
warnings.warn('Email renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order)
except:
logger.exception('Could not render HTML body')
body_html = None
try:
if 'context' in inspect.signature(renderer.render).parameters:
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
elif 'position' in inspect.signature(renderer.render).parameters:
# Backwards compatibility
warnings.warn('Email renderer called without context argument because context argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
else:
# Backwards compatibility
warnings.warn('Email renderer called without position argument because position argument is not '
'supported.',
DeprecationWarning)
body_html = renderer.render(content_plain, signature, raw_subject, order)
except:
logger.exception('Could not render HTML body')
body_html = None
m = OutgoingMail.objects.create(
organizer=organizer,
@@ -790,7 +798,12 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
body = format_map(body, context, mode=placeholder_mode)
else:
tpl = get_template(template)
body = tpl.render(context)
context = {
# Known bug, should behave differently for plain and HTML but we'll fix after security release
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
for k, v in context.items()
}
body = FormattedString(tpl.render(context))
return body
@@ -936,7 +949,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += "\r\n\r\n-- \r\n"
if signature:
signature = signature.format(event=event.name if event else '')
signature = format_map(signature, {"event": event.name if event else ''})
body_plain += signature
body_plain += "\r\n\r\n-- \r\n"
@@ -948,7 +961,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _(
"You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format(
event=event.name, orderurl=build_absolute_uri(
orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={
'order': order.code,
'secret': position.web_secret,

View File

@@ -8,9 +8,6 @@
<h1>{% trans "Not found" %}</h1>
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
<p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<p>

View File

@@ -22,6 +22,7 @@
import logging
from string import Formatter
from django.core.exceptions import SuspiciousOperation
from django.utils.html import conditional_escape
logger = logging.getLogger(__name__)
@@ -37,6 +38,17 @@ class PlainHtmlAlternativeString:
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
class FormattedString(str):
"""
A str subclass that has been specifically marked as "already formatted" for email rendering
purposes to avoid duplicate formatting.
"""
__slots__ = ()
def __str__(self):
return self
class SafeFormatter(Formatter):
"""
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
@@ -77,8 +89,19 @@ class SafeFormatter(Formatter):
# Ignore format_spec
return super().format_field(self._prepare_value(value), '')
def convert_field(self, value, conversion):
# Ignore any conversions
if conversion is None:
return value
else:
return str(value)
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
if isinstance(template, FormattedString):
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
return FormattedString(
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
)

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"PO-Revision-Date: 2026-02-12 20:00+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"
@@ -2544,7 +2544,7 @@ msgstr "終了日"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:12
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:91
msgid "Product"
msgstr "品"
msgstr "品"
#: pretix/base/exporters/orderlist.py:652 pretix/base/models/vouchers.py:315
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:5
@@ -4048,7 +4048,7 @@ msgstr "複数の一致する製品が見つかりました。"
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257
#: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:100
msgid "Product variation"
msgstr "品バリエーション"
msgstr "品バリエーション"
#: pretix/base/modelimport_orders.py:161
msgid "The variation can be specified by its internal ID or full name."
@@ -9144,8 +9144,8 @@ msgid ""
"The voucher code used for one of the items in your cart has already been too "
"often. We adjusted the price of the item in your cart."
msgstr ""
"カートの商品の一つに使用されているバウチャーコードはに使用回数を超えていま"
"す。カート内の商品価格を調整しました。"
"カート内のアイテムに使用されバウチャーコードは、すでに使用回数の上限に達し"
"ています。カート内のアイテムの価格を調整しました。"
#: pretix/base/services/orders.py:182
msgid ""
@@ -24181,7 +24181,7 @@ msgstr "高度な検索"
#: pretix/control/templates/pretixcontrol/orders/index.html:102
#, python-format
msgid "List filtered by answers to question \"%(question)s\"."
msgstr "質問 \"%(question)s\" への回答で絞り込まれたリスト"
msgstr "質問%(question)sへの回答で絞り込まれたリストです。"
#: pretix/control/templates/pretixcontrol/orders/index.html:107
msgid "Remove filter"
@@ -34229,8 +34229,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
"とができます。"
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
"できます。"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:514
#: pretix/presale/templates/pretixpresale/fragment_modals.html:48
@@ -34243,7 +34243,7 @@ msgstr "確保が更新されました"
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:532
msgid "Overview of your ordered products."
msgstr "注文した製品の概要"
msgstr "注文した製品の概要"
#: pretix/presale/templates/pretixpresale/event/fragment_cart_box.html:50
msgid "Continue with order process"

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
"Language: ja\n"
@@ -456,11 +456,11 @@ msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Product"
msgstr "品"
msgstr "品"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "Product variation"
msgstr "品バリエーション"
msgstr "品バリエーション"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
msgid "Gate"
@@ -726,8 +726,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
"とができます。"
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
"できます。"
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"

View File

@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
"PO-Revision-Date: 2026-02-12 00:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
@@ -7993,11 +7993,11 @@ msgid ""
"12345 Any City\n"
"Atlantis"
msgstr ""
"Piet Peeters\n"
"Piet Janssen\n"
"Voorbeeldbedrijf\n"
"Sesamstraat 42\n"
"12345 Ergens\n"
"Niemandsland"
"1234 AB Amsterdam\n"
"Nederland"
#: pretix/base/pdf.py:198
msgid "Attendee street"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
"PO-Revision-Date: 2026-02-12 00:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
@@ -8002,9 +8002,9 @@ msgid ""
"Atlantis"
msgstr ""
"Piet Janssen\n"
"Voorbeeld bedrijf\n"
"Voorbeeldbedrijf\n"
"Sesamstraat 42\n"
"1234AB Amsterdam\n"
"1234 AB Amsterdam\n"
"Nederland"
#: pretix/base/pdf.py:198
@@ -21786,9 +21786,9 @@ msgid ""
"copy all products, categories, quotas, and questions as well as general "
"event settings."
msgstr ""
"Wil je de instellingen van een ander evenement kopiëren? We zullen alle "
"producten, categorieën, quota en vragen overnemen, samen met de algemene "
"evenementsinstellingen."
"Wil je de configuratie van een ander evenement kopiëren? We kopiëren alle "
"producten, categorieën, quota en vragen, evenals de algemene "
"evenementinstellingen."
#: pretix/control/templates/pretixcontrol/events/create_copy.html:13
msgid ""
@@ -21796,25 +21796,25 @@ msgid ""
"need to change some settings manually, e.g. date and time settings and texts "
"that contain the event name."
msgstr ""
"Controleer alle instellingen! Je moet waarschijnlijk nog steeds wat "
"instellingen handmatig aanpassen, bijvoorbeeld datum- en tijdsinstellingen, "
"en teksten die de evenementsnaam bevatten."
"Controleer alle instellingen zorgvuldig. Waarschijnlijk moet je nog enkele "
"instellingen handmatig wijzigen, bijvoorbeeld de datum- en tijdinstellingen "
"en teksten die de naam van het evenement bevatten."
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:7
msgid "Event type"
msgstr "Evenementssoort"
msgstr "Type evenement"
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:13
msgid "Singular event or non-event shop"
msgstr "Enkel evenement of winkel die niet voor een evenement is"
msgstr "Eenmalig evenement of geen evenement"
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:15
msgid ""
"An event with individual configuration. If you create more events later, you "
"can copy the event to save yourself some work."
msgstr ""
"Een evenement met eigen instellingen. Als je later meer evenementen aanmaakt "
"kan je de instellingen van dit evenement kopiëren."
"Een evenement met eigen instellingen. Als je later meer evenementen "
"aanmaakt, kan je de instellingen van dit evenement kopiëren."
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:21
msgid ""
@@ -21834,8 +21834,8 @@ msgid ""
"A series of events that share the same configuration. They can still be "
"different in their dates, locations, prices, and capacities."
msgstr ""
"Een reeks evenementen die dezelfde instellingen delen. De evenementen in de "
"reeks kunnen verschillen in hun datums, prijzen en capaciteiten."
"Een reeks evenementen met dezelfde configuratie. Ze kunnen nog steeds "
"verschillen qua data, locaties, prijzen en capaciteiten."
#: pretix/control/templates/pretixcontrol/events/create_foundation.html:40
msgid ""
@@ -21861,9 +21861,8 @@ msgid ""
"The list below shows all events you have administrative access to. Click on "
"the event name to access event details."
msgstr ""
"De lijst hieronder toont alle evenementen waar je administratieve toegang "
"toe hebt. Klik op de evenementsnaam om de details van het evenement te "
"openen."
"De onderstaande lijst toont alle evenementen waar je beheerderstoegang toe "
"hebt. Klik op de naam van het evenement om de details ervan te bekijken."
#: pretix/control/templates/pretixcontrol/events/index.html:12
#: pretix/control/templates/pretixcontrol/organizers/detail.html:18
@@ -21923,7 +21922,7 @@ msgstr "Aantallen bijgewerkt op %(date)s"
#: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html:3
#, python-format
msgid "Currently available: %(num)s"
msgstr "Op dit moment beschikbaar: %(num)s"
msgstr "Nu beschikbaar: %(num)s"
#: pretix/control/templates/pretixcontrol/global_license.html:8
msgid ""
@@ -21973,7 +21972,7 @@ msgstr "Installatiegegevens"
#: pretix/control/templates/pretixcontrol/global_license.html:34
msgid "Installed plugins"
msgstr "Geïnstalleerde plugins"
msgstr "Geïnstalleerde plug-ins"
#: pretix/control/templates/pretixcontrol/global_license.html:40
msgid "Public information"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 13:19+0000\n"
"PO-Revision-Date: 2026-02-05 14:34+0000\n"
"PO-Revision-Date: 2026-02-14 06:10+0000\n"
"Last-Translator: Nate Horst <nate@agcthailand.org>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
"\n"
@@ -1000,7 +1000,7 @@ msgstr "โดเมนอีเมลที่ใช้สั่งซื้อ
#: pretix/plugins/reports/exporters.py:900
#: pretix/plugins/ticketoutputpdf/exporters.py:96
msgid "Order code"
msgstr "รหัสการสั่งซื้อ"
msgstr "รหัสการลงทะเบียน"
#: pretix/base/datasync/sourcefields.py:380
msgid "Event and order code"
@@ -7190,7 +7190,7 @@ msgstr "สินค้าที่ซื้อแล้ว"
#: pretix/base/services/placeholders.py:424
#: pretix/base/templates/pretixbase/email/order_details.html:151
msgid "View order details"
msgstr "ดูรายละเอียดคำสั่งซื้อ"
msgstr "ดูรายละเอียดการลงทะเบียน"
#: pretix/base/notifications.py:234
#, python-brace-format
@@ -11567,7 +11567,7 @@ msgstr ""
#: pretix/base/settings.py:2856 pretix/base/settings.py:2872
#, python-brace-format
msgid "Your ticket is ready for download: {code}"
msgstr ""
msgstr "ตั๋วของคุณพร้อมสำหรับการดาวน์โหลดแล้ว: {code}"
#: pretix/base/settings.py:2860
#, python-brace-format
@@ -12477,7 +12477,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/base.html:222
#: pretix/presale/templates/pretixpresale/organizers/base.html:100
msgid "Contact"
msgstr ""
msgstr "ติดต่อ"
#: pretix/base/templates/pretixbase/email/shred_completed.txt:2
#, python-format
@@ -12548,21 +12548,23 @@ msgstr ""
#: pretix/base/templates/source.html:5 pretix/base/templates/source.html:9
msgid "Source code"
msgstr ""
msgstr "ซอร์สโค้ด (Source code)"
#: pretix/base/templates/source.html:10
msgid ""
"This site is powered by free software. If you want to read the license terms "
"or obtain the source code, follow these links or instructions:"
msgstr ""
"เว็บไซต์นี้ขับเคลื่อนโดยซอฟต์แวร์เสรี หากคุณต้องการอ่านเงื่อนไขการอนุญาตใช้งานหรือรับซอร์สโค้ด โปร"
"ดไปที่ลิงก์หรือคำแนะนำต่อไปนี้:"
#: pretix/base/ticketoutput.py:182
msgid "Enable ticket format"
msgstr ""
msgstr "เปิดใช้งานรูปแบบตั๋ว"
#: pretix/base/ticketoutput.py:200
msgid "Download ticket"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว"
#: pretix/base/timeframes.py:49
msgctxt "reporting_timeframe"
@@ -19827,7 +19829,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:7
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:40
msgid "Ticket download"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว"
#: pretix/control/templates/pretixcontrol/event/tickets.html:11
msgid "Download settings"
@@ -21760,27 +21762,27 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html:410
msgid "Change products"
msgstr ""
msgstr "เปลี่ยนรายการสินค้า"
#: pretix/control/templates/pretixcontrol/order/index.html:415
#: pretix/presale/templates/pretixpresale/event/order.html:197
msgid "Ordered items"
msgstr ""
msgstr "รายการสินค้าที่สั่งซื้อ"
#: pretix/control/templates/pretixcontrol/order/index.html:434
#, python-format
msgid "Denied scan: %(date)s"
msgstr ""
msgstr "การสแกนถูกปฏิเสธ: %(date)s"
#: pretix/control/templates/pretixcontrol/order/index.html:439
#, python-format
msgid "Exit scan: %(date)s"
msgstr ""
msgstr "สแกนขาออก: %(date)s"
#: pretix/control/templates/pretixcontrol/order/index.html:446
#, python-format
msgid "Entry scan: %(date)s"
msgstr ""
msgstr "สแกนขาเข้า: %(date)s"
#: pretix/control/templates/pretixcontrol/order/index.html:465
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:55
@@ -21922,7 +21924,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:90
#: pretix/presale/templates/pretixpresale/event/order.html:318
msgid "ZIP code and city"
msgstr ""
msgstr "รหัสไปรษณีย์ และชื่อเมือง"
#: pretix/control/templates/pretixcontrol/order/index.html:1047
msgid "Valid EU VAT ID"
@@ -30726,46 +30728,46 @@ msgstr ""
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:65
msgid "Download tickets (PDF)"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว (PDF)"
#: pretix/plugins/ticketoutputpdf/ticketoutput.py:66
msgid "Download ticket (PDF)"
msgstr ""
msgstr "ดาวน์โหลดตั๋ว (PDF)"
#: pretix/plugins/ticketoutputpdf/views.py:62
msgid "Default ticket layout"
msgstr ""
msgstr "รูปแบบตั๋วมาตรฐาน"
#: pretix/plugins/ticketoutputpdf/views.py:119
msgid "The new ticket layout has been created."
msgstr ""
msgstr "สร้างรูปแบบตั๋วใหม่เรียบร้อยแล้ว"
#: pretix/plugins/ticketoutputpdf/views.py:168
#: pretix/plugins/ticketoutputpdf/views.py:198
#: pretix/plugins/ticketoutputpdf/views.py:246
msgid "The requested layout does not exist."
msgstr ""
msgstr "ไม่พบรูปแบบตั๋วที่ร้องขอ"
#: pretix/plugins/ticketoutputpdf/views.py:210
msgid "The selected ticket layout been deleted."
msgstr ""
msgstr "รูปแบบตั๋วที่เลือกถูกลบออกแล้ว"
#: pretix/plugins/ticketoutputpdf/views.py:250
#, python-brace-format
msgid "Ticket PDF layout: {}"
msgstr ""
msgstr "รูปแบบไฟล์ PDF ของตั๋ว: {}"
#: pretix/plugins/webcheckin/apps.py:30 pretix/plugins/webcheckin/apps.py:33
msgid "Web-based check-in"
msgstr ""
msgstr "การเช็คอินผ่านเว็บ"
#: pretix/plugins/webcheckin/apps.py:38
msgid "Turn your browser into a check-in device to perform access control."
msgstr ""
msgstr "เปลี่ยนเบราว์เซอร์ของคุณให้เป็นอุปกรณ์เช็คอินเพื่อควบคุมการเข้างาน"
#: pretix/plugins/webcheckin/apps.py:40 pretix/plugins/webcheckin/signals.py:36
msgid "Web Check-in"
msgstr ""
msgstr "เว็บเช็คอิน (Web Check-in)"
#: pretix/presale/checkoutflow.py:117
msgctxt "checkoutflow"
@@ -31188,7 +31190,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/base.html:242
#: pretix/presale/templates/pretixpresale/organizers/base.html:120
msgid "Imprint"
msgstr ""
msgstr "ข้อมูลทางกฎหมาย (Imprint)"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:12
msgid ""
@@ -32075,24 +32077,25 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:76
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:82
msgid "Please have your ticket ready when entering the event."
msgstr ""
msgstr "โปรดเตรียมตั๋วของคุณให้พร้อมเมื่อเข้าสู่งาน"
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:85
msgid "Download your tickets using the buttons below."
msgstr ""
msgstr "ดาวน์โหลดตั๋วของคุณโดยใช้ปุ่มด้านล่างนี้"
#: pretix/presale/templates/pretixpresale/event/fragment_downloads.html:94
#, python-format
msgid "You will be able to download your tickets here starting on %(date)s."
msgstr ""
"คุณจะสามารถดาวน์โหลดตั๋วของคุณได้ที่นี่ ตั้งแต่วันที่ %(date)s เป็นต้นไป"
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:7
msgid "Where does the event happen?"
msgstr ""
msgstr "กิจกรรมจัดขึ้นที่ไหน?"
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:17
msgid "When does the event happen?"
msgstr ""
msgstr "กิจกรรมจัดขึ้นเมื่อไหร่?"
#: pretix/presale/templates/pretixpresale/event/fragment_event_info.html:26
#, python-format
@@ -32491,7 +32494,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order.html:287
msgid "Your information"
msgstr ""
msgstr "ข้อมูลของคุณ"
#: pretix/presale/templates/pretixpresale/event/order.html:290
msgid "Change your information"

View File

@@ -179,6 +179,7 @@ def _default_context(request):
ctx['html_page_header'] = "".join(h for h in _html_page_header if h)
ctx['footer'] = _footer
ctx['site_url'] = settings.SITE_URL
ctx['request_get_items'] = request.GET.items()
ctx['js_datetime_format'] = get_javascript_format_without_seconds('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format_without_seconds('DATE_INPUT_FORMATS')

View File

@@ -15,7 +15,7 @@
</li>
<li class="text-center">
<form class="form-inline" method="get" id="monthselform" action="{% eventurl event "presale:event.index" cart_namespace=cart_namespace %}">
{% for f, v in request.GET.items %}
{% for f, v in request_get_items %}
{% if f != "date" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}

View File

@@ -15,7 +15,7 @@
</li>
<li class="text-center">
<form class="form-inline" method="get" id="monthselform" action="{% eventurl event "presale:event.index" cart_namespace=cart_namespace %}">
{% for f, v in request.GET.items %}
{% for f, v in request_get_items %}
{% if f != "date" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}

View File

@@ -1,14 +1,14 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Resend order links" %}{% endblock %}
{% block title %}{% trans "Resend order link" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
<meta name="robots" content="noindex, nofollow">
{% endblock %}
{% block content %}
<h2>
{% trans "Resend order links" %}
{% trans "Resend order link" %}
</h2>
<p>
{% blocktrans trimmed %}

View File

@@ -7,7 +7,7 @@
{% if filter_form.fields %}
<form class="event-list-filter-form" method="get" data-save-scrollpos>
<input type="hidden" name="filtered" value="1">
{% for f, v in request.GET.items %}
{% for f, v in request_get_items %}
{% if f not in filter_form.fields and f != "page" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}

View File

@@ -47,7 +47,7 @@
</li>
<li class="text-center">
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
{% for f, v in request.GET.items %}
{% for f, v in request_get_items %}
{% if f != "date" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}

View File

@@ -47,7 +47,7 @@
</li>
<li class="text-center">
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
{% for f, v in request.GET.items %}
{% for f, v in request_get_items %}
{% if f != "date" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}

View File

@@ -47,7 +47,7 @@
</li>
<li class="text-center">
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
{% for f, v in request.GET.items %}
{% for f, v in request_get_items %}
{% if f != "date" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}

View File

@@ -1510,7 +1510,10 @@ class OrderChangeMixin:
'max_count': iao.max_count,
'iao': iao,
'items': [i for i in items if not i.require_voucher],
'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
'items_missing': {
k: v for k, v in current_addon_products_missing.items()
if v and k[0].category_id == iao.addon_category_id
},
})
return positions

View File

@@ -252,7 +252,7 @@ Vue.component('availbox', {
variation: Object
},
mounted: function() {
if (this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
if (!this.$root.cart_exists && this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
this.$refs.quantity.value = 1;
if (this.order_max === 1) {
this.$refs.quantity.checked = true;

View File

@@ -32,21 +32,26 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import datetime
import os
import re
from decimal import Decimal
from email.mime.text import MIMEText
import pytest
from django.conf import settings
from django.core import mail as djmail
from django.test import override_settings
from django.utils.html import escape
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import scope
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.models import Event, Organizer, OutgoingMail, User
from pretix.base.models import (
Event, InvoiceAddress, Order, Organizer, OutgoingMail, User,
)
from pretix.base.services.mail import mail, mail_send_task
@@ -68,6 +73,45 @@ def env():
yield event, user, o
@pytest.fixture
@scopes_disabled()
def item(env):
return env[0].items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
@scopes_disabled()
def order(env, item):
event, _, _ = env
o = Order.objects.create(
code="FOO",
event=event,
email="dummy@dummy.test",
status=Order.STATUS_PENDING,
secret="k24fiuwvu8kxz3y1",
sales_channel=event.organizer.sales_channels.get(identifier="web"),
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
total=23,
locale="en",
)
o.positions.create(
order=o,
item=item,
variation=None,
price=Decimal("23"),
attendee_email="peter@example.org",
attendee_name_parts={"given_name": "Peter", "family_name": "Miller"},
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
pseudonymization_id="ABCDEFGHKL",
)
InvoiceAddress.objects.create(
order=o,
name_parts={"given_name": "Peter", "family_name": "Miller"},
)
return o
@pytest.mark.django_db
def test_send_mail_with_prefix(env):
djmail.outbox = []
@@ -279,7 +323,7 @@ def _extract_html(mail):
def test_placeholder_html_rendering_from_template(env):
djmail.outbox = []
event, user, organizer = env
event.name = "<strong>event & co. kg</strong>"
event.name = "<strong>event & co. kg</strong> {currency}"
event.save()
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
event=event,
@@ -288,25 +332,26 @@ def test_placeholder_html_rendering_from_template(env):
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
assert '**Meta**: *Beep*' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
assert '&lt;' not in djmail.outbox[0].body
assert '&amp;' not in djmail.outbox[0].body
# Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release
assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body
assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body
# todo: assert '&lt;' not in djmail.outbox[0].body
# todo: assert '&amp;' not in djmail.outbox[0].body
assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
assert 'EUR' not in djmail.outbox[0].body
html = _extract_html(djmail.outbox[0])
assert '<strong>event' not in html
assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt;' in html
assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}' in html
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
assert '<strong>Meta</strong>: <em>Beep</em>' in html
assert 'Unevaluated placeholder: {currency}' in html
assert 'EUR' not in html
assert re.search(
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
html
)
assert re.search(
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)
@@ -324,7 +369,7 @@ def test_placeholder_html_rendering_from_string(env):
})
djmail.outbox = []
event, user, organizer = env
event.name = "<strong>event & co. kg</strong>"
event.name = "<strong>event & co. kg</strong> {currency}"
event.save()
ctx = get_email_context(
event=event,
@@ -335,9 +380,9 @@ def test_placeholder_html_rendering_from_string(env):
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body
assert 'Other website: [<strong>event & co. kg</strong> {currency}](https://example.com)' in djmail.outbox[0].body
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
assert '**Meta**: *Beep*' in djmail.outbox[0].body
assert 'URL: https://google.com' in djmail.outbox[0].body
@@ -352,11 +397,13 @@ def test_placeholder_html_rendering_from_string(env):
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
assert '<strong>Meta</strong>: <em>Beep</em>' in html
assert re.search(
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)
assert re.search(
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">'
r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
html
)
assert re.search(
@@ -377,3 +424,141 @@ def test_placeholder_html_rendering_from_string(env):
r'style="[^"]+" target="_blank">Link &amp; Text</a>',
html
)
@pytest.mark.django_db
def test_nested_placeholder_inclusion_full_process(env, order):
# Test that it is not possible to sneak in a placeholder like {url_cancel} inside a user-controlled
# placeholder value like {invoice_company}
event, user, organizer = env
position = order.positions.get()
order.invoice_address.company = "{url_cancel} Corp"
order.invoice_address.save()
event.settings.mail_text_resend_link = LazyI18nString({"en": "Ticket for {invoice_company}"})
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": "Ticket for {invoice_company}"})
djmail.outbox = []
position.resend_link()
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [position.attendee_email]
assert "Ticket for {url_cancel} Corp" == djmail.outbox[0].subject
assert "/cancel" not in djmail.outbox[0].body
assert "/order" not in djmail.outbox[0].body
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain):
assert "Ticket for {url_cancel} Corp" in part
assert "/order/" not in part
assert "/cancel" not in part
@pytest.mark.django_db
def test_nested_placeholder_inclusion_mail_service(env):
# test that it is not possible to have placeholders within the values of placeholders when
# the mail() function is called directly
template = LazyI18nString("Event name: {event}")
djmail.outbox = []
event, user, organizer = env
event.name = "event & {currency} co. kg"
event.slug = "event-co-ag-slug"
event.save()
mail(
"dummy@dummy.dummy",
"{event} Test subject",
template,
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
),
event,
)
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "event & {currency} co. kg" in part or "event &amp; {currency} co. kg" in part
assert "EUR" not in part
@pytest.mark.django_db
@pytest.mark.parametrize("tpl", [
"Event: {event.__class__}",
"Event: {{event.__class__}}",
"Event: {{{event.__class__}}}",
])
def test_variable_inclusion_from_string_full_process(env, tpl, order):
# Test that it is not possible to use placeholders that leak system information in templates
# when run through system processes
event, user, organizer = env
event.name = "event & co. kg"
event.save()
position = order.positions.get()
event.settings.mail_text_resend_link = LazyI18nString({"en": tpl})
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": tpl})
position.resend_link()
assert len(djmail.outbox) == 1
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "{event.__class__}" in part
assert "LazyI18nString" not in part
@pytest.mark.django_db
@pytest.mark.parametrize("tpl", [
"Event: {event.__class__}",
"Event: {{event.__class__}}",
"Event: {{{event.__class__}}}",
])
def test_variable_inclusion_from_string_mail_service(env, tpl):
# Test that it is not possible to use placeholders that leak system information in templates
# when run through mail() directly
event, user, organizer = env
event.name = "event & co. kg"
event.save()
djmail.outbox = []
mail(
"dummy@dummy.dummy",
tpl,
LazyI18nString(tpl),
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456\n" + tpl,
),
event,
)
assert len(djmail.outbox) == 1
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "{event.__class__}" in part
assert "LazyI18nString" not in part
@pytest.mark.django_db
def test_escaped_braces_mail_services(env):
# Test that braces can be escaped by doubling
template = LazyI18nString("Event name: -{{currency}}-")
djmail.outbox = []
event, user, organizer = env
event.name = "event & co. kg"
event.save()
mail(
"dummy@dummy.dummy",
"-{{currency}}- Test subject",
template,
get_email_context(
event=event,
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
),
event,
)
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == [user.email]
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
for part in (html, plain, djmail.outbox[0].subject):
assert "EUR" not in part
assert "-{currency}-" in part

View File

@@ -29,6 +29,8 @@ def test_format_map():
assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}"
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {bar!r}", {"bar": '3'}) == "Foo 3"
assert format_map("Foo {bar!a}", {"bar": '3'}) == "Foo 3"
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"

View File

@@ -1,13 +1,13 @@
{% load i18n %}
This is a test file for sending mails.
Event name: {event}
Event name: {{ event }}
Unevaluated placeholder: {currency}
{% get_current_language as LANGUAGE_CODE %}
The language code used for rendering this email is {{ LANGUAGE_CODE }}.
Payment info:
{payment_info}
{{ payment_info }}
**Meta**: {meta_Test}
**Meta**: {{ meta_Test }}
Event website: [{event}](https://example.org/{event_slug})
Other website: [{event}]({meta_Website})
Event website: [{{event}}](https://example.org/{{event_slug}})