mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
5 Commits
order-sear
...
v4.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
013da0a6aa | ||
|
|
a7c59374db | ||
|
|
23e1dac5da | ||
|
|
5cd8845728 | ||
|
|
34dfc35032 |
@@ -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__ = "4.6.0"
|
||||
__version__ = "4.6.1"
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import io
|
||||
import re
|
||||
import tempfile
|
||||
from collections import OrderedDict, namedtuple
|
||||
from decimal import Decimal
|
||||
@@ -46,26 +45,13 @@ from django.conf import settings
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.formats import localize
|
||||
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.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
|
||||
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
|
||||
|
||||
|
||||
class BaseExporter:
|
||||
@@ -228,7 +214,7 @@ class ListExporter(BaseExporter):
|
||||
pass
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook(write_only=True)
|
||||
wb = SafeWorkbook(write_only=True)
|
||||
ws = wb.create_sheet()
|
||||
self.prepare_xlsx_sheet(ws)
|
||||
try:
|
||||
@@ -242,7 +228,7 @@ class ListExporter(BaseExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
excel_safe(val) for val in line
|
||||
val for val in line
|
||||
])
|
||||
if total:
|
||||
counter += 1
|
||||
@@ -347,7 +333,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
def _render_xlsx(self, form_data, output_file=None):
|
||||
wb = Workbook(write_only=True)
|
||||
wb = SafeWorkbook(write_only=True)
|
||||
n_sheets = len(self.sheets)
|
||||
for i_sheet, (s, l) in enumerate(self.sheets):
|
||||
ws = wb.create_sheet(str(l))
|
||||
@@ -361,8 +347,7 @@ class MultiSheetListExporter(ListExporter):
|
||||
total = line.total
|
||||
continue
|
||||
ws.append([
|
||||
excel_safe(val)
|
||||
for val in line
|
||||
val for val in line
|
||||
])
|
||||
if total:
|
||||
counter += 1
|
||||
|
||||
@@ -692,7 +692,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=label, required=required,
|
||||
min_value=q.valid_number_min or Decimal('0.00'),
|
||||
max_value=q.valid_number_max,
|
||||
help_text=q.help_text,
|
||||
help_text=help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
|
||||
113
src/pretix/helpers/safe_openpyxl.py
Normal file
113
src/pretix/helpers/safe_openpyxl.py
Normal 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
|
||||
@@ -47,6 +47,7 @@ from pretix.base.forms.questions import (
|
||||
BaseInvoiceAddressForm, BaseQuestionsForm, WrappedPhoneNumberPrefixWidget,
|
||||
guess_phone_prefix,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.base.validators import EmailBanlistValidator
|
||||
from pretix.presale.signals import contact_form_fields
|
||||
|
||||
@@ -82,7 +83,7 @@ class ContactForm(forms.Form):
|
||||
self.fields['phone'] = PhoneNumberField(
|
||||
label=_('Phone number'),
|
||||
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),
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
|
||||
@@ -91,7 +92,7 @@ class ContactForm(forms.Form):
|
||||
# is an autofocus field. Who would have thought… See e.g. here:
|
||||
# https://floatboxjs.com/forum/topic.php?post=8440&usebb_sid=2e116486a9ec6b7070e045aea8cded5b#post8440
|
||||
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)
|
||||
for r, response in responses:
|
||||
|
||||
@@ -28,6 +28,7 @@ from pretix.base.forms.questions import (
|
||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget, guess_phone_prefix,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -99,7 +100,7 @@ class WaitingListForm(forms.ModelForm):
|
||||
self.fields['phone'] = PhoneNumberField(
|
||||
label=_("Phone number"),
|
||||
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()
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -219,7 +219,10 @@ $(function () {
|
||||
// multi-input fields have a role=group with aria-labelledby
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -172,7 +172,7 @@ setup(
|
||||
'Django==3.2.*',
|
||||
'django-bootstrap3==15.0.*',
|
||||
'django-compressor==2.4.*',
|
||||
'django-countries>=7.2',
|
||||
'django-countries==7.2.*',
|
||||
'django-filter==21.1',
|
||||
'django-formset-js-improved==0.5.0.2',
|
||||
'django-formtools==2.3',
|
||||
|
||||
38
src/tests/helpers/test_safe_openpyxl.py
Normal file
38
src/tests/helpers/test_safe_openpyxl.py
Normal 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
|
||||
Reference in New Issue
Block a user