diff --git a/readthedocs/core/templatetags/core_tags.py b/readthedocs/core/templatetags/core_tags.py index d37f4d2671d..4f33c67bda9 100644 --- a/readthedocs/core/templatetags/core_tags.py +++ b/readthedocs/core/templatetags/core_tags.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- - """Template tags for core app.""" import hashlib +import json from urllib.parse import urlencode from django import template from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder from django.utils.encoding import force_bytes, force_text from django.utils.safestring import mark_safe @@ -112,3 +112,29 @@ def key(d, key_name): @register.simple_tag def readthedocs_version(): return __version__ + + +@register.filter +def escapejson(data, indent=None): + """ + Escape JSON correctly for inclusion in Django templates + + This code was mostly taken from Django's implementation + https://docs.djangoproject.com/en/2.2/ref/templates/builtins/#json-script + https://github.com/django/django/blob/2.2.2/django/utils/html.py#L74-L92 + + After upgrading to Django 2.1+, we could replace this with Django's implementation + although the inputs and outputs are a bit different. + + Example: + + var jsvar = {{ dictionary_value | escapejson }} + """ + if indent: + indent = int(indent) + _json_script_escapes = { + ord('>'): '\\u003E', + ord('<'): '\\u003C', + ord('&'): '\\u0026', + } + return mark_safe(json.dumps(data, cls=DjangoJSONEncoder, indent=indent).translate(_json_script_escapes)) diff --git a/readthedocs/doc_builder/backends/mkdocs.py b/readthedocs/doc_builder/backends/mkdocs.py index 3cb28f791e3..d87df6ea0db 100644 --- a/readthedocs/doc_builder/backends/mkdocs.py +++ b/readthedocs/doc_builder/backends/mkdocs.py @@ -226,9 +226,9 @@ def generate_rtd_data(self, docs_dir, mkdocs_config): 'global_analytics_code': settings.GLOBAL_ANALYTICS_CODE, 'user_analytics_code': analytics_code, } - data_json = json.dumps(readthedocs_data, indent=4) + data_ctx = { - 'data_json': data_json, + 'readthedocs_data': readthedocs_data, 'current_version': readthedocs_data['version'], 'slug': readthedocs_data['project'], 'html_theme': readthedocs_data['theme'], diff --git a/readthedocs/doc_builder/templates/doc_builder/data.js.tmpl b/readthedocs/doc_builder/templates/doc_builder/data.js.tmpl index 29ab61b0e65..6b3c407263c 100644 --- a/readthedocs/doc_builder/templates/doc_builder/data.js.tmpl +++ b/readthedocs/doc_builder/templates/doc_builder/data.js.tmpl @@ -1,10 +1,13 @@ -var READTHEDOCS_DATA = {{ data_json|safe }} +{% load core_tags %} + + +var READTHEDOCS_DATA = {{ readthedocs_data|escapejson:4 }} // Old variables -var doc_version = "{{ current_version }}"; -var doc_slug = "{{ slug }}"; -var page_name = "{{ pagename }}"; -var html_theme = "{{ html_theme }}"; +var doc_version = "{{ current_version|escapejs }}"; +var doc_slug = "{{ slug|escapejs }}"; +var page_name = "{{ pagename|escapejs }}"; +var html_theme = "{{ html_theme|escapejs }}"; // mkdocs_page_input_path is only defined on the RTD mkdocs theme but it isn't // available on all pages (e.g. missing in search result) diff --git a/readthedocs/rtd_tests/tests/test_core_tags.py b/readthedocs/rtd_tests/tests/test_core_tags.py index 3b9abd547c2..48c5fa85bf7 100644 --- a/readthedocs/rtd_tests/tests/test_core_tags.py +++ b/readthedocs/rtd_tests/tests/test_core_tags.py @@ -110,3 +110,14 @@ def test_restructured_text_invalid(self): ) result = core_tags.restructuredtext(value) self.assertEqual(result, value) + + def test_escapejson(self): + tests = ( + ({}, '{}'), + ({'a': 'b'}, '{"a": "b"}'), + ({"'; //": '""'}, '{"\'; //": "\\"\\""}'), + ({"": ''}, '{"\\u003Cscript\\u003Ealert(\'hi\')\\u003C/script\\u003E": ""}'), + ) + + for in_value, out_value in tests: + self.assertEqual(core_tags.escapejson(in_value), out_value)