Compare commits

...
Author SHA1 Message Date
Raphael Michel a5a163e4ea Log view: Disable counting the number of pages (Z#23241035) 2026-07-23 19:21:29 +02:00
ca03a2556d Log view: New filter form (#6412)
* Rework filter form for event log view

* Log view: New filter form

* isort

* Update src/pretix/control/forms/filter.py

Co-authored-by: luelista <weller@rami.io>

---------

Co-authored-by: luelista <weller@rami.io>
2026-07-23 17:35:18 +02:00
Richard SchreiberandGitHub 2381af4267 Widget: re-create vue2-behaviour to replace container instead of setting innerHTML (#6405)
* Widget: replace container instead of setting innerHTML

* remove console.log call

* fix code-style
2026-07-23 11:36:06 +02:00
Raphael Michel 9fd570a53d Add cautionary comment about vobject bug 2026-07-22 14:49:53 +02:00
Kara Engelhardtandpajowu 072c17c91a Banktransfer: Notify incomplete payments after manual assignment (Z#23241009) 2026-07-22 14:15:14 +02:00
Raphael MichelandGitHub 003bfb707d Fix scoping for OrderPosition.objects (follow-up to #4278) (#6407) 2026-07-22 14:10:34 +02:00
Raphael MichelandRaphael Michel faf8a6b1d2 Translations: Update Arabic
Currently translated at 99.7% (6348 of 6362 strings)

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

powered by weblate
2026-07-22 14:09:38 +02:00
Kara Engelhardtandpajowu a4d10d9550 Sendmail: clarify order status labels (Z#23230149) 2026-07-22 13:43:07 +02:00
Richard Schreiberandpajowu baf3cf04ec fix import and missing license-header 2026-07-21 18:39:07 +02:00
Kara Engelhardtandpajowu fa2009ed03 Add central helper for ssrf address protection 2026-07-21 18:39:07 +02:00
luelistaandGitHub cc082e2001 Improve CSP handling in vite integration; refactor CSP handling into helper functions (Z#23240534) (#6387) 2026-07-21 13:20:31 +02:00
Raphael Michel 930e153db4 Security profiles: Add missing annul checkpoint 2026-07-21 12:30:37 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
560a369e65 Update django-countries requirement from ==8.2.* to ==9.0.* (#6316)
Updates the requirements on [django-countries](https://github.com/SmileyChris/django-countries) to permit the latest version.
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.md)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v8.2.0...v9.0.0)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-version: 9.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 08:53:00 +02:00
24 changed files with 339 additions and 185 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ dependencies = [
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
"django-countries==9.0.*",
"django-filter==26.1",
"django-formset-js-improved==0.5.0.5",
"django-formtools==2.7",
+3
View File
@@ -108,6 +108,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'),
('GET', 'api-v1:checkinrpc.search'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-lookup'),
@@ -146,6 +147,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'),
('GET', 'api-v1:checkinrpc.search'),
)
@@ -182,6 +184,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
('GET', 'api-v1:event.settings'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('POST', 'api-v1:checkinrpc.annull'),
('GET', 'api-v1:checkinrpc.search'),
)
+4 -13
View File
@@ -19,7 +19,6 @@
# 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 ipaddress
import logging
import smtplib
import socket
@@ -43,6 +42,7 @@ from pretix.base.templatetags.rich_text import (
markdown_compile_email, truelink_callback,
)
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
from pretix.helpers.ssrf import should_block_access
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -57,8 +57,6 @@ logger = logging.getLogger('pretix.base.email')
T = TypeVar("T", bound=EmailBackend)
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
try:
@@ -254,16 +252,9 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
af, socktype, proto, canonname, sa = res
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked")
if check_ip4 in _cgnat_net:
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
is_private, msg = should_block_access(sa)
if is_private:
raise socket.error(msg)
sock = None
try:
+40 -14
View File
@@ -19,6 +19,8 @@
# 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 base64
import hashlib
import logging
import re
from collections import OrderedDict
@@ -312,7 +314,28 @@ def _merge_csp(a, b):
for k, v in a.items():
if "'unsafe-inline'" in v:
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha256-")]
def add_to_response_csp(response, csp_to_merge):
if "Content-Security-Policy" in response:
csp = _parse_csp(response["Content-Security-Policy"])
else:
csp = {}
_merge_csp(csp, csp_to_merge)
if csp:
response["Content-Security-Policy"] = _render_csp(csp)
def add_to_response_csp_via_request(request, csp_to_merge):
_merge_csp(request._csp_to_merge, csp_to_merge)
def calculate_csp_hash(data):
hash_str = base64.b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode("ascii")
return f"'sha256-{hash_str}'"
class SecurityMiddleware(MiddlewareMixin):
@@ -332,6 +355,9 @@ class SecurityMiddleware(MiddlewareMixin):
# but at the moment it does not seem to be an issue to just send it.
)
def process_request(self, request):
request._csp_to_merge = {}
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
@@ -343,18 +369,22 @@ 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):
if self._needs_csp(request, resp):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
def _needs_csp(self, request, resp):
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
return False
if getattr(resp, '_csp_ignore', False):
return False
return True
def _build_csp(self, request, resp):
url = resolve(request.path_info)
@@ -387,13 +417,6 @@ class SecurityMiddleware(MiddlewareMixin):
h['style-src'] += ["'unsafe-inline'"]
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
if hasattr(request, 'csp_nonce'):
nonce = f"'nonce-{request.csp_nonce}'"
h['script-src'].append(nonce)
if not settings.VITE_DEV_MODE:
# can't have 'unsafe-inline' and nonce at the same time
h['style-src'].append(nonce)
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
@@ -406,6 +429,9 @@ class SecurityMiddleware(MiddlewareMixin):
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if request._csp_to_merge:
_merge_csp(h, request._csp_to_merge)
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
+3
View File
@@ -238,6 +238,9 @@ class EventMixin:
@property
def timezone(self):
# If we get rid of the shim, verify that
# https://github.com/py-vobject/vobject/issues/117#issuecomment-5045645314
# has been released and included
return pytz_deprecation_shim.timezone(self.settings.timezone)
@property
+8 -5
View File
@@ -2286,9 +2286,12 @@ class OrderRefund(models.Model):
super().save(*args, **kwargs)
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
def get_queryset(self):
return super().get_queryset().filter(canceled=False)
def ActivePositionManager(**scope):
class InnerClass(ScopedManager(**scope).__class__):
def get_queryset(self):
return super().get_queryset().filter(canceled=False)
return InnerClass()
class OrderFee(RoundingCorrectionMixin, models.Model):
@@ -2378,7 +2381,7 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
canceled = models.BooleanField(default=False)
all = ScopedManager(organizer='order__event__organizer')
objects = ActivePositionManager()
objects = ActivePositionManager(organizer='order__event__organizer')
@property
def net_value(self):
@@ -2597,7 +2600,7 @@ class OrderPosition(AbstractPosition):
)
all = ScopedManager(organizer='organizer')
objects = ActivePositionManager()
objects = ActivePositionManager(organizer='organizer')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+14 -6
View File
@@ -19,12 +19,10 @@
# 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 json
import logging
import pathlib
import re
import secrets
from urllib.parse import urljoin
from urllib.request import urlopen
@@ -33,6 +31,10 @@ from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
from pretix.base.middleware import (
add_to_response_csp_via_request, calculate_csp_hash,
)
register = template.Library()
LOGGER = logging.getLogger(__name__)
_MANIFEST = {}
@@ -234,10 +236,16 @@ def vite_importmap(context):
if not imports:
return ""
# Generate a nonce and store it on the request so the CSP middleware can allow it
nonce = secrets.token_urlsafe(16)
json_string = json.dumps({"imports": imports})
# Calculate hash and add it to the CSP info in the request, so that will be merged into the
# response header later on by the middleware
request = context.get('request')
if request:
request.csp_nonce = nonce
csp_hash = calculate_csp_hash(json_string)
add_to_response_csp_via_request(request, {
'style-src': [csp_hash],
'script-src': [csp_hash],
})
return f'<script type="importmap" nonce="{nonce}">{json.dumps({"imports": imports})}</script>'
return f'<script type="importmap">{json_string}</script>'
+90 -1
View File
@@ -39,6 +39,7 @@ from urllib.parse import urlencode
from django import forms
from django.apps import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import (
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
)
@@ -59,7 +60,7 @@ from pretix.base.models import (
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
Voucher,
User, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.base.timeframes import (
@@ -3007,3 +3008,91 @@ class OutgoingMailFilterForm(FilterForm):
qs = qs.order_by("-created", "-pk")
return qs
class LogFilterForm(FilterForm):
source = forms.ChoiceField(
label=_('Source'),
choices=[
('', _('All sources')),
('team', _('Team actions')),
('customer', _('Customer actions')),
('device', _('Device actions')),
],
required=False
)
device = SafeModelChoiceField(
label=_('Device'),
empty_label=_('All devices'),
queryset=Device.objects.none(),
required=False
)
user_email = forms.EmailField(
label=_('User email'),
widget=forms.EmailInput(
attrs={"placeholder": _('All users')}
),
required=False
)
action_type = forms.CharField(
widget=forms.HiddenInput,
required=False,
)
content_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
widget=forms.HiddenInput,
required=False,
)
object = forms.IntegerField(
widget=forms.HiddenInput,
required=False
)
def __init__(self, *args, **kwargs):
self.organizer = kwargs.pop('organizer')
super().__init__(*args, **kwargs)
self.fields['device'].queryset = self.organizer.devices.all().order_by('device_id')
self.fields['device'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.devices.select2', kwargs={
'organizer': self.organizer.slug,
}),
'data-placeholder': _('All devices'),
}
)
self.fields['device'].widget.choices = self.fields['device'].choices
self.fields['device'].label = _('Device')
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('source') == 'team':
qs = qs.filter(user__isnull=False)
elif fdata.get('source') == 'device':
qs = qs.filter(device__isnull=False)
elif fdata.get('source') == 'customer':
qs = qs.filter(user__isnull=True, device__isnull=True)
if fdata.get('device'):
qs = qs.filter(device_id=fdata['device'].pk)
if fdata.get('action_type'):
qs = qs.filter(action_type__in=fdata['action_type'].split(','))
if fdata.get('user_email'):
try:
user = User.objects.get(email=fdata['user_email'].lower())
qs = qs.filter(user=user)
except User.DoesNotExist:
# Do not actively leak info that this user does not exist. Yes, this will likely have a
# timing attack possibility, but with the magic that database query planners do,
# it's probably impossible to avoid that.
qs = qs.none()
if fdata.get('content_type'):
qs = qs.filter(content_type=fdata.get('content_type'))
if fdata.get('object'):
qs = qs.filter(object_id=fdata.get('object'))
return qs
@@ -1,41 +1,12 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "Event logs" %}{% endblock %}
{% block inside %}
<h1>{% trans "Event logs" %}</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
<input type="hidden" name="object" value="{{ request.GET.object }}">
<p>
<select name="user" class="form-control">
<option value="">{% trans "All actions" %}</option>
<option value="yes" {% if request.GET.user == "yes" %}selected="selected"{% endif %}>
{% trans "Team actions" %}
</option>
<option value="no" {% if request.GET.user == "no" %}selected="selected"{% endif %}>
{% trans "Customer actions" %}
</option>
{% for up in userlist %}
{% if up.user__id %}
<option value="{{ up.user__id }}"
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
{{ up.user__email }}
</option>
{% endif %}
{% endfor %}
{% for d in devicelist %}
{% if d.device__id %}
<option value="d-{{ d.device__id }}"
{% if "d-" in request.GET.user and request.GET.user|slice:"2:" == d.device__id|slugify %}selected="selected"{% endif %}>
{{ d.device__name }}
</option>
{% endif %}
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
{% include "pretixcontrol/fragment_log_filter_form.html" %}
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item logentry">
@@ -95,5 +66,5 @@
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% include "pretixcontrol/pagination_huge.html" %}
{% endblock %}
@@ -0,0 +1,38 @@
{% load i18n %}
{% load bootstrap3 %}
{% load icon %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Filter" %}
</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-3 col-xs-6">
{% bootstrap_field filter_form.source %}
</div>
<div class="col-md-3 col-xs-6">
{% bootstrap_field filter_form.device %}
</div>
<div class="col-md-3 col-xs-6">
{% bootstrap_field filter_form.user_email %}
</div>
</div>
{{ filter_form.action_type }}
{{ filter_form.content_type }}
{{ filter_form.object }}
{% if filter_form.is_valid and filter_form.cleaned_data.action_type %}
{% icon "filter" %} <code>{{ filter_form.cleaned_data.action_type }}</code>
{% endif %}
{% if filter_form.is_valid and filter_form.cleaned_data.content_type and filter_form.cleaned_data.object %}
{% icon "filter" %} {% trans "Specific object selected" %}
{% endif %}
<div class="text-right flip">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
@@ -4,24 +4,7 @@
{% block title %}{% trans "Organizer logs" %}{% endblock %}
{% block inside %}
<h1>{% trans "Organizer logs" %}</h1>
<form class="form-inline helper-display-inline" action="" method="get">
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
<input type="hidden" name="object" value="{{ request.GET.object }}">
<p>
<select name="user" class="form-control">
<option value="">{% trans "All actions" %}</option>
{% for up in userlist %}
{% if up.user__id %}
<option value="{{ up.user__id }}"
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
{{ up.user__email }}
</option>
{% endif %}
{% endfor %}
</select>
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
</p>
</form>
{% include "pretixcontrol/fragment_log_filter_form.html" %}
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item logentry">
@@ -81,5 +64,5 @@
</div>
{% endfor %}
</ul>
{% include "pretixcontrol/pagination.html" %}
{% include "pretixcontrol/pagination_huge.html" %}
{% endblock %}
+11 -21
View File
@@ -60,7 +60,7 @@ from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect
from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property
from django.utils.html import conditional_escape, format_html
@@ -119,8 +119,9 @@ from ...helpers.compat import CompatDeleteView
from ...helpers.format import (
PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from ..forms.filter import LogFilterForm
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
from . import CreateView, LargeResultSetPaginator, PaginationMixin, UpdateView
logger = logging.getLogger(__name__)
@@ -1253,6 +1254,7 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/event/logs.html'
model = LogEntry
paginator_class = LargeResultSetPaginator
context_object_name = 'logs'
def get_queryset(self):
@@ -1282,32 +1284,20 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
]
qs = qs.filter(content_type__in=allowed_types)
if self.request.GET.get('user') == 'yes':
qs = qs.filter(user__isnull=False)
elif self.request.GET.get('user') == 'no':
qs = qs.filter(user__isnull=True)
elif self.request.GET.get('user', '').startswith('d-'):
qs = qs.filter(device_id=self.request.GET.get('user')[2:])
elif self.request.GET.get('user'):
qs = qs.filter(user_id=self.request.GET.get('user'))
if self.request.GET.get('action_type'):
qs = qs.filter(action_type=self.request.GET['action_type'])
if self.request.GET.get('content_type'):
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
if self.request.GET.get('object'):
qs = qs.filter(object_id=self.request.GET.get('object'))
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
ctx['devicelist'] = self.request.event.logentry_set.order_by('device__name').distinct().values('device__id', 'device__name')
ctx['filter_form'] = self.filter_form
return ctx
@cached_property
def filter_form(self):
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
class EventComment(EventPermissionRequiredMixin, View):
permission = 'event.settings.general:write'
+3 -9
View File
@@ -35,7 +35,7 @@ from django.utils.translation import ngettext
from django.views import View
from django.views.generic import DetailView, ListView
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
from pretix.base.middleware import add_to_response_csp
from pretix.base.models import OutgoingMail
from pretix.base.services.mail import mail_send_task
from pretix.control.forms.filter import OutgoingMailFilterForm
@@ -108,18 +108,12 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if 'Content-Security-Policy' in response:
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
csps = {
add_to_response_csp(response, {
'frame-src': ['data:'],
# Unfortuantely, we can't avoid unsafe-inline for style here.
# See outgoingmail.js for the protection measures we take.
'style-src': ["'unsafe-inline'"],
}
_merge_csp(h, csps)
response['Content-Security-Policy'] = _render_csp(h)
})
return response
def get_context_data(self, **kwargs):
+13 -6
View File
@@ -113,7 +113,8 @@ from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.exports import ScheduledOrganizerExportForm
from pretix.control.forms.filter import (
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
OrganizerFilterForm, ReusableMediaFilterForm, TeamFilterForm,
LogFilterForm, OrganizerFilterForm, ReusableMediaFilterForm,
TeamFilterForm,
)
from pretix.control.forms.orders import ExporterForm
from pretix.control.forms.organizer import (
@@ -133,7 +134,7 @@ from pretix.control.permissions import (
organizer_permission_required,
)
from pretix.control.signals import nav_organizer
from pretix.control.views import PaginationMixin
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import OF_SELF, GroupConcat
from pretix.helpers.compat import CompatDeleteView
@@ -2663,6 +2664,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/logs.html'
permission = 'organizer.settings.general:write'
model = LogEntry
paginator_class = LargeResultSetPaginator
context_object_name = 'logs'
def get_queryset(self):
@@ -2672,16 +2674,21 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
if self.request.GET.get('action_type'):
qs = qs.filter(action_type=self.request.GET['action_type'])
if self.request.GET.get('user'):
qs = qs.filter(user_id=self.request.GET.get('user'))
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['filter_form'] = self.filter_form
return ctx
@cached_property
def filter_form(self):
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = MembershipType
+4 -13
View File
@@ -19,7 +19,6 @@
# 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 ipaddress
import socket
import sys
import types
@@ -42,8 +41,7 @@ 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')
from pretix.helpers.ssrf import should_block_access
def monkeypatch_vobject_performance():
@@ -150,16 +148,9 @@ def monkeypatch_urllib3_ssrf_protection():
af, socktype, proto, canonname, sa = res
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
if check_ip4 in _cgnat_net:
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
is_private, msg = should_block_access(sa)
if is_private:
raise HTTPError(msg)
sock = None
try:
+40
View File
@@ -0,0 +1,40 @@
#
# 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 ipaddress
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
def should_block_access(sa):
ip_addr = ipaddress.ip_address(sa[0])
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
if ip_addr.is_multicast:
return True, f"Request to multicast address {sa[0]} blocked"
if ip_addr.is_loopback or ip_addr.is_link_local:
return True, f"Request to local address {sa[0]} blocked"
if ip_addr.is_private:
return True, f"Request to private address {sa[0]} blocked"
if check_ip4 in _cgnat_net:
return True, f"Request to RFC 6598 address {sa[0]} blocked"
return False, None
+2 -2
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
"PO-Revision-Date: 2026-07-13 12:35+0000\n"
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
"PO-Revision-Date: 2026-07-20 08:01+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/"
"ar/>\n"
"Language: ar\n"
+8 -1
View File
@@ -73,7 +73,9 @@ from pretix.plugins.banktransfer.payment import BankTransfer
from pretix.plugins.banktransfer.refund_export import (
build_sepa_xml, get_refund_export_csv,
)
from pretix.plugins.banktransfer.tasks import process_banktransfers
from pretix.plugins.banktransfer.tasks import (
notify_incomplete_payment, process_banktransfers,
)
logger = logging.getLogger('pretix.plugins.banktransfer')
@@ -165,6 +167,11 @@ class ActionView(View):
provider='banktransfer',
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
trans.order.refresh_from_db()
if trans.order.pending_sum > Decimal('0.00') and trans.order.status == Order.STATUS_PENDING:
notify_incomplete_payment(trans.order)
return JsonResponse({
'status': 'ok',
})
+3 -13
View File
@@ -32,7 +32,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.forms import SecretKeySettingsField
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
from pretix.base.middleware import add_to_response_csp
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
register_global_settings, register_payment_providers,
@@ -144,12 +144,7 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
(url.namespace == "plugins:paypal2" and url.url_name == "pay")
):
if 'Content-Security-Policy' in response:
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
csps = {
add_to_response_csp(response, {
'script-src': ['https://www.paypal.com', "'nonce-{}'".format(_nonce(request))],
# When the stars align in an unpredictable manner and the temperature is just right, the PayPal SDK might
@@ -164,12 +159,7 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
'connect-src': ['https://www.paypal.com', 'https://www.sandbox.paypal.com'], # Or not - seems to only affect PayPal logging...
'img-src': ['https://t.paypal.com', 'https://www.paypalobjects.com'],
'style-src': ["'unsafe-inline'"] # PayPal does not comply with our nonce unfortunately, see Z#23113213
}
_merge_csp(h, csps)
if h:
response['Content-Security-Policy'] = _render_csp(h)
})
return response
+18 -9
View File
@@ -230,10 +230,15 @@ class OrderMailForm(BaseMailForm):
self.fields['attach_tickets'].help_text = _("Attachment of tickets is disabled in this event's email "
"settings.")
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
choices.insert(0, ('valid_if_pending', _('payment pending but already confirmed')))
choices.insert(0, ('na', _('payment pending (except unapproved or already confirmed)')))
choices.insert(0, ('pa', _('approval pending')))
choices = [
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
('valid_if_pending', _('payment pending but already confirmed')),
('na', _('payment pending (except unapproved or already confirmed)')),
('pa', _('approval pending')),
(Order.STATUS_CANCELED, _('Canceled (fully)')),
(Order.STATUS_EXPIRED, _("Expired")),
]
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
choices.append(
('overdue', _('pending with payment overdue'))
@@ -382,11 +387,15 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
self._set_field_placeholders('subject', ['event', 'order', 'event_or_subevent'])
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'], rich=True)
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
choices.insert(0, ('n__valid_if_pending', _('payment pending but already confirmed')))
choices.insert(0, ('n__not_pending_approval_and_not_valid_if_pending',
_('payment pending (except unapproved or already confirmed)')))
choices.insert(0, ('n__pending_approval', _('approval pending')))
choices = [
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
('n__valid_if_pending', _('payment pending but already confirmed')),
('n__not_pending_approval_and_not_valid_if_pending',
_('payment pending (except unapproved or already confirmed)')),
('n__pending_approval', _('approval pending')),
(Order.STATUS_CANCELED, _('Canceled (fully)')),
(Order.STATUS_EXPIRED, _("Expired")),
]
if not self.event.settings.get('payment_term_expire_automatically', as_type=bool):
choices.append(
('n__pending_overdue', _('pending with payment overdue'))
+3 -14
View File
@@ -31,7 +31,7 @@ from django.utils.translation import gettext_lazy as _
from paypalhttp import HttpResponse
from pretix.base.forms import SecretKeySettingsField
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
from pretix.base.middleware import add_to_response_csp
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
logentry_display, register_global_settings, register_payment_providers,
@@ -201,21 +201,10 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
(url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"])
):
if 'Content-Security-Policy' in response:
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
# https://stripe.com/docs/security/guide#content-security-policy
csps = {
add_to_response_csp(response, {
'connect-src': ['https://api.stripe.com'],
'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'],
'script-src': ['https://js.stripe.com'],
}
_merge_csp(h, csps)
if h:
response['Content-Security-Policy'] = _render_csp(h)
})
return response
@@ -60,8 +60,18 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
app.config.errorHandler = (error, _vm, info) => {
console.error('[pretix-button]', info, error)
}
app.mount(element)
observer.observe(element, { attributes: true })
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
// the HTML-element gets replaced instead of vue3s behaviour to replace innerHTML.
// The latter can cause issues with custom CSS as HTML structure would change.
// app.mount(element)
// observer.observe(element, { attributes: true })
const fragment = document.createDocumentFragment()
app.mount(fragment)
for (const attr of element.attributes) {
fragment.firstChild.setAttribute(attr.name, attr.value)
}
observer.observe(fragment.firstChild, { attributes: true })
element.parentNode.replaceChild(fragment, element)
return app
}
@@ -56,8 +56,18 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
app.config.errorHandler = (error, _vm, info) => {
console.error('[pretix-widget]', info, error)
}
app.mount(element)
observer.observe(element, { attributes: true })
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
// the HTML-element gets replaced instead of vue3s behaviour to replace innerHTML.
// The latter can cause issues with custom CSS as HTML structure would change.
// app.mount(element)
// observer.observe(element, { attributes: true })
const fragment = document.createDocumentFragment()
app.mount(fragment)
for (const attr of element.attributes) {
fragment.firstChild.setAttribute(attr.name, attr.value)
}
observer.observe(fragment.firstChild, { attributes: true })
element.parentNode.replaceChild(fragment, element)
return app
}
@@ -50,6 +50,7 @@ def env():
datetime=now(), expires=now() + timedelta(days=10),
total=23,
sales_channel=o.sales_channels.get(identifier="web"),
email='dummy@dummy.dummy'
)
o2 = Order.objects.create(
code='6789Z', event=event,
@@ -129,7 +130,7 @@ def test_assign_order_unknown(env, client):
@pytest.mark.django_db
def test_assign_order_amount_incorrect(env, client):
def test_assign_order_amount_incorrect(env, client, mailoutbox):
job = BankImportJob.objects.create(event=env[0])
trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo',
state=BankTransaction.STATE_NOMATCH,
@@ -141,6 +142,7 @@ def test_assign_order_amount_incorrect(env, client):
assert r['status'] == 'ok'
trans.refresh_from_db()
assert trans.state == BankTransaction.STATE_VALID
assert len(mailoutbox) == 1
@pytest.mark.django_db