From bee498f3a2f66210db39f0be244ec4fa888b6940 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Sat, 30 Jun 2012 18:54:38 +0100 Subject: [PATCH] Added 'format_html' utility for formatting HTML fragments safely --- django/utils/html.py | 31 +++++++++++++++++++++++ docs/ref/utils.txt | 39 +++++++++++++++++++++++++++++ tests/regressiontests/utils/html.py | 11 ++++++++ 3 files changed, 81 insertions(+) diff --git a/django/utils/html.py b/django/utils/html.py index fe2e6b7a29..390c45dcec 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -72,6 +72,37 @@ def conditional_escape(text): else: return escape(text) +def format_html(format_string, *args, **kwargs): + """ + Similar to str.format, but passes all arguments through conditional_escape, + and calls 'mark_safe' on the result. This function should be used instead + of str.format or % interpolation to build up small HTML fragments. + """ + args_safe = map(conditional_escape, args) + kwargs_safe = dict([(k, conditional_escape(v)) for (k, v) in + kwargs.iteritems()]) + return mark_safe(format_string.format(*args_safe, **kwargs_safe)) + +def format_html_join(sep, format_string, args_generator): + """ + A wrapper format_html, for the common case of a group of arguments that need + to be formatted using the same format string, and then joined using + 'sep'. 'sep' is also passed through conditional_escape. + + 'args_generator' should be an iterator that returns the sequence of 'args' + that will be passed to format_html. + + Example: + + format_html_join('\n', "
  • {0} {1}
  • ", ((u.first_name, u.last_name) + for u in users)) + + """ + return mark_safe(conditional_escape(sep).join( + format_html(format_string, *tuple(args)) + for args in args_generator)) + + def linebreaks(value, autoescape=False): """Converts newlines into

    and
    s.""" value = normalize_newlines(value) diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 549812296b..c74392df36 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -410,6 +410,45 @@ escaping HTML. Similar to ``escape()``, except that it doesn't operate on pre-escaped strings, so it will not double escape. +.. function:: format_html(format_string, *args, **kwargs) + + This is similar to `str.format`_, except that it is appropriate for + building up HTML fragments. All args and kwargs are passed through + :func:`conditional_escape` before being passed to ``str.format``. + + For the case of building up small HTML fragments, this function is to be + preferred over string interpolation using ``%`` or ``str.format`` directly, + because it applies escaping to all arguments - just like the Template system + applies escaping by default. + + So, instead of writing: + + .. code-block:: python + + mark_safe(u"%s %s %s" % (some_html, + escape(some_text), + escape(some_other_text), + )) + + you should instead use: + + .. code-block:: python + + format_html(u"%{0} {1} {2}", + mark_safe(some_html), some_text, some_other_text) + + This has the advantage that you don't need to apply :func:`escape` to each + argument and risk a bug and an XSS vulnerability if you forget one. + + Note that although this function uses ``str.format`` to do the + interpolation, some of the formatting options provided by `str.format`_ + (e.g. number formatting) will not work, since all arguments are passed + through :func:`conditional_escape` which (ultimately) calls + :func:`~django.utils.encoding.force_unicode` on the values. + + +.. _str.format: http://docs.python.org/library/stdtypes.html#str.format + ``django.utils.http`` ===================== diff --git a/tests/regressiontests/utils/html.py b/tests/regressiontests/utils/html.py index 434873b9e0..389ae8ec75 100644 --- a/tests/regressiontests/utils/html.py +++ b/tests/regressiontests/utils/html.py @@ -34,6 +34,17 @@ class TestUtilsHtml(unittest.TestCase): # Verify it doesn't double replace &. self.check_output(f, '<&', '<&') + def test_format_html(self): + self.assertEqual( + html.format_html(u"{0} {1} {third} {fourth}", + u"< Dangerous >", + html.mark_safe(u"safe"), + third="< dangerous again", + fourth=html.mark_safe(u"safe again") + ), + u"< Dangerous > safe < dangerous again safe again" + ) + def test_linebreaks(self): f = html.linebreaks items = (