Commit b0956516 by muzaffaryousaf

Escape html/js with other bug fixes .

TNL-4164
parent 78a61c1f
...@@ -6,14 +6,13 @@ import random ...@@ -6,14 +6,13 @@ import random
from uuid import uuid4 from uuid import uuid4
from datetime import datetime from datetime import datetime
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest, EventsTestMixin
from ...fixtures.course import CourseFixture, XBlockFixtureDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...pages.lms.auto_auth import AutoAuthPage from ...pages.lms.auto_auth import AutoAuthPage
from ...pages.lms.course_nav import CourseNavPage from ...pages.lms.course_nav import CourseNavPage
from ...pages.lms.courseware import CoursewarePage from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent from ...pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent
from ...fixtures.edxnotes import EdxNotesFixture, Note, Range from ...fixtures.edxnotes import EdxNotesFixture, Note, Range
from ..helpers import EventsTestMixin
class EdxNotesTestMixin(UniqueCourseTest): class EdxNotesTestMixin(UniqueCourseTest):
...@@ -536,6 +535,57 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin): ...@@ -536,6 +535,57 @@ class EdxNotesPageTest(EventsTestMixin, EdxNotesTestMixin):
"You have not made any notes in this course yet. Other students in this course are using notes to:", "You have not made any notes in this course yet. Other students in this course are using notes to:",
notes_page_empty.no_content_text) notes_page_empty.no_content_text)
def test_notes_works_correctly_with_xss(self):
"""
Scenario: Note text & tags should be HTML and JS escaped
Given I am enrolled in a course with notes enabled
When I visit the Notes page, with a Notes text and tag containing HTML characters like < and >
Then the text and tags appear as expected due to having been properly escaped
"""
xblocks = self.course_fixture.get_nested_xblocks(category="html")
self._add_notes([
Note(
usage_id=xblocks[0].locator,
user=self.username,
course_id=self.course_fixture._course_key, # pylint: disable=protected-access
text='<script>alert("XSS")</script>',
quote="quote",
updated=datetime(2014, 1, 1, 1, 1, 1, 1).isoformat(),
tags=['<script>alert("XSS")</script>']
),
Note(
usage_id=xblocks[1].locator,
user=self.username,
course_id=self.course_fixture._course_key, # pylint: disable=protected-access
text='<b>bold</b>',
quote="quote",
updated=datetime(2014, 2, 1, 1, 1, 1, 1).isoformat(),
tags=['<i>bold</i>']
)
])
self.notes_page.visit()
notes = self.notes_page.notes
self.assertEqual(len(notes), 2)
self.assertNoteContent(
notes[0],
quote=u"quote",
text='<b>bold</b>',
unit_name="Test Unit 1",
time_updated="Feb 01, 2014 at 01:01 UTC",
tags=['<i>bold</i>']
)
self.assertNoteContent(
notes[1],
quote=u"quote",
text='<script>alert("XSS")</script>',
unit_name="Test Unit 1",
time_updated="Jan 01, 2014 at 01:01 UTC",
tags=['<script>alert("XSS")</script>']
)
def test_recent_activity_view(self): def test_recent_activity_view(self):
""" """
Scenario: User can view all notes by recent activity. Scenario: User can view all notes by recent activity.
......
...@@ -344,7 +344,7 @@ def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, t ...@@ -344,7 +344,7 @@ def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, t
collection['previous'] collection['previous']
) )
return json.dumps(collection, cls=NoteJSONEncoder) return collection
def get_endpoint(api_url, path=""): def get_endpoint(api_url, path=""):
......
...@@ -387,7 +387,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase): ...@@ -387,7 +387,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
}, },
] ]
}, },
json.loads(helpers.get_notes(self.request, self.course)) helpers.get_notes(self.request, self.course)
) )
@patch("edxnotes.helpers.requests.get", autospec=True) @patch("edxnotes.helpers.requests.get", autospec=True)
...@@ -493,7 +493,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase): ...@@ -493,7 +493,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
}, },
] ]
}, },
json.loads(helpers.get_notes(self.request, self.course)) helpers.get_notes(self.request, self.course)
) )
@patch("edxnotes.helpers.requests.get", autospec=True) @patch("edxnotes.helpers.requests.get", autospec=True)
...@@ -520,7 +520,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase): ...@@ -520,7 +520,7 @@ class EdxNotesHelpersTest(ModuleStoreTestCase):
mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE) mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE)
self.assertItemsEqual( self.assertItemsEqual(
NOTES_VIEW_EMPTY_RESPONSE, NOTES_VIEW_EMPTY_RESPONSE,
json.loads(helpers.get_notes(self.request, self.course)) helpers.get_notes(self.request, self.course)
) )
def test_preprocess_collection_no_item(self): def test_preprocess_collection_no_item(self):
...@@ -989,14 +989,14 @@ class EdxNotesViewsTest(ModuleStoreTestCase): ...@@ -989,14 +989,14 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
# pylint: disable=unused-argument # pylint: disable=unused-argument
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True})
@patch("edxnotes.views.get_notes", return_value=json.dumps({'results': []})) @patch("edxnotes.views.get_notes", return_value={'results': []})
def test_edxnotes_view_is_enabled(self, mock_get_notes): def test_edxnotes_view_is_enabled(self, mock_get_notes):
""" """
Tests that appropriate view is received if EdxNotes feature is enabled. Tests that appropriate view is received if EdxNotes feature is enabled.
""" """
enable_edxnotes_for_the_course(self.course, self.user.id) enable_edxnotes_for_the_course(self.course, self.user.id)
response = self.client.get(self.notes_page_url) response = self.client.get(self.notes_page_url)
self.assertContains(response, 'Highlights and notes you\'ve made in course content') self.assertContains(response, 'Highlights and notes you&#39;ve made in course content')
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False}) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False})
def test_edxnotes_view_is_disabled(self): def test_edxnotes_view_is_disabled(self):
...@@ -1012,7 +1012,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase): ...@@ -1012,7 +1012,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
""" """
Tests that search notes successfully respond if EdxNotes feature is enabled. Tests that search notes successfully respond if EdxNotes feature is enabled.
""" """
mock_search.return_value = json.dumps(NOTES_VIEW_EMPTY_RESPONSE) mock_search.return_value = NOTES_VIEW_EMPTY_RESPONSE
enable_edxnotes_for_the_course(self.course, self.user.id) enable_edxnotes_for_the_course(self.course, self.user.id)
response = self.client.get(self.notes_url, {"text": "test"}) response = self.client.get(self.notes_url, {"text": "test"})
self.assertEqual(json.loads(response.content), NOTES_VIEW_EMPTY_RESPONSE) self.assertEqual(json.loads(response.content), NOTES_VIEW_EMPTY_RESPONSE)
......
...@@ -22,6 +22,7 @@ from edxnotes.helpers import ( ...@@ -22,6 +22,7 @@ from edxnotes.helpers import (
get_course_position, get_course_position,
DEFAULT_PAGE, DEFAULT_PAGE,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
NoteJSONEncoder,
) )
...@@ -47,18 +48,19 @@ def edxnotes(request, course_id): ...@@ -47,18 +48,19 @@ def edxnotes(request, course_id):
raise Http404 raise Http404
notes_info = get_notes(request, course) notes_info = get_notes(request, course)
has_notes = (len(notes_info.get('results')) > 0)
context = { context = {
"course": course, "course": course,
"notes_endpoint": reverse("notes", kwargs={"course_id": course_id}), "notes_endpoint": reverse("notes", kwargs={"course_id": course_id}),
"notes": notes_info, "notes": notes_info,
"page_size": DEFAULT_PAGE_SIZE, "page_size": DEFAULT_PAGE_SIZE,
"debug": json.dumps(settings.DEBUG), "debug": settings.DEBUG,
'position': None, 'position': None,
'disabled_tabs': settings.NOTES_DISABLED_TABS, 'disabled_tabs': settings.NOTES_DISABLED_TABS,
'has_notes': has_notes,
} }
if len(json.loads(notes_info)['results']) == 0: if not has_notes:
field_data_cache = FieldDataCache.cache_for_descriptor_descendents( field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2 course.id, request.user, course, depth=2
) )
...@@ -164,7 +166,7 @@ def notes(request, course_id): ...@@ -164,7 +166,7 @@ def notes(request, course_id):
except (EdxNotesParseError, EdxNotesServiceUnavailable) as err: except (EdxNotesParseError, EdxNotesServiceUnavailable) as err:
return JsonResponseBadRequest({"error": err.message}, status=500) return JsonResponseBadRequest({"error": err.message}, status=500)
return HttpResponse(notes_info, content_type="application/json") return HttpResponse(json.dumps(notes_info, cls=NoteJSONEncoder), content_type="application/json")
# pylint: disable=unused-argument # pylint: disable=unused-argument
......
...@@ -34,9 +34,7 @@ var edx = edx || {}; ...@@ -34,9 +34,7 @@ var edx = edx || {};
// Emit an event when the 'course title link' is clicked. // Emit an event when the 'course title link' is clicked.
edx.dashboard.trackCourseTitleClicked = function($courseTitleLink, properties){ edx.dashboard.trackCourseTitleClicked = function($courseTitleLink, properties){
var trackProperty = properties || edx.dashboard.generateTrackProperties; var trackProperty = properties || edx.dashboard.generateTrackProperties;
if (!window.analytics) {
return;
}
window.analytics.trackLink( window.analytics.trackLink(
$courseTitleLink, $courseTitleLink,
'edx.bi.dashboard.course_title.clicked', 'edx.bi.dashboard.course_title.clicked',
...@@ -105,9 +103,6 @@ var edx = edx || {}; ...@@ -105,9 +103,6 @@ var edx = edx || {};
}; };
edx.dashboard.xseriesTrackMessages = function() { edx.dashboard.xseriesTrackMessages = function() {
if (!window.analytics) {
return;
}
$('.xseries-action .btn').each(function(i, element) { $('.xseries-action .btn').each(function(i, element) {
var data = edx.dashboard.generateProgramProperties($(element)); var data = edx.dashboard.generateProgramProperties($(element));
...@@ -117,6 +112,9 @@ var edx = edx || {}; ...@@ -117,6 +112,9 @@ var edx = edx || {};
}; };
$(document).ready(function() { $(document).ready(function() {
if (!window.analytics) {
return;
}
edx.dashboard.trackCourseTitleClicked($('.course-title > a')); edx.dashboard.trackCourseTitleClicked($('.course-title > a'));
edx.dashboard.trackCourseImageLinkClicked($('.cover')); edx.dashboard.trackCourseImageLinkClicked($('.cover'));
edx.dashboard.trackEnterCourseLinkClicked($('.enter-course')); edx.dashboard.trackEnterCourseLinkClicked($('.enter-course'));
......
...@@ -51,10 +51,6 @@ define(['backbone', 'js/edxnotes/utils/utils', 'underscore.string'], function (B ...@@ -51,10 +51,6 @@ define(['backbone', 'js/edxnotes/utils/utils', 'underscore.string'], function (B
} }
return message; return message;
},
getText: function () {
return Utils.nl2br(this.get('text'));
} }
}); });
......
...@@ -31,8 +31,7 @@ define([ ...@@ -31,8 +31,7 @@ define([
getContext: function () { getContext: function () {
return $.extend({}, this.model.toJSON(), { return $.extend({}, this.model.toJSON(), {
message: this.model.getQuote(), message: this.model.getQuote()
text: this.model.getText()
}); });
}, },
...@@ -60,7 +59,9 @@ define([ ...@@ -60,7 +59,9 @@ define([
tagHandler: function (event) { tagHandler: function (event) {
event.preventDefault(); event.preventDefault();
this.options.scrollToTag(event.currentTarget.text); if (!_.isUndefined(this.options.scrollToTag)) {
this.options.scrollToTag(event.currentTarget.text);
}
}, },
redirectTo: function (uri) { redirectTo: function (uri) {
......
...@@ -46,7 +46,7 @@ define([ ...@@ -46,7 +46,7 @@ define([
it('can return appropriate `text`', function () { it('can return appropriate `text`', function () {
var model = this.collection.at(0); var model = this.collection.at(0);
expect(model.getText()).toBe('text<br> with<br>line<br>breaks <br>'); expect(model.get('text')).toBe('text\n with\r\nline\n\rbreaks \r');
}); });
}); });
}); });
...@@ -189,6 +189,10 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -189,6 +189,10 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
background: transparent; background: transparent;
} }
.note-comment-p {
word-wrap: break-word;
}
.note-comment-ul, .note-comment-ul,
.note-comment-ol { .note-comment-ol {
padding: auto; padding: auto;
...@@ -233,11 +237,14 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; ...@@ -233,11 +237,14 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4;
color: $m-gray-d2; color: $m-gray-d2;
} }
// CASE: tag matches a search query
.reference-meta.reference-tags .note-highlight {
background-color: $result-highlight-color-base;
}
.reference-meta.reference-tags {
word-wrap: break-word;
// CASE: tag matches a search query
.note-highlight {
background-color: $result-highlight-color-base;
}
}
// Put commas between tags. // Put commas between tags.
span.reference-meta.reference-tags:after { span.reference-meta.reference-tags:after {
content: ","; content: ",";
......
<%page expression_filter="h"/>
<%inherit file="/main.html" /> <%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import json from edxnotes.helpers import NoteJSONEncoder
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
%> %>
<%block name="bodyclass">view-student-notes is-in-course course</%block> <%block name="bodyclass">view-student-notes is-in-course course</%block>
...@@ -12,10 +15,7 @@ import json ...@@ -12,10 +15,7 @@ import json
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='edxnotes'" /> <%include file="/courseware/course_navigation.html" args="active_page='edxnotes'" />
<%
_notes = json.loads(notes)
has_notes = _notes and _notes.get('results')
%>
<section class="container"> <section class="container">
<div class="wrapper-student-notes"> <div class="wrapper-student-notes">
<div class="student-notes"> <div class="student-notes">
...@@ -111,11 +111,11 @@ import json ...@@ -111,11 +111,11 @@ import json
<%block name="js_extra"> <%block name="js_extra">
<%static:require_module module_name="js/edxnotes/views/page_factory" class_name="NotesPageFactory"> <%static:require_module module_name="js/edxnotes/views/page_factory" class_name="NotesPageFactory">
NotesPageFactory({ NotesPageFactory({
disabledTabs: ${disabled_tabs}, disabledTabs: ${disabled_tabs | n, dump_js_escaped_json},
notes: ${notes}, notes: ${dump_js_escaped_json(notes, NoteJSONEncoder) | n},
notesEndpoint: '${notes_endpoint}', notesEndpoint: ${notes_endpoint | n, dump_js_escaped_json},
pageSize: '${page_size}', pageSize: ${page_size | n, dump_js_escaped_json},
debugMode: ${debug} debugMode: ${debug | n, dump_js_escaped_json}
}); });
</%static:require_module> </%static:require_module>
</%block> </%block>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment