django-multisite2 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_multisite2-2.0.0.dist-info/LICENSE +28 -0
- django_multisite2-2.0.0.dist-info/METADATA +246 -0
- django_multisite2-2.0.0.dist-info/RECORD +51 -0
- django_multisite2-2.0.0.dist-info/WHEEL +5 -0
- django_multisite2-2.0.0.dist-info/top_level.txt +2 -0
- multisite/__init__.py +1 -0
- multisite/admin/__init__.py +3 -0
- multisite/admin/alias_admin.py +58 -0
- multisite/admin/multisite_changelist.py +64 -0
- multisite/admin/multisite_model_admin.py +161 -0
- multisite/apps.py +29 -0
- multisite/exceptions.py +57 -0
- multisite/forms.py +22 -0
- multisite/hacks.py +163 -0
- multisite/management/__init__.py +0 -0
- multisite/management/commands/__init__.py +0 -0
- multisite/management/commands/update_public_suffix_list.py +33 -0
- multisite/middleware/__init__.py +4 -0
- multisite/middleware/cookie_domain_middleware.py +72 -0
- multisite/middleware/dynamic_site_middleware.py +198 -0
- multisite/migrations/0001_initial.py +71 -0
- multisite/migrations/0002_alter_alias_id_alter_alias_is_canonical.py +32 -0
- multisite/migrations/0003_alter_alias_options_alter_alias_unique_together_and_more.py +41 -0
- multisite/migrations/0004_alter_alias_managers.py +22 -0
- multisite/migrations/__init__.py +0 -0
- multisite/models/__init__.py +3 -0
- multisite/models/alias.py +110 -0
- multisite/models/managers.py +196 -0
- multisite/models/signals.py +36 -0
- multisite/models/validators.py +14 -0
- multisite/tests/__init__.py +0 -0
- multisite/tests/get_test_allowed_hosts.py +14 -0
- multisite/tests/get_test_http_response.py +13 -0
- multisite/tests/hosts.py +46 -0
- multisite/tests/test_settings.py +23 -0
- multisite/tests/tests/__init__.py +0 -0
- multisite/tests/tests/request_factory.py +12 -0
- multisite/tests/tests/test_alias.py +271 -0
- multisite/tests/tests/test_contrib_site.py +21 -0
- multisite/tests/tests/test_cookie_domain_middleware.py +196 -0
- multisite/tests/tests/test_dynamic_site_middleware.py +262 -0
- multisite/tests/tests/test_site_cache.py +127 -0
- multisite/tests/tests/test_site_domain.py +24 -0
- multisite/tests/tests/test_site_id.py +76 -0
- multisite/tests/tests/test_template_loader.py +49 -0
- multisite/threadlocals.py +136 -0
- multisite/utils.py +172 -0
- multisite_app/__init__.py +0 -0
- multisite_app/apps.py +6 -0
- multisite_app/urls.py +5 -0
- multisite_app/views.py +8 -0
multisite/exceptions.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MultisiteError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MultisiteSiteDoesNotExist(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MultisiteCacheError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MultisiteFallbackError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MultisiteDisallowedHost(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MultisiteInvalidHostError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MultisiteServerError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MultisiteCookieDomainDepthError(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def debug_raise_disallowed_host_exception(e):
|
|
37
|
+
if getattr(settings, "MULTISITE_DEBUG", None):
|
|
38
|
+
raise MultisiteDisallowedHost(
|
|
39
|
+
f"DisallowedHost. To silence this exception, set MULTISITE_DEBUG=False. Got {e}."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def debug_raise_cache_missed_exception(netloc, alias):
|
|
44
|
+
if getattr(settings, "MULTISITE_DEBUG", None):
|
|
45
|
+
raise MultisiteCacheError(
|
|
46
|
+
f"Cache missed. Got location {netloc} and Alias {alias}. "
|
|
47
|
+
"To silence this exception, set MULTISITE_DEBUG=False."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def debug_check_status_code(response, **kwargs) -> None:
|
|
52
|
+
if response.status_code == 500 and getattr(settings, "MULTISITE_DEBUG", None):
|
|
53
|
+
raise MultisiteServerError(
|
|
54
|
+
f"HttpResponse 500. {kwargs}. Got {response}."
|
|
55
|
+
"To silence this exception, set MULTISITE_DEBUG=False."
|
|
56
|
+
)
|
|
57
|
+
return None
|
multisite/forms.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import absolute_import, unicode_literals
|
|
2
|
+
|
|
3
|
+
from django.contrib.sites.admin import SiteAdmin
|
|
4
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
5
|
+
|
|
6
|
+
from .models import Alias
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SiteForm(SiteAdmin.form):
|
|
10
|
+
def clean_domain(self):
|
|
11
|
+
domain = self.cleaned_data["domain"]
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
alias = Alias.objects.get(domain=domain)
|
|
15
|
+
except ObjectDoesNotExist:
|
|
16
|
+
# New Site that doesn't clobber an Alias
|
|
17
|
+
return domain
|
|
18
|
+
|
|
19
|
+
if alias.site_id == self.instance.pk and alias.is_canonical:
|
|
20
|
+
return domain
|
|
21
|
+
|
|
22
|
+
raise ValidationError('Cannot overwrite non-canonical Alias: "%s"' % alias.domain)
|
multisite/hacks.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
7
|
+
from django.db.models.signals import post_delete, post_save
|
|
8
|
+
|
|
9
|
+
__all__ = ["use_framework_for_site_cache"]
|
|
10
|
+
|
|
11
|
+
from multisite.exceptions import MultisiteSiteDoesNotExist
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def use_framework_for_site_cache():
|
|
15
|
+
"""Patches sites app to use the caching framework instead of a dict.
|
|
16
|
+
|
|
17
|
+
This patch has to exist because SITE_CACHE is normally a dict,
|
|
18
|
+
which is local only to the process. When running multiple
|
|
19
|
+
processes, a change to a Site will not be reflected across other
|
|
20
|
+
ones.
|
|
21
|
+
"""
|
|
22
|
+
from django.contrib.sites import models
|
|
23
|
+
|
|
24
|
+
# Patch the SITE_CACHE
|
|
25
|
+
site_cache = SiteCache()
|
|
26
|
+
models.SITE_CACHE = DictCache(site_cache)
|
|
27
|
+
|
|
28
|
+
# Patch the SiteManager class
|
|
29
|
+
models.SiteManager.clear_cache = site_manager_clear_cache
|
|
30
|
+
models.SiteManager._get_site_by_id = site_manager_get_site_by_id
|
|
31
|
+
|
|
32
|
+
# Hooks to update SiteCache
|
|
33
|
+
post_save.connect(site_cache._site_changed_hook, sender=models.Site)
|
|
34
|
+
post_delete.connect(site_cache._site_deleted_hook, sender=models.Site)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def site_manager_clear_cache(self):
|
|
38
|
+
"""Clears the ``Site`` object cache.
|
|
39
|
+
|
|
40
|
+
Override `SiteManager.clear_cache` so it doesn't clobber
|
|
41
|
+
SITE_CACHE
|
|
42
|
+
"""
|
|
43
|
+
models = sys.modules.get(self.__class__.__module__)
|
|
44
|
+
models.SITE_CACHE.clear()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def site_manager_get_site_by_id(self, site_id):
|
|
48
|
+
"""Patch _get_site_by_id to retrieve the site from the cache at
|
|
49
|
+
the beginning of the method to avoid a race condition.
|
|
50
|
+
|
|
51
|
+
Override SiteManager._get_site_by_id
|
|
52
|
+
"""
|
|
53
|
+
models = sys.modules.get(self.__class__.__module__)
|
|
54
|
+
site = models.SITE_CACHE.get(site_id)
|
|
55
|
+
if site is None:
|
|
56
|
+
try:
|
|
57
|
+
site = self.get(pk=site_id)
|
|
58
|
+
except ObjectDoesNotExist as e:
|
|
59
|
+
raise MultisiteSiteDoesNotExist(f"{e} Got site_id={site_id}.")
|
|
60
|
+
models.SITE_CACHE[site_id] = site
|
|
61
|
+
return site
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SiteCache:
|
|
65
|
+
"""Wrapper for SITE_CACHE that assigns a key_prefix."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, cache=None):
|
|
68
|
+
from django.core.cache import caches
|
|
69
|
+
|
|
70
|
+
if cache is None:
|
|
71
|
+
cache_alias = getattr(settings, "CACHE_MULTISITE_ALIAS", "default")
|
|
72
|
+
self._key_prefix = getattr(
|
|
73
|
+
settings,
|
|
74
|
+
"CACHE_MULTISITE_KEY_PREFIX",
|
|
75
|
+
settings.CACHES[cache_alias].get("KEY_PREFIX", ""),
|
|
76
|
+
)
|
|
77
|
+
cache = caches[cache_alias]
|
|
78
|
+
else:
|
|
79
|
+
self._key_prefix = getattr(
|
|
80
|
+
settings, "CACHE_MULTISITE_KEY_PREFIX", cache.key_prefix
|
|
81
|
+
)
|
|
82
|
+
self._cache = cache
|
|
83
|
+
|
|
84
|
+
def _get_cache_key(self, key):
|
|
85
|
+
return "sites.%s.%s" % (self.key_prefix, key)
|
|
86
|
+
|
|
87
|
+
def _clean_site(self, site):
|
|
88
|
+
# Force site.id to be an int, not a SiteID object.
|
|
89
|
+
site.id = int(site.id)
|
|
90
|
+
return site
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def key_prefix(self):
|
|
94
|
+
return self._key_prefix
|
|
95
|
+
|
|
96
|
+
def get(self, key, *args, **kwargs):
|
|
97
|
+
return self._cache.get(key=self._get_cache_key(key), *args, **kwargs)
|
|
98
|
+
|
|
99
|
+
def set(self, key, value, *args, **kwargs):
|
|
100
|
+
self._cache.set(
|
|
101
|
+
key=self._get_cache_key(key), value=self._clean_site(value), *args, **kwargs
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def delete(self, key, *args, **kwargs):
|
|
105
|
+
self._cache.delete(key=self._get_cache_key(key), *args, **kwargs)
|
|
106
|
+
|
|
107
|
+
def __contains__(self, key, *args, **kwargs):
|
|
108
|
+
return self._cache.__contains__(key=self._get_cache_key(key), *args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def clear(self, *args, **kwargs):
|
|
111
|
+
self._cache.clear(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
def _site_changed_hook(self, sender, instance, raw, *args, **kwargs):
|
|
114
|
+
if raw:
|
|
115
|
+
return
|
|
116
|
+
self.set(key=instance.pk, value=instance)
|
|
117
|
+
|
|
118
|
+
def _site_deleted_hook(self, sender, instance, *args, **kwargs):
|
|
119
|
+
self.delete(key=instance.pk)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DictCache:
|
|
123
|
+
"""Add dictionary protocol to `django.core.cache.backends.
|
|
124
|
+
BaseCache`.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, cache):
|
|
128
|
+
self._cache = cache
|
|
129
|
+
|
|
130
|
+
def __getitem__(self, key):
|
|
131
|
+
"""x.__getitem__(y) <==> x[y]"""
|
|
132
|
+
hash(key) # Raise TypeError if unhashable
|
|
133
|
+
result = self._cache.get(key=key)
|
|
134
|
+
if result is None:
|
|
135
|
+
raise KeyError(key)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def __setitem__(self, key, value):
|
|
139
|
+
"""x.__setitem__(i, y) <==> x[i]=y"""
|
|
140
|
+
hash(key) # Raise TypeError if unhashable
|
|
141
|
+
self._cache.set(key=key, value=value)
|
|
142
|
+
|
|
143
|
+
def __delitem__(self, key):
|
|
144
|
+
"""x.__delitem__(y) <==> del x[y]"""
|
|
145
|
+
hash(key) # Raise TypeError if unhashable
|
|
146
|
+
self._cache.delete(key=key)
|
|
147
|
+
|
|
148
|
+
def __contains__(self, item):
|
|
149
|
+
"""D.__contains__(k) -> True if D has a key k, else False."""
|
|
150
|
+
hash(item) # Raise TypeError if unhashable
|
|
151
|
+
return self._cache.__contains__(key=item)
|
|
152
|
+
|
|
153
|
+
def clear(self):
|
|
154
|
+
"""D.clear() -> None. Remove all items from D."""
|
|
155
|
+
self._cache.clear()
|
|
156
|
+
|
|
157
|
+
def get(self, key, default=None, version=None):
|
|
158
|
+
"""D.key(k[, d]) -> k if D has a key k, else d.
|
|
159
|
+
|
|
160
|
+
Defaults to None.
|
|
161
|
+
"""
|
|
162
|
+
hash(key) # Raise TypeError if unhashable
|
|
163
|
+
return self._cache.get(key=key, default=default, version=version)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
import tldextract
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.core.management.base import BaseCommand
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
def handle(self, **options):
|
|
13
|
+
self.setup_logging(verbosity=options.get("verbosity", 1))
|
|
14
|
+
|
|
15
|
+
cache_dir = getattr(
|
|
16
|
+
settings, "MULTISITE_PUBLIC_SUFFIX_LIST_CACHE_DIR", tempfile.gettempdir()
|
|
17
|
+
)
|
|
18
|
+
self.log("Updating {cache_dir}".format(cache_dir=cache_dir))
|
|
19
|
+
|
|
20
|
+
extract = tldextract.TLDExtract(cache_dir=cache_dir)
|
|
21
|
+
extract.update(fetch_now=True)
|
|
22
|
+
self.log("Done.")
|
|
23
|
+
|
|
24
|
+
def setup_logging(self, verbosity):
|
|
25
|
+
self.verbosity = int(verbosity)
|
|
26
|
+
|
|
27
|
+
# Connect to tldextract's logger
|
|
28
|
+
self.logger = logging.getLogger("tldextract")
|
|
29
|
+
if self.verbosity < 2:
|
|
30
|
+
self.logger.setLevel(logging.CRITICAL)
|
|
31
|
+
|
|
32
|
+
def log(self, msg):
|
|
33
|
+
self.logger.info(msg)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
import tldextract
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
|
|
8
|
+
__all__ = ["CookieDomainMiddleware"]
|
|
9
|
+
|
|
10
|
+
from multisite.exceptions import MultisiteCookieDomainDepthError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cookie_domain_depth():
|
|
14
|
+
return getattr(settings, "MULTISITE_COOKIE_DOMAIN_DEPTH", 0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_public_suffix_list_cache_dir():
|
|
18
|
+
return getattr(settings, "MULTISITE_PUBLIC_SUFFIX_LIST_CACHE_DIR", None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CookieDomainMiddleware:
|
|
22
|
+
def __init__(self, get_response):
|
|
23
|
+
self.get_response = get_response
|
|
24
|
+
try:
|
|
25
|
+
self.depth = int(get_cookie_domain_depth())
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise MultisiteCookieDomainDepthError(
|
|
28
|
+
f"Invalid MULTISITE_COOKIE_DOMAIN_DEPTH. Got {get_cookie_domain_depth()}"
|
|
29
|
+
)
|
|
30
|
+
if self.depth < 0:
|
|
31
|
+
raise MultisiteCookieDomainDepthError(
|
|
32
|
+
f"Invalid MULTISITE_COOKIE_DOMAIN_DEPTH. Got {get_cookie_domain_depth()}"
|
|
33
|
+
)
|
|
34
|
+
self.psl_cache_dir = get_public_suffix_list_cache_dir()
|
|
35
|
+
if self.psl_cache_dir is None:
|
|
36
|
+
self.psl_cache_dir = tempfile.gettempdir()
|
|
37
|
+
self._tldextract = None
|
|
38
|
+
|
|
39
|
+
def __call__(self, request):
|
|
40
|
+
response = self.get_response(request)
|
|
41
|
+
matched = self.match_cookies(response)
|
|
42
|
+
if not matched:
|
|
43
|
+
return response # No cookies to edit
|
|
44
|
+
|
|
45
|
+
parsed = self.tldextract(request.get_host())
|
|
46
|
+
if not parsed.suffix:
|
|
47
|
+
return response # IP address or local path
|
|
48
|
+
if not parsed.domain:
|
|
49
|
+
return response # Only TLD
|
|
50
|
+
|
|
51
|
+
subdomains = parsed.subdomain.split(".") if parsed.subdomain else []
|
|
52
|
+
if not self.depth:
|
|
53
|
+
subdomains = [""]
|
|
54
|
+
elif len(subdomains) < self.depth:
|
|
55
|
+
return response # Not enough subdomain parts
|
|
56
|
+
else:
|
|
57
|
+
subdomains = [""] + subdomains[-self.depth :]
|
|
58
|
+
|
|
59
|
+
domain = ".".join(subdomains + [parsed.domain, parsed.suffix])
|
|
60
|
+
|
|
61
|
+
for morsel in matched:
|
|
62
|
+
morsel["domain"] = domain
|
|
63
|
+
return response
|
|
64
|
+
|
|
65
|
+
def tldextract(self, url):
|
|
66
|
+
if self._tldextract is None:
|
|
67
|
+
self._tldextract = tldextract.TLDExtract(cache_dir=self.psl_cache_dir)
|
|
68
|
+
return self._tldextract(url)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def match_cookies(response):
|
|
72
|
+
return [c for c in response.cookies.values() if not c["domain"]]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from hashlib import md5 as md5_constructor
|
|
4
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.contrib.sites.models import SITE_CACHE, Site
|
|
8
|
+
from django.core import mail
|
|
9
|
+
from django.core.cache import caches
|
|
10
|
+
from django.core.exceptions import DisallowedHost
|
|
11
|
+
from django.db.models.signals import post_delete, post_init, pre_save
|
|
12
|
+
from django.http import HttpResponse, HttpResponsePermanentRedirect
|
|
13
|
+
|
|
14
|
+
from ..exceptions import (
|
|
15
|
+
MultisiteError,
|
|
16
|
+
MultisiteInvalidHostError,
|
|
17
|
+
debug_check_status_code,
|
|
18
|
+
debug_raise_cache_missed_exception,
|
|
19
|
+
debug_raise_disallowed_host_exception,
|
|
20
|
+
)
|
|
21
|
+
from ..models import Alias
|
|
22
|
+
|
|
23
|
+
__all__ = ["DynamicSiteMiddleware"]
|
|
24
|
+
|
|
25
|
+
from ..utils import (
|
|
26
|
+
get_cache_multisite_alias,
|
|
27
|
+
get_cache_multisite_prefix,
|
|
28
|
+
get_fallback_kwargs,
|
|
29
|
+
get_fallback_view_or_404,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DynamicSiteMiddleware:
|
|
34
|
+
def __init__(self, get_response):
|
|
35
|
+
self.get_response = get_response
|
|
36
|
+
if not hasattr(settings.SITE_ID, "set"):
|
|
37
|
+
raise MultisiteError(
|
|
38
|
+
"Invalid type for settings.SITE_ID: %s" % type(settings.SITE_ID).__name__
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.cache_alias = get_cache_multisite_alias()
|
|
42
|
+
self.key_prefix = get_cache_multisite_prefix() or settings.CACHES[
|
|
43
|
+
self.cache_alias
|
|
44
|
+
].get("KEY_PREFIX", "")
|
|
45
|
+
|
|
46
|
+
self.cache = caches[self.cache_alias]
|
|
47
|
+
post_init.connect(
|
|
48
|
+
self.site_domain_cache_hook, sender=Site, dispatch_uid="multisite_post_init"
|
|
49
|
+
)
|
|
50
|
+
pre_save.connect(self.site_domain_changed_hook, sender=Site)
|
|
51
|
+
post_delete.connect(self.site_deleted_hook, sender=Site)
|
|
52
|
+
|
|
53
|
+
def __call__(self, request):
|
|
54
|
+
# if 500 here -> Site ObjectDoesNotExist was raised by get_current_site()
|
|
55
|
+
# debug_check_status_code(response, request=request)
|
|
56
|
+
try:
|
|
57
|
+
netloc = request.get_host().lower()
|
|
58
|
+
except DisallowedHost as e:
|
|
59
|
+
debug_raise_disallowed_host_exception(e)
|
|
60
|
+
settings.SITE_ID.reset()
|
|
61
|
+
response = self.fallback_view(request)
|
|
62
|
+
else:
|
|
63
|
+
cache_key = self.get_cache_key(netloc)
|
|
64
|
+
if (alias := self.cache.get(cache_key)) is not None:
|
|
65
|
+
# found Alias
|
|
66
|
+
self.cache.set(cache_key, alias)
|
|
67
|
+
settings.SITE_ID.set(alias.site_id)
|
|
68
|
+
response = self.redirect_to_canonical(request, alias)
|
|
69
|
+
elif (alias := self.get_alias(netloc)) is None:
|
|
70
|
+
# Cache missed, fallback using settings.MULTISITE_FALLBACK
|
|
71
|
+
debug_raise_cache_missed_exception(netloc, alias)
|
|
72
|
+
settings.SITE_ID.reset()
|
|
73
|
+
response = self.fallback_view(request)
|
|
74
|
+
else:
|
|
75
|
+
# found Site
|
|
76
|
+
self.cache.set(cache_key, alias)
|
|
77
|
+
settings.SITE_ID.set(alias.site_id)
|
|
78
|
+
SITE_CACHE[settings.SITE_ID] = alias.site # Pre-populate SITE_CACHE
|
|
79
|
+
response = self.redirect_to_canonical(request, alias)
|
|
80
|
+
# debug_check_status_code(response, netloc=netloc, alias=alias, site=alias.site)
|
|
81
|
+
return response or self.get_response(request)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def redirect_to_canonical(request, alias: Alias) -> HttpResponse | None:
|
|
85
|
+
if not alias.redirect_to_canonical or alias.is_canonical:
|
|
86
|
+
response = None
|
|
87
|
+
else:
|
|
88
|
+
url = urlsplit(request.build_absolute_uri(request.get_full_path()))
|
|
89
|
+
url = urlunsplit(
|
|
90
|
+
(url.scheme, alias.site.domain, url.path, url.query, url.fragment)
|
|
91
|
+
)
|
|
92
|
+
response = HttpResponsePermanentRedirect(url)
|
|
93
|
+
return response
|
|
94
|
+
|
|
95
|
+
def get_cache_key(self, netloc):
|
|
96
|
+
"""Returns a cache key based on ``netloc``."""
|
|
97
|
+
netloc = md5_constructor(netloc.encode("utf-8"), usedforsecurity=False)
|
|
98
|
+
return "multisite.alias.%s.%s" % (self.key_prefix, netloc.hexdigest())
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def netloc_parse(netloc):
|
|
102
|
+
"""Returns ``(host, port)`` for ``netloc`` of the form
|
|
103
|
+
``'host:port'``.
|
|
104
|
+
|
|
105
|
+
If netloc does not have a port number, ``port`` will be None.
|
|
106
|
+
"""
|
|
107
|
+
if ":" in netloc:
|
|
108
|
+
return netloc.rsplit(":", 1)
|
|
109
|
+
else:
|
|
110
|
+
return netloc, None
|
|
111
|
+
|
|
112
|
+
def get_alias(self, netloc) -> Alias | None:
|
|
113
|
+
"""Returns Alias matching ``netloc``. Otherwise,
|
|
114
|
+
returns None.
|
|
115
|
+
"""
|
|
116
|
+
host, port = self.netloc_parse(netloc)
|
|
117
|
+
try:
|
|
118
|
+
alias = Alias.objects.resolve(host=host, port=port)
|
|
119
|
+
except MultisiteInvalidHostError:
|
|
120
|
+
alias = None
|
|
121
|
+
if alias is None:
|
|
122
|
+
# Running under TestCase or runserver?
|
|
123
|
+
alias = self.get_development_alias(netloc)
|
|
124
|
+
return alias
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def fallback_view(request) -> HttpResponse:
|
|
128
|
+
"""Runs the fallback view function in
|
|
129
|
+
``settings.MULTISITE_FALLBACK``.
|
|
130
|
+
|
|
131
|
+
If ``MULTISITE_FALLBACK`` is None, raises an Http404 error.
|
|
132
|
+
|
|
133
|
+
If ``MULTISITE_FALLBACK`` is callable, will treat that
|
|
134
|
+
callable as a view that returns an HttpResponse.
|
|
135
|
+
|
|
136
|
+
If ``MULTISITE_FALLBACK`` is a string, will resolve it to a
|
|
137
|
+
view that returns an HttpResponse.
|
|
138
|
+
|
|
139
|
+
In order to use a generic view that takes additional
|
|
140
|
+
parameters, ``settings.MULTISITE_FALLBACK_KWARGS`` may be a
|
|
141
|
+
dictionary of additional keyword arguments.
|
|
142
|
+
"""
|
|
143
|
+
view = get_fallback_view_or_404()
|
|
144
|
+
kwargs = get_fallback_kwargs()
|
|
145
|
+
if hasattr(view, "as_view"):
|
|
146
|
+
return view.as_view(**kwargs)(request)
|
|
147
|
+
return view(request, **kwargs)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def get_development_alias(netloc) -> Alias:
|
|
151
|
+
"""Returns valid Alias when in development mode. Otherwise,
|
|
152
|
+
returns None.
|
|
153
|
+
|
|
154
|
+
Development mode is either:
|
|
155
|
+
- Running tests, i.e. manage.py test
|
|
156
|
+
- Running locally in `settings.DEBUG` = True, where the
|
|
157
|
+
hostname is a top-level name, i.e. localhost
|
|
158
|
+
"""
|
|
159
|
+
alias = None
|
|
160
|
+
# When running tests, django.core.mail.outbox exists and
|
|
161
|
+
# netloc == 'testserver'
|
|
162
|
+
is_testserver = hasattr(mail, "outbox") and netloc in (
|
|
163
|
+
"testserver",
|
|
164
|
+
"adminsite.com",
|
|
165
|
+
)
|
|
166
|
+
# When using runserver, assume that host will only have one path
|
|
167
|
+
# component. This covers 'localhost' and your machine name.
|
|
168
|
+
is_local_debug = settings.DEBUG and len(netloc.split(".")) == 1
|
|
169
|
+
if is_testserver or is_local_debug:
|
|
170
|
+
try:
|
|
171
|
+
# Prefer the default SITE_ID
|
|
172
|
+
site_id = settings.SITE_ID.get_default()
|
|
173
|
+
except ValueError:
|
|
174
|
+
# Fallback to the first Site object
|
|
175
|
+
alias = Alias.canonical.order_by("site")[0]
|
|
176
|
+
else:
|
|
177
|
+
alias = Alias.canonical.get(site=site_id)
|
|
178
|
+
return alias
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def site_domain_cache_hook(cls, sender, instance, *args, **kwargs):
|
|
182
|
+
"""Caches `Site.domain` in the object for
|
|
183
|
+
site_domain_changed_hook.
|
|
184
|
+
"""
|
|
185
|
+
instance._domain_cache = instance.domain
|
|
186
|
+
|
|
187
|
+
def site_domain_changed_hook(self, sender, instance, raw, *args, **kwargs):
|
|
188
|
+
"""Clears the cache if `Site.domain` has changed."""
|
|
189
|
+
if raw or instance.pk is None:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
original = getattr(instance, "_domain_cache", None)
|
|
193
|
+
if original != instance.domain:
|
|
194
|
+
self.cache.clear()
|
|
195
|
+
|
|
196
|
+
def site_deleted_hook(self, *args, **kwargs):
|
|
197
|
+
"""Clears the cache if Site was deleted."""
|
|
198
|
+
self.cache.clear()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, unicode_literals
|
|
3
|
+
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
import multisite.models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
dependencies = [
|
|
11
|
+
("sites", "0001_initial"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="Alias",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.AutoField(
|
|
21
|
+
verbose_name="ID",
|
|
22
|
+
serialize=False,
|
|
23
|
+
auto_created=True,
|
|
24
|
+
primary_key=True,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"domain",
|
|
29
|
+
models.CharField(
|
|
30
|
+
help_text='Either "domain" or "domain:port"',
|
|
31
|
+
unique=True,
|
|
32
|
+
max_length=100,
|
|
33
|
+
verbose_name="domain name",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"is_canonical",
|
|
38
|
+
models.NullBooleanField(
|
|
39
|
+
default=None,
|
|
40
|
+
validators=[multisite.models.validators.validate_true_or_none],
|
|
41
|
+
editable=False,
|
|
42
|
+
help_text="Does this domain name match the one in site?",
|
|
43
|
+
verbose_name="is canonical?",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"redirect_to_canonical",
|
|
48
|
+
models.BooleanField(
|
|
49
|
+
default=True,
|
|
50
|
+
help_text="Should this domain name redirect to the one in site?",
|
|
51
|
+
verbose_name="redirect to canonical?",
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"site",
|
|
56
|
+
models.ForeignKey(
|
|
57
|
+
related_name="aliases",
|
|
58
|
+
to="sites.Site",
|
|
59
|
+
on_delete=models.CASCADE,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
options={
|
|
64
|
+
"verbose_name_plural": "aliases",
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
migrations.AlterUniqueTogether(
|
|
68
|
+
name="alias",
|
|
69
|
+
unique_together=set([("is_canonical", "site")]),
|
|
70
|
+
),
|
|
71
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated by Django 4.0.4 on 2022-05-12 23:39
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
import multisite.models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("multisite", "0001_initial"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterField(
|
|
15
|
+
model_name="alias",
|
|
16
|
+
name="id",
|
|
17
|
+
field=models.BigAutoField(
|
|
18
|
+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
migrations.AlterField(
|
|
22
|
+
model_name="alias",
|
|
23
|
+
name="is_canonical",
|
|
24
|
+
field=models.BooleanField(
|
|
25
|
+
default=False,
|
|
26
|
+
editable=False,
|
|
27
|
+
help_text="Does this domain name match the one in site?",
|
|
28
|
+
validators=[multisite.models.validators.validate_true_or_none],
|
|
29
|
+
verbose_name="is canonical?",
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
]
|