mirror of
https://github.com/pretix/pretix.git
synced 2026-07-04 05:01:54 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 943b319557 | |||
| 28b13667ce | |||
| b00d1c9156 | |||
| 120317a8f2 | |||
| 7d5b00a610 | |||
| 83612c7d65 | |||
| 7a5f96369a | |||
| 7fd6bf41f9 | |||
| 458c3d4b83 | |||
| d10d061e45 | |||
| d30bca50f7 | |||
| 3903aca7c9 | |||
| 493c920aba | |||
| 09b7bc00b0 | |||
| 2e195c0274 | |||
| 18cb9c1816 | |||
| c3e0120f9f |
+2
-1
@@ -53,6 +53,7 @@ dependencies = [
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.7.*",
|
||||
"django-phonenumber-field==8.4.*",
|
||||
"django-querytagger==0.0.3",
|
||||
"django-redis==6.0.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.7.*",
|
||||
@@ -93,7 +94,7 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==4.5.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.63.*",
|
||||
"sentry-sdk==2.64.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ localecompile:
|
||||
./manage.py compilemessages
|
||||
|
||||
localegen:
|
||||
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
|
||||
./manage.py makemessages --keep-pot --add-location file -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
|
||||
|
||||
staticfiles: npminstall npmbuild jsi18n
|
||||
./manage.py collectstatic --noinput
|
||||
|
||||
@@ -118,6 +118,7 @@ ALL_LANGUAGES = [
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('es-419', _('Spanish (Latin America)')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
]
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# 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 logging
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from urllib.parse import urlparse, urlsplit
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
@@ -43,6 +45,8 @@ from pretix.multidomain.urlreverse import (
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_supported = None
|
||||
|
||||
|
||||
@@ -223,7 +227,26 @@ def _parse_csp(header):
|
||||
return h
|
||||
|
||||
|
||||
VALID_CSP_DIRECTIVES = [
|
||||
"child-src", "connect-src", "default-src", "fenced-frame-src", "font-src", "form-action", "frame-src", "img-src",
|
||||
"manifest-src", "media-src", "object-src", "prefetch-src", "report-uri", "script-src", "script-src-elem",
|
||||
"script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src",
|
||||
]
|
||||
|
||||
CSP_ILLEGAL_CHARS = re.compile(r'[\s,;]')
|
||||
|
||||
|
||||
def _sanitize_csp(h):
|
||||
for k, v in h.items():
|
||||
if k not in VALID_CSP_DIRECTIVES:
|
||||
raise ValueError("Invalid CSP directive " + k)
|
||||
if any(CSP_ILLEGAL_CHARS.search(el) for el in v):
|
||||
logger.warning("Stripping invalid component from CSP: %r", h)
|
||||
h[k] = [el for el in v if not CSP_ILLEGAL_CHARS.search(el)]
|
||||
|
||||
|
||||
def _render_csp(h):
|
||||
_sanitize_csp(h)
|
||||
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
|
||||
|
||||
|
||||
@@ -243,21 +266,7 @@ def _merge_csp(a, b):
|
||||
|
||||
|
||||
class SecurityMiddleware(MiddlewareMixin):
|
||||
CSP_EXEMPT = (
|
||||
'/api/v1/docs/',
|
||||
)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
def nested_dict_values(d):
|
||||
for v in d.values():
|
||||
if isinstance(v, dict):
|
||||
yield from nested_dict_values(v)
|
||||
else:
|
||||
if isinstance(v, str):
|
||||
yield v
|
||||
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
# pages
|
||||
@@ -268,18 +277,15 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
img_src = []
|
||||
gs = global_settings_object(request)
|
||||
if gs.settings.leaflet_tiles:
|
||||
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||
if not getattr(resp, '_csp_ignore', False):
|
||||
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
font_src = set()
|
||||
if hasattr(request, 'event'):
|
||||
for font in get_fonts(request.event, pdf_support_required=False).values():
|
||||
for path in list(nested_dict_values(font)):
|
||||
font_location = urlparse(path)
|
||||
if font_location.scheme and font_location.netloc:
|
||||
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
|
||||
return resp
|
||||
|
||||
def _build_csp(self, request, resp):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
@@ -288,8 +294,8 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'frame-src': ['{static}'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}"],
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"] + list(font_src),
|
||||
'img-src': ["{static}", "{media}", "data:"],
|
||||
'font-src': ["{static}"],
|
||||
'media-src': ["{static}", "data:"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
@@ -298,6 +304,13 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
||||
}
|
||||
|
||||
gs = global_settings_object(request)
|
||||
if gs.settings.leaflet_tiles:
|
||||
h['img-src'].append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
|
||||
|
||||
if hasattr(request, 'event'):
|
||||
h['font-src'] += list(self._get_font_origins(request.event))
|
||||
|
||||
if settings.VITE_DEV_MODE:
|
||||
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
||||
h['style-src'] += ["'unsafe-inline'"]
|
||||
@@ -309,6 +322,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
if not settings.VITE_DEV_MODE:
|
||||
# can't have 'unsafe-inline' and nonce at the same time
|
||||
h['style-src'].append(nonce)
|
||||
|
||||
# Only include pay.google.com for wallet detection purposes on the Payment selection page
|
||||
if (
|
||||
url.url_name == "event.order.pay.change" or
|
||||
@@ -317,27 +331,32 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
h['script-src'].append('https://pay.google.com')
|
||||
h['frame-src'].append('https://pay.google.com')
|
||||
h['connect-src'].append('https://google.com/pay')
|
||||
|
||||
if settings.LOG_CSP:
|
||||
h['report-uri'] = ["/csp_report/"]
|
||||
|
||||
if 'Content-Security-Policy' in resp:
|
||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||
|
||||
if settings.CSP_ADDITIONAL_HEADER:
|
||||
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
|
||||
|
||||
staticdomain = "'self'"
|
||||
dynamicdomain = "'self'"
|
||||
mediadomain = "'self'"
|
||||
placeholders = {
|
||||
"{static}": ["'self'"],
|
||||
"{dynamic}": ["'self'"],
|
||||
"{media}": ["'self'"],
|
||||
}
|
||||
if settings.MEDIA_URL.startswith('http'):
|
||||
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
|
||||
if settings.STATIC_URL.startswith('http'):
|
||||
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
|
||||
if settings.SITE_URL.startswith('http'):
|
||||
if settings.SITE_URL.find('/', 9) > 0:
|
||||
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
||||
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
||||
else:
|
||||
staticdomain += " " + settings.SITE_URL
|
||||
dynamicdomain += " " + settings.SITE_URL
|
||||
placeholders["{static}"].append(settings.SITE_URL)
|
||||
placeholders["{dynamic}"].append(settings.SITE_URL)
|
||||
|
||||
if hasattr(request, 'organizer') and request.organizer:
|
||||
if hasattr(request, 'event') and request.event:
|
||||
@@ -348,18 +367,29 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
dynamicdomain += " " + domain
|
||||
placeholders["{dynamic}"].append(domain)
|
||||
|
||||
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||
media=mediadomain)
|
||||
for k, v in h.items():
|
||||
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
|
||||
resp['Content-Security-Policy'] = _render_csp(h)
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
for k, v in h.items():
|
||||
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
|
||||
|
||||
return resp
|
||||
return h
|
||||
|
||||
def _get_font_origins(self, event):
|
||||
def nested_dict_values(d):
|
||||
for v in d.values():
|
||||
if isinstance(v, dict):
|
||||
yield from nested_dict_values(v)
|
||||
else:
|
||||
if isinstance(v, str):
|
||||
yield v
|
||||
|
||||
font_src = set()
|
||||
for font in get_fonts(event, pdf_support_required=False).values():
|
||||
for path in list(nested_dict_values(font)):
|
||||
font_location = urlparse(path)
|
||||
if font_location.scheme and font_location.netloc:
|
||||
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
|
||||
return font_src
|
||||
|
||||
|
||||
class RejectInvalidInputMiddleware(MiddlewareMixin):
|
||||
|
||||
@@ -647,25 +647,22 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
|
||||
)
|
||||
|
||||
def has_active_staff_session(self, session_key=None):
|
||||
def has_active_staff_session(self, session_key):
|
||||
"""
|
||||
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
||||
with the given session key.
|
||||
"""
|
||||
return self.get_active_staff_session(session_key) is not None
|
||||
|
||||
def get_active_staff_session(self, session_key=None):
|
||||
if not self.is_staff:
|
||||
def get_active_staff_session(self, session_key):
|
||||
if not self.is_staff or not session_key:
|
||||
return None
|
||||
if not hasattr(self, '_staff_session_cache'):
|
||||
self._staff_session_cache = {}
|
||||
if session_key not in self._staff_session_cache:
|
||||
qs = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True
|
||||
)
|
||||
if session_key:
|
||||
qs = qs.filter(session_key=session_key)
|
||||
sess = qs.first()
|
||||
sess = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True, session_key=session_key
|
||||
).first()
|
||||
if sess:
|
||||
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||
sess.date_end = now()
|
||||
|
||||
@@ -1924,8 +1924,6 @@ DEFAULTS = {
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Hide all past dates from calendar"),
|
||||
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
|
||||
"calendar.")
|
||||
)
|
||||
},
|
||||
'allow_modifications': {
|
||||
|
||||
@@ -19,55 +19,14 @@
|
||||
# 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 logging
|
||||
import os
|
||||
|
||||
from celery import Celery, signals
|
||||
from django.dispatch import receiver
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
app = Celery('pretix')
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
|
||||
|
||||
@receiver(signals.before_task_publish)
|
||||
def on_before_task_publish(sender, body, exchange, routing_key, headers, properties, declare, retry_policy, **kwargs):
|
||||
from pretix.helpers.logs import local
|
||||
|
||||
trace = getattr(local, 'trace', [])
|
||||
request_id = getattr(local, 'request_id', None)
|
||||
if request_id:
|
||||
trace.append(request_id)
|
||||
|
||||
headers["X-Pretix-Trace"] = " ".join(trace)
|
||||
|
||||
|
||||
@receiver(signals.task_received)
|
||||
def on_task_received(sender, request, **kwargs):
|
||||
trace = request._request_dict.get("X-Pretix-Trace")
|
||||
if trace:
|
||||
logger.info(f"Task {request.id} has trace {trace}")
|
||||
|
||||
|
||||
@receiver(signals.task_prerun)
|
||||
def on_task_prerun(sender, task_id, task, **kwargs):
|
||||
from pretix.helpers.logs import local
|
||||
|
||||
if "X-Pretix-Trace" in task.request.headers:
|
||||
local.trace = task.request.headers["X-Pretix-Trace"].split(" ")
|
||||
else:
|
||||
local.trace = []
|
||||
local.trace.append(task_id)
|
||||
|
||||
|
||||
@receiver(signals.task_postrun)
|
||||
def on_task_postrun(sender, task_id, task, **kwargs):
|
||||
from pretix.helpers.logs import local
|
||||
|
||||
local.request_id = None
|
||||
local.trace = []
|
||||
|
||||
@@ -36,6 +36,7 @@ from urllib.parse import quote, urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, resolve_url
|
||||
from django.template.response import TemplateResponse
|
||||
@@ -214,12 +215,15 @@ class AuditLogMiddleware:
|
||||
hijack_history = request.session.get('hijack_history', False)
|
||||
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
|
||||
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
|
||||
if ss:
|
||||
ss.logs.create(
|
||||
url=request.path,
|
||||
method=request.method,
|
||||
impersonating=request.user
|
||||
)
|
||||
if not ss:
|
||||
# Staff session expired or not found
|
||||
logout(request)
|
||||
return redirect_to_login(request.get_full_path())
|
||||
ss.logs.create(
|
||||
url=request.path,
|
||||
method=request.method,
|
||||
impersonating=request.user
|
||||
)
|
||||
else:
|
||||
ss = request.user.get_active_staff_session(request.session.session_key)
|
||||
if ss:
|
||||
|
||||
@@ -31,6 +31,7 @@ from django.contrib.auth import (
|
||||
)
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
@@ -221,11 +222,13 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
|
||||
staff_session = request.user.get_active_staff_session(request.session.session_key)
|
||||
self.request.user.log_action('pretix.control.auth.user.impersonated',
|
||||
user=request.user,
|
||||
data={
|
||||
'other': self.kwargs.get("id"),
|
||||
'other_email': self.object.email
|
||||
'other_email': self.object.email,
|
||||
'staff_session': staff_session.pk,
|
||||
})
|
||||
oldkey = request.session.session_key
|
||||
|
||||
@@ -249,6 +252,12 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
with signals.no_update_last_login(), keep_session_age(request.session):
|
||||
login(request, hijacked, backend=backend)
|
||||
|
||||
request.session.save()
|
||||
staff_session.logs.create(
|
||||
method='(NOTE)',
|
||||
url=f'Begin impersonating user #{hijacked.pk} (request session {oldkey[:8]} -> {request.session.session_key[:8]})',
|
||||
)
|
||||
|
||||
request.session["hijack_history"] = hijack_history
|
||||
|
||||
signals.hijack_started.send(
|
||||
@@ -265,13 +274,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
impersonated = request.user
|
||||
|
||||
hijs = request.session['hijacker_session']
|
||||
staff_session_key = request.session['hijacker_session']
|
||||
prev_session_key = request.session.session_key
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijacked = request.user
|
||||
prev_session = hijack_history.pop()
|
||||
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
|
||||
staff_session = hijacker.get_active_staff_session(staff_session_key)
|
||||
if not staff_session:
|
||||
raise PermissionDenied
|
||||
|
||||
expected_hash = salted_hmac(
|
||||
key_salt=b"hijack-history-hash",
|
||||
@@ -299,17 +310,22 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
hijacked=hijacked,
|
||||
)
|
||||
|
||||
ss = request.user.get_active_staff_session(hijs)
|
||||
if ss:
|
||||
request.session.save()
|
||||
ss.session_key = request.session.session_key
|
||||
ss.save()
|
||||
request.session.save()
|
||||
staff_session.session_key = request.session.session_key
|
||||
staff_session.save()
|
||||
|
||||
staff_session.logs.create(
|
||||
method='(NOTE)',
|
||||
url=f'Stop impersonating user #{hijacked.pk} (request session {prev_session_key[:8]}, '
|
||||
f'staff session {staff_session_key[:8]} -> {request.session.session_key[:8]})',
|
||||
)
|
||||
|
||||
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
|
||||
user=request.user,
|
||||
data={
|
||||
'other': impersonated.pk,
|
||||
'other_email': impersonated.email
|
||||
'other': hijacked.pk,
|
||||
'other_email': hijacked.email,
|
||||
'staff_session': staff_session.pk,
|
||||
})
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.core.signals import request_finished
|
||||
from django.dispatch import receiver
|
||||
@@ -66,9 +65,7 @@ class RequestIdMiddleware:
|
||||
import sentry_sdk
|
||||
sentry_sdk.set_tag("request_id", request.request_id)
|
||||
else:
|
||||
# Web server did not pass a request ID, we still generate one to correlate between django logs and
|
||||
# celery logs
|
||||
local.request_id = request.request_id = str(uuid.uuid4())
|
||||
local.request_id = request.request_id = None
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% load anonymize_email %}
|
||||
{% block thetitle %}
|
||||
{% if messages %}
|
||||
{{ messages|join:" " }} ::
|
||||
{{ messages|join:" " }} ::
|
||||
{% endif %}
|
||||
{% block title %}{% endblock %}{% if request.resolver_match.url_name != "event.index" %} :: {% endif %}{{ event.name }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load static %}
|
||||
{% load eventurl %}
|
||||
{% load cache_large %}
|
||||
{% load money %}
|
||||
@@ -39,6 +40,7 @@
|
||||
{% else %}
|
||||
<meta property="og:url" content="{% abseventurl request.event "presale:event.index" %}" />
|
||||
{% endif %}
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/csrfcookieretry.js" %}"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
{% load money %}
|
||||
{% load expiresformat %}
|
||||
{% load eventurl %}
|
||||
{% load static %}
|
||||
{% load phone_format %}
|
||||
{% load rich_text %}
|
||||
{% load getitem %}
|
||||
@@ -22,6 +23,10 @@
|
||||
{% endif %}
|
||||
{% trans "Order details" %}
|
||||
{% endblock %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/csrfcookieretry.js" %}"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% if "thanks" in request.GET or "paid" in request.GET %}
|
||||
<div class="thank-you">
|
||||
|
||||
@@ -21,22 +21,21 @@
|
||||
data-date="{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}">
|
||||
<p>
|
||||
{% if day.events %}
|
||||
<a href="#selected-day" class="day-label event hidden-sm hidden-md hidden-lg">
|
||||
<a href="#selected-day" class="day-label event hidden-sm hidden-md hidden-lg" aria-describedby="nr-of-events-{{ day.date|date_fast:"Y-m-d" }}">
|
||||
<b aria-hidden="true">{{ day.day }}</b>
|
||||
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="sr-only">
|
||||
{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}
|
||||
</time>
|
||||
<span class="sr-only">
|
||||
({% blocktrans trimmed count count=day.events|length %}
|
||||
{{ count }} event
|
||||
{% plural %}
|
||||
{{ count }} events
|
||||
{% endblocktrans %})
|
||||
</span>
|
||||
<span class="sr-only" id="nr-of-events-{{ day.date|date_fast:"Y-m-d" }}">{% blocktrans trimmed count count=day.events|length %}
|
||||
{{ count }} event
|
||||
{% plural %}
|
||||
{{ count }} events
|
||||
{% endblocktrans %}</span>
|
||||
</a>
|
||||
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="hidden-xs">{{ day.day }}</time>
|
||||
{% else %}
|
||||
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="day-label">{{ day.day }}</time>
|
||||
<span class="sr-only">{% trans "No events" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<ul class="events">
|
||||
|
||||
@@ -650,6 +650,10 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, sales_channel, eve
|
||||
if hide:
|
||||
continue
|
||||
|
||||
if s.event_calendar_future_only:
|
||||
if (se.date_to or se.date_from) < time_machine_now():
|
||||
continue
|
||||
|
||||
timezones.add(s.timezone)
|
||||
tz = ZoneInfo(s.timezone)
|
||||
datetime_from = se.date_from.astimezone(tz)
|
||||
|
||||
@@ -123,7 +123,7 @@ def widget_css_etag(request, version, **kwargs):
|
||||
|
||||
|
||||
def _use_vite(request):
|
||||
if getattr(settings, 'PRETIX_WIDGET_VITE', False):
|
||||
if getattr(settings, 'PRETIX_WIDGET_VITE', False) or "beta" in request.GET:
|
||||
return True
|
||||
origin = request.META.get('HTTP_ORIGIN', '')
|
||||
gs = GlobalSettingsObject()
|
||||
|
||||
@@ -440,6 +440,7 @@ CSRF_COOKIE_NAME = 'pretix_csrftoken'
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
|
||||
INSTALLED_APPS += [ # noqa
|
||||
'django_querytagger',
|
||||
'django_filters',
|
||||
'django_markup',
|
||||
'django_otp',
|
||||
@@ -505,6 +506,7 @@ MIDDLEWARE = [
|
||||
'pretix.helpers.logs.RequestIdMiddleware',
|
||||
'pretix.api.middleware.IdempotencyMiddleware',
|
||||
'pretix.multidomain.middlewares.MultiDomainMiddleware',
|
||||
'django_querytagger.middleware.SetTagMiddleware', # after MultiDomainMiddleware for correct url resolving
|
||||
'pretix.base.middleware.CustomCommonMiddleware',
|
||||
'pretix.multidomain.middlewares.SessionMiddleware',
|
||||
'pretix.multidomain.middlewares.CsrfViewMiddleware',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const COOKIE_NAME = "__Host-pretix_csrftoken";
|
||||
const RELOAD_FLAG = "csrfReloadPerformed";
|
||||
|
||||
const hasCookie = document.cookie
|
||||
.split("; ")
|
||||
.some((c) => c.startsWith(COOKIE_NAME + "="));
|
||||
|
||||
if (!hasCookie && !sessionStorage.getItem(RELOAD_FLAG)) {
|
||||
sessionStorage.setItem(RELOAD_FLAG, "1");
|
||||
location.reload();
|
||||
} else if (hasCookie && sessionStorage.getItem(RELOAD_FLAG)) {
|
||||
sessionStorage.removeItem(RELOAD_FLAG);
|
||||
}
|
||||
});
|
||||
@@ -126,3 +126,81 @@ class LocaleDeterminationTest(TestCase):
|
||||
response = c.get('/dummy/dummy/')
|
||||
language = response['Content-Language']
|
||||
self.assertEqual(language, 'en')
|
||||
|
||||
|
||||
def test_render_csp():
|
||||
from pretix.base.middleware import _render_csp
|
||||
|
||||
assert _render_csp({}) == ""
|
||||
assert _render_csp({'default-src': ["'self'"]}) == "default-src 'self'"
|
||||
|
||||
h = {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': ["'self'"],
|
||||
'object-src': ["'none'"],
|
||||
'frame-src': ["'self'"],
|
||||
'style-src': ["'self'", "'self'"],
|
||||
'connect-src': ["'self'", "'self'"],
|
||||
'img-src': ["'self'", "'self'", "data:"],
|
||||
'font-src': ["'self'"],
|
||||
'media-src': ["'self'", "data:"],
|
||||
'form-action': ["'self'", "https:"],
|
||||
}
|
||||
assert _render_csp(h) == (
|
||||
"default-src 'self'; script-src 'self'; object-src 'none'; frame-src 'self'; style-src 'self' 'self'; "
|
||||
"connect-src 'self' 'self'; img-src 'self' 'self' data:; font-src 'self'; media-src 'self' data:; form-action 'self' https:"
|
||||
)
|
||||
|
||||
|
||||
def test_merge_csp():
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
|
||||
h = {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': ["'self'"],
|
||||
'style-src': ["'self'", "'self'"],
|
||||
'form-action': ["'self'", "https:"],
|
||||
'connect-src': ["'self'", "'self'"],
|
||||
}
|
||||
assert _render_csp(h) == (
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'self'; form-action 'self' https:; connect-src 'self' 'self'"
|
||||
)
|
||||
|
||||
_merge_csp(h, _parse_csp("style-src 'unsafe-inline'; connect-src https://example.com"))
|
||||
assert _render_csp(h) == (
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'self' 'unsafe-inline'; form-action 'self' "
|
||||
"https:; connect-src 'self' 'self' https://example.com"
|
||||
)
|
||||
|
||||
|
||||
def test_roundtrip_csp():
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
|
||||
prod_csp = ("default-src 'self' https://pretix.eu https://static.pretix.cloud; script-src 'self' "
|
||||
"'sha256-+tmFggeXIPOAC2UgcQ3LW/gPHTkwyWg3/D6FOJ5BHGo=' 'unsafe-eval' https://matomo.rami.io "
|
||||
"https://pretix.eu https://static.pretix.cloud https://support.rami.io; object-src 'none'; "
|
||||
"frame-src 'self' https://matomo.rami.io https://pretix.eu https://static.pretix.cloud "
|
||||
"https://support.rami.io https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline' "
|
||||
"data: https://cdn.pretix.cloud https://pretix.eu https://static.pretix…rt.rami.io; connect-src "
|
||||
"'self' https://cdn.pretix.cloud https://matomo.rami.io https://pretix.eu https://static.pretix.cloud "
|
||||
"https://support.rami.io ws://support.rami.io; img-src 'self' data: https://cdn.pretix.cloud "
|
||||
"https://matomo.rami.io https://pretix.eu https://static.pretix.cloud https://support.rami.io; "
|
||||
"font-src 'self' https://pretix.eu https://static.pretix.cloud; media-src 'self' data: "
|
||||
"https://cdn.pretix.cloud https://pretix.eu https://static.pretix.cloud; form-action 'self' "
|
||||
"https: https://pretix.eu")
|
||||
h = _parse_csp(prod_csp)
|
||||
_merge_csp(h, _parse_csp(prod_csp))
|
||||
assert _render_csp(h) == prod_csp
|
||||
|
||||
|
||||
def test_sanitize_csp():
|
||||
from pretix.base.middleware import _render_csp
|
||||
|
||||
h = {
|
||||
'style-src': ["'self'", "https://example.com", "https://example.org https://attack.example.net", "https://\fexample.org",
|
||||
"https://\texample.org", "https://example.org;script-src https://example.org", ],
|
||||
'script-src': ["'self'"],
|
||||
}
|
||||
assert _render_csp(h) == (
|
||||
"style-src 'self' https://example.com; script-src 'self'"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user