Compare commits

...

6 Commits

Author SHA1 Message Date
Raphael Michel
36bbd8b5e4 Remove unused import 2024-06-10 15:46:46 +02:00
Mira Weller
2bfacd925a rename parameter 2024-06-04 14:37:44 +02:00
Mira Weller
795dd64219 simplify placeholder validation 2024-06-04 14:33:57 +02:00
Mira Weller
0882bd9db0 let SafeFormatter optionally raise on missing key 2024-06-04 14:29:27 +02:00
Mira Weller
d3f1f02beb simplify SafeFormatter (skip attribute access code path altogether instead of blocklisting characters) 2024-06-04 14:28:09 +02:00
Mira Weller
7b0f7439f0 Improve validation of email templates 2024-05-29 18:40:18 +02:00
3 changed files with 38 additions and 36 deletions

View File

@@ -32,13 +32,13 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # 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. # License for the specific language governing permissions and limitations under the License.
import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator from django.core.validators import BaseValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from pretix.helpers.format import format_map
class PlaceholderValidator(BaseValidator): class PlaceholderValidator(BaseValidator):
""" """
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
which are not presented in taken list. which are not presented in taken list.
""" """
error_message = _(
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
'brackets on your placeholders match up. '
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
)
def __init__(self, limit_value): def __init__(self, limit_value):
super().__init__(limit_value) super().__init__(limit_value)
self.limit_value = limit_value self.limit_value = limit_value
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
self.__call__(v) self.__call__(v)
return return
if value.count('{') != value.count('}'): try:
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
except ValueError:
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
except KeyError as e:
raise ValidationError( raise ValidationError(
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'), _('Invalid placeholder: {%(value)s}'),
code='invalid_placeholder_syntax',
)
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
invalid_placeholders = []
for placeholder in data_placeholders:
if placeholder not in self.limit_value:
invalid_placeholders.append(placeholder)
if invalid_placeholders:
raise ValidationError(
_('Invalid placeholder(s): %(value)s'),
code='invalid_placeholders', code='invalid_placeholders',
params={'value': ", ".join(invalid_placeholders,)}) params={'value': e.args[0]})
def clean(self, x): def clean(self, x):
return x return x

View File

@@ -73,6 +73,7 @@ from i18nfield.utils import I18nJSONEncoder
from pretix.base.channels import get_all_sales_channels from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
from pretix.base.models.event import EventMetaValue from pretix.base.models.event import EventMetaValue
from pretix.base.services import tickets from pretix.base.services import tickets
@@ -713,11 +714,6 @@ class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
class MailSettingsPreview(EventPermissionRequiredMixin, View): class MailSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings' permission = 'can_change_event_settings'
# return the origin text if key is missing in dict
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
# create index-language mapping # create index-language mapping
@cached_property @cached_property
def supported_locale(self): def supported_locale(self):
@@ -742,7 +738,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
_('This value will be replaced based on dynamic parameters.'), _('This value will be replaced based on dynamic parameters.'),
s s
) )
return self.SafeDict(ctx) return ctx
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
preview_item = request.POST.get('item', '') preview_item = request.POST.get('item', '')
@@ -758,12 +754,21 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
idx = matched.group('idx') idx = matched.group('idx')
if idx in self.supported_locale: if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.event.settings.region): with language(self.supported_locale[idx], self.request.event.settings.region):
if k.startswith('mail_subject_'): try:
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item)) if k.startswith('mail_subject_'):
else: msgs[self.supported_locale[idx]] = format_map(
msgs[self.supported_locale[idx]] = markdown_compile_email( bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
format_map(v, self.placeholders(preview_item)) )
) else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
)
except ValueError:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
PlaceholderValidator.error_message)
except KeyError as e:
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
return JsonResponse({ return JsonResponse({
'item': preview_item, 'item': preview_item,

View File

@@ -30,17 +30,15 @@ class SafeFormatter(Formatter):
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers. (b) does not allow any unwanted shenanigans like attribute access or format specifiers.
""" """
def __init__(self, context): def __init__(self, context, raise_on_missing=False):
self.context = context self.context = context
self.raise_on_missing = raise_on_missing
def get_field(self, field_name, args, kwargs): def get_field(self, field_name, args, kwargs):
if '.' in field_name or '[' in field_name: return self.get_value(field_name, args, kwargs), field_name
logger.warning(f'Ignored invalid field name "{field_name}"')
return ('{' + str(field_name) + '}', field_name)
return super().get_field(field_name, args, kwargs)
def get_value(self, key, args, kwargs): def get_value(self, key, args, kwargs):
if key not in self.context: if not self.raise_on_missing and key not in self.context:
return '{' + str(key) + '}' return '{' + str(key) + '}'
return self.context[key] return self.context[key]
@@ -49,7 +47,7 @@ class SafeFormatter(Formatter):
return super().format_field(value, '') return super().format_field(value, '')
def format_map(template, context): def format_map(template, context, raise_on_missing=False):
if not isinstance(template, str): if not isinstance(template, str):
template = str(template) template = str(template)
return SafeFormatter(context).format(template) return SafeFormatter(context, raise_on_missing).format(template)