Compare commits

...

53 Commits

Author SHA1 Message Date
luelista
c808afc2e9 Apply suggestions from code review
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2026-05-12 17:46:36 +02:00
Mira Weller
baf7e098b6 Old Python compat 2026-04-28 15:23:57 +02:00
Mira Weller
a78e2f3ceb add meta property handling to cross-organizer event clone test case 2026-04-28 14:40:58 +02:00
Mira Weller
2fdd9f991c handle new duplicates in EventMetaValue.{event,property} 2026-04-28 14:40:58 +02:00
Mira Weller
111127c0fa add migrations with fixups 2026-04-28 14:09:31 +02:00
Mira Weller
f1a4b51604 add unique_together on EventMetaProperty (organizer, name) 2026-04-28 14:03:50 +02:00
Mira Weller
c9d29bbca5 Adapt event meta_properties when cloning across organizers 2026-04-28 13:33:06 +02:00
Mira Weller
bfdf959fad Validate that event and property belong to the same organizer 2026-04-28 13:33:06 +02:00
Raphael Michel
35e1df28d9 Overhaul contribution guide & add a AI policy (#6038)
* Overhaul contribution guide & add a AI policy

* Fix broken links
2026-04-21 11:32:56 +02:00
Martin Gross
7e457f7430 Set max_length to 70 but for all name fields together and not only every single one. 2026-04-21 10:45:00 +02:00
Martin Gross
5faa85ed40 isort 2026-04-21 10:45:00 +02:00
Martin Gross
1b88a84a83 Move validation into form field. 2026-04-21 10:45:00 +02:00
Martin Gross
447cffa7a8 Customer Accounts: Limit length; reject URLs in name 2026-04-21 10:45:00 +02:00
dependabot[bot]
6d255bb9cc Update defusedcsv requirement from >=1.1.0 to >=3.0.0 (#6105)
Updates the requirements on [defusedcsv](https://github.com/raphaelm/defusedcsv) to permit the latest version.
- [Commits](https://github.com/raphaelm/defusedcsv/compare/v1.1.0...v3.0.0)

---
updated-dependencies:
- dependency-name: defusedcsv
  dependency-version: 3.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 23:50:50 +02:00
dependabot[bot]
4fe405886e Update tlds requirement from >=2026021400 to >=2026041800 (#6104)
Updates the requirements on [tlds](https://github.com/kichik/tlds) to permit the latest version.
- [Commits](https://github.com/kichik/tlds/commits)

---
updated-dependencies:
- dependency-name: tlds
  dependency-version: '2026041800'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 22:44:03 +02:00
Richard Schreiber
b7d3e8a80a Add invoice numbers to paymentlist export (Z#23227966) (#6097) 2026-04-20 17:55:44 +02:00
Raphael Michel
d0d76ffddc Delete unused code (#6026)
* Delete unused code

* Delete template
2026-04-20 16:56:50 +02:00
dependabot[bot]
c04be5c0d9 Update cryptography requirement from >=44.0.0 to >=46.0.7 (#6084)
Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.0...46.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 15:09:49 +02:00
dependabot[bot]
ee1a8420a5 Update sentry-sdk requirement from ==2.57.* to ==2.58.* (#6095)
Updates the requirements on [sentry-sdk](https://github.com/getsentry/sentry-python) to permit the latest version.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.57.0...2.58.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.58.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 15:09:33 +02:00
dependabot[bot]
d9000c2a66 Update tlds requirement from >=2020041600 to >=2026021400 (#6088) 2026-04-20 15:09:04 +02:00
Yasunobu YesNo Kawaguchi
4530d864d3 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-04-20 10:09:50 +02:00
Yasunobu YesNo Kawaguchi
b968266611 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-04-20 10:09:50 +02:00
Yasunobu YesNo Kawaguchi
640518c1b3 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-04-20 10:09:50 +02:00
Nikolai
0715144a31 Translations: Update Danish
Currently translated at 48.5% (3055 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2026-04-20 10:09:50 +02:00
Tim
58ea7c8656 Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-04-20 10:09:50 +02:00
Tim
a8fe6f505e Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-04-20 10:09:50 +02:00
Nikolai
baeec92203 Translations: Update Danish
Currently translated at 47.7% (3004 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2026-04-20 10:09:50 +02:00
Tim
2f9ac05184 Translations: Update Spanish
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/es/

powered by weblate
2026-04-20 10:09:50 +02:00
Mie Frydensbjerg
4beea63b49 Translations: Update Danish
Currently translated at 46.5% (2925 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2026-04-16 11:01:56 +02:00
Nikolai
5e49df0ef6 Translations: Update Danish
Currently translated at 46.1% (2904 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2026-04-16 11:01:56 +02:00
pajowu
b3bb9fccb5 Translations: Update Danish
Currently translated at 44.2% (2784 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2026-04-16 11:01:56 +02:00
Lukas Bockstaller
e3ffd66691 Giftcard/Reusable Media API: fix expand permission check (Z#23230608) (#6091)
* add failing tests

* add permission checks in to_representation

* only overwrite final representation not the serializer

* styling

* include review
2026-04-15 15:59:08 +02:00
Martin Gross
0f2ebb8687 PPv2: Fix permission-check for ISU (event.settings.general:write to event.settings.payment:write) 2026-04-14 17:02:47 +02:00
Richard Schreiber
efd887b439 API: fix PDF-download name (Z#23231496) 2026-04-14 14:13:14 +02:00
pajowu
8690d65e99 Do not show payment text of canceled and failed payments on invoice (Z#23231070) (#6075) 2026-04-14 13:02:12 +02:00
Richard Schreiber
5682d3ed56 Do not force PDFs to be downloaded (Z#23225892) (#5994)
* Display invoice and tickets inline in browser (Z#23225892)

* Use FileResponse filename for AnswerDownload

* Use inline for PDF-view in pretix-control editor

* use as_attachment for API FileResponses

* do not ignore csp even for disposition=inline

* use as_attachment for file responses in control

* remove unused code

* improve code style

* Invoice preview inline

* do not force download on tickets in backend

* do not force download on AnswerDownload

* imrpove code style

* improve code style

* fix missing int str conversion

* Apply suggestions from code review

Co-authored-by: luelista <mira@teamwiki.de>

---------

Co-authored-by: luelista <mira@teamwiki.de>
2026-04-14 09:12:09 +02:00
pajowu
059ff6c99b Allow buttons to reuse cart (Z#23226853) (#6047)
* Allow buttons to reuse cart (Z#23226853)

* Always keep cart of buttons with items set
2026-04-13 19:32:33 +02:00
Mie Frydensbjerg
f46fc7fa69 Translations: Update Danish
Currently translated at 44.2% (2784 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/da/

powered by weblate
2026-04-13 16:02:34 +02:00
pajowu
3473fa738d Fix AttributeError in CheckPrivateNetworkMixin (#6076) 2026-04-10 12:47:53 +02:00
Ruud Hendrickx
6c7163406e Translations: Update Dutch (Belgium)
Currently translated at 82.9% (5214 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/nl_BE/

powered by weblate
2026-04-10 11:47:38 +02:00
Hijiri Umemoto
49729d2c87 Translations: Update Japanese
Currently translated at 100.0% (6287 of 6287 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/ja/

powered by weblate
2026-04-10 11:47:38 +02:00
pajowu
e80b4b560b customer login: open pw reset link in new tab (Z#23231027) (#6074)
This way customers don't have to break their checkout flow and the link works in a widgets iframe
2026-04-10 11:44:36 +02:00
pajowu
0bb04ca8f0 Email: Check custom SMTP IP at usage time 2026-04-10 10:57:08 +02:00
Raphael Michel
f50548cd02 Fix crash on build 2026-04-10 10:34:15 +02:00
Raphael Michel
bb450e1be9 Add default protection for SSRF 2026-04-10 10:34:15 +02:00
Kian Cross
6d07530d2b Waiting list: group product choices by category (#6006)
* Group waiting list product choices by category

Use optgroups to group products by category in the waiting list selection
dropdown.

Products are normally separated in the UI by category grouping, but this
context is lost in the waiting list form. When multiple products share the
same name, this can make it difficult for customers to distinguish between
them.

* Add tests for waiting list initial selection with optgroups

Verify that the initial product selection (via `?item=` and `?var=`
query parameters) works correctly when choices are grouped by category
into `<optgroup>`s. Covers both plain items and items with variations.
2026-04-10 09:14:34 +02:00
Kara Engelhardt
5d7ee584d9 Fix AttributeError when running tests with debug toolbar installed 2026-04-09 13:21:54 +02:00
Lukas Bockstaller
58cce4b85e adds fallback to PaymentDetailsField (PRETIXEU-D6V) (#6041)
* adds fallback to PaymentDetailsField

* return empty object instead of info_data
2026-04-09 12:32:46 +02:00
luelista
aa420d4353 Do not reset event list type automatically (Z#23226325) (#6068)
Co-authored-by: Kara Engelhardt <engelhardt@pretix.eu>
2026-04-08 18:47:45 +02:00
dependabot[bot]
d2ca217cd8 Bump brace-expansion from 1.1.12 to 1.1.13 in /src/pretix/static/npm_dir (#6050)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 15:30:15 +02:00
dependabot[bot]
cb6d3967a0 Bump picomatch from 2.3.1 to 2.3.2 in /src/pretix/static/npm_dir (#6030)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 15:30:05 +02:00
Raphael Michel
221cbd15ab [SECURITY] API: Add missing event filter for check-ins 2026-04-08 13:57:55 +02:00
Lukas Bockstaller
5c7104634e Order import: handle mixed endings of last line (Z#23230806) (#6066)
* handle mixed line endings in import

* formatting
2026-04-08 13:25:38 +02:00
49 changed files with 1651 additions and 780 deletions

View File

@@ -1,11 +1,16 @@
Contributing to pretix
======================
Hey there and welcome to pretix!
Welcome to pretix, we are happy that you would like to contribute.
Before you do so, please make sure to read the following documents:
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/dev/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/dev/development/setup.html).
- [Contribution workflow](https://docs.pretix.eu/dev/development/contribution/general.html)
- [AI-assisted contribution policy](https://docs.pretix.eu/dev/development/contribution/ai.html)
- [Coding style and quality](https://docs.pretix.eu/dev/development/contribution/style.html)
- [Development setup](https://docs.pretix.eu/dev/development/setup.html)
- [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html)
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
Before we can accept your first PR we'll need you to sign [our **Contributor License Agreement** (CLA)](https://pretix.eu/about/en/cla).
You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
**Before contributing new functionality, always open a discussion first.**

View File

@@ -0,0 +1,24 @@
.. _`aipolicy`:
AI-assisted contribution policy
===============================
pretix is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by humans (and sometimes machines, too).
We ask you to respect the time and effort put in by these humans by not sending low-effort, unqualified work, since it puts the burden of validation on the maintainer.
Therefore, the pretix project has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
- **The human-in-the-loop must fully understand all code.** If you can't explain what your changes do and how they interact with the greater system without the aid of AI tools, do not contribute to this project.
- **Issues and discussions can use AI assistance but must have a full human-in-the-loop.** This means that any content generated with AI must have been reviewed and edited by a human before submission. AI is very good at being overly verbose and including noise that distracts from the main point. Humans must do their research and trim this down.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).** Text and code are the only acceptable AI-generated content, per the other rules in this policy.
- **Bad AI drivers will be excluded from the project.** People who produce bad contributions that are clearly AI (slop) will be blocked from our organization without warning.
This policy was inspired by the `ghostty project`_.
.. _ghostty project: https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md

View File

@@ -1,23 +1,39 @@
General remarks
===============
Contribution workflow
=====================
You are interested in contributing to pretix? That is awesome!
If youre new to contributing to open source software, dont be afraid. Well happily review your code and give you
constructive and friendly feedback on your changes.
constructive and friendly feedback on your changes. Every contribution should go through the following steps.
First of all, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
Discussion & Design
-------------------
pretix is a large and mature project with more of a decade of history and hopefully many more decades to come.
Keeping pretix in good shape over long timeframes is first and foremost a fight against complexity.
With every additional feature, complexity grows, and both features and complexity are hard to remove.
Even if you are doing the initial work of the contribution, accepting the contribution is not free for us.
Not only will we need to maintain the feature, but every feature adds cost to the maintenance of every other feature it interacts with, and every feature adds effort for users to understand how pretix works.
Therefore, we must carefully select what features we add, based on how well they fit the system in general and of how much use they will be to our larger user base.
We strongly ask you to **create a discussion on GitHub for every new feature idea** outlining the use case and the proposed implementation design.
Pull requests without prior discussion will likely just be closed.
For bug fixes and very minor changes, you can skip this step and open a PR right away.
Development
-----------
To develop your contribution, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
If you run into any problems on your way, please do not hesitate to ask us anytime!
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
assured that we will not tolerate any form of harassment.
While developing, please have a look at our :ref:`aipolicy` and our guidelines on :ref:`codestyle`.
Sending a patch
---------------
If you improved pretix in any way, we'd be very happy if you contribute it
back to the main code base! The easiest way to do so is to `create a pull request`_
on our `GitHub repository`_.
Once you have a first draft of your changes, please `create a pull request`_ on our `GitHub repository`_.
We recommend that you create a feature branch for every issue you work on so the changes can
be reviewed individually.
@@ -25,14 +41,17 @@ Please use the test suite to check whether your changes break any existing featu
the code style checks to confirm you are consistent with pretix's coding style. You'll
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
We automatically run the tests and the code style check on every pull request on Travis CI and we wont
We automatically run the tests and the code style check on every pull request through GitHub Actions and we wont
accept any pull requests without all tests passing. However, if you don't find out *why* they are not passing,
just send the pull request and tell us we'll be glad to help.
If you add a new feature, please include appropriate documentation into your patch. If you fix a bug,
please include a regression test, i.e. a test that fails without your changes and passes after applying your changes.
Again: If you get stuck, do not hesitate to contact any of us, or Raphael personally at mail@raphaelmichel.de.
Again: If you get stuck, do not hesitate to contact us through GitHub discussions.
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
assured that we will not tolerate any form of harassment.
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
.. _GitHub repository: https://github.com/pretix/pretix

View File

@@ -6,4 +6,5 @@ Contributing to pretix
general
style
ai
codeofconduct

View File

@@ -1,5 +1,7 @@
.. spelling:word-list:: Rebase rebasing
.. _`codestyle`:
Coding style and quality
========================
@@ -28,8 +30,6 @@ Code
Commits and Pull Requests
-------------------------
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the

View File

@@ -33,9 +33,9 @@ dependencies = [
"bleach==6.3.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"cryptography>=46.0.7",
"css-inline==0.20.*",
"defusedcsv>=1.1.0",
"defusedcsv>=3.0.0",
"dnspython==2.*",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
@@ -93,11 +93,11 @@ dependencies = [
"redis==7.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.57.*",
"sentry-sdk==2.58.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
"tlds>=2026041800",
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",

View File

@@ -31,7 +31,9 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer,
)
from pretix.base.models import Order, OrderPosition, ReusableMedium
from pretix.base.models import (
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
)
logger = logging.getLogger(__name__)
@@ -80,8 +82,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
# No additional permission check performed, documented limitation of the permission system
# Would get to complex/unusable otherwise since the permission depends on the event
# Permission Check performed in to_representation
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
@@ -117,6 +118,27 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
expand_nested = self.context['request'].query_params.getlist('expand')
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if 'linked_orderposition' in expand_nested:
if instance.linked_orderposition is not None:
event = instance.linked_orderposition.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
if 'linked_giftcard.owner_ticket' in expand_nested:
gc = instance.linked_giftcard
if gc is not None and gc.owner_ticket is not None:
event = gc.owner_ticket.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id}
return r
class Meta:
model = ReusableMedium
fields = (

View File

@@ -769,7 +769,11 @@ class PaymentDetailsField(serializers.Field):
pp = value.payment_provider
if not pp:
return {}
return pp.api_payment_details(value)
try:
return pp.api_payment_details(value)
except Exception:
logger.exception("Failed to retrieve payment_details")
return {}
class OrderPaymentSerializer(I18nAwareModelSerializer):

View File

@@ -286,6 +286,19 @@ class GiftCardSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
owner_ticket = instance.owner_ticket
if owner_ticket:
event = owner_ticket.order.event
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['owner_ticket'] = {'id': instance.owner_ticket.id}
return r
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',

View File

@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter().select_related(
qs = Checkin.all.filter(list__event=self.request.event).select_related(
"position",
"device",
)

View File

@@ -381,12 +381,15 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
return FileResponse(
ct.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
)
return resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -1303,14 +1306,17 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
return FileResponse(
answer.file,
filename='{}-{}-{}-{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
),
as_attachment=True,
content_type=ftype or 'application/binary'
)
return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
@@ -1365,15 +1371,18 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
if hasattr(image_file, 'seek'):
image_file.seek(0)
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
return FileResponse(
image_file,
filename='{}-{}-{}-{}.{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
),
as_attachment=True,
content_type=ftype or 'application/binary'
)
return resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
@@ -1399,12 +1408,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
return FileResponse(
ct.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
)
return resp
@action(detail=True, methods=['POST'])
def regenerate_secrets(self, request, **kwargs):
@@ -1986,9 +1998,12 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
if not invoice.file:
raise RetryException()
resp = FileResponse(invoice.file.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(invoice.number),
as_attachment=True,
content_type='application/pdf'
)
@action(detail=True, methods=['POST'])
def transmit(self, request, **kwargs):

View File

@@ -19,7 +19,10 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import ipaddress
import logging
import smtplib
import socket
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -237,3 +240,80 @@ def base_renderers(sender, **kwargs):
def get_email_context(**kwargs):
return PlaceholderContext(**kwargs).render_all()
def create_connection(address, timeout=socket.getdefaulttimeout(),
source_address=None, *, all_errors=False):
# Taken from the python stdlib, extended with a check for local ips
host, port = address
exceptions = []
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
if timeout is not socket.getdefaulttimeout():
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
exceptions.clear()
return sock
except socket.error as exc:
if not all_errors:
exceptions.clear() # raise only the last error
exceptions.append(exc)
if sock is not None:
sock.close()
if len(exceptions):
try:
if not all_errors:
raise exceptions[0]
raise ExceptionGroup("create_connection failed", exceptions)
finally:
# Break explicitly a reference cycle
exceptions.clear()
else:
raise socket.error("getaddrinfo returns an empty list")
class CheckPrivateNetworkMixin:
# _get_socket taken 1:1 from smtplib, just with a call to our own create_connection
def _get_socket(self, host, port, timeout):
# This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit.
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
if self.debuglevel > 0:
self._print_debug('connect: to', (host, port), self.source_address)
return create_connection((host, port), timeout, self.source_address)
class SMTP(CheckPrivateNetworkMixin, smtplib.SMTP):
pass
# SMTP used here instead of mixin, because smtp.SMTP_SSL._get_socket calls super()._get_socket and then wraps this socket
# super()._get_socket needs to be our version from the mixin
class SMTP_SSL(smtplib.SMTP_SSL, SMTP): # noqa: N801
pass
class CheckPrivateNetworkSmtpBackend(EmailBackend):
@property
def connection_class(self):
return SMTP_SSL if self.use_ssl else SMTP

View File

@@ -1103,13 +1103,25 @@ class PaymentListExporter(ListExporter):
def iterate_list(self, form_data):
provider_names = dict(get_all_payment_providers())
i_numbers = Invoice.objects.filter(
order=OuterRef('order_id'),
).values('order').annotate(
m=GroupConcat('full_invoice_no', delimiter=', ')
).values(
'm'
).order_by()
payments = OrderPayment.objects.filter(
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
refunds = OrderRefund.objects.filter(
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
if form_data.get('end_date_range'):
@@ -1135,6 +1147,7 @@ class PaymentListExporter(ListExporter):
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method'), _('Comment'), _('Matching ID'), _('Payment details'),
_('Invoice numbers'),
]
yield headers
@@ -1172,6 +1185,7 @@ class PaymentListExporter(ListExporter):
obj.comment if isinstance(obj, OrderRefund) else "",
matching_id,
payment_details,
obj.order_invoice_numbers,
]
yield row

View File

@@ -90,7 +90,7 @@ from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.templatetags.rich_text import URL_RE, rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
@@ -227,9 +227,15 @@ class NamePartsFormField(forms.MultiValueField):
# bots.
r'^[^$€/%§{}<>~]*$',
message=_('Please do not use special characters in names.')
),
RegexValidator(
URL_RE,
inverse_match=True,
message=_('Please do not use special characters in names.')
)
]
}
self.max_length = defaults['max_length']
self.scheme_name = kwargs.pop('scheme')
self.titles = kwargs.pop('titles')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
@@ -287,7 +293,7 @@ class NamePartsFormField(forms.MultiValueField):
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
if sum(len(v) for v in value.values() if v) > 250:
if sum(len(v) for v in value.values() if v) > (self.max_length or 250):
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
if value.get("salutation") == "empty":

View File

@@ -0,0 +1,81 @@
# Generated by Django 5.2.12 on 2026-04-28 11:34
import logging
from django.db import IntegrityError, migrations, transaction
from django.db.models import Count, F
logger = logging.getLogger(__name__)
def fix_cross_organizer_eventmetavalues(apps, schema_editor):
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
cross_org_values = EventMetaValue.objects.filter(event__organizer__pk__ne=F('property__organizer__pk'))
for emv in cross_org_values:
logger.info(f"Fixup cross-organizer value {emv.event.organizer.slug}/{emv.event.slug}\n before: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {emv.value}")
try:
emv.property = emv.event.organizer.meta_properties.filter(name=emv.property.name).first()
logger.info(f" found existing EventMetaProperty in {emv.event.organizer.slug}")
if EventMetaValue.objects.filter(event=emv.event, property=emv.property).exists():
logger.info(f" EventMetaValue with property in correct organizer already exists, deleting the cross-organizer one")
emv.delete()
continue
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = emv.event.organizer
meta_prop.save(force_insert=True)
logger.info(f" created new EventMetaProperty")
emv.property = meta_prop
logger.info(f" after: {emv.property.name}({emv.property.id}@{emv.property.organizer.slug}) = {emv.value}")
emv.save(update_fields=["property"])
def make_eventmetaproperties_unique(apps, schema_editor):
EventMetaProperty = apps.get_model("pretixbase", "EventMetaProperty")
EventMetaValue = apps.get_model("pretixbase", "EventMetaValue")
duplicates = EventMetaProperty.objects.values('organizer', 'organizer__slug', 'name').annotate(count=Count('id')).filter(count__gt=1)
for dup in duplicates:
logger.info(f"Fixup duplicate property {dup['organizer__slug']} {dup['name']}")
props = list(EventMetaProperty.objects.filter(organizer=dup['organizer'], name=dup['name']))
# TODO: any better idea than picking the property to keep more or less randomly (first in database order)?
target = props[0]
invalid = props[1:]
try:
with transaction.atomic():
affected = EventMetaValue.objects.filter(
event__organizer=dup['organizer'], property__in=invalid
).update(
property=target
)
logger.info(f" Switching {affected} value(s) over to {target.name}({target.id}@{target.organizer.slug})")
except IntegrityError as e:
logger.info(f" Failed to switch all value(s) over to {target.name}({target.id}@{target.organizer.slug})")
logger.info(f" {e}")
for prop in invalid:
newname = f'{prop.name}_DUPLICATE_{prop.id}'
logger.info(f" Renaming {prop.name}({prop.id}@{prop.organizer.slug}) to {newname}({prop.id}@{prop.organizer.slug})")
prop.name = newname
prop.save()
else:
for prop in invalid:
logger.info(f" Deleting {prop.name}({prop.id}@{prop.organizer.slug})")
prop.delete()
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_pluggable_permissions"),
]
operations = [
migrations.RunPython(fix_cross_organizer_eventmetavalues, migrations.RunPython.noop),
migrations.RunPython(make_eventmetaproperties_unique, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.12 on 2026-04-28 11:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0299_fixup_eventmetaproperties"),
]
operations = [
migrations.AlterUniqueTogether(
name="eventmetaproperty",
unique_together={("organizer", "name")},
),
]

View File

@@ -70,6 +70,10 @@ def parse_csv(file, length=None, mode="strict", charset=None):
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# remove stray linebreaks from the end of the file
data = data.rstrip("\n")
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')

View File

@@ -877,6 +877,8 @@ class Event(EventMixin, LoggedModel):
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
)
is_cross_organizer = other.organizer_id != self.organizer_id
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
# Plugins can create data in installed() hook based on existing data of the event.
# Calling set_active_plugins() results in defaults being created while actually data
@@ -905,6 +907,15 @@ class Event(EventMixin, LoggedModel):
for emv in EventMetaValue.objects.filter(event=other):
emv.pk = None
emv.event = self
if is_cross_organizer:
try:
emv.property = self.organizer.meta_properties.get(name=emv.property.name)
except EventMetaProperty.DoesNotExist:
meta_prop = emv.property
meta_prop.pk = None
meta_prop.organizer = self.organizer
meta_prop.save(force_insert=True)
emv.property = meta_prop
emv.save(force_insert=True)
for fl in EventFooterLink.objects.filter(event=other):
@@ -958,13 +969,13 @@ class Event(EventMixin, LoggedModel):
if i.tax_rule_id:
i.tax_rule = tax_map[i.tax_rule_id]
if i.grant_membership_type and other.organizer_id != self.organizer_id:
if i.grant_membership_type and is_cross_organizer:
i.grant_membership_type = None
i.save() # no force_insert since i.picture.save could have already inserted
i.log_action('pretix.object.cloned')
if require_membership_types and other.organizer_id == self.organizer_id:
if require_membership_types and not is_cross_organizer:
i.require_membership_types.set(require_membership_types)
if not i.all_sales_channels:
@@ -979,7 +990,7 @@ class Event(EventMixin, LoggedModel):
v._prefetched_objects_cache = {}
v.save(force_insert=True)
if require_membership_types and other.organizer_id == self.organizer_id:
if require_membership_types and not is_cross_organizer:
v.require_membership_types.set(require_membership_types)
if not v.all_sales_channels:
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
@@ -1830,6 +1841,7 @@ class EventMetaProperty(LoggedModel):
class Meta:
ordering = ("position", "name",)
unique_together = ('organizer', 'name')
@property
def choice_keys(self):
@@ -1863,6 +1875,8 @@ class EventMetaValue(LoggedModel):
self.event.cache.clear()
def save(self, *args, **kwargs):
if self.event and self.event.organizer != self.property.organizer:
raise ValidationError(_("Property and event must belong to the same organizer."))
super().save(*args, **kwargs)
if self.event:
self.event.cache.clear()

View File

@@ -58,6 +58,7 @@ from pretix.base.invoicing.transmission import (
from pretix.base.models import (
ExchangeRate, Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
)
from pretix.base.models.orders import OrderPayment
from pretix.base.models.tax import EU_CURRENCIES
from pretix.base.services.tasks import (
TransactionAwareProfiledEventTask, TransactionAwareTask,
@@ -102,7 +103,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if lp and lp.payment_provider:
if lp and lp.payment_provider and lp.state not in (OrderPayment.PAYMENT_STATE_FAILED, OrderPayment.PAYMENT_STATE_CANCELED):
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:

View File

@@ -763,12 +763,7 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
resp = HttpResponse(fcontent, content_type=ftype)
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
return resp

View File

@@ -300,5 +300,4 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data)
resp['Content-Type'] = mime
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
resp._csp_ignore = True
return resp

View File

@@ -710,22 +710,26 @@ class OrderDownload(AsyncAction, OrderView):
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
return FileResponse(
value.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
),
content_type=value.type
)
return resp
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
return FileResponse(
value.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
),
content_type=value.type
)
return resp
else:
return redirect(self.get_self_url())
@@ -1831,15 +1835,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
return redirect(self.get_order_url())
try:
resp = FileResponse(self.invoice.file.file, content_type='application/pdf')
return FileResponse(
self.invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
content_type='application/pdf'
)
except FileNotFoundError:
invoice_pdf_task.apply(args=(self.invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
class OrderExtend(OrderView):
permission = 'event.orders:write'

View File

@@ -263,12 +263,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
@@ -309,6 +304,5 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
return resp

View File

@@ -19,12 +19,26 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import ipaddress
import socket
import sys
import types
from datetime import datetime
from http import cookies
from django.conf import settings
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from urllib3.exceptions import (
ConnectTimeoutError, HTTPError, LocationParseError, NameResolutionError,
NewConnectionError,
)
from urllib3.util.connection import (
_TYPE_SOCKET_OPTIONS, _set_socket_options, allowed_gai_family,
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
def monkeypatch_vobject_performance():
@@ -89,6 +103,123 @@ def monkeypatch_requests_timeout():
HTTPAdapter.send = httpadapter_send
def monkeypatch_urllib3_ssrf_protection():
"""
pretix allows HTTP requests to untrusted URLs, e.g. through webhooks or external API URLs. This is dangerous since
it can allow access to private networks that should not be reachable by users ("server-side request forgery", SSRF).
Validating URLs at submission is not sufficient, since with DNS rebinding an attacker can make a domain name pass
validation and then resolve to a private IP address on actual execution. Unfortunately, there seems no clean solution
to this in Python land, so we monkeypatch urllib3's connection management to check the IP address to be external
*after* the DNS resolution.
This does not work when a global http(s) proxy is used, but in that scenario the proxy can perform the validation.
"""
if getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
# Settings are not supposed to change during runtime, so we can optimize performance and complexity by skipping
# this if not needed.
return
def create_connection(
address: tuple[str, int],
timeout=_DEFAULT_TIMEOUT,
source_address: tuple[str, int] | None = None,
socket_options: _TYPE_SOCKET_OPTIONS | None = None,
) -> socket.socket:
# This is copied from urllib3.util.connection v2.3.0
host, port = address
if host.startswith("["):
host = host.strip("[]")
err = None
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
# The original create_connection function always returns all records.
family = allowed_gai_family()
try:
host.encode("idna")
except UnicodeError:
raise LocationParseError(f"'{host}', label empty or too long") from None
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)
if timeout is not _DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
err = None
return sock
except OSError as _:
err = _
if sock is not None:
sock.close()
if err is not None:
try:
raise err
finally:
# Break explicitly a reference cycle
err = None
else:
raise OSError("getaddrinfo returns an empty list")
class ProtectionMixin:
def _new_conn(self) -> socket.socket:
# This is 1:1 the version from urllib3.connection.HTTPConnection._new_conn v2.3.0
# just with a call to our own create_connection
try:
sock = create_connection(
(self._dns_host, self.port),
self.timeout,
source_address=self.source_address,
socket_options=self.socket_options,
)
except socket.gaierror as e:
raise NameResolutionError(self.host, self, e) from e
except socket.timeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
) from e
except OSError as e:
raise NewConnectionError(
self, f"Failed to establish a new connection: {e}"
) from e
sys.audit("http.client.connect", self, self.host, self.port)
return sock
class ProtectedHTTPConnection(ProtectionMixin, HTTPConnection):
pass
class ProtectedHTTPSConnection(ProtectionMixin, HTTPSConnection):
pass
HTTPConnectionPool.ConnectionCls = ProtectedHTTPConnection
HTTPSConnectionPool.ConnectionCls = ProtectedHTTPSConnection
def monkeypatch_cookie_morsel():
# See https://code.djangoproject.com/ticket/34613
cookies.Morsel._flags.add("partitioned")
@@ -99,4 +230,5 @@ def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-31 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"PO-Revision-Date: 2026-04-17 03:00+0000\n"
"Last-Translator: Tim <plicnetwork@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"Language: es\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.17\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -4150,7 +4150,7 @@ msgstr "Se encontraron varios productos coincidentes."
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257
#: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:99
msgid "Product variation"
msgstr "Variación del producto"
msgstr "Variante de producto"
#: pretix/base/modelimport_orders.py:161
msgid "The variation can be specified by its internal ID or full name."
@@ -4312,7 +4312,7 @@ msgstr "Ya existe un vale de compra con este código."
#: pretix/base/models/vouchers.py:199 pretix/control/views/vouchers.py:121
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:52
msgid "Maximum usages"
msgstr "Usos máximos"
msgstr "Número máximo de usos"
#: pretix/base/modelimport_vouchers.py:79
msgid "The maximum number of usages must be set."
@@ -4333,14 +4333,14 @@ msgstr "Reservar entrada con cargo a la cuota"
#: pretix/base/modelimport_vouchers.py:127 pretix/base/models/vouchers.py:236
msgid "Allow to bypass quota"
msgstr "Permitir que se anule la cuota"
msgstr "Permitir omitir la cuota"
#: pretix/base/modelimport_vouchers.py:135 pretix/base/models/vouchers.py:242
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:44
#: pretix/control/templates/pretixcontrol/vouchers/detail.html:70
#: pretix/control/views/vouchers.py:121
msgid "Price effect"
msgstr "Efecto sobre los precios"
msgstr "Efecto en el precio"
#: pretix/base/modelimport_vouchers.py:150
#, python-brace-format
@@ -4351,7 +4351,7 @@ msgstr ""
#: pretix/base/modelimport_vouchers.py:160 pretix/base/models/vouchers.py:248
msgid "Voucher value"
msgstr "Valor del vale de compra"
msgstr "Valor del vale"
#: pretix/base/modelimport_vouchers.py:165
msgid "It is pointless to set a value without a price effect."
@@ -4394,7 +4394,7 @@ msgstr "Etiqueta"
#: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:300
msgid "Shows hidden products that match this voucher"
msgstr "Mostrar los ocultados productos válidos con este vale de compra"
msgstr "Muestra los productos ocultos vinculados a este vale"
#: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:304
msgid "Offer all add-on products for free when redeeming this voucher"
@@ -7322,8 +7322,8 @@ msgid ""
"If activated, a holder of this voucher code can buy tickets, even if there "
"are none left."
msgstr ""
"Si se activa, un titular de este vale de compra puede comprar entradas, "
"incluso si no queda ninguna."
"Si se activa, el poseedor de este código de vale podrá comprar entradas "
"incluso si no quedan existencias."
#: pretix/base/models/vouchers.py:257 pretix/control/forms/vouchers.py:69
msgid ""
@@ -7338,14 +7338,14 @@ msgstr ""
#: pretix/base/models/vouchers.py:268
msgid "This variation of the product select above is being used."
msgstr "Esta variación del producto seleccionado arriba está siendo utilizada."
msgstr "Se aplica a la variante del producto seleccionado arriba."
#: pretix/base/models/vouchers.py:277
msgid ""
"If enabled, the voucher is valid for any product affected by this quota."
msgstr ""
"Si está habilitado, el vale de compra es válido para cualquier producto "
"afectado por esta cuota."
"Si se activa, el vale será válido para cualquier producto incluido en esta "
"cuota."
#: pretix/base/models/vouchers.py:284
msgid "Specific seat"
@@ -7357,16 +7357,16 @@ msgid ""
"same value for multiple vouchers, you can get statistics on how many of them "
"have been redeemed etc."
msgstr ""
"Puede utilizar este campo para agrupar múltiples vales de compra. Si "
"introduce el mismo valor para varios vales de compra, puede obtener "
"estadísticas sobre cuántos de ellos se han canjeado, etc."
"Puedes usar este campo para agrupar varios vales. Si introduces el mismo "
"valor en distintos vales, podrás obtener estadísticas sobre cuántos se han "
"canjeado, etc."
#: pretix/base/models/vouchers.py:316 pretix/base/permissions.py:242
#: pretix/control/navigation.py:289
#: pretix/control/templates/pretixcontrol/vouchers/index.html:6
#: pretix/control/templates/pretixcontrol/vouchers/index.html:8
msgid "Vouchers"
msgstr "Vales de compra"
msgstr "Vales"
#: pretix/base/models/vouchers.py:342
msgid "You cannot select a quota that belongs to a different event."
@@ -13365,7 +13365,7 @@ msgstr "Esto eliminará todos los números de teléfono de los pedidos."
#: pretix/base/shredder.py:290
msgid "Emails"
msgstr "Correos electrónicos"
msgstr "Correos"
#: pretix/base/shredder.py:292
msgid ""
@@ -15170,7 +15170,7 @@ msgstr "Todos los productos"
#: pretix/control/views/typeahead.py:780
#, python-brace-format
msgid "{product} Any variation"
msgstr "{product} - Cualquier variación"
msgstr "{product} Cualquier variación"
#: pretix/control/forms/filter.py:566 pretix/control/forms/orders.py:862
msgctxt "subevent"
@@ -15469,7 +15469,7 @@ msgstr "Buscar vale de compra"
#: pretix/control/views/vouchers.py:133
#, python-brace-format
msgid "Any product in quota \"{quota}\""
msgstr "Cualquier producto del contingente \"{quota}\""
msgstr "Cualquier producto en la cuota \"{quota}\""
#: pretix/control/forms/filter.py:2440
msgid "Refund status"
@@ -17068,15 +17068,15 @@ msgstr "ID de butaca específico"
#: pretix/control/forms/vouchers.py:200 pretix/presale/forms/waitinglist.py:103
msgid "Invalid product selected."
msgstr "Producto no válido seleccionado."
msgstr "Se ha seleccionado un producto no válido."
#: pretix/control/forms/vouchers.py:225
msgid ""
"The voucher only matches hidden products but you have not selected that it "
"should show them."
msgstr ""
"El vale de compra solo coincide con productos ocultos pero no has "
"seleccionado que los muestre."
"El vale solo coincide con productos ocultos, pero no has seleccionado que "
"deba mostrarlos."
#: pretix/control/forms/vouchers.py:271
msgid "Codes"
@@ -21474,7 +21474,7 @@ msgstr ""
#: pretix/plugins/ticketoutputpdf/views.py:172
#: pretix/presale/views/customer.py:544 pretix/presale/views/customer.py:597
msgid "Your changes have been saved."
msgstr "Los cambios se han guardado."
msgstr "Se han guardado los cambios."
#: pretix/control/templates/pretixcontrol/event/plugins.html:34
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:34
@@ -28698,7 +28698,7 @@ msgstr "Se ha creado la nueva lista de asistentes."
#: pretix/plugins/ticketoutputpdf/views.py:132
msgid "We could not save your changes. See below for details."
msgstr ""
"No hemos podido guardar los cambios. Consulte los detalles a continuación."
"No se pudieron guardar los cambios. Consulta los detalles a continuación."
#: pretix/control/views/checkin.py:421 pretix/control/views/checkin.py:458
msgid "The requested list does not exist."
@@ -30864,7 +30864,7 @@ msgstr ""
#: pretix/plugins/badges/forms.py:33
msgid "Template"
msgstr "Plantilla"
msgstr "Template"
#: pretix/plugins/badges/forms.py:34
msgid ""

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-03-23 21:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"PO-Revision-Date: 2026-04-20 08:07+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
"Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.17\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -664,7 +664,7 @@ msgstr "ギフトカードを取引で使用済み"
#: pretix/plugins/banktransfer/payment.py:483
#: pretix/presale/forms/customer.py:152
msgid "This field is required."
msgstr "この項目は必須です。"
msgstr "このフィールドは必須です。"
#: pretix/base/addressvalidation.py:213
msgid "Enter a postal code in the format XXX."
@@ -3800,7 +3800,7 @@ msgstr "単価:{net_price} 税抜 / {gross_price} 税込"
#, python-brace-format
msgctxt "invoice"
msgid "Single price: {price}"
msgstr "単価{price}"
msgstr "単価: {price}"
#: pretix/base/invoicing/pdf.py:947 pretix/base/invoicing/pdf.py:952
msgctxt "invoice"
@@ -8206,7 +8206,7 @@ msgstr "参加者の呼びかけに使う名前"
#: pretix/base/services/placeholders.py:732
#: pretix/control/forms/organizer.py:799
msgid "Mr Doe"
msgstr "山田"
msgstr "山田 太郎"
#: pretix/base/pdf.py:672 pretix/base/pdf.py:679
#: pretix/plugins/badges/exporters.py:501
@@ -11001,7 +11001,7 @@ msgstr ""
#: pretix/base/settings.py:1869 pretix/base/settings.py:1877
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:8
msgid "List"
msgstr "リスト"
msgstr "一覧"
#: pretix/base/settings.py:1870 pretix/base/settings.py:1878
msgid "Week calendar"
@@ -12855,7 +12855,7 @@ msgstr "Dr"
#: pretix/base/settings.py:3819 pretix/base/settings.py:3836
msgid "First name"
msgstr "名(First Name)"
msgstr "名"
#: pretix/base/settings.py:3820 pretix/base/settings.py:3837
msgid "Middle name"
@@ -12939,7 +12939,7 @@ msgstr "企業名を必須にするには、請求先住所を必須にする必
#: pretix/base/settings.py:4157
#, python-brace-format
msgid "VAT-ID is not supported for \"{}\"."
msgstr ""
msgstr "VAT-IDは「{}」に対してサポートされていません。"
#: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale."
@@ -19423,7 +19423,7 @@ msgstr "カスタムチェックインルール"
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:117
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html:85
msgid "Edit"
msgstr "編集する"
msgstr "編集"
#: pretix/control/templates/pretixcontrol/checkin/list_edit.html:89
msgid "Visualize"
@@ -20280,7 +20280,7 @@ msgstr "地理座標"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:271
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:275
msgid "Optional"
msgstr "オプション(必須でない項目)"
msgstr "任意"
#: pretix/control/templates/pretixcontrol/event/fragment_geodata.html:22
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:58
@@ -23636,8 +23636,8 @@ msgid ""
"this product was part of the discount calculation for a different product in "
"this order."
msgstr ""
"この製品の価格は自動割引により減額されたか、この製品がこの注文の別の製品の割"
"引計算の一部になっています。"
"自動割引によりこの商品の価格が引き下げられたか、同じ注文の別の商品に対する"
"引計算の対象になっています。"
#: pretix/control/templates/pretixcontrol/order/index.html:496
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:103
@@ -25008,7 +25008,7 @@ msgstr "デバイスの概要"
#: pretix/control/templates/pretixcontrol/organizers/device_edit.html:6
msgid "Device:"
msgstr "デバイス:"
msgstr "デバイス"
#: pretix/control/templates/pretixcontrol/organizers/device_edit.html:8
msgid "Connect a new device"
@@ -25897,7 +25897,7 @@ msgstr "二要素認証が無効です"
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:57
msgid "invited, pending response"
msgstr "招待済み、答待ち"
msgstr "招待済み、答待ち"
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:59
msgid "resend invite"
@@ -26796,8 +26796,6 @@ msgid "Add a two-factor authentication device"
msgstr "2要素認証デバイスを追加してください"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app"
msgstr "Authenticatorアプリを搭載したスマートフォン"
@@ -26806,18 +26804,20 @@ msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator."
msgstr ""
"freeOTP、Google Authenticator、Proton Authenticator などの時間ベースの"
"ワンタイムパスワードアプリをスマートフォンでご利用ください。"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
#, fuzzy
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
msgid "WebAuthn-compatible hardware token"
msgstr "WebAuthn対応のハードウェアトークンYubikey"
msgstr "WebAuthn対応のハードウェアトークン"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
msgid ""
"Use a hardware token like the Yubikey, or other biometric authentication "
"like fingerprint or face recognition."
msgstr ""
"Yubikey などのハードウェアトークンや、指紋や顔認識などの生体認証を使用してく"
"ださい。"
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:"
@@ -33303,7 +33303,7 @@ msgstr "本当にStripeアカウントを切断しますか"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/oauth_disconnect.html:16
msgid "Disconnect"
msgstr "切断します"
msgstr "切断"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:6
msgid "Payment instructions"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-04-01 17:00+0000\n"
"PO-Revision-Date: 2026-04-08 18:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_BE/>\n"
@@ -31346,7 +31346,7 @@ msgstr "We zullen u een e-mail sturen zodra we uw betaling ontvangen hebben."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:7
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:7
msgid "Export bank transfer refunds"
msgstr ""
msgstr "Terugbetalingen per bankoverschrijving exporteren"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:9
#, python-format
@@ -31354,6 +31354,8 @@ msgid ""
"<strong>%(num_new)s</strong> Bank transfer refunds have been placed and are "
"not yet part of an export."
msgstr ""
"<strong>%(num_new)s</strong> terugbetalingen per bankoverschrijving zijn "
"aangemaakt en nog niet geëxporteerd."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:15
msgid "In test mode, your exports will only contain test mode orders."
@@ -31366,6 +31368,8 @@ msgid ""
"If you want, you can now also create these exports for multiple events "
"combined."
msgstr ""
"Als u dat wilt, kunt u deze exportbestanden nu ook voor meerdere evenementen "
"tegelijk aanmaken."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:22
msgid "Go to organizer-level exports"
@@ -31377,7 +31381,7 @@ msgstr "Nieuw exportbestand aanmaken"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:38
msgid "Aggregate transactions to the same bank account"
msgstr ""
msgstr "Overschrijvingen naar hetzelfde rekeningnummer samenvoegen"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:43
msgid ""

View File

@@ -216,7 +216,7 @@ class PayView(PaypalOrderView, TemplateView):
@scopes_disabled()
@event_permission_required('event.settings.general:write')
@event_permission_required('event.settings.payment:write')
def isu_return(request, *args, **kwargs):
getparams = ['merchantId', 'merchantIdInPayPal', 'permissionsGranted', 'accountStatus', 'consentStatus', 'productIntentID', 'isEmailConfirmed']
sessionparams = ['payment_paypal_isu_event', 'payment_paypal_isu_tracking_id']
@@ -526,7 +526,7 @@ def webhook(request, *args, **kwargs):
return HttpResponse(status=200)
@event_permission_required('event.settings.general:write')
@event_permission_required('event.settings.payment:write')
@require_POST
def isu_disconnect(request, **kwargs):
del request.event.settings.payment_paypal_connect_refresh_token

View File

@@ -32,11 +32,8 @@
# 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.
from itertools import chain
from django import forms
from django.core.exceptions import ValidationError
from django.utils.encoding import force_str
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -168,46 +165,6 @@ class QuestionsForm(BaseQuestionsForm):
)
class AddOnRadioSelect(forms.RadioSelect):
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
def optgroups(self, name, value, attrs=None):
attrs = attrs or {}
groups = []
has_selected = False
for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)):
if option_value is None:
option_value = ''
if isinstance(option_label, (list, tuple)):
raise TypeError('Choice groups are not supported here')
group_name = None
subgroup = []
groups.append((group_name, subgroup, index))
selected = (
force_str(option_value) in value and
(has_selected is False or self.allow_multiple_selected)
)
if selected is True and has_selected is False:
has_selected = True
attrs['description'] = option_desc
subgroup.append(self.create_option(
name, option_value, option_label, selected, index,
subindex=None, attrs=attrs,
))
return groups
class AddOnVariationField(forms.ChoiceField):
def valid_value(self, value):
text_value = force_str(value)
for k, v, d in self.choices:
if value == k or text_value == force_str(k):
return True
return False
class MembershipForm(forms.Form):
required_css_class = 'required'

View File

@@ -83,7 +83,7 @@ class AuthenticationForm(forms.Form):
self.request = request
self.customer_cache = None
super().__init__(*args, **kwargs)
self.fields['password'].help_text = "<a href='{}'>{}</a>".format(
self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format(
build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={
'organizer': request.organizer.slug,
}),
@@ -172,7 +172,7 @@ class RegistrationForm(forms.Form):
)
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
max_length=70,
required=True,
scheme=request.organizer.settings.name_scheme,
titles=request.organizer.settings.name_scheme_titles,

View File

@@ -1,3 +0,0 @@
{% load rich_text %}
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% include "django/forms/widgets/input.html" %} {{ widget.label }}</label> {% if widget.attrs.description %}<span class="fa fa-info-circle toggle-variation-description" aria-hidden="true"></span>
<div class="variation-description addon-variation-description">{{ widget.attrs.description|rich_text }}</div>{% endif %}

View File

@@ -91,6 +91,9 @@ event_patterns = [
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/cart/add',
csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()),
name='event.cart.add'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/cart/create',
csrf_exempt(pretix.presale.views.cart.CartCreate.as_view()),
name='event.cart.create'),
re_path(r'unlock/(?P<hash>[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(),
name='event.payment.unlock'),

View File

@@ -555,6 +555,18 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
request.sales_channel.identifier, time_machine_now(default=None))
@method_decorator(allow_cors_if_namespaced, 'dispatch')
class CartCreate(EventViewMixin, CartActionMixin, View):
def get(self, request, *args, **kwargs):
if 'ajax' in self.request.GET:
cart_id = get_or_create_cart_id(self.request, create=True)
return JsonResponse({
'cart_id': cart_id,
})
else:
return redirect_to_url(self.get_success_url())
@method_decorator(allow_frame_if_namespaced, 'dispatch')
class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
task = extend_cart_reservation
@@ -843,9 +855,13 @@ class AnswerDownload(EventViewMixin, View):
return Http404()
ftype, _ = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format(
filename = '{}-cart-{}'.format(
self.request.event.slug.upper(),
os.path.basename(answer.file.name).split('.', 1)[1]
).encode("ascii", "ignore")
)
resp = FileResponse(
answer.file,
filename=filename,
content_type=ftype or 'application/binary'
)
return resp

View File

@@ -681,8 +681,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context = {}
context['list_type'] = self.request.GET.get("style", self.request.event.settings.event_list_type)
if context['list_type'] not in ("calendar", "week") and self.request.event.subevents.filter(date_from__gt=time_machine_now()).count() > 50:
if self.request.event.settings.event_list_type not in ("calendar", "week"):
self.request.event.settings.event_list_type = "calendar"
context['list_type'] = "calendar"
if context['list_type'] == "calendar":

View File

@@ -1220,30 +1220,26 @@ class OrderDownloadMixin:
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
if self.order_position.subevent:
# Subevent date in filename improves accessibility e.g. for screen reader users
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.order_position.subevent.date_from.strftime('%Y_%m_%d'),
self.output.identifier, value.extension
)
else:
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
name_parts = (
self.request.event.slug.upper(),
self.order.code,
str(self.order_position.positionid),
self.order_position.subevent.date_from.strftime('%Y_%m_%d') if self.order_position.subevent else None,
self.output.identifier
)
filename = "-".join(filter(None, name_parts)) + value.extension
return FileResponse(value.file.file, filename=filename, content_type=value.type)
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
return FileResponse(
value.file.file,
filename="{}-{}-{}{}".format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension),
content_type=value.type
)
return resp
else:
return redirect(self.get_self_url())
@@ -1383,13 +1379,14 @@ class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
return redirect(self.get_order_url())
try:
resp = FileResponse(invoice.file.file, content_type='application/pdf')
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number)),
content_type='application/pdf'
)
except FileNotFoundError:
invoice_pdf_task.apply(args=(invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
class OrderChangeMixin:

View File

@@ -66,22 +66,27 @@ class WaitingView(EventViewMixin, FormView):
if customer else None
),
)
choices = []
groups = {}
for i in items:
if not i.allow_waitinglist:
continue
category_name = str(i.category.name) if i.category else ''
group = groups.setdefault(category_name, [])
if i.has_variations:
for v in i.available_variations:
if v.cached_availability[0] == Quota.AVAILABILITY_OK:
continue
choices.append((f'{i.pk}-{v.pk}', f'{i.name} {v.value}'))
group.append((f'{i.pk}-{v.pk}', f'{i.name} {v.value}'))
else:
if i.cached_availability[0] == Quota.AVAILABILITY_OK:
continue
choices.append((f'{i.pk}', f'{i.name}'))
return choices
group.append((f'{i.pk}', f'{i.name}'))
# Remove categories where all items were available (no waiting list choices)
return [(cat, choices) for cat, choices in groups.items() if choices]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()

View File

@@ -530,12 +530,10 @@ class WidgetAPIProductList(EventListMixin, View):
]
if hasattr(self.request, 'event') and data['list_type'] not in ("calendar", "week"):
# only allow list-view of more than 50 subevents if ordering is by data as this can be done in the database
# only allow list-view of more than 50 subevents if ordering is by date as this can be done in the database
# ordering by name is currently not supported in database due to I18NField-JSON
ordering = self.request.event.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
if ordering not in ("date_ascending", "date_descending") and self.request.event.subevents.filter(date_from__gt=now()).count() > 50:
if self.request.event.settings.event_list_type not in ("calendar", "week"):
self.request.event.settings.event_list_type = "calendar"
data['list_type'] = list_type = 'calendar'
if hasattr(self.request, 'event'):

View File

@@ -223,6 +223,7 @@ CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).h
TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False)
USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False)
ALLOW_HTTP_TO_PRIVATE_NETWORKS = config.getboolean('pretix', 'allow_http_to_private_networks', fallback=False)
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
@@ -263,7 +264,8 @@ EMAIL_HOST_PASSWORD = config.get('mail', 'password', fallback='')
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
EMAIL_SUBJECT_PREFIX = '[pretix] '
EMAIL_BACKEND = EMAIL_CUSTOM_SMTP_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
EMAIL_TIMEOUT = 60
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]

View File

@@ -1835,10 +1835,9 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"optional": true,
"dependencies": {
"balanced-match": "^1.0.0",
@@ -2879,9 +2878,9 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"engines": {
"node": ">=8.6"
},
@@ -4936,9 +4935,9 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -5715,9 +5714,9 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="
},
"pify": {
"version": "4.0.1",

View File

@@ -110,6 +110,10 @@ var setCookie = function (cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString();
if (!cvalue) {
var expires = "expires=Thu, 01 Jan 1970 00:00:00 GMT";
cvalue = "";
}
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
};
var getCookie = function (name) {
@@ -726,17 +730,16 @@ var shared_methods = {
buy_callback: function (data) {
if (data.redirect) {
if (data.cart_id) {
this.$root.cart_id = data.cart_id;
setCookie(this.$root.cookieName, data.cart_id, 30);
this.$root.set_cart_id(data.cart_id);
}
if (data.redirect.substr(0, 1) === '/') {
data.redirect = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect;
}
var url = data.redirect;
if (url.indexOf('?')) {
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
} else {
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
}
url += this.$root.consent_parameter;
if (this.$root.additionalURLParams) {
@@ -779,15 +782,24 @@ var shared_methods = {
}
},
resume: function () {
if (!this.$root.get_cart_id() && this.$root.keep_cart) {
// create an empty cart whose id we can persist
this.$root.create_cart(this.resume)
return;
}
var redirect_url;
redirect_url = this.$root.target_url + 'w/' + widget_id + '/';
if (this.$root.subevent && !this.$root.cart_id) {
if (this.$root.subevent && this.$root.is_button && this.$root.items.length === 0) {
// button with subevent but no items
redirect_url += this.$root.subevent + '/';
}
redirect_url += '?iframe=1&locale=' + lang;
if (this.$root.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
if (this.$root.get_cart_id()) {
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
if (this.$root.keep_cart) {
// make sure the cart-id is used, even if the cart is currently empty
redirect_url += '&ajax=1'
}
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
@@ -1864,12 +1876,11 @@ var shared_root_methods = {
if (this.$root.variation_filter) {
url += '&variations=' + encodeURIComponent(this.$root.variation_filter);
}
var cart_id = getCookie(this.cookieName);
if (this.$root.voucher_code) {
url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
}
if (cart_id) {
url += "&cart_id=" + encodeURIComponent(cart_id);
if (this.$root.get_cart_id()) {
url += "&cart_id=" + encodeURIComponent(this.$root.get_cart_id());
}
if (this.$root.date !== null) {
url += "&date=" + this.$root.date.substr(0, 7);
@@ -1939,7 +1950,6 @@ var shared_root_methods = {
root.display_add_to_cart = data.display_add_to_cart;
root.waiting_list_enabled = data.waiting_list_enabled;
root.show_variations_expanded = data.show_variations_expanded || !!root.variation_filter;
root.cart_id = cart_id;
root.cart_exists = data.cart_exists;
root.vouchers_exist = data.vouchers_exist;
root.has_seating_plan = data.has_seating_plan;
@@ -2004,8 +2014,8 @@ var shared_root_methods = {
if (this.$root.voucher_code) {
redirect_url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
}
if (this.$root.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
if (this.$root.get_cart_id()) {
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
@@ -2027,7 +2037,28 @@ var shared_root_methods = {
this.$root.subevent = event.subevent;
this.$root.loading++;
this.$root.reload();
}
},
create_cart: function(callback) {
var url = this.$root.target_url + 'w/' + widget_id + '/cart/create?ajax=1';
this.$root.overlay.frame_loading = true;
api._getJSON(url, (data) => {
this.$root.set_cart_id(data.cart_id);
this.$root.overlay.frame_loading = false;
callback()
}, () => {
this.$root.overlay.error_message = strings['cart_error'];
this.$root.overlay.frame_loading = false;
})
},
get_cart_id: function() {
if (this.$root.keep_cart) {
return getCookie(this.$root.cookieName);
}
},
set_cart_id: function(newValue) {
setCookie(this.$root.cookieName, newValue, 30);
},
};
var shared_root_computed = {
@@ -2049,9 +2080,8 @@ var shared_root_computed = {
},
voucherFormTarget: function () {
var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang;
var cookie = getCookie(this.cookieName);
if (cookie) {
form_target += "&take_cart_id=" + cookie;
if (this.get_cart_id()) {
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
}
if (this.subevent) {
form_target += "&subevent=" + this.subevent;
@@ -2091,9 +2121,8 @@ var shared_root_computed = {
checkout_url += '?' + this.$root.additionalURLParams;
}
var form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url);
var cookie = getCookie(this.cookieName);
if (cookie) {
form_target += "&take_cart_id=" + cookie;
if (this.get_cart_id()) {
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
}
form_target += this.$root.consent_parameter
return form_target
@@ -2329,6 +2358,7 @@ var create_widget = function (element, html_id=null) {
has_seating_plan: false,
has_seating_plan_waitinglist: false,
meta_filter_fields: [],
keep_cart: true,
}
},
created: function () {
@@ -2366,6 +2396,7 @@ var create_button = function (element, html_id=null) {
var raw_items = element.attributes.items ? element.attributes.items.value : "";
var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
var disable_iframe = element.attributes["disable-iframe"] ? true : false;
var keep_cart = element.attributes["keep-cart"] ? true : false;
var button_text = element.innerHTML;
var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
for (var i = 0; i < element.attributes.length; i++) {
@@ -2417,7 +2448,8 @@ var create_button = function (element, html_id=null) {
widget_data: widget_data,
widget_id: 'pretix-widget-' + widget_id,
html_id: html_id,
button_text: button_text
button_text: button_text,
keep_cart: keep_cart || items.length > 0,
}
},
created: function () {
@@ -2426,7 +2458,7 @@ var create_button = function (element, html_id=null) {
observer.observe(this.$el, observerOptions);
},
computed: shared_root_computed,
methods: shared_root_methods
methods: shared_root_methods,
});
create_overlay(app);
return app;
@@ -2492,13 +2524,14 @@ window.PretixWidget.open = function (target_url, voucher, subevent, items, widge
frame_dismissed: false,
widget_data: all_widget_data,
widget_id: 'pretix-widget-' + widget_id,
button_text: ""
button_text: "",
keep_cart: true
}
},
created: function () {
},
computed: shared_root_computed,
methods: shared_root_methods
methods: shared_root_methods,
});
create_overlay(app);
app.$nextTick(function () {

View File

@@ -94,6 +94,9 @@ class DisableMigrations(object):
def __getitem__(self, item):
return None
def setdefault(self, key, default=None):
return
if not os.environ.get("GITHUB_WORKFLOW", ""):
MIGRATION_MODULES = DisableMigrations()

View File

@@ -171,6 +171,35 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
}
@pytest.mark.django_db
def test_giftcard_detail_expand_without_permissions(team, token_client, organizer, event, giftcard):
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
giftcard.owner_ticket = op
giftcard.save()
team.all_event_permissions = False
team.save()
res = dict(TEST_GC_RES)
res["id"] = giftcard.pk
res["issuance"] = giftcard.issuance.isoformat().replace('+00:00', 'Z')
resp = token_client.get('/api/v1/organizers/{}/giftcards/{}/?expand=owner_ticket'.format(organizer.slug, giftcard.pk))
assert resp.status_code == 200
assert resp.data["owner_ticket"] == {
"id": op.pk,
}
TEST_GIFTCARD_CREATE_PAYLOAD = {
"secret": "DEFABC",
"value": "12.00",

View File

@@ -252,6 +252,76 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
}
@pytest.mark.django_db
def test_medium_detail_event_permission_missing(token_client, organizer, event, medium, giftcard, customer, team):
team.all_organizer_permissions = False
team.limit_organizer_permissions = {
"organizer.reusablemedia:read": True,
"organizer.customers:read": True,
"organizer.giftcards:read": True,
}
team.all_event_permissions = False
team.save()
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium.linked_orderposition = op
medium.linked_giftcard = giftcard
medium.customer = customer
medium.save()
giftcard.owner_ticket = op
giftcard.save()
resp = token_client.get(
'/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard&expand='
'linked_giftcard.owner_ticket&expand=linked_orderposition&expand=customer'.format(
organizer.slug, medium.pk
)
)
assert resp.status_code == 200
assert resp.data["linked_orderposition"] == {
"id": op.pk,
}
assert resp.data["linked_giftcard"] == {
"id": giftcard.pk,
"secret": "ABCDEF",
"issuance": giftcard.issuance.isoformat().replace("+00:00", "Z"),
"value": "23.00",
"currency": "EUR",
"testmode": False,
"expires": None,
"conditions": None,
"owner_ticket": {"id": op.pk},
"issuer": "dummy",
}
assert resp.data["customer"] == {
"identifier": customer.identifier,
"external_identifier": None,
"email": "foo@example.org",
"phone": None,
"name": "Foo",
"name_parts": {"_legacy": "Foo"},
"is_active": True,
"is_verified": False,
"last_login": None,
"date_joined": customer.date_joined.isoformat().replace("+00:00", "Z"),
"locale": "en",
"last_modified": customer.last_modified.isoformat().replace("+00:00", "Z"),
"notes": None
}
TEST_MEDIUM_CREATE_PAYLOAD = {
"type": "barcode",
"identifier": "FOOBAR",

View File

@@ -238,6 +238,9 @@ def test_full_clone_cross_organizer_differences():
sc1_c = organizer.sales_channels.create(identifier="c")
sc2_a = organizer2.sales_channels.get(identifier="web")
sc2_c = organizer2.sales_channels.create(identifier="c")
o1_meta_prop_a = organizer.meta_properties.create(name="Prop to copy")
o1_meta_prop_b = organizer.meta_properties.create(name="Prop to find")
o2_meta_prop_b = organizer2.meta_properties.create(name="Prop to find")
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
@@ -262,6 +265,9 @@ def test_full_clone_cross_organizer_differences():
event.settings.payment_giftcard__enabled = True
event.settings.payment_giftcard__restrict_to_sales_channels = ['web', 'b', 'c']
event.meta_values.create(property=o1_meta_prop_a, value='a')
event.meta_values.create(property=o1_meta_prop_b, value='b')
copied_event = Event.objects.create(
organizer=organizer2, name='Dummy2', slug='dummy2',
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
@@ -284,3 +290,9 @@ def test_full_clone_cross_organizer_differences():
assert event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'b', 'c']
assert copied_event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'c']
assert event.meta_values.get(property__name=o1_meta_prop_a.name).property.organizer == organizer
assert copied_event.meta_values.get(property__name=o1_meta_prop_a.name).value == 'a'
assert copied_event.meta_values.get(property__name=o1_meta_prop_a.name).property.organizer == organizer2
assert copied_event.meta_values.get(property=o2_meta_prop_b).value == 'b'
assert copied_event.meta_values.get(property=o2_meta_prop_b).property.organizer == organizer2

View File

@@ -35,8 +35,11 @@
import datetime
import os
import re
import socket
from contextlib import contextmanager
from decimal import Decimal
from email.mime.text import MIMEText
from unittest import mock
import pytest
from django.conf import settings
@@ -591,3 +594,117 @@ def test_attached_ical_localization(env, order):
assert len(djmail.outbox) == 1
assert len(djmail.outbox[0].attachments) == 1
assert description in djmail.outbox[0].attachments[0][1]
PRIVATE_IPS_RES = [
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
]
@contextmanager
def assert_mail_connection(res, should_connect, use_ssl):
with (
mock.patch('socket.socket') as mock_socket,
mock.patch('socket.getaddrinfo', return_value=res),
mock.patch('smtplib.SMTP.getreply', return_value=(220, "")),
mock.patch('smtplib.SMTP.sendmail'),
mock.patch('ssl.SSLContext.wrap_socket') as mock_ssl
):
yield
if should_connect:
mock_socket.assert_called_once()
mock_socket.return_value.connect.assert_called_once_with(res[0][-1])
if use_ssl:
mock_ssl.assert_called_once()
else:
mock_socket.assert_not_called()
mock_socket.return_value.connect.assert_not_called()
mock_ssl.assert_not_called()
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
@pytest.mark.parametrize("use_ssl", [
True, False
])
def test_private_smtp_ip(res, use_ssl, settings):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = False
with assert_mail_connection(res=res, should_connect=False, use_ssl=use_ssl), pytest.raises(match="Request to .* blocked"):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = True
with assert_mail_connection(res=res, should_connect=True, use_ssl=use_ssl):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
@pytest.mark.parametrize("use_ssl", [
True, False
])
@pytest.mark.parametrize("allow_private", [
True, False
])
def test_public_smtp_ip(use_ssl, allow_private, settings):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private
with assert_mail_connection(res=[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('8.8.8.8', 443))], should_connect=True, use_ssl=use_ssl):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
@pytest.mark.django_db
@pytest.mark.parametrize("use_ssl", [
True, False
])
@pytest.mark.parametrize("allow_private_networks", [
True, False
])
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private_networks
event, user, organizer = env
event.settings.smtp_use_custom = True
event.settings.smtp_host = "example.com"
event.settings.smtp_use_ssl = use_ssl
event.settings.smtp_use_tls = False
def send_mail():
m = OutgoingMail.objects.create(
to=['recipient@example.com'],
subject='Test',
body_plain='Test',
sender='sender@example.com',
event=event
)
assert m.status == OutgoingMail.STATUS_QUEUED
mail_send_task.apply(kwargs={
'outgoing_mail': m.pk,
}, max_retries=0)
m.refresh_from_db()
return m
with assert_mail_connection(res=res, should_connect=allow_private_networks, use_ssl=use_ssl):
m = send_mail()
if allow_private_networks:
assert m.status == OutgoingMail.STATUS_SENT
else:
assert m.status == OutgoingMail.STATUS_FAILED

View File

@@ -991,3 +991,30 @@ def test_import_mixed_order_size_consistency(user, event, item):
).get()
assert ('Inconsistent data in row 2: Column Email address contains value "a2@example.com", but for this order, '
'the value has already been set to "a1@example.com".') in str(excinfo.value)
@pytest.mark.django_db
@scopes_disabled()
def test_import_line_endings_mix(event, item, user):
# Ensures import works with mixed file endings.
# See Ticket#23230806 where a file to import ends with \r\n
settings = dict(DEFAULT_SETTINGS)
settings['item'] = 'static:{}'.format(item.pk)
cf = inputfile_factory()
file = cf.file
file.seek(0)
data = file.read()
data = data.replace(b'\n', b'\r')
data = data.rstrip(b'\r\r')
data = data + b'\r\n'
print(data)
cf.file.save("input.csv", ContentFile(data))
cf.save()
import_orders.apply(
args=(event.pk, cf.id, settings, 'en', user.pk)
)
assert event.orders.count() == 3
assert OrderPosition.objects.count() == 3

View File

@@ -0,0 +1,93 @@
#
# 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 <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from socket import AF_INET, SOCK_STREAM
from unittest import mock
import pytest
import requests
from django.test import override_settings
from dns.inet import AF_INET6
from urllib3.exceptions import HTTPError
def test_local_blocked():
with pytest.raises(HTTPError, match="Request to local address.*"):
requests.get("http://localhost", timeout=0.1)
with pytest.raises(HTTPError, match="Request to local address.*"):
requests.get("https://localhost", timeout=0.1)
def test_private_ip_blocked():
with pytest.raises(HTTPError, match="Request to private address.*"):
requests.get("http://10.0.0.1", timeout=0.1)
with pytest.raises(HTTPError, match="Request to private address.*"):
requests.get("https://10.0.0.1", timeout=0.1)
@pytest.mark.django_db
@pytest.mark.parametrize("res", [
[(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
[(AF_INET6, SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
])
def test_dns_resolving_to_local_blocked(res):
with mock.patch('socket.getaddrinfo') as mock_addr:
mock_addr.return_value = res
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
requests.get("https://example.org", timeout=0.1)
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
requests.get("http://example.org", timeout=0.1)
def test_dns_remote_allowed():
class SocketOk(Exception):
pass
def side_effect(*args, **kwargs):
raise SocketOk
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('8.8.8.8', 443))]
mock_socket.side_effect = side_effect
with pytest.raises(SocketOk):
requests.get("https://example.org", timeout=0.1)
@override_settings(ALLOW_HTTP_TO_PRIVATE_NETWORKS=True)
def test_local_is_allowed():
class SocketOk(Exception):
pass
def side_effect(*args, **kwargs):
raise SocketOk
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.1', 443))]
mock_socket.side_effect = side_effect
with pytest.raises(SocketOk):
requests.get("https://example.org", timeout=0.1)

View File

@@ -1162,6 +1162,65 @@ class WaitingListTest(EventTestMixin, SoupTest):
assert wle.voucher is None
assert wle.locale == 'en'
def test_initial_selection(self):
with scopes_disabled():
cat = ItemCategory.objects.create(event=self.event, name='Tickets')
self.item.category = cat
self.item.save()
item2 = Item.objects.create(
event=self.event, name='VIP ticket',
default_price=Decimal('25.00'),
active=True, category=cat,
)
self.q.items.add(item2)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d' % (
self.orga.slug, self.event.slug, item2.pk
)
)
self.assertEqual(response.status_code, 200)
doc = BeautifulSoup(response.render().content, "lxml")
select = doc.find('select', {'name': 'itemvar'})
optgroup = select.find('optgroup')
self.assertIsNotNone(optgroup, 'Choices should be grouped by category')
self.assertEqual(optgroup['label'], 'Tickets')
selected = select.find_all('option', selected=True)
self.assertEqual(len(selected), 1, 'Exactly one option should be pre-selected')
self.assertEqual(selected[0]['value'], str(item2.pk))
def test_initial_selection_with_variation(self):
with scopes_disabled():
cat = ItemCategory.objects.create(event=self.event, name='Tickets')
self.item.category = cat
self.item.has_variations = True
self.item.save()
var1 = ItemVariation.objects.create(item=self.item, value='Standard')
var2 = ItemVariation.objects.create(item=self.item, value='Premium')
self.q.variations.add(var1, var2)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d&var=%d' % (
self.orga.slug, self.event.slug,
self.item.pk, var2.pk,
)
)
self.assertEqual(response.status_code, 200)
doc = BeautifulSoup(response.render().content, "lxml")
select = doc.find('select', {'name': 'itemvar'})
optgroup = select.find('optgroup')
self.assertIsNotNone(optgroup, 'Choices should be grouped by category')
self.assertEqual(optgroup['label'], 'Tickets')
selected = select.find_all('option', selected=True)
self.assertEqual(len(selected), 1, 'Exactly one option should be pre-selected')
self.assertEqual(selected[0]['value'], '%d-%d' % (self.item.pk, var2.pk))
def test_subevent_valid(self):
with scopes_disabled():
self.event.has_subevents = True