clone cms with menus, make menus standalone app

This commit is contained in:
bruks 2026-09-30 15:27:04 +01:00
commit afb096b7a3
1153 changed files with 318897 additions and 37 deletions

10
cms/admin/__init__.py Normal file
View file

@ -0,0 +1,10 @@
import cms.admin.pageadmin # noqa: F401
import cms.admin.permissionadmin # noqa: F401
import cms.admin.placeholderadmin # noqa: F401
import cms.admin.settingsadmin # noqa: F401
import cms.admin.useradmin # noqa: F401
# Piggyback off admin.autodiscover() to discover cms plugins
from cms import plugin_pool
plugin_pool.plugin_pool.discover_plugins()

1714
cms/admin/forms.py Normal file

File diff suppressed because it is too large Load diff

1513
cms/admin/pageadmin.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,232 @@
from copy import deepcopy
from django.contrib import admin
from django.contrib.admin import site
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.contrib.sites.models import Site
from django.db import OperationalError, ProgrammingError
from django.utils.translation import gettext_lazy as _
from cms.admin.forms import (
GlobalPagePermissionAdminForm,
PagePermissionInlineAdminForm,
ViewRestrictionInlineAdminForm,
)
from cms.exceptions import NoPermissionsException
from cms.models import GlobalPagePermission, PagePermission
from cms.utils import page_permissions, permissions
from cms.utils.conf import get_cms_setting
from cms.utils.helpers import classproperty
PERMISSION_ADMIN_INLINES = []
user_model = get_user_model()
admin_class = UserAdmin
for model, admin_instance in site._registry.items():
if model == user_model:
admin_class = admin_instance.__class__
class TabularInline(admin.TabularInline):
pass
class PagePermissionInlineAdmin(TabularInline):
model = PagePermission
# use special form, so we can override of user and group field
form = PagePermissionInlineAdminForm
extra = 0 # edit page load time boost
show_with_view_permissions = False
def has_change_permission(self, request, obj=None):
if not obj:
return False
return page_permissions.user_can_change_page_permissions(
request.user,
page=obj,
site=obj.site,
)
def has_add_permission(self, request, obj=None):
return self.has_change_permission(request, obj)
@classproperty
def raw_id_fields(cls):
# Dynamically set raw_id_fields based on settings
threshold = get_cms_setting('RAW_ID_USERS')
# Given a fresh django-cms install and a django settings with the
# CMS_RAW_ID_USERS = CMS_PERMISSION = True
# django throws an OperationalError (sqlite) or ProgrammingError
# (postgres) when running ./manage migrate because the user table
# doesn't exist yet.
try:
threshold = threshold and get_user_model().objects.count() > threshold
except (OperationalError, ProgrammingError):
threshold = False
return ['user'] if threshold else []
def get_queryset(self, request):
"""
Queryset change, so user with global change permissions can see
all permissions. Otherwise user can see only permissions for
peoples which are under him (he can't see his permissions, because
this will lead to violation, when he can add more power to himself)
"""
site = Site.objects.get_current(request)
try:
# can see only permissions for users which are under him in tree
qs = self.model.objects.subordinate_to_user(request.user, site)
except NoPermissionsException:
return self.model.objects.none()
return qs.filter(can_view=self.show_with_view_permissions)
def get_formset(self, request, obj=None, **kwargs):
"""
Some fields may be excluded here. User can change only
permissions which are available for him. E.g. if user does not haves
can_change flag, he can't change assign can_change permissions.
"""
exclude = self.exclude or []
if obj:
user = request.user
if not obj.has_add_permission(user):
exclude.append('can_add')
if not obj.has_delete_permission(user):
exclude.append('can_delete')
if not obj.has_publish_permission(user):
exclude.append('can_publish')
if not obj.has_advanced_settings_permission(user):
exclude.append('can_change_advanced_settings')
if not obj.has_move_page_permission(user):
exclude.append('can_move_page')
kwargs['exclude'] = exclude
self.form._page = obj
formset_cls = super().get_formset(request, obj=obj, **kwargs)
qs = self.get_queryset(request)
if obj is not None:
qs = qs.filter(page=obj)
formset_cls._queryset = qs
return formset_cls
class ViewRestrictionInlineAdmin(PagePermissionInlineAdmin):
extra = 0 # edit page load time boost
form = ViewRestrictionInlineAdminForm
verbose_name = _("View restriction")
verbose_name_plural = _("View restrictions")
show_with_view_permissions = True
class GlobalPagePermissionAdmin(admin.ModelAdmin):
list_display = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions']
list_filter = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions']
form = GlobalPagePermissionAdminForm
search_fields = []
for field in admin_class.search_fields:
search_fields.append("user__%s" % field)
search_fields.append('group__name')
list_display.append('can_change_advanced_settings')
list_filter.append('can_change_advanced_settings')
def get_list_filter(self, request):
threshold = get_cms_setting('RAW_ID_USERS')
try:
threshold = threshold and get_user_model().objects.count() > threshold
except (OperationalError, ProgrammingError):
threshold = False
filter_copy = deepcopy(self.list_filter)
if threshold:
filter_copy.remove('user')
return filter_copy
def get_exclude(self, request, obj=None):
"""Offer only the ``can_*`` flags the acting user may hand out.
Mirrors ``PagePermissionInlineAdmin.get_formset``: a manager must not be
able to grant -- to themselves or anyone else -- a right they do not hold
(CWE-269). Excluding the field keeps it out of the rendered form *and*
out of a crafted POST. Whether a flag the manager holds on some site may
be granted for the sites actually picked is settled in the form's
``clean()``, once ``sites`` is known.
"""
exclude = list(super().get_exclude(request, obj) or [])
grantable = permissions.get_grantable_global_permissions(request.user)
exclude.extend(
field for field in GlobalPagePermission.get_all_permissions()
if field not in grantable and field not in exclude
)
return exclude
def get_form(self, request, obj=None, **kwargs):
form_class = super().get_form(request, obj, **kwargs)
# The form caps each granted flag to the sites the manager holds it on,
# so it needs to know who is acting. ``modelform_factory`` returns a
# fresh subclass per call, so this is per-request state rather than
# shared mutation of the declared form class.
form_class._current_user = request.user
return form_class
def has_add_permission(self, request):
site = Site.objects.get_current(request)
return permissions.user_can_add_global_permissions(request.user, site)
def has_change_permission(self, request, obj=None):
site = Site.objects.get_current(request)
return (
permissions.user_can_change_global_permissions(request.user, site)
and self._can_manage_grant(request, obj)
)
def has_delete_permission(self, request, obj=None):
site = Site.objects.get_current(request)
return (
permissions.user_can_delete_global_permissions(request.user, site)
and self._can_manage_grant(request, obj)
)
def _can_manage_grant(self, request, obj):
"""Only let a manager edit or delete a grant they could have created.
A grant for a site the manager may not manage permissions on, or holding
flags they may not hand out on its sites, is out of reach: its ``user``,
``group`` and ``sites`` would otherwise let them move those flags to
themselves or to other sites (CWE-269).
"""
if obj is None:
return True
held = {flag for flag in GlobalPagePermission.get_all_permissions() if getattr(obj, flag)}
site_ids = list(obj.sites.values_list("pk", flat=True))
grantable = permissions.get_grantable_global_permissions(request.user, site_ids)
return "can_change_permissions" in grantable and held <= grantable
@classproperty
def raw_id_fields(cls):
# Dynamically set raw_id_fields based on settings
threshold = get_cms_setting('RAW_ID_USERS')
# Given a fresh django-cms install and a django settings with the
# CMS_RAW_ID_USERS = CMS_PERMISSION = True
# django throws an OperationalError (sqlite) or ProgrammingError
# (postgres) when running ./manage migrate because the user table
# doesn't exist yet.
try:
threshold = threshold and get_user_model().objects.count() > threshold
except (OperationalError, ProgrammingError):
threshold = False
return ['user'] if threshold else []
if get_cms_setting('PERMISSION'):
admin.site.register(GlobalPagePermission, GlobalPagePermissionAdmin)
PERMISSION_ADMIN_INLINES.extend([
ViewRestrictionInlineAdmin,
PagePermissionInlineAdmin,
])

