Compare commits

..

7 Commits

Author SHA1 Message Date
Raphael Michel
ae8ba2528a Bump to 2025.10.2 2026-02-16 11:02:13 +01:00
Raphael Michel
2dbb7ebd2d Fix placeholder injection with django templates 2026-02-13 13:37:14 +01:00
Raphael Michel
5dac696cb8 SafeFormatter: Ignore conversion spec 2026-02-13 13:37:14 +01:00
Raphael Michel
474ca35616 Mark strings as formatted to prevent double-formatting 2026-02-13 13:37:14 +01:00
Kara Engelhardt
ff351f2856 SECURITY: Prevent placeholder injcetion in plaintext emails 2026-02-13 13:37:14 +01:00
Raphael Michel
5d87f9a26f Bump to 2025.10.1 2025-12-19 13:06:58 +01:00
Raphael Michel
4b5651862c [SECURITY] Prevent access to arbitrary cached files by UUID (CVE-2025-14881) 2025-12-19 13:06:48 +01:00
60 changed files with 834 additions and 1057 deletions

View File

@@ -35,7 +35,6 @@ dependencies = [
"cryptography>=44.0.0",
"css-inline==0.18.*",
"defusedcsv>=1.1.0",
"dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26",
"django-bootstrap3==25.2",
"django-compressor==4.5.1",
@@ -82,7 +81,7 @@ dependencies = [
"pycountry",
"pycparser==2.23",
"pycryptodome==3.23.*",
"pypdf==6.4.*",
"pypdf==6.3.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*",
"pytz",
@@ -92,7 +91,7 @@ dependencies = [
"redis==6.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.46.*",
"sentry-sdk==2.45.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2025.11.0.dev0"
__version__ = "2025.10.2"

View File

@@ -74,6 +74,11 @@ class ExportersMixin:
@action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)')
def download(self, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs['cfid'])
if not cf.allowed_for_session(self.request, "exporters-api"):
return Response(
{'status': 'failed', 'message': 'Unknown file ID or export failed'},
status=status.HTTP_410_GONE
)
if cf.file:
resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type)
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore")
@@ -109,7 +114,8 @@ class ExportersMixin:
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=False)
cf = CachedFile(web_download=True)
cf.bind_to_session(self.request, "exporters-api")
cf.date = now()
cf.expires = now() + timedelta(hours=24)
cf.save()

View File

@@ -43,7 +43,6 @@ from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import periodic_task
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.celery import get_task_priority
logger = logging.getLogger(__name__)
_ALL_EVENTS = None
@@ -475,10 +474,7 @@ def notify_webhooks(logentry_ids: list):
)
for wh in webhooks:
send_webhook.apply_async(
args=(logentry.id, notification_type.action_type, wh.pk),
priority=get_task_priority("notifications", logentry.organizer_id),
)
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=60, acks_late=True, autoretry_for=(DatabaseError,),)

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

@@ -19,11 +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 re
import dns.resolver
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _, pgettext
@@ -126,9 +123,6 @@ class PeppolIdValidator:
"9959": ".*",
}
def __init__(self, validate_online=False):
self.validate_online = validate_online
def __call__(self, value):
if ":" not in value:
raise ValidationError(_("A Peppol participant ID always starts with a prefix, followed by a colon (:)."))
@@ -142,28 +136,6 @@ class PeppolIdValidator:
raise ValidationError(_("The Peppol participant ID does not match the validation rules for the prefix "
"%(number)s. Please reach out to us if you are sure this ID is correct."),
params={"number": prefix})
if self.validate_online:
base_hostnames = ['edelivery.tech.ec.europa.eu', 'acc.edelivery.tech.ec.europa.eu']
smp_id = base64.b32encode(hashlib.sha256(value.lower().encode()).digest()).decode().rstrip("=")
for base_hostname in base_hostnames:
smp_domain = f'{smp_id}.iso6523-actorid-upis.{base_hostname}'
resolver = dns.resolver.Resolver()
try:
answers = resolver.resolve(smp_domain, 'NAPTR', lifetime=1.0)
if answers:
return value
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
# ID not registered, do not set found=True
pass
except Exception: # noqa
# Error likely on our end or infrastructure is down, allow user to proceed
return value
raise ValidationError(
_("The Peppol participant ID is not registered on the Peppol network."),
)
return value
@@ -183,9 +155,7 @@ class PeppolTransmissionType(TransmissionType):
"transmission_peppol_participant_id": forms.CharField(
label=_("Peppol participant ID"),
validators=[
PeppolIdValidator(
validate_online=True,
),
PeppolIdValidator(),
]
),
}

View File

@@ -31,7 +31,6 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from pretix.helpers.celery import get_task_priority
from pretix.helpers.json import CustomJSONEncoder
@@ -59,6 +58,37 @@ class CachedFile(models.Model):
web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins
session_key = models.TextField(null=True, blank=True) # only allow download in this session
def session_key_for_request(self, request, salt=None):
from ...api.models import OAuthAccessToken, OAuthApplication
from .devices import Device
from .organizer import TeamAPIToken
if hasattr(request, "auth") and isinstance(request.auth, OAuthAccessToken):
k = f'app:{request.auth.application.pk}'
elif hasattr(request, "auth") and isinstance(request.auth, OAuthApplication):
k = f'app:{request.auth.pk}'
elif hasattr(request, "auth") and isinstance(request.auth, TeamAPIToken):
k = f'token:{request.auth.pk}'
elif hasattr(request, "auth") and isinstance(request.auth, Device):
k = f'device:{request.auth.pk}'
elif request.session.session_key:
k = request.session.session_key
else:
raise ValueError("No auth method found to bind to")
if salt:
k = f"{k}!{salt}"
return k
def allowed_for_session(self, request, salt=None):
return (
not self.session_key or
self.session_key_for_request(request, salt) == self.session_key
)
def bind_to_session(self, request, salt=None):
self.session_key = self.session_key_for_request(request, salt)
@receiver(post_delete, sender=CachedFile)
def cached_file_delete(sender, instance, **kwargs):
@@ -132,15 +162,9 @@ class LoggingMixin:
logentry.save()
if logentry.notification_type:
notify.apply_async(
args=(logentry.pk,),
priority=get_task_priority("notifications", logentry.organizer_id),
)
notify.apply_async(args=(logentry.pk,))
if logentry.webhook_type:
notify_webhooks.apply_async(
args=(logentry.pk,),
priority=get_task_priority("notifications", logentry.organizer_id),
)
notify_webhooks.apply_async(args=(logentry.pk,))
return logentry

View File

