Compare commits

...

8 Commits

Author SHA1 Message Date
Raphael Michel
423e64a348 Bump to 4.5.2 2022-02-28 16:13:57 +01:00
Raphael Michel
69a32e2274 Pin django-countries to 7.2 2022-02-28 16:13:32 +01:00
Raphael Michel
25eaf8d625 [SECURITY] Fix stored XSS in help texts 2022-02-28 16:13:25 +01:00
Raphael Michel
ae21ad02a3 [SECURITY] Fix stored XSS in question errors 2022-02-28 16:10:58 +01:00
Raphael Michel
10c76c6391 [SECURITY] Prevent untrusted values from creating Excel formulas 2022-02-28 16:10:58 +01:00
Raphael Michel
e116299311 Bump to 4.5.1 2022-01-26 13:42:32 +01:00
Raphael Michel
6f14fb176e [SECURITY] Make redirect view dependent on referer 2022-01-26 13:41:13 +01:00
Raphael Michel
b03daab452 [SECURITY] Fix (non-exploitable) XSS issue 2022-01-26 13:41:13 +01:00
17 changed files with 247 additions and 43 deletions

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 # 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/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "4.5.0" __version__ = "4.5.2"

View File

@@ -33,7 +33,6 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import io import io
import re
import tempfile import tempfile
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
from decimal import Decimal from decimal import Decimal
@@ -46,26 +45,13 @@ from django.conf import settings
from django.db.models import QuerySet from django.db.models import QuerySet
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, Cell
from pretix.base.models import Event from pretix.base.models import Event
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
def excel_safe(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
class BaseExporter: class BaseExporter:
@@ -228,7 +214,7 @@ class ListExporter(BaseExporter):
pass pass
def _render_xlsx(self, form_data, output_file=None): def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True) wb = SafeWorkbook(write_only=True)
ws = wb.create_sheet() ws = wb.create_sheet()
self.prepare_xlsx_sheet(ws) self.prepare_xlsx_sheet(ws)
try: try:
@@ -242,7 +228,7 @@ class ListExporter(BaseExporter):
total = line.total total = line.total
continue continue
ws.append([ ws.append([
excel_safe(val) for val in line val for val in line
]) ])
if total: if total:
counter += 1 counter += 1
@@ -347,7 +333,7 @@ class MultiSheetListExporter(ListExporter):
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8") return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data, output_file=None): def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True) wb = SafeWorkbook(write_only=True)
n_sheets = len(self.sheets) n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets): for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l)) ws = wb.create_sheet(str(l))
@@ -361,8 +347,7 @@ class MultiSheetListExporter(ListExporter):
total = line.total total = line.total
continue continue
ws.append([ ws.append([
excel_safe(val) val for val in line
for val in line
]) ])
if total: if total:
counter += 1 counter += 1

View File

@@ -674,7 +674,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required, label=label, required=required,
min_value=q.valid_number_min or Decimal('0.00'), min_value=q.valid_number_min or Decimal('0.00'),
max_value=q.valid_number_max, max_value=q.valid_number_max,
help_text=q.help_text, help_text=help_text,
initial=initial.answer if initial else None, initial=initial.answer if initial else None,
) )
elif q.type == Question.TYPE_STRING: elif q.type == Question.TYPE_STRING:

View File

