Compare commits

..

10 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
Raphael Michel
1566f54764 VAT ID validation: Fix crash with invalid Norwegian IDs (PRETIXEU-A3J) 2024-05-29 09:31:58 +02:00
Richard Schreiber
9d380557e1 SEO improvements - add h1.sr-only if only header-image is used
* add hidden h1 with event-title if header-image only

* add event-title to alt-attribute of header-image

* add hidden setting for google_site_verification
2024-05-28 09:18:15 +02:00
Martin Gross
5758e0dd68 PPv2 APM: Create referenced PPObjects for APM Orders; enable webhooks to capture them (#3958) 2024-05-27 13:45:37 +02:00
Martin Gross
b4629e24a5 Downgrade requests to 2.31.* again while waiting for 2.33.3 release 2024-05-27 12:11:40 +02:00
7 changed files with 81 additions and 59 deletions

View File

@@ -91,7 +91,7 @@ dependencies = [
"qrcode==7.4.*",
"redis==5.0.*",
"reportlab==4.2.*",
"requests==2.32.*",
"requests==2.31.*",
"sentry-sdk==1.45.*",
"sepaxml==2.6.*",
"slimit",

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
# License for the specific language governing permissions and limitations under the License.
import re
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
from django.utils.translation import gettext_lazy as _
from i18nfield.strings import LazyI18nString
from pretix.helpers.format import format_map
class PlaceholderValidator(BaseValidator):
"""
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
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):
super().__init__(limit_value)
self.limit_value = limit_value
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
self.__call__(v)
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(
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
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'),
_('Invalid placeholder: {%(value)s}'),
code='invalid_placeholders',
params={'value': ", ".join(invalid_placeholders,)})
params={'value': e.args[0]})
def clean(self, x):
return x

View File

@@ -62,7 +62,10 @@ class VATIDTemporaryError(VATIDError):
def _validate_vat_id_NO(vat_id, country_code):
# Inspired by vat_moss library
vat_id = vat_moss.id.normalize(vat_id)
try:
vat_id = vat_moss.id.normalize(vat_id)
except ValueError:
raise VATIDFinalError(error_messages['invalid'])
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
raise VATIDFinalError(error_messages['invalid'])

View File

@@ -73,6 +73,7 @@ from i18nfield.utils import I18nJSONEncoder
from pretix.base.channels import get_all_sales_channels
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.event import EventMetaValue
from pretix.base.services import tickets
@@ -713,11 +714,6 @@ class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
class MailSettingsPreview(EventPermissionRequiredMixin, View):
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
@cached_property
def supported_locale(self):
@@ -742,7 +738,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
_('This value will be replaced based on dynamic parameters.'),
s
)
return self.SafeDict(ctx)
return ctx
def post(self, request, *args, **kwargs):
preview_item = request.POST.get('item', '')
@@ -758,12 +754,21 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
idx = matched.group('idx')
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.event.settings.region):
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
format_map(v, self.placeholders(preview_item))
)
try:
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = format_map(
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
)
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({
'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
(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.raise_on_missing = raise_on_missing
def get_field(self, field_name, args, kwargs):
if '.' in field_name or '[' in 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)
return self.get_value(field_name, args, kwargs), field_name
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 self.context[key]
@@ -49,7 +47,7 @@ class SafeFormatter(Formatter):
return super().format_field(value, '')
def format_map(template, context):
def format_map(template, context, raise_on_missing=False):
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context).format(template)
return SafeFormatter(context, raise_on_missing).format(template)

View File

@@ -30,6 +30,7 @@ from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.cache import cache
from django.db import transaction
from django.http import HttpRequest
from django.template.loader import get_template
from django.templatetags.static import static
@@ -54,6 +55,7 @@ from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.plugins.paypal2.client.core.environment import (
@@ -585,6 +587,9 @@ class PaypalMethod(BasePaymentProvider):
},
})
response = self.client.execute(paymentreq)
if payment:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=response.result.id)
except IOError as e:
if "RESOURCE_NOT_FOUND" in str(e):
messages.error(request, _('Your payment has failed due to a known issue within PayPal. Please try '
@@ -617,7 +622,13 @@ class PaypalMethod(BasePaymentProvider):
}
return template.render(ctx)
@transaction.atomic
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('payment is already confirmed; possible return-view/webhook race-condition')
return
try:
if request.session.get('payment_paypal_oid', '') == '':
raise PaymentException(_('We were unable to process your payment. See below for details on how to '

View File

@@ -477,29 +477,35 @@ def webhook(request, *args, **kwargs):
amount=payment.amount - known_sum
)
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) \
and sale['status'] == 'COMPLETED':
any_captures = False
all_captures_completed = True
for purchaseunit in sale['purchase_units']:
for capture in purchaseunit['payments']['captures']:
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment,
reference=capture['id'])
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED):
if sale['status'] == 'COMPLETED':
any_captures = False
all_captures_completed = True
for purchaseunit in sale['purchase_units']:
for capture in purchaseunit['payments']['captures']:
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment,
reference=capture['id'])
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
if capture['status'] not in ('COMPLETED', 'REFUNDED', 'PARTIALLY_REFUNDED'):
all_captures_completed = False
else:
any_captures = True
if any_captures and all_captures_completed:
if capture['status'] not in ('COMPLETED', 'REFUNDED', 'PARTIALLY_REFUNDED'):
all_captures_completed = False
else:
any_captures = True
if any_captures and all_captures_completed:
try:
payment.info = json.dumps(sale.dict())
payment.save(update_fields=['info'])
payment.confirm()
except Quota.QuotaExceededException:
pass
elif sale['status'] == 'APPROVED':
request.session['payment_paypal_oid'] = payment.info_data['id']
try:
payment.info = json.dumps(sale.dict())
payment.save(update_fields=['info'])
payment.confirm()
except Quota.QuotaExceededException:
pass
payment.payment_provider.execute_payment(request, payment)
except PaymentException as e:
logger.exception('PayPal2 - Could not capture/execute_payment from Webhook: {}'.format(str(e)))
return HttpResponse(status=200)