Merge pull request #11257 from edx/robrap/refactor-js-utils
Refactor js_util helpers
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
0
openedx/core/djangolib/__init__.py
Normal file
0
openedx/core/djangolib/__init__.py
Normal file
149
openedx/core/djangolib/js_utils.py
Normal file
149
openedx/core/djangolib/js_utils.py
Normal file
@@ -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
|
||||
0
openedx/core/djangolib/tests/__init__.py
Normal file
0
openedx/core/djangolib/tests/__init__.py
Normal file
242
openedx/core/djangolib/tests/test_js_utils.py
Normal file
242
openedx/core/djangolib/tests/test_js_utils.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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/*"
|
||||
|
||||
Reference in New Issue
Block a user