Skip to content

Commit 23ccad1

Browse files
committed
fix: Use explicit site query parameter in page admin
In order to not rely on the 'current site' or an opaque site set in the session, this relies whenever possible on the page's site, falling back to a site query parameter when a page is not available. This query parameter needs to be passed around similar to the language parameter but can fall back to current site if not present initially. Permission to access the chosen site is checked. The default site filter on get_queryset is removed, as it can break get_object by filtering the wrong site. Other listing views do their own site filtering. A graceful transition from the site in session is available by redirecting to a site query parameter when necessary.
1 parent 43db3f3 commit 23ccad1

File tree

10 files changed

+214
-115
lines changed

10 files changed

+214
-115
lines changed

cms/admin/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ class PageTreeForm(forms.Form):
905905

906906
def __init__(self, *args, **kwargs):
907907
self.page = kwargs.pop("page")
908-
self._site = kwargs.pop("site", Site.objects.get_current())
908+
self._site = kwargs.pop("site", self.page.site)
909909
super().__init__(*args, **kwargs)
910910
self.fields["target"].queryset = Page.objects.filter(
911911
site=self._site,

cms/admin/pageadmin.py

Lines changed: 37 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
MovePageForm,
5353
)
5454
from cms.admin.permissionadmin import PERMISSION_ADMIN_INLINES
55+
from cms.admin.site_utils import get_site, get_site_from_request, get_sites_for_user, needs_site_redirect
5556
from cms.cache.permissions import clear_permission_cache
5657
from cms.constants import MODAL_HTML_REDIRECT
5758
from cms.models import (
@@ -86,19 +87,6 @@
8687
require_POST = method_decorator(require_POST)
8788

8889

89-
def get_site(request):
90-
site_id = request.session.get("cms_admin_site")
91-
92-
if not site_id:
93-
return get_current_site()
94-
95-
try:
96-
site = Site.objects._get_site_by_id(site_id)
97-
except Site.DoesNotExist:
98-
site = get_current_site()
99-
return site
100-
101-
10290
class PageDeleteMessageMixin:
10391
"""Expressive and simplified delete confirmation message for pages and translations."""
10492

@@ -169,20 +157,19 @@ def has_change_permission(self, request, obj=None):
169157
Return true if the current user has permission on the page.
170158
Return the string 'All' if the user has all rights.
171159
"""
172-
site = get_site(request)
173160
if obj is None:
161+
site = get_site_from_request(request)
174162
# Checks if user can change at least one page
175163
return page_permissions.user_can_change_at_least_one_page(
176164
user=request.user,
177165
site=site,
178166
)
179-
return page_permissions.user_can_change_page(request.user, page=obj, site=site)
167+
return page_permissions.user_can_change_page(request.user, page=obj, site=obj.site)
180168

181169
def has_change_advanced_settings_permission(self, request, obj=None):
182170
if not obj:
183171
return False
184-
site = get_site(request)
185-
return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj, site=site)
172+
return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj, site=obj.site)
186173

187174
def log_deletion(self, request, object, object_repr):
188175
# DJANGO_42
@@ -211,12 +198,6 @@ def get_preserved_filters(self, request):
211198
preserved_filters["language"] = lang
212199
return preserved_filters.urlencode()
213200

214-
def get_queryset(self, request):
215-
site = get_site(request)
216-
queryset = super().get_queryset(request)
217-
queryset = queryset.filter(site=site)
218-
return queryset
219-
220201
def get_page_from_id(self, page_id):
221202
page_id = self.model._meta.pk.to_python(page_id)
222203

@@ -259,22 +240,15 @@ def get_inline_instances(self, request, obj=None):
259240
return super().get_inline_instances(request, obj)
260241
return []
261242

262-
def get_form(self, request, obj=None, **kwargs):
263-
"""
264-
Get PageForm for the Page model and modify its fields depending on
265-
the request.
266-
"""
267-
form = super().get_form(request, obj, **kwargs)
268-
form._site = get_site(request)
269-
form._request = request
270-
return form
271-
272243
def actions_menu(self, request, object_id, extra_context=None):
273244
page = self.get_object(request, object_id=object_id)
274245

275246
if page is None:
276247
raise self._get_404_exception(object_id)
277248

249+
if not self.has_view_permission(request, obj=page):
250+
raise PermissionDenied("No permission for actions menu")
251+
278252
site = get_site(request)
279253
paste_enabled = request.GET.get("has_copy") or request.GET.get("has_cut")
280254
context = {
@@ -363,11 +337,13 @@ def set_home(self, request, object_id):
363337
set_restart_trigger()
364338
return HttpResponse("ok")
365339

366-
def get_list(self, *args, **kwargs):
340+
def get_list(self, request):
367341
"""
368342
This view is used by the PageSmartLinkWidget as the user type to feed the autocomplete drop-down.
369343
"""
370-
request = args[0]
344+
345+
if not self.has_view_permission(request):
346+
raise PermissionDenied("No permission for page list view")
371347

372348
if request.headers.get("x-requested-with") == "XMLHttpRequest":
373349
query_term = request.GET.get("q", "").strip("/")
@@ -786,21 +762,19 @@ def get_preserved_filters(self, request):
786762
This override is in place to preserve the "language" get parameter in
787763
the "Save" page redirect
788764
"""
789-
site = get_site(request)
765+
site = get_site_from_request(request)
790766
preserved_filters_encoded = super().get_preserved_filters(request)
791767
preserved_filters = QueryDict(preserved_filters_encoded).copy()
792768
lang = get_site_language_from_request(request, site_id=site.pk)
793769

794770
if lang:
795771
preserved_filters["language"] = lang
772+
if site and site.pk != settings.SITE_ID:
773+
preserved_filters["site"] = site.pk
796774
return preserved_filters.urlencode()
797775

798776
def get_queryset(self, request):
799-
site = get_site(request)
800-
languages = get_language_list(site.pk)
801-
queryset = super().get_queryset(request).select_related("page")
802-
queryset = queryset.filter(language__in=languages, page__site=site)
803-
return queryset
777+
return super().get_queryset(request).select_related("page")
804778

805779
def get_urls(self):
806780
"""Get the admin urls"""
@@ -845,6 +819,11 @@ def get_form(self, request, obj=None, **kwargs):
845819
form._request = request
846820
return form
847821

822+
# def get_changeform_initial_data(self, request):
823+
# site = get_site(request)
824+
# language = get_site_language_from_request(request, site_id=site.pk)
825+
# return {"language": language, "site": site}
826+
848827
def slug(self, obj):
849828
# For read-only views: Get slug from the page
850829
if not hasattr(self, "url_obj"):
@@ -877,6 +856,8 @@ def duplicate(self, request, object_id):
877856
return self.add_view(request)
878857

879858
def add_view(self, request, form_url="", extra_context=None):
859+
if request.method == "GET" and (redirect_response := needs_site_redirect(request)):
860+
return redirect_response
880861
site = get_site(request)
881862
language = get_site_language_from_request(request, site_id=site.pk)
882863

@@ -929,7 +910,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
929910
if obj is None:
930911
raise self._get_404_exception(object_id)
931912

932-
site = get_site(request)
913+
site = obj.page.site
933914
context = {
934915
"cms_page": obj.page,
935916
"CMS_PERMISSION": get_cms_setting("PERMISSION"),
@@ -962,7 +943,7 @@ def response_add(self, request, obj):
962943
return super().response_add(request, obj)
963944

964945
def get_filled_languages(self, request, page):
965-
site_id = get_site(request).pk
946+
site_id = page.site.pk
966947
filled_languages = page.get_languages()
967948
allowed_languages = [lang[0] for lang in get_language_tuple(site_id)]
968949
return [lang for lang in filled_languages if lang in allowed_languages]
@@ -978,7 +959,6 @@ def _get_404_exception(self, object_id):
978959
return exception
979960

980961
def _has_add_permission_from_request(self, request):
981-
site = get_site(request)
982962
if parent_id := request.GET.get("parent_page"):
983963
try:
984964
parent_id = IntegerField().clean(parent_id)
@@ -992,9 +972,10 @@ def _has_add_permission_from_request(self, request):
992972
has_perm = page_permissions.user_can_add_subpage(
993973
request.user,
994974
target=parent_item,
995-
site=site,
975+
site=parent_item.site,
996976
)
997977
else:
978+
site = get_site_from_request(request)
998979
has_perm = page_permissions.user_can_add_page(request.user, site=site)
999980
return has_perm
1000981

@@ -1009,13 +990,12 @@ def has_change_permission(self, request, obj=None):
1009990
Return true if the current user has permission on the page.
1010991
Return the string 'All' if the user has all rights.
1011992
"""
1012-
site = get_site(request)
1013993

1014994
if obj:
1015-
return page_permissions.user_can_change_page(request.user, page=obj.page, site=site)
995+
return page_permissions.user_can_change_page(request.user, page=obj.page, site=obj.page.site)
1016996
can_change_page = page_permissions.user_can_change_at_least_one_page(
1017997
user=request.user,
1018-
site=site,
998+
site=get_site_from_request(request),
1019999
)
10201000
return can_change_page
10211001

@@ -1026,13 +1006,12 @@ def has_view_permission(self, request, obj=None):
10261006
"""
10271007
# Identical to has_change_permission, but will remain untouched by any subclassing
10281008
# as done, e.g., by djangocms-versioning
1029-
site = get_site(request)
10301009

10311010
if obj:
1032-
return page_permissions.user_can_change_page(request.user, page=obj.page, site=site)
1011+
return page_permissions.user_can_change_page(request.user, page=obj.page, site=obj.page.site)
10331012
can_view_page = page_permissions.user_can_change_at_least_one_page(
10341013
user=request.user,
1035-
site=get_site(request),
1014+
site=get_site_from_request(request),
10361015
use_cache=False,
10371016
)
10381017
return can_view_page
@@ -1043,48 +1022,34 @@ def has_delete_permission(self, request, obj=None):
10431022
"""
10441023
if not obj:
10451024
return False
1046-
site = get_site(request)
1047-
return page_permissions.user_can_delete_page(request.user, page=obj.page, site=site)
1025+
return page_permissions.user_can_delete_page(request.user, page=obj.page, site=obj.page.site)
10481026

10491027
def has_change_advanced_settings_permission(self, request, obj=None):
10501028
if not obj:
10511029
return False
1052-
site = get_site(request)
1053-
return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj.page, site=site)
1030+
return page_permissions.user_can_change_page_advanced_settings(request.user, page=obj.page, site=obj.page.site)
10541031

10551032
def has_delete_translation_permission(self, request, language, obj=None):
10561033
if not obj:
10571034
return False
10581035

1059-
site = get_site(request)
10601036
has_perm = page_permissions.user_can_delete_page_translation(
10611037
user=request.user,
10621038
page=obj,
10631039
language=language,
1064-
site=site,
1040+
site=obj.site,
10651041
)
10661042
return has_perm
10671043

1068-
def get_sites_for_user(self, user):
1069-
sites = Site.objects.order_by("name")
1070-
1071-
if not get_cms_setting("PERMISSION") or user.is_superuser:
1072-
return sites
1073-
_has_perm = page_permissions.user_can_change_at_least_one_page
1074-
return [site for site in sites if _has_perm(user, site)]
1075-
10761044
def changelist_view(self, request, extra_context=None):
10771045
from django.contrib.admin.views.main import ERROR_FLAG
10781046

1047+
if redirect_response := needs_site_redirect(request):
1048+
return redirect_response
1049+
10791050
if not self.has_change_permission(request, obj=None):
10801051
raise PermissionDenied
10811052

1082-
if request.method == "POST" and "site" in request.POST:
1083-
site_id = request.POST["site"]
1084-
1085-
if site_id.isdigit() and Site.objects.filter(pk=site_id).exists():
1086-
request.session["cms_admin_site"] = site_id
1087-
10881053
site = get_site(request)
10891054
language = get_site_language_from_request(request, site_id=site.pk)
10901055
query = request.GET.get("q", "")
@@ -1142,7 +1107,7 @@ def changelist_view(self, request, extra_context=None):
11421107
"admin": self,
11431108
"tree": {
11441109
"site": site,
1145-
"sites": self.get_sites_for_user(request.user),
1110+
"sites": get_sites_for_user(request.user),
11461111
"query": query,
11471112
"is_filtered": changelist_form.is_filtered(),
11481113
"items": pages,

cms/admin/site_utils.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from django.contrib.sites.models import Site
2+
from django.core.exceptions import (
3+
PermissionDenied,
4+
)
5+
from django.http import (
6+
Http404,
7+
HttpResponseRedirect,
8+
)
9+
from django.utils.translation import gettext as _
10+
11+
from cms.utils import get_current_site, page_permissions
12+
from cms.utils.conf import get_cms_setting
13+
from cms.utils.urlutils import add_url_parameters
14+
15+
16+
def get_site_id_from_request(request):
17+
site_id = request.GET.get("site", request.session.get("cms_admin_site"))
18+
if site_id is None:
19+
return None
20+
21+
try:
22+
return int(site_id)
23+
except ValueError:
24+
raise Http404(_("Invalid site ID."))
25+
26+
27+
def get_site_from_request(request):
28+
site_id = get_site_id_from_request(request)
29+
if site_id is None:
30+
return get_current_site()
31+
try:
32+
return Site.objects._get_site_by_id(site_id)
33+
except Site.DoesNotExist:
34+
raise Http404(_("Site does not exist."))
35+
36+
37+
def needs_site_redirect(request):
38+
user_sites = get_sites_for_user(request.user)
39+
if not user_sites:
40+
raise PermissionDenied(_("You do not have permission to view any sites. Please contact your administrator."))
41+
42+
current_site = get_current_site()
43+
site_id = request.GET.get("site")
44+
legacy_session_site_id = request.session.get("cms_admin_site")
45+
if legacy_session_site_id and site_id is None:
46+
# Remove legacy session and possibly redirect with site query parameter
47+
del request.session["cms_admin_site"]
48+
if legacy_session_site_id == current_site.pk:
49+
return
50+
redirect_url = add_url_parameters(request.path, request.GET, site=legacy_session_site_id)
51+
return HttpResponseRedirect(redirect_url)
52+
53+
# TODO: handle more cases?
54+
55+
56+
def validate_site(request, site):
57+
user_sites = get_sites_for_user(request.user)
58+
if site not in user_sites:
59+
raise PermissionDenied(_("You do not have permission to view this site. Please contact your administrator."))
60+
61+
62+
def get_site(request):
63+
"""
64+
Validates the site from the request and returns it.
65+
If the user does not have permission to view any sites, raises PermissionDenied.
66+
"""
67+
site = get_site_from_request(request)
68+
validate_site(request, site)
69+
return site
70+
71+
72+
def get_sites_for_user(user):
73+
if hasattr(user, "_cms_user_sites"):
74+
return user._cms_user_sites
75+
76+
sites = Site.objects.order_by("name")
77+
if not get_cms_setting("PERMISSION") or user.is_superuser:
78+
user._cms_user_sites = sites
79+
return sites
80+
81+
_has_perm = page_permissions.user_can_change_at_least_one_page
82+
user_sites = [site for site in sites if _has_perm(user, site)]
83+
user._cms_user_sites = user_sites
84+
return user_sites

cms/static/cms/js/modules/cms.pagetree.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -440,17 +440,6 @@ var PageTree = new Class({
440440
that._reloadHelper();
441441
});
442442

443-
// propagate the sites dropdown "li > a" entries to the hidden sites form
444-
this.ui.container.find('.js-cms-pagetree-site-trigger').on(this.click, function(e) {
445-
e.preventDefault();
446-
var el = $(this);
447-
448-
// prevent if parent is active
449-
if (el.parent().hasClass('active')) {
450-
return false;
451-
}
452-
that.ui.siteForm.find('select').val(el.data().id).end().submit();
453-
});
454443

455444
// additional event handlers
456445
this._setupDropdowns();

0 commit comments

Comments
 (0)