diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index e876c555f3..e32b7f7a5f 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -12,8 +12,8 @@ from django.utils import formats
from django.utils.dateformat import format, time_format
from django.utils.encoding import iri_to_uri
from django.utils.html import (
- avoid_wrapping, conditional_escape, escape, escapejs, linebreaks,
- strip_tags, urlize as _urlize,
+ avoid_wrapping, conditional_escape, escape, escapejs,
+ json_script as _json_script, linebreaks, strip_tags, urlize as _urlize,
)
from django.utils.safestring import SafeData, mark_safe
from django.utils.text import (
@@ -82,6 +82,15 @@ def escapejs_filter(value):
return escapejs(value)
+@register.filter(is_safe=True)
+def json_script(value, element_id):
+ """
+ Output value JSON-encoded, wrapped in a ',
+ element_id, mark_safe(json_str)
+ )
+
+
def conditional_escape(text):
"""
Similar to escape(), except that it doesn't operate on pre-escaped strings.
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 634adf42e3..a4e0a0d455 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -1782,6 +1782,46 @@ For example::
If ``value`` is the list ``['a', 'b', 'c']``, the output will be the string
``"a // b // c"``.
+.. templatefilter:: json_script
+
+``json_script``
+---------------
+
+.. versionadded:: 2.1
+
+Safely outputs a Python object as JSON, wrapped in a ``
+
+The resulting data can be accessed in JavaScript like this:
+
+.. code-block:: javascript
+
+ var el = document.getElementById('hello-data');
+ var value = JSON.parse(el.textContent || el.innerText);
+
+XSS attacks are mitigated by escaping the characters "<", ">" and "&". For
+example if ``value`` is ``{'hello': 'world&'}``, the output is:
+
+.. code-block:: html
+
+
+
+This is compatible with a strict Content Security Policy that prohibits in-page
+script execution. It also maintains a clean separation between passive data and
+executable code.
+
.. templatefilter:: last
``last``
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 52519170db..ae3dd67bc9 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -205,7 +205,8 @@ Signals
Templates
~~~~~~~~~
-* ...
+* The new :tfilter:`json_script` filter safely outputs a Python object as JSON,
+ wrapped in a ``'
+ )
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index e6d1fe9a59..e2ebce4556 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -4,8 +4,8 @@ from datetime import datetime
from django.test import SimpleTestCase
from django.utils.functional import lazystr
from django.utils.html import (
- conditional_escape, escape, escapejs, format_html, html_safe, linebreaks,
- smart_urlquote, strip_spaces_between_tags, strip_tags,
+ conditional_escape, escape, escapejs, format_html, html_safe, json_script,
+ linebreaks, smart_urlquote, strip_spaces_between_tags, strip_tags,
)
from django.utils.safestring import mark_safe
@@ -147,6 +147,28 @@ class TestUtilsHtml(SimpleTestCase):
self.check_output(escapejs, value, output)
self.check_output(escapejs, lazystr(value), output)
+ def test_json_script(self):
+ tests = (
+ # "<", ">" and "&" are quoted inside JSON strings
+ (('&<>', '')),
+ # "<", ">" and "&" are quoted inside JSON objects
+ (
+ {'a': ''},
+ ''
+ ),
+ # Lazy strings are quoted
+ (lazystr('&<>'), ''),
+ (
+ {'a': lazystr('')},
+ ''
+ ),
+ )
+ for arg, expected in tests:
+ with self.subTest(arg=arg):
+ self.assertEqual(json_script(arg, 'test_id'), expected)
+
def test_smart_urlquote(self):
items = (
('http://öäü.com/', 'http://xn--4ca9at.com/'),