mirror of
https://github.com/pretix/pretix.git
synced 2026-07-31 09:15:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3442930a99 |
+3
-3
@@ -40,11 +40,11 @@ dependencies = [
|
||||
"Django[argon2]==5.2.*",
|
||||
"django-bootstrap3==26.1",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==9.0.*",
|
||||
"django-countries==8.2.*",
|
||||
"django-filter==26.1",
|
||||
"django-formset-js-improved==0.5.0.5",
|
||||
"django-formtools==2.7",
|
||||
"django-hierarkey==2.0.*,>=2.0.2",
|
||||
"django-hierarkey==2.0.*,>=2.0.1",
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.11.*",
|
||||
"django-libsass==0.9",
|
||||
@@ -94,7 +94,7 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==5.0.*",
|
||||
"requests==2.34.*",
|
||||
"sentry-sdk==2.66.*",
|
||||
"sentry-sdk==2.65.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
@@ -108,7 +108,6 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
('GET', 'api-v1:reusablemedium-list'),
|
||||
('POST', 'api-v1:reusablemedium-lookup'),
|
||||
@@ -147,7 +146,6 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
@@ -184,7 +182,6 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
||||
('GET', 'api-v1:event.settings'),
|
||||
('POST', 'api-v1:upload'),
|
||||
('POST', 'api-v1:checkinrpc.redeem'),
|
||||
('POST', 'api-v1:checkinrpc.annull'),
|
||||
('GET', 'api-v1:checkinrpc.search'),
|
||||
)
|
||||
|
||||
|
||||
@@ -1658,7 +1658,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
count_waitinglist=False,
|
||||
force=request.data.get('force', False),
|
||||
send_mail=send_mail,
|
||||
ignore_date=request.data.get('force', False),
|
||||
)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
@@ -1694,8 +1693,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
auth=self.request.auth,
|
||||
count_waitinglist=False,
|
||||
send_mail=send_mail,
|
||||
force=force,
|
||||
ignore_date=force)
|
||||
force=force)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except PaymentException as e:
|
||||
|
||||
@@ -23,7 +23,6 @@ import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext
|
||||
|
||||
@@ -36,23 +35,21 @@ def get_powered_by(request, safelink=True):
|
||||
d = gs.settings.license_check_input
|
||||
if d.get('poweredby_name'):
|
||||
if d.get('poweredby_url'):
|
||||
msg = format_html(
|
||||
gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>'),
|
||||
msg = gettext('<a {a_name_attr}>powered by {name}</a> <a {a_attr}>based on pretix</a>').format(
|
||||
name=d['poweredby_name'],
|
||||
a_name_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
||||
escape(sl(d['poweredby_url'])) if safelink else escape(d['poweredby_url']),
|
||||
)),
|
||||
a_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
||||
a_name_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl(d['poweredby_url']) if safelink else d['poweredby_url'],
|
||||
),
|
||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||
))
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = format_html(
|
||||
gettext('<a {a_attr}>powered by {name} based on pretix</a>'),
|
||||
msg = gettext('<a {a_attr}>powered by {name} based on pretix</a>').format(
|
||||
name=d['poweredby_name'],
|
||||
a_attr=mark_safe('href="{}" target="_blank" rel="noopener"'.format(
|
||||
a_attr='href="{}" target="_blank" rel="noopener"'.format(
|
||||
sl('https://pretix.eu') if safelink else 'https://pretix.eu',
|
||||
))
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = gettext('<a %(a_attr)s>ticketing powered by pretix</a>') % {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# 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
|
||||
@@ -42,7 +43,6 @@ from pretix.base.templatetags.rich_text import (
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
from pretix.helpers.ssrf import should_block_access
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -57,6 +57,8 @@ logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
T = TypeVar("T", bound=EmailBackend)
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||
try:
|
||||
@@ -252,9 +254,16 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||
is_private, msg = should_block_access(sa)
|
||||
if is_private:
|
||||
raise socket.error(msg)
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
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")
|
||||
if check_ip4 in _cgnat_net:
|
||||
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
|
||||
@@ -33,6 +33,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.
|
||||
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
@@ -40,12 +42,13 @@ from django.conf import settings
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.metrics import pretix_failed_logins
|
||||
from pretix.base.models import User
|
||||
from pretix.helpers.dicts import move_to_end
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -82,20 +85,40 @@ class LoginForm(forms.Form):
|
||||
else:
|
||||
move_to_end(self.fields, 'keep_logged_in')
|
||||
|
||||
@cached_property
|
||||
def ratelimit_key(self):
|
||||
if not settings.HAS_REDIS:
|
||||
return None
|
||||
client_ip = get_client_ip(self.request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return 'pretix_login_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
rate_limit_kwargs = dict(include_ip_from_request=self.request, max_num=10, expire_time=300)
|
||||
if rate_limit("login", **rate_limit_kwargs, increase=False):
|
||||
# Check rate limit without counting up, we increase below only on failed logins
|
||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||
logger.info("Backend login rejected due to rate limit.")
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.get(self.ratelimit_key)
|
||||
if cnt and int(cnt) > 10:
|
||||
pretix_failed_logins.inc(1, reason="ratelimit")
|
||||
logger.info("Backend login rejected due to rate limit.")
|
||||
raise forms.ValidationError(self.error_messages['rate_limit'], code='rate_limit')
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
if self.ratelimit_key:
|
||||
rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 300)
|
||||
logger.info("Backend login invalid.")
|
||||
pretix_failed_logins.inc(1, reason="invalid")
|
||||
# Count towards rate limit (result is ignored, we are checking above)
|
||||
rate_limit("login", **rate_limit_kwargs)
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
|
||||
@@ -959,7 +959,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=_initial,
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
if not help_text:
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
@@ -45,7 +46,6 @@ from pytz import common_timezones
|
||||
from pretix.base.models import User
|
||||
from pretix.control.forms import SingleLanguageWidget
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
|
||||
|
||||
class UserSettingsForm(forms.ModelForm):
|
||||
@@ -128,11 +128,16 @@ class UserPasswordChangeForm(forms.Form):
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
if rate_limit("pwchange", self.user.pk, max_num=10, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
if settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_%s' % self.user.pk)
|
||||
rc.expire('pretix_pwchange_%s' % self.user.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if not check_password(old_pw, self.user.password):
|
||||
raise forms.ValidationError(
|
||||
@@ -170,35 +175,19 @@ class UserEmailChangeForm(forms.Form):
|
||||
error_messages = {
|
||||
'duplicate_identifier': _("There already is an account associated with this email address. "
|
||||
"Please choose a different one."),
|
||||
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
|
||||
}
|
||||
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
|
||||
new_email = forms.EmailField(label=_('New email address'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user')
|
||||
self.request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_new_email(self):
|
||||
email = self.cleaned_data['new_email']
|
||||
|
||||
if rate_limit("emailchange_attempt", include_ip_from_request=self.request, max_num=5, expire_time=300):
|
||||
# Rate limit lookup for conflicting email addresses to make enumeration harder
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['duplicate_identifier'],
|
||||
code='duplicate_identifier',
|
||||
)
|
||||
|
||||
if rate_limit("emailchange", self.user.pk, max_num=2, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
return email
|
||||
|
||||
@@ -43,10 +43,6 @@ from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.helpers.format import PlainHtmlAlternativeString
|
||||
from pretix.helpers.i18n import (
|
||||
get_format_without_seconds, get_javascript_format,
|
||||
get_javascript_format_without_seconds,
|
||||
)
|
||||
|
||||
|
||||
def replace_arabic_numbers(inp):
|
||||
@@ -112,7 +108,7 @@ class DatePickerWidget(forms.DateInput):
|
||||
|
||||
|
||||
class TimePickerWidget(forms.TimeInput):
|
||||
def __init__(self, attrs=None, time_format=None, without_seconds=False):
|
||||
def __init__(self, attrs=None, time_format=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
@@ -121,27 +117,8 @@ class TimePickerWidget(forms.TimeInput):
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
time_attrs['autocomplete'] = 'off'
|
||||
|
||||
if time_format or without_seconds:
|
||||
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
|
||||
def time_format_attr():
|
||||
if without_seconds:
|
||||
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
||||
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
||||
|
||||
time_attrs['data-format'] = lazy(time_format_attr, str)
|
||||
|
||||
def time_format_attr():
|
||||
if without_seconds:
|
||||
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
||||
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
||||
|
||||
time_attrs['data-format'] = lazy(time_format_attr, str)
|
||||
|
||||
def placeholder():
|
||||
if without_seconds:
|
||||
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
|
||||
else:
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
@@ -205,7 +182,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
||||
|
||||
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None, without_seconds=False):
|
||||
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
@@ -228,36 +205,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
|
||||
if date_format or time_format or without_seconds:
|
||||
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
|
||||
def date_format_attr():
|
||||
if without_seconds:
|
||||
return get_javascript_format_without_seconds(date_format or "DATE_INPUT_FORMATS")
|
||||
return get_javascript_format(date_format or "DATE_INPUT_FORMATS")
|
||||
|
||||
date_attrs['data-format'] = lazy(date_format_attr, str)
|
||||
|
||||
def time_format_attr():
|
||||
if without_seconds:
|
||||
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
|
||||
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
|
||||
|
||||
time_attrs['data-format'] = lazy(time_format_attr, str)
|
||||
|
||||
def date_placeholder():
|
||||
if without_seconds:
|
||||
df = date_format or get_format_without_seconds('DATE_INPUT_FORMATS')
|
||||
else:
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
|
||||
).strftime(df)
|
||||
|
||||
def time_placeholder():
|
||||
if without_seconds:
|
||||
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
|
||||
else:
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
|
||||
return now().replace(
|
||||
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
).strftime(tf)
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
# 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 base64
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
@@ -71,44 +69,11 @@ def get_supported_language(requested_language, allowed_languages, default_langua
|
||||
return language
|
||||
|
||||
|
||||
class BaseLocaleMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
This is a reduced LocaleMiddleware that uses only information contained in the WSGI request data
|
||||
to figure out the language (cookie and browser settings). We need it to have a consistent language
|
||||
for error pages that are generated from the middleware stack before we know e.g. which user is logged
|
||||
in or which event is selected.
|
||||
"""
|
||||
|
||||
def process_request(self, request: HttpRequest):
|
||||
language = get_language_from_early_request(request)
|
||||
translation.activate(language)
|
||||
set_region(None)
|
||||
request.LANGUAGE_CODE = language
|
||||
timezone.deactivate()
|
||||
|
||||
def process_response(self, request: HttpRequest, response: HttpResponse):
|
||||
language = translation.get_language()
|
||||
patch_vary_headers(response, ('Accept-Language',))
|
||||
if 'Content-Language' not in response:
|
||||
response['Content-Language'] = language
|
||||
return response
|
||||
|
||||
|
||||
class LocaleMiddleware(MiddlewareMixin):
|
||||
|
||||
"""
|
||||
This is the full LocaleMiddleware that uses all available information to figure out the correct
|
||||
language for the request using all available sources, in this order of priority:
|
||||
|
||||
- Backend: User settings
|
||||
- Language cookie
|
||||
- Frontend: Customer account settings
|
||||
- Browser settings
|
||||
- Frontend: Event/Organizer settings
|
||||
- System default
|
||||
|
||||
It needs to run late in the middleware stack to have all information available for these steps.
|
||||
For some cases, it is even ran a second time since the event is sometimes only figured out after the
|
||||
middleware stack (can happen for plugin views).
|
||||
This middleware sets the correct locale and timezone
|
||||
for a request.
|
||||
"""
|
||||
|
||||
def process_request(self, request: HttpRequest):
|
||||
@@ -223,24 +188,6 @@ def get_default_language():
|
||||
return settings.LANGUAGE_CODE
|
||||
|
||||
|
||||
def get_language_from_early_request(request: HttpRequest) -> str:
|
||||
"""
|
||||
Analyzes the request to find what language the user wants the system to
|
||||
show using only WSGI-available information. Only languages listed in
|
||||
settings.LANGUAGES are taken into account. If the user requests a sublanguage
|
||||
where we have a main language, we send out the main language.
|
||||
"""
|
||||
global _supported
|
||||
if _supported is None:
|
||||
_supported = OrderedDict(settings.LANGUAGES)
|
||||
|
||||
return (
|
||||
get_language_from_cookie(request)
|
||||
or get_language_from_browser(request)
|
||||
or get_default_language()
|
||||
)
|
||||
|
||||
|
||||
def get_language_from_request(request: HttpRequest) -> str:
|
||||
"""
|
||||
Analyzes the request to find what language the user wants the system to
|
||||
@@ -255,6 +202,7 @@ def get_language_from_request(request: HttpRequest) -> str:
|
||||
if request.path.startswith(get_script_prefix() + 'control'):
|
||||
return (
|
||||
get_language_from_user_settings(request)
|
||||
or get_language_from_customer_settings(request)
|
||||
or get_language_from_cookie(request)
|
||||
or get_language_from_browser(request)
|
||||
or get_language_from_event(request)
|
||||
@@ -314,28 +262,7 @@ def _merge_csp(a, b):
|
||||
for k, v in a.items():
|
||||
if "'unsafe-inline'" in v:
|
||||
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
|
||||
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha256-")]
|
||||
|
||||
|
||||
def add_to_response_csp(response, csp_to_merge):
|
||||
if "Content-Security-Policy" in response:
|
||||
csp = _parse_csp(response["Content-Security-Policy"])
|
||||
else:
|
||||
csp = {}
|
||||
|
||||
_merge_csp(csp, csp_to_merge)
|
||||
|
||||
if csp:
|
||||
response["Content-Security-Policy"] = _render_csp(csp)
|
||||
|
||||
|
||||
def add_to_response_csp_via_request(request, csp_to_merge):
|
||||
_merge_csp(request._csp_to_merge, csp_to_merge)
|
||||
|
||||
|
||||
def calculate_csp_hash(data):
|
||||
hash_str = base64.b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode("ascii")
|
||||
return f"'sha256-{hash_str}'"
|
||||
a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
|
||||
|
||||
|
||||
class SecurityMiddleware(MiddlewareMixin):
|
||||
@@ -355,9 +282,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# but at the moment it does not seem to be an issue to just send it.
|
||||
)
|
||||
|
||||
def process_request(self, request):
|
||||
request._csp_to_merge = {}
|
||||
|
||||
def process_response(self, request, resp):
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
@@ -369,22 +293,18 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
if self._needs_csp(request, resp):
|
||||
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
|
||||
if 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
return resp
|
||||
|
||||
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']
|
||||
|
||||
return resp
|
||||
|
||||
def _needs_csp(self, request, resp):
|
||||
if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
|
||||
return False
|
||||
|
||||
if getattr(resp, '_csp_ignore', False):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _build_csp(self, request, resp):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
@@ -417,6 +337,13 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
h['style-src'] += ["'unsafe-inline'"]
|
||||
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
|
||||
|
||||
if hasattr(request, 'csp_nonce'):
|
||||
nonce = f"'nonce-{request.csp_nonce}'"
|
||||
h['script-src'].append(nonce)
|
||||
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
|
||||
@@ -429,9 +356,6 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
if settings.LOG_CSP:
|
||||
h['report-uri'] = ["/csp_report/"]
|
||||
|
||||
if request._csp_to_merge:
|
||||
_merge_csp(h, request._csp_to_merge)
|
||||
|
||||
if 'Content-Security-Policy' in resp:
|
||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||
|
||||
|
||||
@@ -179,12 +179,6 @@ class EventMixin:
|
||||
self.date_to.astimezone(tz), ("D" if short else "l")
|
||||
)
|
||||
|
||||
def is_same_day(self):
|
||||
if not self.date_to:
|
||||
return True
|
||||
else:
|
||||
return self.date_from.astimezone(self.timezone).date() == self.date_to.astimezone(self.timezone).date()
|
||||
|
||||
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False, try_to_show_times=False) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the end date
|
||||
@@ -238,9 +232,6 @@ class EventMixin:
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
# If we get rid of the shim, verify that
|
||||
# https://github.com/py-vobject/vobject/issues/117#issuecomment-5045645314
|
||||
# has been released and included
|
||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||
|
||||
@property
|
||||
|
||||
@@ -2286,12 +2286,9 @@ class OrderRefund(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def ActivePositionManager(**scope):
|
||||
class InnerClass(ScopedManager(**scope).__class__):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(canceled=False)
|
||||
|
||||
return InnerClass()
|
||||
class ActivePositionManager(ScopedManager(organizer='order__event__organizer').__class__):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(canceled=False)
|
||||
|
||||
|
||||
class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
@@ -2381,7 +2378,7 @@ class OrderFee(RoundingCorrectionMixin, models.Model):
|
||||
canceled = models.BooleanField(default=False)
|
||||
|
||||
all = ScopedManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager(organizer='order__event__organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
@@ -2600,7 +2597,7 @@ class OrderPosition(AbstractPosition):
|
||||
)
|
||||
|
||||
all = ScopedManager(organizer='organizer')
|
||||
objects = ActivePositionManager(organizer='organizer')
|
||||
objects = ActivePositionManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -24,7 +24,6 @@ from typing import List, Optional
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -97,8 +96,6 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
||||
:param valid_from_not_chosen: Set to ``True`` to indicate that the customer is in an early step of the checkout flow
|
||||
where the valid_from date is not selected yet. In this case, the valid_from date is not checked.
|
||||
"""
|
||||
if lock and not transaction.get_connection().in_atomic_block:
|
||||
raise Exception('validate_memberships_in_order(lock=True) should only be called in atomic transaction!')
|
||||
tz = event.timezone
|
||||
applicable_positions = [
|
||||
p for p in positions
|
||||
|
||||
@@ -791,12 +791,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
[op.seat for op in sorted_positions if op.seat],
|
||||
shared_lock_objects=[event]
|
||||
)
|
||||
elif any(cp.voucher and cp.voucher.budget for cp in sorted_positions):
|
||||
# Voucher budgets are not guaranteed by the cart manager
|
||||
lock_objects(
|
||||
[op.voucher for op in sorted_positions if op.voucher and op.voucher.budget],
|
||||
shared_lock_objects=[event]
|
||||
)
|
||||
|
||||
q_avail = Counter()
|
||||
v_avail = Counter()
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
# 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 json
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
import secrets
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import urlopen
|
||||
|
||||
@@ -31,10 +33,6 @@ from django import template
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from pretix.base.middleware import (
|
||||
add_to_response_csp_via_request, calculate_csp_hash,
|
||||
)
|
||||
|
||||
register = template.Library()
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
_MANIFEST = {}
|
||||
@@ -236,16 +234,10 @@ def vite_importmap(context):
|
||||
if not imports:
|
||||
return ""
|
||||
|
||||
json_string = json.dumps({"imports": imports})
|
||||
|
||||
# Calculate hash and add it to the CSP info in the request, so that will be merged into the
|
||||
# response header later on by the middleware
|
||||
# Generate a nonce and store it on the request so the CSP middleware can allow it
|
||||
nonce = secrets.token_urlsafe(16)
|
||||
request = context.get('request')
|
||||
if request:
|
||||
csp_hash = calculate_csp_hash(json_string)
|
||||
add_to_response_csp_via_request(request, {
|
||||
'style-src': [csp_hash],
|
||||
'script-src': [csp_hash],
|
||||
})
|
||||
request.csp_nonce = nonce
|
||||
|
||||
return f'<script type="importmap">{json_string}</script>'
|
||||
return f'<script type="importmap" nonce="{nonce}">{json.dumps({"imports": imports})}</script>'
|
||||
|
||||
@@ -197,10 +197,10 @@ class EventWizardBasicsForm(I18nModelForm):
|
||||
'presale_end': SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}, without_seconds=True),
|
||||
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}, without_seconds=True),
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}),
|
||||
'slug': SlugWidget,
|
||||
}
|
||||
|
||||
@@ -521,11 +521,11 @@ class EventUpdateForm(I18nModelForm):
|
||||
'limit_sales_channels': SafeModelMultipleChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}, without_seconds=True),
|
||||
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ from urllib.parse import urlencode
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, Model, OrderBy, OuterRef, Q, QuerySet,
|
||||
)
|
||||
@@ -60,7 +59,7 @@ from pretix.base.models import (
|
||||
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
|
||||
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
|
||||
User, Voucher,
|
||||
Voucher,
|
||||
)
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.base.timeframes import (
|
||||
@@ -771,7 +770,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
|
||||
)
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
self.fields[fname] = forms.TimeField(
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
help_text=_('Exact matches only'),
|
||||
**kwargs,
|
||||
)
|
||||
@@ -3008,91 +3007,3 @@ class OutgoingMailFilterForm(FilterForm):
|
||||
qs = qs.order_by("-created", "-pk")
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class LogFilterForm(FilterForm):
|
||||
source = forms.ChoiceField(
|
||||
label=_('Source'),
|
||||
choices=[
|
||||
('', _('All sources')),
|
||||
('team', _('Team actions')),
|
||||
('customer', _('Customer actions')),
|
||||
('device', _('Device actions')),
|
||||
],
|
||||
required=False
|
||||
)
|
||||
device = SafeModelChoiceField(
|
||||
label=_('Device'),
|
||||
empty_label=_('All devices'),
|
||||
queryset=Device.objects.none(),
|
||||
required=False
|
||||
)
|
||||
user_email = forms.EmailField(
|
||||
label=_('User email'),
|
||||
widget=forms.EmailInput(
|
||||
attrs={"placeholder": _('All users')}
|
||||
),
|
||||
required=False
|
||||
)
|
||||
action_type = forms.CharField(
|
||||
widget=forms.HiddenInput,
|
||||
required=False,
|
||||
)
|
||||
content_type = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
widget=forms.HiddenInput,
|
||||
required=False,
|
||||
)
|
||||
object = forms.IntegerField(
|
||||
widget=forms.HiddenInput,
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['device'].queryset = self.organizer.devices.all().order_by('device_id')
|
||||
self.fields['device'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.devices.select2', kwargs={
|
||||
'organizer': self.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': _('All devices'),
|
||||
}
|
||||
)
|
||||
self.fields['device'].widget.choices = self.fields['device'].choices
|
||||
self.fields['device'].label = _('Device')
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
if fdata.get('source') == 'team':
|
||||
qs = qs.filter(user__isnull=False)
|
||||
elif fdata.get('source') == 'device':
|
||||
qs = qs.filter(device__isnull=False)
|
||||
elif fdata.get('source') == 'customer':
|
||||
qs = qs.filter(user__isnull=True, device__isnull=True)
|
||||
|
||||
if fdata.get('device'):
|
||||
qs = qs.filter(device_id=fdata['device'].pk)
|
||||
|
||||
if fdata.get('action_type'):
|
||||
qs = qs.filter(action_type__in=fdata['action_type'].split(','))
|
||||
|
||||
if fdata.get('user_email'):
|
||||
try:
|
||||
user = User.objects.get(email=fdata['user_email'].lower())
|
||||
qs = qs.filter(user=user)
|
||||
except User.DoesNotExist:
|
||||
# Do not actively leak info that this user does not exist. Yes, this will likely have a
|
||||
# timing attack possibility, but with the magic that database query planners do,
|
||||
# it's probably impossible to avoid that.
|
||||
qs = qs.none()
|
||||
|
||||
if fdata.get('content_type'):
|
||||
qs = qs.filter(content_type=fdata.get('content_type'))
|
||||
|
||||
if fdata.get('object'):
|
||||
qs = qs.filter(object_id=fdata.get('object'))
|
||||
return qs
|
||||
|
||||
@@ -246,8 +246,8 @@ class QuestionForm(I18nModelForm):
|
||||
'valid_string_length_max',
|
||||
]
|
||||
widgets = {
|
||||
'valid_datetime_min': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'valid_datetime_max': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'valid_datetime_min': SplitDateTimePickerWidget(),
|
||||
'valid_datetime_max': SplitDateTimePickerWidget(),
|
||||
'valid_date_min': DatePickerWidget(),
|
||||
'valid_date_max': DatePickerWidget(),
|
||||
'items': forms.CheckboxSelectMultiple(
|
||||
@@ -1427,6 +1427,6 @@ class ItemProgramTimeForm(I18nModelForm):
|
||||
'end': forms.SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'start': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'end': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'start': SplitDateTimePickerWidget(),
|
||||
'end': SplitDateTimePickerWidget(),
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.helpers.i18n import get_javascript_format_without_seconds
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
|
||||
|
||||
@@ -81,11 +80,11 @@ class SubEventForm(I18nModelForm):
|
||||
'presale_end': SplitDateTimeField,
|
||||
}
|
||||
widgets = {
|
||||
'date_from': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
|
||||
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
|
||||
'date_from': SplitDateTimePickerWidget(),
|
||||
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
|
||||
'presale_start': SplitDateTimePickerWidget(),
|
||||
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +162,7 @@ class SubEventBulkEditForm(I18nModelForm):
|
||||
self.fields[k + '_time'] = forms.TimeField(
|
||||
label=self._meta.model._meta.get_field(k).verbose_name,
|
||||
help_text=self._meta.model._meta.get_field(k).help_text,
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
widget=TimePickerWidget(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
@@ -507,12 +506,6 @@ class TimeForm(forms.Form):
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['time_from'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
|
||||
self.fields['time_to'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
|
||||
self.fields['time_admission'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
|
||||
|
||||
|
||||
TimeFormSet = formset_factory(
|
||||
TimeForm,
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
{% block title %}{% trans "Event logs" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Event logs" %}</h1>
|
||||
{% include "pretixcontrol/fragment_log_filter_form.html" %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
||||
<p>
|
||||
<select name="user" class="form-control">
|
||||
<option value="">{% trans "All actions" %}</option>
|
||||
<option value="yes" {% if request.GET.user == "yes" %}selected="selected"{% endif %}>
|
||||
{% trans "Team actions" %}
|
||||
</option>
|
||||
<option value="no" {% if request.GET.user == "no" %}selected="selected"{% endif %}>
|
||||
{% trans "Customer actions" %}
|
||||
</option>
|
||||
{% for up in userlist %}
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}"
|
||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for d in devicelist %}
|
||||
{% if d.device__id %}
|
||||
<option value="d-{{ d.device__id }}"
|
||||
{% if "d-" in request.GET.user and request.GET.user|slice:"2:" == d.device__id|slugify %}selected="selected"{% endif %}>
|
||||
{{ d.device__name }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
<li class="list-group-item logentry">
|
||||
@@ -66,5 +95,5 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "pretixcontrol/pagination_huge.html" %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Filter" %}
|
||||
</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.source %}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.device %}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.user_email %}
|
||||
</div>
|
||||
</div>
|
||||
{{ filter_form.action_type }}
|
||||
{{ filter_form.content_type }}
|
||||
{{ filter_form.object }}
|
||||
{% if filter_form.is_valid and filter_form.cleaned_data.action_type %}
|
||||
{% icon "filter" %} <code>{{ filter_form.cleaned_data.action_type }}</code>
|
||||
{% endif %}
|
||||
{% if filter_form.is_valid and filter_form.cleaned_data.content_type and filter_form.cleaned_data.object %}
|
||||
{% icon "filter" %} {% trans "Specific object selected" %}
|
||||
{% endif %}
|
||||
<div class="text-right flip">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -137,15 +137,6 @@
|
||||
</td>
|
||||
<td>
|
||||
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
||||
{% if staff_session %}
|
||||
<details class="admin-only">
|
||||
<summary>
|
||||
<i class="fa fa-angle-down collapse-indicator"></i>
|
||||
{% trans "Details" %}
|
||||
</summary>
|
||||
<pre class="admin-only">{{ d.info|pprint }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.initialized %}
|
||||
|
||||
@@ -4,7 +4,24 @@
|
||||
{% block title %}{% trans "Organizer logs" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Organizer logs" %}</h1>
|
||||
{% include "pretixcontrol/fragment_log_filter_form.html" %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
<input type="hidden" name="content_type" value="{{ request.GET.content_type }}">
|
||||
<input type="hidden" name="object" value="{{ request.GET.object }}">
|
||||
<p>
|
||||
<select name="user" class="form-control">
|
||||
<option value="">{% trans "All actions" %}</option>
|
||||
{% for up in userlist %}
|
||||
{% if up.user__id %}
|
||||
<option value="{{ up.user__id }}"
|
||||
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
|
||||
{{ up.user__email }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Filter" %}</button>
|
||||
</p>
|
||||
</form>
|
||||
<ul class="list-group">
|
||||
{% for log in logs %}
|
||||
<li class="list-group-item logentry">
|
||||
@@ -64,5 +81,5 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "pretixcontrol/pagination_huge.html" %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,7 +65,6 @@ from pretix.base.forms.auth import (
|
||||
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
|
||||
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
|
||||
from pretix.helpers.http import get_client_ip, redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
from pretix.helpers.security import handle_login_source, session_login
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -320,12 +319,19 @@ class Forgot(TemplateView):
|
||||
if self.form.is_valid():
|
||||
email = self.form.cleaned_data['email']
|
||||
|
||||
has_redis = settings.HAS_REDIS
|
||||
|
||||
try:
|
||||
user = User.objects.get(is_active=True, auth_backend='native', email__iexact=email)
|
||||
|
||||
if rate_limit("pwreset", user.pk, max_num=1, expire_time=3600 * 24):
|
||||
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
|
||||
raise RepeatedResetDenied()
|
||||
if has_redis:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
if rc.exists('pretix_pwreset_%s' % (user.id)):
|
||||
user.log_action('pretix.control.auth.user.forgot_password.denied.repeated')
|
||||
raise RepeatedResetDenied()
|
||||
else:
|
||||
rc.setex('pretix_pwreset_%s' % (user.id), 3600 * 24, '1')
|
||||
|
||||
except User.DoesNotExist:
|
||||
logger.warning('Backend password reset for unregistered e-mail \"' + email + '\" requested.')
|
||||
@@ -338,7 +344,6 @@ class Forgot(TemplateView):
|
||||
user.log_action('pretix.control.auth.user.forgot_password.mail_sent')
|
||||
|
||||
finally:
|
||||
has_redis = settings.HAS_REDIS
|
||||
if has_redis:
|
||||
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions. '
|
||||
'Please note that we will send at most one email every 24 hours.'))
|
||||
@@ -407,7 +412,11 @@ class Recover(TemplateView):
|
||||
messages.success(request, _('You can now login using your new password.'))
|
||||
user.log_action('pretix.control.auth.user.forgot_password.recovered')
|
||||
|
||||
rate_limit_reset("pwreset", user.pk)
|
||||
has_redis = settings.HAS_REDIS
|
||||
if has_redis:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
rc.delete('pretix_pwreset_%s' % user.id)
|
||||
return redirect('control:auth.login')
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
@@ -253,7 +253,7 @@ def quota_widgets(sender, subevent=None, lazy=False, **kwargs):
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET,
|
||||
num='{}/{}'.format(intcomma(left), intcomma(q.size)) if q.size is not None else '\u221e',
|
||||
text=format_html(_('{quota} left'), quota=q.name)
|
||||
text=_('{quota} left').format(quota=escape(q.name))
|
||||
),
|
||||
'lazy': 'quota-{}'.format(q.pk),
|
||||
'display_size': 'small',
|
||||
@@ -307,7 +307,7 @@ def checkin_widget(sender, subevent=None, lazy=False, **kwargs):
|
||||
'content': None if lazy else format_html(
|
||||
NUM_WIDGET,
|
||||
num='{}/{}'.format(intcomma(cl.inside_count), intcomma(cl.position_count)),
|
||||
text=format_html(_('Present – {list}'), list=cl.name)
|
||||
text=_('Present – {list}').format(list=escape(cl.name))
|
||||
),
|
||||
'lazy': 'checkin-{}'.format(cl.pk),
|
||||
'display_size': 'small',
|
||||
|
||||
@@ -60,7 +60,7 @@ from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import conditional_escape, format_html
|
||||
@@ -119,9 +119,8 @@ from ...helpers.compat import CompatDeleteView
|
||||
from ...helpers.format import (
|
||||
PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from ..forms.filter import LogFilterForm
|
||||
from ..logdisplay import OVERVIEW_BANLIST
|
||||
from . import CreateView, LargeResultSetPaginator, PaginationMixin, UpdateView
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1254,7 +1253,6 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
|
||||
class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/event/logs.html'
|
||||
model = LogEntry
|
||||
paginator_class = LargeResultSetPaginator
|
||||
context_object_name = 'logs'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -1284,20 +1282,32 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
]
|
||||
qs = qs.filter(content_type__in=allowed_types)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
if self.request.GET.get('user') == 'yes':
|
||||
qs = qs.filter(user__isnull=False)
|
||||
elif self.request.GET.get('user') == 'no':
|
||||
qs = qs.filter(user__isnull=True)
|
||||
elif self.request.GET.get('user', '').startswith('d-'):
|
||||
qs = qs.filter(device_id=self.request.GET.get('user')[2:])
|
||||
elif self.request.GET.get('user'):
|
||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||
|
||||
if self.request.GET.get('action_type'):
|
||||
qs = qs.filter(action_type=self.request.GET['action_type'])
|
||||
|
||||
if self.request.GET.get('content_type'):
|
||||
qs = qs.filter(content_type=get_object_or_404(ContentType, pk=self.request.GET.get('content_type')))
|
||||
|
||||
if self.request.GET.get('object'):
|
||||
qs = qs.filter(object_id=self.request.GET.get('object'))
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['userlist'] = self.request.event.logentry_set.order_by().distinct().values('user__id', 'user__email')
|
||||
ctx['devicelist'] = self.request.event.logentry_set.order_by('device__name').distinct().values('device__id', 'device__name')
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
|
||||
|
||||
|
||||
class EventComment(EventPermissionRequiredMixin, View):
|
||||
permission = 'event.settings.general:write'
|
||||
|
||||
@@ -35,7 +35,7 @@ from django.utils.translation import ngettext
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from pretix.base.middleware import add_to_response_csp
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
from pretix.base.models import OutgoingMail
|
||||
from pretix.base.services.mail import mail_send_task
|
||||
from pretix.control.forms.filter import OutgoingMailFilterForm
|
||||
@@ -108,12 +108,18 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
add_to_response_csp(response, {
|
||||
if 'Content-Security-Policy' in response:
|
||||
h = _parse_csp(response['Content-Security-Policy'])
|
||||
else:
|
||||
h = {}
|
||||
csps = {
|
||||
'frame-src': ['data:'],
|
||||
# Unfortuantely, we can't avoid unsafe-inline for style here.
|
||||
# See outgoingmail.js for the protection measures we take.
|
||||
'style-src': ["'unsafe-inline'"],
|
||||
})
|
||||
}
|
||||
_merge_csp(h, csps)
|
||||
response['Content-Security-Policy'] = _render_csp(h)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -113,8 +113,7 @@ from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
||||
from pretix.control.forms.filter import (
|
||||
CustomerFilterForm, DeviceFilterForm, EventFilterForm, GiftCardFilterForm,
|
||||
LogFilterForm, OrganizerFilterForm, ReusableMediaFilterForm,
|
||||
TeamFilterForm,
|
||||
OrganizerFilterForm, ReusableMediaFilterForm, TeamFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import ExporterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
@@ -134,7 +133,7 @@ from pretix.control.permissions import (
|
||||
organizer_permission_required,
|
||||
)
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.control.views.mailsetup import MailSettingsSetupView
|
||||
from pretix.helpers import OF_SELF, GroupConcat
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
@@ -2664,7 +2663,6 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
template_name = 'pretixcontrol/organizers/logs.html'
|
||||
permission = 'organizer.settings.general:write'
|
||||
model = LogEntry
|
||||
paginator_class = LargeResultSetPaginator
|
||||
context_object_name = 'logs'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -2674,21 +2672,16 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||
).order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if self.request.GET.get('action_type'):
|
||||
qs = qs.filter(action_type=self.request.GET['action_type'])
|
||||
if self.request.GET.get('user'):
|
||||
qs = qs.filter(user_id=self.request.GET.get('user'))
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return LogFilterForm(data=self.request.GET, organizer=self.request.organizer)
|
||||
|
||||
|
||||
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = MembershipType
|
||||
|
||||
@@ -85,7 +85,6 @@ from pretix.control.views import PaginationMixin
|
||||
from pretix.control.views.event import MetaDataEditorMixin
|
||||
from pretix.helpers import GroupConcat
|
||||
from pretix.helpers.compat import CompatDeleteView
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
|
||||
@@ -879,7 +878,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
|
||||
ctx['rrule_formset'] = self.rrule_formset
|
||||
ctx['time_formset'] = self.time_formset
|
||||
|
||||
tf = get_format_without_seconds('TIME_INPUT_FORMATS')
|
||||
tf = get_format('TIME_INPUT_FORMATS')[0]
|
||||
ctx['time_admission_sample'] = time(8, 30, 0).strftime(tf)
|
||||
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
|
||||
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
|
||||
|
||||
@@ -80,7 +80,6 @@ from pretix.control.permissions import (
|
||||
)
|
||||
from pretix.control.views.auth import get_u2f_appid, get_webauthn_rp_id
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
|
||||
from pretix.helpers.security import session_reauth
|
||||
from pretix.helpers.u2f import websafe_encode
|
||||
|
||||
@@ -850,7 +849,6 @@ class UserPasswordChangeView(FormView):
|
||||
msgs = []
|
||||
msgs.append(_('Your password has been changed.'))
|
||||
self.request.user.send_security_notice(msgs)
|
||||
rate_limit_reset("pwreset", self.request.user.pk)
|
||||
|
||||
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
|
||||
|
||||
@@ -881,7 +879,6 @@ class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
|
||||
|
||||
return {
|
||||
**super().get_form_kwargs(),
|
||||
"request": self.request,
|
||||
"user": self.request.user,
|
||||
}
|
||||
|
||||
@@ -911,10 +908,6 @@ class UserEmailVerifyView(View):
|
||||
messages.success(self.request, _('Your email address was already verified.'))
|
||||
return redirect(reverse('control:user.settings', kwargs={}))
|
||||
|
||||
if rate_limit("emailverify", self.request.user.pk, max_num=2, expire_time=300):
|
||||
messages.error(self.request, _("For security reasons, please wait 5 minutes before you try again."))
|
||||
return redirect(reverse('control:user.settings', kwargs={}))
|
||||
|
||||
self.request.user.send_confirmation_code(
|
||||
session=self.request.session,
|
||||
reason='email_verify',
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# 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
|
||||
@@ -41,7 +42,8 @@ from urllib3.util.connection import (
|
||||
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
||||
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
from pretix.helpers.ssrf import should_block_access
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def monkeypatch_vobject_performance():
|
||||
@@ -148,9 +150,16 @@ def monkeypatch_urllib3_ssrf_protection():
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
is_private, msg = should_block_access(sa)
|
||||
if is_private:
|
||||
raise HTTPError(msg)
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
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")
|
||||
if check_ip4 in _cgnat_net:
|
||||
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
|
||||
from pretix.helpers.http import get_client_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_key(key, parameters):
|
||||
return f'pretix:ratelimit:{key}:' + hashlib.sha256(','.join(str(p) for p in parameters).encode()).hexdigest()
|
||||
|
||||
|
||||
def _get_ip(request):
|
||||
client_ip = get_client_ip(request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private and not settings.DEBUG:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return str(client_ip)
|
||||
|
||||
|
||||
def rate_limit(key: str, *parameters, include_ip_from_request: HttpRequest=None, max_num: int, expire_time: int, increase: bool = True):
|
||||
"""
|
||||
This is a shared utility to implement simple rate limiting in operations like
|
||||
password resets.
|
||||
|
||||
:param key: The key referring to the feature like "pwreset"
|
||||
:param parameters: Any number of things to be hashed as the bucket key
|
||||
:param include_ip_from_request: Add IP address from request to the bucket key. If IP address cannot be determined,
|
||||
rate limit is not applied.
|
||||
:param max_num: The maximum number of actions to performed within expire_time of the first action
|
||||
:param expire_time: The length of the time window in seconds
|
||||
:param increase: Whether to count the call as an event counted towards the rate, or just check
|
||||
:return:
|
||||
"""
|
||||
if not settings.HAS_REDIS:
|
||||
# No rate limiting
|
||||
return False
|
||||
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
|
||||
if include_ip_from_request:
|
||||
ip = _get_ip(include_ip_from_request)
|
||||
if not ip:
|
||||
# IP not discovered, can't rate limit
|
||||
return False
|
||||
parameters = (*parameters, ip)
|
||||
|
||||
redis_key = _get_key(key, parameters)
|
||||
|
||||
if increase:
|
||||
p = rc.pipeline()
|
||||
p.set(redis_key, 0, nx=True, ex=expire_time) # Start a rate limit window if none is running
|
||||
p.incr(redis_key)
|
||||
new_counter = p.execute()[1]
|
||||
else:
|
||||
new_counter = int(rc.get(redis_key) or 0)
|
||||
|
||||
if new_counter > max_num:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def rate_limit_reset(key: str, *parameters, include_ip_from_request: HttpRequest=None):
|
||||
"""
|
||||
Reset a rate limit bucket.
|
||||
"""
|
||||
if not settings.HAS_REDIS:
|
||||
# No rate limiting
|
||||
return
|
||||
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
|
||||
if include_ip_from_request:
|
||||
ip = _get_ip(include_ip_from_request)
|
||||
if not ip:
|
||||
# IP not discovered, can't rate limit
|
||||
return False
|
||||
parameters = (*parameters, ip)
|
||||
|
||||
redis_key = _get_key(key, parameters)
|
||||
rc.delete(redis_key)
|
||||
@@ -1,40 +0,0 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
import ipaddress
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def should_block_access(sa):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
if ip_addr.is_multicast:
|
||||
return True, f"Request to multicast address {sa[0]} blocked"
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
return True, f"Request to local address {sa[0]} blocked"
|
||||
if ip_addr.is_private:
|
||||
return True, f"Request to private address {sa[0]} blocked"
|
||||
if check_ip4 in _cgnat_net:
|
||||
return True, f"Request to RFC 6598 address {sa[0]} blocked"
|
||||
|
||||
return False, None
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-07 09:30+0000\n"
|
||||
"PO-Revision-Date: 2026-07-20 08:01+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2026-07-13 12:35+0000\n"
|
||||
"Last-Translator: ahmed badr <ahmedbadr@oma.com.eg>\n"
|
||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ar/>\n"
|
||||
"Language: ar\n"
|
||||
|
||||
@@ -73,9 +73,7 @@ from pretix.plugins.banktransfer.payment import BankTransfer
|
||||
from pretix.plugins.banktransfer.refund_export import (
|
||||
build_sepa_xml, get_refund_export_csv,
|
||||
)
|
||||
from pretix.plugins.banktransfer.tasks import (
|
||||
notify_incomplete_payment, process_banktransfers,
|
||||
)
|
||||
from pretix.plugins.banktransfer.tasks import process_banktransfers
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.banktransfer')
|
||||
|
||||
@@ -167,11 +165,6 @@ class ActionView(View):
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
|
||||
trans.order.refresh_from_db()
|
||||
if trans.order.pending_sum > Decimal('0.00') and trans.order.status == Order.STATUS_PENDING:
|
||||
notify_incomplete_payment(trans.order)
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'ok',
|
||||
})
|
||||
|
||||
@@ -44,7 +44,6 @@ from django.contrib import messages
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as __, gettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -113,10 +112,9 @@ class Paypal(BasePaymentProvider):
|
||||
label=_('Client ID'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
help_text=format_html(
|
||||
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
|
||||
help_text=_('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>').format(
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html',
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
|
||||
)
|
||||
)),
|
||||
('secret',
|
||||
|
||||
@@ -36,7 +36,6 @@ from django.template.loader import get_template
|
||||
from django.templatetags.static import static
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as __, gettext_lazy as _
|
||||
@@ -111,8 +110,7 @@ class PaypalSettingsHolder(BasePaymentProvider):
|
||||
label=_('Client ID'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
help_text=format_html(
|
||||
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
|
||||
help_text=_('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>').format(
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
|
||||
from pretix.base.middleware import add_to_response_csp
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
from pretix.base.settings import settings_hierarkey
|
||||
from pretix.base.signals import (
|
||||
register_global_settings, register_payment_providers,
|
||||
@@ -144,7 +144,12 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
|
||||
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
|
||||
(url.namespace == "plugins:paypal2" and url.url_name == "pay")
|
||||
):
|
||||
add_to_response_csp(response, {
|
||||
if 'Content-Security-Policy' in response:
|
||||
h = _parse_csp(response['Content-Security-Policy'])
|
||||
else:
|
||||
h = {}
|
||||
|
||||
csps = {
|
||||
'script-src': ['https://www.paypal.com', "'nonce-{}'".format(_nonce(request))],
|
||||
|
||||
# When the stars align in an unpredictable manner and the temperature is just right, the PayPal SDK might
|
||||
@@ -159,7 +164,12 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
|
||||
'connect-src': ['https://www.paypal.com', 'https://www.sandbox.paypal.com'], # Or not - seems to only affect PayPal logging...
|
||||
'img-src': ['https://t.paypal.com', 'https://www.paypalobjects.com'],
|
||||
'style-src': ["'unsafe-inline'"] # PayPal does not comply with our nonce unfortunately, see Z#23113213
|
||||
})
|
||||
}
|
||||
|
||||
_merge_csp(h, csps)
|
||||
|
||||
if h:
|
||||
response['Content-Security-Policy'] = _render_csp(h)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -34,22 +33,15 @@ from pretix.control.views.event import (
|
||||
|
||||
|
||||
class ReturnSettingsForm(SettingsForm):
|
||||
returnurl_prefix = forms.CharField(
|
||||
returnurl_prefix = forms.RegexField(
|
||||
label=_("Base redirection URLs"),
|
||||
help_text=_("Redirection will only be allowed to URLs that start with one of these prefixes. "
|
||||
"Enter one allowed URL prefix per line. "
|
||||
"Enter one or more allowed URL prefix per line. "
|
||||
"URL prefixes must include a slash after the hostname."),
|
||||
required=False,
|
||||
widget=forms.Textarea,
|
||||
regex=re.compile(r'^((https://.*/.*|http://localhost[:/].*)\n*)*$')
|
||||
)
|
||||
line_regex = re.compile(r'^(https://.*/.*|http://localhost(:[0-9]+)?/.*)$')
|
||||
|
||||
def clean_returnurl_prefix(self):
|
||||
val = self.cleaned_data['returnurl_prefix']
|
||||
for l in val.split("\n"):
|
||||
if not re.match(self.line_regex, l):
|
||||
raise ValidationError(_('All values must be URLs that include at last one slash after the hostname.'))
|
||||
return val
|
||||
|
||||
|
||||
class ReturnSettings(EventSettingsViewMixin, EventSettingsFormView):
|
||||
|
||||
@@ -230,15 +230,10 @@ class OrderMailForm(BaseMailForm):
|
||||
self.fields['attach_tickets'].help_text = _("Attachment of tickets is disabled in this event's email "
|
||||
"settings.")
|
||||
|
||||
choices = [
|
||||
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||
('valid_if_pending', _('payment pending but already confirmed')),
|
||||
('na', _('payment pending (except unapproved or already confirmed)')),
|
||||
('pa', _('approval pending')),
|
||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
||||
(Order.STATUS_EXPIRED, _("Expired")),
|
||||
]
|
||||
|
||||
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
||||
choices.insert(0, ('valid_if_pending', _('payment pending but already confirmed')))
|
||||
choices.insert(0, ('na', _('payment pending (except unapproved or already confirmed)')))
|
||||
choices.insert(0, ('pa', _('approval pending')))
|
||||
if not event.settings.get('payment_term_expire_automatically', as_type=bool):
|
||||
choices.append(
|
||||
('overdue', _('pending with payment overdue'))
|
||||
@@ -387,15 +382,11 @@ class RuleForm(FormPlaceholderMixin, I18nModelForm):
|
||||
self._set_field_placeholders('subject', ['event', 'order', 'event_or_subevent'])
|
||||
self._set_field_placeholders('template', ['event', 'order', 'event_or_subevent'], rich=True)
|
||||
|
||||
choices = [
|
||||
(Order.STATUS_PAID, _('Paid (or canceled with paid fee)')),
|
||||
('n__valid_if_pending', _('payment pending but already confirmed')),
|
||||
('n__not_pending_approval_and_not_valid_if_pending',
|
||||
_('payment pending (except unapproved or already confirmed)')),
|
||||
('n__pending_approval', _('approval pending')),
|
||||
(Order.STATUS_CANCELED, _('Canceled (fully)')),
|
||||
(Order.STATUS_EXPIRED, _("Expired")),
|
||||
]
|
||||
choices = [(e, l) for e, l in Order.STATUS_CHOICE if e != 'n']
|
||||
choices.insert(0, ('n__valid_if_pending', _('payment pending but already confirmed')))
|
||||
choices.insert(0, ('n__not_pending_approval_and_not_valid_if_pending',
|
||||
_('payment pending (except unapproved or already confirmed)')))
|
||||
choices.insert(0, ('n__pending_approval', _('approval pending')))
|
||||
if not self.event.settings.get('payment_term_expire_automatically', as_type=bool):
|
||||
choices.append(
|
||||
('n__pending_overdue', _('pending with payment overdue'))
|
||||
|
||||
@@ -31,7 +31,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from paypalhttp import HttpResponse
|
||||
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.middleware import add_to_response_csp
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
from pretix.base.settings import settings_hierarkey
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_global_settings, register_payment_providers,
|
||||
@@ -201,10 +201,21 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
|
||||
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
|
||||
(url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"])
|
||||
):
|
||||
add_to_response_csp(response, {
|
||||
if 'Content-Security-Policy' in response:
|
||||
h = _parse_csp(response['Content-Security-Policy'])
|
||||
else:
|
||||
h = {}
|
||||
|
||||
# https://stripe.com/docs/security/guide#content-security-policy
|
||||
csps = {
|
||||
'connect-src': ['https://api.stripe.com'],
|
||||
'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'],
|
||||
'script-src': ['https://js.stripe.com'],
|
||||
})
|
||||
}
|
||||
|
||||
_merge_csp(h, csps)
|
||||
|
||||
if h:
|
||||
response['Content-Security-Policy'] = _render_csp(h)
|
||||
|
||||
return response
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import functools
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import random
|
||||
|
||||
from django import forms
|
||||
@@ -30,6 +32,7 @@ from django.contrib.auth.password_validation import (
|
||||
)
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core import signing
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
@@ -41,7 +44,6 @@ from pretix.base.forms.questions import (
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import Customer
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
|
||||
|
||||
@@ -203,6 +205,23 @@ class RegistrationForm(forms.Form):
|
||||
min_value=0,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def ratelimit_key(self):
|
||||
if not settings.HAS_REDIS:
|
||||
return None
|
||||
client_ip = get_client_ip(self.request)
|
||||
if not client_ip:
|
||||
return None
|
||||
try:
|
||||
client_ip = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
# Web server not set up correctly
|
||||
return None
|
||||
if client_ip.is_private:
|
||||
# This is the private IP of the server, web server not set up correctly
|
||||
return None
|
||||
return 'pretix_customer_registration_{}'.format(hashlib.sha1(str(client_ip).encode()).hexdigest())
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
|
||||
@@ -236,11 +255,17 @@ class RegistrationForm(forms.Form):
|
||||
code='incomplete'
|
||||
)
|
||||
else:
|
||||
if rate_limit("customer_signup", include_ip_from_request=self.request, max_num=10, expire_time=600):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
if self.ratelimit_key:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr(self.ratelimit_key)
|
||||
rc.expire(self.ratelimit_key, 600)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
return self.cleaned_data
|
||||
|
||||
def create(self):
|
||||
@@ -334,11 +359,6 @@ class ResetPasswordForm(forms.Form):
|
||||
def clean_email(self):
|
||||
if 'email' not in self.cleaned_data:
|
||||
return
|
||||
if rate_limit("customer_pwreset_check", max_num=10, expire_time=600):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
try:
|
||||
self.customer = self.request.organizer.customers.get(email=self.cleaned_data['email'].lower(), provider__isnull=True)
|
||||
return self.customer.email
|
||||
@@ -350,8 +370,13 @@ class ResetPasswordForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('email'):
|
||||
if rate_limit("customer_pwreset", self.customer.pk, max_num=2, expire_time=600):
|
||||
if d.get('email') and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwreset_customer_%s' % self.customer.pk)
|
||||
rc.expire('pretix_pwreset_customer_%s' % self.customer.pk, 600)
|
||||
if cnt > 2:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
@@ -420,8 +445,13 @@ class ChangePasswordForm(forms.Form):
|
||||
def clean_password_current(self):
|
||||
old_pw = self.cleaned_data.get('password_current')
|
||||
|
||||
if old_pw:
|
||||
if rate_limit("customer_pwchange", self.customer.pk, max_num=10, expire_time=300):
|
||||
if old_pw and settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_customer_%s' % self.customer.pk)
|
||||
rc.expire('pretix_pwchange_customer_%s' % self.customer.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
@@ -491,11 +521,17 @@ class ChangeInfoForm(forms.ModelForm):
|
||||
old_pw = self.cleaned_data.get('password_current')
|
||||
|
||||
if old_pw:
|
||||
if rate_limit("customer_pwchange", self.instance.pk, max_num=10, expire_time=300):
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
if settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
|
||||
rc = get_redis_connection("redis")
|
||||
cnt = rc.incr('pretix_pwchange_customer_%s' % self.instance.pk)
|
||||
rc.expire('pretix_pwchange_customer_%s' % self.instance.pk, 300)
|
||||
if cnt > 10:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['rate_limit'],
|
||||
code='rate_limit',
|
||||
)
|
||||
|
||||
if not check_password(old_pw, self.instance.password):
|
||||
raise forms.ValidationError(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load icon %}
|
||||
{% load eventurl %}
|
||||
{% load date_fast %}
|
||||
|
||||
<div class="event-list full-width-list alternating-rows">
|
||||
{% for subev in subevent_list.subevent_list %}
|
||||
@@ -18,14 +17,8 @@
|
||||
<span data-time="{{ subev.date_from.isoformat }}" data-timezone="{{ event.timezone }}" data-time-short>
|
||||
{% icon "clock-o" %}
|
||||
<span class="sr-only">{% trans "Time of day" %}</span>
|
||||
<time datetime="{{ subev.date_from|date_fast:"H:i" }}">{{ subev.date_from|date_fast:"TIME_FORMAT" }}</time>
|
||||
<time datetime="{{ subev.date_from.isoformat }}">{{ subev.date_from|date:"TIME_FORMAT" }}</time>
|
||||
</span>
|
||||
{% if event.settings.show_date_to and subev.date_to and subev.is_same_day %}
|
||||
<span data-time="{{ subev.date_to.isoformat }}" data-timezone="{{ event.timezone }}" data-time-short>
|
||||
<span aria-hidden="true">–</span><span class="sr-only">{% trans "until" context "timerange" %}</span>
|
||||
<time datetime="{{ subev.date_to|date_fast:"H:i" }}">{{ subev.date_to|date_fast:"TIME_FORMAT" }}</time>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="col-md-3 col-xs-6">
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
{% icon "clock-o" %}
|
||||
<span class="sr-only">{% trans "Time of day" %}</span>
|
||||
<time datetime="{{ e.date_from|date_fast:"H:i" }}">{{ e.date_from|date:"TIME_FORMAT" }}</time>
|
||||
{% if e.settings.show_date_to and e.date_to and e.is_same_day %}
|
||||
{% if e.settings.show_date_to and e.date_to and e.date_to.date == e.date_from.date %}
|
||||
<span aria-hidden="true">–</span><span class="sr-only">{% trans "until" context "timerange" %}</span>
|
||||
<time datetime="{{ e.date_to|date_fast:"H:i" }}">{{ e.date_to|date:"TIME_FORMAT" }}</time>
|
||||
{% endif %}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
# 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 django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -41,7 +42,6 @@ from django.views.generic import TemplateView
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.services.mail import INVALID_ADDRESS, mail
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.helpers.ratelimit import rate_limit
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.forms.user import ResendLinkForm
|
||||
from pretix.presale.views import EventViewMixin
|
||||
@@ -61,12 +61,17 @@ class ResendLinkView(EventViewMixin, TemplateView):
|
||||
|
||||
user = self.link_form.cleaned_data.get('email')
|
||||
|
||||
if rate_limit("order_resend", self.request.event.pk, user, max_num=1, expire_time=3600 * 24):
|
||||
messages.error(request, _('If the email address you entered is valid and associated with a ticket, we have '
|
||||
'already sent you an email with a link to your ticket in the past {number} hours. '
|
||||
'If the email did not arrive, please check your spam folder and also double check '
|
||||
'that you used the correct email address.').format(number=24))
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link'))
|
||||
if settings.HAS_REDIS:
|
||||
from django_redis import get_redis_connection
|
||||
rc = get_redis_connection("redis")
|
||||
if rc.exists('pretix_resend_{}_{}'.format(request.event.pk, user)):
|
||||
messages.error(request, _('If the email address you entered is valid and associated with a ticket, we have '
|
||||
'already sent you an email with a link to your ticket in the past {number} hours. '
|
||||
'If the email did not arrive, please check your spam folder and also double check '
|
||||
'that you used the correct email address.').format(number=24))
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.resend_link'))
|
||||
else:
|
||||
rc.setex('pretix_resend_{}_{}'.format(request.event.pk, user), 3600 * 24, '1')
|
||||
|
||||
orders = self.request.event.orders.filter(email__iexact=user)
|
||||
|
||||
|
||||
@@ -512,7 +512,6 @@ REST_FRAMEWORK = {
|
||||
|
||||
MIDDLEWARE = [
|
||||
'pretix.helpers.logs.RequestIdMiddleware',
|
||||
'pretix.base.middleware.BaseLocaleMiddleware',
|
||||
'pretix.api.middleware.IdempotencyMiddleware',
|
||||
'pretix.multidomain.middlewares.MultiDomainMiddleware',
|
||||
'django_querytagger.middleware.SetTagMiddleware', # after MultiDomainMiddleware for correct url resolving
|
||||
|
||||
@@ -124,7 +124,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find(".datetimepicker").each(function () {
|
||||
$(this).datetimepicker({
|
||||
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-datetimeformat"),
|
||||
format: $("body").attr("data-datetimeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: !$(this).prop("required"),
|
||||
@@ -147,7 +147,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find(".datepickerfield").each(function () {
|
||||
var opts = {
|
||||
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-dateformat"),
|
||||
format: $("body").attr("data-dateformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: !$(this).prop("required"),
|
||||
@@ -205,7 +205,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find(".timepickerfield").each(function () {
|
||||
var opts = {
|
||||
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-timeformat"),
|
||||
format: $("body").attr("data-timeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: !$(this).prop("required"),
|
||||
|
||||
@@ -464,8 +464,6 @@ details.details-open .panel-title::before {
|
||||
.alert > dl:last-child,
|
||||
td > p:last-child,
|
||||
.panel-body > dl:last-child,
|
||||
.panel-body > ul:last-child,
|
||||
.panel-body > ol:last-child,
|
||||
.panel-body > .table:last-child,
|
||||
.panel-body > .table-responsive:last-child > .table:last-child,
|
||||
table td ul:last-child {
|
||||
|
||||
@@ -11,7 +11,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find(".datetimepicker").each(function () {
|
||||
$(this).datetimepicker({
|
||||
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-datetimeformat"),
|
||||
format: $("body").attr("data-datetimeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: !$(this).prop("required"),
|
||||
@@ -34,7 +34,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find(".datepickerfield").each(function () {
|
||||
var opts = {
|
||||
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-dateformat"),
|
||||
format: $("body").attr("data-dateformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: !$(this).prop("required"),
|
||||
@@ -91,7 +91,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find(".timepickerfield").each(function () {
|
||||
var opts = {
|
||||
format: $(this).attr("data-format") ? $(this).attr("data-format") : $("body").attr("data-timeformat"),
|
||||
format: $("body").attr("data-timeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: !$(this).prop("required"),
|
||||
|
||||
@@ -60,18 +60,8 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-button]', info, error)
|
||||
}
|
||||
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
|
||||
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
|
||||
// The latter can cause issues with custom CSS as HTML structure would change.
|
||||
// app.mount(element)
|
||||
// observer.observe(element, { attributes: true })
|
||||
const fragment = document.createDocumentFragment()
|
||||
app.mount(fragment)
|
||||
for (const attr of element.attributes) {
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value)
|
||||
}
|
||||
observer.observe(fragment.firstChild, { attributes: true })
|
||||
element.parentNode.replaceChild(fragment, element)
|
||||
app.mount(element)
|
||||
observer.observe(element, { attributes: true })
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -56,18 +56,8 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
|
||||
app.config.errorHandler = (error, _vm, info) => {
|
||||
console.error('[pretix-widget]', info, error)
|
||||
}
|
||||
// Instead of mounting to the element directly, replicate vue2.7 behaviour, where
|
||||
// the HTML-element gets replaced instead of vue3’s behaviour to replace innerHTML.
|
||||
// The latter can cause issues with custom CSS as HTML structure would change.
|
||||
// app.mount(element)
|
||||
// observer.observe(element, { attributes: true })
|
||||
const fragment = document.createDocumentFragment()
|
||||
app.mount(fragment)
|
||||
for (const attr of element.attributes) {
|
||||
fragment.firstChild.setAttribute(attr.name, attr.value)
|
||||
}
|
||||
observer.observe(fragment.firstChild, { attributes: true })
|
||||
element.parentNode.replaceChild(fragment, element)
|
||||
app.mount(element)
|
||||
observer.observe(element, { attributes: true })
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ TEST_HISTORY_RES = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_list_list(token_client, organizer, event, clist, item, subevent, django_assert_num_queries):
|
||||
res = dict(TEST_LIST_RES)
|
||||
res["id"] = clist.pk
|
||||
@@ -422,7 +422,7 @@ def test_list_update(token_client, organizer, event, clist):
|
||||
assert cl.name == "VIP"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
@@ -680,7 +680,7 @@ def _redeem(token_client, org, clist, p, body=None):
|
||||
), body or {}, format='json')
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first().pk
|
||||
@@ -1368,7 +1368,7 @@ def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist,
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
@@ -1400,7 +1400,7 @@ def test_checkin_pdf_data_requires_permission(token_client, event, team, organiz
|
||||
assert not resp.data['results'][0].get('pdf_data')
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_expand(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
op = order.positions.first()
|
||||
|
||||
@@ -214,7 +214,7 @@ def _redeem(token_client, org, clist, p, body=None, query='', headers={}):
|
||||
), body, format='json', headers={})
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
@@ -996,7 +996,7 @@ def test_redeem_conflicting_lists(token_client, organizer, clist, clist_all, eve
|
||||
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order,
|
||||
django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -1848,7 +1848,7 @@ def test_event_block_unblock_seat_bulk(token_client, organizer, event, seatingpl
|
||||
assert not s2.blocked
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item):
|
||||
event.settings.seating_minimal_distance = 2
|
||||
|
||||
|
||||
@@ -730,35 +730,6 @@ def test_payment_create_confirmed(token_client, organizer, event, order):
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_payment_create_confirmed_after_expiry(token_client, organizer, event, order):
|
||||
djmail.outbox = []
|
||||
order.expires = now() - datetime.timedelta(days=2)
|
||||
order.save()
|
||||
event.settings.payment_term_last = (now() - datetime.timedelta(days=2)).strftime('%Y-%m-%d')
|
||||
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
|
||||
organizer.slug, event.slug, order.code
|
||||
), format='json', data={
|
||||
'provider': 'banktransfer',
|
||||
'state': 'confirmed',
|
||||
'amount': order.total,
|
||||
'send_email': False,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
},
|
||||
"force": True
|
||||
})
|
||||
with scopes_disabled():
|
||||
p = order.payments.last()
|
||||
assert resp.status_code == 201
|
||||
assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||
assert p.info_data == {'foo': 'bar'}
|
||||
order.refresh_from_db()
|
||||
assert order.status == Order.STATUS_PAID
|
||||
assert len(djmail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_payment_create_pending(token_client, organizer, event, order):
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
|
||||
@@ -1087,7 +1058,7 @@ def test_orderposition_list_limited_read(
|
||||
('/api/v1/organizers/{}/orderpositions/', "organizer")
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_orderposition_list(
|
||||
endpoint_template,
|
||||
endpoint_type,
|
||||
@@ -2065,7 +2036,7 @@ def test_blocked_secret_list(token_client, organizer, event):
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_queries):
|
||||
# order detail
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?pdf_data=true'.format(
|
||||
|
||||
@@ -732,7 +732,7 @@ def test_five_tickets_one_free(event):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("itemcount", [3, 10, 50])
|
||||
def test_query_count_many_items(event, itemcount):
|
||||
setup_items(event, 'Tickets', 'both', 'discounts',
|
||||
@@ -784,7 +784,7 @@ def test_query_count_many_items(event, itemcount):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("catcount", [1, 10, 50])
|
||||
def test_query_count_many_categories_and_discounts(event, catcount):
|
||||
for n in range(1, catcount + 1):
|
||||
@@ -838,7 +838,7 @@ def test_query_count_many_categories_and_discounts(event, catcount):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("catcount", [2, 10, 50])
|
||||
def test_query_count_many_cartpos(event, catcount):
|
||||
for n in range(1, catcount + 1):
|
||||
|
||||
@@ -26,7 +26,6 @@ from zoneinfo import ZoneInfo
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
from freezegun import freeze_time
|
||||
@@ -211,8 +210,7 @@ def test_validate_membership_required(event, customer, membership, requiring_tic
|
||||
assert "requires an active" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@transaction.atomic
|
||||
@pytest.mark.django_db
|
||||
def test_validate_membership_ensure_locking(event, customer, membership, requiring_ticket, membership_type, django_assert_num_queries):
|
||||
with django_assert_num_queries(4) as captured:
|
||||
validate_memberships_in_order(
|
||||
|
||||
@@ -28,7 +28,6 @@ from django.test import override_settings
|
||||
from django.utils import translation
|
||||
from django_scopes import scopes_disabled
|
||||
from fakeredis import FakeRedisConnection
|
||||
from hierarkey.proxy import dirty_cache_keys
|
||||
from xdist.dsession import DSession
|
||||
|
||||
from pretix.testutils.mock import get_redis_connection
|
||||
@@ -83,11 +82,6 @@ def reset_locale():
|
||||
translation.activate("en")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_hierarkey_cache_state():
|
||||
dirty_cache_keys.set(set())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fakeredis_client(monkeypatch):
|
||||
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
|
||||
@@ -126,7 +120,6 @@ def fakeredis_client(monkeypatch):
|
||||
redis = get_redis_connection("default", True)
|
||||
redis.flushall()
|
||||
monkeypatch.setattr('django_redis.get_redis_connection', get_redis_connection, raising=False)
|
||||
monkeypatch.setattr('pretix.base.metrics.redis', redis, raising=False)
|
||||
yield redis
|
||||
|
||||
|
||||
|
||||
@@ -504,8 +504,33 @@ class Login2FAFormTest(TestCase):
|
||||
assert "recovery code" in djmail.outbox[0].body
|
||||
|
||||
|
||||
class FakeRedis(object):
|
||||
def get_redis_connection(self, connection_string):
|
||||
return self
|
||||
|
||||
def __init__(self):
|
||||
self.storage = {}
|
||||
|
||||
def pipeline(self):
|
||||
return self
|
||||
|
||||
def hincrbyfloat(self, rkey, key, amount):
|
||||
return self
|
||||
|
||||
def commit(self):
|
||||
return self
|
||||
|
||||
def exists(self, rkey):
|
||||
return rkey in self.storage
|
||||
|
||||
def setex(self, rkey, value, expiration):
|
||||
self.storage[rkey] = value
|
||||
|
||||
def execute(self):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("class_monkeypatch")
|
||||
@pytest.mark.usefixtures("fakeredis_client")
|
||||
class PasswordRecoveryFormTest(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -535,6 +560,11 @@ class PasswordRecoveryFormTest(TestCase):
|
||||
|
||||
@override_settings(HAS_REDIS=True)
|
||||
def test_email_reset_twice_redis(self):
|
||||
fake_redis = FakeRedis()
|
||||
m = self.monkeypatch
|
||||
m.setattr('django_redis.get_redis_connection', fake_redis.get_redis_connection, raising=False)
|
||||
m.setattr('pretix.base.metrics.redis', fake_redis, raising=False)
|
||||
|
||||
djmail.outbox = []
|
||||
|
||||
response = self.client.post('/control/forgot', {
|
||||
|
||||
@@ -50,7 +50,6 @@ def env():
|
||||
datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=23,
|
||||
sales_channel=o.sales_channels.get(identifier="web"),
|
||||
email='dummy@dummy.dummy'
|
||||
)
|
||||
o2 = Order.objects.create(
|
||||
code='6789Z', event=event,
|
||||
@@ -130,7 +129,7 @@ def test_assign_order_unknown(env, client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_assign_order_amount_incorrect(env, client, mailoutbox):
|
||||
def test_assign_order_amount_incorrect(env, client):
|
||||
job = BankImportJob.objects.create(event=env[0])
|
||||
trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo',
|
||||
state=BankTransaction.STATE_NOMATCH,
|
||||
@@ -142,7 +141,6 @@ def test_assign_order_amount_incorrect(env, client, mailoutbox):
|
||||
assert r['status'] == 'ok'
|
||||
trans.refresh_from_db()
|
||||
assert trans.state == BankTransaction.STATE_VALID
|
||||
assert len(mailoutbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
Reference in New Issue
Block a user