Compare commits

..
Author SHA1 Message Date
Mira Weller e86500e9fc Fix templates 2026-07-09 18:09:47 +02:00
Mira Weller a72e32b82d Use JSON encoding for data-replace-with-qr 2026-07-09 18:09:41 +02:00
Mira Weller 2e2fe874e1 Remove more usages of |safe 2026-07-09 18:08:06 +02:00
Mira Weller ce8c4c649d Replace |safe with |escapejson where appropriate 2026-07-09 18:08:06 +02:00
31 changed files with 61 additions and 132 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ dependencies = [
"django-countries==8.2.*",
"django-filter==25.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.7",
"django-formtools==2.6.1",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hijack==3.7.*",
"django-i18nfield==1.11.*",
-21
View File
@@ -266,22 +266,6 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin):
SAFE_TYPES = (
# CSP policies are only used for:
# - HTML and SVG in top-level contexts
# - SVG or JS Workers delivered in embedded contexts
# See: https://www.w3.org/TR/CSP2/#which-policy-applies
# Therefore, we can save bandwidth on not including our (sometimes huge) policy
# on API responses or CSS. We do however include it with other types as a precaution
# (whitelist instead of blacklist) and we also do not whitelist JavaScript in
# we ever add service workers to not break the protection of this feature:
# https://www.w3.org/TR/CSP2/#sandboxing-and-workers
'application/json',
'text/css',
# We used to skip CSP for PDF since it was necessary for inline previews in Safari,
# but at the moment it does not seem to be an issue to just send it.
)
def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
@@ -293,11 +277,6 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
if 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
+2 -54
View File
@@ -936,7 +936,7 @@ class BasePaymentProvider:
"""
Will be called if the *event administrator* views the details of a payment.
It should return HTML code containing information regarding the current payment
It should return a SafeString containing HTML code, with information regarding the current payment
status and, if applicable, next steps.
The default implementation returns an empty string.
@@ -961,7 +961,7 @@ class BasePaymentProvider:
"""
Will be called if the *event administrator* views the details of a refund.
It should return HTML code containing information regarding the current refund
It should return a SafeString containing HTML code, with information regarding the current refund
status and, if applicable, next steps.
The default implementation returns an empty string.
@@ -1706,58 +1706,6 @@ class GiftCardPayment(BasePaymentProvider):
)
class BaseHistoricalPaymentProvider(BasePaymentProvider):
"""
Base class for payment providers that no longer exist but can't be deleted to make sure historical
payments are shown correctly.
Subclasses are recommended to only implement:
- identifier
- verbose_name
- public_name
- payment_control_render
- payment_control_render_short
- refund_control_render
- refund_control_render_short
- render_invoice_text
- render_invoice_stamp
- api_payment_details
- api_refund_details
- shred_payment_info
- matching_id
- refund_matching_id
"""
@property
def is_enabled(self) -> bool:
return False
@property
def settings_form_fields(self) -> dict:
return {}
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return False
def payment_is_valid_session(self, request: HttpRequest, payment: OrderPayment):
return False
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
return False
def payment_refund_supported(self, payment: OrderPayment) -> bool:
return False
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
return False
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
def execute_refund(self, refund: OrderRefund):
raise PaymentException(_("This payment provider exists for historical purposes only and is no longer usable."))
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return [FreeOrderProvider, BoxOfficeProvider, OffsettingProvider, ManualPayment, GiftCardPayment]
+1 -1
View File
@@ -59,7 +59,7 @@ def on_task_prerun(sender, task_id, task, **kwargs):
from pretix.helpers.logs import local
local.request_id = task_id
if task.request.headers and "X-Pretix-Trace" in task.request.headers:
if "X-Pretix-Trace" in task.request.headers:
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
else:
local.trace = []
@@ -3,6 +3,7 @@
{% load i18n %}
{% load static %}
{% load compress %}
{% load escapejson %}
{% block content %}
<form class="form-signin" action="" method="post" id="webauthn-form">
{% csrf_token %}
@@ -30,8 +31,7 @@
</form>
{% if jsondata %}
<script type="text/json" id="webauthn-login">
{{ jsondata|safe }}
{{ jsondata|escapejson }}
</script>
{% endif %}
{% compress js %}
@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% load wrap_in %}
{% block title %}
{% trans "Cancel order" %}
{% endblock %}
@@ -26,7 +27,7 @@
{% if form.cancellation_fee %}
{% if fee %}
{% with fee|money:request.event.currency as f %}
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
order, but for a cancellation performed by you, you need to set the cancellation fee here:
{% endblocktrans %}</p>
@@ -903,7 +903,7 @@
<tr>
<td colspan="1"></td>
<td colspan="5">
{{ p.html_info|safe }}
{{ p.html_info }}
{% if staff_session %}
<p>
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
@@ -1018,7 +1018,7 @@
</dl>
{% endif %}
{% if r.html_info %}
{{ r.html_info|safe }}
{{ r.html_info }}
{% endif %}
{% if staff_session %}
<p>
@@ -2,6 +2,7 @@
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% load escapejson %}
{% block inner %}
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
@@ -18,7 +19,7 @@
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
</li>
<li>{% trans "Scan the following configuration code:" %}<br><br>
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
<br>
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
@@ -560,10 +560,11 @@
</div>
</div>
</div>
<script type="text/plain" id="schema-url">{% static "schema/pdf-layout.schema.json" %}</script>
<script type="text/javascript" src="{% static "pdfjs/pdf.js" %}"></script>
<script type="text/javascript" src="{% static "ajv/ajv2020.bundle.min.js" %}"></script>
<script type="text/javascript" src="{% static "fabric/fabric.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/editor.js" %}"></script>
<script type="text/javascript" src="{% static "schema/pdf-layout.validate.js" %}"></script>
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_dark.png' %}" id="poweredby-dark" class="sr-only">
<img src="{% static 'pretixpresale/pdf/powered_by_pretix_white.png' %}" id="poweredby-white" class="sr-only">
{% for family, styles in fonts.items %}
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load escapejson %}
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
{% block content %}
<h1>{% trans "Add a two-factor authentication device" %}</h1>
@@ -32,7 +33,7 @@
</li>
<li>
{% trans "Add a new account to the app by scanning the following barcode:" %}
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
<p>
<a data-toggle="collapse" href="#no_scan">
{% trans "Can't scan the barcode?" %}
@@ -81,9 +82,4 @@
</li>
</ol>
<script type="text/json" id="qrdata">
{{ qrdata|safe }}
</script>
{% endblock %}
@@ -3,6 +3,7 @@
{% load bootstrap3 %}
{% load static %}
{% load compress %}
{% load escapejson %}
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
{% block content %}
<h1>{% trans "Add a two-factor authentication device" %}</h1>
@@ -26,9 +27,7 @@
{% trans "Device registration failed." %}
</div>
<script type="text/json" id="webauthn-enroll">
{{ jsondata|safe }}
{{ jsondata|escapejson }}
</script>
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
@@ -3,6 +3,7 @@
{% load bootstrap3 %}
{% load compress %}
{% load static %}
{% load escapejson %}
{% block content %}
<form class="form-signin" id="webauthn-form" action="" method="post">
{% csrf_token %}
@@ -43,7 +44,7 @@
{% if jsondata %}
<script type="text/json" id="webauthn-login">
{{ jsondata|safe }}
{{ jsondata|escapejson }}
</script>
{% endif %}
{% compress js %}
+1 -2
View File
@@ -243,8 +243,7 @@ def invite(request, token):
if request.user.is_authenticated:
if inv.team.members.filter(pk=request.user.pk).exists():
messages.error(request, _('You cannot accept the invitation for "{}" as you already are part of '
'this team. If you want to add a different user or create a new account, '
'log out and click the invitation link again.').format(inv.team.name))
'this team.').format(inv.team.name))
return redirect('control:index')
else:
with transaction.atomic():
+1 -6
View File
@@ -952,12 +952,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
context=context,
)
r = HttpResponse(v, content_type='text/html')
r['Content-Security-Policy'] = (
# Plugin-provided email templates will contain inline styles or remote images
# but emails should not contain JS
"style-src 'unsafe-inline'; "
"img-src https: data:"
)
r._csp_ignore = True
return r
else:
raise Http404(_('Unknown email renderer.'))
+2 -2
View File
@@ -551,10 +551,10 @@ class OrderDetail(OrderView):
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
for p in ctx['payments']:
if p.payment_provider:
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
for r in ctx['refunds']:
if r.payment_provider:
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
ctx['comment_form'] = CommentForm(initial={
'comment': self.order.comment,
+1
View File
@@ -70,6 +70,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
if 'placeholders' in request.GET:
return self.get_placeholders_help(request)
resp = super().get(request, *args, **kwargs)
resp._csp_ignore = True
return resp
def get_placeholders_help(self, request):
+1 -1
View File
@@ -47,5 +47,5 @@ def escapejson(value):
@keep_lazy(str, SafeText)
def escapejson_attr(value):
"""Hex encodes characters for use in a html attributw script."""
"""Hex encodes characters for use in a html attribute."""
return mark_safe(force_str(value).translate(_json_escapes_attr))
+1
View File
@@ -162,6 +162,7 @@ class XHRView(View):
paypal_order = prov._create_paypal_order(request, None, cart_total)
r = JsonResponse(paypal_order.dict() if paypal_order else {})
r._csp_ignore = True
return r
@@ -323,7 +323,7 @@ $(function () {
}
}
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
waitingDialog.hide();
@@ -432,4 +432,4 @@ $(function () {
}
}
);
});
});
@@ -9,7 +9,7 @@
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
{% if payment_intent_next_action_redirect_url %}
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script>
{{ payment_intent_next_action_redirect_url|json_script:"stripe_payment_intent_next_action_redirect_url" }}
{% endif %}
{% if payment_intent_redirect_action_handling %}
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
+1
View File
@@ -48,6 +48,7 @@ def theme_css(request, **kwargs):
obj = getattr(request, "event", request.organizer)
css = get_theme_vars_css(obj, widget=False)
resp = HttpResponse(css, content_type="text/css")
resp._csp_ignore = True
resp["Access-Control-Allow-Origin"] = "*"
if "version" in request.GET:
resp["Expires"] = http_date(time.time() + 3600 * 24 * 30)
+4
View File
@@ -164,6 +164,7 @@ def widget_css(request, version, **kwargs):
css = f"/* v{version} */\n" + theme_css + widget_css
resp = FileResponse(css, content_type='text/css')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -245,6 +246,7 @@ def widget_js(request, version, lang, **kwargs):
cached_js = cache.get(cache_prefix)
if cached_js and not settings.DEBUG:
resp = HttpResponse(cached_js, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -276,6 +278,7 @@ def widget_js(request, version, lang, **kwargs):
gs.settings.set(checksum_key, checksum)
cache.set(cache_prefix, data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'
return resp
@@ -411,6 +414,7 @@ class WidgetAPIProductList(EventListMixin, View):
self.post_process(data)
resp = JsonResponse(data)
resp['Access-Control-Allow-Origin'] = '*'
resp._csp_ignore = True
return resp
def get(self, request, *args, **kwargs):
File diff suppressed because one or more lines are too long
@@ -1,7 +1,4 @@
/*globals $, gettext, fabric, PDFJS*/
window.module = {} // Hack to load ajv-compiled schema without actual module loading
fabric.Poweredby = fabric.util.createClass(fabric.Image, {
type: 'poweredby',
@@ -222,6 +219,7 @@ var editor = {
uploaded_file_id: null,
_window_loaded: false,
_fabric_loaded: false,
schema: null,
_px2mm: function (v) {
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
@@ -1322,11 +1320,14 @@ var editor = {
_source_save: function () {
try {
var Ajv = window.ajv2020
var ajv = new Ajv()
var validate = ajv.compile(editor.schema)
var data = JSON.parse($("#source-textarea").val())
var valid = validate30(data)
var valid = validate(data)
if (!valid) {
console.log(validate30.errors)
console.log(validate.errors)
alert("Invalid input syntax. If you're familiar with this, check out the developer console for a full " +
"error log. Otherwise, please contact support.")
} else {
@@ -1462,6 +1463,10 @@ var editor = {
editor._update_save_button();
});
$("#pdf-info-width, #pdf-info-height").bind('change input', editor._paper_size_warning);
$.getJSON($("#schema-url").text(), function (data) {
editor.schema = data;
})
}
};
@@ -874,14 +874,6 @@ function setup_basics(el) {
});
});
el.find(".qrcode-canvas").each(function () {
$(this).qrcode(
{
text: $.trim($($(this).attr("data-qrdata")).html())
}
);
});
el.find(".propagated-settings-box").find("input, textarea, select").not("[readonly]")
.attr("data-propagated-locked", "true").prop("readonly", true);
@@ -1,6 +1,5 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$comment": "If the schema is changed, regenerate pdf-layout.validate.js with $ ajv compile -s ./pretix/static/schema/pdf-layout.schema.json --spec=draft2020 -o ./pretix/static/schema/pdf-layout.validate.js",
"title": "Ticket Layout",
"description": "Dynamic elements for a PDF layout",
"type": "array",
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -258,7 +258,7 @@ def test_list_list(token_client, organizer, event, clist, item, subevent, django
res["id"] = clist.pk
res["limit_products"] = [item.pk]
with django_assert_num_queries(9):
with django_assert_num_queries(11):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@@ -437,7 +437,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
p3["addon_to"] = p1["id"]
# All items
with django_assert_num_queries(22):
with django_assert_num_queries(24):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
organizer.slug, event.slug, clist_all.pk
))
+4 -4
View File
@@ -1865,7 +1865,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert resp.status_code == 200
event.refresh_from_db()
with assert_num_queries(10):
with assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1875,7 +1875,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
with scope(organizer=organizer):
v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0'))
with assert_num_queries(12):
with assert_num_queries(14):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
@@ -1883,7 +1883,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
assert len(resp.data['results']) == 1
assert resp.data['results'][0]['voucher']['id'] == v0.pk
with assert_num_queries(10):
with assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=true'
.format(organizer.slug, event.slug))
@@ -1894,7 +1894,7 @@ def test_event_expand_seat_filter_and_querycount(token_client, organizer, event,
v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1'))
v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2'))
with assert_num_queries(14):
with assert_num_queries(16):
resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/'
'?expand=orderposition&expand=cartposition&expand=voucher&is_available=false'
.format(organizer.slug, event.slug))
+1 -1
View File
@@ -425,7 +425,7 @@ def test_item_list(token_client, organizer, event, team, item):
@pytest.mark.django_db
def test_item_list_queries(token_client, organizer, event, team, item, item3):
with assert_num_queries(16):
with assert_num_queries(18):
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
+2 -2
View File
@@ -1166,10 +1166,10 @@ def test_orderposition_list(
'type': 'entry'
}]
if '/events/' in endpoint:
with django_assert_num_queries(16):
with django_assert_num_queries(18):
resp = token_client.get(endpoint + '?has_checkin=true')
else:
with django_assert_num_queries(15):
with django_assert_num_queries(17):
resp = token_client.get(endpoint + '?has_checkin=true')
assert [res] == resp.data['results']