Repository: pretalx/pretalx

Issue:
control characters in submissions cause invalid schedule XML
## Expected Behavior

The schedule XML should be valid, regardless of what speakers insert into their submission summary.

## Current Behavior

Inserting control characters (e.g. BEL 0x07, form feed / page break 0x0C) in a submission summary, breaks the resulting schedule XML (according the chrome/giggity).

> This page contains the following errors:
> error on line 555 at column 1: PCDATA invalid Char value 12

## Steps to Reproduce

1. get a control character: `echo -ne "\a" > /tmp/controlchar`
2. get it into your clipboard somehow
3. create a new submission, add character to summary
4. export schedule XML
5. open in chrome/firefox/giggity and receive error

## Context

A speaker copy pasted text from a rich text editor into summary. Page break in the content caused the described failure.

## Your Environment

* Version: v0.7.1
* Environment name and version (e.g. Chrome 39, python 3.5): python 3.6.6
* Operating System and version (desktop or mobile): N/A
* Link to your instance, if in production: N/A, fixed by removing the character


Discussion:

Resolving patch:
diff --git a/src/pretalx/common/templatetags/xmlescape.py b/src/pretalx/common/templatetags/xmlescape.py
new file mode 100644
--- /dev/null
+++ b/src/pretalx/common/templatetags/xmlescape.py
@@ -0,0 +1,18 @@
+from xml.sax.saxutils import escape
+
+import django.utils.safestring
+from django import template
+
+register = template.Library()
+strip_ascii = {c: None for c in list(range(0, 9)) + list(range(11, 32))}
+
+
+@register.filter
+def xmlescape(text):
+    text = str(text)  # resolve lazy i18n string
+    text = text.translate(strip_ascii)  # remove ascii control characters
+    text = escape(text)  # escape ><&
+    text = text.encode('ascii', 'xmlcharrefreplace').decode()  # convert all non-ascii to &#xxx;
+    text = text.replace('"', "&quot;")
+    text = text.replace("'", "&apos;")
+    return django.utils.safestring.mark_safe(text)


Test patch:
diff --git a/src/tests/functional/agenda/test_schedule_export.py b/src/tests/functional/agenda/test_schedule_export.py
--- a/src/tests/functional/agenda/test_schedule_export.py
+++ b/src/tests/functional/agenda/test_schedule_export.py
@@ -30,6 +30,20 @@ def test_schedule_frab_xml_export(slot, client, schedule_schema):
     )  # Will raise if the schedule does not match the schema
 
 
+@pytest.mark.django_db
+def test_schedule_frab_xml_export_control_char(slot, client, schedule_schema):
+    slot.submission.description = "control char: \a"
+    slot.submission.save()
+
+    response = client.get(
+        reverse(f'agenda:core-frab-xml', kwargs={'event': slot.submission.event.slug}),
+        follow=True,
+    )
+
+    parser = etree.XMLParser()
+    etree.fromstring(response.content, parser)
+
+
 @pytest.mark.django_db
 def test_schedule_frab_json_export(
     slot, answered_choice_question, personal_answer, client, orga_user, schedule_schema
diff --git a/src/tests/unit/common/test_common_templatetags.py b/src/tests/unit/common/test_common_templatetags.py
--- a/src/tests/unit/common/test_common_templatetags.py
+++ b/src/tests/unit/common/test_common_templatetags.py
@@ -1,6 +1,7 @@
 import pytest
 
 from pretalx.common.templatetags.times import times
+from pretalx.common.templatetags.xmlescape import xmlescape
 
 
 @pytest.mark.parametrize('number,output', (
@@ -15,3 +16,17 @@
 ))
 def test_common_templatetag_times(number, output):
     assert times(number) == output
+
+
+@pytest.mark.parametrize('input_,output', (
+    ('i am a normal string ??!!$%/()=?', 'i am a normal string ??!!$%/()=?'),
+    ('<', '&lt;'),
+    ('>', '&gt;'),
+    ('"', '&quot;'),
+    ("'", '&apos;'),
+    ('&', '&amp;'),
+    ('a\aa', 'aa'),
+    ('ä', '&#228;'),
+))
+def test_common_templatetag_xmlescape(input_, output):
+    assert xmlescape(input_) == output