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![img](src)", "text/markdown", "

hello world

"), + ("hello world\n![img](src)", "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 }}

+ @@ -149,7 +150,7 @@ export default { byIdentifier[e.identifier] = e }) return this.group.settings.map(e => { - return byIdentifier[e] + return {...byIdentifier[e.name], fieldType: e.fieldType, fieldParams: e.fieldParams || {}} }) }, fileSettings () { diff --git a/front/src/components/common/ContentForm.vue b/front/src/components/common/ContentForm.vue index 0ec2fa941..f47daa6fc 100644 --- a/front/src/components/common/ContentForm.vue +++ b/front/src/components/common/ContentForm.vue @@ -26,13 +26,20 @@
- + {{ remainingChars }}

@@ -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 @@