@@ -35,14 +35,11 @@
import json
import logging
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import connections, models
from django.utils.functional import cached_property
from pretix.helpers.celery import get_task_priority
class VisibleOnlyManager(models.Manager):
def get_queryset(self):
@@ -189,19 +186,7 @@ class LogEntry(models.Model):
to_notify = [o.id for o in objects if o.notification_type]
if to_notify:
organizer_ids = set(o.organizer_id for o in objects if o.notification_type)
notify.apply_async(
args=(to_notify,),
priority=settings.PRIORITY_CELERY_HIGHEST_FUNC(
get_task_priority("notifications", oid) for oid in organizer_ids
),
)
notify.apply_async(args=(to_notify,))
to_wh = [o.id for o in objects if o.webhook_type]
if to_wh:
organizer_ids = set(o.organizer_id for o in objects if o.webhook_type)
notify_webhooks.apply_async(
args=(to_wh,),
priority=settings.PRIORITY_CELERY_HIGHEST_FUNC(
get_task_priority("notifications", oid) for oid in organizer_ids
),
)
notify_webhooks.apply_async(args=(to_wh,))

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 (
@@ -1181,7 +1181,8 @@ class Order(LockModel, LoggedModel):
try:
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,
@@ -2926,7 +2927,8 @@ class OrderPosition(AbstractPosition):
recipient = self.attendee_email
try:
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

@@ -76,7 +76,9 @@ from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.services.tickets import get_tickets_for_order
from pretix.base.signals import email_filter, global_email_filter
from pretix.celery_app import app
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
@@ -200,6 +202,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.')
@@ -222,8 +227,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
'invoice_company': ''
})
renderer = ClassicMailRenderer(None, organizer)
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
subject = str(subject).format_map(TolerantDict(context))
content_plain = render_mail(template, context, placeholder_mode=None)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
sender = (
sender or
(event.settings.get('mail_from') if event else None) or
@@ -255,6 +261,10 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
else:
timezone = ZoneInfo(settings.TIME_ZONE)
if not isinstance(content_plain, FormattedString):
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
else:
body_plain = content_plain
if settings_holder:
if settings_holder.settings.mail_bcc:
for bcc_mail in settings_holder.settings.mail_bcc.split(','):
@@ -270,7 +280,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
signature = str(settings_holder.settings.get('mail_text_signature'))
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"
@@ -288,7 +298,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
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,
@@ -304,7 +314,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
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.open', kwargs={
'order': order.code,
'secret': order.secret,
@@ -316,7 +326,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
with override(timezone):
try:
content_plain = render_mail(template, context, placeholder_mode=None)
if plain_text_only:
body_html = None
elif 'context' in inspect.signature(renderer.render).parameters:
@@ -337,8 +346,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
logger.exception('Could not render HTML body')
body_html = None
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
send_task = mail_send_task.si(
to=[email] if isinstance(email, str) else list(email),
cc=cc,
@@ -759,7 +766,12 @@ def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_P
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

View File

@@ -32,7 +32,6 @@ from pretix.base.services.mail import mail_send_task
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
from pretix.base.signals import notification
from pretix.celery_app import app
from pretix.helpers.celery import get_task_priority
from pretix.helpers.urls import build_absolute_uri
@@ -89,18 +88,12 @@ def notify(logentry_ids: list):
for um, enabled in notify_specific.items():
user, method = um
if enabled:
send_notification.apply_async(
args=(logentry.id, notification_type.action_type, user.pk, method),
priority=get_task_priority("notifications", logentry.organizer_id),
)
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
for um, enabled in notify_global.items():
user, method = um
if enabled and um not in notify_specific:
send_notification.apply_async(
args=(logentry.id, notification_type.action_type, user.pk, method),
priority=get_task_priority("notifications", logentry.organizer_id),
)
send_notification.apply_async(args=(logentry.id, notification_type.action_type, user.pk, method))
notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)

View File

@@ -1,188 +0,0 @@
from ast import literal_eval
from collections import namedtuple
from datetime import datetime
from typing import Union
from django import forms
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Event
from pretix.control.forms import SplitDateTimeField
from pretix.control.forms.widgets import Select2
SubEventSelection = namedtuple(
typename='SubEventSelection',
field_names=['selection', 'subevents', 'start', 'end', ],
defaults=['subevent', None, None, None],
)
subeventselectionparts = namedtuple(
typename='subeventselectionparts',
field_names=['selection', 'subevents', 'start', 'end']
)
class SubEventSelectionWrapper:
def __init__(self, data: Union[None, SubEventSelection]):
self.data = data
def get_queryset(self, event: Event):
if self.data.selection == 'subevent':
if self.data.subevents is None:
return event.subevents.all()
else:
return event.subevents.filter(pk=self.data.subevents)
elif self.data.selection == 'timerange':
if self.data.start and self.data.end:
return event.subevents.filter(date_from__lte=self.data.start,
date_from__gte=self.data.end)
elif self.data.start:
return event.subevents.filter(date_from__gte=self.data.start)
elif self.data.end:
return event.subevents.filter(date_from__lte=self.data.end)
return event.subevents.all()
def to_string(self) -> str:
if self.data:
if self.data.selection == 'subevent':
return 'SUBEVENT/pk/{}'.format(self.data.subevents.pk)
elif self.data.selection == 'timerange':
if self.data.start and self.data.end:
return 'SUBEVENT/range/{}/{}'.format(self.data.start.isoformat(), self.data.end.isoformat())
elif self.data.start:
return 'SUBEVENT/from/{}'.format(self.data.start)
elif self.data.end:
return 'SUBEVENT/to/{}'.format(self.data.end)
return 'SUBEVENT'
@classmethod
def from_string(cls, input: str):
data = SubEventSelection(selection='subevent')
if input.startswith('SUBEVENT'):
parts = input.split('/')
if len(parts) == 1:
data = SubEventSelection(selection='subevent')
elif parts[1] == 'pk':
data = SubEventSelection(
selection='subevent',
subevents=literal_eval(parts[2])
)
elif parts[1] == 'range':
data = SubEventSelection(
selection="timerange",
start=datetime.fromisoformat(parts[2]),
end=datetime.fromisoformat(parts[3]),
)
elif parts[1] == 'from':
data = SubEventSelection(
selection="timerange",
start=datetime.fromisoformat(parts[2]),
)
elif parts[1] == 'to':
data = SubEventSelection(
selection="timerange",
end=datetime.fromisoformat(parts[3]),
)
return SubEventSelectionWrapper(
data=data
)
class SubeventSelectionWidget(forms.MultiWidget):
template_name = 'pretixcontrol/forms/widgets/subeventselection.html'
parts = SubEventSelection
def __init__(self, event: Event, status_choices, subevent_choices, *args, **kwargs):
widgets = subeventselectionparts(
selection=forms.RadioSelect(
choices=status_choices,
),
subevents=Select2(
attrs={
'class': 'simple-subevent-choice',
'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
}),
'data-placeholder': pgettext_lazy('subevent', 'All dates')
},
),
start=SplitDateTimePickerWidget(),
end=SplitDateTimePickerWidget(),
)
widgets.subevents.choices = subevent_choices
super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value):
if isinstance(value, str):
value = SubEventSelectionWrapper.from_string(value)
if isinstance(value, subeventselectionparts):
return value
return subeventselectionparts(selection='subevent', start=None, end=None, subevents=None)
class SubeventSelectionField(forms.MultiValueField):
widget = SubeventSelectionWidget
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
choices = [
("subevent", _("Subevent")),
("timerange", _("Timerange"))
]
fields = SubEventSelection(
selection=forms.ChoiceField(
choices=choices,
required=True,
),
subevents=forms.ModelChoiceField(
required=False,
queryset=self.event.subevents,
empty_label=pgettext_lazy('subevent', 'All dates')
),
start=SplitDateTimeField(
required=False,
),
end=SplitDateTimeField(
required=False,
),
)
kwargs['widget'] = SubeventSelectionWidget(
event=self.event,
status_choices=choices,
subevent_choices=fields.subevents.widget.choices,
)
super().__init__(
fields=fields, require_all_fields=False, *args, **kwargs
)
def compress(self, data_list):
if not data_list:
return None
return SubEventSelectionWrapper(data=SubEventSelection(*data_list)).to_string()
def clean(self, value):
data = subeventselectionparts(*value)
if data.selection == "timerange":
if (data.start != ["", ""] and data.end != ["", ""]) and data.end < data.start:
raise ValidationError(_("The end date must be after the start date."))
if (data.start == ["", ""]) and (data.end == ["", ""]):
raise ValidationError(_('At least one of start and end must be specified.'))
return super().clean(value)

View File

@@ -36,9 +36,8 @@ class DownloadView(TemplateView):
def object(self) -> CachedFile:
try:
o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True)
if o.session_key:
if o.session_key != self.request.session.session_key:
raise Http404()
if not o.allowed_for_session(self.request):
raise Http404()
return o
except (ValueError, ValidationError): # Invalid URLs
raise Http404()

View File

