diff --git a/doc/api/deviceauth.rst b/doc/api/deviceauth.rst index acf3d22a8..494268a77 100644 --- a/doc/api/deviceauth.rst +++ b/doc/api/deviceauth.rst @@ -208,20 +208,6 @@ Additionally, when creating a device through the user interface or API, a user c the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security policies for official pretix apps like pretixSCAN and pretixPOS. -Removing a device ------------------ - -If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to -invalidate your API key. There is no way to reverse this operation. - -.. sourcecode:: http - - POST /api/v1/device/revoke HTTP/1.1 - Host: pretix.eu - Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd - -This can also be done by the user through the web interface. - Event selection --------------- diff --git a/doc/api/resources/webhooks.rst b/doc/api/resources/webhooks.rst index 94dcc1b68..8568d5067 100644 --- a/doc/api/resources/webhooks.rst +++ b/doc/api/resources/webhooks.rst @@ -60,6 +60,9 @@ The following values for ``action_types`` are valid with pretix core: * ``pretix.event.added`` * ``pretix.event.changed`` * ``pretix.event.deleted`` + * ``pretix.giftcards.created`` + * ``pretix.giftcards.modified`` + * ``pretix.giftcards.transaction.*`` * ``pretix.voucher.added`` * ``pretix.voucher.changed`` * ``pretix.voucher.deleted`` diff --git a/doc/requirements.rtd.txt b/doc/requirements.rtd.txt index 19638c5b7..e4be75047 100644 --- a/doc/requirements.rtd.txt +++ b/doc/requirements.rtd.txt @@ -1,6 +1,6 @@ sphinx==9.1.* sphinx-rtd-theme~=3.1.0 -sphinxcontrib-httpdomain~=1.8.1 +sphinxcontrib-httpdomain~=2.0.0 sphinxcontrib-images~=1.0.1 sphinxcontrib-jquery~=4.1 sphinxcontrib-spelling~=8.0.2 diff --git a/doc/requirements.txt b/doc/requirements.txt index 74538aae4..ef664a515 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ -e ../ sphinx==9.1.* sphinx-rtd-theme~=3.1.0 -sphinxcontrib-httpdomain~=1.8.1 +sphinxcontrib-httpdomain~=2.0.0 sphinxcontrib-images~=1.0.1 sphinxcontrib-jquery~=4.1 sphinxcontrib-spelling~=8.0.2 diff --git a/pyproject.toml b/pyproject.toml index 437177516..f04657f13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "celery==5.6.*", "chardet==5.2.*", "cryptography>=44.0.0", - "css-inline==0.19.*", + "css-inline==0.20.*", "defusedcsv>=1.1.0", "dnspython==2.*", "Django[argon2]==4.2.*,>=4.2.26", @@ -65,7 +65,7 @@ dependencies = [ "kombu==5.6.*", "libsass==0.23.*", "lxml", - "markdown==3.10.1", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3. + "markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3. # We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7 "mt-940==4.30.*", "oauthlib==3.3.*", @@ -73,7 +73,7 @@ dependencies = [ "packaging", "paypalrestsdk==1.13.*", "paypal-checkout-serversdk==1.0.*", - "PyJWT==2.10.*", + "PyJWT==2.11.*", "phonenumberslite==9.0.*", "Pillow==12.1.*", "pretix-plugin-build", @@ -92,7 +92,7 @@ dependencies = [ "redis==7.1.*", "reportlab==4.4.*", "requests==2.32.*", - "sentry-sdk==2.50.*", + "sentry-sdk==2.53.*", "sepaxml==2.7.*", "stripe==7.9.*", "text-unidecode==1.*", @@ -110,10 +110,10 @@ dev = [ "aiohttp==3.13.*", "coverage", "coveralls", - "fakeredis==2.33.*", + "fakeredis==2.34.*", "flake8==7.3.*", "freezegun", - "isort==7.0.*", + "isort==8.0.*", "pep8-naming==0.15.*", "potypo", "pytest-asyncio>=0.24", diff --git a/src/pretix/__init__.py b/src/pretix/__init__.py index 9f5eafff1..500cb9419 100644 --- a/src/pretix/__init__.py +++ b/src/pretix/__init__.py @@ -19,4 +19,4 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -__version__ = "2026.1.0" +__version__ = "2026.2.0" diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 6f5e995e0..4fb4dce05 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -1743,6 +1743,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): rounding_mode = self.context["event"].settings.tax_rounding changed = apply_rounding( rounding_mode, + ia, self.context["event"].currency, [*pos_map.values(), *fees] ) diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index ce3ed39b7..6d8f6743b 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -49,7 +49,7 @@ from pretix.base.plugins import ( PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_ORGANIZER, ) -from pretix.base.services.mail import SendMailException, mail +from pretix.base.services.mail import mail from pretix.base.settings import validate_organizer_settings from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.multidomain.urlreverse import build_absolute_uri @@ -363,24 +363,21 @@ class TeamInviteSerializer(serializers.ModelSerializer): ) def _send_invite(self, instance): - try: - mail( - instance.email, - _('pretix account invitation'), - 'pretixcontrol/email/invitation.txt', - { - 'user': self, - 'organizer': self.context['organizer'].name, - 'team': instance.team.name, - 'url': build_global_uri('control:auth.invite', kwargs={ - 'token': instance.token - }) - }, - event=None, - locale=get_language_without_region() # TODO: expose? - ) - except SendMailException: - pass # Already logged + mail( + instance.email, + _('pretix account invitation'), + 'pretixcontrol/email/invitation.txt', + { + 'user': self, + 'organizer': self.context['organizer'].name, + 'team': instance.team.name, + 'url': build_global_uri('control:auth.invite', kwargs={ + 'token': instance.token + }) + }, + event=None, + locale=get_language_without_region() # TODO: expose? + ) def create(self, validated_data): if 'email' in validated_data: diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 7a7d50e97..7fb054e24 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -188,11 +188,15 @@ class CheckinListViewSet(viewsets.ModelViewSet): clist = self.get_object() if serializer.validated_data.get('nonce'): if kwargs.get('position'): - prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first() + prev = kwargs['position'].all_checkins.filter( + nonce=serializer.validated_data['nonce'], + successful=False + ).first() else: prev = clist.checkins.filter( nonce=serializer.validated_data['nonce'], raw_barcode=serializer.validated_data['raw_barcode'], + successful=False ).first() if prev: # Ignore because nonce is already handled diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 2b5a7f82b..8ec782876 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -90,7 +90,6 @@ from pretix.base.services.invoices import ( generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, regenerate_invoice, transmit_invoice, ) -from pretix.base.services.mail import SendMailException from pretix.base.services.orders import ( OrderChangeManager, OrderError, _order_placed_email, _order_placed_email_attendee, approve_order, cancel_order, deny_order, @@ -439,8 +438,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet): return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except PaymentException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) - except SendMailException: - pass return self.retrieve(request, [], **kwargs) return Response( @@ -634,10 +631,7 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet): order = self.get_object() if not order.email: return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST) - try: - order.resend_link(user=self.request.user, auth=self.request.auth) - except SendMailException: - return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE) + order.resend_link(user=self.request.user, auth=self.request.auth) return Response( status=status.HTTP_204_NO_CONTENT @@ -1616,8 +1610,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): ) except Quota.QuotaExceededException: pass - except SendMailException: - pass serializer = OrderPaymentSerializer(r, context=serializer.context) @@ -1655,8 +1647,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except PaymentException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) - except SendMailException: - pass return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index f084a1679..6ffb894ca 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -249,12 +249,24 @@ class GiftCardViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): value = serializer.validated_data.pop('value') inst = serializer.save(issuer=self.request.organizer) - inst.transactions.create(value=value, acceptor=self.request.organizer) inst.log_action( - 'pretix.giftcards.transaction.manual', + action='pretix.giftcards.created', user=self.request.user, auth=self.request.auth, - data=merge_dicts(self.request.data, {'id': inst.pk}) + ) + inst.transactions.create(value=value, acceptor=self.request.organizer) + inst.log_action( + action='pretix.giftcards.transaction.manual', + user=self.request.user, + auth=self.request.auth, + data=merge_dicts( + self.request.data, + { + 'id': inst.pk, + 'acceptor_id': self.request.organizer.id, + 'acceptor_slug': self.request.organizer.slug + } + ) ) @transaction.atomic() @@ -269,7 +281,7 @@ class GiftCardViewSet(viewsets.ModelViewSet): inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency, testmode=serializer.instance.testmode) inst.log_action( - 'pretix.giftcards.modified', + action='pretix.giftcards.modified', user=self.request.user, auth=self.request.auth, data=self.request.data, @@ -282,10 +294,14 @@ class GiftCardViewSet(viewsets.ModelViewSet): diff = value - old_value inst.transactions.create(value=diff, acceptor=self.request.organizer) inst.log_action( - 'pretix.giftcards.transaction.manual', + action='pretix.giftcards.transaction.manual', user=self.request.user, auth=self.request.auth, - data={'value': diff} + data={ + 'value': diff, + 'acceptor_id': self.request.organizer.id, + 'acceptor_slug': self.request.organizer.slug + } ) return inst @@ -309,10 +325,15 @@ class GiftCardViewSet(viewsets.ModelViewSet): }, status=status.HTTP_409_CONFLICT) gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer) gc.log_action( - 'pretix.giftcards.transaction.manual', + action='pretix.giftcards.transaction.manual', user=self.request.user, auth=self.request.auth, - data={'value': value, 'text': text} + data={ + 'value': value, + 'text': text, + 'acceptor_id': self.request.organizer.id, + 'acceptor_slug': self.request.organizer.slug + } ) return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK) diff --git a/src/pretix/api/webhooks.py b/src/pretix/api/webhooks.py index ac3889157..39a5a613b 100644 --- a/src/pretix/api/webhooks.py +++ b/src/pretix/api/webhooks.py @@ -174,6 +174,38 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent): } +class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent): + def build_payload(self, logentry: LogEntry): + giftcard = logentry.content_object + if not giftcard: + return None + + return { + 'notification_id': logentry.pk, + 'issuer_id': logentry.organizer_id, + 'issuer_slug': logentry.organizer.slug, + 'giftcard': giftcard.pk, + 'action': logentry.action_type, + } + + +class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent): + def build_payload(self, logentry: LogEntry): + giftcard = logentry.content_object + if not giftcard: + return None + + return { + 'notification_id': logentry.pk, + 'issuer_id': logentry.organizer_id, + 'issuer_slug': logentry.organizer.slug, + 'acceptor_id': logentry.parsed_data.get('acceptor_id'), + 'acceptor_slug': logentry.parsed_data.get('acceptor_slug'), + 'giftcard': giftcard.pk, + 'action': logentry.action_type, + } + + class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent): def build_payload(self, logentry: LogEntry): @@ -433,6 +465,18 @@ def register_default_webhook_events(sender, **kwargs): 'pretix.customer.anonymized', _('Customer account anonymized'), ), + ParametrizedGiftcardWebhookEvent( + 'pretix.giftcards.created', + _('Gift card added'), + ), + ParametrizedGiftcardWebhookEvent( + 'pretix.giftcards.modified', + _('Gift card modified'), + ), + ParametrizedGiftcardTransactionWebhookEvent( + 'pretix.giftcards.transaction.*', + _('Gift card used in transaction'), + ) ) diff --git a/src/pretix/base/email.py b/src/pretix/base/email.py index ab71fb45f..155b29657 100644 --- a/src/pretix/base/email.py +++ b/src/pretix/base/email.py @@ -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, diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index d4bac74a5..4495242cb 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -651,6 +651,7 @@ class OrderListExporter(MultiSheetListExporter): pgettext('address', 'State'), _('Voucher'), _('Voucher budget usage'), + _('Voucher tag'), _('Pseudonymization ID'), _('Ticket secret'), _('Seat ID'), @@ -769,6 +770,7 @@ class OrderListExporter(MultiSheetListExporter): op.state_for_address or '', op.voucher.code if op.voucher else '', op.voucher_budget_use if op.voucher_budget_use else '', + op.voucher.tag if op.voucher else '', op.pseudonymization_id, op.secret, ] diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 165c1c92e..1fd4b8759 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -890,18 +890,18 @@ class BaseQuestionsForm(forms.Form): if not help_text: if q.valid_date_min and q.valid_date_max: help_text = format_lazy( - 'Please enter a date between {min} and {max}.', + _('Please enter a date between {min} and {max}.'), min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"), max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"), ) elif q.valid_date_min: help_text = format_lazy( - 'Please enter a date no earlier than {min}.', + _('Please enter a date no earlier than {min}.'), min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"), ) elif q.valid_date_max: help_text = format_lazy( - 'Please enter a date no later than {max}.', + _('Please enter a date no later than {max}.'), max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"), ) if initial and initial.answer: @@ -939,18 +939,18 @@ class BaseQuestionsForm(forms.Form): if not help_text: if q.valid_datetime_min and q.valid_datetime_max: help_text = format_lazy( - 'Please enter a date and time between {min} and {max}.', + _('Please enter a date and time between {min} and {max}.'), min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"), max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"), ) elif q.valid_datetime_min: help_text = format_lazy( - 'Please enter a date and time no earlier than {min}.', + _('Please enter a date and time no earlier than {min}.'), min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"), ) elif q.valid_datetime_max: help_text = format_lazy( - 'Please enter a date and time no later than {max}.', + _('Please enter a date and time no later than {max}.'), max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"), ) diff --git a/src/pretix/base/invoicing/email.py b/src/pretix/base/invoicing/email.py index d3064af77..155e7906a 100644 --- a/src/pretix/base/invoicing/email.py +++ b/src/pretix/base/invoicing/email.py @@ -33,7 +33,7 @@ from pretix.base.invoicing.transmission import ( transmission_types, ) from pretix.base.models import Invoice, InvoiceAddress -from pretix.base.services.mail import SendMailException, mail, render_mail +from pretix.base.services.mail import mail, render_mail from pretix.helpers.format import format_map @@ -133,41 +133,37 @@ class EmailTransmissionProvider(TransmissionProvider): template = invoice.order.event.settings.get('mail_text_order_invoice', as_type=LazyI18nString) subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString) - try: - # Do not set to completed because that is done by the email sending task - subject = format_map(subject, context) - email_content = render_mail(template, context) - mail( - [recipient], - subject, - template, - context=context, - event=invoice.order.event, - locale=invoice.order.locale, - order=invoice.order, - invoices=[invoice], - attach_tickets=False, - auto_email=True, - attach_ical=False, - plain_text_only=True, - no_order_links=True, - ) - except SendMailException: - raise - else: - invoice.order.log_action( - 'pretix.event.order.email.invoice', - user=None, - auth=None, - data={ - 'subject': subject, - 'message': email_content, - 'position': None, - 'recipient': recipient, - 'invoices': [invoice.pk], - 'attach_tickets': False, - 'attach_ical': False, - 'attach_other_files': [], - 'attach_cached_files': [], - } - ) + # Do not set to completed because that is done by the email sending task + subject = format_map(subject, context) + email_content = render_mail(template, context) + mail( + [recipient], + subject, + template, + context=context, + event=invoice.order.event, + locale=invoice.order.locale, + order=invoice.order, + invoices=[invoice], + attach_tickets=False, + auto_email=True, + attach_ical=False, + plain_text_only=True, + no_order_links=True, + ) + invoice.order.log_action( + 'pretix.event.order.email.invoice', + user=None, + auth=None, + data={ + 'subject': subject, + 'message': email_content, + 'position': None, + 'recipient': recipient, + 'invoices': [invoice.pk], + 'attach_tickets': False, + 'attach_ical': False, + 'attach_other_files': [], + 'attach_cached_files': [], + } + ) diff --git a/src/pretix/base/invoicing/transmission.py b/src/pretix/base/invoicing/transmission.py index e04c02e61..9c6899381 100644 --- a/src/pretix/base/invoicing/transmission.py +++ b/src/pretix/base/invoicing/transmission.py @@ -21,6 +21,7 @@ # from typing import Optional +from django.utils.translation import gettext_lazy as _ from django_countries.fields import Country from pretix.base.models import Invoice, InvoiceAddress @@ -106,6 +107,22 @@ class TransmissionType: def transmission_info_to_form_data(self, transmission_info: dict) -> dict: return transmission_info + def describe_info(self, transmission_info: dict, country: Country, is_business: bool): + form_data = self.transmission_info_to_form_data(transmission_info) + data = [] + visible_field_keys = self.invoice_address_form_fields_visible(country, is_business) + for k, f in self.invoice_address_form_fields.items(): + if k not in visible_field_keys: + continue + v = form_data.get(k) + if v is True: + v = _("Yes") + elif v is False: + v = _("No") + if v: + data.append((f.label, v)) + return data + def pdf_watermark(self) -> Optional[str]: """ Return a watermark that should be rendered across the PDF file. diff --git a/src/pretix/base/metrics.py b/src/pretix/base/metrics.py index f0d024f8d..8454098f8 100755 --- a/src/pretix/base/metrics.py +++ b/src/pretix/base/metrics.py @@ -294,14 +294,28 @@ def metric_values(): channel = app.broker_connection().channel() if hasattr(channel, 'client') and channel.client is not None: client = channel.client + priority_steps = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("priority_steps", [0]) + sep = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("sep", ":") + for q in settings.CELERY_TASK_QUEUES: - llen = client.llen(q.name) - lfirst = client.lindex(q.name, -1) - metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = llen - if lfirst: - ldata = json.loads(lfirst) - dt = time.time() - ldata.get('created', 0) - metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = dt + queue_lengths = [] + queue_delays = [] + for prio in priority_steps: + if prio: + qname = f"{q.name}{sep}{prio}" + else: + qname = q.name + queue_length = client.llen(qname) + queue_lengths.append(queue_length) + oldest_queue_item = client.lindex(qname, -1) + if oldest_queue_item: + ldata = json.loads(oldest_queue_item) + oldest_item_age = time.time() - ldata.get('created', 0) + queue_delays.append(oldest_item_age) + + metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = sum(queue_lengths) + if queue_delays: + metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = max(queue_delays) else: metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0 diff --git a/src/pretix/base/migrations/0297_outgoingmail.py b/src/pretix/base/migrations/0297_outgoingmail.py new file mode 100644 index 000000000..034d77ac4 --- /dev/null +++ b/src/pretix/base/migrations/0297_outgoingmail.py @@ -0,0 +1,120 @@ +# Generated by Django 4.2.26 on 2026-01-22 13:44 +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import pretix.base.models.mail + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0296_invoice_invoice_from_state"), + ] + + operations = [ + migrations.CreateModel( + name="OutgoingMail", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("guid", models.UUIDField(db_index=True, default=uuid.uuid4)), + ("status", models.CharField(default="queued", max_length=200)), + ("created", models.DateTimeField(auto_now_add=True)), + ("sent", models.DateTimeField(blank=True, null=True)), + ("inflight_since", models.DateTimeField(blank=True, null=True)), + ("retry_after", models.DateTimeField(blank=True, null=True)), + ("error", models.TextField(null=True)), + ("error_detail", models.TextField(null=True)), + ("sensitive", models.BooleanField(default=False)), + ("subject", models.TextField()), + ("body_plain", models.TextField()), + ("body_html", models.TextField(null=True)), + ("sender", models.CharField(max_length=500)), + ("headers", models.JSONField(default=dict)), + ("to", models.JSONField(default=list)), + ("cc", models.JSONField(default=list)), + ("bcc", models.JSONField(default=list)), + ("recipient_count", models.IntegerField()), + ("should_attach_tickets", models.BooleanField(default=False)), + ("should_attach_ical", models.BooleanField(default=False)), + ("should_attach_other_files", models.JSONField(default=list)), + ("actual_attachments", models.JSONField(default=list)), + ( + "customer", + models.ForeignKey( + null=True, + on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED, + related_name="outgoing_mails", + to="pretixbase.customer", + ), + ), + ( + "event", + models.ForeignKey( + null=True, + on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED, + related_name="outgoing_mails", + to="pretixbase.event", + ), + ), + ( + "order", + models.ForeignKey( + null=True, + on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED, + related_name="outgoing_mails", + to="pretixbase.order", + ), + ), + ( + "orderposition", + models.ForeignKey( + null=True, + on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED, + related_name="outgoing_mails", + to="pretixbase.orderposition", + ), + ), + ( + "organizer", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="outgoing_mails", + to="pretixbase.organizer", + ), + ), + ( + "should_attach_cached_files", + models.ManyToManyField( + related_name="outgoing_mails", to="pretixbase.cachedfile" + ), + ), + ( + "should_attach_invoices", + models.ManyToManyField( + related_name="outgoing_mails", to="pretixbase.invoice" + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="outgoing_mails", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ("-created",), + }, + ), + ] diff --git a/src/pretix/base/modelimport_vouchers.py b/src/pretix/base/modelimport_vouchers.py index a9e2199ff..61f4edf3b 100644 --- a/src/pretix/base/modelimport_vouchers.py +++ b/src/pretix/base/modelimport_vouchers.py @@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn): class PriceModeColumn(ImportColumn): identifier = 'price_mode' - verbose_name = gettext_lazy('Price mode') + verbose_name = gettext_lazy('Price effect') default_value = None initial = 'static:none' @@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn): elif value in reverse: return reverse[value] else: - raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format( + raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format( value=value, options=', '.join(d.keys()) )) @@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn): def clean(self, value, previous_values): value = super().clean(value, previous_values) if value and previous_values.get("price_mode") == "none": - raise ValidationError(_("It is pointless to set a value without a price mode.")) + raise ValidationError(_("It is pointless to set a value without a price effect.")) return value def assign(self, value, obj: Voucher, **kwargs): diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index 372fc8287..3657cfb4c 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -41,6 +41,7 @@ from .items import ( itempicture_upload_to, ) from .log import LogEntry +from .mail import OutgoingMail from .media import ReusableMedium from .memberships import Membership, MembershipType from .notifications import NotificationSetting diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index 44ff3587d..0d2ba97a2 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -334,27 +334,24 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): return self.email def send_security_notice(self, messages, email=None): - from pretix.base.services.mail import SendMailException, mail + from pretix.base.services.mail import mail - try: - with language(self.locale): - msg = '- ' + '\n- '.join(str(m) for m in messages) + with language(self.locale): + msg = '- ' + '\n- '.join(str(m) for m in messages) - mail( - email or self.email, - _('Account information changed'), - 'pretixcontrol/email/security_notice.txt', - { - 'user': self, - 'messages': msg, - 'url': build_absolute_uri('control:user.settings') - }, - event=None, - user=self, - locale=self.locale - ) - except SendMailException: - pass # Already logged + mail( + email or self.email, + _('Account information changed'), + 'pretixcontrol/email/security_notice.txt', + { + 'user': self, + 'messages': msg, + 'url': build_absolute_uri('control:user.settings') + }, + event=None, + user=self, + locale=self.locale + ) def send_confirmation_code(self, session, reason, email=None, state=None): """ diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index 0a15dd1f9..dd95ad03b 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -130,6 +130,8 @@ class LoggingMixin: organizer_id = self.event.organizer_id elif hasattr(self, 'organizer_id'): organizer_id = self.organizer_id + elif hasattr(self, 'issuer_id'): + organizer_id = self.issuer_id if user and not user.is_authenticated: user = None diff --git a/src/pretix/base/models/customers.py b/src/pretix/base/models/customers.py index d7d34db98..ef9a3b26b 100644 --- a/src/pretix/base/models/customers.py +++ b/src/pretix/base/models/customers.py @@ -40,6 +40,7 @@ from i18nfield.fields import I18nCharField from phonenumber_field.modelfields import PhoneNumberField from pretix.base.banlist import banned +from pretix.base.i18n import language from pretix.base.models.base import LoggedModel from pretix.base.models.fields import MultiStringField from pretix.base.models.giftcards import GiftCardTransaction @@ -164,6 +165,28 @@ class Customer(LoggedModel): self.attendee_profiles.all().delete() self.invoice_addresses.all().delete() + def send_security_notice(self, message, email=None): + from pretix.base.services.mail import SendMailException, mail + from pretix.multidomain.urlreverse import build_absolute_uri + + try: + with language(self.locale): + mail( + email or self.email, + self.organizer.settings.mail_subject_customer_security_notice, + self.organizer.settings.mail_text_customer_security_notice, + { + **self.get_email_context(), + 'message': str(message), + 'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index') + }, + customer=self, + organizer=self.organizer, + locale=self.locale + ) + except SendMailException: + pass # Already logged + @scopes_disabled() def assign_identifier(self): charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') @@ -293,6 +316,7 @@ class Customer(LoggedModel): locale=self.locale, customer=self, organizer=self.organizer, + sensitive=True, ) def usable_gift_cards(self, used_cards=[]): diff --git a/src/pretix/base/models/mail.py b/src/pretix/base/models/mail.py new file mode 100644 index 000000000..2128c2859 --- /dev/null +++ b/src/pretix/base/models/mail.py @@ -0,0 +1,222 @@ +# +# 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 . +# +# 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 +# . +# +import uuid + +from django.core.mail import get_connection +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_scopes import scope, scopes_disabled + + +def CASCADE_IF_QUEUED(collector, field, sub_objs, using): + # If the email is still queued and the thing it is related to vanishes, the email can vanish as well + cascade_objs = [ + o for o in sub_objs if o.status == OutgoingMail.STATUS_QUEUED + ] + if cascade_objs: + models.CASCADE(collector, field, cascade_objs, using) + + # In all other cases, set to NULL to keep the email on record + models.SET_NULL(collector, field, [o for o in sub_objs if o not in cascade_objs], using) + + +class OutgoingMail(models.Model): + STATUS_QUEUED = "queued" + STATUS_WITHHELD = "withheld" + STATUS_INFLIGHT = "inflight" + STATUS_AWAITING_RETRY = "awaiting_retry" + STATUS_FAILED = "failed" + STATUS_SENT = "sent" + STATUS_BOUNCED = "bounced" + STATUS_ABORTED = "aborted" + STATUS_CHOICES = ( + (STATUS_QUEUED, _("queued")), + (STATUS_INFLIGHT, _("being sent")), + (STATUS_AWAITING_RETRY, _("awaiting retry")), + (STATUS_WITHHELD, _("withheld")), # for plugin use + (STATUS_FAILED, _("failed")), + (STATUS_ABORTED, _("aborted")), + (STATUS_SENT, _("sent")), + (STATUS_BOUNCED, _("bounced")), # for plugin use + ) + STATUS_LIST_ABORTABLE = { + STATUS_QUEUED, + STATUS_WITHHELD, + STATUS_AWAITING_RETRY, + } + STATUS_LIST_RETRYABLE = { + STATUS_FAILED, + STATUS_WITHHELD, + } + + # The GUID is a globally unique ID for the email added to a header of the email for later tracing + # in bug reports etc. We could theoretically also use this as a basis for the Message-ID header, but + # we currently don't since we are unsure if some intermediary SMTP servers have opinions on setting + # their own Message-ID headers. + guid = models.UUIDField(db_index=True, default=uuid.uuid4) + + status = models.CharField(max_length=200, choices=STATUS_CHOICES, default=STATUS_QUEUED) + created = models.DateTimeField(auto_now_add=True) + + # sent will be the time the email was sent or the email failed + sent = models.DateTimeField(null=True, blank=True) + + inflight_since = models.DateTimeField(null=True, blank=True) + retry_after = models.DateTimeField(null=True, blank=True) + + error = models.TextField(null=True, blank=True) + error_detail = models.TextField(null=True, blank=True) + + # There is a conflict here between the different purposes of the model. As a system administrator, + # one wants *all* emails to be persisted as long as possible to debug issues. This means that if + # e.g. the event or order is deleted, we want SET_NULL behavior. However, in that case, the email + # would be an "orphan" forever and there's no way to remove the personal information. + # We try to find a middle-ground with the following behaviour: + # - The email is always deleted if the entire organizer or user is deleted + # - The email is always deleted if it has not yet been sent + # - The email is kept in all other cases + # This is only an acceptable trade-off since emails are stored for a short period only, and because + # orders and customers are never deleted during normal operation. If we ever make this a long-term + # storage / email archive, we'd need to find another way to make sure personal information is removed + # if personal information of orders etc is removed. + organizer = models.ForeignKey( + 'pretixbase.Organizer', + on_delete=models.CASCADE, + related_name='outgoing_mails', + null=True, blank=True, + ) + event = models.ForeignKey( + 'pretixbase.Event', + on_delete=CASCADE_IF_QUEUED, + related_name='outgoing_mails', + null=True, blank=True, + ) + order = models.ForeignKey( + 'pretixbase.Order', + on_delete=CASCADE_IF_QUEUED, + related_name='outgoing_mails', + null=True, blank=True, + ) + orderposition = models.ForeignKey( + 'pretixbase.OrderPosition', + on_delete=CASCADE_IF_QUEUED, + related_name='outgoing_mails', + null=True, blank=True, + ) + customer = models.ForeignKey( + 'pretixbase.Customer', + on_delete=CASCADE_IF_QUEUED, + related_name='outgoing_mails', + null=True, blank=True, + ) + user = models.ForeignKey( + 'pretixbase.User', + on_delete=models.CASCADE, + related_name='outgoing_mails', + null=True, blank=True, + ) + + sensitive = models.BooleanField(default=False) + subject = models.TextField() + body_plain = models.TextField() + body_html = models.TextField(null=True) + sender = models.CharField(max_length=500) + headers = models.JSONField(default=dict) + to = models.JSONField(default=list) + cc = models.JSONField(default=list) + bcc = models.JSONField(default=list) + recipient_count = models.IntegerField() + + # We don't store the actual invoices, tickets or calendar invites, so if the email is re-sent at a later time, a + # newer version of the files might be used. We accept that risk to save on storage and also because the new + # version might actually be more useful. + should_attach_invoices = models.ManyToManyField( + 'pretixbase.Invoice', + related_name='outgoing_mails' + ) + should_attach_tickets = models.BooleanField(default=False) + should_attach_ical = models.BooleanField(default=False) + + # clean_cached_files makes sure not to delete these as long as the email is in a retryable state + should_attach_cached_files = models.ManyToManyField( + 'pretixbase.CachedFile', + related_name='outgoing_mails', + ) + + # This is used to send files stored in settings. In most cases, these aren't short-lived and should still be there + # if the email is sent. Otherwise, they will be skipped. We accept that risk. + should_attach_other_files = models.JSONField(default=list) + + # [{name, type size}] of the attachments we actually setn + actual_attachments = models.JSONField(default=list) + + class Meta: + ordering = ('-created',) + + def get_mail_backend(self): + if self.event: + return self.event.get_mail_backend() + elif self.organizer: + return self.organizer.get_mail_backend() + else: + return get_connection(fail_silently=False) + + def scope_manager(self): + if self.organizer: + return scope(organizer=self.organizer) # noqa + else: + return scopes_disabled() # noqa + + @property + def is_failed(self): + return self.status in ( + OutgoingMail.STATUS_FAILED, + OutgoingMail.STATUS_AWAITING_RETRY, + OutgoingMail.STATUS_BOUNCED, + ) + + def save(self, *args, **kwargs): + if self.orderposition_id and not self.order_id: + self.order = self.orderposition.order + if self.order_id and not self.event_id: + self.event = self.order.event + if self.event_id and not self.organizer_id: + self.organizer = self.event.organizer + if self.customer_id and not self.organizer_id: + self.organizer = self.customer.organizer + self.recipient_count = len(self.to) + len(self.cc) + len(self.bcc) + super().save(*args, **kwargs) + + def log_parameters(self): + if self.order: + error_log_action_type = 'pretix.event.order.email.error' + log_target = self.order + elif self.customer: + error_log_action_type = 'pretix.customer.email.error' + log_target = self.customer + elif self.user: + error_log_action_type = 'pretix.user.email.error' + log_target = self.user + else: + error_log_action_type = 'pretix.email.error' + log_target = None + return log_target, error_log_action_type diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index c9c4da871..a2b48ed98 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -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 ( @@ -1167,9 +1167,7 @@ class Order(LockModel, LoggedModel): only be attached for this position and child positions, the link will only point to the position and the attendee email will be used if available. """ - from pretix.base.services.mail import ( - SendMailException, mail, render_mail, - ) + from pretix.base.services.mail import mail, render_mail if not self.email and not (position and position.attendee_email): return @@ -1179,35 +1177,32 @@ class Order(LockModel, LoggedModel): if position and position.attendee_email: recipient = position.attendee_email - try: - email_content = render_mail(template, context) + email_content = render_mail(template, 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, - invoices=invoices, attach_tickets=attach_tickets, - position=position, auto_email=auto_email, attach_ical=attach_ical, - attach_other_files=attach_other_files, attach_cached_files=attach_cached_files, - ) - except SendMailException: - raise - else: - self.log_action( - log_entry_type, - user=user, - auth=auth, - data={ - 'subject': subject, - 'message': email_content, - 'position': position.positionid if position else None, - 'recipient': recipient, - 'invoices': [i.pk for i in invoices] if invoices else [], - 'attach_tickets': attach_tickets, - 'attach_ical': attach_ical, - 'attach_other_files': attach_other_files, - 'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [], - } - ) + mail( + recipient, subject, template, context, + self.event, self.locale, self, headers=headers, sender=sender, + invoices=invoices, attach_tickets=attach_tickets, + position=position, auto_email=auto_email, attach_ical=attach_ical, + attach_other_files=attach_other_files, attach_cached_files=attach_cached_files, + ) + self.log_action( + log_entry_type, + user=user, + auth=auth, + data={ + 'subject': subject, + 'message': email_content, + 'position': position.positionid if position else None, + 'recipient': recipient, + 'invoices': [i.pk for i in invoices] if invoices else [], + 'attach_tickets': attach_tickets, + 'attach_ical': attach_ical, + 'attach_other_files': attach_other_files, + 'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [], + } + ) def resend_link(self, user=None, auth=None): with language(self.locale, self.event.settings.region): @@ -2024,40 +2019,30 @@ class OrderPayment(models.Model): transmit_invoice.apply_async(args=(self.order.event_id, invoice.pk, False)) def _send_paid_mail_attendee(self, position, user): - from pretix.base.services.mail import SendMailException - with language(self.order.locale, self.order.event.settings.region): email_template = self.order.event.settings.mail_text_order_paid_attendee email_subject = self.order.event.settings.mail_subject_order_paid_attendee email_context = get_email_context(event=self.order.event, order=self.order, position=position) - try: - position.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.order_paid', user, - invoices=[], - attach_tickets=True, - attach_ical=self.order.event.settings.mail_attach_ical - ) - except SendMailException: - logger.exception('Order paid email could not be sent') + position.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.order_paid', user, + invoices=[], + attach_tickets=True, + attach_ical=self.order.event.settings.mail_attach_ical + ) def _send_paid_mail(self, invoice, user, mail_text): - from pretix.base.services.mail import SendMailException - with language(self.order.locale, self.order.event.settings.region): email_template = self.order.event.settings.mail_text_order_paid email_subject = self.order.event.settings.mail_subject_order_paid email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text) - try: - self.order.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.order_paid', user, - invoices=[invoice] if invoice else [], - attach_tickets=True, - attach_ical=self.order.event.settings.mail_attach_ical - ) - except SendMailException: - logger.exception('Order paid email could not be sent') + self.order.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.order_paid', user, + invoices=[invoice] if invoice else [], + attach_tickets=True, + attach_ical=self.order.event.settings.mail_attach_ical + ) @property def refunded_amount(self): @@ -2915,45 +2900,40 @@ class OrderPosition(AbstractPosition): :param attach_tickets: Attach tickets of this order, if they are existing and ready to download :param attach_ical: Attach relevant ICS files """ - from pretix.base.services.mail import ( - SendMailException, mail, render_mail, - ) + from pretix.base.services.mail import mail, render_mail if not self.attendee_email: return with language(self.order.locale, self.order.event.settings.region): recipient = self.attendee_email - try: - email_content = render_mail(template, context) + email_content = render_mail(template, 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, - position=self, - invoices=invoices, - attach_tickets=attach_tickets, - attach_ical=attach_ical, - attach_other_files=attach_other_files, - ) - except SendMailException: - raise - else: - self.order.log_action( - log_entry_type, - user=user, - auth=auth, - data={ - 'subject': subject, - 'message': email_content, - 'recipient': recipient, - 'invoices': [i.pk for i in invoices] if invoices else [], - 'attach_tickets': attach_tickets, - 'attach_ical': attach_ical, - 'attach_other_files': attach_other_files, - 'attach_cached_files': [], - } - ) + mail( + recipient, subject, template, context, + self.event, self.order.locale, order=self.order, headers=headers, sender=sender, + position=self, + invoices=invoices, + attach_tickets=attach_tickets, + attach_ical=attach_ical, + attach_other_files=attach_other_files, + ) + self.order.log_action( + log_entry_type, + user=user, + auth=auth, + data={ + 'subject': subject, + 'message': email_content, + 'recipient': recipient, + 'invoices': [i.pk for i in invoices] if invoices else [], + 'attach_tickets': attach_tickets, + 'attach_ical': attach_ical, + 'attach_other_files': attach_other_files, + 'attach_cached_files': [], + } + ) def resend_link(self, user=None, auth=None): @@ -3529,18 +3509,10 @@ class InvoiceAddress(models.Model): def describe_transmission(self): from pretix.base.invoicing.transmission import transmission_types data = [] - t, __ = transmission_types.get(identifier=self.transmission_type) data.append((_("Transmission type"), t.public_name)) - form_data = t.transmission_info_to_form_data(self.transmission_info or {}) - for k, f in t.invoice_address_form_fields.items(): - v = form_data.get(k) - if v is True: - v = _("Yes") - elif v is False: - v = _("No") - if v: - data.append((f.label, v)) + if self.transmission_info: + data += t.describe_info(self.transmission_info, self.country, self.is_business) return data diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 3b4e919c9..12ccd4a46 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -239,7 +239,7 @@ class Voucher(LoggedModel): ) ) price_mode = models.CharField( - verbose_name=_("Price mode"), + verbose_name=_("Price effect"), max_length=100, choices=PRICE_MODES, default='none' diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 68d46cac4..6ae14591c 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -34,7 +34,7 @@ from phonenumber_field.modelfields import PhoneNumberField from pretix.base.email import get_email_context from pretix.base.i18n import language from pretix.base.models import User, Voucher -from pretix.base.services.mail import SendMailException, mail, render_mail +from pretix.base.services.mail import mail, render_mail from pretix.helpers import OF_SELF from ...helpers.format import format_map @@ -272,34 +272,30 @@ class WaitingListEntry(LoggedModel): with language(self.locale, self.event.settings.region): recipient = self.email - try: - email_content = render_mail(template, context) - subject = format_map(subject, context) - mail( - recipient, subject, template, context, - self.event, - self.locale, - headers=headers, - sender=sender, - auto_email=auto_email, - attach_other_files=attach_other_files, - attach_cached_files=attach_cached_files, - ) - except SendMailException: - raise - else: - self.log_action( - log_entry_type, - user=user, - auth=auth, - data={ - 'subject': subject, - 'message': email_content, - 'recipient': recipient, - 'attach_other_files': attach_other_files, - 'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [], - } - ) + email_content = render_mail(template, context) + subject = format_map(subject, context) + mail( + recipient, subject, template, context, + self.event, + self.locale, + headers=headers, + sender=sender, + auto_email=auto_email, + attach_other_files=attach_other_files, + attach_cached_files=attach_cached_files, + ) + self.log_action( + log_entry_type, + user=user, + auth=auth, + data={ + 'subject': subject, + 'message': email_content, + 'recipient': recipient, + 'attach_other_files': attach_other_files, + 'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [], + } + ) @staticmethod def clean_itemvar(event, item, variation): diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 67e54a824..31b97526d 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -1231,8 +1231,8 @@ class ManualPayment(BasePaymentProvider): def is_allowed(self, request: HttpRequest, total: Decimal=None): return 'pretix.plugins.manualpayment' in self.event.plugins and super().is_allowed(request, total) - def order_change_allowed(self, order: Order): - return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order) + def order_change_allowed(self, order: Order, request=None): + return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order, request) @property def public_name(self): @@ -1646,6 +1646,14 @@ class GiftCardPayment(BasePaymentProvider): 'transaction_id': trans.pk, } payment.confirm(send_mail=not is_early_special_case, generate_invoice=not is_early_special_case) + gc.log_action( + action='pretix.giftcards.transaction.payment', + data={ + 'value': trans.value, + 'acceptor_id': self.event.organizer.id, + 'acceptor_slug': self.event.organizer.slug + } + ) except PaymentException as e: payment.fail(info={'error': str(e)}) raise e @@ -1670,6 +1678,15 @@ class GiftCardPayment(BasePaymentProvider): 'transaction_id': trans.pk, } refund.done() + gc.log_action( + action='pretix.giftcards.transaction.refund', + data={ + 'value': refund.amount, + 'acceptor_id': self.event.organizer.id, + 'acceptor_slug': self.event.organizer.slug, + 'text': refund.comment, + } + ) @receiver(register_payment_providers, dispatch_uid="payment_free") diff --git a/src/pretix/base/plugins.py b/src/pretix/base/plugins.py index 4f32b6682..35647a1ba 100644 --- a/src/pretix/base/plugins.py +++ b/src/pretix/base/plugins.py @@ -65,7 +65,7 @@ def get_all_plugins(*, event=None, organizer=None) -> List[type]: if app.name in settings.PRETIX_PLUGINS_EXCLUDE: continue - level = getattr(app, "level", PLUGIN_LEVEL_EVENT) + level = getattr(meta, "level", PLUGIN_LEVEL_EVENT) if level == PLUGIN_LEVEL_EVENT: if event and hasattr(app, 'is_available'): if not app.is_available(event): diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index 694ea3598..012f01b85 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -36,7 +36,7 @@ from pretix.base.models import ( SubEvent, TaxRule, User, WaitingListEntry, ) from pretix.base.services.locking import LockTimeoutException -from pretix.base.services.mail import SendMailException, mail +from pretix.base.services.mail import mail from pretix.base.services.orders import ( OrderChangeManager, OrderError, _cancel_order, _try_auto_refund, ) @@ -53,17 +53,14 @@ logger = logging.getLogger(__name__) def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent): with language(wle.locale, wle.event.settings.region): email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event) - try: - mail( - wle.email, - format_map(subject, email_context), - message, - email_context, - wle.event, - locale=wle.locale - ) - except SendMailException: - logger.exception('Waiting list canceled email could not be sent') + mail( + wle.email, + format_map(subject, email_context), + message, + email_context, + wle.event, + locale=wle.locale + ) def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent, @@ -77,14 +74,11 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount, order=order, position_or_address=ia, event=order.event) real_subject = format_map(subject, email_context) - try: - order.send_mail( - real_subject, message, email_context, - 'pretix.event.order.email.event_canceled', - user, - ) - except SendMailException: - logger.exception('Order canceled email could not be sent') + order.send_mail( + real_subject, message, email_context, + 'pretix.event.order.email.event_canceled', + user, + ) for p in positions: if subevent and p.subevent_id != subevent.id: @@ -97,15 +91,12 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s refund_amount=refund_amount, position_or_address=p, order=order, position=p) - try: - order.send_mail( - real_subject, message, email_context, - 'pretix.event.order.email.event_canceled', - position=p, - user=user - ) - except SendMailException: - logger.exception('Order canceled email could not be sent to attendee') + order.send_mail( + real_subject, message, email_context, + 'pretix.event.order.email.event_canceled', + position=p, + user=user + ) @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 5a93f4d78..6080378f0 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -1639,7 +1639,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment if fee.tax_rule and not fee.tax_rule.pk: fee.tax_rule = None # TODO: deprecate - apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) + apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees]) if total != 0 and payments: @@ -1679,7 +1679,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment fees.append(pf) # Re-apply rounding as grand total has changed - apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) + apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees]) # Re-calculate to_pay as grand total has changed diff --git a/src/pretix/base/services/cleanup.py b/src/pretix/base/services/cleanup.py index 6667b2c3f..41376b920 100644 --- a/src/pretix/base/services/cleanup.py +++ b/src/pretix/base/services/cleanup.py @@ -23,11 +23,12 @@ from datetime import timedelta from django.conf import settings from django.core.management import call_command +from django.db.models import Exists, OuterRef from django.dispatch import receiver from django.utils.timezone import now from django_scopes import scopes_disabled -from pretix.base.models import CachedCombinedTicket, CachedTicket +from pretix.base.models import CachedCombinedTicket, CachedTicket, OutgoingMail from pretix.base.models.customers import CustomerSSOGrant from ..models import CachedFile, CartPosition, InvoiceAddress @@ -49,7 +50,18 @@ def clean_cart_positions(sender, **kwargs): @receiver(signal=periodic_task) @scopes_disabled() def clean_cached_files(sender, **kwargs): - for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()): + has_queued_email = Exists( + OutgoingMail.objects.filter( + should_attach_cached_files__pk=OuterRef("pk"), + status__in=( + OutgoingMail.STATUS_QUEUED, + OutgoingMail.STATUS_INFLIGHT, + OutgoingMail.STATUS_AWAITING_RETRY, + OutgoingMail.STATUS_FAILED, + ), + ) + ) + for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()).exclude(has_queued_email): cf.delete() diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index a21eec419..42565422e 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -39,10 +39,12 @@ import mimetypes import os import re import smtplib +import uuid import warnings +from datetime import timedelta from email.mime.image import MIMEImage from email.utils import formataddr -from typing import Any, Dict, List, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Union from urllib.parse import urljoin, urlparse from zoneinfo import ZoneInfo @@ -51,16 +53,16 @@ from celery import chain from celery.exceptions import MaxRetriesExceededError from django.conf import settings from django.core.files.storage import default_storage -from django.core.mail import ( - EmailMultiAlternatives, SafeMIMEMultipart, get_connection, -) +from django.core.mail import EmailMultiAlternatives, SafeMIMEMultipart from django.core.mail.message import SafeMIMEText -from django.db import transaction +from django.db import connection, transaction +from django.db.models import Q +from django.dispatch import receiver from django.template.loader import get_template from django.utils.html import escape from django.utils.timezone import now, override from django.utils.translation import gettext as _, pgettext -from django_scopes import scope, scopes_disabled +from django_scopes import scopes_disabled from i18nfield.strings import LazyI18nString from text_unidecode import unidecode @@ -70,12 +72,18 @@ from pretix.base.models import ( CachedFile, Customer, Event, Invoice, InvoiceAddress, Order, OrderPosition, Organizer, User, ) +from pretix.base.models.mail import OutgoingMail from pretix.base.services.invoices import invoice_pdf_task 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.base.signals import ( + email_filter, global_email_filter, periodic_task, +) from pretix.celery_app import app -from pretix.helpers.format import SafeFormatter, format_map +from pretix.helpers import OF_SELF +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 @@ -91,9 +99,18 @@ class TolerantDict(dict): class SendMailException(Exception): + """ + Deprecated, not thrown any more. + """ pass +class WithholdMailException(Exception): + def __init__(self, error, error_detail): + self.error = error + self.error_detail = error_detail + + def clean_sender_name(sender_name: str) -> str: # Even though we try to properly escape sender names, some characters seem to cause problems when the escaping # fails due to some forwardings, etc. @@ -137,7 +154,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None, customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None, attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None, - plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None): + plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None, + sensitive: bool=False): """ Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. @@ -193,132 +211,93 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La only allowed to use together with ``plain_text_only`` since HTML renderers add their own links. + :param sensitive: If set to ``True``, the email content will not be shown as part of log entries, used e.g. for + password resets. Bcc will also not be used. + :raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean that the email has been sent, just that it has been queued by the email backend. """ 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.') + settings_holder = event or organizer + headers = headers or {} + guid = uuid.uuid4() if auto_email: headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN' headers['Auto-Submitted'] = 'auto-generated' headers.setdefault('X-Mailer', 'pretix') + headers.setdefault('X-PX-Correlation', str(guid)) - with language(locale): + bcc = list(bcc or []) + if settings_holder and settings_holder.settings.mail_bcc and not sensitive: + for bcc_mail in settings_holder.settings.mail_bcc.split(','): + bcc.append(bcc_mail.strip()) + + if (settings_holder + and settings_holder.settings.mail_from in (settings.DEFAULT_FROM_EMAIL, settings.MAIL_FROM_ORGANIZERS) + and settings_holder.settings.contact_mail and not headers.get('Reply-To')): + headers['Reply-To'] = settings_holder.settings.contact_mail + + if settings_holder: + timezone = settings_holder.timezone + elif user: + timezone = ZoneInfo(user.timezone) + else: + timezone = ZoneInfo(settings.TIME_ZONE) + + if event and attach_tickets and not event.settings.mail_attach_tickets: + attach_tickets = False + + with language(locale), override(timezone): if isinstance(context, dict) and order: - try: - context.update({ - 'invoice_name': order.invoice_address.name, - 'invoice_company': order.invoice_address.company - }) - except InvoiceAddress.DoesNotExist: - context.update({ - 'invoice_name': '', - '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)) - sender = ( - sender or - (event.settings.get('mail_from') if event else None) or - (organizer.settings.get('mail_from') if organizer else None) or - settings.MAIL_FROM - ) - if event: - sender_name = clean_sender_name(event.settings.mail_from_name or str(event.name)) - sender = formataddr((sender_name, sender)) - elif organizer: - sender_name = clean_sender_name(organizer.settings.mail_from_name or str(organizer.name)) - sender = formataddr((sender_name, sender)) - else: - sender = formataddr((clean_sender_name(settings.PRETIX_INSTANCE_NAME), sender)) - - subject = raw_subject = str(subject).replace('\n', ' ').replace('\r', '')[:900] - signature = "" - - bcc = list(bcc or []) - - settings_holder = event or organizer - - if event: - timezone = event.timezone - elif user: - timezone = ZoneInfo(user.timezone) - elif organizer: - timezone = organizer.timezone - else: - timezone = ZoneInfo(settings.TIME_ZONE) + _autoextend_context(context, order) + # Build raw content + content_plain = render_mail(template, context, placeholder_mode=None) if settings_holder: - if settings_holder.settings.mail_bcc: - for bcc_mail in settings_holder.settings.mail_bcc.split(','): - bcc.append(bcc_mail.strip()) - - if settings_holder.settings.mail_from in (settings.DEFAULT_FROM_EMAIL, settings.MAIL_FROM_ORGANIZERS) \ - and settings_holder.settings.contact_mail and not headers.get('Reply-To'): - headers['Reply-To'] = settings_holder.settings.contact_mail - - subject = prefix_subject(settings_holder, subject) - - body_plain += "\r\n\r\n-- \r\n" - signature = str(settings_holder.settings.get('mail_text_signature')) - if signature: - signature = signature.format(event=event.name if event else '') - body_plain += signature - body_plain += "\r\n\r\n-- \r\n" + else: + signature = "" - if event: - renderer = event.get_html_mail_renderer() + # Build full plain-text body + if not isinstance(content_plain, FormattedString): + body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN) + else: + body_plain = content_plain + body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links) - if order and order.testmode: - subject = "[TESTMODE] " + subject + # Build subject + if not isinstance(subject, FormattedString): + subject = format_map(subject, context) - if order and position and not no_order_links: - body_plain += _( - "You are receiving this email because someone placed an order for {event} for you." - ).format(event=event.name) - body_plain += "\r\n" - 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( - order.event, 'presale:event.order.position', kwargs={ - 'order': order.code, - 'secret': position.web_secret, - 'position': position.positionid, - } - ) - ) - elif order and not no_order_links: - body_plain += _( - "You are receiving this email because you placed an order for {event}." - ).format(event=event.name) - body_plain += "\r\n" - 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( - order.event, 'presale:event.order.open', kwargs={ - 'order': order.code, - 'secret': order.secret, - 'hash': order.email_confirm_secret() - } - ) - ) - body_plain += "\r\n" + subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900] + if settings_holder: + subject = prefix_subject(settings_holder, subject) + if (order and order.testmode) or (not order and event and event.testmode): + subject = "[TESTMODE] " + subject + + # Build sender + sender = _full_sender(sender, event, organizer) + + # Build HTML body + if plain_text_only: + body_html = None + else: + if event: + renderer = event.get_html_mail_renderer() + else: + renderer = ClassicMailRenderer(None, organizer) - 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: + if 'context' in inspect.signature(renderer.render).parameters: body_html = renderer.render(content_plain, signature, raw_subject, order, position, context) elif 'position' in inspect.signature(renderer.render).parameters: # Backwards compatibility @@ -336,28 +315,37 @@ 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) + m = OutgoingMail.objects.create( + organizer=organizer, + event=event, + order=order, + orderposition=position, + customer=customer, + user=user, + to=[email.lower()] if isinstance(email, str) else [e.lower() for e in email], + cc=[e.lower() for e in cc] if cc else [], + bcc=[e.lower() for e in bcc] if bcc else [], + subject=subject, + body_plain=body_plain, + body_html=body_html, + sender=sender, + headers=headers or {}, + should_attach_tickets=attach_tickets, + should_attach_ical=attach_ical, + should_attach_other_files=attach_other_files or [], + sensitive=sensitive, + ) + if invoices and not position: + m.should_attach_invoices.add(*invoices) + if attach_cached_files: + for cf in attach_cached_files: + if not isinstance(cf, CachedFile): + m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf)) + else: + m.should_attach_cached_files.add(cf) send_task = mail_send_task.si( - to=[email] if isinstance(email, str) else list(email), - cc=cc, - bcc=bcc, - subject=subject, - body=body_plain, - html=body_html, - sender=sender, - event=event.id if event else None, - headers=headers, - invoices=[i.pk for i in invoices] if invoices and not position else [], - order=order.pk if order else None, - position=position.pk if position else None, - attach_tickets=attach_tickets, - attach_ical=attach_ical, - user=user.pk if user else None, - organizer=organizer.pk if organizer else None, - customer=customer.pk if customer else None, - attach_cached_files=[(cf.id if isinstance(cf, CachedFile) else cf) for cf in attach_cached_files] if attach_cached_files else [], - attach_other_files=attach_other_files, + outgoing_mail=m.id ) if invoices: @@ -392,194 +380,261 @@ class CustomEmail(EmailMultiAlternatives): @app.task(base=TransactionAwareTask, bind=True, acks_late=True) -def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str, - event: int = None, position: int = None, headers: dict = None, cc: List[str] = None, bcc: List[str] = None, - invoices: List[int] = None, order: int = None, attach_tickets=False, user=None, - organizer=None, customer=None, attach_ical=False, attach_cached_files: List[int] = None, - attach_other_files: List[str] = None) -> bool: - email = CustomEmail(subject, body, sender, to=to, cc=cc, bcc=bcc, headers=headers) - if html is not None: +def mail_send_task(self, **kwargs) -> bool: + if "outgoing_mail" in kwargs: + outgoing_mail = kwargs.get("outgoing_mail") + elif "to" in kwargs: + # May only occur while upgrading from pretix versions before OutgoingMail when celery tasks are still in-queue + # during the upgrade. Can be removed after 2026.2.x is released, and then the signature can be changed to + # mail_send_task(self, *, outgoing_mail) + with scopes_disabled(): + mail_send(**kwargs) + return False + else: + raise ValueError("Unknown arguments") + + with transaction.atomic(): + try: + outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail) + except OutgoingMail.DoesNotExist: + logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}") + return False + if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT: + logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}") + return False + elif outgoing_mail.status not in (OutgoingMail.STATUS_AWAITING_RETRY, OutgoingMail.STATUS_QUEUED): + logger.info(f"Ignoring job for email {outgoing_mail.guid} in final state {outgoing_mail.status}") + return False + outgoing_mail.status = OutgoingMail.STATUS_INFLIGHT + outgoing_mail.inflight_since = now() + outgoing_mail.save(update_fields=["status", "inflight_since"]) + + headers = dict(outgoing_mail.headers) + headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid)) + email = CustomEmail( + subject=outgoing_mail.subject, + body=outgoing_mail.body_plain, + from_email=outgoing_mail.sender, + to=outgoing_mail.to, + cc=outgoing_mail.cc, + bcc=outgoing_mail.bcc, + headers=headers, + ) + + # Rewrite all tags from real URLs or data URLs to inline attachments referred to by content ID + if outgoing_mail.body_html is not None: html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET) - html_with_cid, cid_images = replace_images_with_cid_paths(html) + html_with_cid, cid_images = replace_images_with_cid_paths(outgoing_mail.body_html) html_message.attach(SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET)) attach_cid_images(html_message, cid_images, verify_ssl=True) email.attach_alternative(html_message, "multipart/related") - log_target = None + log_target, error_log_action_type = outgoing_mail.log_parameters() + invoices_attached = [] - if user: - user = User.objects.get(pk=user) - error_log_action_type = 'pretix.user.email.error' - log_target = user + with outgoing_mail.scope_manager(): + # Attach tickets + if outgoing_mail.should_attach_tickets and outgoing_mail.order: + with language(outgoing_mail.order.locale, outgoing_mail.event.settings.region): + args = [] + attach_size = 0 + for name, ct in get_tickets_for_order(outgoing_mail.order, base_position=outgoing_mail.orderposition): + try: + content = ct.file.read() + args.append((name, content, ct.type)) + attach_size += len(content) + except Exception as e: + # This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out + # why (probably some race condition with ticket cache invalidation?), so retry later. + try: + logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry') + retry_after = 60 + outgoing_mail.error = "Tickets not ready" + outgoing_mail.error_detail = str(e) + outgoing_mail.sent = now() + outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY + outgoing_mail.retry_after = now() + timedelta(seconds=retry_after) + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", + "actual_attachments"]) + self.retry(max_retries=5, countdown=retry_after) + except MaxRetriesExceededError: + # Well then, something is really wrong, let's send it without attachment before we + # don't send at all + logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment') + pass - if event: - with scopes_disabled(): - event = Event.objects.get(id=event) - organizer = event.organizer - backend = event.get_mail_backend() - cm = lambda: scope(organizer=event.organizer) # noqa - elif organizer: - with scopes_disabled(): - organizer = Organizer.objects.get(id=organizer) - backend = organizer.get_mail_backend() - cm = lambda: scope(organizer=organizer) # noqa - else: - backend = get_connection(fail_silently=False) - cm = lambda: scopes_disabled() # noqa - - with cm(): - if customer: - customer = Customer.objects.get(pk=customer) - if not user: - error_log_action_type = 'pretix.customer.email.error' - log_target = customer - - if event: - if order: - try: - order = event.orders.get(pk=order) - error_log_action_type = 'pretix.event.order.email.error' - log_target = order - except Order.DoesNotExist: - order = None + if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024: + # Do not attach more than (limit - 1 MB) in tickets (1MB space for invoice, email itself, …), + # it will bounce way too often. + # 1 MB is the buffer for the rest of the email (text, invoice, calendar, pictures) + # 1.37 is the factor for base64 encoding https://en.wikipedia.org/wiki/Base64 + for a in args: + try: + email.attach(*a) + except: + pass else: - with language(order.locale, event.settings.region): - if not event.settings.mail_attach_tickets: - attach_tickets = False - if position: - try: - position = order.positions.get(pk=position) - except OrderPosition.DoesNotExist: - attach_tickets = False - if attach_tickets: - args = [] - attach_size = 0 - for name, ct in get_tickets_for_order(order, base_position=position): - try: - content = ct.file.read() - args.append((name, content, ct.type)) - attach_size += len(content) - except: - # This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out - # why (probably some race condition with ticket cache invalidation?), so retry later. - try: - self.retry(max_retries=5, countdown=60) - except MaxRetriesExceededError: - # Well then, something is really wrong, let's send it without attachment before we - # don't sent at all - logger.exception('Could not attach invoice to email') - pass + outgoing_mail.order.log_action( + 'pretix.event.order.email.attachments.skipped', + data={ + 'subject': 'Attachments skipped', + 'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size), + 'recipient': '', + 'invoices': [], + } + ) - if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024: - # Do not attach more than (limit - 1 MB) in tickets (1MB space for invoice, email itself, …), - # it will bounce way to often. - # 1 MB is the buffer for the rest of the email (text, invoice, calendar, pictures) - # 1.37 is the factor for base64 encoding https://en.wikipedia.org/wiki/Base64 - for a in args: - try: - email.attach(*a) - except: - pass - else: - order.log_action( - 'pretix.event.order.email.attachments.skipped', - data={ - 'subject': 'Attachments skipped', - 'message': 'Attachment have not been send because {} bytes are likely too large to arrive.'.format(attach_size), - 'recipient': '', - 'invoices': [], - } - ) - if attach_ical: - fname = re.sub('[^a-zA-Z0-9 ]', '-', unidecode(pgettext('attachment_filename', 'Calendar invite'))) - for i, cal in enumerate(get_private_icals(event, [position] if position else order.positions.all())): - email.attach('{}{}.ics'.format(fname, f'-{i + 1}' if i > 0 else ''), cal.serialize(), 'text/calendar') - - email = email_filter.send_chained(event, 'message', message=email, order=order, user=user) + # Attach calendar files + if outgoing_mail.should_attach_ical and outgoing_mail.order: + fname = re.sub('[^a-zA-Z0-9 ]', '-', unidecode(pgettext('attachment_filename', 'Calendar invite'))) + icals = get_private_icals( + outgoing_mail.event, + [outgoing_mail.orderposition] if outgoing_mail.orderposition else outgoing_mail.order.positions.all() + ) + for i, cal in enumerate(icals): + name = '{}{}.ics'.format(fname, f'-{i + 1}' if i > 0 else '') + content = cal.serialize() + mimetype = 'text/calendar' + email.attach(name, content, mimetype) invoices_to_mark_transmitted = [] - if invoices: - invoices = Invoice.objects.filter(pk__in=invoices) - for inv in invoices: - if inv.file: - try: - # We try to give the invoice a more human-readable name, e.g. "Invoice_ABC-123.pdf" instead of - # just "ABC-123.pdf", but we only do so if our currently selected language allows to do this - # as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this - # has shown to cause deliverability problems of the email and deliverability wins. - with language(order.locale if order else inv.locale, event.settings.region if event else None): - filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf' - if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename): - filename = inv.number.replace(' ', '_') + '.pdf' - filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename) - with language(inv.order.locale): - email.attach( - filename, - inv.file.file.read(), - 'application/pdf' + for inv in outgoing_mail.should_attach_invoices.all(): + if inv.file: + try: + # We try to give the invoice a more human-readable name, e.g. "Invoice_ABC-123.pdf" instead of + # just "ABC-123.pdf", but we only do so if our currently selected language allows to do this + # as ASCII text. For example, we would not want a "فاتورة_" prefix for our filename since this + # has shown to cause deliverability problems of the email and deliverability wins. + with language(outgoing_mail.order.locale if outgoing_mail.order else inv.locale, outgoing_mail.event.settings.region): + filename = pgettext('invoice', 'Invoice {num}').format(num=inv.number).replace(' ', '_') + '.pdf' + if not re.match("^[a-zA-Z0-9-_%./,&:# ]+$", filename): + filename = inv.number.replace(' ', '_') + '.pdf' + filename = re.sub("[^a-zA-Z0-9-_.]+", "_", filename) + content = inv.file.file.read() + with language(inv.order.locale): + email.attach( + filename, + content, + 'application/pdf' + ) + invoices_attached.append(inv) + except Exception: + logger.exception(f'Could not attach invoice to email {outgoing_mail.guid}') + pass + else: + if inv.transmission_type == "email": + # Mark invoice as sent when it was sent to the requested address *either* at the time of invoice + # creation *or* as of right now. + expected_recipients = [ + (inv.invoice_to_transmission_info or {}).get("transmission_email_address") + or inv.order.email, + ] + try: + expected_recipients.append( + (inv.order.invoice_address.transmission_info or {}).get("transmission_email_address") + or inv.order.email ) + except InvoiceAddress.DoesNotExist: + pass + expected_recipients = {e.lower() for e in expected_recipients if e} + if any(t in expected_recipients for t in outgoing_mail.to): + invoices_to_mark_transmitted.append(inv) - if inv.transmission_type == "email": - # Mark invoice as sent when it was sent to the requested address *either* at the time of - # invoice creation *or* as of right now. - expected_recipients = [ - (inv.invoice_to_transmission_info or {}).get("transmission_email_address") - or inv.order.email, - ] - try: - expected_recipients.append( - (inv.order.invoice_address.transmission_info or {}).get("transmission_email_address") - or inv.order.email - ) - except InvoiceAddress.DoesNotExist: - pass - if any(t in expected_recipients for t in to): - invoices_to_mark_transmitted.append(inv) - except: - logger.exception('Could not attach invoice to email') - pass + for fname in outgoing_mail.should_attach_other_files: + ftype, _ = mimetypes.guess_type(fname) + data = default_storage.open(fname).read() + try: + email.attach( + clean_filename(os.path.basename(fname)), + data, + ftype + ) + except: + logger.exception(f'Could not attach file to email {outgoing_mail.guid}') + pass - if attach_other_files: - for fname in attach_other_files: - ftype, _ = mimetypes.guess_type(fname) - data = default_storage.open(fname).read() + for cf in outgoing_mail.should_attach_cached_files.all(): + if cf.file: try: email.attach( - clean_filename(os.path.basename(fname)), - data, - ftype + cf.filename, + cf.file.file.read(), + cf.type, ) except: - logger.exception('Could not attach file to email') + logger.exception(f'Could not attach file to email {outgoing_mail.guid}') pass - if attach_cached_files: - for cf in CachedFile.objects.filter(id__in=attach_cached_files): - if cf.file: - try: - email.attach( - cf.filename, - cf.file.file.read(), - cf.type, - ) - except: - logger.exception('Could not attach file to email') - pass - - email = global_email_filter.send_chained(event, 'message', message=email, user=user, order=order, - organizer=organizer, customer=customer) + outgoing_mail.actual_attachments = [ + { + "name": a[0], + "size": len(a[1]), + "type": a[2], + } for a in email.attachments + ] + try: + if outgoing_mail.event: + with outgoing_mail.scope_manager(): + email = email_filter.send_chained( + sender=outgoing_mail.event, + chain_kwarg_name='message', + message=email, + order=outgoing_mail.order, + user=outgoing_mail.user, + outgoing_mail=outgoing_mail, + ) + + email = global_email_filter.send_chained( + sender=outgoing_mail.event, + chain_kwarg_name='message', + message=email, + user=outgoing_mail.user, + order=outgoing_mail.order, + organizer=outgoing_mail.organizer, + customer=outgoing_mail.customer, + outgoing_mail=outgoing_mail, + ) + except WithholdMailException as e: + outgoing_mail.status = OutgoingMail.STATUS_WITHHELD + outgoing_mail.error = e.error + outgoing_mail.error_detail = e.error_detail + outgoing_mail.sent = now() + outgoing_mail.retry_after = None + outgoing_mail.actual_attachments = [ + { + "name": a[0], + "size": len(a[1]), + "type": a[2], + } for a in email.attachments + ] + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", "actual_attachments"]) + logger.info(f"Email {outgoing_mail.guid} withheld") + return False + + # Seems duplicate, but needs to be in this order since plugins might change this + outgoing_mail.actual_attachments = [ + { + "name": a[0], + "size": len(a[1]), + "type": a[2], + } for a in email.attachments + ] + backend = outgoing_mail.get_mail_backend() try: backend.send_messages([email]) - except (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused) as e: - if e.smtp_code in (101, 111, 421, 422, 431, 432, 442, 447, 452): - if e.smtp_code == 432 and settings.HAS_REDIS: - # This is likely Microsoft Exchange Online which has a pretty bad rate limit of max. 3 concurrent - # SMTP connections which is *easily* exceeded with many celery threads. Just retrying with exponential - # backoff won't be good enough if we have a lot of emails, instead we'll need to make sure our retry - # intervals scatter such that the email won't all be retried at the same time again and cause the - # same problem. - # See also https://docs.microsoft.com/en-us/exchange/troubleshoot/send-emails/smtp-submission-improvements + except Exception as e: + logger.exception(f'Error sending email {outgoing_mail.guid}') + retry_strategy = _retry_strategy(e) + err, err_detail = _format_error(e) + + outgoing_mail.error = err + outgoing_mail.error_detail = err_detail + outgoing_mail.sent = now() + + # Run retries + try: + if retry_strategy == "microsoft_concurrency" and settings.HAS_REDIS: from django_redis import get_redis_connection redis_key = "pretix_mail_retry_" + hashlib.sha1(f"{getattr(backend, 'username', '_')}@{getattr(backend, 'host', '_')}".encode()).hexdigest() @@ -589,124 +644,80 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st max_retries = 10 retry_after = min(30 + cnt * 10, 1800) - else: - # Most likely some other kind of temporary failure, retry again (but pretty soon) + + outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY + outgoing_mail.retry_after = now() + timedelta(seconds=retry_after) + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", "actual_attachments"]) + self.retry(max_retries=max_retries, countdown=retry_after) # throws RetryException, ends function flow + elif retry_strategy in ("microsoft_concurrency", "quick"): max_retries = 5 retry_after = [10, 30, 60, 300, 900, 900][self.request.retries] + outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY + outgoing_mail.retry_after = now() + timedelta(seconds=retry_after) + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", "actual_attachments"]) + self.retry(max_retries=max_retries, countdown=retry_after) # throws RetryException, ends function flow - try: - self.retry(max_retries=max_retries, countdown=retry_after) - except MaxRetriesExceededError: - if log_target: - log_target.log_action( - error_log_action_type, - data={ - 'subject': 'SMTP code {}, max retries exceeded'.format(e.smtp_code), - 'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error), - 'recipient': '', - 'invoices': [], - } - ) - for i in invoices_to_mark_transmitted: - i.set_transmission_failed(provider="email_pdf", data={ - "reason": "exception", - "exception": "SMTP code {}, max retries exceeded".format(e.smtp_code), - }) - raise e + elif retry_strategy == "slow": + retry_after = [60, 300, 600, 1200, 1800, 1800][self.request.retries] + outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY + outgoing_mail.retry_after = now() + timedelta(seconds=retry_after) + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", "actual_attachments"]) + self.retry(max_retries=5, countdown=retry_after) # throws RetryException, ends function flow - logger.exception('Error sending email') + except MaxRetriesExceededError: + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": "{}, max retries exceeded".format(err), + "detail": err_detail, + }) + + if log_target: + log_target.log_action( + error_log_action_type, + data={ + 'subject': f'{err} (max retries exceeded)', + 'message': err_detail, + 'recipient': '', + 'invoices': [], + } + ) + + outgoing_mail.status = OutgoingMail.STATUS_FAILED + outgoing_mail.sent = now() + outgoing_mail.retry_after = None + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", "actual_attachments"]) + return False + + # If we reach this, it's a non-retryable error + outgoing_mail.status = OutgoingMail.STATUS_FAILED + outgoing_mail.sent = now() + outgoing_mail.retry_after = None + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after", "actual_attachments"]) + for i in invoices_to_mark_transmitted: + i.set_transmission_failed(provider="email_pdf", data={ + "reason": "exception", + "exception": err, + "detail": err_detail, + }) if log_target: log_target.log_action( error_log_action_type, data={ - 'subject': 'SMTP code {}'.format(e.smtp_code), - 'message': e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error), + 'subject': err, + 'message': err_detail, 'recipient': '', 'invoices': [], } ) - for i in invoices_to_mark_transmitted: - i.set_transmission_failed(provider="email_pdf", data={ - "reason": "exception", - "exception": "SMTP code {}".format(e.smtp_code), - }) - - raise SendMailException('Failed to send an email to {}.'.format(to)) - except smtplib.SMTPRecipientsRefused as e: - smtp_codes = [a[0] for a in e.recipients.values()] - - if not any(c >= 500 for c in smtp_codes) or any(b'Message is too large' in a[1] for a in e.recipients.values()): - # This is not a permanent failure (mailbox full, service unavailable), retry later, but with large - # intervals. One would think that "Message is too lage" is a permanent failure, but apparently it is not. - # We have documented cases of emails to Microsoft returning the error occasionally and then later - # allowing the very same email. - try: - self.retry(max_retries=5, countdown=[60, 300, 600, 1200, 1800, 1800][self.request.retries]) - except MaxRetriesExceededError: - # ignore and go on with logging the error - pass - - logger.exception('Error sending email') - if log_target: - message = [] - for e, val in e.recipients.items(): - message.append(f'{e}: {val[0]} {val[1].decode()}') - - log_target.log_action( - error_log_action_type, - data={ - 'subject': 'SMTP error', - 'message': '\n'.join(message), - 'recipient': '', - 'invoices': [], - } - ) - for i in invoices_to_mark_transmitted: - i.set_transmission_failed(provider="email_pdf", data={ - "reason": "exception", - "exception": "SMTP error", - }) - - raise SendMailException('Failed to send an email to {}.'.format(to)) - except Exception as e: - if isinstance(e, OSError) and not isinstance(e, smtplib.SMTPNotSupportedError): - try: - self.retry(max_retries=5, countdown=[10, 30, 60, 300, 900, 900][self.request.retries]) - except MaxRetriesExceededError: - if log_target: - log_target.log_action( - error_log_action_type, - data={ - 'subject': 'Internal error', - 'message': f'Max retries exceeded after error "{str(e)}"', - 'recipient': '', - 'invoices': [], - } - ) - for i in invoices_to_mark_transmitted: - i.set_transmission_failed(provider="email_pdf", data={ - "reason": "exception", - "exception": "Internal error", - }) - raise e - if log_target: - log_target.log_action( - error_log_action_type, - data={ - 'subject': 'Internal error', - 'message': str(e), - 'recipient': '', - 'invoices': [], - } - ) - for i in invoices_to_mark_transmitted: - i.set_transmission_failed(provider="email_pdf", data={ - "reason": "exception", - "exception": "Internal error", - }) - logger.exception('Error sending email') - raise SendMailException('Failed to send an email to {}.'.format(to)) + return False else: + outgoing_mail.status = OutgoingMail.STATUS_SENT + outgoing_mail.error = None + outgoing_mail.error_detail = None + outgoing_mail.sent = now() + outgoing_mail.retry_after = None + outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "actual_attachments", "retry_after"]) for i in invoices_to_mark_transmitted: if i.transmission_status != Invoice.TRANSMISSION_STATUS_COMPLETED: i.transmission_date = now() @@ -715,7 +726,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st i.transmission_info = { "sent": [ { - "recipients": to, + "recipients": outgoing_mail.to, "datetime": now().isoformat(), } ] @@ -727,7 +738,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st elif i.transmission_provider == "email_pdf": i.transmission_info["sent"].append( { - "recipients": to, + "recipients": outgoing_mail.to, "datetime": now().isoformat(), } ) @@ -741,24 +752,67 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st "transmission_provider": "email_pdf", "transmission_type": "email", "data": { - "recipients": [to], + "recipients": outgoing_mail.to, }, } ) + return True -def mail_send(*args, **kwargs): - mail_send_task.apply_async(args=args, kwargs=kwargs) +def mail_send(to: List[str], subject: str, body: str, html: Optional[str], sender: str, + event: int | Event = None, position: int | OrderPosition = None, headers: dict = None, + cc: List[str] = None, bcc: List[str] = None, invoices: List[int | Invoice] = None, order: int | Order = None, + attach_tickets=False, user: int | User=None, organizer: int | Organizer=None, customer: int | Customer=None, + attach_ical=False, attach_cached_files: List[int | CachedFile] = None, attach_other_files: List[str] = None): + """ + Low-level function to send mails, kept for backwards-compatibility. You should usually use mail() instead. + """ + m = OutgoingMail.objects.create( + organizer_id=organizer.pk if isinstance(organizer, Organizer) else organizer, + event_id=event.pk if isinstance(event, Event) else event, + order_id=order.pk if isinstance(order, Order) else order, + orderposition_id=position.pk if isinstance(position, OrderPosition) else position, + customer_id=customer.pk if isinstance(customer, Customer) else customer, + user_id=user.pk if isinstance(user, User) else user, + to=[to.lower()] if isinstance(to, str) else [e.lower() for e in to], + cc=[e.lower() for e in cc] if cc else [], + bcc=[e.lower() for e in bcc] if bcc else [], + subject=subject, + body_plain=body, + body_html=html, + sender=sender, + headers=headers or {}, + should_attach_tickets=attach_tickets, + should_attach_ical=attach_ical, + should_attach_other_files=attach_other_files or [], + ) + if invoices and not position: + if isinstance(invoices[0], int): + invoices = Invoice.objects.filter(pk__in=invoices) + m.should_attach_invoices.add(*invoices) + if attach_cached_files: + for cf in attach_cached_files: + if not isinstance(cf, CachedFile): + m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf)) + else: + m.should_attach_cached_files.add(cf) + + mail_send_task.apply_async(kwargs={"outgoing_mail": m.pk}) -def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN): +def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter.MODE_RICH_TO_PLAIN): if isinstance(template, LazyI18nString): body = str(template) if context and placeholder_mode: 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 @@ -866,3 +920,203 @@ def normalize_image_url(url): else: url = urljoin(settings.MEDIA_URL, url) return url + + +def _autoextend_context(context, order): + try: + context.update({ + 'invoice_name': order.invoice_address.name, + 'invoice_company': order.invoice_address.company + }) + except InvoiceAddress.DoesNotExist: + context.update({ + 'invoice_name': '', + 'invoice_company': '' + }) + + +def _full_sender(sender_address, event, organizer): + sender_address = ( + sender_address or + (event.settings.get('mail_from') if event else None) or + (organizer.settings.get('mail_from') if organizer else None) or + settings.MAIL_FROM + ) + if event: + sender_name = event.settings.mail_from_name or str(event.name) + elif organizer: + sender_name = organizer.settings.mail_from_name or str(organizer.name) + else: + sender_name = settings.PRETIX_INSTANCE_NAME + + sender = formataddr((clean_sender_name(sender_name), sender_address)) + return sender + + +def _wrap_plain_body(content_plain, signature, event, order, position, no_order_links): + body_plain = content_plain + body_plain += "\r\n\r\n-- \r\n" + + if signature: + signature = format_map(signature, {"event": event.name if event else ''}) + body_plain += signature + body_plain += "\r\n\r\n-- \r\n" + + if event and order and position and not no_order_links: + body_plain += _( + "You are receiving this email because someone placed an order for {event} for you." + ).format(event=event.name) + body_plain += "\r\n" + body_plain += _( + "You can view your order details at the following URL:\n{orderurl}." + ).replace("\n", "\r\n").format( + orderurl=build_absolute_uri( + order.event, 'presale:event.order.position', kwargs={ + 'order': order.code, + 'secret': position.web_secret, + 'position': position.positionid, + } + ) + ) + elif event and order and not no_order_links: + body_plain += _( + "You are receiving this email because you placed an order for {event}." + ).format(event=event.name) + body_plain += "\r\n" + 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( + order.event, 'presale:event.order.open', kwargs={ + 'order': order.code, + 'secret': order.secret, + 'hash': order.email_confirm_secret() + } + ) + ) + body_plain += "\r\n" + + return body_plain + + +def _retry_strategy(e: Exception): + if isinstance(e, (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused)): + if e.smtp_code == 432: + # This is likely Microsoft Exchange Online which has a pretty bad rate limit of max. 3 concurrent + # SMTP connections which is *easily* exceeded with many celery threads. Just retrying with exponential + # backoff won't be good enough if we have a lot of emails, instead we'll need to make sure our retry + # intervals scatter such that the email won't all be retried at the same time again and cause the + # same problem. + # See also https://docs.microsoft.com/en-us/exchange/troubleshoot/send-emails/smtp-submission-improvements + return "microsoft_concurrency" + + if e.smtp_code in (101, 111, 421, 422, 431, 432, 442, 447, 452): + return "quick" + + elif isinstance(e, smtplib.SMTPRecipientsRefused): + smtp_codes = [a[0] for a in e.recipients.values()] + + if not any(c >= 500 for c in smtp_codes) or any(b'Message is too large' in a[1] for a in e.recipients.values()): + # This is not a permanent failure (mailbox full, service unavailable), retry later, but with large + # intervals. One would think that "Message is too lage" is a permanent failure, but apparently it is not. + # We have documented cases of emails to Microsoft returning the error occasionally and then later + # allowing the very same email. + return "slow" + + elif isinstance(e, OSError) and not isinstance(e, smtplib.SMTPNotSupportedError): + # Most likely some other kind of temporary failure, retry again (but pretty soon) + return "quick" + + +def _format_error(e: Exception): + if isinstance(e, (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused)): + return 'SMTP code {}'.format(e.smtp_code), e.smtp_error.decode() if isinstance(e.smtp_error, bytes) else str(e.smtp_error) + elif isinstance(e, smtplib.SMTPRecipientsRefused): + message = [] + for e, val in e.recipients.items(): + message.append(f'{e}: {val[0]} {val[1].decode()}') + return 'SMTP recipients refudes', '\n'.join(message) + else: + return 'Internal error', str(e) + + +def _is_queue_long(queue_name="mail"): + """ + Checks an estimate if there is currently a long celery queue for emails. If so, + there's no reason to retry stuck emails, because they are stuck because of the + queue and we don't need to add more oil to the fire. + + This does not need to be perfect, as it is safe to run the same task twice, it just + wastes ressources. + """ + if not settings.HAS_CELERY: + return False + if not settings.CELERY_BROKER_URL.startswith("redis://"): + return False # check not supported + priority_steps = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("priority_steps", [0]) + sep = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("sep", ":") + client = app.broker_connection().channel().client + queue_length = 0 + for prio in priority_steps: + if prio: + qname = f"{queue_name}{sep}{prio}" + else: + qname = queue_name + queue_length += client.llen(qname) + + return queue_length > 100 + + +@receiver(signal=periodic_task) +@scopes_disabled() +def retry_stuck_inflight_mails(sender, **kwargs): + """ + Retry emails that are stuck in "inflight" state, e.g. their celery task just died. + """ + with transaction.atomic(): + for m in OutgoingMail.objects.filter( + status=OutgoingMail.STATUS_INFLIGHT, + inflight_since__lt=now() - timedelta(hours=1), + ).select_for_update(of=OF_SELF, skip_locked=connection.features.has_select_for_update_skip_locked): + m.status = OutgoingMail.STATUS_QUEUED + m.save() + mail_send_task.apply_async(kwargs={"outgoing_mail": m.pk}) + + +@receiver(signal=periodic_task) +@scopes_disabled() +def retry_stuck_queued_mails(sender, **kwargs): + """ + Retry emails that are stuck in "queued" state, e.g. their celery task never started. We do this only + when there is currently almost no queue, to avoid many tasks being scheduled for the same mail if that + mail is still waiting in the queue (even if that would be safe, all tasks except the first one would be a no-op, + but it would create many more useless tasks in a high-load situation). + """ + if _is_queue_long(): + logger.info("Do not retry stuck mails as the queue is long.") + return + + for m in OutgoingMail.objects.filter( + Q( + status=OutgoingMail.STATUS_QUEUED, + created__lt=now() - timedelta(hours=1), + ) | Q( + status=OutgoingMail.STATUS_AWAITING_RETRY, + retry_after__lt=now() - timedelta(hours=1), + ) + ): + mail_send_task.apply_async(kwargs={"outgoing_mail": m.pk}) + + +@receiver(signal=periodic_task) +@scopes_disabled() +def delete_old_emails(sender, **kwargs): + """ + OutgoingMail is currently not intended to be an archive, because it would be hard to do in a + privacy-first design, so we delete after some time. + """ + cutoff = now() - timedelta(seconds=settings.OUTGOING_MAIL_RETENTION) + OutgoingMail.objects.filter( + Q(sent__lt=cutoff) | + Q(sent__isnull=True, created__lt=cutoff) + ).delete() diff --git a/src/pretix/base/services/notifications.py b/src/pretix/base/services/notifications.py index 5a75dfafb..0a70e7c32 100644 --- a/src/pretix/base/services/notifications.py +++ b/src/pretix/base/services/notifications.py @@ -19,6 +19,8 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import uuid + import css_inline from django.conf import settings from django.template.loader import get_template @@ -26,7 +28,9 @@ from django.utils.timezone import override from django_scopes import scope, scopes_disabled from pretix.base.i18n import language -from pretix.base.models import LogEntry, NotificationSetting, User +from pretix.base.models import ( + LogEntry, NotificationSetting, OutgoingMail, User, +) from pretix.base.notifications import Notification, get_all_notification_types from pretix.base.services.mail import mail_send_task from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask @@ -153,16 +157,26 @@ def send_notification_mail(notification: Notification, user: User): tpl_plain = get_template('pretixbase/email/notification.txt') body_plain = tpl_plain.render(ctx) - mail_send_task.apply_async(kwargs={ - 'to': [user.email], - 'subject': '[{}] {}: {}'.format( + guid = uuid.uuid4() + m = OutgoingMail.objects.create( + guid=guid, + user=user, + to=[user.email], + subject='[{}] {}: {}'.format( settings.PRETIX_INSTANCE_NAME, notification.event.settings.mail_prefix or notification.event.slug.upper(), notification.title ), - 'body': body_plain, - 'html': body_html, - 'sender': settings.MAIL_FROM_NOTIFICATIONS, - 'headers': {}, - 'user': user.pk + body_plain=body_plain, + body_html=body_html, + sender=settings.MAIL_FROM_NOTIFICATIONS, + headers={ + 'X-Auto-Response-Suppress': 'OOF, NRN, AutoReply, RN', + 'Auto-Submitted': 'auto-generated', + 'X-Mailer': 'pretix', + 'X-PX-Correlation': str(guid), + }, + ) + mail_send_task.apply_async(kwargs={ + 'outgoing_mail': m.pk, }) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 365247483..904113e15 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -90,7 +90,6 @@ from pretix.base.services.invoices import ( from pretix.base.services.locking import ( LOCK_TRUST_WINDOW, LockTimeoutException, lock_objects, ) -from pretix.base.services.mail import SendMailException from pretix.base.services.memberships import ( create_membership, validate_memberships_in_order, ) @@ -248,6 +247,16 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None for gc in position.issued_gift_cards.all(): gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk) gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer) + gc.log_action( + action='pretix.giftcards.transaction.manual', + user=user, + auth=auth, + data={ + 'value': position.price, + 'acceptor_id': order.event.organizer.id, + 'acceptor_slug': order.event.organizer.slug + } + ) break for m in position.granted_memberships.all(): @@ -438,33 +447,27 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee email_context = get_email_context(event=order.event, order=order) - try: - order.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.order_approved', user, - attach_tickets=True, - attach_ical=order.event.settings.mail_attach_ical and ( - not order.event.settings.mail_attach_ical_paid_only or - order.total == Decimal('0.00') or - order.valid_if_pending - ), - invoices=[invoice] if invoice and transmit_invoice_mail else [] - ) - except SendMailException: - logger.exception('Order approved email could not be sent') + order.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.order_approved', user, + attach_tickets=True, + attach_ical=order.event.settings.mail_attach_ical and ( + not order.event.settings.mail_attach_ical_paid_only or + order.total == Decimal('0.00') or + order.valid_if_pending + ), + invoices=[invoice] if invoice and transmit_invoice_mail else [] + ) if email_attendees: for p in order.positions.all(): if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: email_attendee_context = get_email_context(event=order.event, order=order, position=p) - try: - p.send_mail( - email_attendee_subject, email_attendee_template, email_attendee_context, - 'pretix.event.order.email.order_approved', user, - attach_tickets=True, - ) - except SendMailException: - logger.exception('Order approved email could not be sent to attendee') + p.send_mail( + email_attendee_subject, email_attendee_template, email_attendee_context, + 'pretix.event.order.email.order_approved', user, + attach_tickets=True, + ) return order.pk @@ -501,13 +504,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): email_template = order.event.settings.mail_text_order_denied email_subject = order.event.settings.mail_subject_order_denied email_context = get_email_context(event=order.event, order=order, comment=comment) - try: - order.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.order_denied', user - ) - except SendMailException: - logger.exception('Order denied email could not be sent') + order.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.order_denied', user + ) return order.pk @@ -558,6 +558,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device ) else: gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer) + gc.log_action( + action='pretix.giftcards.transaction.manual', + user=user, + data={ + 'value': -position.price, + 'acceptor_id': order.event.organizer.id, + 'acceptor_slug': order.event.organizer.slug + } + ) for m in position.granted_memberships.all(): m.canceled = True @@ -660,14 +669,11 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device email_template = order.event.settings.mail_text_order_canceled email_subject = order.event.settings.mail_subject_order_canceled email_context = get_email_context(event=order.event, order=order, comment=comment or "") - try: - order.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.order_canceled', user, - invoices=transmit_invoices_mail, - ) - except SendMailException: - logger.exception('Order canceled email could not be sent') + order.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.order_canceled', user, + invoices=transmit_invoices_mail, + ) for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)): try: @@ -968,7 +974,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li fee.tax_rule = None # TODO: deprecate # Apply rounding to get final total in case no payment fees will be added - apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) + apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees]) payments_assigned = Decimal("0.00") @@ -995,7 +1001,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li p['fee'] = pf # Re-apply rounding as grand total has changed - apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees]) + apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees]) # Re-calculate to_pay as grand total has changed @@ -1108,46 +1114,40 @@ def _order_placed_email(event: Event, order: Order, email_template, subject_temp log_entry: str, invoice, payments: List[OrderPayment], is_free=False): email_context = get_email_context(event=event, order=order, payments=payments) - try: - order.send_mail( - subject_template, email_template, email_context, - log_entry, - invoices=[invoice] if invoice else [], - attach_tickets=True, - attach_ical=event.settings.mail_attach_ical and ( - not event.settings.mail_attach_ical_paid_only or - is_free or - order.valid_if_pending - ), - attach_other_files=[a for a in [ - event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] - ] if a], - ) - except SendMailException: - logger.exception('Order received email could not be sent') + order.send_mail( + subject_template, email_template, email_context, + log_entry, + invoices=[invoice] if invoice else [], + attach_tickets=True, + attach_ical=event.settings.mail_attach_ical and ( + not event.settings.mail_attach_ical_paid_only or + is_free or + order.valid_if_pending + ), + attach_other_files=[a for a in [ + event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] + ] if a], + ) def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, subject_template, log_entry: str, is_free=False): email_context = get_email_context(event=event, order=order, position=position) - try: - position.send_mail( - subject_template, email_template, email_context, - log_entry, - invoices=[], - attach_tickets=True, - attach_ical=event.settings.mail_attach_ical and ( - not event.settings.mail_attach_ical_paid_only or - is_free or - order.valid_if_pending - ), - attach_other_files=[a for a in [ - event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] - ] if a], - ) - except SendMailException: - logger.exception('Order received email could not be sent to attendee') + position.send_mail( + subject_template, email_template, email_context, + log_entry, + invoices=[], + attach_tickets=True, + attach_ical=event.settings.mail_attach_ical and ( + not event.settings.mail_attach_ical_paid_only or + is_free or + order.valid_if_pending + ), + attach_other_files=[a for a in [ + event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):] + ] if a], + ) def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str], @@ -1476,13 +1476,10 @@ def send_expiry_warnings(sender, **kwargs): email_template = settings.mail_text_order_pending_warning email_subject = settings.mail_subject_order_pending_warning - try: - o.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.expire_warning_sent' - ) - except SendMailException: - logger.exception('Reminder email could not be sent') + o.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.expire_warning_sent' + ) @receiver(signal=periodic_task) @@ -1543,14 +1540,11 @@ def send_download_reminders(sender, **kwargs): email_template = event.settings.mail_text_download_reminder email_subject = event.settings.mail_subject_download_reminder email_context = get_email_context(event=event, order=o) - try: - o.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.download_reminder_sent', - attach_tickets=True - ) - except SendMailException: - logger.exception('Reminder email could not be sent') + o.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.download_reminder_sent', + attach_tickets=True + ) if event.settings.mail_send_download_reminder_attendee: for p in positions: @@ -1564,14 +1558,11 @@ def send_download_reminders(sender, **kwargs): email_template = event.settings.mail_text_download_reminder_attendee email_subject = event.settings.mail_subject_download_reminder_attendee email_context = get_email_context(event=event, order=o, position=p) - try: - o.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.download_reminder_sent', - attach_tickets=True, position=p - ) - except SendMailException: - logger.exception('Reminder email could not be sent to attendee') + o.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.download_reminder_sent', + attach_tickets=True, position=p + ) def notify_user_changed_order(order, user=None, auth=None, invoices=[]): @@ -1579,13 +1570,10 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]): email_template = order.event.settings.mail_text_order_changed email_context = get_email_context(event=order.event, order=order) email_subject = order.event.settings.mail_subject_order_changed - try: - order.send_mail( - email_subject, email_template, email_context, - 'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True, - ) - except SendMailException: - logger.exception('Order changed email could not be sent') + order.send_mail( + email_subject, email_template, email_context, + 'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True, + ) class OrderChangeManager: @@ -1641,6 +1629,7 @@ class OrderChangeManager: ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until')) AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) + ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple()) class AddPositionResult: _position: Optional[OrderPosition] @@ -1804,6 +1793,7 @@ class OrderChangeManager: positions = self.order.positions.select_related('item', 'item__tax_rule') ia = self._invoice_address tax_rules = self._current_tax_rules() + self._operations.append(self.ForceRecomputeOperation()) for pos in positions: tax_rule = tax_rules.get(pos.pk, pos.tax_rule) @@ -2094,6 +2084,43 @@ class OrderChangeManager: ) item_counts[item] += 1 + # Detect removed add-ons and create RemoveOperations + for cp, al in list(current_addons.items()): + for k, v in al.items(): + input_num = input_addons[cp.id].get(k, 0) + current_num = len(current_addons[cp].get(k, [])) + if input_num < current_num: + for a in current_addons[cp][k][:current_num - input_num]: + if a.canceled: + continue + is_unavailable = ( + # If an item is no longer available due to time, it should usually also be no longer + # user-removable, because e.g. the stock has already been ordered. + # We always pass has_voucher=True because if a product now requires a voucher, it usually does + # not mean it should be unremovable for others. + # This also prevents accidental removal through the UI because a hidden product will no longer + # be part of the input. + (a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent)) + or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel)) + or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent) + or ( + not a.item.all_sales_channels and + not a.item.limit_sales_channels.contains(self.order.sales_channel) + ) + ) + if is_unavailable: + # "Re-select" add-on + selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1 + continue + if a.checkins.filter(list__consider_tickets_used=True).exists(): + raise OrderError( + error_messages['addon_already_checked_in'] % { + 'addon': str(a.item.name), + } + ) + self.cancel(a) + item_counts[a.item] -= 1 + # Check constraints on the add-on combinations for op in toplevel_op: item = op.item @@ -2126,41 +2153,6 @@ class OrderChangeManager: } ) - # Detect removed add-ons and create RemoveOperations - for cp, al in list(current_addons.items()): - for k, v in al.items(): - input_num = input_addons[cp.id].get(k, 0) - current_num = len(current_addons[cp].get(k, [])) - if input_num < current_num: - for a in current_addons[cp][k][:current_num - input_num]: - if a.canceled: - continue - is_unavailable = ( - # If an item is no longer available due to time, it should usually also be no longer - # user-removable, because e.g. the stock has already been ordered. - # We always pass has_voucher=True because if a product now requires a voucher, it usually does - # not mean it should be unremovable for others. - # This also prevents accidental removal through the UI because a hidden product will no longer - # be part of the input. - (a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent)) - or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel)) - or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent) - or ( - not item.all_sales_channels and - not item.limit_sales_channels.contains(self.order.sales_channel) - ) - ) - if is_unavailable: - continue - if a.checkins.filter(list__consider_tickets_used=True).exists(): - raise OrderError( - error_messages['addon_already_checked_in'] % { - 'addon': str(a.item.name), - } - ) - self.cancel(a) - item_counts[a.item] -= 1 - for item, count in item_counts.items(): if count == 0: continue @@ -2461,6 +2453,16 @@ class OrderChangeManager: )) else: gc.transactions.create(value=-position.price, order=self.order, acceptor=self.order.event.organizer) + gc.log_action( + action='pretix.giftcards.transaction.manual', + user=self.user, + auth=self.auth, + data={ + 'value': -position.price, + 'acceptor_id': self.order.event.organizer.id, + 'acceptor_slug': self.order.event.organizer.slug + } + ) for m in position.granted_memberships.with_usages().all(): m.canceled = True @@ -2478,6 +2480,16 @@ class OrderChangeManager: )) else: gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer) + gc.log_action( + action='pretix.giftcards.transaction.manual', + user=self.user, + auth=self.auth, + data={ + 'value': -opa.position.price, + 'acceptor_id': self.order.event.organizer.id, + 'acceptor_slug': self.order.event.organizer.slug + } + ) for m in opa.granted_memberships.with_usages().all(): m.canceled = True @@ -2640,6 +2652,10 @@ class OrderChangeManager: except BlockedTicketSecret.DoesNotExist: pass # todo: revoke list handling + elif isinstance(op, self.ForceRecomputeOperation): + self.order.log_action('pretix.event.order.changed.recomputed', user=self.user, auth=self.auth, data={}) + else: + raise TypeError(f"Unknown operation {type(op)}") for p in secret_dirty: assign_ticket_secret( @@ -2694,7 +2710,10 @@ class OrderChangeManager: fees.append(new_fee) changed_by_rounding = set(apply_rounding( - self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees + self.order.tax_rounding_mode, + self._invoice_address, + self.event.currency, + [p for p in split_positions if not p.canceled] + fees )) split_order.total = sum([p.price for p in split_positions if not p.canceled]) @@ -2716,7 +2735,10 @@ class OrderChangeManager: fee.delete() changed_by_rounding |= set(apply_rounding( - self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees + self.order.tax_rounding_mode, + self._invoice_address, + self.event.currency, + [p for p in split_positions if not p.canceled] + fees )) split_order.total = sum([p.price for p in split_positions if not p.canceled]) + sum([f.value for f in fees]) @@ -2833,7 +2855,12 @@ class OrderChangeManager: if fee_changed: fees = list(self.order.fees.all()) - changed = apply_rounding(self.order.tax_rounding_mode, self.order.event.currency, [*positions, *fees]) + changed = apply_rounding( + self.order.tax_rounding_mode, + self._invoice_address, + self.order.event.currency, + [*positions, *fees] + ) for l in changed: if isinstance(l, OrderPosition): l.save(update_fields=[ @@ -3131,7 +3158,10 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial customer=order.customer, testmode=order.testmode ) - giftcard.log_action('pretix.giftcards.created', data={}) + giftcard.log_action( + action='pretix.giftcards.created', + data={} + ) r = order.refunds.create( order=order, payment=None, @@ -3269,8 +3299,12 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay positions = list(order.positions.all()) fees = list(order.fees.all()) + try: + ia = order.invoice_address + except InvoiceAddress.DoesNotExist: + ia = None rounding_changed = set(apply_rounding( - order.tax_rounding_mode, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]] + order.tax_rounding_mode, ia, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]] )) total_without_fee = sum(c.price for c in positions) + sum(f.value for f in fees if f.pk != fee.pk) pending_sum_without_fee = max(Decimal("0.00"), total_without_fee - already_paid) @@ -3295,7 +3329,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay fee = None rounding_changed |= set(apply_rounding( - order.tax_rounding_mode, order.event.currency, [*positions, *fees] + order.tax_rounding_mode, ia, order.event.currency, [*positions, *fees] )) for l in rounding_changed: if isinstance(l, OrderPosition): @@ -3414,7 +3448,18 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs): currency=sender.currency, issued_in=p, testmode=order.testmode, expires=sender.organizer.default_gift_card_expiry, ) - gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer) + gc.log_action( + action='pretix.giftcards.created', + ) + trans = gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer) + gc.log_action( + action='pretix.giftcards.transaction.manual', + data={ + 'value': trans.value, + 'acceptor_id': order.event.organizer.id, + 'acceptor_slug': order.event.organizer.slug + } + ) any_giftcards = True p.secret = gc.secret p.save(update_fields=['secret']) diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py index 7ef2ae826..cd332f157 100644 --- a/src/pretix/base/services/pricing.py +++ b/src/pretix/base/services/pricing.py @@ -211,7 +211,8 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel], return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)] -def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep_gross"], currency: str, +def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only_business", "sum_by_net_keep_gross"], + invoice_address: Optional[InvoiceAddress], currency: str, lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list: """ Given a list of order positions / cart positions / order fees (may be mixed), applies the given rounding mode @@ -226,11 +227,17 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep When rounding mode is set to ``"sum_by_net"``, the gross prices and tax values of the individual lines will be adjusted such that the per-taxrate/taxcode subtotal is rounded correctly. The net prices will stay constant. - :param rounding_mode: One of ``"line"``, ``"sum_by_net"``, or ``"sum_by_net_keep_gross"``. + :param rounding_mode: One of ``"line"``, ``"sum_by_net"``, ``"sum_by_net_only_business"``, or ``"sum_by_net_keep_gross"``. + :param invoice_address: The invoice address, or ``None`` :param currency: Currency that will be used to determine rounding precision :param lines: List of order/cart contents :return: Collection of ``lines`` members that have been changed and may need to be persisted to the database. """ + if rounding_mode == "sum_by_net_only_business": + if invoice_address and invoice_address.is_business: + rounding_mode = "sum_by_net" + else: + rounding_mode = "line" def _key(line): return (line.tax_rate, line.tax_code or "") diff --git a/src/pretix/base/services/shredder.py b/src/pretix/base/services/shredder.py index 84ef4eee0..6211ad4a8 100644 --- a/src/pretix/base/services/shredder.py +++ b/src/pretix/base/services/shredder.py @@ -48,7 +48,7 @@ from django.utils.translation import gettext_lazy as _ from pretix.base.i18n import language from pretix.base.models import CachedFile, Event, User, cachedfile_name -from pretix.base.services.mail import SendMailException, mail +from pretix.base.services.mail import mail from pretix.base.services.tasks import ProfiledEventTask from pretix.base.shredder import ShredError from pretix.celery_app import app @@ -171,21 +171,18 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo if user: with language(user.locale): - try: - mail( - user.email, - _('Data shredding completed'), - 'pretixbase/email/shred_completed.txt', - { - 'user': user, - 'organizer': event.organizer.name, - 'event': str(event.name), - 'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'), - 'shredders': ', '.join([str(s.verbose_name) for s in shredders]) - }, - event=None, - user=user, - locale=user.locale, - ) - except SendMailException: - pass # Already logged + mail( + user.email, + _('Data shredding completed'), + 'pretixbase/email/shred_completed.txt', + { + 'user': user, + 'organizer': event.organizer.name, + 'event': str(event.name), + 'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'), + 'shredders': ', '.join([str(s.verbose_name) for s in shredders]) + }, + event=None, + user=user, + locale=user.locale, + ) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 1562133f9..4758f3393 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -81,6 +81,7 @@ from pretix.helpers.countries import CachedCountries, pycountry_add ROUNDING_MODES = ( ('line', _('Compute taxes for every line individually')), ('sum_by_net', _('Compute taxes based on net total')), + ('sum_by_net_only_business', _('For business customers, compute taxes based on net total. For individuals, use line-based rounding')), ('sum_by_net_keep_gross', _('Compute taxes based on net total with stable gross prices')), # We could also have sum_by_gross, but we're not aware of any use-cases for it ) @@ -2947,6 +2948,28 @@ If you did not request a new password, please ignore this email. Best regards, +Your {organizer} team""")) # noqa: W291 + }, + 'mail_subject_customer_security_notice': { + 'type': LazyI18nString, + 'default': LazyI18nString.from_gettext(gettext_noop("Changes to your account at {organizer}")), + }, + 'mail_text_customer_security_notice': { + 'type': LazyI18nString, + 'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name}, + +the following change has been made to your account at {organizer}: + +{message} + +You can review and change your account settings here: + +{url} + +If this change was not performed by you, please contact us immediately. + +Best regards, + Your {organizer} team""")) # noqa: W291 }, 'smtp_use_custom': { diff --git a/src/pretix/base/shredder.py b/src/pretix/base/shredder.py index a8110e987..21b280305 100644 --- a/src/pretix/base/shredder.py +++ b/src/pretix/base/shredder.py @@ -51,7 +51,7 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer from pretix.base.i18n import LazyLocaleException from pretix.base.models import ( CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment, - OrderPosition, OrderRefund, QuestionAnswer, + OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer, ) from pretix.base.services.invoices import invoice_pdf_task from pretix.base.signals import register_data_shredders @@ -329,6 +329,10 @@ class EmailAddressShredder(BaseDataShredder): sleep_time=2, ) + slow_delete( + OutgoingMail.objects.filter(event=self.event) + ) + for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total): changed = bool(o.email) or bool(o.customer) o.email = None diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index 3ab54bf0e..c917f318b 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -944,32 +944,40 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t email_filter = EventPluginSignal() """ -Arguments: ``message``, ``order``, ``user`` +Arguments: ``message``, ``order``, ``user``, ``outgoing_mail`` This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to return a (possibly modified) copy of the message object passed to you. As with all event-plugin signals, the ``sender`` keyword argument will contain the event. The ``message`` argument will contain an ``EmailMultiAlternatives`` object. +The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object +might have newer information if a previous plugin already modified the email. If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise it will be ``None``. If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as well, otherwise it will be ``None``. + +You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting. """ global_email_filter = GlobalSignal() """ -Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer`` +Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer``, ``outgoing_mail`` This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to return a (possibly modified) copy of the message object passed to you. This signal is called on all events and even if there is no known event. ``sender`` is an event or None. The ``message`` argument will contain an ``EmailMultiAlternatives`` object. +The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object +might have newer information if a previous plugin already modified the email. If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise it will be ``None``. If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as well, otherwise it will be ``None``. + +You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting. """ diff --git a/src/pretix/base/templates/404.html b/src/pretix/base/templates/404.html index 6fa272a0b..506132269 100644 --- a/src/pretix/base/templates/404.html +++ b/src/pretix/base/templates/404.html @@ -8,9 +8,6 @@

{% trans "Not found" %}

{% trans "I'm afraid we could not find the the resource you requested." %}

{{ exception }}

- {% if request.user.is_staff and not staff_session %}

diff --git a/src/pretix/base/templatetags/rich_text.py b/src/pretix/base/templatetags/rich_text.py index 2fabad485..307a2d6d5 100644 --- a/src/pretix/base/templatetags/rich_text.py +++ b/src/pretix/base/templatetags/rich_text.py @@ -156,7 +156,7 @@ def safelink_callback(attrs, new=False): Makes sure that all links to a different domain are passed through a redirection handler to ensure there's no passing of referers with secrets inside them. """ - url = attrs.get((None, 'href'), '/') + url = html.unescape(attrs.get((None, 'href'), '/')) if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'): signer = signing.Signer(salt='safe-redirect') attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url)) diff --git a/src/pretix/base/validators.py b/src/pretix/base/validators.py index 15cf67b0f..6a1ede39c 100644 --- a/src/pretix/base/validators.py +++ b/src/pretix/base/validators.py @@ -95,6 +95,7 @@ class OrganizerSlugBanlistValidator(BanlistValidator): 'csp_report', 'widget', 'lead', + 'scheduling', ] diff --git a/src/pretix/base/views/js_helpers.py b/src/pretix/base/views/js_helpers.py index b3cd198d5..16dfef2b5 100644 --- a/src/pretix/base/views/js_helpers.py +++ b/src/pretix/base/views/js_helpers.py @@ -20,6 +20,7 @@ # . # import pycountry +from django.conf import settings from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.utils.translation import gettext, pgettext, pgettext_lazy @@ -29,6 +30,7 @@ from django_scopes import scope from pretix.base.addressvalidation import ( COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED, ) +from pretix.base.i18n import language from pretix.base.invoicing.transmission import get_transmission_types from pretix.base.models import Organizer from pretix.base.models.tax import VAT_ID_COUNTRIES @@ -89,7 +91,7 @@ def _info(cc): } -def address_form(request): +def _address_form(request): cc = request.GET.get("country", "DE") info = _info(cc) @@ -157,4 +159,15 @@ def address_form(request): # The help text explains that it is optional, so we want to hide that if it is required info["vat_id"]["helptext_visible"] = False + return info + + +def address_form(request): + locale = request.GET.get('locale') + if locale in dict(settings.LANGUAGES): + with language(locale): + info = _address_form(request) + else: + info = _address_form(request) + return JsonResponse(info) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 917b4de1c..ca215a9ae 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -867,6 +867,11 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm): "The gross price of some products may be changed to ensure correct rounding, while the net " "prices will be kept as configured. This may cause the actual payment amount to differ." ), + "sum_by_net_only_business": _( + "Same as above, but only applied to business customers. Line-based rounding will be used for consumers. " + "Recommended when e-invoicing is only used for business customers and consumers do not receive " + "invoices. This can cause the payment amount to change when the invoice address is changed." + ), "sum_by_net_keep_gross": _( "Recommended for e-invoicing when you primarily sell to consumers. " "The gross or net price of some products may be changed automatically to ensure correct " diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py index 783c12e85..7b3a06ed0 100644 --- a/src/pretix/control/forms/filter.py +++ b/src/pretix/control/forms/filter.py @@ -57,8 +57,9 @@ from pretix.base.forms.widgets import ( from pretix.base.models import ( Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue, Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition, - OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel, - SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher, + OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota, + SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, + Voucher, ) from pretix.base.signals import register_payment_providers from pretix.base.timeframes import ( @@ -2815,3 +2816,61 @@ class DeviceFilterForm(FilterForm): qs = qs.order_by('-device_id') return qs + + +class OutgoingMailFilterForm(FilterForm): + orders = { + 'date': 'created', + '-date': '-created', + } + query = forms.CharField( + label=_('Search email address or subject'), + widget=forms.TextInput(attrs={ + 'placeholder': _('Search email address or subject'), + }), + required=False + ) + event = forms.ModelChoiceField( + queryset=Event.objects.none(), + label=_('Event'), + empty_label=_('All events'), + required=False, + ) + status = forms.ChoiceField( + label=_('Status'), + choices=[ + ('', _('All')), + *OutgoingMail.STATUS_CHOICES, + ], + required=False + ) + + def __init__(self, *args, **kwargs): + request = kwargs.pop('request') + super().__init__(*args, **kwargs) + self.fields['event'].queryset = request.organizer.events.all() + + def filter_qs(self, qs): + fdata = self.cleaned_data + + if fdata.get('query'): + query = fdata.get('query') + qs = qs.filter( + Q(to__containsstring=query.lower()) + | Q(cc__containsstring=query.lower()) + | Q(bcc__containsstring=query.lower()) + | Q(subject__icontains=query) + ) + + if fdata.get('event'): + qs = qs.filter(event=fdata['event']) + + if fdata.get('status'): + qs = qs.filter(status=fdata['status']) + + if fdata.get('ordering'): + qs = qs.order_by(self.get_order_by()) + else: + qs = qs.order_by("-created", "-pk") + + return qs diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 1f10385af..06743457b 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -585,6 +585,7 @@ class MailSettingsForm(SettingsForm): help_text=''.join([ str(_("All emails will be sent to this address as a Bcc copy.")), str(_("You can specify multiple recipients separated by commas.")), + str(_("Sensitive emails like password resets will not be sent in Bcc.")), ]), validators=[multimail_validate], required=False, @@ -634,6 +635,16 @@ class MailSettingsForm(SettingsForm): required=False, widget=I18nMarkdownTextarea, ) + mail_subject_customer_security_notice = I18nFormField( + label=_("Subject"), + required=False, + widget=I18nTextInput, + ) + mail_text_customer_security_notice = I18nFormField( + label=_("Text"), + required=False, + widget=I18nMarkdownTextarea, + ) base_context = { 'mail_text_customer_registration': ['customer', 'url'], @@ -642,6 +653,8 @@ class MailSettingsForm(SettingsForm): 'mail_subject_customer_email_change': ['customer', 'url'], 'mail_text_customer_reset': ['customer', 'url'], 'mail_subject_customer_reset': ['customer', 'url'], + 'mail_text_customer_security_notice': ['customer', 'url', 'message'], + 'mail_subject_customer_security_notice': ['customer', 'url', 'message'], } def _get_sample_context(self, base_parameters): @@ -655,6 +668,9 @@ class MailSettingsForm(SettingsForm): 'presale:organizer.customer.activate' ) + '?token=' + get_random_string(30) + if 'message' in base_parameters: + placeholders['message'] = _('Your password has been changed.') + if 'customer' in base_parameters: placeholders['name'] = pgettext_lazy('person_name_sample', 'John Doe') name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme] diff --git a/src/pretix/control/forms/waitinglist.py b/src/pretix/control/forms/waitinglist.py index 7dc33c4a9..cf77380f6 100644 --- a/src/pretix/control/forms/waitinglist.py +++ b/src/pretix/control/forms/waitinglist.py @@ -19,17 +19,44 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +from django import forms from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from django_scopes.forms import SafeModelChoiceField +from phonenumber_field.formfields import PhoneNumberField from pretix.base.forms import I18nModelForm +from pretix.base.forms.questions import ( + NamePartsFormField, WrappedPhoneNumberPrefixWidget, +) from pretix.base.models import WaitingListEntry from pretix.control.forms.widgets import Select2 -class WaitingListEntryTransferForm(I18nModelForm): +class WaitingListEntryEditForm(I18nModelForm): + itemvar = forms.ChoiceField( + error_messages={ + 'invalid_choice': _("Select a valid choice.") + } + ) def __init__(self, *args, **kwargs): + self.instance = kwargs.get('instance', None) + initial = kwargs.get('initial', {}) + + choices = [] + if self.instance and self.instance.pk and 'itemvar' not in initial: + if self.instance.variation is not None: + initial['itemvar'] = f'{self.instance.item.pk}-{self.instance.variation.pk}' + if self.instance.variation.active is False: + choices.append((initial['itemvar'], str(self.instance.variation))) + else: + initial['itemvar'] = self.instance.item.pk + if self.instance.item.active is False: + choices.append((initial['itemvar'], str(self.instance))) + + kwargs['initial'] = initial + super().__init__(*args, **kwargs) if self.event.has_subevents: @@ -45,12 +72,73 @@ class WaitingListEntryTransferForm(I18nModelForm): } ) self.fields['subevent'].widget.choices = self.fields['subevent'].choices + else: + del self.fields['subevent'] + + if self.event.settings.waiting_list_names_asked: + self.fields['name_parts'] = NamePartsFormField( + max_length=255, + required=self.event.settings.waiting_list_names_required, + scheme=self.event.organizer.settings.name_scheme, + titles=self.event.organizer.settings.name_scheme_titles, + label=_('Name'), + ) + else: + del self.fields['name_parts'] + + if not self.event.settings.waiting_list_phones_asked: + del self.fields['phone'] + + items = self.event.items.filter(active=True).prefetch_related( + 'variations' + ) + + for item in items: + if len(item.variations.all()) > 0: + for variation in item.variations.all(): + if variation.active: + choices.append( + ('{}-{}'.format(item.pk, variation.pk), '{} - {}'.format(str(item), str(variation))) + ) + else: + choices.append(('{}'.format(item.pk), str(item))) + + self.fields['itemvar'].label = _("Product") + self.fields['itemvar'].help_text = _("Only includes active products.") + self.fields['itemvar'].required = True + self.fields['itemvar'].choices = choices + + def clean(self): + cleaned_data = super().clean() + + if self.instance.voucher is not None: + raise forms.ValidationError(_('A voucher for this waiting list entry was already sent out.')) + + itemvar = cleaned_data.get('itemvar') + if itemvar: + self.instance.item = self.event.items.get(pk=itemvar.split('-')[0]) + if '-' in itemvar: + self.instance.variation = self.instance.item.variations.get(pk=itemvar.split('-')[1]) + + if ((self.instance.item and not self.instance.item.active) or + (self.instance.variation and not self.instance.variation.active)): + self.add_error('itemvar', _('The selected product is not active.')) + + return cleaned_data class Meta: model = WaitingListEntry fields = [ + 'email', + 'name_parts', + 'phone', 'subevent', ] field_classes = { 'subevent': SafeModelChoiceField, + 'email': forms.EmailField, + 'phone': PhoneNumberField, + } + widgets = { + 'phone': WrappedPhoneNumberPrefixWidget, } diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 02ccce2e4..c12b866d0 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -170,6 +170,12 @@ class OrderFeeAdded(OrderChangeLogEntryType): plain = _('A fee has been added') +@log_entry_types.new() +class OrderRecomputed(OrderChangeLogEntryType): + action_type = 'pretix.event.order.changed.recomputed' + plain = _('Taxes and rounding have been recomputed') + + @log_entry_types.new() class OrderFeeChanged(OrderChangeLogEntryType): action_type = 'pretix.event.order.changed.feevalue' @@ -699,6 +705,8 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType): 'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'), 'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'), 'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'), + 'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'), + 'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'), 'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'), 'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'), 'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'), @@ -793,6 +801,8 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType): 'pretix.giftcards.created': _('The gift card has been created.'), 'pretix.giftcards.modified': _('The gift card has been changed.'), 'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'), + 'pretix.giftcards.transaction.payment': _('A payment has been performed.'), + 'pretix.giftcards.transaction.refund': _('A refund has been performed. '), 'pretix.team.token.created': _('The token "{name}" has been created.'), 'pretix.team.token.deleted': _('The token "{name}" has been revoked.'), 'pretix.event.checkin.reset': _('The check-in and print log state has been reset.') diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index c3a0cbb8f..a6e320978 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -679,6 +679,15 @@ def get_organizer_navigation(request): 'active': (url.url_name == 'organizer.datasync.failedjobs'), }]) + nav.append({ + 'label': _('Outgoing emails'), + 'url': reverse('control:organizer.outgoingmails', kwargs={ + 'organizer': request.organizer.slug, + }), + 'active': 'organizer.outgoingmail' in url.url_name, + 'icon': 'send', + }) + merge_in(nav, sorted( sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)), []), diff --git a/src/pretix/control/templates/pretixcontrol/auth/login.html b/src/pretix/control/templates/pretixcontrol/auth/login.html index ff9b9b3bb..fc23c24c1 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/login.html +++ b/src/pretix/control/templates/pretixcontrol/auth/login.html @@ -19,6 +19,14 @@
{% endif %} + {% if possible_cookie_problem %} +

