diff --git a/doc/user/organizers/domain.rst b/doc/user/organizers/domain.rst index ee7eef021a..f8db00eaa9 100644 --- a/doc/user/organizers/domain.rst +++ b/doc/user/organizers/domain.rst @@ -14,30 +14,23 @@ and with pretix, you can do this. On this page, you find out the necessary steps With the pretix.eu hosted service --------------------------------- -Step 1: DNS Configuration -######################### +Go to "Organizers" in the backend and select your organizer account. Then, go to "Settings" and "Custom Domain". + +This page will show you instructions on how to set up your own domain. Basically, it works like this: Go to the website of the provider you registered your domain name with. Look for the "DNS" settings page in their interface. Unfortunately, we can't tell you exactly how that is named and how it looks, since it is different for every domain provider. Use this interface to add a new subdomain record, e.g. ``tickets`` of the type ``CNAME`` (might also be called "alias"). -The value of the record should be ``www.pretix.eu``. - -Step 2: Wait for the DNS entry to propagate -########################################### +The value of the record should be the one shown on the "Custom Domain" page in pretix' backend. Submit your changes and wait a bit, it can regularly take up to three hours for DNS changes to propagate to the caches of all DNS servers. You can try checking by accessing your new subdomain, ``http://tickets.awesomepartycorp.com``. If DNS was changed successfully, you should see a SSL certificate error. If you ignore the error and access the page anyways, you should get a pretix-themed error page with the headline "Unknown domain". -Step 3: Tell us -############### - -Write an email to support@pretix.eu, naming your new domain and your organizer account. We will then generate a SSL -certificate for you (for free!) and configure the domain. - +Now, tell us about your domain on the "Custom Domain" page to get started. With a custom pretix installation --------------------------------- diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py index b2706121ea..0a964317fa 100644 --- a/src/pretix/base/middleware.py +++ b/src/pretix/base/middleware.py @@ -15,7 +15,9 @@ from django.utils.translation.trans_real import ( ) from pretix.base.settings import GlobalSettingsObject -from pretix.multidomain.urlreverse import get_domain +from pretix.multidomain.urlreverse import ( + get_event_domain, get_organizer_domain, +) _supported = None @@ -231,7 +233,10 @@ class SecurityMiddleware(MiddlewareMixin): dynamicdomain += " " + settings.SITE_URL if hasattr(request, 'organizer') and request.organizer: - domain = get_domain(request.organizer) + if hasattr(request, 'event') and request.event: + domain = get_event_domain(request.event, fallback=True) + else: + domain = get_organizer_domain(request.organizer) if domain: siteurlsplit = urlsplit(settings.SITE_URL) if siteurlsplit.port and siteurlsplit.port not in (80, 443): diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index dd84f6e776..d2f577d938 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1,4 +1,4 @@ -from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse from django import forms from django.conf import settings @@ -32,6 +32,7 @@ from pretix.control.forms import ( SplitDateTimeField, SplitDateTimePickerWidget, ) from pretix.control.forms.widgets import Select2 +from pretix.multidomain.models import KnownDomain from pretix.multidomain.urlreverse import build_absolute_uri from pretix.plugins.banktransfer.payment import BankTransfer from pretix.presale.style import get_fonts @@ -291,6 +292,15 @@ class EventUpdateForm(I18nModelForm): def __init__(self, *args, **kwargs): self.change_slug = kwargs.pop('change_slug', False) + self.domain = kwargs.pop('domain', False) + + kwargs.setdefault('initial', {}) + self.instance = kwargs['instance'] + if self.domain and self.instance: + initial_domain = self.instance.domains.first() + if initial_domain: + kwargs['initial'].setdefault('domain', initial_domain.domainname) + super().__init__(*args, **kwargs) if not self.change_slug: self.fields['slug'].widget.attrs['readonly'] = 'readonly' @@ -298,6 +308,47 @@ class EventUpdateForm(I18nModelForm): self.fields['location'].widget.attrs['placeholder'] = _( 'Sample Conference Center\nHeidelberg, Germany' ) + if self.domain: + self.fields['domain'] = forms.CharField( + max_length=255, + label=_('Custom domain'), + required=False, + help_text=_('You need to configure the custom domain in the webserver beforehand.') + ) + + def clean_domain(self): + d = self.cleaned_data['domain'] + if d: + if d == urlparse(settings.SITE_URL).hostname: + raise ValidationError( + _('You cannot choose the base domain of this installation.') + ) + if KnownDomain.objects.filter(domainname=d).exclude(event=self.instance.pk).exists(): + raise ValidationError( + _('This domain is already in use for a different event or organizer.') + ) + return d + + def save(self, commit=True): + instance = super().save(commit) + + if self.domain: + current_domain = instance.domains.first() + if self.cleaned_data['domain']: + if current_domain and current_domain.domainname != self.cleaned_data['domain']: + current_domain.delete() + KnownDomain.objects.create( + organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain'] + ) + elif not current_domain: + KnownDomain.objects.create( + organizer=instance.organizer, event=instance, domainname=self.cleaned_data['domain'] + ) + elif current_domain: + current_domain.delete() + instance.cache.clear() + + return instance def clean_slug(self): if self.change_slug: diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index dca2f381a8..117e82f319 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -95,9 +95,10 @@ class OrganizerUpdateForm(OrganizerForm): raise ValidationError( _('You cannot choose the base domain of this installation.') ) - if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk).exists(): + if KnownDomain.objects.filter(domainname=d).exclude(organizer=self.instance.pk, + event__isnull=True).exists(): raise ValidationError( - _('This domain is already in use for a different organizer.') + _('This domain is already in use for a different event or organizer.') ) return d diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index f6521f2a98..dd0d15bdcb 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -19,6 +19,9 @@ {% trans "Basics" %} {% bootstrap_field form.name layout="control" %} {% bootstrap_field form.slug layout="control" %} + {% if form.domain %} + {% bootstrap_field form.domain layout="control" %} + {% endif %} {% bootstrap_field form.date_from layout="control" %} {% bootstrap_field form.date_to layout="control" %}
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 07ab55a077..ff1f6f83db 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -48,7 +48,7 @@ from pretix.control.forms.event import ( from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.views.user import RecentAuthenticationRequiredMixin from pretix.helpers.database import rolledback_transaction -from pretix.multidomain.urlreverse import get_domain +from pretix.multidomain.urlreverse import get_event_domain from pretix.plugins.stripe.payment import StripeSettingsHolder from pretix.presale.style import regenerate_css @@ -162,7 +162,10 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired change_css = True if form.has_changed(): self.request.event.log_action('pretix.event.changed', user=self.request.user, data={ - k: getattr(self.request.event, k) for k in form.changed_data + k: (form.cleaned_data.get(k).name + if isinstance(form.cleaned_data.get(k), File) + else form.cleaned_data.get(k)) + for k in form.changed_data }) if change_css: @@ -184,6 +187,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired kwargs = super().get_form_kwargs() if self.request.user.has_active_staff_session(self.request.session.session_key): kwargs['change_slug'] = True + kwargs['domain'] = True return kwargs def post(self, request, *args, **kwargs): @@ -1208,7 +1212,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['urlprefix'] = settings.SITE_URL - domain = get_domain(self.request.organizer) + domain = get_event_domain(self.request.event, fallback=True) if domain: siteurlsplit = urlsplit(settings.SITE_URL) if siteurlsplit.port and siteurlsplit.port not in (80, 443): diff --git a/src/pretix/multidomain/event_domain_urlconf.py b/src/pretix/multidomain/event_domain_urlconf.py new file mode 100644 index 0000000000..4e8fa06d87 --- /dev/null +++ b/src/pretix/multidomain/event_domain_urlconf.py @@ -0,0 +1,35 @@ +import importlib.util + +from django.apps import apps +from django.conf.urls import include, url + +from pretix.multidomain.plugin_handler import plugin_event_urls +from pretix.presale.urls import event_patterns, locale_patterns +from pretix.urls import common_patterns + +presale_patterns = [ + url(r'', include((locale_patterns + [ + url(r'', include(event_patterns)), + ], 'presale'))) +] + +raw_plugin_patterns = [] +for app in apps.get_app_configs(): + if hasattr(app, 'PretixPluginMeta'): + if importlib.util.find_spec(app.name + '.urls'): + urlmod = importlib.import_module(app.name + '.urls') + if hasattr(urlmod, 'event_patterns'): + patterns = plugin_event_urls(urlmod.event_patterns, plugin=app.name) + raw_plugin_patterns.append( + url(r'', include((patterns, app.label))) + ) + +plugin_patterns = [ + url(r'', include((raw_plugin_patterns, 'plugins'))) +] + +# The presale namespace comes last, because it contains a wildcard catch +urlpatterns = common_patterns + plugin_patterns + presale_patterns + +handler404 = 'pretix.base.views.errors.page_not_found' +handler500 = 'pretix.base.views.errors.server_error' diff --git a/src/pretix/multidomain/maindomain_urlconf.py b/src/pretix/multidomain/maindomain_urlconf.py index bc2274d3c2..08d41b5b34 100644 --- a/src/pretix/multidomain/maindomain_urlconf.py +++ b/src/pretix/multidomain/maindomain_urlconf.py @@ -1,5 +1,4 @@ import importlib.util -import warnings from django.apps import apps from django.conf.urls import include, url @@ -38,14 +37,6 @@ for app in apps.get_app_configs(): raw_plugin_patterns.append( url(r'', include((single_plugin_patterns, app.label))) ) - elif importlib.util.find_spec(app.name + '.maindomain_urls'): # noqa - warnings.warn('Please put your config in an \'urls\' module using the urlpatterns and event_patterns ' - 'attribute. Support for maindomain_urls in plugins will be dropped in the future.', - DeprecationWarning) - urlmod = importlib.import_module(app.name + '.maindomain_urls') - raw_plugin_patterns.append( - url(r'', include((urlmod, app.label))) - ) plugin_patterns = [ url(r'', include((raw_plugin_patterns, 'plugins'))) diff --git a/src/pretix/multidomain/middlewares.py b/src/pretix/multidomain/middlewares.py index fc729f50af..7752c38515 100644 --- a/src/pretix/multidomain/middlewares.py +++ b/src/pretix/multidomain/middlewares.py @@ -14,7 +14,7 @@ from django.utils.cache import patch_vary_headers from django.utils.deprecation import MiddlewareMixin from django.utils.http import http_date -from pretix.base.models import Organizer +from pretix.base.models import Event, Organizer from pretix.helpers.cookies import set_cookie_without_samesite from pretix.multidomain.models import KnownDomain @@ -37,33 +37,42 @@ class MultiDomainMiddleware(MiddlewareMixin): domain, port = split_domain_port(host) default_domain, default_port = split_domain_port(urlparse(settings.SITE_URL).netloc) - if domain: - request.host = domain - request.port = int(port) if port else None + request.port = int(port) if port else None + request.host = domain + if domain == default_domain: + request.urlconf = "pretix.multidomain.maindomain_urlconf" + elif domain: + cached = cache.get('pretix_multidomain_instance_{}'.format(domain)) - orga = cache.get('pretix_multidomain_organizer_instance_{}'.format(domain)) - if orga is None: + if cached is None: try: - kd = KnownDomain.objects.select_related('organizer').get(domainname=domain) # noqa + kd = KnownDomain.objects.select_related('organizer', 'event').get(domainname=domain) # noqa orga = kd.organizer + event = kd.event except KnownDomain.DoesNotExist: orga = False - cache.set('pretix_multidomain_organizer_instance_{}'.format(domain), orga, 3600) + event = False + cache.set('pretix_multidomain_instance_{}'.format(domain), (orga, event), 3600) + else: + orga, event = cached - if orga: + if event: + request.event_domain = True + request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga) + request.event = event if isinstance(event, Event) else orga.events.get(pk=event) + request.urlconf = "pretix.multidomain.event_domain_urlconf" + elif orga: request.organizer_domain = True request.organizer = orga if isinstance(orga, Organizer) else Organizer.objects.get(pk=orga) - request.urlconf = "pretix.multidomain.subdomain_urlconf" + request.urlconf = "pretix.multidomain.organizer_domain_urlconf" + elif settings.DEBUG or domain in LOCAL_HOST_NAMES: + request.urlconf = "pretix.multidomain.maindomain_urlconf" else: - if settings.DEBUG or domain in LOCAL_HOST_NAMES or domain == default_domain: - request.urlconf = "pretix.multidomain.maindomain_urlconf" - else: - raise DisallowedHost("Unknown host: %r" % host) - + raise DisallowedHost("Unknown host: %r" % host) else: raise DisallowedHost("Invalid HTTP_HOST header: %r." % host) - # We need to manually set the urlconf for the whole thread. Normally, Django's basic request + # We need to manually set the urlconf for the whole thread. Normally, Django's basic request handling # would do this for us, but we already need it in place for the other middlewares. set_urlconf(request.urlconf) diff --git a/src/pretix/multidomain/migrations/0002_knowndomain_event.py b/src/pretix/multidomain/migrations/0002_knowndomain_event.py new file mode 100644 index 0000000000..72ece37d2c --- /dev/null +++ b/src/pretix/multidomain/migrations/0002_knowndomain_event.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.11 on 2020-03-20 14:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0146_giftcardtransaction_text'), + ('pretixmultidomain', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='knowndomain', + name='event', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='domains', to='pretixbase.Event'), + ), + ] diff --git a/src/pretix/multidomain/models.py b/src/pretix/multidomain/models.py index dc8eb0baea..b958b4a1c4 100644 --- a/src/pretix/multidomain/models.py +++ b/src/pretix/multidomain/models.py @@ -1,13 +1,15 @@ from django.core.cache import cache from django.db import models from django.utils.translation import ugettext_lazy as _ +from django_scopes import scopes_disabled -from pretix.base.models import Organizer +from pretix.base.models import Event, Organizer class KnownDomain(models.Model): domainname = models.CharField(max_length=255, primary_key=True) organizer = models.ForeignKey(Organizer, blank=True, null=True, related_name='domains', on_delete=models.CASCADE) + event = models.ForeignKey(Event, blank=True, null=True, related_name='domains', on_delete=models.PROTECT) class Meta: verbose_name = _("Known domain") @@ -16,16 +18,28 @@ class KnownDomain(models.Model): def __str__(self): return self.domainname + @scopes_disabled() def save(self, *args, **kwargs): super().save(*args, **kwargs) - if self.organizer: + if self.event: + self.event.get_cache().clear() + elif self.organizer: self.organizer.get_cache().clear() + for event in self.organizer.events.all(): + event.get_cache().clear() cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname)) - cache.delete('pretix_multidomain_organizer_instance_{}'.format(self.domainname)) + cache.delete('pretix_multidomain_instance_{}'.format(self.domainname)) + cache.delete('pretix_multidomain_event_{}'.format(self.domainname)) + @scopes_disabled() def delete(self, *args, **kwargs): - if self.organizer: + if self.event: + self.event.get_cache().clear() + elif self.organizer: self.organizer.get_cache().clear() + for event in self.organizer.events.all(): + event.get_cache().clear() cache.delete('pretix_multidomain_organizer_{}'.format(self.domainname)) - cache.delete('pretix_multidomain_organizer_instance_{}'.format(self.domainname)) + cache.delete('pretix_multidomain_instance_{}'.format(self.domainname)) + cache.delete('pretix_multidomain_event_{}'.format(self.domainname)) super().delete(*args, **kwargs) diff --git a/src/pretix/multidomain/subdomain_urlconf.py b/src/pretix/multidomain/organizer_domain_urlconf.py similarity index 74% rename from src/pretix/multidomain/subdomain_urlconf.py rename to src/pretix/multidomain/organizer_domain_urlconf.py index e6558408c9..500a8e8f24 100644 --- a/src/pretix/multidomain/subdomain_urlconf.py +++ b/src/pretix/multidomain/organizer_domain_urlconf.py @@ -1,5 +1,4 @@ import importlib.util -import warnings from django.apps import apps from django.conf.urls import include, url @@ -33,15 +32,6 @@ for app in apps.get_app_configs(): url(r'', include((patterns, app.label))) ) - elif importlib.util.find_spec(app.name + '.subdomain_urls'): # noqa - warnings.warn('Please put your config in an \'urls\' module using the event_patterns ' - 'attribute. Support for subdomain_urls in plugins will be dropped in the future.', - DeprecationWarning) - urlmod = importlib.import_module(app.name + '.subdomain_urls') - raw_plugin_patterns.append( - url(r'', include((urlmod, app.label))) - ) - plugin_patterns = [ url(r'', include((raw_plugin_patterns, 'plugins'))) ] diff --git a/src/pretix/multidomain/urlreverse.py b/src/pretix/multidomain/urlreverse.py index c098c21d6e..707cfdaedf 100644 --- a/src/pretix/multidomain/urlreverse.py +++ b/src/pretix/multidomain/urlreverse.py @@ -1,15 +1,48 @@ from urllib.parse import urljoin, urlsplit from django.conf import settings +from django.db.models import Q from django.urls import reverse from pretix.base.models import Event, Organizer +from .models import KnownDomain -def get_domain(organizer): + +def get_event_domain(event, fallback=False, return_info=False): + assert isinstance(event, Event) + suffix = ('_fallback' if fallback else '') + ('_info' if return_info else '') + domain = getattr(event, '_cached_domain' + suffix, None) or event.cache.get('domain' + suffix) + if domain is None: + domain = None, None + if fallback: + domains = KnownDomain.objects.filter( + Q(event=event) | Q(organizer_id=event.organizer_id, event__isnull=True) + ) + domains_event = [d for d in domains if d.event_id == event.pk] + domains_org = [d for d in domains if not d.event_id] + if domains_event: + domain = domains_event[0].domainname, "event" + elif domains_org: + domain = domains_org[0].domainname, "organizer" + else: + domains = event.domains.all() + domain = domains[0].domainname if domains else None, "event" + event.cache.set('domain' + suffix, domain or 'none') + setattr(event, '_cached_domain' + suffix, domain or 'none') + elif domain == 'none': + setattr(event, '_cached_domain' + suffix, 'none') + domain = None, None + else: + setattr(event, '_cached_domain' + suffix, domain) + return domain if return_info or not isinstance(domain, tuple) else domain[0] + + +def get_organizer_domain(organizer): + assert isinstance(organizer, Organizer) domain = getattr(organizer, '_cached_domain', None) or organizer.cache.get('domain') if domain is None: - domains = organizer.domains.all() + domains = organizer.domains.filter(event__isnull=True) domain = domains[0].domainname if domains else None organizer.cache.set('domain', domain or 'none') organizer._cached_domain = domain or 'none' @@ -45,7 +78,7 @@ def mainreverse(name, kwargs=None): def eventreverse(obj, name, kwargs=None): """ Works similar to ``django.core.urlresolvers.reverse`` but takes into account that some - organizers might have their own (sub)domain instead of a subpath. + organizers or events might have their own (sub)domain instead of a subpath. Non-keyword arguments are not supported as we want do discourage using them for better readability. @@ -58,7 +91,7 @@ def eventreverse(obj, name, kwargs=None): needed. :returns: An absolute URL (including scheme and host) as a string """ - from pretix.multidomain import subdomain_urlconf, maindomain_urlconf + from pretix.multidomain import event_domain_urlconf, organizer_domain_urlconf, maindomain_urlconf c = None if not kwargs: @@ -69,18 +102,27 @@ def eventreverse(obj, name, kwargs=None): kwargs = kwargs or {} if isinstance(obj, Event): - kwargs['event'] = obj.slug organizer = obj.organizer + event = obj + kwargs['event'] = obj.slug elif isinstance(obj, Organizer): organizer = obj + event = None else: raise TypeError('obj should be Event or Organizer') - domain = get_domain(organizer) + + if event: + domain, domaintype = get_event_domain(obj, fallback=True, return_info=True) + else: + domain, domaintype = get_organizer_domain(organizer), "organizer" + if domain: + if domaintype == "event" and 'event' in kwargs: + del kwargs['event'] if 'organizer' in kwargs: del kwargs['organizer'] - path = reverse(name, kwargs=kwargs, urlconf=subdomain_urlconf) + path = reverse(name, kwargs=kwargs, urlconf=event_domain_urlconf if domaintype == "event" else organizer_domain_urlconf) siteurlsplit = urlsplit(settings.SITE_URL) if siteurlsplit.port and siteurlsplit.port not in (80, 443): domain = '%s:%d' % (domain, siteurlsplit.port) diff --git a/src/pretix/plugins/stripe/tasks.py b/src/pretix/plugins/stripe/tasks.py index e703ff6b05..f7260dee04 100644 --- a/src/pretix/plugins/stripe/tasks.py +++ b/src/pretix/plugins/stripe/tasks.py @@ -6,14 +6,14 @@ from django.conf import settings from pretix.base.services.tasks import EventTask from pretix.celery_app import app -from pretix.multidomain.urlreverse import get_domain +from pretix.multidomain.urlreverse import get_event_domain from pretix.plugins.stripe.models import RegisteredApplePayDomain logger = logging.getLogger(__name__) def get_domain_for_event(event): - domain = get_domain(event.organizer) + domain = get_event_domain(event, fallback=True) if not domain: siteurlsplit = urlsplit(settings.SITE_URL) return siteurlsplit.hostname diff --git a/src/pretix/presale/middleware.py b/src/pretix/presale/middleware.py index fc56d4bccf..e541b2fefe 100644 --- a/src/pretix/presale/middleware.py +++ b/src/pretix/presale/middleware.py @@ -18,7 +18,7 @@ class EventMiddleware: if url.namespace != 'presale': return self.get_response(request) - if 'organizer' in url.kwargs or 'event' in url.kwargs: + if 'organizer' in url.kwargs or 'event' in url.kwargs or getattr(request, 'event_domain', False): redirect = _detect_event(request, require_live=url.url_name != 'event.widget.productlist') if redirect: return redirect diff --git a/src/pretix/presale/style.py b/src/pretix/presale/style.py index e160a8ee3d..b52062b939 100644 --- a/src/pretix/presale/style.py +++ b/src/pretix/presale/style.py @@ -20,7 +20,9 @@ from pretix.base.services.tasks import ( TransactionAwareProfiledEventTask, TransactionAwareTask, ) from pretix.celery_app import app -from pretix.multidomain.urlreverse import get_domain +from pretix.multidomain.urlreverse import ( + get_event_domain, get_organizer_domain, +) from pretix.presale.signals import sass_postamble, sass_preamble logger = logging.getLogger('pretix.presale.style') @@ -33,7 +35,10 @@ def compile_scss(object, file="main.scss", fonts=True): def static(path): sp = _static(path) if not settings.MEDIA_URL.startswith("/") and sp.startswith("/"): - domain = get_domain(object.organizer if isinstance(object, Event) else object) + if isinstance(object, Event): + domain = get_event_domain(object, fallback=True) + else: + domain = get_organizer_domain(object) if domain: siteurlsplit = urlsplit(settings.SITE_URL) if siteurlsplit.port and siteurlsplit.port not in (80, 443): diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py index 190df1c00b..bd9b419d37 100644 --- a/src/pretix/presale/utils.py +++ b/src/pretix/presale/utils.py @@ -14,7 +14,9 @@ from django_scopes import scope from pretix.base.channels import WebshopSalesChannel from pretix.base.middleware import LocaleMiddleware from pretix.base.models import Event, Organizer -from pretix.multidomain.urlreverse import get_domain +from pretix.multidomain.urlreverse import ( + get_event_domain, get_organizer_domain, +) from pretix.presale.signals import process_request, process_response SessionStore = import_module(settings.SESSION_ENGINE).SessionStore @@ -31,7 +33,10 @@ def _detect_event(request, require_live=True, require_plugin=None): url = resolve(request.path_info) try: - if hasattr(request, 'organizer_domain'): + if hasattr(request, 'event_domain'): + # We are on an event's custom domain + pass + elif hasattr(request, 'organizer_domain'): # We are on an organizer's custom domain if 'organizer' in url.kwargs and url.kwargs['organizer']: if url.kwargs['organizer'] != request.organizer.slug: @@ -44,6 +49,16 @@ def _detect_event(request, require_live=True, require_plugin=None): organizer=request.organizer, ) request.organizer = request.organizer + + # If this event has a custom domain, send the user there + domain = get_event_domain(request.event) + if domain: + if request.port and request.port not in (80, 443): + domain = '%s:%d' % (domain, request.port) + path = request.get_full_path().split("/", 2)[-1] + r = redirect(urljoin('%s://%s' % (request.scheme, domain), path)) + r['Access-Control-Allow-Origin'] = '*' + return r else: # We are on our main domain if 'event' in url.kwargs and 'organizer' in url.kwargs: @@ -55,6 +70,16 @@ def _detect_event(request, require_live=True, require_plugin=None): organizer__slug=url.kwargs['organizer'] ) request.organizer = request.event.organizer + + # If this event has a custom domain, send the user there + domain = get_event_domain(request.event) + if domain: + if request.port and request.port not in (80, 443): + domain = '%s:%d' % (domain, request.port) + path = request.get_full_path().split("/", 3)[-1] + r = redirect(urljoin('%s://%s' % (request.scheme, domain), path)) + r['Access-Control-Allow-Origin'] = '*' + return r elif 'organizer' in url.kwargs: request.organizer = Organizer.objects.using(db).get( slug=url.kwargs['organizer'] @@ -63,7 +88,7 @@ def _detect_event(request, require_live=True, require_plugin=None): raise Http404() # If this organizer has a custom domain, send the user there - domain = get_domain(request.organizer) + domain = get_organizer_domain(request.organizer) if domain: if request.port and request.port not in (80, 443): domain = '%s:%d' % (domain, request.port) diff --git a/src/tests/multidomain/test_middlewares.py b/src/tests/multidomain/test_middlewares.py index a2afa0b5f2..fd2eb783fd 100644 --- a/src/tests/multidomain/test_middlewares.py +++ b/src/tests/multidomain/test_middlewares.py @@ -26,6 +26,11 @@ def test_control_only_on_main_domain(env, client): assert r.status_code == 302 assert r['Location'] == 'http://example.com/control/login' + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + r = client.get('/control/login', HTTP_HOST='barfoo') + assert r.status_code == 302 + assert r['Location'] == 'http://example.com/control/login' + @pytest.mark.django_db def test_append_slash(env, client): @@ -41,21 +46,30 @@ def test_unknown_domain(env, client): @pytest.mark.django_db -def test_event_on_custom_domain(env, client): +def test_event_on_org_domain(env, client): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) r = client.get('/2015/', HTTP_HOST='foobar') assert r.status_code == 200 + assert b'' in r.content @pytest.mark.django_db -def test_path_without_trailing_slash_on_custom_domain(env, client): +def test_event_on_custom_domain(env, client): + KnownDomain.objects.create(domainname='foobar', organizer=env[0], event=env[1]) + r = client.get('/', HTTP_HOST='foobar') + assert r.status_code == 200 + assert b'' in r.content + + +@pytest.mark.django_db +def test_path_without_trailing_slash_on_org_domain(env, client): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) r = client.get('/widget/product_list', HTTP_HOST='foobar') assert r.status_code == 200 @pytest.mark.django_db -def test_event_with_custom_domain_on_main_domain(env, client): +def test_event_with_org_domain_on_main_domain(env, client): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) r = client.get('/mrmcd/2015/', HTTP_HOST='example.com') assert r.status_code == 302 @@ -63,7 +77,24 @@ def test_event_with_custom_domain_on_main_domain(env, client): @pytest.mark.django_db -def test_organizer_with_custom_domain_on_main_domain(env, client): +def test_event_with_custom_domain_on_main_domain(env, client): + KnownDomain.objects.create(domainname='foobar', organizer=env[0], event=env[1]) + r = client.get('/mrmcd/2015/', HTTP_HOST='example.com') + assert r.status_code == 302 + assert r['Location'] == 'http://foobar' + + +@pytest.mark.django_db +def test_event_with_custom_domain_on_org_domain(env, client): + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + r = client.get('/2015/', HTTP_HOST='foobar') + assert r.status_code == 302 + assert r['Location'] == 'http://barfoo' + + +@pytest.mark.django_db +def test_organizer_with_org_domain_on_main_domain(env, client): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) r = client.get('/mrmcd/', HTTP_HOST='example.com') assert r.status_code == 302 @@ -71,7 +102,7 @@ def test_organizer_with_custom_domain_on_main_domain(env, client): @pytest.mark.django_db -def test_event_on_custom_domain_only_with_wrong_organizer(env, client): +def test_event_on_org_domain_only_with_wrong_organizer(env, client): organizer2 = Organizer.objects.create(name='Dummy', slug='dummy') Event.objects.create( organizer=organizer2, name='D1234', slug='1234', @@ -83,7 +114,7 @@ def test_event_on_custom_domain_only_with_wrong_organizer(env, client): @pytest.mark.django_db -def test_unknown_event_on_custom_domain(env, client): +def test_unknown_event_on_org_domain(env, client): organizer2 = Organizer.objects.create(name='Dummy', slug='dummy') Event.objects.create( organizer=organizer2, name='D1234', slug='1234', @@ -95,7 +126,7 @@ def test_unknown_event_on_custom_domain(env, client): @pytest.mark.django_db -def test_cookie_domain_on_custom_domain(env, client): +def test_cookie_domain_on_org_domain(env, client): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) client.post('/2015/cart/add', HTTP_HOST='foobar') r = client.get('/2015/', HTTP_HOST='foobar') @@ -103,6 +134,16 @@ def test_cookie_domain_on_custom_domain(env, client): assert r.client.cookies['pretix_session']['domain'] == '' +@pytest.mark.django_db +def test_cookie_domain_on_event_domain(env, client): + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + client.post('/cart/add', HTTP_HOST='barfoo') + r = client.get('/', HTTP_HOST='barfoo') + assert r.client.cookies['pretix_csrftoken']['domain'] == '' + assert r.client.cookies['pretix_session']['domain'] == '' + + @pytest.mark.django_db def test_cookie_domain_on_main_domain(env, client): with override_settings(SESSION_COOKIE_DOMAIN='example.com'): @@ -113,12 +154,11 @@ def test_cookie_domain_on_main_domain(env, client): @pytest.mark.django_db +@override_settings(USE_X_FORWARDED_HOST=True) def test_with_forwarded_host(env, client): - settings.USE_X_FORWARDED_HOST = True KnownDomain.objects.create(domainname='foobar', organizer=env[0]) r = client.get('/2015/', HTTP_X_FORWARDED_HOST='foobar') assert r.status_code == 200 - settings.USE_X_FORWARDED_HOST = False @pytest.mark.django_db diff --git a/src/tests/multidomain/test_urlreverse.py b/src/tests/multidomain/test_urlreverse.py index 7af87f79ea..67f3f822dd 100644 --- a/src/tests/multidomain/test_urlreverse.py +++ b/src/tests/multidomain/test_urlreverse.py @@ -30,6 +30,13 @@ def test_event_main_domain_front_page(env): @pytest.mark.django_db def test_event_custom_domain_kwargs(env): + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + assert eventreverse(env[1], 'presale:event.checkout', {'step': 'payment'}) == 'http://barfoo/checkout/payment/' + + +@pytest.mark.django_db +def test_event_org_domain_kwargs(env): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) assert eventreverse(env[1], 'presale:event.checkout', {'step': 'payment'}) == 'http://foobar/2015/checkout/payment/' @@ -40,21 +47,36 @@ def test_event_main_domain_kwargs(env): @pytest.mark.django_db -def test_event_custom_domain_front_page(env): +def test_event_org_domain_front_page(env): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) assert eventreverse(env[1], 'presale:event.index') == 'http://foobar/2015/' assert eventreverse(env[0], 'presale:organizer.index') == 'http://foobar/' @pytest.mark.django_db -def test_event_custom_domain_keep_port(env): +def test_event_custom_domain_front_page(env): + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + assert eventreverse(env[1], 'presale:event.index') == 'http://barfoo/' + assert eventreverse(env[0], 'presale:organizer.index') == '/mrmcd/' + + +@pytest.mark.django_db +def test_event_custom_and_org_domain_front_page(env): + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + assert eventreverse(env[1], 'presale:event.index') == 'http://barfoo/' + assert eventreverse(env[0], 'presale:organizer.index') == 'http://foobar/' + + +@pytest.mark.django_db +def test_event_org_domain_keep_port(env): settings.SITE_URL = 'http://example.com:8081' KnownDomain.objects.create(domainname='foobar', organizer=env[0]) assert eventreverse(env[1], 'presale:event.index') == 'http://foobar:8081/2015/' @pytest.mark.django_db -def test_event_custom_domain_keep_scheme(env): +def test_event_org_domain_keep_scheme(env): settings.SITE_URL = 'https://example.com' KnownDomain.objects.create(domainname='foobar', organizer=env[0]) assert eventreverse(env[1], 'presale:event.index') == 'https://foobar/2015/' @@ -82,7 +104,7 @@ def test_event_main_domain_cache(env): 'LOCATION': 'unique-snowflake', } }) -def test_event_custom_domain_cache(env): +def test_event_org_domain_cache(env): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) env[0].get_cache().clear() with assert_num_queries(1): @@ -91,6 +113,45 @@ def test_event_custom_domain_cache(env): eventreverse(env[1], 'presale:event.index') +@pytest.mark.django_db +@override_settings(CACHES={ + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake', + } +}) +def test_event_custom_domain_cache(env): + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + env[0].get_cache().clear() + with assert_num_queries(1): + eventreverse(env[1], 'presale:event.index') + with assert_num_queries(0): + eventreverse(env[1], 'presale:event.index') + + +@pytest.mark.django_db +@override_settings(CACHES={ + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake', + } +}) +@scopes_disabled() +def test_event_org_domain_cache_clear(env): + kd = KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + env[0].cache.clear() + with assert_num_queries(1): + eventreverse(env[1], 'presale:event.index') + kd.delete() + with assert_num_queries(2): + ev = Event.objects.get(pk=env[1].pk) + assert ev.pk == env[1].pk + assert ev.organizer == env[0] + with assert_num_queries(1): + eventreverse(ev, 'presale:event.index') + + @pytest.mark.django_db @override_settings(CACHES={ 'default': { @@ -100,7 +161,8 @@ def test_event_custom_domain_cache(env): }) @scopes_disabled() def test_event_custom_domain_cache_clear(env): - kd = KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + kd = KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) env[0].cache.clear() with assert_num_queries(1): eventreverse(env[1], 'presale:event.index') @@ -120,5 +182,12 @@ def test_event_main_domain_absolute(env): @pytest.mark.django_db def test_event_custom_domain_absolute(env): + KnownDomain.objects.create(domainname='foobar', organizer=env[0]) + KnownDomain.objects.create(domainname='barfoo', organizer=env[0], event=env[1]) + assert build_absolute_uri(env[1], 'presale:event.index') == 'http://barfoo/' + + +@pytest.mark.django_db +def test_event_org_domain_absolute(env): KnownDomain.objects.create(domainname='foobar', organizer=env[0]) assert build_absolute_uri(env[1], 'presale:event.index') == 'http://foobar/2015/'