diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index e37cf799399a105f1ed85c1774b48d890705c269..ed2d9e367097ae9c02070d76314f8de4edbea230 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -70,7 +70,7 @@ from openedx.core.djangoapps.programs.utils import get_programs from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.lib.courses import course_image_url -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json from student import auth from student.auth import has_course_author_access, has_studio_write_access, has_studio_read_access from student.roles import ( @@ -324,10 +324,10 @@ def course_search_index_handler(request, course_key_string): try: reindex_course_and_check_access(course_key, request.user) except SearchIndexingError as search_err: - return HttpResponse(escape_json_dumps({ + return HttpResponse(dump_js_escaped_json({ "user_message": search_err.error_list }), content_type=content_type, status=500) - return HttpResponse(escape_json_dumps({ + return HttpResponse(dump_js_escaped_json({ "user_message": _("Course has been successfully reindexed.") }), content_type=content_type, status=200) diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py index 21be96119e6fc107fcaf0c17fadd4ec518188803..e770fe0d18efd77678d6318a7f2d6c1d6af55b60 100644 --- a/cms/djangoapps/contentstore/views/entrance_exam.py +++ b/cms/djangoapps/contentstore/views/entrance_exam.py @@ -10,7 +10,7 @@ from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import ensure_csrf_cookie from django.http import HttpResponse, HttpResponseBadRequest -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json from contentstore.views.helpers import create_xblock, remove_entrance_exam_graders from contentstore.views.item import delete_item from models.settings.course_metadata import CourseMetadata @@ -186,7 +186,7 @@ def _get_entrance_exam(request, course_key): # pylint: disable=W0613 try: exam_descriptor = modulestore().get_item(exam_key) return HttpResponse( - escape_json_dumps({'locator': unicode(exam_descriptor.location)}), + dump_js_escaped_json({'locator': unicode(exam_descriptor.location)}), status=200, content_type='application/json') except ItemNotFoundError: return HttpResponse(status=404) diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py index af4e6c1e27d88cfcdfd22515cf8220f789976441..2eea0bc2a77f86ba81d3121e79b78c4a46567cda 100644 --- a/cms/djangoapps/contentstore/views/error.py +++ b/cms/djangoapps/contentstore/views/error.py @@ -4,7 +4,7 @@ from django.http import (HttpResponse, HttpResponseServerError, HttpResponseNotFound) from edxmako.shortcuts import render_to_string, render_to_response import functools -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json __all__ = ['not_found', 'server_error', 'render_404', 'render_500'] @@ -18,7 +18,7 @@ def jsonable_error(status=500, message="The Studio servers encountered an error" @functools.wraps(func) def inner(request, *args, **kwargs): if request.is_ajax(): - content = escape_json_dumps({"error": message}) + content = dump_js_escaped_json({"error": message}) return HttpResponse(content, content_type="application/json", status=status) else: diff --git a/cms/djangoapps/contentstore/views/organization.py b/cms/djangoapps/contentstore/views/organization.py index 13ac777ee9096c9d3604dc95f7b5fe0be0c338f0..6c530dfe4fddd7bd2f5d7c09b7e7cc2481761e30 100644 --- a/cms/djangoapps/contentstore/views/organization.py +++ b/cms/djangoapps/contentstore/views/organization.py @@ -4,7 +4,7 @@ from django.utils.decorators import method_decorator from django.views.generic import View from django.http import HttpResponse -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json from util.organizations_helpers import get_organizations @@ -20,4 +20,4 @@ class OrganizationListView(View): """Returns organization list as json.""" organizations = get_organizations() org_names_list = [(org["short_name"]) for org in organizations] - return HttpResponse(escape_json_dumps(org_names_list), content_type='application/json; charset=utf-8') + return HttpResponse(dump_js_escaped_json(org_names_list), content_type='application/json; charset=utf-8') diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py index 4c60792d2ac23b3f327cd3834eb1c7e38fb511cb..8f72f7ba1dc139d1e8ef63f983b3fd5091b70033 100644 --- a/cms/djangoapps/contentstore/views/tests/test_certificates.py +++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py @@ -276,8 +276,9 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat @mock.patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_certificate_info_in_response(self): """ - Test that certificate has been created and rendered properly. + Test that certificate has been created and rendered properly with non-audit course mode. """ + CourseModeFactory.create(course_id=self.course.id, mode_slug='verified') response = self.client.ajax_post( self._url(), data=CERTIFICATE_JSON_WITH_SIGNATORIES @@ -298,6 +299,22 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat self.assertEqual(data[0]['description'], 'Test description') self.assertEqual(data[0]['version'], CERTIFICATE_SCHEMA_VERSION) + @mock.patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) + def test_certificate_info_not_in_response(self): + """ + Test that certificate has not been rendered audit only course mode. + """ + response = self.client.ajax_post( + self._url(), + data=CERTIFICATE_JSON_WITH_SIGNATORIES + ) + + self.assertEqual(response.status_code, 201) + + # in html response + result = self.client.get_html(self._url()) + self.assertNotIn('Test certificate', result.content) + def test_unsupported_http_accept_header(self): """ Test if not allowed header present in request. diff --git a/cms/templates/base.html b/cms/templates/base.html index 32ce03dcdd78639c8b721b488219ea7da2a58f18..512669c9b2a7b460f1d7fc600cb6b3f819c5831d 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -2,8 +2,8 @@ <%namespace name='static' file='static_content.html'/> <%! from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import ( - escape_json_dumps, escape_js_string +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string ) %> <!doctype html> @@ -47,7 +47,7 @@ from openedx.core.lib.js_utils import ( <a class="nav-skip" href="#content">${_("Skip to main content")}</a> <script type="text/javascript"> - window.baseUrl = "${escape_js_string(settings.STATIC_URL) | n}"; + window.baseUrl = "${settings.STATIC_URL | n, js_escaped_string}"; var require = {baseUrl: window.baseUrl}; </script> <script type="text/javascript" src="${static.url("js/vendor/require.js")}"></script> @@ -85,14 +85,14 @@ from openedx.core.lib.js_utils import ( % if context_course: require(['js/factories/course'], function(CourseFactory) { CourseFactory({ - id: "${escape_js_string(context_course.id) | n}", + id: "${context_course.id | n, js_escaped_string}", name: "${context_course.display_name_with_default_escaped | h}", url_name: "${context_course.location.name | h}", org: "${context_course.location.org | h}", num: "${context_course.location.course | h}", - display_course_number: "${_(context_course.display_coursenumber) if context_course.display_coursenumber else ''}", + display_course_number: "${context_course.display_coursenumber | n, js_escaped_string}", revision: "${context_course.location.revision | h}", - self_paced: ${escape_json_dumps(context_course.self_paced) | n} + self_paced: ${context_course.self_paced | n, dump_js_escaped_json} }); }); % endif diff --git a/cms/templates/certificates.html b/cms/templates/certificates.html index d038cfe37e47813dc9ed4f26cdd9209df419178d..99ec02ab51645886e517997b5eb6a950542fbf60 100644 --- a/cms/templates/certificates.html +++ b/cms/templates/certificates.html @@ -4,7 +4,9 @@ <%! from contentstore import utils from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> <%block name="title">${_("Course Certificates")}</%block> @@ -29,11 +31,19 @@ CMS.User.isGlobalStaff = '${is_global_staff}'=='True' ? true : false; </%block> <%block name="requirejs"> +% if has_certificate_modes: require(["js/certificates/factories/certificates_page_factory"], function(CertificatesPageFactory) { - if(${escape_json_dumps(has_certificate_modes)}) { - CertificatesPageFactory(${escape_json_dumps(certificates) | n}, "${certificate_url}", "${course_outline_url}", ${escape_json_dumps(course_modes) | n}, ${escape_json_dumps(certificate_web_view_url) | n}, ${escape_json_dumps(is_active) | n}, ${escape_json_dumps(certificate_activation_handler_url) | n} ); - } + CertificatesPageFactory( + ${certificates | n, dump_js_escaped_json}, + "${certificate_url | n, js_escaped_string}", + "${course_outline_url | n, js_escaped_string}", + ${course_modes | n, dump_js_escaped_json}, + ${certificate_web_view_url | n, dump_js_escaped_json}, + ${is_active | n, dump_js_escaped_json}, + ${certificate_activation_handler_url | n, dump_js_escaped_json} + ); }); +% endif </%block> <%block name="content"> diff --git a/cms/templates/container.html b/cms/templates/container.html index 16bed1f9702c1a69d87fc32bcbeb38dbe40a784c..2d5f40f21d99504952c365d6b7a48008c3e72044 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -9,7 +9,9 @@ else: </%def> <%! from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) from util.markup import HTML, ugettext as _ %> <%block name="title">${xblock.display_name_with_default_escaped} ${xblock_type_display_name(xblock) | h}</%block> @@ -32,11 +34,11 @@ from util.markup import HTML, ugettext as _ <%block name="requirejs"> require(["js/factories/container"], function(ContainerFactory) { ContainerFactory( - ${ escape_json_dumps(component_templates) | n }, - ${ escape_json_dumps(xblock_info) | n }, - "${action | h}", + ${component_templates | n, dump_js_escaped_json}, + ${xblock_info | n, dump_js_escaped_json}, + "${action | n, js_escaped_string}", { - isUnitPage: ${ escape_json_dumps(is_unit_page) | n }, + isUnitPage: ${is_unit_page | n, dump_js_escaped_json}, canEdit: true } ); diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index dc2d4640ce941387cde16fddfbb1b08df1748df1..549a17dba2b7af48828e7f33ad3da853edb87f06 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -3,8 +3,9 @@ <%namespace name='static' file='static_content.html'/> <%! from django.utils.translation import ugettext as _ -from django.template.defaultfilters import escapejs -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> ## TODO decode course # from context_course into title. @@ -23,11 +24,11 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="requirejs"> require(["js/factories/course_info"], function(CourseInfoFactory) { CourseInfoFactory( - "${updates_url}", - "${handouts_locator | escapejs}", - "${base_asset_url}", - ${escape_json_dumps(push_notification_enabled) | n} - ); + "${updates_url | n, js_escaped_string}", + "${handouts_locator | n, js_escaped_string}", + "${base_asset_url | n, js_escaped_string}", + ${push_notification_enabled | n, dump_js_escaped_json} + ); }); </%block> diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index c88499567b0ed70a99fe26ef6c6a69b626f1f196..6c1f97d90ef19f2010f67454af88a904cdaf3b7a 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -4,7 +4,7 @@ import logging from util.date_utils import get_default_time_display from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json from contentstore.utils import reverse_usage_url from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration %> @@ -15,7 +15,10 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration <%block name="requirejs"> require(["js/factories/outline"], function (OutlineFactory) { - OutlineFactory(${escape_json_dumps(course_structure) | n}, ${escape_json_dumps(initial_state) | n}); + OutlineFactory( + ${course_structure | n, dump_js_escaped_json}, + ${initial_state | n, dump_js_escaped_json} + ); }); </%block> diff --git a/cms/templates/export.html b/cms/templates/export.html index 6a8168b4e4f2768af1b668f3a301cb29aecd921b..c6829d446962df5d2683dcfbc926550e05037e5b 100644 --- a/cms/templates/export.html +++ b/cms/templates/export.html @@ -11,7 +11,9 @@ else: <%! from django.utils.translation import ugettext as _ - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) %> <%block name="title"> %if library: @@ -24,11 +26,11 @@ else: <%block name="requirejs"> % if in_err: - var hasUnit = ${escape_json_dumps(bool(unit)) | n}, - editUnitUrl = "${edit_unit_url or ""}", - courselikeHomeUrl = "${courselike_home_url or ""}", - is_library = ${escape_json_dumps(library) | n} - errMsg = ${escape_json_dumps(raw_err_msg or "") | n}; + var hasUnit = ${bool(unit) | n, dump_js_escaped_json}, + editUnitUrl = "${edit_unit_url | n, js_escaped_string}", + courselikeHomeUrl = "${courselike_home_url | n, js_escaped_string}", + is_library = ${library | n, dump_js_escaped_json} + errMsg = "${raw_err_msg | n, js_escaped_string}"; require(["js/factories/export"], function(ExportFactory) { ExportFactory(hasUnit, editUnitUrl, courselikeHomeUrl, is_library, errMsg); diff --git a/cms/templates/group_configurations.html b/cms/templates/group_configurations.html index 1fa5cc61319a408412c7e9565bec8afdb3f686c7..9bc00afbc9a5a05ba3124ad5a8b7cdc9cfa60d6b 100644 --- a/cms/templates/group_configurations.html +++ b/cms/templates/group_configurations.html @@ -5,7 +5,9 @@ <%! from contentstore import utils from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> <%block name="title">${_("Group Configurations")}</%block> @@ -21,7 +23,13 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="requirejs"> require(["js/factories/group_configurations"], function(GroupConfigurationsFactory) { - GroupConfigurationsFactory(${escape_json_dumps(should_show_experiment_groups) | n}, ${escape_json_dumps(experiment_group_configurations) | n}, ${escape_json_dumps(content_group_configuration) | n}, "${group_configuration_url}", "${course_outline_url}"); + GroupConfigurationsFactory( + ${should_show_experiment_groups | n, dump_js_escaped_json}, + ${experiment_group_configurations | n, dump_js_escaped_json}, + ${content_group_configuration | n, dump_js_escaped_json}, + "${group_configuration_url | n, js_escaped_string}", + "${course_outline_url | n, js_escaped_string}" + ); }); </%block> diff --git a/cms/templates/import.html b/cms/templates/import.html index 989c2cbc1285628cd940d3259a6841a1db3d6c9f..b00db006d2f80125c3b75e562bd3092fd1190097 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -10,7 +10,9 @@ else: <%namespace name='static' file='static_content.html'/> <%! from django.utils.translation import ugettext as _ - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) %> <%block name="title"> %if library: @@ -239,6 +241,9 @@ else: <%block name="requirejs"> require(["js/factories/import"], function(ImportFactory) { - ImportFactory("${import_status_url}", ${escape_json_dumps(library) | n}); + ImportFactory( + "${import_status_url | n, js_escaped_string}", + ${library | n, dump_js_escaped_json} + ); }); </%block> diff --git a/cms/templates/library.html b/cms/templates/library.html index cf8eebcbd7105dd3963a4f891a9d8897ff4c1775..50ffd7e3fbf282f37a17696e225591de08f6b347 100644 --- a/cms/templates/library.html +++ b/cms/templates/library.html @@ -3,7 +3,7 @@ <%! from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json %> <%block name="title">${context_library.display_name_with_default_escaped} ${xblock_type_display_name(context_library)}</%block> <%block name="bodyclass">is-signedin course container view-container view-library</%block> @@ -24,8 +24,8 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="requirejs"> require(["js/factories/library"], function(LibraryFactory) { LibraryFactory( - ${escape_json_dumps(component_templates) | n}, - ${escape_json_dumps(xblock_info) | n}, + ${component_templates | n, dump_js_escaped_json}, + ${xblock_info | n, dump_js_escaped_json}, { isUnitPage: false, page_size: 10, diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 687247c5903efa66d8bbd58232cf7c2c0dc5dcdd..94212ee0a285a84f0c41e09f2f6eee00b4233d26 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -2,7 +2,10 @@ <%! from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from openedx.core.lib.js_utils import escape_json_dumps + +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> <%def name="online_help_token()"><% return "team_course" %></%def> <%block name="title">${_("Course Team Settings")}</%block> @@ -114,11 +117,11 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="requirejs"> require(["js/factories/manage_users"], function(ManageCourseUsersFactory) { ManageCourseUsersFactory( - "${context_course.display_name | h}", - ${escape_json_dumps(users) | n}, - "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}", - ${ request.user.id }, - ${str(allow_actions).lower()} + "${context_course.display_name_with_default | h}", + ${users | n, dump_js_escaped_json}, + "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'}) | n, js_escaped_string}", + ${request.user.id | n, dump_js_escaped_json}, + ${allow_actions | n, dump_js_escaped_json} ); }); </%block> diff --git a/cms/templates/manage_users_lib.html b/cms/templates/manage_users_lib.html index f0b5f3e1d247c2e9bf926864dd151d6ba5d69f3f..e8f4b0f51bd574af772a124217aff1c83ed6a7da 100644 --- a/cms/templates/manage_users_lib.html +++ b/cms/templates/manage_users_lib.html @@ -2,7 +2,10 @@ <%! from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from openedx.core.lib.js_utils import escape_json_dumps + +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> <%def name="online_help_token()"><% return "team_library" %></%def> <%block name="title">${_("Library User Access")}</%block> @@ -107,11 +110,11 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="requirejs"> require(["js/factories/manage_users_lib"], function(ManageLibraryUsersFactory) { ManageLibraryUsersFactory( - "${context_library.display_name_with_default_escaped | h}", - ${escape_json_dumps(users) | n}, - "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}", - ${ request.user.id }, - ${str(allow_actions).lower()} + "${context_library.display_name_with_default | h}", + ${users | n, dump_js_escaped_json}, + "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'}) | n, js_escaped_string}", + ${request.user.id | n, dump_js_escaped_json}, + ${allow_actions | n, dump_js_escaped_json} ); }); </%block> diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 265d2da82b53f111f19794f56f11a0e8027421d4..c10e9ebc8b1e33c92e40edda182f01c1e339a616 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -8,7 +8,9 @@ import urllib from django.utils.translation import ugettext as _ from contentstore import utils - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) %> <%block name="header_extras"> @@ -31,7 +33,10 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; <%block name="requirejs"> require(["js/factories/settings"], function(SettingsFactory) { - SettingsFactory("${details_url}", ${escape_json_dumps(show_min_grade_warning) | n}); + SettingsFactory( + "${details_url | n, js_escaped_string}", + ${show_min_grade_warning | n, dump_js_escaped_json} + ); }); </%block> diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 5bfb09679e3d0ddb8941af9f7a6f6783a31539ed..6c26c5eae7d7aefb641d74998e5909ff37e58988 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -4,7 +4,9 @@ <%! from django.utils.translation import ugettext as _ from contentstore import utils - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) %> <%block name="title">${_("Advanced Settings")}</%block> <%block name="bodyclass">is-signedin course advanced view-settings</%block> @@ -19,7 +21,10 @@ <%block name="requirejs"> require(["js/factories/settings_advanced"], function(SettingsAdvancedFactory) { - SettingsAdvancedFactory(${escape_json_dumps(advanced_dict) | n}, "${advanced_settings_url}"); + SettingsAdvancedFactory( + ${advanced_dict | n, dump_js_escaped_json}, + "${advanced_settings_url | n, js_escaped_string}" + ); }); </%block> diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index acb5f35e7f0a954653947ec98c74850e56830c05..44989d201d69db76db822a891cfa3cd53d628952 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -9,7 +9,9 @@ from contentstore import utils from django.utils.translation import ugettext as _ from models.settings.encoder import CourseSettingsEncoder - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string + ) %> <%block name="header_extras"> @@ -25,7 +27,11 @@ </%block> <%block name="requirejs"> require(["js/factories/settings_graders"], function(SettingsGradersFactory) { - SettingsGradersFactory(_.extend(${escape_json_dumps(course_details, cls=CourseSettingsEncoder) | n}, {is_credit_course: ${escape_json_dumps(is_credit_course) | n}}), "${grading_url}"); + SettingsGradersFactory( + _.extend(${dump_js_escaped_json(course_details, cls=CourseSettingsEncoder) | n}, + {is_credit_course: ${is_credit_course | n, dump_js_escaped_json}}), + "${grading_url | n, js_escaped_string}" + ); }); </%block> diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index 854639d198849694c5f44a44d3969511d53bda2a..9cee2fda1f2f3e9010c27ac96084ab4ae4224437 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -2,7 +2,9 @@ from django.utils.translation import ugettext as _ from contentstore.views.helpers import xblock_studio_url from contentstore.utils import is_visible_to_specific_content_groups -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> <% xblock_url = xblock_studio_url(xblock) @@ -24,10 +26,10 @@ messages = xblock.validate().to_json() <script> require(["jquery", "js/factories/xblock_validation"], function($, XBlockValidationFactory) { XBlockValidationFactory( - ${escape_json_dumps(messages) | n}, - $.parseJSON("${bool(xblock_url)}".toLowerCase()), // xblock_url will be None or a string - $.parseJSON("${bool(is_root)}".toLowerCase()), // is_root will be None or a boolean - $('div.xblock-validation-messages[data-locator="${xblock.location | h}"]') + ${messages | n, dump_js_escaped_json}, + ${bool(xblock_url) | n, dump_js_escaped_json}, // xblock_url will be None or a string + ${bool(is_root) | n, dump_js_escaped_json}, // is_root will be None or a boolean + $('div.xblock-validation-messages[data-locator="${xblock.location | n, js_escaped_string}"]') ); }); </script> diff --git a/cms/templates/textbooks.html b/cms/templates/textbooks.html index 4fc8ae552b8b29c4591b452601d2b4c42731625d..c550fe43d202509486f2610606c03b00b4deba1d 100644 --- a/cms/templates/textbooks.html +++ b/cms/templates/textbooks.html @@ -3,7 +3,7 @@ <%namespace name='static' file='static_content.html'/> <%! from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json %> <%block name="title">${_("Textbooks")}</%block> @@ -28,7 +28,7 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE}" </%block> <%block name="requirejs"> require(["js/factories/textbooks"], function(TextbooksFactory) { - TextbooksFactory(${escape_json_dumps(textbooks) | n}); + TextbooksFactory(${textbooks | n, dump_js_escaped_json}); }); </%block> diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 145d6a5c3d1c5ed86da1a0124bb653c9c77392e5..43229700d681825a6030182cfdab74b6a4c5c5e8 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -20,7 +20,7 @@ from django.http import HttpRequest from course_modes.models import CourseMode from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json from student.tests.factories import UserFactory from student_account.views import account_settings_context from third_party_auth.tests.testutil import simulate_running_pipeline, ThirdPartyAuthTestMixin @@ -387,7 +387,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi "finishAuthUrl": finish_auth_url, "errorMessage": None, } - auth_info = escape_json_dumps(auth_info) + auth_info = dump_js_escaped_json(auth_info) expected_data = '"third_party_auth": {auth_info}'.format( auth_info=auth_info diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index bc577ec938fcb8c62f23f8f41e42e4d15ee04c28..fdcc7e707f943b20470ac988cb46a43a7ec0e520 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -1,7 +1,11 @@ ## mako <%! import json %> -<%! from django.utils.translation import ugettext as _ %> -<%! from openedx.core.lib.js_utils import escape_json_dumps %> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +%> <%namespace name='static' file='/static_content.html'/> <%inherit file="/main.html" /> @@ -33,20 +37,20 @@ <%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory"> TeamsTabFactory({ - courseID: '${ unicode(course.id) }', - topics: ${ escape_json_dumps(topics) | n }, - userInfo: ${ escape_json_dumps(user_info) | n }, - topicUrl: '${ topic_url }', - topicsUrl: '${ topics_url }', - teamsUrl: '${ teams_url }', - teamsDetailUrl: '${ teams_detail_url }', - teamMembershipsUrl: '${ team_memberships_url }', - teamMembershipDetailUrl: '${ team_membership_detail_url }', - myTeamsUrl: '${ my_teams_url }', - maxTeamSize: ${ course.teams_max_size }, - languages: ${ escape_json_dumps(languages) | n }, - countries: ${ escape_json_dumps(countries) | n }, - teamsBaseUrl: '${ teams_base_url }' + courseID: '${unicode(course.id) | n, js_escaped_string}', + topics: ${topics | n, dump_js_escaped_json}, + userInfo: ${user_info | n, dump_js_escaped_json}, + topicUrl: '${topic_url | n, js_escaped_string}', + topicsUrl: '${topics_url | n, js_escaped_string}', + teamsUrl: '${teams_url | n, js_escaped_string}', + teamsDetailUrl: '${teams_detail_url | n, js_escaped_string}', + teamMembershipsUrl: '${team_memberships_url | n, js_escaped_string}', + teamMembershipDetailUrl: '${team_membership_detail_url | n, js_escaped_string}', + myTeamsUrl: '${my_teams_url | n, js_escaped_string}', + maxTeamSize: ${course.teams_max_size | n, dump_js_escaped_json}, + languages: ${languages | n, dump_js_escaped_json}, + countries: ${countries | n, dump_js_escaped_json}, + teamsBaseUrl: '${teams_base_url | n, js_escaped_string}' }); </%static:require_module> </%block> diff --git a/lms/envs/test.py b/lms/envs/test.py index 429d5c050770bbaee6effb412726c5ab9eb80a88..ab51774b5e5c170bfe38592d26fb271af09c9a00 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -495,7 +495,8 @@ MICROSITE_LOGISTRATION_HOSTNAME = 'logistration.testserver' # add extra template directory for test-only templates MAKO_TEMPLATES['main'].extend([ COMMON_ROOT / 'test' / 'templates', - COMMON_ROOT / 'test' / 'test_microsites' + COMMON_ROOT / 'test' / 'test_microsites', + REPO_ROOT / 'openedx' / 'core' / 'djangolib' / 'tests' / 'templates', ]) diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 0fdcc6836a8ea07aa266dcc25c0604cbca46bb8b..f9914bfe8f378468e7cd30c952a66f6e80d0a6bb 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -1,7 +1,7 @@ <%! import json from django.utils.translation import ugettext as _ - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import dump_js_escaped_json %> <%inherit file="../main.html" /> <% @@ -19,7 +19,7 @@ % endfor <%static:require_module module_name="js/discovery/discovery_factory" class_name="DiscoveryFactory"> DiscoveryFactory( - ${ escape_json_dumps(course_discovery_meanings) | n }, + ${course_discovery_meanings | n, dump_js_escaped_json}, getParameterByName('search_query') ); </%static:require_module> diff --git a/lms/templates/financial-assistance/apply.html b/lms/templates/financial-assistance/apply.html index 03b773527532673abb4ac70980732cff6dcd496d..ca588d264a3d224d444b1534ac305306f357bbec 100644 --- a/lms/templates/financial-assistance/apply.html +++ b/lms/templates/financial-assistance/apply.html @@ -2,21 +2,23 @@ <%! import json -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) %> <%namespace name='static' file='/static_content.html'/> <%block name="js_extra"> <%static:require_module module_name="js/financial-assistance/financial_assistance_form_factory" class_name="FinancialAssistanceFactory"> FinancialAssistanceFactory({ - fields: ${escape_json_dumps(fields)}, - user_details: ${escape_json_dumps(user_details)}, - header_text: ${escape_json_dumps(header_text)}, - student_faq_url: ${json.dumps(student_faq_url)}, - dashboard_url: ${json.dumps(dashboard_url)}, - account_settings_url: ${json.dumps(account_settings_url)}, - platform_name: ${escape_json_dumps(platform_name)}, - submit_url: ${json.dumps(submit_url)} + fields: ${fields | n, dump_js_escaped_json}, + user_details: ${user_details | n, dump_js_escaped_json}, + header_text: ${header_text | n, dump_js_escaped_json}, + student_faq_url: '${student_faq_url | n, js_escaped_string}', + dashboard_url: '${dashboard_url | n, js_escaped_string}', + account_settings_url: '${account_settings_url | n, js_escaped_string}', + platform_name: '${platform_name | n, js_escaped_string}', + submit_url: '${submit_url | n, js_escaped_string}' }); </%static:require_module> </%block> diff --git a/lms/templates/student_account/login_and_register.html b/lms/templates/student_account/login_and_register.html index 962936bb93859ef984491fdb8f4aee48492fb998..e83d1cbeec8d51d53c83d5e28017c6c7619baf19 100644 --- a/lms/templates/student_account/login_and_register.html +++ b/lms/templates/student_account/login_and_register.html @@ -1,7 +1,7 @@ <%! import json from django.utils.translation import ugettext as _ - from openedx.core.lib.js_utils import escape_json_dumps + from openedx.core.djangolib.js_utils import dump_js_escaped_json %> <%namespace name='static' file='/static_content.html'/> @@ -11,7 +11,7 @@ <%block name="js_extra"> <%static:require_module module_name="js/student_account/logistration_factory" class_name="LogistrationFactory"> - var options = ${ escape_json_dumps(data) | n }; + var options = ${data | n, dump_js_escaped_json}; LogistrationFactory(options); </%static:require_module> </%block> diff --git a/lms/templates/student_profile/learner_profile.html b/lms/templates/student_profile/learner_profile.html index 17ab3043d87db38382aa161458ac1c0f6934a556..e975796ddaaccd6f63bb6cfd88fadacd1411ad08 100644 --- a/lms/templates/student_profile/learner_profile.html +++ b/lms/templates/student_profile/learner_profile.html @@ -4,7 +4,7 @@ import json from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import dump_js_escaped_json %> <%block name="pagetitle">${_("Learner Profile")}</%block> @@ -24,7 +24,7 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="js_extra"> <%static:require_module module_name="js/student_profile/views/learner_profile_factory" class_name="LearnerProfileFactory"> - var options = ${ escape_json_dumps(data) | n }; + var options = ${data | n, dump_js_escaped_json}; LearnerProfileFactory(options); </%static:require_module> </%block> diff --git a/lms/templates/support/enrollment.html b/lms/templates/support/enrollment.html index 84f34394728f2f3dc2c7cf3c4c435e7a4eee0387..d5dc8e2d7ad544407e5126c1d76476fa42b80644 100644 --- a/lms/templates/support/enrollment.html +++ b/lms/templates/support/enrollment.html @@ -1,7 +1,6 @@ <%! from django.utils.translation import ugettext as _ - -from openedx.core.lib.js_utils import escape_json_dumps +from openedx.core.djangolib.js_utils import js_escaped_string %> <%namespace name='static' file='../static_content.html'/> @@ -11,9 +10,9 @@ from openedx.core.lib.js_utils import escape_json_dumps <%block name="js_extra"> <%static:require_module module_name="support/js/enrollment_factory" class_name="EnrollmentFactory"> new EnrollmentFactory({ - user: ${escape_json_dumps(username)}, - enrollmentsUrl: ${escape_json_dumps(enrollmentsUrl)}, - enrollmentSupportUrl: ${escape_json_dumps(enrollmentSupportUrl)}, + user: '${username | n, js_escaped_string}', + enrollmentsUrl: '${enrollmentsUrl | n, js_escaped_string}', + enrollmentSupportUrl: '${enrollmentSupportUrl | n, js_escaped_string}', }); </%static:require_module> </%block> diff --git a/openedx/README.rst b/openedx/README.rst index aece6f16f64d40c4157955c4f4767eea12d6a82a..ced9d71fc8f676f43bc8e2642541112e5c83bad5 100644 --- a/openedx/README.rst +++ b/openedx/README.rst @@ -6,6 +6,7 @@ from Open edX will eventually live here, including the code in the lms, cms, and common directories. If you're adding a new Django app, place it in core/djangoapps. If you're adding +utilities that require Django, place them in core/djangolib. If you're adding code that defines no Django models or views of its own but is widely useful, put it in core/lib. diff --git a/openedx/core/djangolib/__init__.py b/openedx/core/djangolib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/openedx/core/djangolib/js_utils.py b/openedx/core/djangolib/js_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..35a20a7504d59c645a8cb575e810d2cf9008b044 --- /dev/null +++ b/openedx/core/djangolib/js_utils.py @@ -0,0 +1,149 @@ +""" +Utilities for dealing with Javascript and JSON. +""" +import json + +from django.utils.html import escapejs +from mako.filters import decode +from markupsafe import escape + +from xmodule.modulestore import EdxJSONEncoder + + +def _escape_json_for_js(json_dumps_string): + """ + Escape output of JSON dumps that is safe to be embedded in a <SCRIPT> tag. + + This implementation is based on escaping performed in + simplejson.JSONEncoderForHTML. + + Arguments: + json_dumps_string (string): A JSON string to be escaped. + + This must be the output of json.dumps to ensure: + 1. The string contains valid JSON, and + 2. That non-ascii characters are properly escaped + + Returns: + (string) Escaped JSON that is safe to be embedded in HTML. + + """ + json_dumps_string = json_dumps_string.replace("&", "\\u0026") + json_dumps_string = json_dumps_string.replace(">", "\\u003e") + json_dumps_string = json_dumps_string.replace("<", "\\u003c") + return json_dumps_string + + +def dump_js_escaped_json(obj, cls=EdxJSONEncoder): + """ + JSON dumps and escapes objects that are safe to be embedded in JavaScript. + + Use this for anything but strings (e.g. dicts, tuples, lists, bools, and + numbers). For strings, use js_escaped_string. + + The output of this method is also usable as plain-old JSON. + + Usage: + Used as follows in a Mako template inside a <SCRIPT> tag:: + + var json_obj = ${obj | n, dump_js_escaped_json} + + If you must use the cls argument, then use as follows:: + + var json_obj = ${dump_js_escaped_json(obj, cls) | n} + + Use the "n" Mako filter above. It is possible that the default filter + may include html escaping in the future, and this ensures proper + escaping. + + Ensure ascii in json.dumps (ensure_ascii=True) allows safe skipping of + Mako's default filter decode.utf8. + + Arguments: + obj: The object soon to become a JavaScript escaped JSON string. The + object can be anything but strings (e.g. dicts, tuples, lists, bools, and + numbers). + cls (class): The JSON encoder class (defaults to EdxJSONEncoder). + + Returns: + (string) Escaped encoded JSON. + + """ + json_string = json.dumps(obj, ensure_ascii=True, cls=cls) + json_string = _escape_json_for_js(json_string) + return json_string + + +def dump_html_escaped_json(obj, cls=EdxJSONEncoder): + """ + JSON dumps and escapes objects that are safe to be embedded in HTML. + + Use this for anything but strings (e.g. dicts, tuples, lists, bools, and + numbers). For strings, just used the default html filter. + + Usage: + Used as follows in a Mako template inside a HTML, like in + a data attribute:: + + data-obj='${obj | n, dump_html_escaped_json}' + + If you must use the cls argument, then use as follows:: + + data-obj='${dump_html_escaped_json(obj, cls) | n}' + + Use the "n" Mako filter above. The default filter will include + html escaping in the future, and this ensures proper ordering of + these calls. + + Ensure ascii in json.dumps (ensure_ascii=True) allows safe skipping of + Mako's default filter decode.utf8. + + Arguments: + obj: The object soon to become an HTML escaped JSON string. The object + can be anything but strings (e.g. dicts, tuples, lists, bools, and + numbers). + cls (class): The JSON encoder class (defaults to EdxJSONEncoder). + + Returns: + (string) Escaped encoded JSON. + + """ + json_string = json.dumps(obj, ensure_ascii=True, cls=cls) + json_string = escape(json_string) + return json_string + + +def js_escaped_string(string_for_js): + """ + Mako filter that escapes text for use in a JavaScript string. + + If None is provided, returns an empty string. + + Usage: + Used as follows in a Mako template inside a <SCRIPT> tag:: + + var my_string_for_js = "${my_string_for_js | n, js_escaped_string}" + + The surrounding quotes for the string must be included. + + Use the "n" Mako filter above. It is possible that the default filter + may include html escaping in the future, and this ensures proper + escaping. + + Mako's default filter decode.utf8 is applied here since this default + filter is skipped in the Mako template with "n". + + Arguments: + string_for_js (string): Text to be properly escaped for use in a + JavaScript string. + + Returns: + (string) Text properly escaped for use in a JavaScript string as + unicode. Returns empty string if argument is None. + + """ + if string_for_js is None: + string_for_js = "" + string_for_js = decode.utf8(string_for_js) + string_for_js = escapejs(string_for_js) + return string_for_js diff --git a/openedx/core/djangolib/tests/__init__.py b/openedx/core/djangolib/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/openedx/core/djangolib/tests/test_js_utils.py b/openedx/core/djangolib/tests/test_js_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6bfb51476c88b2ce305bd352c6b96e844f6edec1 --- /dev/null +++ b/openedx/core/djangolib/tests/test_js_utils.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +""" +Tests for js_utils.py +""" +import json +from unittest import TestCase +import HTMLParser + +from mako.template import Template + +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, dump_html_escaped_json, js_escaped_string +) + + +class TestJSUtils(TestCase): + """ + Test JS utils + """ + + class NoDefaultEncoding(object): + """ + Helper class that has no default JSON encoding + """ + def __init__(self, value): + self.value = value + + class SampleJSONEncoder(json.JSONEncoder): + """ + A test encoder that is used to prove that the encoder does its job before the escaping. + """ + # pylint: disable=method-hidden + def default(self, noDefaultEncodingObj): + return noDefaultEncodingObj.value.replace("<script>", "sample-encoder-was-here") + + def test_dump_js_escaped_json_escapes_unsafe_html(self): + """ + Test dump_js_escaped_json properly escapes &, <, and >. + """ + malicious_dict = {"</script><script>alert('hello, ');</script>": "</script><script>alert('&world!');</script>"} + expected_escaped_json = ( + r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": ''' + r'''"\u003c/script\u003e\u003cscript\u003ealert('\u0026world!');\u003c/script\u003e"}''' + ) + + escaped_json = dump_js_escaped_json(malicious_dict) + self.assertEquals(expected_escaped_json, escaped_json) + + def test_dump_js_escaped_json_with_custom_encoder_escapes_unsafe_html(self): + """ + Test dump_js_escaped_json first encodes with custom JSNOEncoder before escaping &, <, and > + + The test encoder class should first perform the replacement of "<script>" with + "sample-encoder-was-here", and then should escape the remaining &, <, and >. + + """ + malicious_dict = { + "</script><script>alert('hello, ');</script>": + self.NoDefaultEncoding("</script><script>alert('&world!');</script>") + } + expected_custom_escaped_json = ( + r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": ''' + r'''"\u003c/script\u003esample-encoder-was-herealert('\u0026world!');\u003c/script\u003e"}''' + ) + + escaped_json = dump_js_escaped_json(malicious_dict, cls=self.SampleJSONEncoder) + self.assertEquals(expected_custom_escaped_json, escaped_json) + + def test_dump_html_escaped_json_escapes_unsafe_html(self): + """ + Test dump_html_escaped_json properly escapes &, <, and >. + """ + malicious_dict = {"</script><script>alert('hello, ');</script>": "</script><script>alert('&world!');</script>"} + expected_escaped_json = ( + "{"</script><script>alert('hello, ');</script>": " + ""</script><script>alert('&world!');</script>"}" + ) + + escaped_json = dump_html_escaped_json(malicious_dict) + self.assertEquals(expected_escaped_json, escaped_json) + + def test_dump_html_escaped_json_with_custom_encoder_escapes_unsafe_html(self): + """ + Test dump_html_escaped_json first encodes with custom JSNOEncoder before escaping &, <, and > + + The test encoder class should first perform the replacement of "<script>" with + "sample-encoder-was-here", and then should escape the remaining &, <, and >. + + """ + malicious_dict = { + "</script><script>alert('hello, ');</script>": + self.NoDefaultEncoding("</script><script>alert('&world!');</script>") + } + expected_custom_escaped_json = ( + "{"</script><script>alert('hello, ');</script>": " + ""</script>sample-encoder-was-herealert('&world!');</script>"}" + ) + escaped_json = dump_html_escaped_json(malicious_dict, cls=self.SampleJSONEncoder) + self.assertEquals(expected_custom_escaped_json, escaped_json) + + def test_js_escaped_string_escapes_unsafe_html(self): + """ + Test js_escaped_string escapes &, <, and >, as well as returns a unicode type + """ + malicious_js_string = "</script><script>alert('hello, ');</script>" + + expected_escaped_string_for_js = unicode( + r"\u003C/script\u003E\u003Cscript\u003Ealert(\u0027hello, \u0027)\u003B\u003C/script\u003E" + ) + escaped_string_for_js = js_escaped_string(malicious_js_string) + self.assertEquals(expected_escaped_string_for_js, escaped_string_for_js) + + def test_js_escaped_string_with_none(self): + """ + Test js_escaped_string returns empty string for None + """ + escaped_string_for_js = js_escaped_string(None) + self.assertEquals(u"", escaped_string_for_js) + + def test_mako(self): + """ + Tests the full suite of Mako best practices by running all of the + combinations of types of data and types of escaping through a Mako + template. + + Additionally, validates the best practices themselves by validating + the expectations to ensure they can properly be unescaped and/or + parsed from json where applicable. + """ + test_dict = { + 'test_string': u'test-=&\\;\'"<>☃'.encode(encoding='utf-8'), + 'test_tuple': (1, 2, 3), + 'test_number': 3.5, + 'test_bool': False, + } + + template = Template( + """ + <%! + from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, dump_html_escaped_json, js_escaped_string + ) + %> + <body> + <div + data-test-dict='${test_dict | n, dump_html_escaped_json}' + data-test-string='${test_dict["test_string"]}' + data-test-tuple='${test_dict["test_tuple"] | n, dump_html_escaped_json}' + data-test-number='${test_dict["test_number"] | n, dump_html_escaped_json}' + data-test-bool='${test_dict["test_bool"] | n, dump_html_escaped_json}' + ></div> + + <script> + var test_dict = ${test_dict | n, dump_js_escaped_json} + var test_string = '${test_dict["test_string"] | n, js_escaped_string}' + var test_none_string = '${None | n, js_escaped_string}' + var test_tuple = ${test_dict["test_tuple"] | n, dump_js_escaped_json} + var test_number = ${test_dict["test_number"] | n, dump_js_escaped_json} + var test_bool = ${test_dict["test_bool"] | n, dump_js_escaped_json} + var test_none_json = ${None | n, dump_js_escaped_json} + </script> + </body> + """, + default_filters=['decode.utf8', 'h'], + ) + out = template.render(test_dict=test_dict) + + expected_json_for_html = ( + r"{"test_bool": false, "test_number": 3.5, " + r""test_tuple": [1, 2, 3], "test_string": " + r""test-=&\\;'\"<>\u2603"}" + ) + expected_attr_json_for_html = "data-test-dict='" + expected_json_for_html + "'" + self._validate_expectation_of_json_for_html(test_dict, expected_json_for_html) + self.assertIn(expected_attr_json_for_html, out) + self.assertIn(u"data-test-string='test-=&\\;'"<>☃'", out) + self.assertIn("data-test-tuple='[1, 2, 3]'", out) + self.assertIn("data-test-number='3.5'", out) + self.assertIn("data-test-bool='false'", out) + expected_string_for_js_in_dict = r'''test-=\u0026\\;'\"\u003c\u003e\u2603''' + self._validate_expectation_of_string_for_js(test_dict['test_string'], expected_string_for_js_in_dict) + self.assertIn( + ( + 'var test_dict = {"test_bool": false, "test_number": 3.5, ' + '"test_tuple": [1, 2, 3], "test_string": "' + expected_string_for_js_in_dict + '"}' + ), out) + expected_string_for_js = r"test\u002D\u003D\u0026\u005C\u003B\u0027\u0022\u003C\u003E☃" + self._validate_expectation_of_string_for_js(test_dict['test_string'], expected_string_for_js) + self.assertIn( + "var test_string = '" + expected_string_for_js.decode(encoding='utf-8') + "'", + out) + self.assertIn("var test_none_string = ''", out) + self.assertIn("var test_tuple = [1, 2, 3]", out) + self.assertIn("var test_number = 3.5", out) + self.assertIn("var test_bool = false", out) + self.assertIn("var test_none_json = null", out) + + def _validate_expectation_of_json_for_html(self, test_dict, expected_json_for_html_string): + """ + Proves that the expectation string is a reasonable one, since it is + not very human readable with all of the escaping. + + Ensures that after unescaping (html) the string can be parsed to a + (nearly) equivalent dict. + + Assertions will fail if the expectation is invalid. + + Arguments: + test_dict: The original dict to be tested in the Mako template. + expected_json_for_html_string: An html escaped json string that + should be parseable into a near equivalent to test_dict. + + """ + html_parser = HTMLParser.HTMLParser() + + expected_json = html_parser.unescape(expected_json_for_html_string) + parsed_expected_dict = json.loads(expected_json) + # tuples become arrays in json, so it is parsed to a list that is + # switched back to a tuple before comparing + parsed_expected_dict['test_tuple'] = tuple(parsed_expected_dict['test_tuple']) + self.assertEqual(test_dict['test_string'].decode(encoding='utf-8'), parsed_expected_dict['test_string']) + self.assertEqual(test_dict['test_tuple'], parsed_expected_dict['test_tuple']) + self.assertEqual(test_dict['test_number'], parsed_expected_dict['test_number']) + self.assertEqual(test_dict['test_bool'], parsed_expected_dict['test_bool']) + + def _validate_expectation_of_string_for_js(self, test_string, expected_string_for_js): + """ + Proves that the expectation string is a reasonable one, since it is + not very human readable with all of the escaping. + + Ensures that after parsing the string is equal to the original. + + Assertions will fail if the expectation is invalid. + + Arguments: + test_string: The original string to be tested in the Mako template. + expected_string_for_js: An escaped for js string that should be + parseable into the same string as test_string. + + """ + parsed_expected_string = json.loads('"' + expected_string_for_js + '"') + self.assertEqual(test_string.decode(encoding='utf-8'), parsed_expected_string) diff --git a/openedx/core/lib/js_utils.py b/openedx/core/lib/js_utils.py deleted file mode 100644 index 020ad6066bea9d3a8e74c3858e2af29bc54f08f2..0000000000000000000000000000000000000000 --- a/openedx/core/lib/js_utils.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Utilities for dealing with Javascript and JSON. -""" -import json -from django.template.defaultfilters import escapejs -from mako.filters import decode -from xmodule.modulestore import EdxJSONEncoder - - -def _escape_json_for_html(json_string): - """ - Escape JSON that is safe to be embedded in HTML. - - This implementation is based on escaping performed in simplejson.JSONEncoderForHTML. - - Arguments: - json_string (string): The JSON string to be escaped - - Returns: - (string) Escaped JSON that is safe to be embedded in HTML. - - """ - json_string = json_string.replace("&", "\\u0026") - json_string = json_string.replace(">", "\\u003e") - json_string = json_string.replace("<", "\\u003c") - return json_string - - -def escape_json_dumps(obj, cls=EdxJSONEncoder): - """ - JSON dumps and escapes JSON that is safe to be embedded in HTML. - - Usage: - Can be used inside a Mako template inside a <SCRIPT> as follows: - var my_json = ${escape_json_dumps(my_object) | n} - - Use the "n" Mako filter above. It is possible that the - default filter may include html escaping in the future, and - we must make sure to get the proper escaping. - - Ensure ascii in json.dumps (ensure_ascii=True) allows safe skipping of Mako's - default filter decode.utf8. - - Arguments: - obj: The json object to be encoded and dumped to a string - cls (class): The JSON encoder class (defaults to EdxJSONEncoder) - - Returns: - (string) Escaped encoded JSON - - """ - encoded_json = json.dumps(obj, ensure_ascii=True, cls=cls) - encoded_json = _escape_json_for_html(encoded_json) - return encoded_json - - -def escape_js_string(js_string): - """ - Escape a javascript string that is safe to be embedded in HTML. - - Usage: - Can be used inside a Mako template inside a <SCRIPT> as follows: - var my_js_string = "${escape_js_string(my_js_string) | n}" - - Must include the surrounding quotes for the string. - - Use the "n" Mako filter above. It is possible that the - default filter may include html escaping in the future, and - we must make sure to get the proper escaping. - - Mako's default filter decode.utf8 is applied here since this default - filter is skipped in the Mako template with "n". - - Arguments: - js_string (string): The javascript string to be escaped - - Returns: - (string) Escaped javascript as unicode - - """ - js_string = decode.utf8(js_string) - js_string = escapejs(js_string) - return js_string diff --git a/openedx/core/lib/tests/test_js_utils.py b/openedx/core/lib/tests/test_js_utils.py deleted file mode 100644 index 767917bb0b11a3fe242a5e5d5c784fb2c7cb61fb..0000000000000000000000000000000000000000 --- a/openedx/core/lib/tests/test_js_utils.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Tests for js_utils.py -""" -import json -from unittest import TestCase -from openedx.core.lib.js_utils import ( - escape_json_dumps, escape_js_string -) - - -class TestJSUtils(TestCase): - """ - Test JS utils - """ - - class NoDefaultEncoding(object): - """ - Helper class that has no default JSON encoding - """ - def __init__(self, value): - self.value = value - - class SampleJSONEncoder(json.JSONEncoder): - """ - A test encoder that is used to prove that the encoder does its job before the escaping. - """ - # pylint: disable=method-hidden - def default(self, noDefaultEncodingObj): - return noDefaultEncodingObj.value.replace("<script>", "sample-encoder-was-here") - - def test_escape_json_dumps_escapes_unsafe_html(self): - """ - Test escape_json_dumps properly escapes &, <, and >. - """ - malicious_json = {"</script><script>alert('hello, ');</script>": "</script><script>alert('&world!');</script>"} - expected_encoded_json = ( - r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": ''' - r'''"\u003c/script\u003e\u003cscript\u003ealert('\u0026world!');\u003c/script\u003e"}''' - ) - - encoded_json = escape_json_dumps(malicious_json) - self.assertEquals(expected_encoded_json, encoded_json) - - def test_escape_json_dumps_with_custom_encoder_escapes_unsafe_html(self): - """ - Test escape_json_dumps first encodes with custom JSNOEncoder before escaping &, <, and > - - The test encoder class should first perform the replacement of "<script>" with - "sample-encoder-was-here", and then should escape the remaining &, <, and >. - - """ - malicious_json = { - "</script><script>alert('hello, ');</script>": - self.NoDefaultEncoding("</script><script>alert('&world!');</script>") - } - expected_custom_encoded_json = ( - r'''{"\u003c/script\u003e\u003cscript\u003ealert('hello, ');\u003c/script\u003e": ''' - r'''"\u003c/script\u003esample-encoder-was-herealert('\u0026world!');\u003c/script\u003e"}''' - ) - - encoded_json = escape_json_dumps(malicious_json, cls=self.SampleJSONEncoder) - self.assertEquals(expected_custom_encoded_json, encoded_json) - - def test_escape_js_string_escapes_unsafe_html(self): - """ - Test escape_js_string escapes &, <, and >, as well as returns a unicode type - """ - malicious_js_string = "</script><script>alert('hello, ');</script>" - - expected_escaped_js_string = unicode( - r"\u003C/script\u003E\u003Cscript\u003Ealert(\u0027hello, \u0027)\u003B\u003C/script\u003E" - ) - escaped_js_string = escape_js_string(malicious_js_string) - self.assertEquals(expected_escaped_js_string, escaped_js_string) diff --git a/pavelib/utils/test/suites/nose_suite.py b/pavelib/utils/test/suites/nose_suite.py index 16699d13ef8c7544b3970a1be75de64710db07d5..e7f93defd8171d4e447fffae32cce796e0c215d0 100644 --- a/pavelib/utils/test/suites/nose_suite.py +++ b/pavelib/utils/test/suites/nose_suite.py @@ -158,6 +158,7 @@ class SystemTestSuite(NoseTestSuite): if self.root == 'lms': default_test_id += " {system}/tests.py" + default_test_id += " openedx/core/djangolib" if self.root == 'cms': default_test_id += " {system}/tests/*"