forked from ungleich-public/dynamicweb
1092 lines
58 KiB
Python
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)
|