@@ -47,7 +47,6 @@ from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import now
from django.utils.translation import gettext as __, gettext_lazy as _
from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
@@ -57,14 +56,11 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
Item, ItemCategory, ItemProgramTime, ItemVariation, Order, OrderPosition,
Question, QuestionOption, Quota,
Item, ItemCategory, ItemProgramTime, ItemVariation, Question,
QuestionOption, Quota,
)
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
from pretix.base.subevent import (
SubeventSelectionField, SubEventSelectionWrapper,
)
from pretix.control.forms import (
ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField,
SalesChannelCheckboxSelectMultiple, SplitDateTimeField,
@@ -276,87 +272,6 @@ class QuestionOptionForm(I18nModelForm):
]
class QuestionFilterForm(forms.Form):
STATUS_VARIANTS = [
("", _("All orders")),
("p", _("Paid")),
("pv", _("Paid or confirmed")),
("n", _("Pending")),
("np", _("Pending or paid")),
("o", _("Pending (overdue)")),
("e", _("Expired")),
("ne", _("Pending or expired")),
("c", _("Canceled"))
]
status = forms.ChoiceField(
choices=STATUS_VARIANTS,
widget=forms.Select(
attrs={
'class': 'form-control',
}
),
required=False,
label=_("Status"),
initial="np",
)
item = forms.ChoiceField(
choices=[],
widget=forms.Select(
attrs={'class': 'form-control'}
),
required=False,
label=_("Items")
)
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
super().__init__(*args, **kwargs)
if self.event.has_subevents:
self.fields['subevent_selection'] = SubeventSelectionField(
event=self.event,
label=_("Subevents"),
help_text=_("Select the subevents that should be included in the statistics either by subevent or by the timerange in which they occur."),
)
self.fields['item'].choices = [('', _('All products'))] + [(item.id, item.name) for item in
Item.objects.filter(event=self.event)]
def filter_qs(self):
fdata = self.cleaned_data
opqs = OrderPosition.objects.filter(
order__event=self.event,
)
sub_event_qs = SubEventSelectionWrapper.from_string(fdata['subevent_selection']).get_queryset(self.event)
opqs = opqs.filter(subevent__in=sub_event_qs)
s = fdata.get("status", "np")
if s != "":
if s == 'o':
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
order__expires__lt=now().replace(hour=0, minute=0, second=0))
elif s == 'np':
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
elif s == 'pv':
opqs = opqs.filter(
Q(order__status=Order.STATUS_PAID) |
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
)
elif s == 'ne':
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
else:
opqs = opqs.filter(order__status=s)
if s not in (Order.STATUS_CANCELED, ""):
opqs = opqs.filter(canceled=False)
if fdata.get("item", "") != "":
i = fdata.get("item", "")
opqs = opqs.filter(item_id__in=(i,))
return opqs
class QuotaForm(I18nModelForm):
itemvars = forms.MultipleChoiceField(
label=_("Products"),

View File

@@ -126,9 +126,7 @@
{% endif %}
<a class="navbar-brand" href="{% url "control:index" %}">
<img src="{% static "pretixbase/img/pretix-icon-white-mini.svg" %}" />
<span>
{{ settings.PRETIX_INSTANCE_NAME }}
</span>
{{ settings.PRETIX_INSTANCE_NAME }}
</a>
</div>
<ul class="nav navbar-nav navbar-top-links navbar-left flip hidden-xs">

View File

@@ -1,38 +0,0 @@
{% load i18n %}
<div class="subevent-selection col-lg-12">
{% for group_name, group_choices, group-index in widget.subwidgets.0.optgroups %}
{% for selopt in group_choices %}
<div class="radio">
<label class="col-lg-2">
<input type="radio" name="{{ widget.subwidgets.0.name }}" value="{{ selopt.value }}"
{% include "django/forms/widgets/attrs.html" with widget=selopt %} />
{{ selopt.label }}
</label>
{% if selopt.value == "subevent" %}
{% with widget.subwidgets.1 as widget %}
{% include widget.template_name %}
{% endwith %}
{% elif selopt.value == "timerange" %}
{% with widget.subwidgets.2 as widget %}
{% include widget.template_name %}
{% endwith %}
<span class="spacer">{% trans "until" %}</span>
{% with widget.subwidgets.3 as widget %}
{% include widget.template_name %}
{% endwith %}
{% endif %}
</div>
{% endfor %}
{% endfor %}
</div>

View File

@@ -5,11 +5,6 @@
{% load formset_tags %}
{% block title %}{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}{% endblock %}
{% block inside %}
{% for e in form.errors.values %}
<div class="alert alert-danger has-error">
{{ e }}
</div>
{% endfor %}
<h1>
{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
@@ -25,24 +20,35 @@
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-lg-4 col-sm-6 col-xs-6">
{% bootstrap_label form.status.label %}
{% bootstrap_field form.status layout="inline" %}
</div>
<div class="col-lg-8 col-sm-6 col-xs-6">
{% bootstrap_label form.item.label %}
{% bootstrap_field form.item layout="inline" %}
</div>
<div class="col-lg-12 col-sm-6 col-xs-6">
{% bootstrap_label form.subevent_selection.label %}
{{ form.subevent_selection }}
<div class="help-block">
{{ form.subevent_selection.help_text }}
<div class="col-lg-2 col-sm-6 col-xs-6">
<select name="status" class="form-control">
<option value="" {% if request.GET.status == "" %}selected="selected"{% endif %}>{% trans "All orders" %}</option>
<option value="p" {% if request.GET.status == "p" %}selected="selected"{% endif %}>{% trans "Paid" %}</option>
<option value="pv" {% if request.GET.status == "pv" %}selected="selected"{% endif %}>{% trans "Paid or confirmed" %}</option>
<option value="n" {% if request.GET.status == "n" %}selected="selected"{% endif %}>{% trans "Pending" %}</option>
<option value="np" {% if request.GET.status == "np" or "status" not in request.GET %}selected="selected"{% endif %}>{% trans "Pending or paid" %}</option>
<option value="o" {% if request.GET.status == "o" %}selected="selected"{% endif %}>{% trans "Pending (overdue)" %}</option>
<option value="e" {% if request.GET.status == "e" %}selected="selected"{% endif %}>{% trans "Expired" %}</option>
<option value="ne" {% if request.GET.status == "ne" %}selected="selected"{% endif %}>{% trans "Pending or expired" %}</option>
<option value="c" {% if request.GET.status == "c" %}selected="selected"{% endif %}>{% trans "Canceled" %}</option>
</select>
</div>
<div class="col-lg-5 col-sm-6 col-xs-6">
<select name="item" class="form-control">
<option value="">{% trans "All products" %}</option>
{% for item in items %}
<option value="{{ item.id }}"
{% if request.GET.item|add:0 == item.id %}selected="selected"{% endif %}>
{{ item.name }}
</option>
{% endfor %}
</select>
</div>
{% if request.event.has_subevents %}
<div class="col-lg-5 col-sm-6 col-xs-6">
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
</div>
</div>
{% endif %}
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">

View File

@@ -38,7 +38,6 @@ from collections import OrderedDict, namedtuple
from itertools import groupby
from json.decoder import JSONDecodeError
from django import forms
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.files import File
@@ -46,7 +45,6 @@ from django.db import transaction
from django.db.models import (
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q,
)
from django.dispatch import receiver
from django.forms.models import inlineformset_factory
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
@@ -65,10 +63,9 @@ from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
ItemVariationSerializer,
)
from pretix.base.exporter import ListExporter
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation,
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
SeatCategoryMapping, Voucher,
)
@@ -76,13 +73,12 @@ from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability, register_data_exporters
from pretix.base.signals import quota_availability
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
ItemVariationsFormSet, QuestionFilterForm, QuestionForm,
QuestionOptionForm, QuotaForm,
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -664,73 +660,46 @@ class QuestionMixin:
return ctx
class QuestionAnswerExporter(ListExporter):
identifier = 'question_answer_exporter'
verbose_name = _('Question answers exporter')
description = _('Download a spreadsheet containing question answers')
category = _('Order data')
@property
def additional_form_fields(self):
form = {
'question':
forms.ModelChoiceField(
label=_('Question'),
queryset=Question.objects.filter(event=self.event),
),
**QuestionFilterForm(event=self.event).fields
}
return form
def iterate_list(self, form_data):
question = Question.objects.filter(event=self.event).get(pk=form_data['question'])
opqs = QuestionFilterForm(event=self.event, data=form_data).order_position_queryset()
qs = QuestionAnswer.objects.filter(
question=question, orderposition__isnull=False,
)
qs = qs.filter(orderposition__in=opqs)
headers = [
_("Subevent"),
_("Event start time"),
_("Order"),
_("Order position"),
question.question
]
yield headers
yield self.ProgressSetTotal(total=qs.count())
for questionAnswer in qs.iterator(chunk_size=1000):
row = [
questionAnswer.orderposition.subevent.name,
questionAnswer.orderposition.subevent.date_from.replace(tzinfo=None),
questionAnswer.orderposition.order.code,
questionAnswer.orderposition.positionid,
questionAnswer.answer
]
yield row
@receiver(register_data_exporters, dispatch_uid="exporter_questions_exporter")
def register_data_exporter(sender, **kwargs):
return QuestionAnswerExporter
class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView):
class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingView, DetailView):
model = Question
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
template_name_field = 'question'
def get_answer_statistics(self, opqs: OrderPosition):
def get_answer_statistics(self):
opqs = OrderPosition.objects.filter(
order__event=self.request.event,
)
qs = QuestionAnswer.objects.filter(
question=self.object, orderposition__isnull=False,
)
if self.request.GET.get("subevent", "") != "":
opqs = opqs.filter(subevent=self.request.GET["subevent"])
s = self.request.GET.get("status", "np")
if s != "":
if s == 'o':
opqs = opqs.filter(order__status=Order.STATUS_PENDING,
order__expires__lt=now().replace(hour=0, minute=0, second=0))
elif s == 'np':
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])
elif s == 'pv':
opqs = opqs.filter(
Q(order__status=Order.STATUS_PAID) |
Q(order__status=Order.STATUS_PENDING, order__valid_if_pending=True)
)
elif s == 'ne':
opqs = opqs.filter(order__status__in=[Order.STATUS_PENDING, Order.STATUS_EXPIRED])
else:
opqs = opqs.filter(order__status=s)
if s not in (Order.STATUS_CANCELED, ""):
opqs = opqs.filter(canceled=False)
if self.request.GET.get("item", "") != "":
i = self.request.GET.get("item", "")
opqs = opqs.filter(item_id__in=(i,))
qs = qs.filter(orderposition__in=opqs)
op_cnt = opqs.filter(item__in=self.object.items.all()).count()
@@ -778,20 +747,8 @@ class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['items'] = self.object.items.all()
if self.request.GET:
ctx['form'] = QuestionFilterForm(
data=self.request.GET,
event=self.request.event,
)
else:
ctx['form'] = QuestionFilterForm(
event=self.request.event,
)
if ctx['form'].is_valid():
opqs = ctx['form'].filter_qs()
stats = self.get_answer_statistics(opqs)
ctx['stats'], ctx['total'] = stats
stats = self.get_answer_statistics()
ctx['stats'], ctx['total'] = stats
return ctx
def get_object(self, queryset=None) -> Question:

View File

@@ -38,6 +38,7 @@ from datetime import timedelta
from django.conf import settings
from django.contrib import messages
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
@@ -85,6 +86,7 @@ class BaseImportView(TemplateView):
filename='import.csv',
type='text/csv',
)
cf.bind_to_session(request, "modelimport")
cf.file.save('import.csv', request.FILES['file'])
if self.request.POST.get("charset") in ENCODINGS:
@@ -137,7 +139,10 @@ class BaseProcessView(AsyncAction, FormView):
@cached_property
def file(self):
return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
cf = get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv")
if not cf.allowed_for_session(self.request, "modelimport"):
raise Http404()
return cf
@cached_property
def parsed(self):

View File

@@ -247,7 +247,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
cf = None
if request.POST.get("background", "").strip():
try:
cf = CachedFile.objects.get(id=request.POST.get("background"))
cf = CachedFile.objects.get(id=request.POST.get("background"), web_download=True)
except CachedFile.DoesNotExist:
pass

View File

@@ -38,7 +38,8 @@ from collections import OrderedDict
from zipfile import ZipFile
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import get_language, gettext_lazy as _
@@ -94,6 +95,8 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
cf = CachedFile.objects.get(pk=kwargs['file'])
except CachedFile.DoesNotExist:
raise ShredError(_("The download file could no longer be found on the server, please try to start again."))
if not cf.allowed_for_session(self.request):
raise Http404()
with ZipFile(cf.file.file, 'r') as zipfile:
indexdata = json.loads(zipfile.read('index.json').decode())
@@ -111,7 +114,7 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
ctx = super().get_context_data(**kwargs)
ctx['shredders'] = self.shredders
ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders)
ctx['file'] = get_object_or_404(CachedFile, pk=kwargs.get("file"))
ctx['file'] = cf
return ctx

View File

@@ -1,62 +0,0 @@
#
# 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/>.
#
from django.conf import settings
THRESHOLD_DOWNGRADE_TO_MID = 50
THRESHOLD_DOWNGRADE_TO_LOW = 250
def get_task_priority(shard, organizer_id):
"""
This is an attempt to build a simple "fair-use" policy for webhooks and notifications. The problem is that when
one organizer creates e.g. 20,000 orders through the API, that might schedule 20,000 webhooks and every other
organizer will need to wait for these webhooks to go through.
We try to fix that by building three queues: high-prio, mid-prio, and low-prio. Every organizer starts in the
high-prio queue, and all their tasks are routed immediately. Once an organizer submits more than X jobs of a
certain type per minute, they get downgraded to the mid-prio queue, and then if they submit even more to the
low-prio queue. That way, if another organizer has "regular usage", they are prioritized over the organizer with
high load.
"""
from django_redis import get_redis_connection
if not settings.HAS_REDIS:
return settings.PRIORITY_CELERY_HIGH
# We use redis directly instead of the Django cache API since the Django cache API does not support INCR for
# nonexistant keys
rc = get_redis_connection("redis")
cache_key = f"pretix:task_priority:{shard}:{organizer_id}"
# Make sure counters expire after a while when not used
p = rc.pipeline()
p.incr(cache_key)
p.expire(cache_key, 60)
new_counter = p.execute()[0]
if new_counter >= THRESHOLD_DOWNGRADE_TO_LOW:
return settings.PRIORITY_CELERY_LOW
elif new_counter >= THRESHOLD_DOWNGRADE_TO_MID:
return settings.PRIORITY_CELERY_MID
else:
return settings.PRIORITY_CELERY_HIGH

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)
)

File diff suppressed because it is too large Load Diff

View File

@@ -711,7 +711,7 @@ class PaypalMethod(BasePaymentProvider):
description = '{prefix}{orderstring}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
orderstring=__('Order {order} for {event}').format(
event=self.event.name,
event=request.event.name,
order=payment.order.code
),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''

View File