File diff suppressed because it is too large Load diff

156
cms/admin/settingsadmin.py Normal file
View file

@ -0,0 +1,156 @@
import copy
import json
from functools import update_wrapper
from urllib.parse import urlparse
from django.contrib import admin
from django.db import transaction
from django.http import (
HttpResponse,
HttpResponseBadRequest,
HttpResponseRedirect,
)
from django.http.request import QueryDict
from django.urls import Resolver404, path, re_path, resolve
from django.utils.decorators import method_decorator
from django.utils.html import conditional_escape
from django.utils.translation import override
from django.views.decorators.csrf import csrf_protect
from cms.admin.forms import RequestToolbarForm
from cms.models import UserSettings
from cms.models.contentmodels import PageContent
from cms.utils.page import get_page_from_request
from cms.utils.page_permissions import user_can_view_page
from cms.utils.permissions import user_can_view_placeholder_source
from cms.utils.urlutils import admin_reverse
@admin.register(UserSettings)
class SettingsAdmin(admin.ModelAdmin):
def get_urls(self):
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view)
info = self.model._meta.app_label, self.model._meta.model_name
return [
path('session_store/', self.session_store, name='%s_%s_session_store' % info),
path('cms-toolbar/', wrap(self.get_toolbar), name='%s_%s_get_toolbar' % info),
path('', wrap(self.change_view), name='%s_%s_change' % info),
re_path(r'^(.+)/$', wrap(self.change_view), name='%s_%s_change' % info),
]
@method_decorator(csrf_protect)
@transaction.atomic
def change_view(self, request, id=None):
model = self.model
try:
obj = model.objects.get(user=request.user)
except model.DoesNotExist:
return self.add_view(request)
return super().change_view(request, str(obj.pk))
def session_store(self, request):
"""
either POST or GET
POST should have a settings parameter
"""
if not request.user.is_staff:
return HttpResponse(json.dumps(""),
content_type="application/json")
if request.method == "POST":
request.session['cms_settings'] = conditional_escape(request.POST['settings'])
request.session.save()
return HttpResponse(
json.dumps(request.session.get('cms_settings', '')),
content_type="application/json"
)
def get_toolbar(self, request):
from cms.toolbar.toolbar import CMSToolbar
form = RequestToolbarForm(request.GET or None)
if not form.is_valid():
return HttpResponseBadRequest('Invalid parameters')
form_data = form.cleaned_data
cms_path = form_data.get('cms_path') or request.path_info
origin_url = urlparse(cms_path)
attached_obj = form_data.get('attached_obj')
if isinstance(attached_obj, PageContent):
current_page = attached_obj.page
else:
current_page = get_page_from_request(request, use_path=origin_url.path, clean_path=True)
# The object id/type come straight from the client, so enforce an
# object-level view check before rendering a toolbar for it. Without
# this, any staff user could read the title/breadcrumb/edit URLs of
# pages on other sites or of view-restricted pages they cannot access.
# Deny with the same generic response as an unknown object so this does
# not become an existence oracle.
if isinstance(attached_obj, PageContent):
if not user_can_view_page(request.user, current_page):
return HttpResponseBadRequest('Invalid parameters')
elif attached_obj is not None:
if not user_can_view_placeholder_source(request.user, attached_obj):
return HttpResponseBadRequest('Invalid parameters')
elif current_page is not None and not user_can_view_page(request.user, current_page):
return HttpResponseBadRequest('Invalid parameters')
data = QueryDict(query_string=origin_url.query, mutable=True)
placeholders = request.GET.getlist("placeholders[]")
if placeholders:
data.setlist('placeholders[]', placeholders)
request = copy.copy(request)
request.GET = data
request.current_page = current_page
try:
request.resolver_match = resolve(origin_url.path)
except Resolver404:
pass
request.toolbar = CMSToolbar(request, request_path=origin_url.path, _async=True)
request.toolbar.set_object(attached_obj or current_page)
return HttpResponse(request.toolbar.render())
def save_model(self, request, obj, form, change):
obj.user = request.user
obj.save()
def response_post_save_change(self, request, obj):
#
# When the user changes his language setting, we need to do two things:
# 1. Change the language-prefix for the sideframed admin view
# 2. Reload the whole window so that the new language affects the
# toolbar, etc.
#
# To do this, we first redirect the sideframe to the correct new, URL,
# but we pass a GET param 'reload_window', which instructs JS on that
# page to strip (to avoid infinite redirection loops) that param then
# reload the whole window again.
#
with override(obj.language):
post_url = admin_reverse(
'cms_usersettings_change',
args=[obj.id, ],
current_app=self.admin_site.name
)
return HttpResponseRedirect(f"{post_url}?reload_window")
def has_change_permission(self, request, obj=None):
if obj and obj.user == request.user:
return True
return False
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
return {}