@@ -0,0 +1,26 @@
{% extends "error.html" %}
{% load i18n %}
{% load rich_text %}
{% load static %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
Please only proceed if you trust this website to be safe.
{% endblocktrans %}
</h3>
<p>
<a href="{{ url }}" class="btn btn-primary btn-lg">
{% blocktrans trimmed with host=hostname %}
Proceed to {{ host }}
{% endblocktrans %}
</a>
</p>
</div>
{% endblock %}

View File

@@ -24,6 +24,21 @@ import urllib.parse
from django.core import signing from django.core import signing
from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.shortcuts import render
def _is_samesite_referer(request):
referer = request.META.get('HTTP_REFERER')
if referer is None:
return False
referer = urllib.parse.urlparse(referer)
# Make sure we have a valid URL for Referer.
if '' in (referer.scheme, referer.netloc):
return False
return (referer.scheme, referer.netloc) == (request.scheme, request.get_host())
def redir_view(request): def redir_view(request):
@@ -32,6 +47,14 @@ def redir_view(request):
url = signer.unsign(request.GET.get('url', '')) url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature: except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter') return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
return render(request, 'pretixbase/redirect.html', {
'hostname': u.hostname,
'url': url,
})
r = HttpResponseRedirect(url) r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex' r['X-Robots-Tag'] = 'noindex'
return r return r

View File

@@ -74,17 +74,17 @@
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }} {{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if c.type == "exit" %} {% if c.type == "exit" %}
{% if c.auto_checked_in %} {% if c.auto_checked_in %}
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html" <span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span> title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% endif %} {% endif %}
{% elif c.forced and c.successful %} {% elif c.forced and c.successful %}
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html" <span class="fa fa-fw fa-warning" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span> title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
{% elif c.forced and not c.successful %} {% elif c.forced and not c.successful %}
<br> <br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small> <small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% elif c.auto_checked_in %} {% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html" <span class="fa fa-fw fa-magic" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span> title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% endif %} {% endif %}
</td> </td>

View File

@@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<div class="quotabox availability" data-toggle="tooltip_html" data-placement="top" <div class="quotabox availability" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}"> title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
{% if q.size|default_if_none:"NONE" == "NONE" %} {% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress"> <div class="progress">
<div class="progress-bar progress-bar-success progress-bar-100"> <div class="progress-bar progress-bar-success progress-bar-100">

View File

@@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<a class="quotabox" data-toggle="tooltip_html" data-placement="top" <a class="quotabox" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}" title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
href="{% url "control:event.items.quotas.show" event=q.event.slug organizer=q.event.organizer.slug quota=q.pk %}"> href="{% url "control:event.items.quotas.show" event=q.event.slug organizer=q.event.organizer.slug quota=q.pk %}">
{% if q.size|default_if_none:"NONE" == "NONE" %} {% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress"> <div class="progress">

View File

@@ -360,19 +360,19 @@
{% if line.checkins.all %} {% if line.checkins.all %}
{% for c in line.all_checkins.all %} {% for c in line.all_checkins.all %}
{% if not c.successful %} {% if not c.successful %}
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span> <span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.type == "exit" %} {% elif c.type == "exit" %}
{% if c.auto_checked_in %} {% if c.auto_checked_in %}
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span> <span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% else %} {% else %}
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span> <span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %} {% endif %}
{% elif c.forced %} {% elif c.forced %}
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span> <span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.auto_checked_in %} {% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span> <span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% else %} {% else %}
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span> <span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@@ -0,0 +1,113 @@
import re
from inspect import isgenerator
from openpyxl import Workbook
from openpyxl.cell.cell import (
ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, TIME_TYPES, TYPE_FORMULA, TYPE_STRING,
Cell,
)
from openpyxl.compat import NUMERIC_TYPES
from openpyxl.utils import column_index_from_string
from openpyxl.utils.exceptions import ReadOnlyWorkbookException
from openpyxl.worksheet._write_only import WriteOnlyWorksheet
from openpyxl.worksheet.worksheet import Worksheet
SAFE_TYPES = NUMERIC_TYPES + TIME_TYPES + (bool, type(None))
"""
This module provides a safer version of openpyxl's `Workbook` class to generate XLSX files from
user-generated data using `WriteOnlyWorksheet` and `ws.append()`. We commonly use these methods
to output e.g. order data, which contains data from untrusted sources such as attendee names.
There are mainly two problems this solves:
- It makes sure strings starting with = are treated as text, not as a formula, as openpyxl will
otherwise assume, which can be used for remote code execution.
- It removes characters considered invalid by Excel to avoid exporter crashes.
"""
def remove_invalid_excel_chars(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
def SafeCell(*args, value=None, **kwargs):
value = remove_invalid_excel_chars(value)
c = Cell(*args, value=value, **kwargs)
if c.data_type == TYPE_FORMULA:
c.data_type = TYPE_STRING
return c
class SafeAppendMixin:
def append(self, iterable):
row_idx = self._current_row + 1
if isinstance(iterable, (list, tuple, range)) or isgenerator(iterable):
for col_idx, content in enumerate(iterable, 1):
if isinstance(content, Cell):
# compatible with write-only mode
cell = content
if cell.parent and cell.parent != self:
raise ValueError("Cells cannot be copied from other worksheets")
cell.parent = self
cell.column = col_idx
cell.row = row_idx
else:
cell = SafeCell(self, row=row_idx, column=col_idx, value=remove_invalid_excel_chars(content))
self._cells[(row_idx, col_idx)] = cell
elif isinstance(iterable, dict):
for col_idx, content in iterable.items():
if isinstance(col_idx, str):
col_idx = column_index_from_string(col_idx)
cell = SafeCell(self, row=row_idx, column=col_idx, value=content)
self._cells[(row_idx, col_idx)] = cell
else:
self._invalid_row(iterable)
self._current_row = row_idx
class SafeWriteOnlyWorksheet(SafeAppendMixin, WriteOnlyWorksheet):
pass
class SafeWorksheet(SafeAppendMixin, Worksheet):
pass
class SafeWorkbook(Workbook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._sheets:
# monkeypatch existing sheets
for s in self._sheets:
s.append = SafeAppendMixin.append
def create_sheet(self, title=None, index=None):
if self.read_only:
raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
if self.write_only:
new_ws = SafeWriteOnlyWorksheet(parent=self, title=title)
else:
new_ws = SafeWorksheet(parent=self, title=title)
self._add_sheet(sheet=new_ws, index=index)
return new_ws

View File

@@ -3,7 +3,7 @@
<div class="form-horizontal stripe-container"> <div class="form-horizontal stripe-container">
{% if is_moto %} {% if is_moto %}
<h1> <h1>
<span class="label label-info pull-right flip" data-toggle="tooltip_html" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span> <span class="label label-info pull-right flip" data-toggle="tooltip" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span>
</h1> </h1>
<div class="clearfix"></div> <div class="clearfix"></div>
{% endif %} {% endif %}

View File

@@ -51,6 +51,7 @@ from pretix.base.forms.questions import (
guess_country, guess_country,
) )
from pretix.base.i18n import get_babel_locale, language from pretix.base.i18n import get_babel_locale, language
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.validators import EmailBanlistValidator from pretix.base.validators import EmailBanlistValidator
from pretix.presale.signals import contact_form_fields from pretix.presale.signals import contact_form_fields
@@ -89,7 +90,7 @@ class ContactForm(forms.Form):
self.fields['phone'] = PhoneNumberField( self.fields['phone'] = PhoneNumberField(
label=_('Phone number'), label=_('Phone number'),
required=self.event.settings.order_phone_required, required=self.event.settings.order_phone_required,
help_text=self.event.settings.checkout_phone_helptext, help_text=rich_text(self.event.settings.checkout_phone_helptext),
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just # We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
# a country code but no number as an initial value. It's a bit hacky, but should be stable for # a country code but no number as an initial value. It's a bit hacky, but should be stable for
# the future. # the future.
@@ -102,7 +103,7 @@ class ContactForm(forms.Form):
# is an autofocus field. Who would have thought… See e.g. here: # is an autofocus field. Who would have thought… See e.g. here:
# https://floatboxjs.com/forum/topic.php?post=8440&usebb_sid=2e116486a9ec6b7070e045aea8cded5b#post8440 # https://floatboxjs.com/forum/topic.php?post=8440&usebb_sid=2e116486a9ec6b7070e045aea8cded5b#post8440
self.fields['email'].widget.attrs['autofocus'] = 'autofocus' self.fields['email'].widget.attrs['autofocus'] = 'autofocus'
self.fields['email'].help_text = self.event.settings.checkout_email_helptext self.fields['email'].help_text = rich_text(self.event.settings.checkout_email_helptext)
responses = contact_form_fields.send(self.event, request=self.request) responses = contact_form_fields.send(self.event, request=self.request)
for r, response in responses: for r, response in responses:

View File

@@ -30,6 +30,7 @@ from pretix.base.forms.questions import (
) )
from pretix.base.i18n import get_babel_locale, language from pretix.base.i18n import get_babel_locale, language
from pretix.base.models import Quota, WaitingListEntry from pretix.base.models import Quota, WaitingListEntry
from pretix.base.templatetags.rich_text import rich_text
from pretix.presale.views.event import get_grouped_items from pretix.presale.views.event import get_grouped_items
@@ -105,7 +106,7 @@ class WaitingListForm(forms.ModelForm):
self.fields['phone'] = PhoneNumberField( self.fields['phone'] = PhoneNumberField(
label=_("Phone number"), label=_("Phone number"),
required=event.settings.waiting_list_phones_required, required=event.settings.waiting_list_phones_required,
help_text=event.settings.waiting_list_phones_explanation_text, help_text=rich_text(event.settings.waiting_list_phones_explanation_text),
widget=WrappedPhoneNumberPrefixWidget() widget=WrappedPhoneNumberPrefixWidget()
) )
else: else:

View File

@@ -675,7 +675,21 @@ $(function () {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="tooltip_html"]').tooltip({ $('[data-toggle="tooltip_html"]').tooltip({
'html': true 'html': true,
'whiteList': {
// Global attributes allowed on any supplied element below.
'*': ['class', 'dir', 'id', 'lang', 'role'],
b: [],
br: [],
code: [],
div: [], // required for template
h3: ['class', 'role'], // required for template
i: [],
small: [],
span: [],
strong: [],
u: [],
}
}); });
var url = document.location.toString(); var url = document.location.toString();

