diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index 5b9b5bf2d..ca870e141 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -250,36 +250,42 @@ def join_queries_or(left, right): def render_markdown(text): - return markdown.markdown(text, extensions=["nl2br"]) + return markdown.markdown(text, extensions=["nl2br", "extra"]) -HTMl_CLEANER = bleach.sanitizer.Cleaner( +SAFE_TAGS = [ + "p", + "a", + "abbr", + "acronym", + "b", + "blockquote", + "code", + "em", + "i", + "li", + "ol", + "strong", + "ul", +] +HTMl_CLEANER = bleach.sanitizer.Cleaner(strip=True, tags=SAFE_TAGS) + +HTML_PERMISSIVE_CLEANER = bleach.sanitizer.Cleaner( strip=True, - tags=[ - "p", - "a", - "abbr", - "acronym", - "b", - "blockquote", - "code", - "em", - "i", - "li", - "ol", - "strong", - "ul", - ], + tags=SAFE_TAGS + ["h1", "h2", "h3", "h4", "h5", "h6", "div", "section", "article"], + attributes=["class", "rel", "alt", "title"], ) HTML_LINKER = bleach.linkifier.Linker() -def clean_html(html): - return HTMl_CLEANER.clean(html) +def clean_html(html, permissive=False): + return ( + HTML_PERMISSIVE_CLEANER.clean(html) if permissive else HTMl_CLEANER.clean(html) + ) -def render_html(text, content_type): +def render_html(text, content_type, permissive=False): rendered = render_markdown(text) if content_type == "text/html": rendered = text @@ -288,7 +294,7 @@ def render_html(text, content_type): else: rendered = render_markdown(text) rendered = HTML_LINKER.linkify(rendered) - return clean_html(rendered).strip().replace("\n", "") + return clean_html(rendered, permissive=permissive).strip().replace("\n", "") def render_plain_text(html): diff --git a/api/funkwhale_api/common/views.py b/api/funkwhale_api/common/views.py index 1611d8e63..05cb025c3 100644 --- a/api/funkwhale_api/common/views.py +++ b/api/funkwhale_api/common/views.py @@ -191,5 +191,10 @@ class TextPreviewView(views.APIView): if "text" not in payload: return response.Response({"detail": "Invalid input"}, status=400) - data = {"rendered": utils.render_html(payload["text"], "text/markdown")} + permissive = payload.get("permissive", False) + data = { + "rendered": utils.render_html( + payload["text"], "text/markdown", permissive=permissive + ) + } return response.Response(data, status=200) diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py index 66d8211c1..c4340d4b8 100644 --- a/api/funkwhale_api/instance/dynamic_preferences_registry.py +++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py @@ -38,9 +38,7 @@ class InstanceLongDescription(types.StringPreference): name = "long_description" verbose_name = "Long description" default = "" - help_text = ( - "Instance long description, displayed in the about page (markdown allowed)." - ) + help_text = "Instance long description, displayed in the about page." widget = widgets.Textarea field_kwargs = {"required": False} @@ -52,9 +50,7 @@ class InstanceTerms(types.StringPreference): name = "terms" verbose_name = "Terms of service" default = "" - help_text = ( - "Terms of service and privacy policy for your instance (markdown allowed)." - ) + help_text = "Terms of service and privacy policy for your instance." widget = widgets.Textarea field_kwargs = {"required": False} @@ -66,7 +62,7 @@ class InstanceRules(types.StringPreference): name = "rules" verbose_name = "Rules" default = "" - help_text = "Rules/Code of Conduct (markdown allowed)." + help_text = "Rules/Code of Conduct." widget = widgets.Textarea field_kwargs = {"required": False} diff --git a/api/tests/common/test_utils.py b/api/tests/common/test_utils.py index 478a9e8cc..f5b3836ab 100644 --- a/api/tests/common/test_utils.py +++ b/api/tests/common/test_utils.py @@ -103,27 +103,40 @@ def test_join_url(start, end, expected): @pytest.mark.parametrize( - "text, content_type, expected", + "text, content_type, permissive, expected", [ - ("hello world", "text/markdown", "
hello world
"), - ("hello world", "text/plain", "hello world
"), - ("hello world", "text/html", "hello world"), + ("hello world", "text/markdown", False, "hello world
"), + ("hello world", "text/plain", False, "hello world
"), + ( + "hello world", + "text/html", + False, + "hello world", + ), # images and other non whitelisted html should be removed - ("hello world\n", "text/markdown", "hello world
"), + ("hello world\n", "text/markdown", False, "hello world
"), ( "hello world\n\n\n\n", "text/markdown", + False, "hello world
", ), ( "hello world
\n\n", "text/html", + False, "hello world
", ), + ( + 'hello world
\n\n', + "text/markdown", + True, + 'hello world
', + ), ], ) -def test_render_html(text, content_type, expected): - result = utils.render_html(text, content_type) +def test_render_html(text, content_type, permissive, expected): + result = utils.render_html(text, content_type, permissive=permissive) assert result == expected diff --git a/api/tests/common/test_views.py b/api/tests/common/test_views.py index 761a2940e..3e1255fcc 100644 --- a/api/tests/common/test_views.py +++ b/api/tests/common/test_views.py @@ -281,3 +281,15 @@ def test_can_render_text_preview(api_client, db): expected = {"rendered": utils.render_html(payload["text"], "text/markdown")} assert response.status_code == 200 assert response.data == expected + + +def test_can_render_text_preview_permissive(api_client, db): + payload = {"text": "Hello world", "permissive": True} + url = reverse("api:v1:text-preview") + response = api_client.post(url, payload) + + expected = { + "rendered": utils.render_html(payload["text"], "text/markdown", permissive=True) + } + assert response.status_code == 200 + assert response.data == expected diff --git a/changes/changelog.d/923.enhancement b/changes/changelog.d/923.enhancement new file mode 100644 index 000000000..44b9d60e8 --- /dev/null +++ b/changes/changelog.d/923.enhancement @@ -0,0 +1 @@ +Use same markdown widget for all content fields (rules, description, reports, notes, etc.) diff --git a/front/src/components/admin/SettingsGroup.vue b/front/src/components/admin/SettingsGroup.vue index 70894c7d6..cbc07d949 100644 --- a/front/src/components/admin/SettingsGroup.vue +++ b/front/src/components/admin/SettingsGroup.vue @@ -17,24 +17,25 @@{{ setting.help_text }}
+
@@ -49,7 +56,12 @@ export default {
props: {
value: {type: String, default: ""},
fieldId: {type: String, default: "change-content"},
+ placeholder: {type: String, default: null},
autofocus: {type: Boolean, default: false},
+ charLimit: {type: Number, default: 5000, required: false},
+ rows: {type: Number, default: 5, required: false},
+ permissive: {type: Boolean, default: false},
+ required: {type: Boolean, default: false},
},
data () {
return {
@@ -57,7 +69,6 @@ export default {
preview: null,
newValue: this.value,
isLoadingPreview: false,
- charLimit: 5000,
}
},
mounted () {
@@ -71,7 +82,7 @@ export default {
async loadPreview () {
this.isLoadingPreview = true
try {
- let response = await axios.post('text-preview/', {text: this.value})
+ let response = await axios.post('text-preview/', {text: this.newValue, permissive: this.permissive})
this.preview = response.data.rendered
} catch {
@@ -86,11 +97,12 @@ export default {
}
},
remainingChars () {
- return this.charLimit - this.value.length
+ return this.charLimit - (this.value || "").length
}
},
watch: {
newValue (v) {
+ this.preview = null
this.$emit('input', v)
},
value: {
@@ -104,7 +116,7 @@ export default {
immediate: true,
},
async isPreviewing (v) {
- if (v && !!this.value && this.preview === null) {
+ if (v && !!this.value && this.preview === null && !this.isLoadingPreview) {
await this.loadPreview()
}
if (!v) {
diff --git a/front/src/components/library/EditForm.vue b/front/src/components/library/EditForm.vue
index 70f83d049..17b781e59 100644
--- a/front/src/components/library/EditForm.vue
+++ b/front/src/components/library/EditForm.vue
@@ -79,7 +79,7 @@
-
+