dynamicweb/cms/tests/test_views.py

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")