Skip to content

Commit 69ba73e

Browse files
authored
fix: escaped characters in autocomplete view of link plugin (#217)
1 parent 8a46fdd commit 69ba73e

File tree

2 files changed

+54
-18
lines changed

2 files changed

+54
-18
lines changed

djangocms_frontend/contrib/link/helpers.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
from django.contrib.contenttypes.models import ContentType
88
from django.core.exceptions import FieldError, ObjectDoesNotExist
99
from django.utils.encoding import force_str
10-
from django.utils.html import mark_safe
11-
12-
from djangocms_frontend.settings import EMPTY_CHOICE
1310

1411
LINK_MODELS = getattr(django_settings, "DJANGOCMS_FRONTEND_LINK_MODELS", [])
1512

@@ -62,12 +59,21 @@ def get_object_for_value(value):
6259
return None
6360

6461

62+
def unescape(text, nbsp):
63+
return (text.replace(" ", nbsp)
64+
.replace("&", "&")
65+
.replace("&lt;", "<")
66+
.replace("&gt;", ">")
67+
.replace("&quot;", '"')
68+
.replace("&#x27;", "'"))
69+
70+
6571
def get_link_choices(request, term="", lang=None, nbsp=None):
6672
global _querysets
6773

6874
if nbsp is None:
6975
nbsp = "" if term else "\u2000"
70-
available_objects = [dict(id=EMPTY_CHOICE[0][0], text=EMPTY_CHOICE[0][1])]
76+
available_objects = []
7177
# Now create our list of cms pages
7278
type_id = ContentType.objects.get_for_model(Page).id
7379
for value, descr in get_page_choices(lang):
@@ -78,7 +84,9 @@ def get_link_choices(request, term="", lang=None, nbsp=None):
7884
"children": [
7985
dict(
8086
id=f"{type_id}-{page}",
81-
text=mark_safe(name.replace("&nbsp;", nbsp)),
87+
# django admin's autocomplete view requires unescaped strings
88+
# get_page_choices escepes strings, so we undo the escape
89+
text=unescape(name, nbsp),
8290
)
8391
for page, name in descr
8492
if not isinstance(term, str) or term.upper() in name.upper()

tests/link/test_plugins.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from cms.api import add_plugin
22
from cms.test_utils.testcases import CMSTestCase
3+
from cms.utils.urlutils import admin_reverse
34
from django.http import HttpRequest
45

56
from djangocms_frontend import settings
@@ -116,19 +117,6 @@ def test_plugin(self):
116117
f'Cound not find class="btn btn-outline-primary" in {response.content.decode("utf-8")}',
117118
)
118119

119-
def test_smart_link_field(self):
120-
slf = SmartLinkField()
121-
choices = get_choices(None)
122-
self.assertEqual("example.com", choices[1][0]) # Site name
123-
self.assertIn(("2-1", "home"), choices[1][1])
124-
125-
cleaned = slf.clean("2-1")
126-
self.assertEqual(dict(model="cms.page", pk=1), cleaned)
127-
128-
self.assertEqual(slf.prepare_value("blabla"), "")
129-
self.assertEqual(slf.prepare_value(dict(model="cms.page", pk=1)), "2-1")
130-
self.assertEqual(slf.prepare_value(self.home), "2-1")
131-
132120
def test_link_form(self):
133121
request = HttpRequest()
134122
request.POST = {
@@ -165,3 +153,43 @@ def test_link_form(self):
165153
request.POST["external_link"] = None
166154
form = LinkForm(request.POST)
167155
self.assertFalse(form.is_valid()) # no anchor for mail
156+
157+
158+
class AutocompleteViewTestCase(TestFixture, CMSTestCase):
159+
160+
def test_smart_link_field(self):
161+
slf = SmartLinkField()
162+
choices = get_choices(None)
163+
self.assertEqual("example.com", choices[0][0]) # Site name
164+
self.assertIn(("2-1", "home"), choices[0][1])
165+
166+
cleaned = slf.clean("2-1")
167+
self.assertEqual(dict(model="cms.page", pk=1), cleaned)
168+
169+
self.assertEqual(slf.prepare_value("blabla"), "")
170+
self.assertEqual(slf.prepare_value(dict(model="cms.page", pk=1)), "2-1")
171+
self.assertEqual(slf.prepare_value(self.home), "2-1")
172+
173+
def test_autocomplete_view(self):
174+
tricky_title = """d'acceuil: <script>alert("XSS");</script>"""
175+
page = self.create_page(
176+
title=tricky_title,
177+
template="page.html",
178+
)
179+
expected_choices = [
180+
"home", "content", tricky_title,
181+
]
182+
183+
self.publish(page, self.language)
184+
autocomplete_url = admin_reverse("link_link_autocomplete")
185+
186+
with self.login_user_context(self.superuser):
187+
response = self.client.get(autocomplete_url)
188+
189+
autocomplete_result = response.json()
190+
choices = autocomplete_result.get("results")[0]
191+
192+
self.assertFalse((autocomplete_result.get("pagination") or {}).get("more"))
193+
194+
for expected, sent in zip(expected_choices, choices.get("children")):
195+
self.assertEqual(expected, sent.get("text"))

0 commit comments

Comments
 (0)