import datetime import re from types import SimpleNamespace from unittest.mock import patch import iptools from django.conf import settings from django.contrib import admin from django.contrib.admin import site from django.contrib.auth.models import AnonymousUser, Permission from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.http import HttpResponse, QueryDict from django.template.defaultfilters import date as date_filter, truncatewords from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse from django.utils.encoding import force_str from django.utils.functional import lazy from django.utils.html import escape from django.utils.http import urlencode from django.utils.translation import get_language, gettext_lazy as _, override from cms.admin.forms import RequestToolbarForm from cms.api import ( add_plugin, assign_user_to_page, create_page, create_page_content, ) from cms.cms_toolbars import ( ADMIN_MENU_IDENTIFIER, ADMINISTRATION_BREAK, DEFAULT_HELP_MENU_ITEMS, HELP_MENU_IDENTIFIER, LANGUAGE_MENU_IDENTIFIER, PAGE_MENU_IDENTIFIER, PageToolbar, get_user_model, ) from cms.models import PagePermission, Placeholder, UserSettings from cms.test_utils.project.placeholderapp.models import CharPksExample, Example1 from cms.test_utils.project.placeholderapp.views import ClassDetail, detail_view from cms.test_utils.testcases import URL_CMS_USERSETTINGS, CMSTestCase from cms.test_utils.util.context_managers import UserLoginContext, override_placeholder_conf from cms.toolbar import utils from cms.toolbar.items import ( AjaxItem, Break, ItemSearchResult, LinkItem, SubMenu, ToolbarAPIMixin, ) from cms.toolbar.toolbar import CMSToolbar, EmptyToolbar from cms.toolbar.utils import ( add_live_url_querystring_param, get_object_edit_url, get_object_for_language, get_object_live_url, get_object_preview_url, get_object_structure_url, get_plugin_tree, get_toolbar_from_request, ) from cms.toolbar_pool import toolbar_pool from cms.utils.conf import get_cms_setting from cms.utils.i18n import get_language_tuple from cms.utils.urlutils import admin_reverse from cms.views import details class ToolbarTestBase(CMSTestCase): def get_anon(self): return AnonymousUser() def get_staff(self): staff = self._create_user("staff", True, False) staff.user_permissions.add(Permission.objects.get(codename="change_page")) return staff def get_nonstaff(self): nonstaff = self._create_user("nonstaff") nonstaff.user_permissions.add(Permission.objects.get(codename="change_page")) return nonstaff def get_superuser(self): superuser = self._create_user("superuser", True, True) return superuser def _get_example_obj(self): obj = Example1.objects.create(char_1="one", char_2="two", char_3="tree", char_4="four") obj.save() return obj @override_settings(ROOT_URLCONF="cms.test_utils.project.nonroot_urls") class ToolbarMiddlewareTest(ToolbarTestBase): @override_settings(CMS_TOOLBAR_HIDE=False) def test_no_app_setted_show_toolbar_in_non_cms_urls(self): request = self.get_page_request(None, self.get_anon(), "/en/example/") self.assertTrue(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=False) def test_no_app_setted_show_toolbar_in_cms_urls(self): page = create_page("foo", "col_two.html", "en") request = self.get_page_request(page, self.get_anon()) self.assertTrue(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=False) def test_app_setted_hide_toolbar_in_non_cms_urls_toolbar_hide_unsetted(self): request = self.get_page_request(None, self.get_anon(), "/en/example/") self.assertTrue(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=True) def test_app_setted_hide_toolbar_in_non_cms_urls(self): request = self.get_page_request(None, self.get_anon(), "/en/example/") self.assertFalse(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=False) def test_app_setted_show_toolbar_in_cms_urls(self): page = create_page("foo", "col_two.html", "en") page = create_page("foo", "col_two.html", "en", parent=page) request = self.get_page_request(page, self.get_anon()) self.assertTrue(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=True) def test_app_setted_show_toolbar_in_cms_urls_subpage(self): page = create_page("foo", "col_two.html", "en") page = create_page("foo", "col_two.html", "en", parent=page) request = self.get_page_request(page, self.get_anon()) self.assertTrue(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=True) def test_app_setted_show_toolbar_in_admin_endpoints(self): from cms.middleware.toolbar import ToolbarMiddleware page = create_page("foo", "col_two.html", "en") content = page.pagecontent_set.first() # edit endpoint request = self.get_request(get_object_edit_url(content)) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # structure endpoint request = self.get_request(get_object_structure_url(content)) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # preview endpoint request = self.get_request(get_object_preview_url(content)) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # edit-field endpoint request = self.get_request(admin_reverse("cms_placeholder_edit_field", args=(content.pk, "en"))) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) @override_settings(CMS_TOOLBAR_HIDE=True) def test_app_setted_provide_toolbar_obj_to_edit_actions(self): from cms.middleware.toolbar import ToolbarMiddleware # Add plugin request = self.get_request(admin_reverse("cms_placeholder_add_plugin")) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # Edit plugin request = self.get_request(admin_reverse("cms_placeholder_edit_plugin", args=(1,))) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # clear placeholder request = self.get_request(admin_reverse("cms_placeholder_clear_placeholder", args=(1,))) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # move plugin request = self.get_request(admin_reverse("cms_placeholder_move_plugin")) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) # copy plugin request = self.get_request(admin_reverse("cms_placeholder_copy_plugins")) mid = ToolbarMiddleware(lambda req: HttpResponse("")) mid(request) self.assertTrue(hasattr(request, "toolbar")) def test_cms_internal_ips_unset(self): with self.settings(CMS_INTERNAL_IPS=[]): request = self.get_page_request(None, self.get_staff(), "/en/example/") self.assertTrue(hasattr(request, "toolbar")) def test_cms_internal_ips_set_no_match(self): with self.settings( CMS_INTERNAL_IPS=[ "123.45.67.89", ] ): request = self.get_page_request(None, self.get_staff(), "/en/example/") self.assertFalse(hasattr(request, "toolbar")) def test_cms_internal_ips_set_match(self): with self.settings( CMS_INTERNAL_IPS=[ "127.0.0.0", "127.0.0.1", "127.0.0.2", ] ): request = self.get_page_request(None, self.get_staff(), "/en/example/") self.assertTrue(hasattr(request, "toolbar")) def test_cms_internal_ips_iptools(self): with self.settings(CMS_INTERNAL_IPS=iptools.IpRangeList(("127.0.0.0", "127.0.0.255"))): request = self.get_page_request(None, self.get_staff(), "/en/example/") self.assertTrue(hasattr(request, "toolbar")) def test_cms_internal_ips_iptools_bad_range(self): with self.settings(CMS_INTERNAL_IPS=iptools.IpRangeList(("128.0.0.0", "128.0.0.255"))): request = self.get_page_request(None, self.get_staff(), "/en/example/") self.assertFalse(hasattr(request, "toolbar")) class MissingToolbarTest(CMSTestCase): """ The plugin endpoints must degrade instead of crashing when the toolbar is absent, e.g. because CMS_INTERNAL_IPS excludes the client or because ToolbarMiddleware is not installed. They enforce their own permissions and must not depend on the middleware having run. """ def get_request_without_toolbar(self): request = RequestFactory().post("/") request.user = self.get_superuser() self.assertFalse(hasattr(request, "toolbar")) return request def test_empty_toolbar_has_a_clipboard_attribute(self): toolbar = EmptyToolbar(RequestFactory().get("/")) self.assertIsNone(toolbar.clipboard) def test_get_clipboard_from_request_without_toolbar(self): self.assertIsNone(get_toolbar_from_request(self.get_request_without_toolbar()).clipboard) def test_get_plugin_tree_without_toolbar(self): page = create_page("home", "nav_playground.html", "en") placeholder = page.get_placeholders("en")[0] plugin = add_plugin(placeholder, "LinkPlugin", "en", name="link", external_link="http://example.com") tree = get_plugin_tree(self.get_request_without_toolbar(), [plugin]) self.assertIn("html", tree) self.assertEqual(len(tree["plugins"]), 1) def test_copy_plugins_permission_denied_without_clipboard(self): """Without a clipboard there is nothing to copy into, so deny rather than crash.""" page = create_page("home", "nav_playground.html", "en") placeholder = page.get_placeholders("en")[0] plugin = add_plugin(placeholder, "LinkPlugin", "en", name="link", external_link="http://example.com") admin_instance = site._registry[Placeholder] self.assertFalse(admin_instance.has_copy_plugins_permission(self.get_request_without_toolbar(), [plugin])) def test_add_plugin_endpoint_does_not_crash_without_toolbar(self): """Regression: rendering the close frame used to raise AttributeError.""" page = create_page("home", "nav_playground.html", "en") placeholder = page.get_placeholders("en")[0] data = { "plugin_type": "LinkPlugin", "placeholder_id": placeholder.pk, "plugin_language": "en", "plugin_position": 1, "cms_path": "/en/", } url = admin_reverse("cms_placeholder_add_plugin") + "?" + urlencode(data) # CMS_INTERNAL_IPS excludes the test client, so no toolbar is attached. with self.settings(CMS_INTERNAL_IPS=["203.0.113.10"]): with self.login_user_context(self.get_superuser()): response = self.client.post( url, {"name": "link", "external_link": "http://example.com"} ) self.assertEqual(response.status_code, 200) self.assertEqual(placeholder.get_plugins("en").count(), 1) @override_settings(CMS_PERMISSION=False) class ToolbarTests(ToolbarTestBase): def get_page_item(self, toolbar): items = toolbar.get_left_items() + toolbar.get_right_items() page_item = [item for item in items if force_str(getattr(item, "name", None)) == "Page"] self.assertEqual(len(page_item), 1) return page_item[0] def test_toolbar_login(self): admin = self.get_superuser() endpoint = reverse("cms_login") + "?next=/en/admin/" username = getattr(admin, get_user_model().USERNAME_FIELD) password = getattr(admin, get_user_model().USERNAME_FIELD) response = self.client.post(endpoint, data={"username": username, "password": password}) self.assertRedirects(response, "/en/admin/") self.assertTrue(settings.SESSION_COOKIE_NAME in response.cookies) def test_toolbar_login_error(self): admin = self.get_superuser() endpoint = reverse("cms_login") + "?next=/en/admin/" username = getattr(admin, get_user_model().USERNAME_FIELD) response = self.client.post(endpoint, data={"username": username, "password": "invalid"}) self.assertRedirects(response, "/en/admin/?cms_toolbar_login_error=1", target_status_code=302) self.assertFalse(settings.SESSION_COOKIE_NAME in response.cookies) def test_toolbar_login_invalid_redirect_to(self): admin = self.get_superuser() endpoint = reverse("cms_login") + "?next=http://example.com" username = getattr(admin, get_user_model().USERNAME_FIELD) password = getattr(admin, get_user_model().USERNAME_FIELD) response = self.client.post(endpoint, data={"username": username, "password": password}) self.assertRedirects(response, "/en/") self.assertTrue(settings.SESSION_COOKIE_NAME in response.cookies) def test_toolbar_login_error_preserves_query_string(self): # The error flag must be appended without clobbering an existing query. admin = self.get_superuser() endpoint = reverse("cms_login") + "?next=/en/admin/%3Ffoo%3Dbar" username = getattr(admin, get_user_model().USERNAME_FIELD) response = self.client.post(endpoint, data={"username": username, "password": "invalid"}) self.assertRedirects( response, "/en/admin/?foo=bar&cms_toolbar_login_error=1", target_status_code=302, ) self.assertFalse(settings.SESSION_COOKIE_NAME in response.cookies) def test_toolbar_login_unresolvable_redirect_to(self): # A same-host path that does not resolve against the URL configuration # must not be used as a redirect target (open redirect protection). admin = self.get_superuser() endpoint = reverse("cms_login") + "?next=/not-a-language/does-not-resolve@@/" username = getattr(admin, get_user_model().USERNAME_FIELD) password = getattr(admin, get_user_model().USERNAME_FIELD) response = self.client.post(endpoint, data={"username": username, "password": password}) self.assertRedirects(response, "/en/") self.assertTrue(settings.SESSION_COOKIE_NAME in response.cookies) def test_toolbar_login_resolvable_redirect_to(self): # A same-host path that resolves to a project view is accepted. admin = self.get_superuser() endpoint = reverse("cms_login") + "?next=/en/admin/" username = getattr(admin, get_user_model().USERNAME_FIELD) password = getattr(admin, get_user_model().USERNAME_FIELD) response = self.client.post(endpoint, data={"username": username, "password": password}) self.assertRedirects(response, "/en/admin/", target_status_code=200) self.assertTrue(settings.SESSION_COOKIE_NAME in response.cookies) @override_settings(CMS_TOOLBARS=["cms.test_utils.project.sampleapp.cms_toolbars.ToolbarWithMedia"]) def test_toolbar_media(self): """ Toolbar classes can declare a media class or property to be rendered along with the toolbar. """ old_pool = toolbar_pool.toolbars toolbar_pool.clear() cms_page = create_page("toolbar-page", "col_two.html", "en") page_content = self.get_pagecontent_obj(cms_page) endpoint = get_object_edit_url(page_content) with self.login_user_context(self.get_superuser()): response = self.client.get(endpoint) self.assertContains(response, 'src="/static/samplemap/js/sampleapp.js"') self.assertContains(response, 'href="/static/samplemap/css/sampleapp.css"') toolbar_pool.toolbars = old_pool toolbar_pool._discovered = True def test_toolbar_request_endpoint_validation(self): endpoint = self.get_admin_url(UserSettings, "get_toolbar") cms_page = create_page("toolbar-page", "col_two.html", "en") page_content = self.get_pagecontent_obj(cms_page) with self.login_user_context(self.get_superuser()): response = self.client.get( endpoint, data={ "obj_id": page_content.pk, "obj_type": "cms.pagecontent", "cms_path": get_object_edit_url(page_content), }, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Clipboard") response = self.client.get( endpoint, data={ "obj_id": page_content.pk, "obj_type": "cms.pagecontent", "cms_path": get_object_edit_url(page_content) + "q", # Invalid }, ) self.assertEqual(response.status_code, 200) # No clipboard exposed to invalid cms_path self.assertNotContains(response, "Clipboard") # Invalid app / model response = self.client.get( endpoint, data={"obj_id": cms_page.pk, "obj_type": "cms.somemodel", "cms_path": cms_page.get_absolute_url("en")}, ) self.assertEqual(response.status_code, 400) @override_settings(CMS_PERMISSION=True) def test_toolbar_request_endpoint_enforces_object_view_permission(self): # The obj_id/obj_type come straight from the client, so the endpoint # must enforce an object-level view check before rendering a toolbar # (which discloses the object's title/breadcrumb/edit URLs). A staff # user who cannot view a restricted page must be denied. endpoint = self.get_admin_url(UserSettings, "get_toolbar") cms_page = create_page("secret-page", "col_two.html", "en") page_content = self.get_pagecontent_obj(cms_page) # Grant view to one user only -> the page is now view-restricted. assign_user_to_page(cms_page, self.get_standard_user(), can_view=True) data = { "obj_id": page_content.pk, "obj_type": "cms.pagecontent", "cms_path": get_object_edit_url(page_content), } # A staff user without view permission is denied with the same generic # response as an unknown object (no existence oracle). with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.get(endpoint, data=data) self.assertEqual(response.status_code, 400) self.assertNotContains(response, "Clipboard", status_code=400) # A superuser can still render the toolbar. with self.login_user_context(self.get_superuser()): response = self.client.get(endpoint, data=data) self.assertEqual(response.status_code, 200) self.assertContains(response, "Clipboard") def test_toolbar_request_form(self): cms_page = create_page("toolbar-page", "col_two.html", "en") generic_obj = self._get_example_obj() # Valid forms form = RequestToolbarForm( { "obj_id": cms_page.pk, "obj_type": "cms.page", "cms_path": cms_page.get_absolute_url("en"), } ) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data["attached_obj"], cms_page) form = RequestToolbarForm( { "obj_id": generic_obj.pk, "obj_type": "placeholderapp.example1", "cms_path": cms_page.get_absolute_url("en"), } ) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data["attached_obj"], generic_obj) # Invalid forms form = RequestToolbarForm( { "obj_id": 1000, "obj_type": "cms.page", "cms_path": cms_page.get_absolute_url("en"), } ) self.assertFalse(form.is_valid()) form = RequestToolbarForm( { "obj_id": cms_page.pk, "obj_type": "cms.somemodel", "cms_path": cms_page.get_absolute_url("en"), } ) self.assertFalse(form.is_valid()) form = RequestToolbarForm( { "obj_id": cms_page.pk, "obj_type": "cms.page", "cms_path": "https://example.com/some-path/", } ) self.assertFalse(form.is_valid()) def test_no_page_anon(self): request = self.get_page_request(None, self.get_anon(), "/") toolbar = CMSToolbar(request) toolbar.populate() toolbar.post_template_populate() items = toolbar.get_left_items() + toolbar.get_right_items() self.assertEqual(len(items), 0) def test_no_page_staff(self): request = self.get_page_request(None, self.get_staff(), "/") toolbar = CMSToolbar(request) toolbar.populate() toolbar.post_template_populate() items = toolbar.get_left_items() + toolbar.get_right_items() self.assertEqual(len(items), 5, items) admin_items = toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER, "Test").get_items() self.assertEqual(len(admin_items), 12, admin_items) def test_no_page_superuser(self): request = self.get_page_request(None, self.get_superuser(), "/") toolbar = CMSToolbar(request) toolbar.populate() toolbar.post_template_populate() items = toolbar.get_left_items() + toolbar.get_right_items() self.assertEqual(len(items), 5) admin_items = toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER, "Test").get_items() self.assertEqual(len(admin_items), 13, admin_items) def test_anon(self): page = create_page("test", "nav_playground.html", "en") request = self.get_page_request(page, self.get_anon()) toolbar = CMSToolbar(request) items = toolbar.get_left_items() + toolbar.get_right_items() self.assertEqual(len(items), 0) def test_nonstaff(self): page = create_page("test", "nav_playground.html", "en") request = self.get_page_request(page, self.get_nonstaff()) toolbar = CMSToolbar(request) items = toolbar.get_left_items() + toolbar.get_right_items() self.assertEqual(len(items), 0) @override_settings(CMS_PERMISSION=True) def test_template_change_permission(self): """The template lives on the page content, so the *change* permission governs it -- not *change advanced settings*. """ page = create_page("test", "nav_playground.html", "en") page_content = self.get_pagecontent_obj(page) edit_url = get_object_edit_url(page_content) def get_template_items(user): request = self.get_page_request(page, user, edit_url) page_item = self.get_page_item(CMSToolbar(request)) return [item for item in page_item.items if force_str(getattr(item, "name", "")) == "Templates"] # Staff user with change page permissions only -- no advanced settings. staff_user = self.get_staff_user_with_no_permissions() self.add_permission(staff_user, "change_page") global_permission = self.add_global_permission(staff_user, can_change=True, can_delete=True) template_item = get_template_items(staff_user) self.assertEqual(len(template_item), 1) self.assertFalse(template_item[0].disabled) # Granting advanced settings on top changes nothing. global_permission.can_change_advanced_settings = True global_permission.save() staff_user = self.reload(staff_user) template_item = get_template_items(staff_user) self.assertEqual(len(template_item), 1) self.assertFalse(template_item[0].disabled) def test_markup(self): page = create_page("toolbar-page", "nav_playground.html", "en") page_content = self.get_pagecontent_obj(page) page_edit_url = get_object_edit_url(page_content) superuser = self.get_superuser() with self.login_user_context(superuser): response = self.client.get(page_edit_url) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "nav_playground.html") self.assertContains(response, '
Generic') def test_markup_link_custom_module(self): superuser = self.get_superuser() page = create_page("toolbar-page", "col_two.html", "en") page_content = self.get_pagecontent_obj(page) page_structure_url = get_object_structure_url(page_content) with self.login_user_context(superuser): response = self.client.get(page_structure_url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'href="LinkPlugin">') self.assertContains( response, '
Different Grouper' ) def test_placeholder_menu_items(self): superuser = self.get_superuser() page = create_page("toolbar-page", "col_two.html", "en") page_content = self.get_pagecontent_obj(page) page_structure_url = get_object_structure_url(page_content) placeholders = page_content.get_placeholders() self.assertEqual(len(placeholders), 2) # we get two placeholders with self.login_user_context(superuser): response = self.client.get(page_structure_url) self.assertEqual(response.status_code, 200) expected_bits = ( # Copy all in placeholder menu '', # Past in placeholder menu '', # Empty all placeholder menu (for both placeholders) '', # Extra items in placeholder menu '', '