Compare commits

...

5 Commits

Author SHA1 Message Date
Raphael Michel
d1ba5c298d Create regression tests 2026-03-30 19:16:17 +02:00
Raphael Michel
c6c48537dd Do not create cart session on view with active session 2026-03-30 19:16:03 +02:00
Raphael Michel
ab08dea9f7 Skip useless code paths in CartMixin 2026-03-30 18:58:10 +02:00
Raphael Michel
4d15731528 Do not create useless cart session accessing invoice address 2026-03-30 18:57:45 +02:00
Raphael Michel
a2cef22ea8 Bump version to 2026.4.0.dev0 2026-03-30 15:01:39 +02:00
4 changed files with 60 additions and 13 deletions

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.3.0"
__version__ = "2026.4.0.dev0"

View File

@@ -70,18 +70,21 @@ def cached_invoice_address(request):
# do not create a session, if we don't have a session we also don't have an invoice address ;)
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address
cs = cart_session(request)
iapk = cs.get('invoice_address')
if not iapk:
cs = cart_session(request, create=False)
if cs is None:
request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
iapk = cs.get('invoice_address')
if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address
@@ -111,6 +114,12 @@ class CartMixin:
return cached_invoice_address(self.request)
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
if not self.request.session.session_key and not order:
# The user has not even a session ID yet, so they can't have a cart and we can save a lot of work
return {
'positions': [],
# Other keys are not used on non-checkout pages
}
if queryset is not None:
prefetch = []
if answers:
@@ -166,7 +175,8 @@ class CartMixin:
else:
fees = []
if not order:
if not order and lcp:
# Do not re-round for empty cart (useless) or confirmed order (incorrect)
apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees])
total = sum([c.price for c in lcp]) + sum([f.value for f in fees])
@@ -277,6 +287,12 @@ class CartMixin:
}
def current_selected_payments(self, positions, fees, invoice_address, *, warn=False):
from pretix.presale.views.cart import get_or_create_cart_id
if not get_or_create_cart_id(self.request, create=False):
# No active cart ID, no payments there
return []
raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here

View File

@@ -417,7 +417,7 @@ def get_or_create_cart_id(request, create=True):
return new_id
def cart_session(request):
def cart_session(request, create=True):
"""
Before pretix 1.8.0, all checkout-related information (like the entered email address) was stored
in the user's regular session dictionary. This led to data interference and leaks for example if a
@@ -428,7 +428,9 @@ def cart_session(request):
active cart session sub-dictionary for read and write access.
"""
request.session.modified = True
cart_id = get_or_create_cart_id(request)
cart_id = get_or_create_cart_id(request, create=create)
if not cart_id and not create:
return None
return request.session['carts'][cart_id]

View File

@@ -36,6 +36,7 @@
import datetime
import re
from decimal import Decimal
from importlib import import_module
from json import loads
from zoneinfo import ZoneInfo
@@ -80,6 +81,34 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn(str(self.event.name), doc.find("h1").text)
def test_no_session_cookie_set_on_event_index_view(self):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
assert settings.SESSION_COOKIE_NAME not in self.client.cookies
def test_no_cart_session_added_on_event_index_view(self):
# Make sure a session is present by doing a cart op on another event
event2 = Event.objects.create(
organizer=self.orga, name='30C3b', slug='30c3b',
date_from=datetime.datetime(now().year + 1, 12, 26, 14, 0, tzinfo=datetime.timezone.utc),
live=True,
)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, event2.slug), {
'item_%d' % 1337: '1', # item does not need to exist
'ajax': 1
})
assert settings.SESSION_COOKIE_NAME in self.client.cookies
# Visit shop, make sure no session is created
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
session = SessionStore(self.client.cookies[settings.SESSION_COOKIE_NAME].value).load()
assert set(session.keys()) == {
f"current_cart_event_{event2.pk}", "carts"
}
def test_not_found(self):
resp = self.client.get('/%s/%s/' % ('foo', 'bar'))
self.assertEqual(resp.status_code, 404)