dynamicweb/cms/tests/test_placeholder_admin.py

1092 lines
58 KiB
Python

from unittest.mock import patch
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.forms.models import model_to_dict
from django.test.utils import CaptureQueriesContext, override_settings
from cms.api import add_plugin, create_page
from cms.models import CMSPlugin, Placeholder, UserSettings
from cms.test_utils.project.placeholderapp.models import Example1
from cms.test_utils.testcases import CMSTestCase
from cms.utils.urlutils import admin_reverse
class PlaceholderAdminTestCase(CMSTestCase):
def test_add_plugin_endpoint(self):
"""
The Placeholder admin add_plugin endpoint works
"""
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="test")
plugins = placeholder.get_plugins("en").filter(plugin_type="LinkPlugin")
uri = self.get_add_plugin_uri(
placeholder=placeholder,
plugin_type="LinkPlugin",
language="en",
)
with self.login_user_context(superuser):
data = {"name": "A Link", "external_link": "https://www.django-cms.org"}
response = self.client.post(uri, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(plugins.count(), 1)
def test_add_plugin_rejected_in_disallowed_slot(self):
"""A plugin with allowed_slots cannot be added to a slot it does not allow."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="sidebar")
plugins = placeholder.get_plugins("en").filter(plugin_type="LinkPlugin")
uri = self.get_add_plugin_uri(
placeholder=placeholder,
plugin_type="LinkPlugin",
language="en",
)
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with patch.object(LinkPlugin, "allowed_slots", ["content"]):
with self.login_user_context(superuser):
data = {"name": "A Link", "external_link": "https://www.django-cms.org"}
response = self.client.post(uri, data)
self.assertEqual(response.status_code, 400)
self.assertEqual(plugins.count(), 0)
def test_add_plugin_allowed_in_listed_slot(self):
"""A plugin with allowed_slots can be added to a slot it allows."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="content")
plugins = placeholder.get_plugins("en").filter(plugin_type="LinkPlugin")
uri = self.get_add_plugin_uri(
placeholder=placeholder,
plugin_type="LinkPlugin",
language="en",
)
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with patch.object(LinkPlugin, "allowed_slots", ["content"]):
with self.login_user_context(superuser):
data = {"name": "A Link", "external_link": "https://www.django-cms.org"}
response = self.client.post(uri, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(plugins.count(), 1)
def test_add_plugin_with_ghost(self):
"""Adding a text plugin works. Text plugins create a ghost plugin."""
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="test")
plugins = placeholder.get_plugins("en").filter(plugin_type="TextPlugin")
uri = self.get_add_plugin_uri(
placeholder=placeholder,
plugin_type="TextPlugin",
language="en",
)
with self.login_user_context(superuser):
data = {"body": "<p>Some markup</p>"}
response = self.client.post(uri, data, follow=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(plugins.count(), 1)
def test_add_plugins_from_placeholder(self):
"""
User can copy plugins from one placeholder to another
"""
superuser = self.get_superuser()
source_placeholder = Placeholder.objects.create(slot="source")
target_placeholder = Placeholder.objects.create(slot="target")
source_plugin = self._add_plugin_to_placeholder(source_placeholder)
endpoint = self.get_copy_plugin_uri(source_plugin)
with self.login_user_context(superuser):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"target_language": "en",
"target_placeholder_id": target_placeholder.pk,
}
response = self.client.post(endpoint, data)
# Test that the target placeholder has the plugin copied from the source placeholder
self.assertEqual(response.status_code, 200)
self.assertTrue(source_placeholder.get_plugins("en").filter(pk=source_plugin.pk).exists())
self.assertTrue(target_placeholder.get_plugins("en").filter(plugin_type=source_plugin.plugin_type).exists())
def test_copy_plugins_rejected_into_disallowed_slot(self):
"""A plugin with allowed_slots cannot be copied into a slot it does not allow."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
source_placeholder = Placeholder.objects.create(slot="content")
target_placeholder = Placeholder.objects.create(slot="sidebar")
source_plugin = self._add_plugin_to_placeholder(source_placeholder)
endpoint = self.get_copy_plugin_uri(source_plugin)
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with patch.object(LinkPlugin, "allowed_slots", ["content"]):
with self.login_user_context(superuser):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"target_language": "en",
"target_placeholder_id": target_placeholder.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 400)
# Nothing was copied into the disallowed target slot.
self.assertFalse(target_placeholder.get_plugins("en").exists())
def test_copy_plugins_allowed_into_listed_slot(self):
"""A plugin with allowed_slots can be copied into a slot it allows."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
source_placeholder = Placeholder.objects.create(slot="content")
target_placeholder = Placeholder.objects.create(slot="sidebar")
source_plugin = self._add_plugin_to_placeholder(source_placeholder)
endpoint = self.get_copy_plugin_uri(source_plugin)
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
with patch.object(LinkPlugin, "allowed_slots", ["content", "sidebar"]):
with self.login_user_context(superuser):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"target_language": "en",
"target_placeholder_id": target_placeholder.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertTrue(
target_placeholder.get_plugins("en").filter(plugin_type="LinkPlugin").exists()
)
def test_copy_plugins_to_clipboard(self):
"""
User can copy plugins from a placeholder to the clipboard
"""
superuser = self.get_superuser()
user_settings = UserSettings.objects.create(
language="en",
user=superuser,
clipboard=Placeholder.objects.create(),
)
source_placeholder = Placeholder.objects.create(slot="source")
source_plugin = self._add_plugin_to_placeholder(source_placeholder)
endpoint = self.get_copy_plugin_uri(source_plugin)
with self.login_user_context(superuser):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"source_plugin_id": source_plugin.pk,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
response = self.client.post(endpoint, data)
# Test that the target placeholder has the plugin copied from the source placeholder (clipboard)
self.assertEqual(response.status_code, 200)
self.assertTrue(source_placeholder.get_plugins("en").filter(pk=source_plugin.pk).exists())
self.assertTrue(
user_settings.clipboard.get_plugins("en").filter(plugin_type=source_plugin.plugin_type).exists()
)
def test_copy_placeholder_to_clipboard(self):
"""
User can copy a placeholder to the clipboard
"""
superuser = self.get_superuser()
user_settings = UserSettings.objects.create(
language="en",
user=superuser,
clipboard=Placeholder.objects.create(),
)
user_settings.clipboard.source = user_settings
user_settings.clipboard.save()
source_placeholder = Placeholder.objects.create(slot='source')
source_plugin = self._add_plugin_to_placeholder(source_placeholder)
endpoint = self.get_copy_plugin_uri(source_plugin)
with self.login_user_context(superuser):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertTrue(source_placeholder.get_plugins("en").filter(pk=source_plugin.pk).exists())
self.assertTrue(user_settings.clipboard.get_plugins("en").filter(plugin_type="PlaceholderPlugin").exists())
def _create_clipboard_with_placeholder(self, user, language="en"):
"""Puts a PlaceholderPlugin with one LinkPlugin (``language``) on the
user's clipboard, as "Copy all" does, and returns the PlaceholderPlugin."""
user_settings = UserSettings.objects.create(
language=language,
user=user,
clipboard=Placeholder.objects.create(slot="clipboard"),
)
placeholder_plugin = add_plugin(user_settings.clipboard, "PlaceholderPlugin", language)
add_plugin(
placeholder_plugin.placeholder_ref,
"LinkPlugin",
language,
name="Pasted Link",
external_link="https://www.django-cms.org",
)
return placeholder_plugin
def test_paste_placeholder_shifts_positions_of_target_language(self):
"""
Pasting a placeholder from the clipboard shifts the positions of the
existing plugins in the *target* language, even when the clipboard
content was copied in a different language.
Regression test for issue 09: ``_paste_placeholder`` computed the
position offset from the clipboard plugin's language instead of the
target language. With existing plugins only in the target language,
no shift happened and the pasted plugins collided with the existing
positions (IntegrityError on the placeholder/language/position
unique constraint).
"""
superuser = self.get_superuser()
# Clipboard content was copied while working in "en".
placeholder_plugin = self._create_clipboard_with_placeholder(superuser, language="en")
# The paste target already holds two "de" plugins (positions 1 and 2).
target_placeholder = Placeholder.objects.create(slot="target")
for name in ("Existing 1", "Existing 2"):
add_plugin(
target_placeholder,
"LinkPlugin",
"de",
name=name,
external_link="https://www.django-cms.org",
)
endpoint = self.get_move_plugin_uri(placeholder_plugin, language="de")
with self.login_user_context(superuser):
data = {
"plugin_id": placeholder_plugin.pk,
"placeholder_id": target_placeholder.pk,
"move_a_copy": "true",
"target_language": "de",
"target_position": 1, # paste before the existing plugins
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
plugins = list(target_placeholder.get_plugins("de").order_by("position"))
self.assertEqual(len(plugins), 3)
positions = [plugin.position for plugin in plugins]
self.assertEqual(len(set(positions)), len(positions), f"Plugin positions collide: {positions}")
# The pasted plugin sits before the pre-existing plugins.
names = [plugin.get_bound_plugin().name for plugin in plugins]
self.assertEqual(names, ["Pasted Link", "Existing 1", "Existing 2"])
def test_paste_placeholder_clears_cache_of_target_language(self):
"""
Pasting a placeholder from the clipboard invalidates the target
placeholder's cache for the *target* language.
Regression test for issue 09: ``_paste_placeholder`` cleared the
cache for the clipboard plugin's language, so the target language
kept serving stale content.
"""
superuser = self.get_superuser()
# Clipboard content was copied while working in "en", pasted into "de".
placeholder_plugin = self._create_clipboard_with_placeholder(superuser, language="en")
target_placeholder = Placeholder.objects.create(slot="target")
endpoint = self.get_move_plugin_uri(placeholder_plugin, language="de")
with self.login_user_context(superuser):
data = {
"plugin_id": placeholder_plugin.pk,
"placeholder_id": target_placeholder.pk,
"move_a_copy": "true",
"target_language": "de",
"target_position": 1,
}
with patch.object(Placeholder, "clear_cache", autospec=True) as mocked_clear_cache:
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
cleared_languages = [
call.args[1]
for call in mocked_clear_cache.call_args_list
if call.args[0] == target_placeholder
]
self.assertEqual(cleared_languages, ["de"])
def test_edit_plugin_endpoint(self):
"""
The Placeholder admin edit_plugins endpoint works
"""
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="edit_plugin_placeholder")
plugin = self._add_plugin_to_placeholder(placeholder)
endpoint = self.get_change_plugin_uri(plugin)
with self.login_user_context(superuser):
data = model_to_dict(plugin, fields=["name", "external_link"])
data["name"] = "Contents modified"
response = self.client.post(endpoint, data)
plugin.refresh_from_db()
self.assertEqual(response.status_code, 200)
self.assertEqual(plugin.name, data["name"])
def test_delete_plugin_endpoint(self):
"""
The Placeholder admin delete_plugin endpoint works
"""
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="source")
plugin = self._add_plugin_to_placeholder(placeholder)
endpoint = self.get_delete_plugin_uri(plugin)
with self.login_user_context(superuser):
data = {"post": True}
response = self.client.post(endpoint, data)
self.assertContains(response, '<div class="success"></div>')
self.assertFalse(CMSPlugin.objects.filter(pk=plugin.pk).exists())
def test_delete_plugin_confirmation_context(self):
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="source")
plugin = self._add_plugin_to_placeholder(placeholder)
endpoint = self.get_delete_plugin_uri(plugin)
with patch(
"cms.admin.placeholderadmin.PlaceholderAdmin.delete_confirmation_max_display",
1,
create=True,
), self.login_user_context(superuser):
response = self.client.get(endpoint)
self.assertEqual(response.status_code, 200)
self.assertIn("delete_confirmation_max_display", response.context)
self.assertEqual(response.context["delete_confirmation_max_display"], 1)
def test_clear_placeholder_endpoint(self):
"""
The Placeholder admin delete_plugin endpoint works
"""
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="source")
self._add_plugin_to_placeholder(placeholder)
endpoint = self.get_clear_placeholder_url(placeholder)
with self.login_user_context(superuser):
response = self.client.post(endpoint, {"test": 0})
self.assertEqual(response.status_code, 200)
self.assertEqual(placeholder.get_plugins("en").count(), 0)
def test_clear_placeholder_confirmation_context(self):
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="source")
self._add_plugin_to_placeholder(placeholder)
endpoint = self.get_clear_placeholder_url(placeholder)
with self.login_user_context(superuser):
response = self.client.get(endpoint)
self.assertEqual(response.status_code, 200)
self.assertIn("delete_confirmation_max_display", response.context)
self.assertIsNone(response.context["delete_confirmation_max_display"])
def test_clear_clipboard_requires_post(self):
"""
Clearing the clipboard mutates state immediately (no confirmation
page), so it must reject CSRF-exempt GET requests. A GET leaves the
clipboard untouched; a POST clears it.
"""
superuser = self.get_superuser()
user_settings = UserSettings.objects.create(
language="en",
user=superuser,
clipboard=Placeholder.objects.create(slot="clipboard"),
)
self._add_plugin_to_placeholder(user_settings.clipboard)
endpoint = self.get_clear_placeholder_url(user_settings.clipboard)
with self.login_user_context(superuser):
get_response = self.client.get(endpoint)
self.assertEqual(get_response.status_code, 405)
# The clipboard was not cleared by the GET request.
self.assertEqual(user_settings.clipboard.get_plugins("en").count(), 1)
post_response = self.client.post(endpoint)
self.assertEqual(post_response.status_code, 200)
self.assertEqual(user_settings.clipboard.get_plugins("en").count(), 0)
def _fill_page_body(self, page, lang):
ph_en = page.get_placeholders(lang).get(slot="placeholder")
# add misc plugins
mcol1 = add_plugin(ph_en, "MultiColumnPlugin", lang, position="first-child")
add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol1)
col2 = add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol1)
mcol2 = add_plugin(ph_en, "MultiColumnPlugin", lang, position="first-child", target=col2)
add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol2)
col4 = add_plugin(ph_en, "ColumnPlugin", lang, position="first-child", target=mcol2)
# add *nested* plugin without model
add_plugin(ph_en, "NoCustomModel", lang, target=col4)
# add *nested* link and text plugins
add_plugin(ph_en, "LinkPlugin", lang, target=col4, name="A Link", external_link="https://www.django-cms.org")
add_plugin(ph_en, "StylePlugin", lang, target=col4, tag_type="div")
@override_settings(
CMS_PLACEHOLDER_CONF={
"simple.html": {"excluded_plugins": ["InheritPlugin"]},
}
)
def test_object_edit_endpoint(self):
page = create_page("Page 1", "simple.html", "en")
self._fill_page_body(page, "en")
content = page.get_content_obj()
content_type = ContentType.objects.get(app_label="cms", model="pagecontent")
user = self.get_superuser()
settings = UserSettings.objects.create(
language="en", user=user, clipboard=Placeholder.objects.create(slot="clipboard")
)
settings.clipboard.source = settings
settings.clipboard.save()
endpoint = admin_reverse(
"cms_placeholder_render_object_edit",
args=(
content_type.pk,
content.pk,
),
)
with self.login_user_context(user):
with CaptureQueriesContext(connection) as queries:
response = self.client.get(endpoint)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(queries), 15, "\n".join([f"{i}. {q['sql']}" for i, q in enumerate(queries, start=1)]))
# Queries
# 1. SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1 LIMIT 21
# 2. SELECT "django_content_type"."id", "django_content_type"."app_label", "django_content_type"."model" FROM "django_content_type" WHERE "django_content_type"."id" = 18 LIMIT 21
# 3. SELECT "cms_pagecontent"."id", "cms_pagecontent"."language", "cms_pagecontent"."title", "cms_pagecontent"."page_title", "cms_pagecontent"."menu_title", "cms_pagecontent"."meta_description", "cms_pagecontent"."redirect", "cms_pagecontent"."page_id", "cms_pagecontent"."creation_date", "cms_pagecontent"."created_by", "cms_pagecontent"."changed_by", "cms_pagecontent"."changed_date", "cms_pagecontent"."in_navigation", "cms_pagecontent"."soft_root", "cms_pagecontent"."template", "cms_pagecontent"."limit_visibility_in_menu", "cms_pagecontent"."xframe_options", "cms_page"."id", "cms_page"."path", "cms_page"."depth", "cms_page"."numchild", "cms_page"."parent_id", "cms_page"."site_id", "cms_page"."created_by", "cms_page"."changed_by", "cms_page"."creation_date", "cms_page"."changed_date", "cms_page"."reverse_id", "cms_page"."navigation_extenders", "cms_page"."login_required", "cms_page"."is_home", "cms_page"."application_urls", "cms_page"."application_namespace", "cms_page"."is_page_type" FROM "cms_pagecontent" INNER JOIN "cms_page" ON ("cms_pagecontent"."page_id" = "cms_page"."id") WHERE "cms_pagecontent"."id" = 1 LIMIT 21
# 4. SELECT "cms_usersettings"."id", "cms_usersettings"."user_id", "cms_usersettings"."language", "cms_usersettings"."clipboard_id", "cms_placeholder"."id", "cms_placeholder"."slot", "cms_placeholder"."default_width", "cms_placeholder"."content_type_id", "cms_placeholder"."object_id" FROM "cms_usersettings" LEFT OUTER JOIN "cms_placeholder" ON ("cms_usersettings"."clipboard_id" = "cms_placeholder"."id") WHERE "cms_usersettings"."user_id" = 1 LIMIT 21
# 5. SELECT "django_site"."id", "django_site"."domain", "django_site"."name" FROM "django_site" ORDER BY "django_site"."name" ASC
# 6. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "cms_placeholder"."id", "cms_placeholder"."slot", "cms_placeholder"."default_width", "cms_placeholder"."content_type_id", "cms_placeholder"."object_id" FROM "cms_cmsplugin" INNER JOIN "cms_placeholder" ON ("cms_cmsplugin"."placeholder_id" = "cms_placeholder"."id") WHERE "cms_cmsplugin"."placeholder_id" = 2 ORDER BY "cms_cmsplugin"."position" ASC LIMIT 1
# 7. SELECT 1 AS "a" FROM "cms_pagepermission" INNER JOIN "cms_page" ON ("cms_pagepermission"."page_id" = "cms_page"."id") WHERE (("cms_pagepermission"."page_id" = 1 AND ("cms_pagepermission"."grant_on" = 5 OR "cms_pagepermission"."grant_on" = 3 OR "cms_pagepermission"."grant_on" = 1)) AND "cms_pagepermission"."can_view") LIMIT 1
# 8. SELECT "cms_pagecontent"."id", "cms_pagecontent"."language", "cms_pagecontent"."title", "cms_pagecontent"."page_title", "cms_pagecontent"."menu_title", "cms_pagecontent"."meta_description", "cms_pagecontent"."redirect", "cms_pagecontent"."page_id", "cms_pagecontent"."creation_date", "cms_pagecontent"."created_by", "cms_pagecontent"."changed_by", "cms_pagecontent"."changed_date", "cms_pagecontent"."in_navigation", "cms_pagecontent"."soft_root", "cms_pagecontent"."template", "cms_pagecontent"."limit_visibility_in_menu", "cms_pagecontent"."xframe_options" FROM "cms_pagecontent" WHERE "cms_pagecontent"."page_id" = 1
# 9. SELECT "extensionapp_mypagecontentextension"."id", "extensionapp_mypagecontentextension"."extended_object_id", "extensionapp_mypagecontentextension"."extra_title" FROM "extensionapp_mypagecontentextension" WHERE "extensionapp_mypagecontentextension"."extended_object_id" = 1 LIMIT 21
# 10. SELECT "extensionapp_mypageextension"."id", "extensionapp_mypageextension"."extended_object_id", "extensionapp_mypageextension"."extra" FROM "extensionapp_mypageextension" WHERE "extensionapp_mypageextension"."extended_object_id" = 1 LIMIT 21
# 11. SELECT "cms_placeholder"."id", "cms_placeholder"."slot", "cms_placeholder"."default_width", "cms_placeholder"."content_type_id", "cms_placeholder"."object_id" FROM "cms_placeholder" WHERE ("cms_placeholder"."content_type_id" = 18 AND "cms_placeholder"."object_id" = 1)
# 12. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date" FROM "cms_cmsplugin" WHERE ("cms_cmsplugin"."language" = 'en' AND "cms_cmsplugin"."placeholder_id" IN (1)) ORDER BY "cms_cmsplugin"."position" ASC
# 13. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "multicolumn_multicolumns"."cmsplugin_ptr_id" FROM "multicolumn_multicolumns" INNER JOIN "cms_cmsplugin" ON ("multicolumn_multicolumns"."cmsplugin_ptr_id" = "cms_cmsplugin"."id") WHERE "multicolumn_multicolumns"."cmsplugin_ptr_id" IN (1, 4) ORDER BY "cms_cmsplugin"."position" ASC
# 14. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "link_link"."cmsplugin_ptr_id", "link_link"."name", "link_link"."external_link" FROM "link_link" INNER JOIN "cms_cmsplugin" ON ("link_link"."cmsplugin_ptr_id" = "cms_cmsplugin"."id") WHERE "link_link"."cmsplugin_ptr_id" IN (7) ORDER BY "cms_cmsplugin"."position" ASC
# 15. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "style_style"."cmsplugin_ptr_id", "style_style"."label", "style_style"."tag_type", "style_style"."class_name", "style_style"."additional_classes" FROM "style_style" INNER JOIN "cms_cmsplugin" ON ("style_style"."cmsplugin_ptr_id" = "cms_cmsplugin"."id") WHERE "style_style"."cmsplugin_ptr_id" IN (9) ORDER BY "cms_cmsplugin"."position" ASC
@override_settings(
CMS_PLACEHOLDER_CONF={
"simple.html": {"excluded_plugins": ["InheritPlugin"]},
}
)
def test_object_structure_endpoint(self):
page = create_page("Page 1", "simple.html", "en")
self._fill_page_body(page, "en")
content = page.get_content_obj()
content_type = ContentType.objects.get(app_label="cms", model="pagecontent")
user = self.get_superuser()
settings = UserSettings.objects.create(
language="en", user=user, clipboard=Placeholder.objects.create(slot="clipboard")
)
settings.clipboard.source = settings
settings.clipboard.save()
endpoint = admin_reverse(
"cms_placeholder_render_object_structure",
args=(
content_type.pk,
content.pk,
),
)
with self.login_user_context(user):
with CaptureQueriesContext(connection) as queries:
response = self.client.get(endpoint)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(queries), 17, "\n".join([f"{i}. {q['sql']}" for i, q in enumerate(queries, start=1)]))
# Queries
# 1. SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1 LIMIT 21
# 2. SELECT "django_content_type"."id", "django_content_type"."app_label", "django_content_type"."model" FROM "django_content_type" WHERE "django_content_type"."id" = 18 LIMIT 21
# 3. SELECT "cms_pagecontent"."id", "cms_pagecontent"."language", "cms_pagecontent"."title", "cms_pagecontent"."page_title", "cms_pagecontent"."menu_title", "cms_pagecontent"."meta_description", "cms_pagecontent"."redirect", "cms_pagecontent"."page_id", "cms_pagecontent"."creation_date", "cms_pagecontent"."created_by", "cms_pagecontent"."changed_by", "cms_pagecontent"."changed_date", "cms_pagecontent"."in_navigation", "cms_pagecontent"."soft_root", "cms_pagecontent"."template", "cms_pagecontent"."limit_visibility_in_menu", "cms_pagecontent"."xframe_options", "cms_page"."id", "cms_page"."path", "cms_page"."depth", "cms_page"."numchild", "cms_page"."parent_id", "cms_page"."site_id", "cms_page"."created_by", "cms_page"."changed_by", "cms_page"."creation_date", "cms_page"."changed_date", "cms_page"."reverse_id", "cms_page"."navigation_extenders", "cms_page"."login_required", "cms_page"."is_home", "cms_page"."application_urls", "cms_page"."application_namespace", "cms_page"."is_page_type" FROM "cms_pagecontent" INNER JOIN "cms_page" ON ("cms_pagecontent"."page_id" = "cms_page"."id") WHERE "cms_pagecontent"."id" = 1 LIMIT 21
# 4. SELECT "cms_usersettings"."id", "cms_usersettings"."user_id", "cms_usersettings"."language", "cms_usersettings"."clipboard_id", "cms_placeholder"."id", "cms_placeholder"."slot", "cms_placeholder"."default_width", "cms_placeholder"."content_type_id", "cms_placeholder"."object_id" FROM "cms_usersettings" LEFT OUTER JOIN "cms_placeholder" ON ("cms_usersettings"."clipboard_id" = "cms_placeholder"."id") WHERE "cms_usersettings"."user_id" = 1 LIMIT 21
# 5. SELECT "cms_pagecontent"."id", "cms_pagecontent"."language", "cms_pagecontent"."title", "cms_pagecontent"."page_title", "cms_pagecontent"."menu_title", "cms_pagecontent"."meta_description", "cms_pagecontent"."redirect", "cms_pagecontent"."page_id", "cms_pagecontent"."creation_date", "cms_pagecontent"."created_by", "cms_pagecontent"."changed_by", "cms_pagecontent"."changed_date", "cms_pagecontent"."in_navigation", "cms_pagecontent"."soft_root", "cms_pagecontent"."template", "cms_pagecontent"."limit_visibility_in_menu", "cms_pagecontent"."xframe_options" FROM "cms_pagecontent" WHERE "cms_pagecontent"."page_id" = 1
# 6. SELECT "django_site"."id", "django_site"."domain", "django_site"."name" FROM "django_site" ORDER BY "django_site"."name" ASC
# 7. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "cms_placeholder"."id", "cms_placeholder"."slot", "cms_placeholder"."default_width", "cms_placeholder"."content_type_id", "cms_placeholder"."object_id" FROM "cms_cmsplugin" INNER JOIN "cms_placeholder" ON ("cms_cmsplugin"."placeholder_id" = "cms_placeholder"."id") WHERE "cms_cmsplugin"."placeholder_id" = 2 ORDER BY "cms_cmsplugin"."position" ASC LIMIT 1
# 8. SELECT 1 AS "a" FROM "cms_pagepermission" INNER JOIN "cms_page" ON ("cms_pagepermission"."page_id" = "cms_page"."id") WHERE (("cms_pagepermission"."page_id" = 1 AND ("cms_pagepermission"."grant_on" = 5 OR "cms_pagepermission"."grant_on" = 3 OR "cms_pagepermission"."grant_on" = 1)) AND "cms_pagepermission"."can_view") LIMIT 1
# 9. SELECT "cms_pagecontent"."id", "cms_pagecontent"."language", "cms_pagecontent"."title", "cms_pagecontent"."page_title", "cms_pagecontent"."menu_title", "cms_pagecontent"."meta_description", "cms_pagecontent"."redirect", "cms_pagecontent"."page_id", "cms_pagecontent"."creation_date", "cms_pagecontent"."created_by", "cms_pagecontent"."changed_by", "cms_pagecontent"."changed_date", "cms_pagecontent"."in_navigation", "cms_pagecontent"."soft_root", "cms_pagecontent"."template", "cms_pagecontent"."limit_visibility_in_menu", "cms_pagecontent"."xframe_options" FROM "cms_pagecontent" WHERE "cms_pagecontent"."page_id" = 1
# 10. SELECT "extensionapp_mypagecontentextension"."id", "extensionapp_mypagecontentextension"."extended_object_id", "extensionapp_mypagecontentextension"."extra_title" FROM "extensionapp_mypagecontentextension" WHERE "extensionapp_mypagecontentextension"."extended_object_id" = 1 LIMIT 21
# 11. SELECT "extensionapp_mypageextension"."id", "extensionapp_mypageextension"."extended_object_id", "extensionapp_mypageextension"."extra" FROM "extensionapp_mypageextension" WHERE "extensionapp_mypageextension"."extended_object_id" = 1 LIMIT 21
# 12. SELECT "cms_placeholder"."id", "cms_placeholder"."slot", "cms_placeholder"."default_width", "cms_placeholder"."content_type_id", "cms_placeholder"."object_id" FROM "cms_placeholder" WHERE ("cms_placeholder"."content_type_id" = 18 AND "cms_placeholder"."object_id" = 1)
# 13. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date" FROM "cms_cmsplugin" WHERE ("cms_cmsplugin"."language" = 'en' AND "cms_cmsplugin"."placeholder_id" IN (1)) ORDER BY "cms_cmsplugin"."position" ASC
# 14. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "multicolumn_multicolumns"."cmsplugin_ptr_id" FROM "multicolumn_multicolumns" INNER JOIN "cms_cmsplugin" ON ("multicolumn_multicolumns"."cmsplugin_ptr_id" = "cms_cmsplugin"."id") WHERE "multicolumn_multicolumns"."cmsplugin_ptr_id" IN (1, 4) ORDER BY "cms_cmsplugin"."position" ASC
# 15. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "link_link"."cmsplugin_ptr_id", "link_link"."name", "link_link"."external_link" FROM "link_link" INNER JOIN "cms_cmsplugin" ON ("link_link"."cmsplugin_ptr_id" = "cms_cmsplugin"."id") WHERE "link_link"."cmsplugin_ptr_id" IN (7) ORDER BY "cms_cmsplugin"."position" ASC
# 16. SELECT "cms_cmsplugin"."language" AS "language" FROM "cms_cmsplugin" WHERE "cms_cmsplugin"."placeholder_id" = 1 ORDER BY "cms_cmsplugin"."position" ASC
# 17. SELECT "cms_cmsplugin"."id", "cms_cmsplugin"."placeholder_id", "cms_cmsplugin"."parent_id", "cms_cmsplugin"."position", "cms_cmsplugin"."language", "cms_cmsplugin"."plugin_type", "cms_cmsplugin"."creation_date", "cms_cmsplugin"."changed_date", "style_style"."cmsplugin_ptr_id", "style_style"."label", "style_style"."tag_type", "style_style"."class_name", "style_style"."additional_classes" FROM "style_style" INNER JOIN "cms_cmsplugin" ON ("style_style"."cmsplugin_ptr_id" = "cms_cmsplugin"."id") WHERE "style_style"."cmsplugin_ptr_id" IN (9) ORDER BY "cms_cmsplugin"."position" ASC
def test_get_toolbar_endpoint(self):
"""Toolbar endpoint returns the toolbar including the page menu when called from the edit endpoint"""
page = create_page("Page 1", "simple.html", "en")
self._fill_page_body(page, "en")
content = page.get_content_obj()
content_type = ContentType.objects.get(app_label="cms", model="pagecontent")
user = self.get_superuser()
toolbar_endpoint = admin_reverse("cms_usersettings_get_toolbar")
edit_endpoint = admin_reverse(
"cms_placeholder_render_object_edit",
args=(
content_type.pk,
content.pk,
),
)
with self.login_user_context(user):
response = self.client.get(f"{toolbar_endpoint}?obj_id={content.id}&obj_type=cms.pagecontent&cms_path={edit_endpoint}")
self.assertContains(response, '<span>Page<span class="cms-icon cms-icon-arrow"></span></span>') # Contains page menu
class PlaceholderAdminSecurityTestCase(CMSTestCase):
def test_move_plugin_rejects_cyclic_reparenting(self):
"""A plugin must not be reparented under one of its own descendants.
Doing so creates a cycle in the plugin tree, which makes the recursive
descendant/ancestor queries loop indefinitely (denial of service).
"""
superuser = self.get_superuser()
placeholder = Placeholder.objects.create(slot="source")
parent = add_plugin(
placeholder, "LinkPlugin", "en", name="parent", external_link="https://example.com"
)
child = add_plugin(
placeholder, "LinkPlugin", "en", name="child", external_link="https://example.com", target=parent
)
endpoint = self.get_move_plugin_uri(parent)
with self.login_user_context(superuser):
data = {
"plugin_id": parent.pk,
"target_language": "en",
"target_position": child.position,
# child is a descendant of parent: reparenting parent under it
# would create a cycle.
"plugin_parent": child.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 400)
parent.refresh_from_db()
child.refresh_from_db()
# No cycle: parent stays at the root, child stays under parent.
self.assertIsNone(parent.parent_id)
self.assertEqual(child.parent_id, parent.pk)
def test_move_plugin_rejected_into_disallowed_slot(self):
"""A plugin with allowed_slots cannot be moved into a slot it does not allow."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
source = Placeholder.objects.create(slot="content")
target = Placeholder.objects.create(slot="sidebar")
plugin = add_plugin(source, "LinkPlugin", "en", name="link", external_link="https://example.com")
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
endpoint = self.get_move_plugin_uri(plugin)
with patch.object(LinkPlugin, "allowed_slots", ["content"]):
with self.login_user_context(superuser):
data = {
"plugin_id": plugin.pk,
"placeholder_id": target.pk,
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 400)
plugin.refresh_from_db()
# The plugin stays in its original placeholder.
self.assertEqual(plugin.placeholder_id, source.pk)
def test_move_plugin_rejected_when_descendant_disallowed_in_slot(self):
"""Moving a subtree is rejected if any descendant is disallowed in the target slot."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
source = Placeholder.objects.create(slot="content")
target = Placeholder.objects.create(slot="sidebar")
# The moved (root) plugin is allowed in every slot, but its child is restricted.
parent = add_plugin(source, "StylePlugin", "en", label="wrap")
add_plugin(source, "LinkPlugin", "en", name="link", external_link="https://example.com", target=parent)
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
endpoint = self.get_move_plugin_uri(parent)
with patch.object(LinkPlugin, "allowed_slots", ["content"]):
with self.login_user_context(superuser):
data = {
"plugin_id": parent.pk,
"placeholder_id": target.pk,
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 400)
parent.refresh_from_db()
# The subtree stays in its original placeholder.
self.assertEqual(parent.placeholder_id, source.pk)
def test_move_plugin_allowed_into_listed_slot(self):
"""A plugin with allowed_slots can be moved into a slot it allows."""
from cms.plugin_pool import plugin_pool
superuser = self.get_superuser()
source = Placeholder.objects.create(slot="content")
target = Placeholder.objects.create(slot="sidebar")
plugin = add_plugin(source, "LinkPlugin", "en", name="link", external_link="https://example.com")
LinkPlugin = plugin_pool.get_plugin("LinkPlugin")
endpoint = self.get_move_plugin_uri(plugin)
with patch.object(LinkPlugin, "allowed_slots", ["content", "sidebar"]):
with self.login_user_context(superuser):
data = {
"plugin_id": plugin.pk,
"placeholder_id": target.pk,
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
plugin.refresh_from_db()
# The plugin moved into the allowed target placeholder.
self.assertEqual(plugin.placeholder_id, target.pk)
def test_copy_plugin_to_clipboard_requires_source_permission(self):
"""Copying a plugin to the clipboard must check source-side permission.
Otherwise a staff user with no access to a placeholder can exfiltrate
its (secret) plugin content by copying it into their own clipboard.
"""
attacker = self._create_user(
"attacker", is_staff=True, is_superuser=False, permissions=["add_link"]
)
user_settings = UserSettings.objects.create(
language="en",
user=attacker,
clipboard=Placeholder.objects.create(),
)
user_settings.clipboard.source = user_settings
user_settings.clipboard.save()
# A placeholder the attacker has no permission on.
victim_placeholder = Placeholder.objects.create(slot="secret")
victim_plugin = add_plugin(
victim_placeholder,
"LinkPlugin",
"en",
name="VictimSecretLink",
external_link="https://secret.example.com",
)
endpoint = self.get_copy_plugin_uri(victim_plugin)
with self.login_user_context(attacker):
data = {
"source_language": "en",
"source_placeholder_id": victim_placeholder.pk,
"source_plugin_id": victim_plugin.pk,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 403)
self.assertFalse(user_settings.clipboard.get_plugins("en").exists())
def test_copy_placeholder_to_clipboard_requires_source_permission(self):
"""Copying a whole placeholder to the clipboard must check source-side
permission as well."""
attacker = self._create_user(
"attacker", is_staff=True, is_superuser=False, permissions=["add_link"]
)
user_settings = UserSettings.objects.create(
language="en",
user=attacker,
clipboard=Placeholder.objects.create(),
)
user_settings.clipboard.source = user_settings
user_settings.clipboard.save()
victim_placeholder = Placeholder.objects.create(slot="secret")
add_plugin(
victim_placeholder,
"LinkPlugin",
"en",
name="VictimSecretLink",
external_link="https://secret.example.com",
)
endpoint = self.get_copy_placeholder_uri(victim_placeholder)
with self.login_user_context(attacker):
data = {
"source_language": "en",
"source_placeholder_id": victim_placeholder.pk,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 403)
self.assertFalse(user_settings.clipboard.get_plugins("en").exists())
def test_copy_plugin_to_clipboard_requires_add_permission(self):
"""Copying a plugin to the clipboard must check add permission for the
plugin type.
A staff user who is missing the plugin's add permission must not be
able to copy it into their own clipboard, even when the plugin lives
in a placeholder they would otherwise be allowed to read.
"""
# No plugin permissions at all: cannot add a LinkPlugin anywhere.
user = self._create_user("editor", is_staff=True, is_superuser=False)
user_settings = UserSettings.objects.create(
language="en",
user=user,
clipboard=Placeholder.objects.create(),
)
user_settings.clipboard.source = user_settings
user_settings.clipboard.save()
source_placeholder = Placeholder.objects.create(slot="source")
source_plugin = add_plugin(
source_placeholder,
"LinkPlugin",
"en",
name="A Link",
external_link="https://www.django-cms.org",
)
endpoint = self.get_copy_plugin_uri(source_plugin)
with self.login_user_context(user):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"source_plugin_id": source_plugin.pk,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 403)
self.assertFalse(user_settings.clipboard.get_plugins("en").exists())
def test_copy_placeholder_to_clipboard_requires_add_permission(self):
"""Copying a whole placeholder to the clipboard must check add
permission for the contained plugin types as well."""
# No plugin permissions at all: cannot add a LinkPlugin anywhere.
user = self._create_user("editor", is_staff=True, is_superuser=False)
user_settings = UserSettings.objects.create(
language="en",
user=user,
clipboard=Placeholder.objects.create(),
)
user_settings.clipboard.source = user_settings
user_settings.clipboard.save()
source_placeholder = Placeholder.objects.create(slot="source")
add_plugin(
source_placeholder,
"LinkPlugin",
"en",
name="A Link",
external_link="https://www.django-cms.org",
)
endpoint = self.get_copy_placeholder_uri(source_placeholder)
with self.login_user_context(user):
data = {
"source_language": "en",
"source_placeholder_id": source_placeholder.pk,
"target_language": "en",
"target_placeholder_id": user_settings.clipboard.pk,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 403)
self.assertFalse(user_settings.clipboard.get_plugins("en").exists())
def _get_clipboard(self, user, with_source=True):
user_settings = UserSettings.objects.create(
language="en",
user=user,
clipboard=Placeholder.objects.create(slot="clipboard"),
)
if with_source:
user_settings.clipboard.source = user_settings
user_settings.clipboard.save()
return user_settings.clipboard
def test_paste_plugin_requires_source_permission(self):
"""Pasting a plugin must check permission on the placeholder it is
pasted *from*.
The plugin to paste is identified by a client-supplied id, so a staff
user could otherwise name any plugin in the installation and have its
content copied into a placeholder they do control -- their own
clipboard, for instance.
"""
attacker = self._create_user(
"attacker", is_staff=True, is_superuser=False, permissions=["add_link"]
)
clipboard = self._get_clipboard(attacker)
# A placeholder the attacker has no permission on.
victim_placeholder = Placeholder.objects.create(slot="secret")
victim_plugin = add_plugin(
victim_placeholder,
"LinkPlugin",
"en",
name="VictimSecretLink",
external_link="https://secret.example.com",
)
endpoint = self.get_move_plugin_uri(victim_plugin)
with self.login_user_context(attacker):
data = {
"plugin_id": victim_plugin.pk,
"placeholder_id": clipboard.pk,
"move_a_copy": "true",
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 403)
self.assertFalse(clipboard.get_plugins("en").exists())
def test_paste_placeholder_requires_source_permission(self):
"""Pasting a placeholder reference must check permission on the
placeholder holding it.
A clipboard belongs to a single user, so its contents must not be
pasteable by anybody else.
"""
attacker = self._create_user(
"attacker", is_staff=True, is_superuser=False, permissions=["add_link"]
)
victim = self._create_user(
"victim", is_staff=True, is_superuser=False, permissions=["add_link"]
)
attacker_clipboard = self._get_clipboard(attacker)
victim_clipboard = self._get_clipboard(victim)
# The victim copied a whole placeholder into their clipboard.
reference = add_plugin(
victim_clipboard, "PlaceholderPlugin", "en", name="Victim clipboard"
)
add_plugin(
reference.placeholder_ref,
"LinkPlugin",
"en",
name="VictimSecretLink",
external_link="https://secret.example.com",
)
endpoint = self.get_move_plugin_uri(reference)
with self.login_user_context(attacker):
data = {
"plugin_id": reference.pk,
"placeholder_id": attacker_clipboard.pk,
"move_a_copy": "true",
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 403)
self.assertFalse(attacker_clipboard.get_plugins("en").exists())
def test_paste_plugin_from_own_clipboard_without_source(self):
"""A user may always paste from their own clipboard.
Clipboards created before they got a source object still have none by
the time the plugin to paste is fetched, so the source-side check must
not rely on the clipboard's source alone.
"""
user = self.get_staff_user_with_no_permissions()
self.add_permission(user, "change_example1")
self.add_permission(user, "add_link")
clipboard = self._get_clipboard(user, with_source=False)
target = Example1.objects.create(
char_1="one", char_2="two", char_3="tree", char_4="four"
).placeholder
plugin = add_plugin(
clipboard,
"LinkPlugin",
"en",
name="A Link",
external_link="https://www.django-cms.org",
)
endpoint = self.get_move_plugin_uri(plugin)
with self.login_user_context(user):
data = {
"plugin_id": plugin.pk,
"placeholder_id": target.pk,
"move_a_copy": "true",
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(target.get_plugins("en").count(), 1)
def test_paste_plugin_rejects_parent_in_other_placeholder(self):
"""``plugin_parent`` must live in the placeholder the copy lands in.
The id is client-supplied, so an unscoped lookup would let a staff user
name a plugin in a placeholder they have no permission on. The copy
would then be parented outside its own placeholder, and the response
renders the foreign parent's whole subtree back to the caller.
"""
user = self._create_user(
"attacker",
is_staff=True,
is_superuser=False,
permissions=["add_link", "change_link", "change_example1"],
)
clipboard = self._get_clipboard(user)
# A page placeholder the user has no permission on.
page = create_page("restricted", "nav_playground.html", "en")
restricted = page.get_placeholders("en")[0]
foreign_parent = add_plugin(
restricted,
"LinkPlugin",
"en",
name="VictimSecretLink",
external_link="https://secret.example.com",
)
self.assertFalse(restricted.has_change_permission(user))
# A placeholder the user does control.
target = Example1.objects.create(
char_1="one", char_2="two", char_3="tree", char_4="four"
).placeholder
plugin = add_plugin(
clipboard,
"LinkPlugin",
"en",
name="A Link",
external_link="https://www.django-cms.org",
)
endpoint = self.get_move_plugin_uri(plugin)
with self.login_user_context(user):
data = {
"plugin_id": plugin.pk,
"placeholder_id": target.pk,
"plugin_parent": foreign_parent.pk,
"move_a_copy": "true",
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 404)
self.assertNotContains(response, "VictimSecretLink", status_code=404)
self.assertFalse(target.get_plugins("en").exists())
self.assertEqual(foreign_parent.cmsplugin_set.count(), 0)
def test_paste_plugin_accepts_parent_in_target_placeholder(self):
"""The regular case still works: a parent inside the target placeholder."""
user = self._create_user(
"editor",
is_staff=True,
is_superuser=False,
permissions=["add_link", "change_link", "change_example1"],
)
clipboard = self._get_clipboard(user)
target = Example1.objects.create(
char_1="one", char_2="two", char_3="tree", char_4="four"
).placeholder
parent = add_plugin(
target, "LinkPlugin", "en", name="Parent", external_link="https://example.com"
)
plugin = add_plugin(
clipboard,
"LinkPlugin",
"en",
name="A Link",
external_link="https://www.django-cms.org",
)
endpoint = self.get_move_plugin_uri(plugin)
with self.login_user_context(user):
data = {
"plugin_id": plugin.pk,
"placeholder_id": target.pk,
"plugin_parent": parent.pk,
"move_a_copy": "true",
"target_language": "en",
"target_position": 2,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 200)
self.assertEqual(target.get_plugins("en").count(), 2)
self.assertEqual(parent.cmsplugin_set.get().placeholder_id, target.pk)
def test_move_plugin_rejects_existing_parent_in_other_placeholder(self):
"""An unchanged ``plugin_parent`` is not trusted blindly either.
A child whose stored parent lives in another placeholder (e.g., left
behind by the unscoped paste lookup) must not let the caller render
that foreign parent's subtree by resubmitting the parent's id.
"""
user = self._create_user(
"attacker",
is_staff=True,
is_superuser=False,
permissions=["add_link", "change_link", "change_example1"],
)
page = create_page("restricted", "nav_playground.html", "en")
restricted = page.get_placeholders("en")[0]
foreign_parent = add_plugin(
restricted,
"LinkPlugin",
"en",
name="VictimSecretLink",
external_link="https://secret.example.com",
)
self.assertFalse(restricted.has_change_permission(user))
target = Example1.objects.create(
char_1="one", char_2="two", char_3="tree", char_4="four"
).placeholder
plugin = add_plugin(
target,
"LinkPlugin",
"en",
name="A Link",
external_link="https://www.django-cms.org",
)
# Simulate a malformed cross-placeholder relation already in the database.
CMSPlugin.objects.filter(pk=plugin.pk).update(parent=foreign_parent)
endpoint = self.get_move_plugin_uri(plugin)
with self.login_user_context(user):
data = {
"plugin_id": plugin.pk,
"plugin_parent": foreign_parent.pk,
"target_language": "en",
"target_position": 1,
}
response = self.client.post(endpoint, data)
self.assertEqual(response.status_code, 404)
self.assertNotContains(response, "VictimSecretLink", status_code=404)