forked from ungleich-public/dynamicweb
801 lines
34 KiB
Python
801 lines
34 KiB
Python
import re
|
|
import sys
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.models import Permission
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.cache import cache
|
|
from django.http import Http404, HttpResponse
|
|
from django.template import Variable
|
|
from django.test.utils import override_settings
|
|
from django.urls import clear_url_caches, reverse
|
|
from django.utils import translation
|
|
from django.utils.deprecation import MiddlewareMixin
|
|
from django.utils.translation import override as force_language
|
|
|
|
from cms.api import add_plugin, create_page, create_page_content
|
|
from cms.middleware.toolbar import ToolbarMiddleware
|
|
from cms.models import PageContent, PagePermission, Placeholder, UserSettings
|
|
from cms.page_rendering import _handle_no_page
|
|
from cms.test_utils.testcases import CMSTestCase
|
|
from cms.test_utils.util.fuzzy_int import FuzzyInt
|
|
from cms.toolbar.utils import (
|
|
get_object_edit_url,
|
|
get_object_preview_url,
|
|
get_object_structure_url,
|
|
)
|
|
from cms.utils.conf import get_cms_setting
|
|
from cms.utils.page import get_page_from_request
|
|
from cms.utils.urlutils import admin_reverse
|
|
from cms.views import details, login, render_object_structure
|
|
from menus.menu_pool import menu_pool
|
|
|
|
APP_NAME = "SampleApp"
|
|
APP_MODULE = "cms.test_utils.project.sampleapp.cms_apps"
|
|
|
|
|
|
# Middleware that sets language based on subdomain - used in test_language_code_is_respected_with_prefix_subdomains
|
|
class LanguageSettingMiddleware(MiddlewareMixin):
|
|
"""Middleware that sets language based on subdomain.
|
|
Example URLs:
|
|
- de.example.com -> German
|
|
- en.example.com -> English
|
|
Falls back to German if no valid language subdomain is found.
|
|
"""
|
|
|
|
def process_request(self, request):
|
|
# Get the hostname from the request and remove port if present
|
|
hostname = request.get_host().split(":")[0]
|
|
|
|
# Extract language code from the subdomain
|
|
try:
|
|
language_code = hostname.split(".")[0].lower()
|
|
|
|
# Only allow English and German
|
|
if language_code == "en":
|
|
request.LANGUAGE_CODE = "en"
|
|
translation.activate("en")
|
|
else:
|
|
# Default to German for any other subdomain
|
|
request.LANGUAGE_CODE = "de"
|
|
translation.activate("de")
|
|
except IndexError:
|
|
pass
|
|
|
|
|
|
@override_settings(
|
|
CMS_PERMISSION=True,
|
|
ROOT_URLCONF="cms.test_utils.project.urls",
|
|
)
|
|
class ViewTests(CMSTestCase):
|
|
def setUp(self):
|
|
clear_url_caches()
|
|
|
|
def tearDown(self):
|
|
super().tearDown()
|
|
clear_url_caches()
|
|
|
|
def test_welcome_screen_debug_on(self):
|
|
clear_url_caches()
|
|
with self.settings(DEBUG=True):
|
|
response = self.client.get("/en/")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.template_name, "cms/welcome.html")
|
|
|
|
def test_welcome_screen_debug_off(self):
|
|
with self.settings(DEBUG=False):
|
|
response = self.client.get("/en/")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(response.template_name, "cms/welcome.html")
|
|
|
|
def _create_page_type(self, title="my type"):
|
|
from cms import constants
|
|
from cms.models import Page
|
|
|
|
root = create_page("Page Types", "nav_playground.html", "en", reverse_id=constants.PAGE_TYPES_ID)
|
|
page_type = create_page(title, "nav_playground.html", "en", parent=root)
|
|
Page.objects.filter(pk__in=(root.pk, page_type.pk)).update(is_page_type=True)
|
|
return Page.objects.get(pk=page_type.pk)
|
|
|
|
def test_page_types_are_not_served(self):
|
|
"""
|
|
Page types are blueprints used to create new pages, not content, so they
|
|
must not be rendered on the public site -- not even for a superuser.
|
|
"""
|
|
page_type = self._create_page_type()
|
|
add_plugin(page_type.get_placeholders("en").get(slot="body"), "TextPlugin", "en", body="blueprint")
|
|
url = page_type.get_absolute_url("en")
|
|
|
|
self.assertEqual(self.client.get(url).status_code, 404)
|
|
|
|
with self.login_user_context(self.get_superuser()):
|
|
self.assertEqual(self.client.get(url).status_code, 404)
|
|
|
|
def test_page_types_root_is_not_served(self):
|
|
page_type = self._create_page_type()
|
|
self.assertEqual(self.client.get(page_type.parent.get_absolute_url("en")).status_code, 404)
|
|
|
|
def test_handle_no_page(self):
|
|
"""
|
|
Test handle nopage correctly works with DEBUG=True
|
|
"""
|
|
request = self.get_request("/not-existing/")
|
|
self.assertRaises(Http404, _handle_no_page, request)
|
|
|
|
def test_handle_no_page_for_root_url(self):
|
|
"""
|
|
Test if _handle_no_page correctly works for root url
|
|
"""
|
|
request = self.get_request("/en/")
|
|
response = _handle_no_page(request)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response.url, reverse("admin:cms_pagecontent_changelist"))
|
|
|
|
def test_handle_no_page_for_root_url_no_homepage(self):
|
|
"""
|
|
Test details view when visiting root and homepage doesn't exist
|
|
"""
|
|
create_page("one", "nav_playground.html", "en")
|
|
response = self.client.get("/en/")
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_apphook_not_hooked(self):
|
|
"""
|
|
Test details view when apphook pool has apphooks, but they're not
|
|
actually hooked
|
|
"""
|
|
if APP_MODULE in sys.modules:
|
|
del sys.modules[APP_MODULE]
|
|
apphooks = (f"{APP_MODULE}.{APP_NAME}",)
|
|
page = create_page("page2", "nav_playground.html", "en")
|
|
with self.settings(CMS_APPHOOKS=apphooks):
|
|
self.apphook_clear()
|
|
response = self.client.get(page.get_absolute_url())
|
|
self.assertEqual(response.status_code, 200)
|
|
self.apphook_clear()
|
|
|
|
def test_redirect_preview_in_edit_mode(self):
|
|
user = self.get_superuser()
|
|
page = create_page("page", "nav_playground.html", "fr")
|
|
page_content = create_page_content("en", "home", page, redirect="https://example.com")
|
|
|
|
page.set_as_homepage()
|
|
expected = f"""
|
|
<div class="cms-screenblock">
|
|
<div class="cms-screenblock-inner">
|
|
<h1>This page has no preview!</h1>
|
|
<p>It is being redirected to: <a href="{page_content.redirect}">{page_content.redirect}</a></p>
|
|
</div>
|
|
</div>
|
|
"""
|
|
with self.login_user_context(user), force_language('fr'):
|
|
with self.subTest("Edit endpoint shows redirect message"):
|
|
url = get_object_edit_url(page_content, language='fr')
|
|
response = self.client.get(url)
|
|
self.assertContains(response, expected, count=1, html=True)
|
|
with self.subTest("Preview endpoint shows redirect message"):
|
|
url = get_object_preview_url(page_content, language='fr')
|
|
response = self.client.get(url)
|
|
self.assertContains(response, expected, count=1, html=True)
|
|
|
|
def test_external_redirect(self):
|
|
# test external redirect
|
|
redirect_one = "https://www.django-cms.org/"
|
|
one = create_page("one", "nav_playground.html", "en", redirect=redirect_one)
|
|
url = one.get_absolute_url()
|
|
request = self.get_request(url)
|
|
response = details(request, one.get_path("en"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response["Location"], redirect_one)
|
|
|
|
def test_internal_neutral_redirect(self):
|
|
# test internal language neutral redirect
|
|
redirect_one = "https://www.django-cms.org/"
|
|
redirect_two = "/"
|
|
one = create_page("one", "nav_playground.html", "en", redirect=redirect_one)
|
|
two = create_page("two", "nav_playground.html", "en", parent=one, redirect=redirect_two)
|
|
url = two.get_absolute_url()
|
|
request = self.get_request(url)
|
|
response = details(request, two.get_path("en"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response["Location"], "/en/")
|
|
|
|
def test_internal_forced_redirect(self):
|
|
# test internal forced language redirect
|
|
redirect_one = "https://www.django-cms.org/"
|
|
redirect_three = "/en/"
|
|
one = create_page("one", "nav_playground.html", "en", redirect=redirect_one)
|
|
three = create_page("three", "nav_playground.html", "en", parent=one, redirect=redirect_three)
|
|
url = three.get_absolute_url()
|
|
request = self.get_request(url)
|
|
response = details(request, three.get_path("en"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response["Location"], redirect_three)
|
|
|
|
def test_redirect_to_self(self):
|
|
one = create_page("one", "nav_playground.html", "en", redirect="/one/")
|
|
url = one.get_absolute_url()
|
|
request = self.get_request(url)
|
|
response = details(request, one.get_path("en"))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_redirect_to_self_with_host(self):
|
|
one = create_page("one", "nav_playground.html", "en", redirect="http://testserver/en/one/")
|
|
url = one.get_absolute_url()
|
|
request = self.get_request(url)
|
|
response = details(request, one.get_path("en"))
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_redirect_not_preserving_query_parameters(self):
|
|
# test redirect checking that the query parameters aren't preserved
|
|
redirect = "/en/"
|
|
one = create_page("one", "nav_playground.html", "en", redirect=redirect)
|
|
url = one.get_absolute_url()
|
|
params = "?param_name=param_value"
|
|
request = self.get_request(url + params)
|
|
response = details(request, one.get_path(language="en"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response["Location"], redirect)
|
|
|
|
@override_settings(CMS_REDIRECT_PRESERVE_QUERY_PARAMS=True)
|
|
def test_redirect_preserving_query_parameters(self):
|
|
# test redirect checking that query parameters are preserved
|
|
redirect = "/en/"
|
|
one = create_page("one", "nav_playground.html", "en", redirect=redirect)
|
|
url = one.get_absolute_url()
|
|
params = "?param_name=param_value"
|
|
request = self.get_request(url + params)
|
|
response = details(request, one.get_path(language="en"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response["Location"], redirect + params)
|
|
|
|
@override_settings(CMS_REDIRECT_TO_LOWERCASE_SLUG=True)
|
|
def test_redirecting_to_lowercase_slug(self):
|
|
redirect = "/en/one/"
|
|
one = create_page("one", "nav_playground.html", "en", redirect=redirect)
|
|
url = reverse("pages-details-by-slug", kwargs={"slug": "One"})
|
|
request = self.get_request(url)
|
|
response = details(request, one.get_path(language="en"))
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertEqual(response["Location"], redirect)
|
|
|
|
def test_login_required(self):
|
|
self.create_homepage("page", "nav_playground.html", "en", login_required=True)
|
|
plain_url = "/accounts/"
|
|
login_rx = re.compile("%s\\?(signin=|next=/en/)&" % plain_url)
|
|
with self.settings(LOGIN_URL=plain_url + "?signin"):
|
|
request = self.get_request("/en/")
|
|
response = details(request, "")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertTrue(login_rx.search(response["Location"]))
|
|
login_rx = re.compile("%s\\?(signin=|next=/)&" % plain_url)
|
|
with self.settings(USE_I18N=False, LOGIN_URL=plain_url + "?signin"):
|
|
request = self.get_request("/")
|
|
response = details(request, "")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertTrue(login_rx.search(response["Location"]))
|
|
|
|
def test_edit_permission(self):
|
|
page = create_page("page", "nav_playground.html", "en")
|
|
page_content = self.get_pagecontent_obj(page)
|
|
page_preview_url = get_object_preview_url(page_content)
|
|
# Anon user
|
|
response = self.client.get(page_preview_url)
|
|
self.assertRedirects(response, f"/en/admin/login/?next={page_preview_url}")
|
|
|
|
# Superuser
|
|
user = self.get_superuser()
|
|
with self.login_user_context(user):
|
|
response = self.client.get(page_preview_url)
|
|
toolbar = response.wsgi_request.toolbar
|
|
edit_button = toolbar.get_right_items()[2].buttons[0]
|
|
self.assertEqual(edit_button.name, "Edit")
|
|
self.assertEqual(edit_button.url, get_object_edit_url(page_content))
|
|
self.assertEqual(edit_button.extra_classes, ["cms-btn", "cms-btn-action", "cms-btn-switch-edit"])
|
|
|
|
# Admin but with no permission
|
|
user = self.get_staff_user_with_no_permissions()
|
|
user.user_permissions.add(Permission.objects.get(codename="change_page"))
|
|
|
|
with self.login_user_context(user):
|
|
response = self.client.get(page_preview_url)
|
|
toolbar = response.wsgi_request.toolbar
|
|
self.assertEqual(len(toolbar.get_right_items()), 2) # Only has Create button and color switch
|
|
|
|
PagePermission.objects.create(can_change=True, user=user, page=page)
|
|
with self.login_user_context(user):
|
|
response = self.client.get(page_preview_url)
|
|
toolbar = response.wsgi_request.toolbar
|
|
edit_button = toolbar.get_right_items()[2].buttons[0]
|
|
self.assertEqual(edit_button.name, "Edit")
|
|
self.assertEqual(edit_button.url, get_object_edit_url(page_content))
|
|
self.assertEqual(edit_button.extra_classes, ["cms-btn", "cms-btn-action", "cms-btn-switch-edit"])
|
|
|
|
def test_toolbar_switch_urls(self):
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
user = self.get_superuser()
|
|
user_settings = UserSettings(language="en", user=user)
|
|
placeholder = Placeholder(slot="clipboard")
|
|
placeholder.save()
|
|
user_settings.clipboard = placeholder
|
|
user_settings.save()
|
|
|
|
page = create_page("page", "nav_playground.html", "en")
|
|
page_content = create_page_content("fr", "french home", page)
|
|
|
|
page.set_as_homepage()
|
|
|
|
with self.login_user_context(user), force_language("fr"):
|
|
edit_url = get_object_edit_url(page_content, language="fr")
|
|
preview_url = get_object_preview_url(page_content, language="fr")
|
|
structure_url = get_object_structure_url(page_content, language="fr")
|
|
|
|
response = self.client.get(edit_url)
|
|
expected = f"""
|
|
<a href="{structure_url}" class="cms-btn cms-btn-disabled" title="Toggle structure"
|
|
data-cms-structure-btn='{{ "url": "{structure_url}", "name": "Structure" }}'
|
|
data-cms-content-btn='{{ "url": "{edit_url}", "name": "Content" }}'>
|
|
<span class="cms-icon cms-icon-plugins"></span></a>
|
|
"""
|
|
self.assertContains(
|
|
response,
|
|
expected,
|
|
count=1,
|
|
html=True,
|
|
)
|
|
toolbar = response.wsgi_request.toolbar
|
|
self.assertEqual(len(toolbar.get_right_items()[2].buttons), 1)
|
|
preview_button = toolbar.get_right_items()[2].buttons[0]
|
|
self.assertEqual(preview_button.name, _("Preview"))
|
|
self.assertEqual(preview_button.url, preview_url)
|
|
self.assertEqual(preview_button.extra_classes, ["cms-btn", "cms-btn-switch-save"])
|
|
|
|
response = self.client.get(preview_url)
|
|
self.assertContains(
|
|
response,
|
|
expected,
|
|
count=1,
|
|
html=True,
|
|
)
|
|
toolbar = response.wsgi_request.toolbar
|
|
self.assertEqual(len(toolbar.get_right_items()[2].buttons), 1)
|
|
edit_button = toolbar.get_right_items()[2].buttons[0]
|
|
self.assertEqual(edit_button.name, _("Edit"))
|
|
self.assertEqual(edit_button.url, edit_url)
|
|
self.assertEqual(edit_button.extra_classes, ["cms-btn", "cms-btn-action", "cms-btn-switch-edit"])
|
|
|
|
def test_incorrect_slug_for_language(self):
|
|
"""
|
|
Test details view when page slug and current language don't match.
|
|
In this case we refer to the user's current language and the page slug we have for that language.
|
|
"""
|
|
create_page("home", "nav_playground.html", "en")
|
|
cms_page = create_page("stevejobs", "nav_playground.html", "en")
|
|
create_page_content("de", "jobs", cms_page)
|
|
response = self.client.get("/de/stevejobs/")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, "/de/jobs/")
|
|
|
|
@override_settings(
|
|
MIDDLEWARE=[
|
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
"django.middleware.locale.LocaleMiddleware",
|
|
"cms.tests.test_views.LanguageSettingMiddleware",
|
|
"django.middleware.common.CommonMiddleware",
|
|
"django.middleware.csrf.CsrfViewMiddleware",
|
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
"cms.middleware.user.CurrentUserMiddleware",
|
|
"cms.middleware.page.CurrentPageMiddleware",
|
|
"cms.middleware.toolbar.ToolbarMiddleware",
|
|
"cms.middleware.language.LanguageCookieMiddleware",
|
|
],
|
|
ALLOWED_HOSTS=["en.example.com", "de.example.com"],
|
|
)
|
|
def test_language_code_is_respected_with_prefix_subdomains(self):
|
|
"""
|
|
Test that the language code is respected with subdomains.
|
|
Tests that:
|
|
- German subdomain (de.example.com) serves content in German
|
|
- English subdomain (en.example.com) serves content in English
|
|
"""
|
|
# Create a homepage with English and German versions
|
|
page = create_page("home", "nav_playground.html", "en")
|
|
create_page_content("de", "heim", page)
|
|
page.set_as_homepage()
|
|
|
|
# Test German subdomain
|
|
response = self.client.get("/", HTTP_HOST="de.example.com")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, "/de/")
|
|
self.assertEqual(response.wsgi_request.LANGUAGE_CODE, "de")
|
|
|
|
# Test English subdomain
|
|
response = self.client.get("/", HTTP_HOST="en.example.com")
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertRedirects(response, "/en/")
|
|
self.assertEqual(response.wsgi_request.LANGUAGE_CODE, "en")
|
|
|
|
def test_page_sanitisation_xss_attack(self):
|
|
"""
|
|
When sending a request the CMS uses get_page_from_request to return the appropriate page.
|
|
None should be returned
|
|
"""
|
|
request = self.get_request("/")
|
|
request.path_info = "<script>alert('attack!')</script>"
|
|
|
|
response = get_page_from_request(request)
|
|
|
|
# If this method is passed a parameter which is not a valid primary key
|
|
# for a page object nothing should be returned.
|
|
self.assertEqual(response, None)
|
|
|
|
def test_malicious_content_login_request(self):
|
|
username = getattr(self.get_superuser(), get_user_model().USERNAME_FIELD)
|
|
request = self.get_request(
|
|
"/en/admin/login/?q=<script>alert('Attack')</script>",
|
|
post_data={"username": username, "password": username},
|
|
)
|
|
|
|
response = login(request)
|
|
|
|
self.assertNotIn(response.url, "<script>alert('Attack')</script>")
|
|
|
|
def test_queries(self):
|
|
create_page("home", "simple.html", "en")
|
|
cms_page = create_page("dreinhardt", "simple.html", "en")
|
|
url = cms_page.get_absolute_url()
|
|
with self.assertNumQueries(5):
|
|
# 1. get_page_from_request: checks PageUrl
|
|
# 2. get page contents: PageContent
|
|
# 3. Check permissions
|
|
# 4. Get placeholders
|
|
# 5. Get plugins
|
|
self.client.get(url)
|
|
|
|
|
|
@override_settings(ROOT_URLCONF="cms.test_utils.project.urls")
|
|
class ContextTests(CMSTestCase):
|
|
def test_context_current_page(self):
|
|
"""
|
|
Asserts the number of queries triggered by
|
|
`cms.context_processors.cms_settings` and `cms.middleware.page`
|
|
"""
|
|
from django.template import context
|
|
|
|
page_template = "nav_playground.html"
|
|
original_context = {"TEMPLATES": settings.TEMPLATES}
|
|
page = self.create_homepage("page", page_template, "en")
|
|
page_2 = create_page("page-2", page_template, "en", parent=page)
|
|
|
|
# Tests for standard django applications
|
|
# 1 query is executed in get_app_patterns(), not related
|
|
# to cms.context_processors.cms_settings.
|
|
# Executing this oputside queries assertion context ensure
|
|
# repetability
|
|
self.client.get("/en/plain_view/")
|
|
|
|
cache.clear()
|
|
menu_pool.clear()
|
|
context._standard_context_processors = None
|
|
|
|
# Number of queries when context processor is enabled
|
|
with self.settings(**original_context):
|
|
with self.assertNumQueries(FuzzyInt(0, 17)):
|
|
response = self.client.get("/en/plain_view/")
|
|
# One query when determining current page
|
|
with self.assertNumQueries(FuzzyInt(0, 1)):
|
|
self.assertFalse(response.context["request"].current_page)
|
|
self.assertFalse(response.context["request"]._current_page_cache)
|
|
# Zero more queries when determining the current template
|
|
with self.assertNumQueries(0):
|
|
# Template is the first in the CMS_TEMPLATES list
|
|
template = Variable("CMS_TEMPLATE").resolve(response.context)
|
|
self.assertEqual(template, get_cms_setting("TEMPLATES")[0][0])
|
|
cache.clear()
|
|
menu_pool.clear()
|
|
|
|
# Number of queries when context processors is enabled
|
|
with self.settings(**original_context):
|
|
with self.assertNumQueries(FuzzyInt(13, 30)) as context:
|
|
response = self.client.get("/en/page-2/")
|
|
template = Variable("CMS_TEMPLATE").resolve(response.context)
|
|
self.assertEqual(template, page_template)
|
|
num_queries_page = len(context.captured_queries)
|
|
cache.clear()
|
|
menu_pool.clear()
|
|
page_2.update_translations(template="INHERIT")
|
|
|
|
with self.settings(**original_context):
|
|
# One query more triggered as page inherits template from ancestor
|
|
with self.assertNumQueries(num_queries_page + 1):
|
|
response = self.client.get("/en/page-2/")
|
|
template = Variable("CMS_TEMPLATE").resolve(response.context)
|
|
self.assertEqual(template, page_template)
|
|
|
|
|
|
class EndpointTests(CMSTestCase):
|
|
def setUp(self) -> None:
|
|
page_template = "simple.html"
|
|
self.page = self.create_homepage("page", page_template, "en")
|
|
|
|
self.page_content_en = self.page.get_content_obj()
|
|
self.page_content_fr = create_page_content("fr", "french home", self.page)
|
|
self.content_type = ContentType.objects.get_for_model(PageContent)
|
|
|
|
self.client.force_login(self.get_superuser())
|
|
|
|
def tearDown(self) -> None:
|
|
self.page.delete()
|
|
|
|
def test_render_object_structure(self):
|
|
request = self.get_request("/")
|
|
request.user = self.get_superuser()
|
|
mid = ToolbarMiddleware(lambda req: HttpResponse(""))
|
|
mid(request)
|
|
response = render_object_structure(request, self.content_type.id, self.page_content_en.pk)
|
|
|
|
self.assertEqual(request.current_page, self.page)
|
|
self.assertContains(response, '<div class="cms-toolbar">')
|
|
|
|
def test_render_object_structure_i18n(self):
|
|
"""Structure view shows the page content's language not the request's language."""
|
|
placeholder = self.page.get_placeholders("fr").first()
|
|
self._add_plugin_to_placeholder(placeholder, "TextPlugin", language="fr")
|
|
with force_language("fr"):
|
|
setting, _ = UserSettings.objects.get_or_create(user=self.get_superuser())
|
|
setting.language = "fr"
|
|
setting.save()
|
|
structure_endpoint_url = admin_reverse(
|
|
"cms_placeholder_render_object_structure",
|
|
args=(
|
|
self.content_type.id,
|
|
self.page_content_fr.pk,
|
|
),
|
|
)
|
|
response = self.client.get(structure_endpoint_url)
|
|
self.assertContains(response, "<strong>Texte</strong>")
|
|
|
|
setting.language = "en"
|
|
setting.save()
|
|
|
|
response = self.client.get(structure_endpoint_url)
|
|
self.assertContains(response, "<strong>Text</strong>")
|
|
|
|
|
|
class EndpointPermissionTests(CMSTestCase):
|
|
"""Security tests for the object render endpoints (edit/preview/structure).
|
|
|
|
The structure endpoint must enforce the same page-view permission as the
|
|
edit and preview endpoints, which deny access to staff users that cannot
|
|
view a restricted page.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
from cms.api import add_plugin, assign_user_to_page
|
|
|
|
self.page = create_page("restricted", "simple.html", "en")
|
|
self.page_content = self.page.get_content_obj("en")
|
|
placeholder = self.page_content.get_placeholders().first()
|
|
# Secret content that must not leak to unauthorized staff users.
|
|
add_plugin(
|
|
placeholder,
|
|
"LinkPlugin",
|
|
"en",
|
|
name="SuperSecretLinkName",
|
|
external_link="https://secret.example.com",
|
|
)
|
|
# Restrict the page to view: granting view to the superuser turns the
|
|
# page into a view-restricted page for everybody else.
|
|
assign_user_to_page(self.page, self.get_superuser(), can_view=True)
|
|
self.content_type = ContentType.objects.get_for_model(PageContent)
|
|
|
|
def _endpoint(self, name):
|
|
return admin_reverse(name, args=(self.content_type.id, self.page_content.pk))
|
|
|
|
def test_edit_and_preview_endpoints_deny_view_restricted_page(self):
|
|
"""Baseline: edit and preview already deny access via render_page()."""
|
|
staff = self.get_staff_user_with_no_permissions()
|
|
with self.login_user_context(staff):
|
|
for name in (
|
|
"cms_placeholder_render_object_edit",
|
|
"cms_placeholder_render_object_preview",
|
|
):
|
|
response = self.client.get(self._endpoint(name))
|
|
self.assertEqual(
|
|
response.status_code,
|
|
404,
|
|
msg=f"{name} leaked a view-restricted page (status {response.status_code})",
|
|
)
|
|
|
|
def test_structure_endpoint_denies_view_restricted_page(self):
|
|
"""The structure endpoint must not render a page the user cannot view."""
|
|
staff = self.get_staff_user_with_no_permissions()
|
|
with self.login_user_context(staff):
|
|
response = self.client.get(
|
|
self._endpoint("cms_placeholder_render_object_structure")
|
|
)
|
|
self.assertEqual(
|
|
response.status_code,
|
|
404,
|
|
msg="structure endpoint leaked a view-restricted page",
|
|
)
|
|
self.assertNotContains(
|
|
response, "SuperSecretLinkName", status_code=404
|
|
)
|
|
|
|
def test_structure_endpoint_allows_permitted_user(self):
|
|
"""A user who may view the page still gets the structure board."""
|
|
with self.login_user_context(self.get_superuser()):
|
|
response = self.client.get(
|
|
self._endpoint("cms_placeholder_render_object_structure")
|
|
)
|
|
self.assertContains(response, "SuperSecretLinkName")
|
|
|
|
|
|
class NonPageContentStructureEndpointTests(CMSTestCase):
|
|
"""The structure endpoint must also authorize non-PageContent objects.
|
|
|
|
Registered through ``admin_site.admin_view`` the endpoint only requires an
|
|
active staff user. Without an object-level check, any staff user could read
|
|
the placeholder/plugin structure of a frontend-editable object (e.g. a model
|
|
using ``PlaceholderRelationField``) without permission to view it.
|
|
|
|
Viewing the (read-only) structure board requires only *view* permission;
|
|
mutating the plugins stays gated by change permission at the plugin
|
|
endpoints. This supports headless reviewers inspecting structure without
|
|
edit rights.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
from cms.test_utils.project.placeholder_relation_field_app.models import (
|
|
FancyPoll,
|
|
)
|
|
from cms.utils.placeholder import rescan_placeholders_for_obj
|
|
|
|
self.target = FancyPoll.objects.create(name="private-fancy-poll")
|
|
self.placeholder = rescan_placeholders_for_obj(self.target)["content"]
|
|
|
|
def _url(self):
|
|
return get_object_structure_url(self.target, language="en")
|
|
|
|
def _marker(self):
|
|
return f'"placeholder_id": "{self.placeholder.pk}"'
|
|
|
|
def _staff_with_perm(self, codename):
|
|
staff = self._create_user(f"staff_{codename}", is_staff=True, is_superuser=False)
|
|
staff.user_permissions.add(
|
|
Permission.objects.get(
|
|
content_type__app_label="placeholder_relation_field_app",
|
|
codename=codename,
|
|
)
|
|
)
|
|
return staff
|
|
|
|
def test_denies_staff_without_view_permission(self):
|
|
staff = self.get_staff_user_with_no_permissions()
|
|
with self.login_user_context(staff):
|
|
response = self.client.get(self._url())
|
|
self.assertEqual(
|
|
response.status_code,
|
|
404,
|
|
msg="structure endpoint leaked a non-PageContent object's structure",
|
|
)
|
|
self.assertNotContains(response, self._marker(), status_code=404)
|
|
|
|
def test_allows_staff_with_view_permission(self):
|
|
# Headless / read-only reviewers may inspect structure with view rights.
|
|
staff = self._staff_with_perm("view_fancypoll")
|
|
with self.login_user_context(staff):
|
|
response = self.client.get(self._url())
|
|
self.assertContains(response, self._marker())
|
|
|
|
def test_view_only_user_gets_read_only_structure_board(self):
|
|
"""View permission opens the board, but the editing UX is disabled.
|
|
|
|
Mutations stay gated by change permission at the plugin endpoints, so
|
|
the structure board must render read-only (no drag/add/cut/delete) for a
|
|
user who may view but not change the object.
|
|
"""
|
|
view_only = self._staff_with_perm("view_fancypoll")
|
|
with self.login_user_context(view_only):
|
|
response = self.client.get(self._url())
|
|
self.assertContains(response, self._marker())
|
|
# Read-only markers from toolbar_with_structure.html / dragbar / dragitem.
|
|
self.assertContains(response, "cms-drag-disabled")
|
|
self.assertContains(response, "Placeholder not editable")
|
|
|
|
# A user who may change the object still gets the editable board.
|
|
editor = self._staff_with_perm("change_fancypoll")
|
|
with self.login_user_context(editor):
|
|
response = self.client.get(self._url())
|
|
self.assertContains(response, self._marker())
|
|
self.assertNotContains(response, "Placeholder not editable")
|
|
|
|
def test_allows_staff_with_change_permission(self):
|
|
staff = self._staff_with_perm("change_fancypoll")
|
|
with self.login_user_context(staff):
|
|
response = self.client.get(self._url())
|
|
self.assertContains(response, self._marker())
|
|
|
|
def test_allows_superuser(self):
|
|
with self.login_user_context(self.get_superuser()):
|
|
response = self.client.get(self._url())
|
|
self.assertContains(response, self._marker())
|
|
|
|
|
|
class NonPageContentObjectEndpointTests(CMSTestCase):
|
|
"""The preview and edit endpoints must also authorize non-PageContent objects.
|
|
|
|
Companion to :class:`NonPageContentStructureEndpointTests`. Registered
|
|
through ``admin_site.admin_view`` these endpoints only require an active
|
|
staff user, and unlike the structure board they render the object's full
|
|
plugin output. Without an object-level check, any staff user could read the
|
|
rendered content of any frontend-editable object (e.g. a model registered
|
|
via ``cms_toolbar_enabled_models``) without permission to view it.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
from cms.api import add_plugin
|
|
from cms.test_utils.project.placeholderapp.models import Example1
|
|
|
|
self.target = Example1.objects.create(
|
|
char_1="private-object", char_2="b", char_3="c", char_4="d",
|
|
)
|
|
self.placeholder = self.target.placeholder
|
|
add_plugin(
|
|
self.placeholder, "LinkPlugin", "en",
|
|
name="SuperSecretLinkName", external_link="https://secret.example.com",
|
|
)
|
|
|
|
def _staff_with_perm(self, codename):
|
|
staff = self._create_user(f"staff_{codename}", is_staff=True, is_superuser=False)
|
|
staff.user_permissions.add(
|
|
Permission.objects.get(
|
|
content_type__app_label="placeholderapp", codename=codename
|
|
)
|
|
)
|
|
return staff
|
|
|
|
def _urls(self):
|
|
return {
|
|
"preview": get_object_preview_url(self.target, language="en"),
|
|
"edit": get_object_edit_url(self.target, language="en"),
|
|
}
|
|
|
|
def test_denies_staff_without_view_permission(self):
|
|
staff = self.get_staff_user_with_no_permissions()
|
|
with self.login_user_context(staff):
|
|
for name, url in self._urls().items():
|
|
with self.subTest(endpoint=name):
|
|
response = self.client.get(url)
|
|
self.assertEqual(
|
|
response.status_code,
|
|
404,
|
|
msg=f"{name} endpoint leaked a non-PageContent object's content",
|
|
)
|
|
self.assertNotContains(
|
|
response, "SuperSecretLinkName", status_code=404
|
|
)
|
|
|
|
def test_allows_staff_with_view_permission(self):
|
|
staff = self._staff_with_perm("view_example1")
|
|
with self.login_user_context(staff):
|
|
for name, url in self._urls().items():
|
|
with self.subTest(endpoint=name):
|
|
self.assertContains(self.client.get(url), "SuperSecretLinkName")
|
|
|
|
def test_allows_staff_with_change_permission(self):
|
|
staff = self._staff_with_perm("change_example1")
|
|
with self.login_user_context(staff):
|
|
for name, url in self._urls().items():
|
|
with self.subTest(endpoint=name):
|
|
self.assertContains(self.client.get(url), "SuperSecretLinkName")
|
|
|
|
def test_allows_superuser(self):
|
|
with self.login_user_context(self.get_superuser()):
|
|
for name, url in self._urls().items():
|
|
with self.subTest(endpoint=name):
|
|
self.assertContains(self.client.get(url), "SuperSecretLinkName")
|