171
cms/admin/useradmin.py Normal file
View file

@ -0,0 +1,171 @@
from copy import deepcopy
from django.contrib import admin
from django.contrib.admin import ModelAdmin, site
from django.contrib.admin.utils import unquote
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext
from cms.admin.forms import PageUserChangeForm, PageUserGroupForm
from cms.exceptions import NoPermissionsException
from cms.models import Page, PagePermission, PageUser, PageUserGroup
from cms.utils.compat.forms import UserAdmin
from cms.utils.conf import get_cms_setting
from cms.utils.permissions import (
get_model_permission_codename,
get_subordinate_groups,
get_subordinate_users,
get_user_permission_level,
)
user_model = get_user_model()
admin_class: type[ModelAdmin] = UserAdmin
for model, admin_instance in site._registry.items():
if model == user_model:
admin_class = admin_instance.__class__
class GenericCmsPermissionAdmin:
def get_subordinates(self, user, site):
raise NotImplementedError
def _has_change_permissions_permission(self, request):
"""
User is able to add/change objects only if he haves can change
permission on some page.
"""
site = Site.objects.get_current(request)
try:
get_user_permission_level(request.user, site)
except NoPermissionsException:
return False
return True
def get_form(self, request, obj=None, **kwargs):
form_class = super().get_form(request, obj, **kwargs)
form_class._current_user = request.user
return form_class
def get_queryset(self, request):
queryset = super().get_queryset(request)
site = Site.objects.get_current(request)
user_ids = self.get_subordinates(request.user, site).values_list('pk', flat=True)
return queryset.filter(pk__in=user_ids)
def has_add_permission(self, request):
has_model_perm = super().has_add_permission(request)
if not has_model_perm:
return False
return self._has_change_permissions_permission(request)
def has_change_permission(self, request, obj=None):
has_model_perm = super().has_change_permission(request, obj)
if not has_model_perm:
return False
return self._has_change_permissions_permission(request)
def has_delete_permission(self, request, obj=None):
has_model_perm = super().has_delete_permission(request, obj)
if not has_model_perm:
return False
return self._has_change_permissions_permission(request)
def has_view_permission(self, request, obj=None):
# For django 2.1
# Default is to return True if user got `change` perm, but we have to
# get in consideration also cms permission system
return self.has_change_permission(request, obj)
class PageUserAdmin(GenericCmsPermissionAdmin, admin_class):
form = PageUserChangeForm
model = PageUser
def get_subordinates(self, user, site):
return get_subordinate_users(user, site).values_list('pk', flat=True)
def get_readonly_fields(self, request, obj=None):
fields = super().get_readonly_fields(request, obj)
if not request.user.is_superuser:
# Non superusers can't set superuser status on
# their subordinates.
fields = list(fields) + ['is_superuser']
return fields
def has_change_permission(self, request, obj=None):
if obj is not None and obj.is_superuser and not request.user.is_superuser:
# Superusers are nobody's subordinate. get_queryset() already hides
# them, this guards the views that resolve an object by other means.
return False
return super().has_change_permission(request, obj)
def has_delete_permission(self, request, obj=None):
if obj is not None and obj.is_superuser and not request.user.is_superuser:
return False
return super().has_delete_permission(request, obj)
def user_change_password(self, request, id, form_url=''):
# Django's password view only checks has_change_permission(), so a
# subordinate check has to happen before anything else.
user = self.get_object(request, unquote(id))
if user is not None and user.is_superuser and not request.user.is_superuser:
raise PermissionDenied
return super().user_change_password(request, id, form_url)
def save_model(self, request, obj, form, change):
if not change:
# By default set the staff flag to True
# when a PageUser is first created
obj.is_staff = True
# Set the created_by field to the current user
obj.created_by = request.user
super().save_model(request, obj, form, change)
class PageUserGroupAdmin(GenericCmsPermissionAdmin, admin.ModelAdmin):
form = PageUserGroupForm
list_display = ('name', 'created_by')
fieldsets = [
(None, {'fields': ('name',)}),
]
def get_fieldsets(self, request, obj=None):
"""
Nobody can grant more than he haves, so check for user permissions
to Page and User model and render fieldset depending on them.
"""
fieldsets = deepcopy(self.fieldsets)
perm_models = (
(Page, gettext('Page permissions')),
(PageUser, gettext('User & Group permissions')),
(PagePermission, gettext('Page permissions management')),
)
for i, perm_model in enumerate(perm_models):
fields = []
model, title = perm_model
name = model.__name__.lower()
for key in ('add', 'change', 'delete'):
perm_code = get_model_permission_codename(model, action=key)
if request.user.has_perm(perm_code):
fields.append(f'can_{key}_{name}')
if fields:
fieldsets.insert(2 + i, (title, {'fields': (fields,)}))
return fieldsets
def get_subordinates(self, user, site):
return get_subordinate_groups(user, site).values_list('pk', flat=True)
if get_cms_setting('PERMISSION'):
admin.site.register(PageUser, PageUserAdmin)
admin.site.register(PageUserGroup, PageUserGroupAdmin)

1192
cms/admin/utils.py Normal file

File diff suppressed because it is too large Load diff