diff options
-rw-r--r-- | CHANGES | 2 | ||||
-rw-r--r-- | docs/api.rst | 12 | ||||
-rw-r--r-- | jinja2/defaults.py | 6 | ||||
-rw-r--r-- | jinja2/filters.py | 36 | ||||
-rw-r--r-- | jinja2/utils.py | 31 | ||||
-rw-r--r-- | tests/test_filters.py | 13 |
6 files changed, 97 insertions, 3 deletions
@@ -25,6 +25,8 @@ Version 2.9 the string is barely truncated at all. - Change the logic for macro autoescaping to be based on the runtime autoescaping information at call time instead of macro define time. +- Ported a modified version of the `tojson` filter from Flask to Jinja2 + and hooked it up with the new policy framework. Version 2.8.2 ------------- diff --git a/docs/api.rst b/docs/api.rst index 107acd65..8bf0fdfe 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -565,6 +565,18 @@ Example:: The default target that is issued for links from the `urlize` filter if no other target is defined by the call explicitly. +``json.dumps_function``: + If this is set to a value other than `None` then the `tojson` filter + will dump with this function instead of the default one. Note that + this function should accept arbitrary extra arguments which might be + passed in the future from the filter. Currently the only argument + that might be passed is `indent`. The default dump function is + ``json.dumps``. + +``json.dumps_kwargs``: + Keyword arguments to be passed to the dump function. The default is + ``{'sort_keys': True}``. + Utilities --------- diff --git a/jinja2/defaults.py b/jinja2/defaults.py index bdb538d5..90ccb65f 100644 --- a/jinja2/defaults.py +++ b/jinja2/defaults.py @@ -41,8 +41,10 @@ DEFAULT_NAMESPACE = { # default policies DEFAULT_POLICIES = { - 'urlize.rel': 'noopener', - 'urlize.target': None, + 'urlize.rel': 'noopener', + 'urlize.target': None, + 'json.dumps_function': None, + 'json.dumps_kwargs': {'sort_keys': True}, } diff --git a/jinja2/filters.py b/jinja2/filters.py index 33e0ff14..05c2fc45 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -15,7 +15,7 @@ from random import choice from itertools import groupby from collections import namedtuple from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \ - unicode_urlencode + unicode_urlencode, htmlsafe_json_dumps from jinja2.runtime import Undefined from jinja2.exceptions import FilterArgumentError from jinja2._compat import imap, string_types, text_type, iteritems @@ -916,6 +916,39 @@ def do_rejectattr(*args, **kwargs): return select_or_reject(args, kwargs, lambda x: not x, True) +@evalcontextfilter +def do_tojson(eval_ctx, value, indent=None): + """Dumps a structure to JSON so that it's safe to use in ``<script>`` + tags. It accepts the same arguments and returns a JSON string. Note that + this is available in templates through the ``|tojson`` filter which will + also mark the result as safe. Due to how this function escapes certain + characters this is safe even if used outside of ``<script>`` tags. + + The following characters are escaped in strings: + + - ``<`` + - ``>`` + - ``&`` + - ``'`` + + This makes it safe to embed such strings in any place in HTML with the + notable exception of double quoted attributes. In that case single + quote your attributes or HTML escape it in addition. + + The indent parameter can be used to enable pretty printing. Set it to + the number of spaces that the structures should be indented with. + + .. versionadded:: 2.9 + """ + policies = eval_ctx.environment.policies + dumper = policies['json.dumps_function'] + options = policies['json.dumps_kwargs'] + if indent is not None: + options = dict(options) + options['indent'] = indent + return htmlsafe_json_dumps(value, dumper=dumper, **options) + + def prepare_map(args, kwargs): context = args[0] seq = args[1] @@ -1021,4 +1054,5 @@ FILTERS = { 'wordcount': do_wordcount, 'wordwrap': do_wordwrap, 'xmlattr': do_xmlattr, + 'tojson': do_tojson, } diff --git a/jinja2/utils.py b/jinja2/utils.py index 96b13521..38e5edb2 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ import re +import json import errno from collections import deque from threading import Lock @@ -37,6 +38,8 @@ internal_code = set() concat = u''.join +_slash_escape = '\\/' not in json.dumps('/') + def contextfunction(f): """This decorator can be used to mark a function or method context callable. @@ -485,6 +488,34 @@ except ImportError: pass +def htmlsafe_json_dumps(obj, dumper=None, **kwargs): + """Works exactly like :func:`dumps` but is safe for use in ``<script>`` + tags. It accepts the same arguments and returns a JSON string. Note that + this is available in templates through the ``|tojson`` filter which will + also mark the result as safe. Due to how this function escapes certain + characters this is safe even if used outside of ``<script>`` tags. + + The following characters are escaped in strings: + + - ``<`` + - ``>`` + - ``&`` + - ``'`` + + This makes it safe to embed such strings in any place in HTML with the + notable exception of double quoted attributes. In that case single + quote your attributes or HTML escape it in addition. + """ + if dumper is None: + dumper = json.dumps + rv = dumper(obj, **kwargs) \ + .replace(u'<', u'\\u003c') \ + .replace(u'>', u'\\u003e') \ + .replace(u'&', u'\\u0026') \ + .replace(u"'", u'\\u0027') + return rv + + @implements_iterator class Cycler(object): """A cycle helper for templates.""" diff --git a/tests/test_filters.py b/tests/test_filters.py index ba57136d..1a8a1640 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -576,3 +576,16 @@ class TestFilter(object): tmpl = env.from_string('{{ users|rejectattr("id", "odd")|' 'map(attribute="name")|join("|") }}') assert tmpl.render(users=users) == 'jane' + + def test_json_dump(self): + env = Environment(autoescape=True) + t = env.from_string('{{ x|tojson }}') + assert t.render(x={'foo': 'bar'}) == '{"foo": "bar"}' + assert t.render(x='"bar\'') == '"\"bar\u0027"' + + def my_dumps(value, **options): + assert options == {'foo': 'bar'} + return '42' + env.policies['json.dumps_function'] = my_dumps + env.policies['json.dumps_kwargs'] = {'foo': 'bar'} + assert t.render(x=23) == '42' |