@@ -438,6 +438,9 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
) if var.original_price or item.original_price else None
if not display_add_to_cart:
display_add_to_cart = not item.requires_seat and var.order_max > 0
var.current_unavailability_reason = var.unavailability_reason(has_voucher=voucher, subevent=subevent)
item.original_price = (
@@ -468,8 +471,6 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
item.best_variation_availability = max([v.cached_availability[0] for v in item.available_variations])
item._remove = not bool(item.available_variations)
if not item._remove and not display_add_to_cart:
display_add_to_cart = not item.requires_seat and any(v.order_max > 0 for v in item.available_variations)
if not quota_cache_existed and not voucher and not allow_addons and not base_qs_set and not filter_items and not filter_categories:
event.cache.set(quota_cache_key, quota_cache, 5)

View File

@@ -347,53 +347,11 @@ if HAS_CELERY:
CELERY_RESULT_BACKEND = config.get('celery', 'backend')
if HAS_CELERY_BROKER_TRANSPORT_OPTS:
CELERY_BROKER_TRANSPORT_OPTIONS = loads(config.get('celery', 'broker_transport_options'))
else:
CELERY_BROKER_TRANSPORT_OPTIONS = {}
if HAS_CELERY_BACKEND_TRANSPORT_OPTS:
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = loads(config.get('celery', 'backend_transport_options'))
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
if CELERY_BROKER_URL.startswith("amqp://"):
# https://docs.celeryq.dev/en/latest/userguide/routing.html#routing-options-rabbitmq-priorities
# Enable priorities for all queues
CELERY_TASK_QUEUE_MAX_PRIORITY = 3
# On RabbitMQ, higher number is higher priority, and having less levels makes rabbitmq use less CPU and RAM
PRIORITY_CELERY_LOW = 1
PRIORITY_CELERY_MID = 2
PRIORITY_CELERY_HIGH = 3
PRIORITY_CELERY_LOWEST_FUNC = min
PRIORITY_CELERY_HIGHEST_FUNC = max
# Set default
CELERY_TASK_DEFAULT_PRIORITY = PRIORITY_CELERY_MID
elif CELERY_BROKER_URL.startswith("redis://"):
# https://docs.celeryq.dev/en/latest/userguide/routing.html#redis-message-priorities
CELERY_BROKER_TRANSPORT_OPTIONS.update({
"queue_order_strategy": "priority",
"sep": ":",
"priority_steps": [0, 4, 8]
})
# On redis, lower number is higher priority, and it appears that there are always levels 0-9 even though it
# is only really executed based on the 3 steps listed above.
PRIORITY_CELERY_LOW = 9
PRIORITY_CELERY_MID = 5
PRIORITY_CELERY_HIGH = 0
PRIORITY_CELERY_LOWEST_FUNC = max
PRIORITY_CELERY_HIGHEST_FUNC = min
CELERY_TASK_DEFAULT_PRIORITY = PRIORITY_CELERY_MID
else:
# No priority support assumed
PRIORITY_CELERY_LOW = 0
PRIORITY_CELERY_MID = 0
PRIORITY_CELERY_HIGH = 0
PRIORITY_CELERY_LOWEST_FUNC = min
PRIORITY_CELERY_HIGHEST_FUNC = max
else:
CELERY_TASK_ALWAYS_EAGER = True
PRIORITY_CELERY_LOW = 0
PRIORITY_CELERY_MID = 0
PRIORITY_CELERY_HIGH = 0
PRIORITY_CELERY_LOWEST_FUNC = min
PRIORITY_CELERY_HIGHEST_FUNC = max
CACHE_TICKETS_HOURS = config.getint('cache', 'tickets', fallback=24 * 3)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Ebene_1"
viewBox="0 0 128 128"
version="1.1"
sodipodi:docname="mstile.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="4.125"
inkscape:cx="28.242424"
inkscape:cy="53.090909"
inkscape:window-width="2556"
inkscape:window-height="1239"
inkscape:window-x="0"
inkscape:window-y="180"
inkscape:window-maximized="1"
inkscape:current-layer="Ebene_1" />
<defs
id="defs1">
<style
id="style1">.cls-1{fill:#f8f8f8;}</style>
</defs>
<g
id="g2"
transform="matrix(0.47350597,0,0,0.47350597,11.541278,11.202115)">
<path
class="cls-1"
d="M 97.457879,82.258482 C 96.737879,82.358482 96.237879,82.558482 95.797879,82.758482 L 98.177879,99.668482 C 98.587879,99.748482 99.127879,99.798482 99.777879,99.708482 103.29788,99.208482 104.38788,96.068482 103.58788,90.318482 102.75788,84.448482 101.05788,81.758482 97.467879,82.258482 Z"
id="path1" />
<path
class="cls-1"
d="M 162.82788,60.358482 C 163.53788,60.188482 163.98788,59.598482 163.88788,58.878482 L 159.32788,26.438482 C 159.22788,25.718482 158.55788,25.218482 157.83788,25.318482 L 121.58788,30.408482 122.97788,40.298482 C 123.22788,42.048482 121.90788,43.788482 120.15788,44.038482 118.40788,44.288482 116.66788,42.968482 116.41788,41.218482 L 115.02788,31.328482 47.917879,40.768482 C 47.197879,40.868482 46.697879,41.538482 46.797879,42.258482 L 51.357879,74.698482 C 51.457879,75.418482 52.057879,75.868482 52.777879,75.828482 64.027879,74.908482 74.207879,82.928482 75.807879,94.288482 77.407879,105.64848 69.817879,116.09848 58.737879,118.24848 58.027879,118.41848 57.577879,119.00848 57.677879,119.72848 L 62.237879,152.16848 C 62.337879,152.88848 63.007879,153.38848 63.727879,153.28848 L 130.84788,143.85848 129.43788,133.83848 C 129.18788,132.02848 130.44788,130.35848 132.25788,130.09848 134.06788,129.83848 135.74788,131.16848 135.99788,132.91848 L 137.40788,142.93848 173.65788,137.84848 C 174.37788,137.74848 174.87788,137.07848 174.77788,136.35848 L 170.21788,103.91848 C 170.11788,103.19848 169.51788,102.74848 168.79788,102.78848 157.54788,103.70848 147.37788,95.748482 145.77788,84.388482 144.17788,73.028482 151.75788,62.518482 162.83788,60.358482 Z M 102.98788,105.10848 C 101.22788,105.35848 99.697879,105.36848 98.947879,105.27848 L 100.53788,116.56848 90.617879,117.95848 85.317879,80.228482 C 87.817879,78.608482 91.277879,77.198482 96.697879,76.428482 105.37788,75.208482 111.96788,79.008482 113.35788,88.868482 114.60788,97.748482 110.23788,104.07848 102.99788,105.09848 Z M 133.22788,113.24848 C 133.47788,114.99848 132.15788,116.73848 130.40788,116.98848 128.65788,117.23848 126.91788,115.91848 126.66788,114.16848 L 124.28788,97.248482 C 124.02788,95.378482 125.23788,93.768482 127.10788,93.508482 128.97788,93.248482 130.58788,94.518482 130.84788,96.328482 Z M 128.11788,76.878482 C 128.36788,78.688482 127.10788,80.358482 125.29788,80.618482 123.48788,80.878482 121.81788,79.608482 121.55788,77.798482 L 119.17788,60.878482 C 118.92788,59.068482 120.18788,57.398482 121.99788,57.138482 123.74788,56.888482 125.48788,58.208482 125.73788,59.958482 Z"
id="path2" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><style>.cls-1{fill:#492267;}</style></defs><path class="cls-1" d="m50.67,56.95c-.72.1-1.22.3-1.66.5l2.38,16.91c.41.08.95.13,1.6.04,3.52-.5,4.61-3.64,3.81-9.39-.83-5.87-2.53-8.56-6.12-8.06Z"/><path class="cls-1" d="m116.04,35.05c.71-.17,1.16-.76,1.06-1.48L112.54,1.13c-.1-.72-.77-1.22-1.49-1.12l-37.5,5.27.73,5.22c.16,1.12-.62,2.15-1.74,2.31s-2.15-.62-2.31-1.74l-.73-5.22L1.13,15.46c-.72.1-1.22.77-1.12,1.49l4.56,32.44c.1.72.7,1.17,1.42,1.13,11.25-.92,21.43,7.1,23.03,18.46,1.6,11.36-5.99,21.81-17.07,23.96-.71.17-1.16.76-1.06,1.48l4.56,32.44c.1.72.77,1.22,1.49,1.12l68.37-9.61-.73-5.22c-.16-1.15.59-2.15,1.74-2.31s2.15.62,2.31,1.74l.73,5.22,37.5-5.27c.72-.1,1.22-.77,1.12-1.49l-4.56-32.44c-.1-.72-.7-1.17-1.42-1.13-11.25.92-21.42-7.04-23.02-18.4-1.6-11.36,5.98-21.87,17.06-24.03Zm-59.84,44.75c-1.76.25-3.29.26-4.04.17l1.59,11.29-9.92,1.39-5.3-37.73c2.5-1.62,5.96-3.03,11.38-3.8,8.68-1.22,15.27,2.58,16.66,12.44,1.25,8.88-3.12,15.21-10.36,16.23Zm30.73,20.71c.16,1.12-.62,2.15-1.74,2.31-1.12.16-2.15-.62-2.31-1.74l-1.47-10.44c-.16-1.12.62-2.15,1.74-2.31s2.16.66,2.31,1.74l1.47,10.44Zm-3.17-22.58c.15,1.08-.66,2.16-1.74,2.31s-2.16-.66-2.31-1.74l-1.47-10.44c-.16-1.15.59-2.15,1.74-2.31,1.12-.16,2.15.62,2.31,1.74l1.47,10.44Zm-3.16-22.45c.16,1.12-.62,2.15-1.74,2.31-1.12.16-2.15-.62-2.31-1.74l-1.47-10.44c-.16-1.12.62-2.15,1.74-2.31s2.16.66,2.31,1.74l1.47,10.44Zm-3.17-22.58c.15,1.08-.66,2.16-1.74,2.31s-2.16-.66-2.31-1.74l-1.47-10.44c-.16-1.15.59-2.15,1.74-2.31s2.15.62,2.31,1.74l1.47,10.44Z"/></svg>
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="933.333" height="933.333" viewBox="0 0 700.000000 700.000000"><path d="M0 109v109l4.3.1c33 .5 65.2 14.8 90 40.1 31.8 32.5 43.6 76.6 33.3 124.3-1.2 5.4-5.7 17.8-9 24.8-12.2 25.6-36.1 49.6-61.6 61.9-15.9 7.6-35.2 12.4-51.7 12.7L0 482v218h436.5v-60.5l9.2-.1c6-.1 9.3.3 9.6 1 .2.6.3 14.3.3 30.3l-.1 29.3H700V482l-4.7-.1c-32.5-.4-64.9-14.7-88.9-39.2-25.5-26.1-38.6-58.6-37.9-94.4.6-27.9 7.6-50 23.4-73.8 6.4-9.8 23.8-27 34.1-33.7 15.6-10.3 33.3-17.9 47.5-20.4 10-1.7 15.9-2.4 20.8-2.4h5.7V0H455.5v58h-19V.5L218.3.2 0 0v109zm454-6.1c1.3.1 1.5 5.7 1.5 44.6 0 43.8 0 44.5-2 44.6-7.9.3-15.7 0-16.3-.6-.4-.3-.7-20.3-.7-44.3v-43.7l4.5-.5c2.5-.3 6.1-.4 8-.3 1.9.1 4.2.2 5 .2zm1.5 178.6c0 24.5-.2 44.5-.5 44.6-.3.1-4.6.2-9.5.2l-9 .2V237h19v44.5zM287 256.4c37.8 3.1 65.2 23.6 75 55.9 3.6 12 4.4 18.2 4.4 34.2.1 18.9-.5 24-3.9 35.9-8.2 28.5-26.2 47.7-52 55.6-7.1 2.2-9.8 2.5-24 2.5-8.8-.1-17.8-.4-20.1-.9l-4-.8V510.5l-31.7.3-31.7.2V269.3l6.8-2.6c9.8-3.8 18.7-6.4 27.2-7.7 4.1-.7 8.2-1.4 9.1-1.5.9-.2 6.3-.7 12-1 5.7-.4 10.5-.8 10.6-.9.4-.3 14.8.2 22.3.8zm168.5 159.1c0 24.5-.2 44.6-.5 44.6-1.9.4-18 .4-18.1-.1-.4-1.5-.5-86.7-.1-87.8.3-.8 3.1-1.2 9.6-1.2h9.1v44.5zm-.1 133.7c-.1 24.4-.2 44.6-.3 44.9-.1.7-18.1.7-18.2 0-.4-1.6-.5-86.8-.1-87.9.3-.8 3.1-1.2 9.6-1.2h9.1l-.1 44.2z"/><path d="M264.5 293.9c-2 .8-2 1.8-2.1 54.5v53.8l2.5 1c1.4.5 5.8.8 9.8.5 20.6-1.5 29.5-18.2 29.6-55.7.2-37.8-9.2-54.5-30.5-54.8-4-.1-8.2.3-9.3.7z"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" viewBox="0 0 109.594 109.594"><g transform="scale(.94461)"><path d="M45.52 48.98c-.74 0-1.28.13-1.75.27v17.43c.4.13.94.27 1.61.27 3.63 0 5.18-3.03 5.18-8.95s-1.35-9.02-5.05-9.02z" fill="#3b1c4a"/><path d="M114.72 36.13c.74-.07 1.28-.61 1.28-1.35V1.35c0-.74-.61-1.35-1.35-1.35H75.99v5.38c0 1.15-.94 2.09-2.09 2.09s-2.09-.94-2.09-2.09V0H1.35C.61 0 0 .61 0 1.35v33.44c0 .74.54 1.28 1.28 1.35 11.51.67 20.66 10.23 20.66 21.94s-9.15 21.2-20.66 21.8c-.74.07-1.28.61-1.28 1.35v33.44c0 .74.61 1.35 1.35 1.35h70.48v-5.38c0-1.19.9-2.09 2.09-2.09s2.09.94 2.09 2.09v5.38h38.66c.74 0 1.35-.61 1.35-1.35V81.23c0-.74-.54-1.28-1.28-1.35-11.51-.67-20.66-10.16-20.66-21.87s9.15-21.26 20.66-21.87zM47.87 72.87c-1.82 0-3.36-.2-4.1-.4v11.64H33.54V45.22C36.3 43.94 40 43 45.58 43c8.95 0 15.07 4.78 15.07 14.94 0 9.15-5.32 14.94-12.78 14.94zm28.12 25.3c0 1.15-.94 2.09-2.09 2.09s-2.09-.94-2.09-2.09V87.4c0-1.15.94-2.09 2.09-2.09s2.09.97 2.09 2.09zm0-23.28c0 1.11-.97 2.09-2.09 2.09-1.12 0-2.09-.97-2.09-2.09V64.12c0-1.19.9-2.09 2.09-2.09s2.09.94 2.09 2.09zm0-23.15c0 1.15-.94 2.09-2.09 2.09s-2.09-.94-2.09-2.09V40.97c0-1.15.94-2.09 2.09-2.09s2.09.97 2.09 2.09zm0-23.28c0 1.11-.97 2.09-2.09 2.09-1.12 0-2.09-.97-2.09-2.09V17.69c0-1.19.9-2.09 2.09-2.09s2.09.94 2.09 2.09z" fill="#3b1c4a"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><style>.cls-1{fill:#f8f8f8;}</style></defs><path class="cls-1" d="m50.67,56.95c-.72.1-1.22.3-1.66.5l2.38,16.91c.41.08.95.13,1.6.04,3.52-.5,4.61-3.64,3.81-9.39-.83-5.87-2.53-8.56-6.12-8.06Z"/><path class="cls-1" d="m116.04,35.05c.71-.17,1.16-.76,1.06-1.48L112.54,1.13c-.1-.72-.77-1.22-1.49-1.12l-36.25,5.09,1.39,9.89c.25,1.75-1.07,3.49-2.82,3.74-1.75.25-3.49-1.07-3.74-2.82l-1.39-9.89L1.13,15.46c-.72.1-1.22.77-1.12,1.49l4.56,32.44c.1.72.7,1.17,1.42,1.13,11.25-.92,21.43,7.1,23.03,18.46s-5.99,21.81-17.07,23.96c-.71.17-1.16.76-1.06,1.48l4.56,32.44c.1.72.77,1.22,1.49,1.12l67.12-9.43-1.41-10.02c-.25-1.81,1.01-3.48,2.82-3.74s3.49,1.07,3.74,2.82l1.41,10.02,36.25-5.09c.72-.1,1.22-.77,1.12-1.49l-4.56-32.44c-.1-.72-.7-1.17-1.42-1.13-11.25.92-21.42-7.04-23.02-18.4-1.6-11.36,5.98-21.87,17.06-24.03Zm-59.84,44.75c-1.76.25-3.29.26-4.04.17l1.59,11.29-9.92,1.39-5.3-37.73c2.5-1.62,5.96-3.03,11.38-3.8,8.68-1.22,15.27,2.58,16.66,12.44,1.25,8.88-3.12,15.21-10.36,16.23Zm30.24,8.14c.25,1.75-1.07,3.49-2.82,3.74s-3.49-1.07-3.74-2.82l-2.38-16.92c-.26-1.87.95-3.48,2.82-3.74s3.48,1.01,3.74,2.82l2.38,16.92Zm-5.11-36.37c.25,1.81-1.01,3.48-2.82,3.74s-3.48-1.01-3.74-2.82l-2.38-16.92c-.25-1.81,1.01-3.48,2.82-3.74,1.75-.25,3.49,1.07,3.74,2.82l2.38,16.92Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116 116"><path fill="#f8f8f8" d="M45.16 48.35c-.74 0-1.28.13-1.75.27v17.43c.4.13.94.27 1.61.27 3.63 0 5.18-3.03 5.18-8.95s-1.35-9.02-5.05-9.02Z"/><path fill="#f8f8f8" d="M114.36 35.5c.74-.07 1.28-.61 1.28-1.35V.71c0-.74-.61-1.35-1.35-1.35H76.93v10.2c0 1.8-1.58 3.38-3.38 3.38s-3.38-1.58-3.38-3.38V-.63H.98C.24-.63-.36-.03-.36.71v33.44c0 .74.54 1.28 1.28 1.35 11.51.67 20.66 10.23 20.66 21.94S12.42 78.63.92 79.23c-.74.07-1.28.61-1.28 1.35v33.44c0 .74.61 1.35 1.35 1.35h69.19v-10.33c0-1.86 1.52-3.38 3.38-3.38s3.38 1.58 3.38 3.38v10.33h37.36c.74 0 1.35-.61 1.35-1.35V80.58c0-.74-.54-1.28-1.28-1.35-11.51-.67-20.66-10.16-20.66-21.87s9.15-21.26 20.66-21.87ZM47.51 72.24c-1.82 0-3.36-.2-4.1-.4v11.64H33.18V44.59c2.76-1.28 6.46-2.22 12.04-2.22 8.95 0 15.07 4.78 15.07 14.94 0 9.15-5.32 14.94-12.78 14.94Zm29.41 12.53c0 1.8-1.58 3.38-3.38 3.38s-3.38-1.58-3.38-3.38V67.33c0-1.93 1.45-3.38 3.38-3.38s3.38 1.52 3.38 3.38v17.44Zm0-37.49c0 1.86-1.52 3.38-3.38 3.38s-3.38-1.52-3.38-3.38V29.84c0-1.86 1.52-3.38 3.38-3.38s3.38 1.58 3.38 3.38v17.44Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Ebene_1"
viewBox="0 0 128 128"
version="1.1"
sodipodi:docname="pretix-icon-white-on-purple.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="4.125"
inkscape:cx="101.69697"
inkscape:cy="80.727273"
inkscape:window-width="2556"
inkscape:window-height="1275"
inkscape:window-x="0"
inkscape:window-y="144"
inkscape:window-maximized="1"
inkscape:current-layer="Ebene_1" /><defs
id="defs1"><style
id="style1">.cls-1{fill:#f8f8f8;}</style><style
id="style1-9">.cls-1{fill:#492267;}</style></defs><rect
style="fill:#492267;stroke-width:1.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:13.3;paint-order:fill markers stroke;fill-opacity:1"
id="rect2"
width="128"
height="128"
x="0"
y="0" /><path
class="cls-1"
d="M 53.800397,58.604437 C 53.249481,58.680953 52.8669,58.833985 52.530229,58.987018 L 54.351313,71.925899 C 54.665029,71.987112 55.078217,72.02537 55.575572,71.956506 58.26894,71.573925 59.102966,69.171318 58.490837,64.771639 57.855753,60.280141 56.554978,58.221856 53.808048,58.604437 Z"
id="path1"
style="stroke-width:0.765162" /><g
id="g2"
transform="matrix(0.76558824,0,0,0.76558824,15.00618,15.00618)"
style="fill:#f8f8f8;fill-opacity:1"><path
class="cls-1"
d="M 50.67,56.95 C 49.95,57.05 49.45,57.25 49.01,57.45 L 51.39,74.36 C 51.8,74.44 52.34,74.49 52.99,74.4 56.51,73.9 57.6,70.76 56.8,65.01 55.97,59.14 54.27,56.45 50.68,56.95 Z"
id="path1-3"
style="fill:#f8f8f8;fill-opacity:1" /><path
class="cls-1"
d="M 116.04,35.05 C 116.75,34.88 117.2,34.29 117.1,33.57 L 112.54,1.13 C 112.44,0.41 111.77,-0.09 111.05,0.01 L 73.55,5.28 74.28,10.5 C 74.44,11.62 73.66,12.65 72.54,12.81 71.42,12.97 70.39,12.19 70.23,11.07 L 69.5,5.85 1.13,15.46 C 0.41,15.56 -0.09,16.23 0.01,16.95 L 4.57,49.39 C 4.67,50.11 5.27,50.56 5.99,50.52 17.24,49.6 27.42,57.62 29.02,68.98 30.62,80.34 23.03,90.79 11.95,92.94 11.24,93.11 10.79,93.7 10.89,94.42 L 15.45,126.86 C 15.55,127.58 16.22,128.08 16.94,127.98 L 85.31,118.37 84.58,113.15 C 84.42,112 85.17,111 86.32,110.84 87.47,110.68 88.47,111.46 88.63,112.58 L 89.36,117.8 126.86,112.53 C 127.58,112.43 128.08,111.76 127.98,111.04 L 123.42,78.6 C 123.32,77.88 122.72,77.43 122,77.47 110.75,78.39 100.58,70.43 98.98,59.07 97.38,47.71 104.96,37.2 116.04,35.04 Z M 56.2,79.8 C 54.44,80.05 52.91,80.06 52.16,79.97 L 53.75,91.26 43.83,92.65 38.53,54.92 C 41.03,53.3 44.49,51.89 49.91,51.12 58.59,49.9 65.18,53.7 66.57,63.56 67.82,72.44 63.45,78.77 56.21,79.79 Z M 86.93,100.51 C 87.09,101.63 86.31,102.66 85.19,102.82 84.07,102.98 83.04,102.2 82.88,101.08 L 81.41,90.64 C 81.25,89.52 82.03,88.49 83.15,88.33 84.27,88.17 85.31,88.99 85.46,90.07 Z M 83.76,77.93 C 83.91,79.01 83.1,80.09 82.02,80.24 80.94,80.39 79.86,79.58 79.71,78.5 L 78.24,68.06 C 78.08,66.91 78.83,65.91 79.98,65.75 81.1,65.59 82.13,66.37 82.29,67.49 Z M 80.6,55.48 C 80.76,56.6 79.98,57.63 78.86,57.79 77.74,57.95 76.71,57.17 76.55,56.05 L 75.08,45.61 C 74.92,44.49 75.7,43.46 76.82,43.3 77.94,43.14 78.98,43.96 79.13,45.04 Z M 77.43,32.9 C 77.58,33.98 76.77,35.06 75.69,35.21 74.61,35.36 73.53,34.55 73.38,33.47 L 71.91,23.03 C 71.75,21.88 72.5,20.88 73.65,20.72 74.8,20.56 75.8,21.34 75.96,22.46 Z"
id="path2-6"
style="fill:#f8f8f8;fill-opacity:1" /></g></svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><style>.cls-1{fill:#492267;}</style></defs><path class="cls-1" d="m50.67,56.95c-.72.1-1.22.3-1.66.5l2.38,16.91c.41.08.95.13,1.6.04,3.52-.5,4.61-3.64,3.81-9.39-.83-5.87-2.53-8.56-6.12-8.06Z"/><path class="cls-1" d="m116.04,35.05c.71-.17,1.16-.76,1.06-1.48L112.54,1.13c-.1-.72-.77-1.22-1.49-1.12l-37.5,5.27.73,5.22c.16,1.12-.62,2.15-1.74,2.31s-2.15-.62-2.31-1.74l-.73-5.22L1.13,15.46c-.72.1-1.22.77-1.12,1.49l4.56,32.44c.1.72.7,1.17,1.42,1.13,11.25-.92,21.43,7.1,23.03,18.46,1.6,11.36-5.99,21.81-17.07,23.96-.71.17-1.16.76-1.06,1.48l4.56,32.44c.1.72.77,1.22,1.49,1.12l68.37-9.61-.73-5.22c-.16-1.15.59-2.15,1.74-2.31s2.15.62,2.31,1.74l.73,5.22,37.5-5.27c.72-.1,1.22-.77,1.12-1.49l-4.56-32.44c-.1-.72-.7-1.17-1.42-1.13-11.25.92-21.42-7.04-23.02-18.4-1.6-11.36,5.98-21.87,17.06-24.03Zm-59.84,44.75c-1.76.25-3.29.26-4.04.17l1.59,11.29-9.92,1.39-5.3-37.73c2.5-1.62,5.96-3.03,11.38-3.8,8.68-1.22,15.27,2.58,16.66,12.44,1.25,8.88-3.12,15.21-10.36,16.23Zm30.73,20.71c.16,1.12-.62,2.15-1.74,2.31-1.12.16-2.15-.62-2.31-1.74l-1.47-10.44c-.16-1.12.62-2.15,1.74-2.31s2.16.66,2.31,1.74l1.47,10.44Zm-3.17-22.58c.15,1.08-.66,2.16-1.74,2.31s-2.16-.66-2.31-1.74l-1.47-10.44c-.16-1.15.59-2.15,1.74-2.31,1.12-.16,2.15.62,2.31,1.74l1.47,10.44Zm-3.16-22.45c.16,1.12-.62,2.15-1.74,2.31-1.12.16-2.15-.62-2.31-1.74l-1.47-10.44c-.16-1.12.62-2.15,1.74-2.31s2.16.66,2.31,1.74l1.47,10.44Zm-3.17-22.58c.15,1.08-.66,2.16-1.74,2.31s-2.16-.66-2.31-1.74l-1.47-10.44c-.16-1.15.59-2.15,1.74-2.31s2.15.62,2.31,1.74l1.47,10.44Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" viewBox="0 0 109.594 109.594"><g transform="scale(.94461)"><path d="M45.52 48.98c-.74 0-1.28.13-1.75.27v17.43c.4.13.94.27 1.61.27 3.63 0 5.18-3.03 5.18-8.95s-1.35-9.02-5.05-9.02z" fill="#3b1c4a"/><path d="M114.72 36.13c.74-.07 1.28-.61 1.28-1.35V1.35c0-.74-.61-1.35-1.35-1.35H75.99v5.38c0 1.15-.94 2.09-2.09 2.09s-2.09-.94-2.09-2.09V0H1.35C.61 0 0 .61 0 1.35v33.44c0 .74.54 1.28 1.28 1.35 11.51.67 20.66 10.23 20.66 21.94s-9.15 21.2-20.66 21.8c-.74.07-1.28.61-1.28 1.35v33.44c0 .74.61 1.35 1.35 1.35h70.48v-5.38c0-1.19.9-2.09 2.09-2.09s2.09.94 2.09 2.09v5.38h38.66c.74 0 1.35-.61 1.35-1.35V81.23c0-.74-.54-1.28-1.28-1.35-11.51-.67-20.66-10.16-20.66-21.87s9.15-21.26 20.66-21.87zM47.87 72.87c-1.82 0-3.36-.2-4.1-.4v11.64H33.54V45.22C36.3 43.94 40 43 45.58 43c8.95 0 15.07 4.78 15.07 14.94 0 9.15-5.32 14.94-12.78 14.94zm28.12 25.3c0 1.15-.94 2.09-2.09 2.09s-2.09-.94-2.09-2.09V87.4c0-1.15.94-2.09 2.09-2.09s2.09.97 2.09 2.09zm0-23.28c0 1.11-.97 2.09-2.09 2.09-1.12 0-2.09-.97-2.09-2.09V64.12c0-1.19.9-2.09 2.09-2.09s2.09.94 2.09 2.09zm0-23.15c0 1.15-.94 2.09-2.09 2.09s-2.09-.94-2.09-2.09V40.97c0-1.15.94-2.09 2.09-2.09s2.09.97 2.09 2.09zm0-23.28c0 1.11-.97 2.09-2.09 2.09-1.12 0-2.09-.97-2.09-2.09V17.69c0-1.19.9-2.09 2.09-2.09s2.09.94 2.09 2.09z" fill="#3b1c4a"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,2 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 69.21"><path style="fill: #faf8fc;" d="M40.1,37.97c.37,2.66-.13,4.12-1.76,4.34-.3.04-.55.02-.74-.02l-1.1-7.82c.2-.09.44-.18.77-.23,1.66-.23,2.45,1.01,2.83,3.73ZM60.79,30.87c-1.36.19-1.91,1.53-1.47,3.78l3.61-1.06c-.25-2-.96-2.88-2.14-2.72ZM114.57,27.5c.74,5.26,5.45,8.94,10.65,8.51.34-.02.61.19.66.52l2.11,15.01c.05.33-.19.64-.52.69l-48.7,6.84h0l.15-.02-.34-2.42c-.08-.58-.62-.99-1.21-.91-.59.08-.99.61-.91,1.21l.34,2.42.15-.02h0L7.84,69.05c-.33.05-.64-.19-.69-.52l-2.11-15.01c-.05-.33.16-.61.49-.68,5.13-1,8.64-5.83,7.9-11.09-.74-5.26-5.45-8.97-10.66-8.54-.34.02-.61-.19-.66-.52L0,17.66c-.05-.33.19-.64.52-.69L69.49,7.28l.34,2.42c.08.58.62.99,1.21.91s.99-.62.91-1.21l-.34-2.42L120.16.16c.33-.05.64.19.69.52l2.11,15.01c.05.33-.16.61-.49.68-5.13,1-8.63,5.87-7.89,11.12ZM44.63,37.3c-.64-4.56-3.69-6.32-7.71-5.76-2.51.35-4.11,1.01-5.27,1.76l2.45,17.46,4.59-.65-.73-5.23c.35.04,1.05.04,1.87-.08,3.35-.47,5.37-3.4,4.8-7.51ZM53.46,29.28c-.46.03-.91.07-1.34.13-2.96.39-4.94,1.03-6.17,1.88l1.72,12.27,4.59-.65-1.44-10.24c.57-.3,1.39-.41,2.3-.11l.34-3.28ZM60.61,28.24c-4.08.57-5.77,3.68-5.22,7.57.55,3.9,3.19,6.45,7.45,5.85,2.42-.34,3.86-1.01,4.94-1.71l-1.42-2.67c-.67.46-1.84.97-3.38,1.18-1.66.23-2.48-.36-2.98-1.71l7.43-2.12c-.38-4.44-2.71-7.01-6.81-6.4ZM77.12,46.23c-.08-.56-.64-.99-1.21-.91-.58.08-.99.62-.91,1.21l.68,4.83c.08.58.62.99,1.21.91.58-.08.99-.62.91-1.21l-.68-4.83ZM75.65,35.77c-.08-.58-.62-.99-1.21-.91s-.99.61-.91,1.21l.68,4.83c.08.56.64.99,1.21.91.56-.08.99-.64.91-1.21l-.68-4.83ZM74.19,25.38c-.08-.56-.64-.99-1.21-.91s-.99.62-.91,1.21l.68,4.83c.08.58.62.99,1.21.91s.99-.62.91-1.21l-.68-4.83ZM72.73,14.93c-.08-.58-.62-.99-1.21-.91s-.99.61-.91,1.21l.68,4.83c.08.56.64.99,1.21.91s.99-.64.91-1.21l-.68-4.83ZM87.74,24.65l-1.87.26-.53-3.81-4.43,1.79.37,2.66-1.36.19.42,2.99,1.36-.19.87,6.22c.31,2.24,1.89,3.83,4.82,3.42,1.06-.15,1.83-.54,2.14-.76l-.39-2.81c-.35.17-.52.26-.85.3-.69.1-1.08-.22-1.21-1.18l-.82-5.83,1.87-.26h.03s-.42-3-.42-3ZM94.42,23.74l-4.59.65,1.83,12.99,4.59-.65-1.83-12.99ZM93.97,19.92c-.16-1.15-1.35-1.91-2.68-1.72-1.3.18-2.23,1.24-2.07,2.39s1.35,1.94,2.65,1.75c1.33-.19,2.25-1.27,2.09-2.42ZM110.72,34.73l-5.12-6.43,2.93-6.54-4.26.6-1.26,3.5h-.06s-2.03-3.03-2.03-3.03l-4.77.67,4.54,5.77-3.14,7.28,4.62-.65,1.22-3.84h.06s2.17,3.35,2.17,3.35l5.11-.69Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="254.156" height="109.594" version="1.1"><g transform="scale(1.9856)"><path d="M36.29 23.21c-.35 0-.61.06-.83.13v8.29c.19.06.45.13.77.13 1.73 0 2.46-1.44 2.46-4.26s-.64-4.29-2.4-4.29z" fill="#f8f8f8"/><path d="M61.22 23.15c-1.44 0-2.21 1.31-2.08 3.71l3.9-.58c.03-2.11-.58-3.14-1.82-3.14z" fill="#f8f8f8"/><path d="M127.39 17.1c.35-.03.61-.29.61-.64V.64c0-.35-.29-.64-.64-.64H75.94v2.48c0 .62-.5 1.12-1.12 1.12-.62 0-1.12-.5-1.12-1.12V0H.64C.29 0 0 .29 0 .64v15.82c0 .35.26.61.61.64 5.47.32 9.82 4.86 9.82 10.43 0 5.57-4.35 10.08-9.82 10.37-.35.03-.61.29-.61.64v15.82c0 .35.29.64.64.64h73.22-.16v-2.48c0-.63.49-1.12 1.12-1.12.63 0 1.12.5 1.12 1.12V55h-.16 51.58c.35 0 .64-.29.64-.64V38.54c0-.35-.26-.61-.61-.64-5.47-.32-9.82-4.83-9.82-10.4s4.35-10.11 9.82-10.4zM37.41 34.57c-.86 0-1.6-.1-1.95-.19v5.54H30.6v-18.5c1.31-.61 3.07-1.06 5.73-1.06 4.26 0 7.17 2.27 7.17 7.1 0 4.35-2.53 7.1-6.08 7.1zm15.58-10.78c-.9-.45-1.76-.45-2.4-.22v10.85h-4.86V21.43c1.41-.7 3.55-1.09 6.69-1.06.45 0 .93.03 1.41.06L53 23.79Zm14.56 4.26-8.03 1.12c.32 1.47 1.09 2.21 2.85 2.21 1.63 0 2.91-.35 3.68-.74l1.09 2.98c-1.22.58-2.82 1.06-5.38 1.06-4.51 0-6.88-3.04-6.88-7.17s2.21-7.1 6.53-7.1c4.35-.03 6.4 2.98 6.14 7.65zm8.38 18.56c0 .62-.5 1.12-1.12 1.12-.62 0-1.12-.5-1.12-1.12v-5.12c0-.62.5-1.12 1.12-1.12.62 0 1.12.52 1.12 1.12zm0-11.07c0 .6-.52 1.12-1.12 1.12-.6 0-1.12-.52-1.12-1.12v-5.12c0-.63.49-1.12 1.12-1.12.63 0 1.12.5 1.12 1.12zm0-11.01c0 .62-.5 1.12-1.12 1.12-.62 0-1.12-.5-1.12-1.12v-5.12c0-.62.5-1.12 1.12-1.12.62 0 1.12.52 1.12 1.12zm0-11.07c0 .6-.52 1.12-1.12 1.12-.6 0-1.12-.52-1.12-1.12V8.34c0-.63.49-1.12 1.12-1.12.63 0 1.12.5 1.12 1.12zM90.11 23.8h-2.02v6.18c0 1.02.35 1.41 1.09 1.41.35 0 .54-.06.93-.19v2.98c-.35.19-1.22.48-2.34.48-3.1 0-4.51-1.89-4.51-4.26v-6.59h-1.44v-3.17h1.44v-2.82l4.86-1.22v4.03h1.98v3.17zm7.07 10.62h-4.86V20.66h4.86zm-2.43-15.58c-1.38 0-2.5-.99-2.5-2.21s1.12-2.18 2.5-2.18 2.53.96 2.53 2.18c0 1.22-1.12 2.21-2.53 2.21zm12.35 15.58-1.76-3.81h-.06l-1.82 3.81h-4.9l4.32-7.1-3.87-6.66h5.06l1.66 3.46h.06l1.82-3.46h4.51l-4 6.37 4.38 7.42-5.41-.03z" fill="#f8f8f8"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Ebene_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 70"><defs><style>.cls-1{fill:#492267;}</style></defs><path class="cls-1" d="m37.27,34.63c-.33.05-.57.14-.77.23l1.1,7.82c.19.03.44.06.74.02,1.63-.23,2.14-1.69,1.76-4.34-.38-2.72-1.17-3.96-2.83-3.73Z"/><path class="cls-1" d="m60.79,31.26c-1.36.19-1.91,1.53-1.47,3.78l3.61-1.06c-.25-2-.96-2.88-2.14-2.72Z"/><path class="cls-1" d="m122.47,16.77c.33-.08.54-.35.49-.68l-2.11-15.01c-.05-.33-.36-.57-.69-.52l-48.67,6.84.34,2.42c.07.52-.29,1-.8,1.07s-1-.29-1.07-.8l-.34-2.42L.53,17.37c-.33.05-.57.36-.52.69l2.11,15.01c.05.33.32.54.66.52,5.21-.42,9.92,3.29,10.66,8.54.74,5.26-2.77,10.09-7.9,11.09-.33.08-.54.35-.49.68l2.11,15.01c.05.33.36.57.69.52l69.12-9.71h-.03s-.34-2.41-.34-2.41c-.08-.53.27-.99.8-1.07s1,.29,1.07.8l.34,2.42h-.03s48.7-6.84,48.7-6.84c.33-.05.57-.36.52-.69l-2.11-15.01c-.05-.33-.32-.54-.66-.52-5.21.42-9.92-3.26-10.65-8.51-.74-5.26,2.77-10.12,7.89-11.12Zm-82.63,28.43c-.82.11-1.52.12-1.87.08l.73,5.23-4.59.65-2.45-17.46c1.16-.75,2.76-1.4,5.27-1.76,4.02-.56,7.07,1.19,7.71,5.76.58,4.11-1.44,7.04-4.8,7.51Zm13.28-12.25c-.91-.3-1.72-.19-2.3.11l1.44,10.24-4.59.65-1.72-12.27c1.24-.85,3.21-1.5,6.17-1.88.42-.06.88-.09,1.34-.13l-.34,3.28Zm14.31,2.09l-7.43,2.12c.5,1.35,1.32,1.94,2.98,1.71,1.54-.22,2.7-.72,3.38-1.18l1.42,2.67c-1.07.71-2.52,1.37-4.94,1.71-4.26.6-6.9-1.96-7.45-5.85-.55-3.9,1.14-7,5.22-7.57,4.1-.61,6.44,1.96,6.81,6.4Zm10.26,16.43c.07.52-.29,1-.8,1.07s-1-.29-1.07-.8l-.68-4.83c-.07-.52.29-1,.8-1.07s1,.31,1.07.8l.68,4.83Zm-1.47-10.45c.07.5-.31,1-.8,1.07s-1-.31-1.07-.8l-.68-4.83c-.08-.53.27-.99.8-1.07s1,.29,1.07.8l.68,4.83Zm-1.46-10.39c.07.52-.29,1-.8,1.07s-1-.29-1.07-.8l-.68-4.83c-.07-.52.29-1,.8-1.07s1,.31,1.07.8l.68,4.83Zm-1.47-10.45c.07.5-.31,1-.8,1.07s-1-.31-1.07-.8l-.68-4.83c-.08-.53.27-.99.8-1.07s1,.29,1.07.8l.68,4.83Zm14.88,7.86h-.03s-1.87.27-1.87.27l.82,5.83c.14.97.52,1.28,1.21,1.18.33-.05.51-.13.85-.3l.39,2.81c-.31.23-1.08.61-2.14.76-2.93.41-4.51-1.18-4.82-3.42l-.87-6.22-1.36.19-.42-2.99,1.36-.19-.37-2.66,4.43-1.79.53,3.81,1.87-.26.42,2.99Zm8.09,9.09l-4.59.65-1.83-12.99,4.59-.65,1.83,12.99Zm-4.36-14.39c-1.3.18-2.49-.61-2.65-1.75-.16-1.15.77-2.2,2.07-2.39s2.51.57,2.68,1.72-.76,2.23-2.09,2.42Zm13.73,13.07l-2.17-3.36h-.06s-1.22,3.85-1.22,3.85l-4.62.65,3.14-7.28-4.54-5.77,4.77-.67,2.03,3.04h.06s1.26-3.51,1.26-3.51l4.26-.6-2.93,6.54,5.12,6.43-5.11.69Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="254.156" height="109.594" version="1.1"><g transform="matrix(1.9856 0 0 1.9856 0 .193)"><path d="M36.29 23.21c-.35 0-.61.06-.83.13v8.29c.19.06.45.13.77.13 1.73 0 2.46-1.44 2.46-4.26s-.64-4.29-2.4-4.29z" fill="#3b1c4a"/><path d="M61.22 23.15c-1.44 0-2.21 1.31-2.08 3.71l3.9-.58c.03-2.11-.58-3.14-1.82-3.14z" fill="#3b1c4a"/><path d="M127.39 17.1c.35-.03.61-.29.61-.64V.64c0-.35-.29-.64-.64-.64H75.81v2.48c0 .55-.45.99-.99.99s-.99-.45-.99-.99V0H.64C.29 0 0 .29 0 .64v15.82c0 .35.26.61.61.64 5.47.32 9.82 4.86 9.82 10.43 0 5.57-4.35 10.08-9.82 10.37-.35.03-.61.29-.61.64v15.82c0 .35.29.64.64.64h73.22-.03v-2.48c0-.57.43-.99.99-.99s.99.45.99.99V55h-.03 51.58c.35 0 .64-.29.64-.64V38.54c0-.35-.26-.61-.61-.64-5.47-.32-9.82-4.83-9.82-10.4s4.35-10.11 9.82-10.4zM37.41 34.57c-.86 0-1.6-.1-1.95-.19v5.54H30.6v-18.5c1.31-.61 3.07-1.06 5.73-1.06 4.26 0 7.17 2.27 7.17 7.1 0 4.35-2.53 7.1-6.08 7.1zm15.58-10.78c-.9-.45-1.76-.45-2.4-.22v10.85h-4.86V21.43c1.41-.7 3.55-1.09 6.69-1.06.45 0 .93.03 1.41.06L53 23.79Zm14.56 4.26-8.03 1.12c.32 1.47 1.09 2.21 2.85 2.21 1.63 0 2.91-.35 3.68-.74l1.09 2.98c-1.22.58-2.82 1.06-5.38 1.06-4.51 0-6.88-3.04-6.88-7.17s2.21-7.1 6.53-7.1c4.35-.03 6.4 2.98 6.14 7.65zm8.26 18.56c0 .55-.45.99-.99.99s-.99-.45-.99-.99v-5.12c0-.55.45-.99.99-.99s.99.46.99.99zm0-11.07c0 .53-.46.99-.99.99s-.99-.46-.99-.99v-5.12c0-.57.43-.99.99-.99s.99.45.99.99zm0-11.01c0 .55-.45.99-.99.99s-.99-.45-.99-.99v-5.12c0-.55.45-.99.99-.99s.99.46.99.99zm0-11.07c0 .53-.46.99-.99.99s-.99-.46-.99-.99V8.34c0-.57.43-.99.99-.99s.99.45.99.99zm14.3 10.34h-2.02v6.18c0 1.02.35 1.41 1.09 1.41.35 0 .54-.06.93-.19v2.98c-.35.19-1.22.48-2.34.48-3.1 0-4.51-1.89-4.51-4.26v-6.59h-1.44v-3.17h1.44v-2.82l4.86-1.22v4.03h1.98v3.17zm7.07 10.62h-4.86V20.66h4.86zm-2.43-15.58c-1.38 0-2.5-.99-2.5-2.21s1.12-2.18 2.5-2.18 2.53.96 2.53 2.18c0 1.22-1.12 2.21-2.53 2.21zm12.35 15.58-1.76-3.81h-.06l-1.82 3.81h-4.9l4.32-7.1-3.87-6.66h5.06l1.66 3.46h.06l1.82-3.46h4.51l-4 6.37 4.38 7.42-5.41-.03z" fill="#3b1c4a"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 135.467 135.467"><g transform="matrix(7.9501 0 0 7.9501 -115.332 -24.995)"><rect width="17.04" height="17.04" x="14.507" y="3.144" fill="#492267" color="#000" overflow="visible" rx="1.664" ry="1.664" style="marker:none"/><g fill="#fff"><path d="M17.702 6.339v2.13h-1.065v-2.13c0-.588.477-1.065 1.065-1.065h2.13v1.065h-2.13m10.65-1.065c.588 0 1.065.477 1.065 1.065v2.13h-1.065v-2.13h-2.13V5.274h2.13m-10.65 9.585v2.13h2.13v1.065h-2.13a1.065 1.065 0 0 1-1.065-1.065v-2.13h1.065m10.65 2.13v-2.13h1.065v2.13c0 .588-.477 1.065-1.065 1.065h-2.13v-1.065z"/><path d="M22.606 7.253c-1.563 0-2.6.256-3.367.617v10.867h2.856V15.49c.21.06.631.12 1.142.12 2.09 0 3.577-1.623 3.577-4.178 0-2.84-1.698-4.179-4.208-4.179zm-.015 1.668c1.037 0 1.398.828 1.398 2.526 0 1.653-.42 2.494-1.443 2.494a1.17 1.17 0 0 1-.451-.075v-4.87c.135-.045.286-.075.496-.075z" color="#000" overflow="visible" style="marker:none" transform="matrix(.53249 0 0 .53249 10.765 5.453)"/></g></g><path fill="none" d="M-115.332-24.995H75.47v190.802h-190.802z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 135.467 135.467"><g transform="matrix(7.9501 0 0 7.9501 -115.332 -24.995)"><rect width="17.04" height="17.04" x="14.507" y="3.144" fill="#3b1c4a" color="#000" overflow="visible" rx="1.664" ry="1.664" style="marker:none"/><g fill="#fff"><path d="M17.702 6.339v2.13h-1.065v-2.13c0-.588.477-1.065 1.065-1.065h2.13v1.065h-2.13m10.65-1.065c.588 0 1.065.477 1.065 1.065v2.13h-1.065v-2.13h-2.13V5.274h2.13m-10.65 9.585v2.13h2.13v1.065h-2.13a1.065 1.065 0 0 1-1.065-1.065v-2.13h1.065m10.65 2.13v-2.13h1.065v2.13c0 .588-.477 1.065-1.065 1.065h-2.13v-1.065z"/><path d="M22.606 7.253c-1.563 0-2.6.256-3.367.617v10.867h2.856V15.49c.21.06.631.12 1.142.12 2.09 0 3.577-1.623 3.577-4.178 0-2.84-1.698-4.179-4.208-4.179zm-.015 1.668c1.037 0 1.398.828 1.398 2.526 0 1.653-.42 2.494-1.443 2.494a1.17 1.17 0 0 1-.451-.075v-4.87c.135-.045.286-.075.496-.075z" color="#000" overflow="visible" style="marker:none" transform="matrix(.53249 0 0 .53249 10.765 5.453)"/></g></g><path fill="none" d="M-115.332-24.995H75.47v190.802h-190.802z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -67,7 +67,7 @@ $panel-success-heading-bg: var(--pretix-brand-success-tint-50);
$panel-danger-border: var(--pretix-brand-danger-tint-50);
$panel-danger-heading-bg: var(--pretix-brand-danger-tint-50);
$panel-warning-border: var(--pretix-brand-warning-tint-50);
$panel-warning-heading-bg: var(--pretix-brand-warning-tint-50);
$panel-warning-heading-bg: var(--pretix-brand-warning-tine-50);
$panel-default-border: #e5e5e5 !default;
$panel-default-heading-bg: #e5e5e5 !default;

View File

@@ -216,20 +216,6 @@ td > .form-group > .checkbox {
.input-group-btn .btn {
padding-bottom: 8px;
}
.subevent-selection{
.splitdatetimerow{
max-width: 500px;
display: inline-block;
}
.spacer{
margin-left: 20px;
margin-right: 20px;
}
.select2{
max-width:500px;
display: inline-block;
}
}
.reldatetime {
input[type=text], select {
display: inline-block;

View File

@@ -36,21 +36,10 @@ nav.navbar {
margin: 0;
border: 0;
}
.navbar-brand {
position: relative;
padding-bottom: 10px;
padding-top: 10px;
padding-left: 10px;
}
.navbar-brand span {
padding: 4px 8px 6px; // Optical alignment next to logo
display: inline-block;
}
.navbar-brand img {
width: auto;
height: 100%;
display: inline-block;
vertical-align: top;
width: auto;
display: inline;
}
#side-menu img.fa-img {

View File

@@ -32,20 +32,23 @@
# 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.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, User
from pretix.base.models import Event, InvoiceAddress, Order, Organizer, User
from pretix.base.services.mail import mail
@@ -67,6 +70,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.UTC),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.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 = []
@@ -188,7 +230,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,
@@ -197,25 +239,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
)
@@ -231,7 +274,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,
@@ -242,9 +285,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
@@ -257,11 +300,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(
@@ -272,3 +317,141 @@ def test_placeholder_html_rendering_from_string(env):
r'URL with text: <a href="https://google.com" rel="noopener" style="[^"]+" target="_blank">Test</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}})