+ {% blocktrans trimmed %} + It looks like your browser is not accepting our cookie and you need to log in repeatedly. Please + check if your browser is set to block cookies, or delete all existing cookies and retry. + {% endblocktrans %} +
+ {% endif %} {% csrf_token %} {% bootstrap_form form %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/mail.html b/src/pretix/control/templates/pretixcontrol/organizers/mail.html index a80483a0b..463caeca1 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/mail.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/mail.html @@ -65,6 +65,9 @@ {% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %} {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_subject_customer_reset,mail_text_customer_reset" %} + + {% blocktrans asvar title_security_notice %}Customer account security notification{% endblocktrans %} + {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="security_notice" title=title_security_notice items="mail_subject_customer_security_notice,mail_text_customer_security_notice" %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html b/src/pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html new file mode 100644 index 000000000..8ba0cf854 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/organizers/outgoing_mail.html @@ -0,0 +1,222 @@ +{% extends "pretixcontrol/organizers/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load urlreplace %} +{% load icon %} +{% load compress %} +{% load static %} +{% block inner %} +

+ {% trans "Outgoing email" %} +

+
+
+

{% trans "Email details" %}

+
+
+
+
+ +
+
{% trans "From" context "email" %}
+
{{ sender }}
+
{% trans "To" context "email" %}
+
{{ mail.to|join:", " }}
+ {% if mail.cc %} +
{% trans "Cc" context "email" %}
+
{{ mail.cc|join:", " }}
+ {% endif %} + {% if mail.bcc %} +
{% trans "Bcc" context "email" %}
+
{{ mail.bcc|join:", " }}
+ {% endif %} +
{% trans "Subject" %}
+
{{ mail.subject }}
+
{% trans "Status" %}
+
+ {% if mail.status == "queued" %} + {% icon "clock-o" %} {% trans "queued" %} + {% elif mail.status == "inflight" %} + {% icon "send" %} {% trans "being sent" %} + {% elif mail.status == "awaiting_retry" %} + {% icon "repeat" %} {% trans "will be retried" %} + {% elif mail.status == "failed" %} + {% icon "warning" %} {% trans "failed" %} + {% elif mail.status == "bounced" %} + {% icon "exclamation-circle" %} {% trans "bounced" %} + {% elif mail.status == "withheld" %} + {% icon "ban" %} {% trans "withheld" %} + {% elif mail.status == "aborted" %} + {% icon "ban" %} {% trans "aborted" %} + {% elif mail.status == "sent" %} + {% icon "check" %} {% trans "sent" %} + {% endif %} +
+
{% trans "Creation" %}
+
{{ mail.created|date:"SHORT_DATETIME_FORMAT" }}
+ {% if mail.sent %} +
{% trans "Sent" %}
+
{{ mail.sent|date:"SHORT_DATETIME_FORMAT" }}
+ {% endif %} + {% if mail.retry_after and mail.status == "awaiting_retry" %} +
{% trans "Next attempt (estimate)" %}
+
{{ mail.retry_after|date:"SHORT_DATETIME_FORMAT" }}
+ {% endif %} + {% if mail.event %} +
{% trans "Event" %}
+
+ + {{ mail.event }} + +
+ {% endif %} + {% if mail.order %} +
{% trans "Order" %}
+
+ + {{ mail.order.code }}{% if mail.orderposition %}- + {{ mail.orderposition.positionid }}{% endif %} +
+ {% endif %} + {% if mail.customer %} +
{% trans "Customer" %}
+
+ {% icon "user fa-fw" %} + + {{ mail.customer }} + +
+ {% endif %} +
+
+ {% if mail.actual_attachments %} +
+ {% trans "Attachments" %}
+
    + {% for a in mail.actual_attachments %} +
  • + {% if a.type == "text/calendar" %} + {% icon "calendar-plus-o fa-fw" %} + {% elif a.type == "application/pdf" %} + {% icon "file-pdf-o fa-fw" %} + {% elif "image/" in a.type %} + {% icon "file-image-o fa-fw" %} + {% elif "msword" in a.type or "document" in a.type %} + {% icon "file-word-o fa-fw" %} + {% elif "excel" in a.type or "spreadsheet" in a.type %} + {% icon "file-excel-o fa-fw" %} + {% elif "powerpoint" in a.type or "presentation" in a.type %} + {% icon "file-powerpoint-o fa-fw" %} + {% elif "pkpass" in a.type %} + {% icon "qrcode fa-fw" %} + {% else %} + {% icon "file-o fa-fw" %} + {% endif %} + {{ a.name }} + + ({{ a.size|filesizeformat }}) + +
  • + {% endfor %} +
+
+ {% endif %} +
+
+
+
+ + +
+ {% if mail.is_failed %} +
+ + {{ mail.error }} + +
{{ mail.error_detail }}
+
+ {% endif %} + {% if mail.body_html %} +
+ {% if mail.sensitive %} +
+

+ {% icon "eye-slash fa-4x" %} +

+

+ {% blocktrans trimmed %} + Sensitive content not shown for security reasons + {% endblocktrans %} +

+
+ {% else %} + {{ data_url|json_script:"mail_body_html" }} + {% endif %} +
+ {% endif %} +
+ {% if mail.sensitive %} +
+

+ {% icon "eye-slash fa-4x" %} +

+

+ {% blocktrans trimmed %} + Sensitive content not shown for security reasons + {% endblocktrans %} +

+
+ {% else %} +
{{ mail.body_plain }}
+ {% endif %} +
+
+
{% for k, v in mail.headers.items %}{{ k }}: {{ v }}
{% endfor %}
+

+ {% trans "Additional headers will be added by the mail server and are not visible here." %} +

+
+
+ +
+ {% compress js %} + + {% endcompress %} +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/outgoing_mails.html b/src/pretix/control/templates/pretixcontrol/organizers/outgoing_mails.html new file mode 100644 index 000000000..5de235cd9 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/organizers/outgoing_mails.html @@ -0,0 +1,185 @@ +{% extends "pretixcontrol/organizers/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load urlreplace %} +{% load icon %} +{% block inner %} +

+ {% trans "Outgoing emails" %} +

+

+ {% blocktrans trimmed with days=days %} + This is an overview of all emails sent by your organizer account in the last {{ days }} days. + {% endblocktrans %} +

+ {% if mails|length == 0 and not filter_form.filtered %} +
+

+ {% blocktrans trimmed %} + You haven't sent any emails recently. + {% endblocktrans %} +

+
+ {% else %} +
+
+

{% trans "Filter" %}

+
+ +
+
+ {% bootstrap_field filter_form.query %} +
+
+ {% bootstrap_field filter_form.status %} +
+
+ {% bootstrap_field filter_form.event %} +
+
+
+ +
+ +
+
+ {% csrf_token %} + {% for field in filter_form %} + {{ field.as_hidden }} + {% endfor %} +
+ + + + + + + + + + + + {% if page_obj.paginator.num_pages > 1 %} + + + + + {% endif %} + + + {% for m in mails %} + + + + + + + + + + {% endfor %} + +
+ + {% trans "Subject" %}{% trans "Recipients" %}{% trans "Context" %}{% trans "Status" %}{% trans "Date" %} + + +
+ + + + {{ m.subject }} + + {% if m.sensitive %} + {% icon "eye-slash" %} + {% endif %} + + {{ m.to|join:", " }} + {% if m.cc %} +
{% trans "Cc" context "email" %}: {{ m.cc|join:", " }} + {% endif %} + {% if m.bcc %} +
{% trans "Bcc" context "email" %}: {{ m.bcc|join:", " }} + {% endif %} +
+ {% if m.event %} +
+ {% icon "calendar fa-fw" %} + + {{ m.event }} + +
+ {% endif %} + {% if m.order %} +
+ {% icon "shopping-cart fa-fw" %} + + {{ m.order.code }}{% if m.orderposition %}-{{ m.orderposition.positionid }}{% endif %} +
+ {% endif %} + {% if m.customer %} +
+ {% icon "user fa-fw" %} + + {{ m.customer }} + +
+ {% endif %} +
+ {% if m.status == "queued" %} + {% icon "clock-o" %} {% trans "queued" %} + {% elif m.status == "inflight" %} + {% icon "send" %} {% trans "being sent" %} + {% elif m.status == "awaiting_retry" %} + {% icon "repeat" %} {% trans "will be retried" %} + {% elif m.status == "failed" %} + {% icon "warning" %} {% trans "failed" %} + {% elif m.status == "bounced" %} + {% icon "exclamation-circle" %} {% trans "bounced" %} + {% elif m.status == "withheld" %} + {% icon "ban" %} {% trans "withheld" %} + {% elif m.status == "aborted" %} + {% icon "ban" %} {% trans "aborted" %} + {% elif m.status == "sent" %} + {% icon "check" %} {% trans "sent" %} + {% endif %} + + {{ m.created|date:"SHORT_DATETIME_FORMAT" }} + {% if m.sent %} +
+ {% trans "Sent:" %} {{ m.sent|date:"SHORT_DATETIME_FORMAT" }} + {% endif %} +
+ {% icon "eye" %} +
+
+
+ + +
+
+ {% include "pretixcontrol/pagination.html" %} + {% endif %} +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/user/2fa_main.html b/src/pretix/control/templates/pretixcontrol/user/2fa_main.html index 308c57851..0f0899834 100644 --- a/src/pretix/control/templates/pretixcontrol/user/2fa_main.html +++ b/src/pretix/control/templates/pretixcontrol/user/2fa_main.html @@ -144,14 +144,23 @@

- {% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %} + {% blocktrans trimmed %} + If you lose access to your devices, you can use one of your emergency tokens to log in. + We recommend to store them in a safe place, e.g. printed out or in a password manager. + Every token can be used at most once. + {% endblocktrans %}

-

{% trans "Unused tokens:" %}

-
    - {% for t in static_tokens %} -
  • {{ t.token }}
  • - {% endfor %} -
+ {% if static_tokens_device %} +

+ {% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %} + You generated your emergency tokens on {{ generation_date_time }}. + {% endblocktrans %} +

+ {% else %} +

+ {% trans "You don't have any emergency tokens yet." %} +

+ {% endif %} {% trans "Generate new emergency tokens" %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/edit.html b/src/pretix/control/templates/pretixcontrol/waitinglist/edit.html new file mode 100644 index 000000000..d4362f9c0 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/edit.html @@ -0,0 +1,33 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Edit entry" %}{% endblock %} +{% block content %} +

{% trans "Edit entry" %}

+
+ {% csrf_token %} + + {% if form.subevent %} + {% bootstrap_field form.subevent layout="control" %} + {% endif %} + + {% bootstrap_field form.email layout="control" %} + + {% if form.name_parts %} + {% bootstrap_field form.name_parts layout="control" %} + {% endif %} + + {% if form.phone %} + {% bootstrap_field form.phone layout="control" %} + {% endif %} + {% bootstrap_field form.itemvar layout="control" %} +
+ + {% trans "Cancel" %} + + +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index 1539b2c9d..42c4a9033 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -124,6 +124,7 @@ {% endfor %} + {% if request.event.has_subevents %} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html index 6f331b060..fb53d66e3 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html @@ -15,7 +15,7 @@
  • - {% for f, v in request.GET.items %} + {% for f, v in request_get_items %} {% if f != "date" %} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html index 89e20f600..9ff6f8069 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order.html +++ b/src/pretix/presale/templates/pretixpresale/event/order.html @@ -304,7 +304,7 @@
    {% trans "Phone number" %}
    {{ order.phone|phone_format }}
    {% endif %} - {% if invoice_address_asked %} + {% if invoice_address_asked and order.invoice_address.is_business %}
    {% trans "Company" %}
    {{ order.invoice_address.company }}
    {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/order_modify.html b/src/pretix/presale/templates/pretixpresale/event/order_modify.html index 02d08304c..11ceb9c3c 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_modify.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_modify.html @@ -36,7 +36,7 @@
    + data-address-information-url="{% url "js_helpers.address_form" %}?invoice=true&organizer={{ event.organizer.slug|urlencode }}&event={{ event.slug|urlencode }}&locale={{ request.LANGUAGE_CODE }}">
    {% if event.settings.invoice_address_explanation_text %}
    diff --git a/src/pretix/presale/templates/pretixpresale/event/resend_link.html b/src/pretix/presale/templates/pretixpresale/event/resend_link.html index f5a6358d4..b923d9f7a 100644 --- a/src/pretix/presale/templates/pretixpresale/event/resend_link.html +++ b/src/pretix/presale/templates/pretixpresale/event/resend_link.html @@ -1,14 +1,14 @@ {% extends "pretixpresale/event/base.html" %} {% load i18n %} {% load bootstrap3 %} -{% block title %}{% trans "Resend order links" %}{% endblock %} +{% block title %}{% trans "Resend order link" %}{% endblock %} {% block custom_header %} {{ block.super }} {% endblock %} {% block content %}

    - {% trans "Resend order links" %} + {% trans "Resend order link" %}

    {% blocktrans trimmed %} diff --git a/src/pretix/presale/templates/pretixpresale/fragment_event_list_filter.html b/src/pretix/presale/templates/pretixpresale/fragment_event_list_filter.html index f763a54d8..674737ded 100644 --- a/src/pretix/presale/templates/pretixpresale/fragment_event_list_filter.html +++ b/src/pretix/presale/templates/pretixpresale/fragment_event_list_filter.html @@ -7,7 +7,7 @@ {% if filter_form.fields %} - {% for f, v in request.GET.items %} + {% for f, v in request_get_items %} {% if f not in filter_form.fields and f != "page" %} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/organizers/calendar.html b/src/pretix/presale/templates/pretixpresale/organizers/calendar.html index a2b0e4b04..16e1044dd 100644 --- a/src/pretix/presale/templates/pretixpresale/organizers/calendar.html +++ b/src/pretix/presale/templates/pretixpresale/organizers/calendar.html @@ -47,7 +47,7 @@

  • - {% for f, v in request.GET.items %} + {% for f, v in request_get_items %} {% if f != "date" %} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/organizers/calendar_day.html b/src/pretix/presale/templates/pretixpresale/organizers/calendar_day.html index 820f07e3f..dbb4dae4b 100644 --- a/src/pretix/presale/templates/pretixpresale/organizers/calendar_day.html +++ b/src/pretix/presale/templates/pretixpresale/organizers/calendar_day.html @@ -47,7 +47,7 @@
  • - {% for f, v in request.GET.items %} + {% for f, v in request_get_items %} {% if f != "date" %} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/organizers/calendar_week.html b/src/pretix/presale/templates/pretixpresale/organizers/calendar_week.html index b4077d6d0..c95e4cff6 100644 --- a/src/pretix/presale/templates/pretixpresale/organizers/calendar_week.html +++ b/src/pretix/presale/templates/pretixpresale/organizers/calendar_week.html @@ -47,7 +47,7 @@
  • - {% for f, v in request.GET.items %} + {% for f, v in request_get_items %} {% if f != "date" %} {% endif %} diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index 3046711e2..640b4532b 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -167,7 +167,7 @@ class CartMixin: fees = [] if not order: - apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*lcp, *fees]) + apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees]) total = sum([c.price for c in lcp]) + sum([f.value for f in fees]) net_total = sum(p.price - p.tax_value for p in lcp) + sum([f.net_value for f in fees]) @@ -264,6 +264,11 @@ class CartMixin: 'max_expiry_extend': max_expiry_extend, 'is_ordered': bool(order), 'itemcount': sum(c.count for c in positions if not c.addon_to), + 'show_rounding_info': ( + self.request.event.settings.tax_rounding == "sum_by_net_only_business" and + not self.request.event.settings.display_net_prices and + sum(c.price_includes_rounding_correction for c in positions) + sum(f.price_includes_rounding_correction for f in fees) + ), 'itemvarsums': itemvarsums, 'current_selected_payments': [ p for p in self.current_selected_payments(lcp, fees, self.invoice_address) @@ -275,7 +280,7 @@ class CartMixin: raw_payments = copy.deepcopy(self.cart_session.get('payments', [])) fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here - apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*positions, *fees]) + apply_rounding(self.request.event.settings.tax_rounding, invoice_address, self.request.event.currency, [*positions, *fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees]) payments = [] @@ -329,7 +334,7 @@ class CartMixin: fees.append(pf) # Re-apply rounding as grand total has changed - apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*positions, *fees]) + apply_rounding(self.request.event.settings.tax_rounding, invoice_address, self.request.event.currency, [*positions, *fees]) total = sum([c.price for c in positions]) + sum([f.value for f in fees]) # Re-calculate to_pay as grand total has changed diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index fa0dd884b..0919bea23 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -286,6 +286,7 @@ class SetPasswordView(FormView): self.customer.is_verified = True self.customer.save() self.customer.log_action('pretix.customer.password.set', {}) + self.customer.send_security_notice(_("Your password has been changed.")) messages.success( self.request, _('Your new password has been set! You can now use it to log in.'), @@ -325,6 +326,7 @@ class ResetPasswordView(FormView): locale=customer.locale, customer=customer, organizer=self.request.organizer, + sensitive=True, ) messages.success( self.request, @@ -540,6 +542,7 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView): customer.set_password(form.cleaned_data['password']) customer.save() messages.success(self.request, _('Your changes have been saved.')) + customer.send_security_notice(_("Your password has been changed.")) update_customer_session_auth_hash(self.request, customer) return HttpResponseRedirect(self.get_success_url()) @@ -630,11 +633,15 @@ class ConfirmChangeView(View): try: with transaction.atomic(): + old_email = customer.email customer.email = data['email'] customer.save() customer.log_action('pretix.customer.changed', { 'email': data['email'] }) + msg = _('Your email address has been changed from {old_email} to {email}.').format(old_email=old_email, email=customer.email) + customer.send_security_notice(msg, email=old_email) + customer.send_security_notice(msg, email=customer.email) except IntegrityError: messages.success(request, _('Your email address has not been updated since the address is already in use ' 'for another customer account.')) diff --git a/src/pretix/presale/views/oidc_op.py b/src/pretix/presale/views/oidc_op.py index a8178545f..e79dbb194 100644 --- a/src/pretix/presale/views/oidc_op.py +++ b/src/pretix/presale/views/oidc_op.py @@ -393,7 +393,6 @@ class TokenView(View): if grant.code_challenge_method == "S256": expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=") - print(grant.code_challenge, expected_challenge) if expected_challenge != grant.code_challenge: return JsonResponse({ "error": "invalid_grant", diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 685b78506..090c8b4ad 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -40,7 +40,7 @@ import logging import mimetypes import os import re -from collections import OrderedDict, defaultdict +from collections import Counter, OrderedDict, defaultdict from decimal import Decimal from urllib.parse import quote @@ -77,7 +77,6 @@ from pretix.base.services.invoices import ( generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task, invoice_qualified, ) -from pretix.base.services.mail import SendMailException from pretix.base.services.orders import ( OrderChangeManager, OrderError, _try_auto_refund, cancel_order, change_payment_provider, error_messages, @@ -656,10 +655,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView): amount=Decimal('0.00'), fee=None ) - try: - p.confirm() - except SendMailException: - pass + p.confirm() else: p._mark_order_paid( payment_refund_sum=self.order.payment_refund_sum @@ -1431,11 +1427,13 @@ class OrderChangeMixin: 'categories': [] } current_addon_products = defaultdict(list) + current_addon_products_missing = Counter() for a in p.addons.all(): if a.canceled: continue if not a.is_bundled: current_addon_products[a.item_id, a.variation_id].append(a) + current_addon_products_missing[a.item, a.variation] += 1 for iao in p.item.addons.all(): ckey = '{}-{}'.format(p.subevent.pk if p.subevent else 0, iao.addon_category.pk) @@ -1473,6 +1471,7 @@ class OrderChangeMixin: if i.has_variations: for v in i.available_variations: v.initial = len(current_addon_products[i.pk, v.pk]) + current_addon_products_missing[i, v] = 0 if v.initial and i.free_price: a = current_addon_products[i.pk, v.pk][0] v.initial_price = TaxedPrice( @@ -1488,6 +1487,7 @@ class OrderChangeMixin: i.expand = any(v.initial for v in i.available_variations) else: i.initial = len(current_addon_products[i.pk, None]) + current_addon_products_missing[i, None] = 0 if i.initial and i.free_price: a = current_addon_products[i.pk, None][0] i.initial_price = TaxedPrice( @@ -1509,7 +1509,11 @@ class OrderChangeMixin: 'min_count': iao.min_count, 'max_count': iao.max_count, 'iao': iao, - 'items': [i for i in items if not i.require_voucher] + 'items': [i for i in items if not i.require_voucher], + 'items_missing': { + k: v for k, v in current_addon_products_missing.items() + if v and k[0].category_id == iao.addon_category_id + }, }) return positions diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py index ecc123a05..72f880ccd 100644 --- a/src/pretix/presale/views/organizer.py +++ b/src/pretix/presale/views/organizer.py @@ -1300,6 +1300,8 @@ class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView): class OrganizerIcalDownload(OrganizerViewMixin, View): def get(self, request, *args, **kwargs): cutoff = now() - timedelta(days=31) + # generally limit to 1000 entries as this seems to be a limitation on ics-files for some calendar software + limit = 1000 events = list( filter_qs_by_attr( self.request.organizer.events.filter( @@ -1318,7 +1320,7 @@ class OrganizerIcalDownload(OrganizerViewMixin, View): 'organizer', queryset=Organizer.objects.prefetch_related('_settings_objects') ) - ) + )[:limit] ) events += list( filter_qs_by_attr( @@ -1346,8 +1348,11 @@ class OrganizerIcalDownload(OrganizerViewMixin, View): ) ).order_by( 'date_from' - ) + )[:limit] ) + if len(events) > limit: + events.sort(key=lambda e: e.date_from) + events = events[:limit] if 'locale' in request.GET and request.GET.get('locale') in dict(settings.LANGUAGES): with language(request.GET.get('locale'), self.request.organizer.settings.region): diff --git a/src/pretix/presale/views/user.py b/src/pretix/presale/views/user.py index a95118408..b81457a1b 100644 --- a/src/pretix/presale/views/user.py +++ b/src/pretix/presale/views/user.py @@ -32,8 +32,6 @@ # 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 logging - from django.conf import settings from django.contrib import messages from django.utils.functional import cached_property @@ -42,7 +40,7 @@ from django.views import View from django.views.generic import TemplateView from pretix.base.email import get_email_context -from pretix.base.services.mail import INVALID_ADDRESS, SendMailException, mail +from pretix.base.services.mail import INVALID_ADDRESS, mail from pretix.helpers.http import redirect_to_url from pretix.multidomain.urlreverse import eventreverse from pretix.presale.forms.user import ResendLinkForm @@ -83,13 +81,7 @@ class ResendLinkView(EventViewMixin, TemplateView): subject = self.request.event.settings.mail_subject_resend_all_links template = self.request.event.settings.mail_text_resend_all_links context = get_email_context(event=self.request.event, orders=orders) - try: - mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE) - except SendMailException: - logger = logging.getLogger('pretix.presale.user') - logger.exception('A mail resending order links to {} could not be sent.'.format(user)) - messages.error(self.request, _('We have trouble sending emails right now, please check back later.')) - return self.get(request, *args, **kwargs) + mail(user, subject, template, context, event=self.request.event, locale=self.request.LANGUAGE_CODE) messages.success(self.request, _('If there were any orders by this user, they will receive an email with their order codes.')) return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) @@ -105,7 +97,8 @@ class UnlockHashView(EventViewMixin, View): def get(self, request, *args, **kwargs): hashes = request.session.get('pretix_unlock_hashes', []) - hashes.append(kwargs.get('hash')) + if kwargs.get('hash') not in hashes: + hashes.append(kwargs.get('hash')) request.session['pretix_unlock_hashes'] = hashes if 'voucher' in request.GET: diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 627275449..508b185b3 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -211,6 +211,10 @@ USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fal REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False) +if REQUEST_ID_HEADER in config.cp.BOOLEAN_STATES: + raise ImproperlyConfigured( + "request_id_header should be set to a header name, not a boolean value." + ) if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False): SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -855,6 +859,8 @@ COUNTRIES_OVERRIDE = { DATA_UPLOAD_MAX_NUMBER_FIELDS = 25000 DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB +OUTGOING_MAIL_RETENTION = 14 * 24 * 3600 # 14 days in seonds + # File sizes are in MiB FILE_UPLOAD_MAX_SIZE_IMAGE = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_image", fallback=10) FILE_UPLOAD_MAX_SIZE_FAVICON = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_favicon", fallback=1) diff --git a/src/pretix/static/npm_dir/package-lock.json b/src/pretix/static/npm_dir/package-lock.json index 12bac1172..7b9d48970 100644 --- a/src/pretix/static/npm_dir/package-lock.json +++ b/src/pretix/static/npm_dir/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@babel/core": "^7.28.5", - "@babel/preset-env": "^7.28.5", + "@babel/preset-env": "^7.29.0", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "rollup": "^2.79.1", @@ -19,12 +19,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -33,9 +32,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "engines": { "node": ">=6.9.0" } @@ -89,12 +88,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -115,12 +114,11 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -152,16 +150,16 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -175,19 +173,17 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "license": "MIT", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -206,15 +202,15 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -229,39 +225,37 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", - "license": "MIT", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -283,10 +277,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "engines": { "node": ">=6.9.0" } @@ -295,7 +288,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", @@ -309,14 +301,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -365,14 +356,13 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -391,11 +381,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -467,12 +457,12 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -493,12 +483,11 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -508,12 +497,11 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -553,13 +541,13 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -569,13 +557,12 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { @@ -601,11 +588,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -615,13 +602,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -631,12 +617,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -646,16 +632,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -665,13 +651,12 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -696,13 +681,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -727,13 +711,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "license": "MIT", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -758,12 +741,12 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -773,11 +756,11 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", - "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -835,12 +818,11 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -865,11 +847,11 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -910,13 +892,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -926,14 +907,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "dependencies": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -959,13 +940,12 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "license": "MIT", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -990,12 +970,11 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1005,12 +984,11 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1020,15 +998,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1054,12 +1032,11 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1069,11 +1046,11 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1098,13 +1075,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1114,14 +1090,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1146,11 +1121,11 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1160,13 +1135,12 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1206,12 +1180,11 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1282,13 +1255,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1314,13 +1286,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1330,79 +1301,79 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", - "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", "dependencies": { - "@babel/compat-data": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.5", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.4", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1434,30 +1405,29 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1465,9 +1435,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -1773,12 +1743,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { @@ -1794,23 +1764,23 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -1834,6 +1804,14 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", @@ -1880,9 +1858,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -1898,10 +1876,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1930,9 +1909,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "funding": [ { "type": "opencollective", @@ -2076,11 +2055,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", - "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "dependencies": { - "browserslist": "^4.25.1" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -2148,9 +2127,9 @@ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { "ms": "^2.1.3" }, @@ -2209,9 +2188,9 @@ "optional": true }, "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==" + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==" }, "node_modules/emojis-list": { "version": "2.1.0", @@ -2660,9 +2639,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": { "jsesc": "bin/jsesc" }, @@ -2850,10 +2829,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -3306,14 +3284,12 @@ "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "license": "MIT" + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "license": "MIT", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dependencies": { "regenerate": "^1.4.2" }, @@ -3322,17 +3298,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "license": "MIT", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -3341,27 +3316,25 @@ "node_modules/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "license": "MIT" + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "license": "BSD-2-Clause", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -3624,7 +3597,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "license": "MIT", "engines": { "node": ">=4" } @@ -3633,7 +3605,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -3643,19 +3614,17 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "license": "MIT", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "engines": { "node": ">=4" } @@ -3666,9 +3635,9 @@ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -3773,19 +3742,19 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "requires": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==" + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==" }, "@babel/core": { "version": "7.28.5", @@ -3822,12 +3791,12 @@ } }, "@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "requires": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -3842,11 +3811,11 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "requires": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -3874,16 +3843,16 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "requires": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "dependencies": { @@ -3895,12 +3864,12 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "requires": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "dependencies": { @@ -3912,15 +3881,15 @@ } }, "@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", "requires": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" } }, "@babel/helper-globals": { @@ -3929,31 +3898,31 @@ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" }, "@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "requires": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" } }, "@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "requires": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" } }, "@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "requires": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" } }, "@babel/helper-optimise-call-expression": { @@ -3965,9 +3934,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==" + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==" }, "@babel/helper-remap-async-to-generator": { "version": "7.27.1", @@ -3980,13 +3949,13 @@ } }, "@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "requires": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -4014,13 +3983,13 @@ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" }, "@babel/helper-wrap-function": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", - "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "requires": { - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" } }, "@babel/helpers": { @@ -4033,11 +4002,11 @@ } }, "@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "requires": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { @@ -4076,12 +4045,12 @@ } }, "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" } }, "@babel/plugin-proposal-private-property-in-object": { @@ -4091,19 +4060,19 @@ "requires": {} }, "@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-syntax-unicode-sets-regex": { @@ -4124,22 +4093,22 @@ } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.29.0" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "requires": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" } }, @@ -4152,51 +4121,51 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "requires": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" } }, "@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" } }, "@babel/plugin-transform-destructuring": { @@ -4209,12 +4178,12 @@ } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-duplicate-keys": { @@ -4226,12 +4195,12 @@ } }, "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-dynamic-import": { @@ -4243,20 +4212,20 @@ } }, "@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", - "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-export-namespace-from": { @@ -4287,11 +4256,11 @@ } }, "@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-literals": { @@ -4303,11 +4272,11 @@ } }, "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-member-expression-literals": { @@ -4328,23 +4297,23 @@ } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "requires": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "requires": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.29.0" } }, "@babel/plugin-transform-modules-umd": { @@ -4357,12 +4326,12 @@ } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-new-target": { @@ -4374,31 +4343,31 @@ } }, "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "requires": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/traverse": "^7.28.6" } }, "@babel/plugin-transform-object-super": { @@ -4411,19 +4380,19 @@ } }, "@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" } }, @@ -4436,22 +4405,22 @@ } }, "@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "requires": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-property-literals": { @@ -4463,20 +4432,20 @@ } }, "@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-reserved-words": { @@ -4496,11 +4465,11 @@ } }, "@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "requires": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" } }, @@ -4537,12 +4506,12 @@ } }, "@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/plugin-transform-unicode-regex": { @@ -4555,88 +4524,88 @@ } }, "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" } }, "@babel/preset-env": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", - "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", "requires": { - "@babel/compat-data": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.5", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.4", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "dependencies": { @@ -4658,33 +4627,33 @@ } }, "@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "requires": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" } }, "@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "requires": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "requires": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -4897,12 +4866,12 @@ "optional": true }, "babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", "requires": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "dependencies": { @@ -4914,20 +4883,20 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.6" } }, "babel-walk": { @@ -4945,6 +4914,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "optional": true }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==" + }, "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", @@ -4981,14 +4955,15 @@ } }, "browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "requires": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" } }, "call-bind": { @@ -5005,9 +4980,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==" + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==" }, "chalk": { "version": "2.4.2", @@ -5121,11 +5096,11 @@ } }, "core-js-compat": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", - "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "requires": { - "browserslist": "^4.25.1" + "browserslist": "^4.28.1" } }, "css": { @@ -5182,9 +5157,9 @@ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==" }, "debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { "ms": "^2.1.3" }, @@ -5225,9 +5200,9 @@ "optional": true }, "electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==" + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==" }, "emojis-list": { "version": "2.1.0", @@ -5558,9 +5533,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" }, "json5": { "version": "0.5.1", @@ -5699,9 +5674,9 @@ "optional": true }, "node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" }, "normalize-path": { "version": "3.0.0", @@ -6084,24 +6059,24 @@ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "requires": { "regenerate": "^1.4.2" } }, "regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "requires": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" } }, "regjsgen": { @@ -6110,19 +6085,19 @@ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" }, "regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "requires": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" } }, "resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "requires": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -6335,14 +6310,14 @@ } }, "unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==" }, "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==" }, "uniq": { "version": "1.0.1", @@ -6350,9 +6325,9 @@ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" }, "update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "requires": { "escalade": "^3.2.0", "picocolors": "^1.1.1" diff --git a/src/pretix/static/npm_dir/package.json b/src/pretix/static/npm_dir/package.json index 21d74efa0..755c5028d 100644 --- a/src/pretix/static/npm_dir/package.json +++ b/src/pretix/static/npm_dir/package.json @@ -5,7 +5,7 @@ "scripts": {}, "dependencies": { "@babel/core": "^7.28.5", - "@babel/preset-env": "^7.28.5", + "@babel/preset-env": "^7.29.0", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "vue": "^2.7.16", diff --git a/src/pretix/static/pretixcontrol/js/ui/outgoingmail.js b/src/pretix/static/pretixcontrol/js/ui/outgoingmail.js new file mode 100644 index 000000000..43600d40a --- /dev/null +++ b/src/pretix/static/pretixcontrol/js/ui/outgoingmail.js @@ -0,0 +1,43 @@ +function is_sandbox_supported() { + const iframe = document.createElement('iframe'); + return 'sandbox' in iframe; +} + +function safe_render(url, parent) { + // Estimate the height that prevents the user from having to scroll on two levels to see the full email + const height = ( + Math.max(400, window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector("footer").getBoundingClientRect().height - 20) + ) + "px"; + + const iframe = ( + // Per the HTML spec, a data: URL in an iframe is treated as its own origin: + // https://github.com/whatwg/html/pull/1756 + // It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild: + // https://github.com/whatwg/html/issues/12091 + // Together with the sandbox attribute disallowing all JavaScript, and the fact + // that we sanitize the HTML before we even save it to the database, this should + // still be the safest way to render HTML in the context of our backend. + $("