View File

@@ -213,7 +213,10 @@ $(function () {
// multi-input fields have a role=group with aria-labelledby // multi-input fields have a role=group with aria-labelledby
var label = this.hasAttribute("aria-labelledby") ? $("#" + this.getAttribute("aria-labelledby")) : $("[for="+target.attr("id")+"]"); var label = this.hasAttribute("aria-labelledby") ? $("#" + this.getAttribute("aria-labelledby")) : $("[for="+target.attr("id")+"]");
content.append("<li><a href='#" + target.attr("id") + "'>" + label.get(0).childNodes[0].nodeValue + "</a>: "+desc.text()+"</li>"); var $li = $("<li>");
$li.text(": " + desc.text())
$li.prepend($("<a>").attr("href", "#" + target.attr("id")).text(label.get(0).childNodes[0].nodeValue))
content.append($li);
}); });
$(this).append(content); $(this).append(content);
}); });

View File

@@ -172,7 +172,7 @@ setup(
'Django==3.2.*', 'Django==3.2.*',
'django-bootstrap3==15.0.*', 'django-bootstrap3==15.0.*',
'django-compressor==2.4.*', 'django-compressor==2.4.*',
'django-countries>=7.2', 'django-countries==7.2.*',
'django-filter==21.1', 'django-filter==21.1',
'django-formset-js-improved==0.5.0.2', 'django-formset-js-improved==0.5.0.2',
'django-formtools==2.3', 'django-formtools==2.3',

View File

@@ -0,0 +1,38 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io 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 openpyxl.cell.cell import TYPE_STRING
from pretix.helpers.safe_openpyxl import SafeWorkbook
def test_nullbyte_removed():
wb = SafeWorkbook()
ws = wb.create_sheet()
ws.append(["foo\u0000bar"])
assert ws.cell(1, 1).value == "foobar"
def test_no_formulas():
wb = SafeWorkbook()
ws = wb.create_sheet()
ws.append(["=1+1"])
assert ws.cell(1, 1).data_type == TYPE_STRING