forked from ungleich-public/dynamicweb
448 lines
19 KiB
Python
448 lines
19 KiB
Python
from urllib.parse import quote, urlsplit, urlunsplit
|
|
|
|
from django.apps import apps
|
|
from django.conf import settings
|
|
from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login
|
|
from django.contrib.auth.views import redirect_to_login
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.http import (
|
|
Http404,
|
|
HttpResponse,
|
|
HttpResponseBadRequest,
|
|
HttpResponseRedirect,
|
|
)
|
|
from django.shortcuts import render
|
|
from django.template.defaultfilters import title
|
|
from django.template.response import TemplateResponse
|
|
from django.urls import (
|
|
NoReverseMatch,
|
|
Resolver404,
|
|
get_script_prefix,
|
|
resolve,
|
|
reverse,
|
|
)
|
|
from django.utils.cache import patch_cache_control
|
|
from django.utils.encoding import iri_to_uri
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import activate
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from cms.apphook_pool import apphook_pool
|
|
from cms.appresolver import applications_page_check
|
|
from cms.cache.page import get_page_cache
|
|
from cms.exceptions import LanguageError
|
|
from cms.forms.login import CMSToolbarLoginForm
|
|
from cms.models import Page, PageContent
|
|
from cms.page_rendering import (
|
|
_handle_no_apphook,
|
|
_handle_no_page,
|
|
_render_welcome_page,
|
|
render_pagecontent,
|
|
)
|
|
from cms.toolbar.utils import get_object_preview_url, get_toolbar_from_request
|
|
from cms.utils import get_current_site
|
|
from cms.utils.conf import get_cms_setting
|
|
from cms.utils.helpers import is_editable_model
|
|
from cms.utils.i18n import (
|
|
get_default_language_for_site,
|
|
get_fallback_languages,
|
|
get_language_list,
|
|
get_public_languages,
|
|
get_redirect_on_fallback,
|
|
is_language_prefix_patterns_used,
|
|
)
|
|
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.placeholder import get_declared_placeholders_for_obj, get_placeholder_conf
|
|
|
|
|
|
def _clean_redirect_url(redirect_url, language):
|
|
if (redirect_url and is_language_prefix_patterns_used() and redirect_url[0] == "/" and not redirect_url.startswith(
|
|
f"/{language}/"
|
|
)):
|
|
# add language prefix to url
|
|
redirect_url = f"/{language}/{redirect_url.lstrip('/')}"
|
|
return redirect_url
|
|
|
|
|
|
def details(request, slug):
|
|
"""
|
|
The main view of the Django-CMS! Takes a request and a slug, renders the
|
|
page.
|
|
"""
|
|
is_authenticated = request.user.is_authenticated
|
|
response_timestamp = now()
|
|
if get_cms_setting("PAGE_CACHE") and (
|
|
not hasattr(request, 'toolbar') or (
|
|
not request.toolbar.edit_mode_active and not request.toolbar.show_toolbar and not is_authenticated
|
|
)
|
|
):
|
|
cache_content = get_page_cache(request)
|
|
if cache_content is not None:
|
|
# ``xframe_options_exempt`` is absent on cache entries written before
|
|
# this field was added; default to True to preserve their behaviour
|
|
# until they expire.
|
|
content, headers, expires_datetime, *rest = cache_content
|
|
xframe_options_exempt = rest[0] if rest else True
|
|
response = HttpResponse(content)
|
|
# Replay the page's clickjacking decision. For "Inherit" pages this
|
|
# is False, so Django's XFrameOptionsMiddleware still adds the site
|
|
# default X-Frame-Options header to the cached response.
|
|
response.xframe_options_exempt = xframe_options_exempt
|
|
response.headers = headers
|
|
# Recalculate the max-age header for this cached response
|
|
max_age = int(
|
|
(expires_datetime - response_timestamp).total_seconds() + 0.5)
|
|
patch_cache_control(response, max_age=max_age)
|
|
return response
|
|
|
|
# Get a Page model object from the request
|
|
site = get_current_site(request)
|
|
page = get_page_from_request(request, use_path=slug)
|
|
|
|
if page and page.is_page_type:
|
|
# Page types are blueprints used to create new pages, not content: they
|
|
# must not be served on the public site. The admin renders them through
|
|
# the object preview/edit endpoints instead.
|
|
return _handle_no_page(request)
|
|
|
|
if not page and not slug and not Page.objects.on_site(site).exists():
|
|
# render the welcome page if the requested path is root "/"
|
|
# and there's no pages
|
|
return _render_welcome_page(request)
|
|
|
|
if not page and get_cms_setting("REDIRECT_TO_LOWERCASE_SLUG"):
|
|
# Redirect to the lowercase version of the slug
|
|
if slug.lower() != slug:
|
|
# Only redirect if the slug changes
|
|
redirect_url = reverse("pages-details-by-slug", kwargs={"slug": slug.lower()})
|
|
if get_cms_setting('REDIRECT_PRESERVE_QUERY_PARAMS'):
|
|
query_string = request.META.get('QUERY_STRING')
|
|
if query_string:
|
|
redirect_url += "?" + query_string
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
if not page:
|
|
# raise 404 or redirect to PageContent's
|
|
# changelist in the admin if this is a
|
|
# request to the root URL
|
|
return _handle_no_page(request)
|
|
|
|
request.current_page = page
|
|
request.site = site
|
|
|
|
if hasattr(request, 'user') and request.user.is_staff:
|
|
user_languages = get_language_list(site_id=site.pk)
|
|
else:
|
|
user_languages = get_public_languages(site_id=site.pk)
|
|
|
|
request_language = None
|
|
if hasattr(request, "LANGUAGE_CODE"):
|
|
# use language from middleware - usually django.middleware.locale.LocaleMiddleware
|
|
request_language = request.LANGUAGE_CODE
|
|
if not request_language:
|
|
request_language = get_default_language_for_site(site.pk)
|
|
|
|
if not page.is_home and request_language not in user_languages:
|
|
# The homepage is treated differently because
|
|
# when a request goes to the root of the site (/)
|
|
# without a language, Django will redirect to the user's
|
|
# browser language which might not be a valid cms language,
|
|
# this means we need to correctly redirect that request.
|
|
return _handle_no_page(request)
|
|
|
|
# we use the _get_page_content_cache method to populate the cache with all public languages
|
|
# The languages are then filtered out by the user allowed languages
|
|
page._get_page_content_cache(None, fallback=True, force_reload=True)
|
|
pagecontent_languages = list(page.page_content_cache.keys())
|
|
available_languages = [
|
|
language for language in user_languages
|
|
if language in pagecontent_languages
|
|
]
|
|
|
|
own_urls = [
|
|
request.build_absolute_uri(request.path),
|
|
'/%s' % request.path,
|
|
request.path,
|
|
]
|
|
|
|
try:
|
|
redirect_on_fallback = get_redirect_on_fallback(request_language, site_id=site.pk)
|
|
except LanguageError:
|
|
redirect_on_fallback = False
|
|
|
|
if request_language not in user_languages:
|
|
# Language is not allowed
|
|
# Use the default site language
|
|
default_language = get_default_language_for_site(site.pk)
|
|
fallbacks = get_fallback_languages(default_language, site_id=site.pk)
|
|
fallbacks = [default_language] + fallbacks
|
|
else:
|
|
fallbacks = get_fallback_languages(request_language, site_id=site.pk)
|
|
|
|
# Only fallback to languages the user is allowed to see
|
|
fallback_languages = [
|
|
language for language in fallbacks
|
|
if language != request_language and language in available_languages
|
|
]
|
|
language_is_unavailable = request_language not in available_languages
|
|
first_fallback_language = next(iter(fallback_languages or []), None)
|
|
|
|
if language_is_unavailable and not fallback_languages:
|
|
# There is no page with the requested language
|
|
# and there's no configured fallbacks
|
|
return _handle_no_page(request)
|
|
elif language_is_unavailable and redirect_on_fallback:
|
|
# There is no page with the requested language and
|
|
# redirect_on_fallback is True,
|
|
# so redirect to the first configured / available fallback language
|
|
redirect_url = page.get_absolute_url(
|
|
first_fallback_language, fallback=False)
|
|
else:
|
|
page_path = page.get_absolute_url(request_language)
|
|
page_slug = page.get_path(request_language) or page.get_slug(request_language)
|
|
|
|
if slug and slug != page_slug and request.path[:len(page_path)] != page_path:
|
|
# The current language does not match its slug.
|
|
# Redirect to the current language.
|
|
return HttpResponseRedirect(page_path)
|
|
# Check if the page has a redirect url defined for this language.
|
|
redirect_url = page.get_redirect(request_language, fallback=False) or ''
|
|
redirect_url = _clean_redirect_url(redirect_url, request_language)
|
|
|
|
if redirect_url:
|
|
if redirect_url not in own_urls:
|
|
if get_cms_setting('REDIRECT_PRESERVE_QUERY_PARAMS'):
|
|
query_string = request.META.get('QUERY_STRING')
|
|
if query_string:
|
|
redirect_url += "?" + query_string
|
|
# prevent redirect to self
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
# permission checks
|
|
if page.login_required and not request.user.is_authenticated:
|
|
return redirect_to_login(quote(request.get_full_path()), settings.LOGIN_URL)
|
|
|
|
content_language = request_language
|
|
if language_is_unavailable:
|
|
# When redirect_on_fallback is False and
|
|
# language is unavailable, render the content
|
|
# in the first fallback language available
|
|
# by switching to it
|
|
content_language = first_fallback_language
|
|
# translation.activate() is used without context
|
|
# as the context won't be preserved when the
|
|
# plugins get rendered
|
|
activate(content_language)
|
|
|
|
content = page.get_content_obj(language=content_language)
|
|
# use the page object with populated cache
|
|
content.page = page
|
|
if hasattr(request, 'toolbar'):
|
|
request.toolbar.set_object(content)
|
|
|
|
return render_pagecontent(request, content)
|
|
|
|
|
|
def _get_login_redirect_url(request, redirect_to):
|
|
"""Return a safe redirect target for the toolbar login view.
|
|
|
|
The ``next`` parameter is fully controlled by the client, so it must never
|
|
be trusted as-is (CWE-601, open redirect). A target is only accepted when
|
|
|
|
* it stays on the current host and uses an allowed scheme, and
|
|
* its path can be resolved against the project's URL configuration, i.e. it
|
|
actually points at a view served by this site.
|
|
|
|
Anything else falls back to the CMS root. The returned URL is rebuilt from
|
|
the validated, host-less components, so neither the scheme nor the host
|
|
supplied by the client is ever echoed back into the redirect.
|
|
"""
|
|
fallback = reverse("pages-root")
|
|
if not redirect_to or not url_has_allowed_host_and_scheme(
|
|
url=redirect_to, allowed_hosts={request.get_host()}
|
|
):
|
|
return fallback
|
|
|
|
parts = urlsplit(redirect_to)
|
|
# ``resolve()`` expects a path relative to the URL configuration, i.e.
|
|
# without the (optional) script prefix that the client-supplied URL carries.
|
|
path = parts.path
|
|
script_prefix = get_script_prefix()
|
|
if script_prefix != "/" and path.startswith(script_prefix):
|
|
path = "/" + path[len(script_prefix):]
|
|
|
|
try:
|
|
resolve(path)
|
|
except Resolver404:
|
|
return fallback
|
|
|
|
# Rebuild the redirect from the validated components only, dropping the
|
|
# client-supplied scheme and host. ``parts.path`` keeps the script prefix
|
|
# (the browser needs it); only the copy passed to ``resolve()`` had it
|
|
# stripped. ``iri_to_uri`` encodes unsafe characters without
|
|
# double-encoding an already percent-encoded path.
|
|
return iri_to_uri(urlunsplit(("", "", parts.path, parts.query, parts.fragment)))
|
|
|
|
|
|
@require_POST
|
|
def login(request):
|
|
redirect_to = _get_login_redirect_url(request, request.GET.get(REDIRECT_FIELD_NAME))
|
|
|
|
if request.user.is_authenticated:
|
|
return HttpResponseRedirect(redirect_to)
|
|
|
|
form = CMSToolbarLoginForm(request=request, data=request.POST)
|
|
|
|
if form.is_valid():
|
|
auth_login(request, form.user_cache)
|
|
else:
|
|
# Add the error flag to the query component so it is preserved next to
|
|
# any existing parameters and stays ahead of a possible fragment.
|
|
parts = urlsplit(redirect_to)
|
|
query = f'{parts.query}&cms_toolbar_login_error=1' if parts.query else 'cms_toolbar_login_error=1'
|
|
redirect_to = urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment))
|
|
return HttpResponseRedirect(redirect_to)
|
|
|
|
|
|
def render_object_structure(request, content_type_id, object_id):
|
|
try:
|
|
content_type = ContentType.objects.get_for_id(content_type_id)
|
|
except ContentType.DoesNotExist as err:
|
|
raise Http404 from err
|
|
|
|
try:
|
|
if issubclass(content_type.model_class(), PageContent):
|
|
content_type_obj = PageContent._base_manager.select_related("page", "page__site").get(pk=object_id)
|
|
request.current_page = content_type_obj.page
|
|
# Enforce the same page-view permission as the edit and preview
|
|
# endpoints (which check it via render_page()). Without this, a
|
|
# staff user who cannot view a restricted page could still read its
|
|
# plugin structure through this endpoint.
|
|
if not user_can_view_page(request.user, content_type_obj.page):
|
|
raise Http404
|
|
else:
|
|
content_type_obj = content_type.get_object_for_this_type(pk=object_id)
|
|
# Viewing the structure board requires only view permission (as the
|
|
# page branch above does via user_can_view_page). Mutations remain
|
|
# gated by change permission at the plugin endpoints.
|
|
if not user_can_view_placeholder_source(request.user, content_type_obj):
|
|
raise Http404
|
|
except ObjectDoesNotExist as err:
|
|
raise Http404 from err
|
|
|
|
context = {
|
|
'object': content_type_obj,
|
|
'cms_toolbar': request.toolbar,
|
|
}
|
|
toolbar = get_toolbar_from_request(request)
|
|
toolbar.set_object(content_type_obj)
|
|
return render(request, 'cms/toolbar/structure.html', context)
|
|
|
|
|
|
def render_placeholder_content(request, obj, context):
|
|
context["cms_placeholder_slots"] = (
|
|
(
|
|
placeholder.slot,
|
|
get_placeholder_conf("name", placeholder.slot, default=title(placeholder.slot)),
|
|
placeholder.inherit,
|
|
)
|
|
for placeholder in get_declared_placeholders_for_obj(obj)
|
|
)
|
|
return TemplateResponse(request, "cms/headless/placeholder.html", context)
|
|
|
|
|
|
def render_object_endpoint(request, content_type_id, object_id, require_editable):
|
|
try:
|
|
content_type = ContentType.objects.get_for_id(content_type_id)
|
|
except ContentType.DoesNotExist as err:
|
|
raise Http404 from err
|
|
else:
|
|
model = content_type.model_class()
|
|
|
|
if require_editable and not is_editable_model(model):
|
|
return HttpResponseBadRequest('Requested object does not support frontend rendering')
|
|
|
|
try:
|
|
if issubclass(model, PageContent):
|
|
# An apphook might be attached to a PageContent object
|
|
content_type_obj = model.admin_manager.select_related("page", "page__site").get(pk=object_id)
|
|
request.current_page = content_type_obj.page
|
|
if (
|
|
content_type_obj.page.application_urls and
|
|
content_type_obj.page.application_urls in dict(apphook_pool.get_apphooks()) and
|
|
(not require_editable or content_type_obj.is_editable(request))
|
|
):
|
|
try:
|
|
# If so, try get the absolute URL and pass it to the toolbar as request_path
|
|
# The apphook's view function will be called.
|
|
absolute_url = content_type_obj.get_absolute_url()
|
|
from cms.toolbar.toolbar import CMSToolbar
|
|
request.toolbar = CMSToolbar(request, request_path=absolute_url)
|
|
# Make page content's placeholders available, can be overwritten by apphook view
|
|
request.toolbar.set_object(content_type_obj)
|
|
# Resolve the apphook's url to get its view function
|
|
view_func, args, kwargs = resolve(absolute_url)
|
|
if view_func is not details:
|
|
return view_func(request, *args, **kwargs)
|
|
except Resolver404:
|
|
# Apphook does not provide a view for its "root", show warning message
|
|
return _handle_no_apphook(request)
|
|
else:
|
|
content_type_obj = content_type.get_object_for_this_type(pk=object_id)
|
|
# Enforce the same object-level view check as the structure endpoint
|
|
# (and, for the page branch above, render_page()'s user_can_view_page).
|
|
# Without this, any staff user could read the rendered plugin content
|
|
# of any frontend-editable object. Mutations remain gated by change
|
|
# permission at the plugin endpoints.
|
|
if not user_can_view_placeholder_source(request.user, content_type_obj):
|
|
raise Http404
|
|
except ObjectDoesNotExist as err:
|
|
raise Http404 from err
|
|
|
|
# Attempt to resolve current_page via the object's absolute URL (for apphooks)
|
|
if not getattr(request, "current_page", None):
|
|
try:
|
|
object_url = content_type_obj.get_absolute_url()
|
|
except (AttributeError, NoReverseMatch):
|
|
object_url = None
|
|
|
|
if object_url:
|
|
original_path_info = request.path_info
|
|
try:
|
|
# Temporarily patch the request object and use the appresolver
|
|
request.path_info = object_url
|
|
request.current_page = applications_page_check(request)
|
|
finally:
|
|
request.path_info = original_path_info
|
|
|
|
extension = apps.get_app_config('cms').cms_extension
|
|
|
|
if model not in extension.toolbar_enabled_models:
|
|
return HttpResponseBadRequest('Requested object does not support frontend rendering')
|
|
|
|
toolbar = get_toolbar_from_request(request)
|
|
toolbar.set_object(content_type_obj)
|
|
request.site = get_current_site(request)
|
|
|
|
redirect = getattr(content_type_obj, "redirect", None)
|
|
if isinstance(redirect, str):
|
|
toolbar.redirect_url = redirect
|
|
|
|
if require_editable and not toolbar.object_is_editable():
|
|
# If not editable, switch from edit to preview endpoint
|
|
return HttpResponseRedirect(get_object_preview_url(content_type_obj))
|
|
|
|
render_func = extension.toolbar_enabled_models[model]
|
|
return render_func(request, content_type_obj)
|
|
|
|
|
|
def render_object_edit(request, content_type_id, object_id):
|
|
return render_object_endpoint(request, content_type_id, object_id, require_editable=True)
|
|
|
|
|
|
def render_object_preview(request, content_type_id, object_id):
|
|
return render_object_endpoint(request, content_type_id, object_id, require_editable=False)
|