diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 686c9275cf..8588eef7b1 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -50,7 +50,7 @@ from edxmako.shortcuts import render_to_string from lms.djangoapps.lms_xblock.field_data import LmsFieldData from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig from openedx.core.djangoapps.bookmarks.services import BookmarksService -from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes +from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem from lms.djangoapps.verify_student.services import VerificationService, ReverificationService from openedx.core.djangoapps.credit.services import CreditService from openedx.core.djangoapps.util.user_utils import SystemUser @@ -62,6 +62,7 @@ from openedx.core.lib.xblock_utils import ( wrap_xblock, request_token as xblock_request_token, ) +from openedx.core.lib.url_utils import unquote_slashes, quote_slashes from student.models import anonymous_id_for_user, user_by_anonymous_id from student.roles import CourseBetaTesterRole from util import milestones_helpers diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index 60877c2e7d..af9b604013 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -7,21 +7,19 @@ Contains: for testing Xmodules with mongo store. """ -from django.test.utils import override_settings from django.core.urlresolvers import reverse from django.test.client import Client from edxmako.shortcuts import render_to_string +from lms.djangoapps.lms_xblock.field_data import LmsFieldData +from openedx.core.lib.url_utils import quote_slashes from student.tests.factories import UserFactory, CourseEnrollmentFactory -from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE from xblock.field_data import DictFieldData +from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE from xmodule.tests import get_test_system, get_test_descriptor_system -from opaque_keys.edx.locations import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from lms.djangoapps.lms_xblock.field_data import LmsFieldData -from lms.djangoapps.lms_xblock.runtime import quote_slashes class BaseTestXmodule(ModuleStoreTestCase): diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py index 2f89385659..cdafc0397f 100644 --- a/lms/djangoapps/courseware/tests/test_lti_integration.py +++ b/lms/djangoapps/courseware/tests/test_lti_integration.py @@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse from courseware.tests import BaseTestXmodule from courseware.views.views import get_course_lti_endpoints -from lms.djangoapps.lms_xblock.runtime import quote_slashes +from openedx.core.lib.url_utils import quote_slashes from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.x_module import STUDENT_VIEW diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 92fafe8a5f..57dbc46aa9 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -36,10 +36,10 @@ from courseware.models import StudentModule from courseware.tests.factories import StudentModuleFactory, UserFactory, GlobalStaffFactory from courseware.tests.tests import LoginEnrollmentTestCase from courseware.tests.test_submitting_problems import TestSubmittingProblems -from lms.djangoapps.lms_xblock.runtime import quote_slashes from lms.djangoapps.lms_xblock.field_data import LmsFieldData from openedx.core.lib.courses import course_image_url from openedx.core.lib.gating import api as gating_api +from openedx.core.lib.url_utils import quote_slashes from student.models import anonymous_id_for_user from xmodule.modulestore.tests.django_utils import ( ModuleStoreTestCase, diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index f2580fb674..0d53e247b4 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -19,21 +19,21 @@ from capa.tests.response_xml_factory import ( OptionResponseXMLFactory, CustomResponseXMLFactory, SchematicResponseXMLFactory, CodeResponseXMLFactory, ) -from lms.djangoapps.grades import course_grades, progress from course_modes.models import CourseMode from courseware.models import StudentModule, BaseStudentModuleHistory from courseware.tests.helpers import LoginEnrollmentTestCase -from lms.djangoapps.lms_xblock.runtime import quote_slashes -from student.models import anonymous_id_for_user, CourseEnrollment -from submissions import api as submissions_api -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from xmodule.partitions.partitions import Group, UserPartition +from lms.djangoapps.grades import course_grades, progress from openedx.core.djangoapps.credit.api import ( set_credit_requirements, get_credit_requirement_status ) from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory +from openedx.core.lib.url_utils import quote_slashes +from student.models import anonymous_id_for_user, CourseEnrollment +from submissions import api as submissions_api +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.partitions.partitions import Group, UserPartition class ProblemSubmissionTestMixin(TestCase): diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index f5cec94ab6..6804aba89c 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -22,8 +22,8 @@ from django.conf import settings from util.json_request import JsonResponse from mock import patch -from lms.djangoapps.lms_xblock.runtime import quote_slashes from openedx.core.lib.xblock_utils import wrap_xblock +from openedx.core.lib.url_utils import quote_slashes from xmodule.html_module import HtmlDescriptor from xmodule.modulestore.django import modulestore from xmodule.tabs import CourseTab diff --git a/lms/djangoapps/instructor_task/tests/test_base.py b/lms/djangoapps/instructor_task/tests/test_base.py index be8f26b534..408b2aa80f 100644 --- a/lms/djangoapps/instructor_task/tests/test_base.py +++ b/lms/djangoapps/instructor_task/tests/test_base.py @@ -14,12 +14,12 @@ from celery.states import SUCCESS, FAILURE from django.core.urlresolvers import reverse from django.test.testcases import TestCase from django.contrib.auth.models import User -from lms.djangoapps.lms_xblock.runtime import quote_slashes -from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey from capa.tests.response_xml_factory import OptionResponseXMLFactory from courseware.model_data import StudentModule from courseware.tests.tests import LoginEnrollmentTestCase +from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey +from openedx.core.lib.url_utils import quote_slashes from student.tests.factories import CourseEnrollmentFactory, UserFactory from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index 315aac8fc6..2626f4f0bd 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -38,8 +38,8 @@ from instructor_task.tests.test_base import ( OPTION_2, ) from capa.responsetypes import StudentInputError -from lms.djangoapps.lms_xblock.runtime import quote_slashes from lms.djangoapps.grades.new.course_grade import CourseGradeFactory +from openedx.core.lib.url_utils import quote_slashes log = logging.getLogger(__name__) diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index cfc99c5b08..ec948d2372 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -1,8 +1,6 @@ """ Module implementing `xblock.runtime.Runtime` functionality for the LMS """ -import re - from django.conf import settings from django.core.urlresolvers import reverse @@ -10,6 +8,7 @@ from badges.service import BadgingService from badges.utils import badges_enabled from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api from openedx.core.lib.xblock_utils import xblock_local_resource_url +from openedx.core.lib.url_utils import quote_slashes from request_cache.middleware import RequestCache import xblock.reference.plugins from xmodule.library_tools import LibraryToolsService @@ -21,55 +20,6 @@ from xmodule.x_module import ModuleSystem from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig -def _quote_slashes(match): - """ - Helper function for `quote_slashes` - """ - matched = match.group(0) - # We have to escape ';', because that is our - # escape sequence identifier (otherwise, the escaping) - # couldn't distinguish between us adding ';_' to the string - # and ';_' appearing naturally in the string - if matched == ';': - return ';;' - elif matched == '/': - return ';_' - else: - return matched - - -def quote_slashes(text): - """ - Quote '/' characters so that they aren't visible to - django's url quoting, unquoting, or url regex matching. - - Escapes '/'' to the sequence ';_', and ';' to the sequence - ';;'. By making the escape sequence fixed length, and escaping - identifier character ';', we are able to reverse the escaping. - """ - return re.sub(ur'[;/]', _quote_slashes, text) - - -def _unquote_slashes(match): - """ - Helper function for `unquote_slashes` - """ - matched = match.group(0) - if matched == ';;': - return ';' - elif matched == ';_': - return '/' - else: - return matched - - -def unquote_slashes(text): - """ - Unquote slashes quoted by `quote_slashes` - """ - return re.sub(r'(;;|;_)', _unquote_slashes, text) - - def handler_url(block, handler_name, suffix='', query='', thirdparty=False): """ This method matches the signature for `xblock.runtime:Runtime.handler_url()` diff --git a/lms/djangoapps/lms_xblock/test/test_runtime.py b/lms/djangoapps/lms_xblock/test/test_runtime.py index dc97754d07..0f37ea35ed 100644 --- a/lms/djangoapps/lms_xblock/test/test_runtime.py +++ b/lms/djangoapps/lms_xblock/test/test_runtime.py @@ -10,11 +10,10 @@ from urlparse import urlparse from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locations import BlockUsageLocator, CourseLocator, SlashSeparatedCourseKey -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from badges.tests.factories import BadgeClassFactory from badges.tests.test_models import get_image -from lms.djangoapps.lms_xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem +from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem from xblock.fields import ScopeIds from xmodule.modulestore.django import ModuleI18nService from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -23,32 +22,6 @@ from xblock.exceptions import NoSuchServiceError from student.tests.factories import UserFactory from xmodule.modulestore.tests.factories import CourseFactory -TEST_STRINGS = [ - '', - 'foobar', - 'foo/bar', - 'foo/bar;', - 'foo;;bar', - 'foo;_bar', - 'foo/', - '/bar', - 'foo//bar', - 'foo;;;bar', -] - - -@ddt -class TestQuoteSlashes(TestCase): - """Test the quote_slashes and unquote_slashes functions""" - - @data(*TEST_STRINGS) - def test_inverse(self, test_string): - self.assertEquals(test_string, unquote_slashes(quote_slashes(test_string))) - - @data(*TEST_STRINGS) - def test_escaped(self, test_string): - self.assertNotIn('/', quote_slashes(test_string)) - class BlockMock(Mock): """Mock class that we fill with our "handler" methods.""" diff --git a/lms/djangoapps/lti_provider/views.py b/lms/djangoapps/lti_provider/views.py index c89260ac90..5b948c518c 100644 --- a/lms/djangoapps/lti_provider/views.py +++ b/lms/djangoapps/lti_provider/views.py @@ -11,9 +11,9 @@ from lti_provider.outcomes import store_outcome_parameters from lti_provider.models import LtiConsumer from lti_provider.signature_validator import SignatureValidator from lti_provider.users import authenticate_lti_user -from lms_xblock.runtime import unquote_slashes from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys import InvalidKeyError +from openedx.core.lib.url_utils import unquote_slashes from util.views import add_p3p_header log = logging.getLogger("edx.lti_provider") diff --git a/openedx/core/djangoapps/bookmarks/views.py b/openedx/core/djangoapps/bookmarks/views.py index 31a6578ff6..c81a69ac0b 100644 --- a/openedx/core/djangoapps/bookmarks/views.py +++ b/openedx/core/djangoapps/bookmarks/views.py @@ -27,8 +27,8 @@ from openedx.core.lib.api.permissions import IsUserInUrl from xmodule.modulestore.exceptions import ItemNotFoundError -from lms.djangoapps.lms_xblock.runtime import unquote_slashes from openedx.core.lib.api.paginators import DefaultPagination +from openedx.core.lib.url_utils import unquote_slashes from . import DEFAULT_FIELDS, OPTIONAL_FIELDS, api from .serializers import BookmarkSerializer diff --git a/openedx/core/lib/tests/test_url_utils.py b/openedx/core/lib/tests/test_url_utils.py new file mode 100644 index 0000000000..2fb293d889 --- /dev/null +++ b/openedx/core/lib/tests/test_url_utils.py @@ -0,0 +1,33 @@ +""" +Tests for url_utils module. +""" +from ddt import ddt, data +from django.test import TestCase +from openedx.core.lib.url_utils import quote_slashes, unquote_slashes + + +TEST_STRINGS = [ + '', + 'foobar', + 'foo/bar', + 'foo/bar;', + 'foo;;bar', + 'foo;_bar', + 'foo/', + '/bar', + 'foo//bar', + 'foo;;;bar', +] + + +@ddt +class TestQuoteSlashes(TestCase): + """Test the quote_slashes and unquote_slashes functions""" + + @data(*TEST_STRINGS) + def test_inverse(self, test_string): + self.assertEquals(test_string, unquote_slashes(quote_slashes(test_string))) + + @data(*TEST_STRINGS) + def test_escaped(self, test_string): + self.assertNotIn('/', quote_slashes(test_string)) diff --git a/openedx/core/lib/tests/test_xblock_utils.py b/openedx/core/lib/tests/test_xblock_utils.py index 4a6180f55a..fff35e495c 100644 --- a/openedx/core/lib/tests/test_xblock_utils.py +++ b/openedx/core/lib/tests/test_xblock_utils.py @@ -9,7 +9,7 @@ import uuid from django.test.client import RequestFactory -from lms.djangoapps.lms_xblock.runtime import quote_slashes +from openedx.core.lib.url_utils import quote_slashes from xblock.fragment import Fragment from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase diff --git a/openedx/core/lib/url_utils.py b/openedx/core/lib/url_utils.py new file mode 100644 index 0000000000..4f23ce21ba --- /dev/null +++ b/openedx/core/lib/url_utils.py @@ -0,0 +1,53 @@ +""" +Contains common utilities for URL escaping. +""" +import re + + +def quote_slashes(text): + """ + Quote '/' characters so that they aren't visible to + django's url quoting, unquoting, or url regex matching. + + Escapes '/'' to the sequence ';_', and ';' to the sequence + ';;'. By making the escape sequence fixed length, and escaping + identifier character ';', we are able to reverse the escaping. + """ + return re.sub(ur'[;/]', _quote_slashes, text) + + +def unquote_slashes(text): + """ + Unquote slashes quoted by `quote_slashes` + """ + return re.sub(r'(;;|;_)', _unquote_slashes, text) + + +def _quote_slashes(match): + """ + Helper function for `quote_slashes` + """ + matched = match.group(0) + # We have to escape ';', because that is our + # escape sequence identifier (otherwise, the escaping) + # couldn't distinguish between us adding ';_' to the string + # and ';_' appearing naturally in the string + if matched == ';': + return ';;' + elif matched == '/': + return ';_' + else: + return matched + + +def _unquote_slashes(match): + """ + Helper function for `unquote_slashes` + """ + matched = match.group(0) + if matched == ';;': + return ';' + elif matched == ';_': + return '/' + else: + return matched diff --git a/openedx/tests/xblock_integration/test_crowdsource_hinter.py b/openedx/tests/xblock_integration/test_crowdsource_hinter.py index eaf9a24753..bbb35271f3 100644 --- a/openedx/tests/xblock_integration/test_crowdsource_hinter.py +++ b/openedx/tests/xblock_integration/test_crowdsource_hinter.py @@ -6,16 +6,14 @@ import unittest from nose.plugins.attrib import attr +from django.conf import settings from django.core.urlresolvers import reverse -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory - from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory -from lms.djangoapps.lms_xblock.runtime import quote_slashes - -from django.conf import settings +from openedx.core.lib.url_utils import quote_slashes +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory class TestCrowdsourceHinter(SharedModuleStoreTestCase, LoginEnrollmentTestCase): diff --git a/openedx/tests/xblock_integration/test_recommender.py b/openedx/tests/xblock_integration/test_recommender.py index cda8ca823e..06d3c7b40c 100644 --- a/openedx/tests/xblock_integration/test_recommender.py +++ b/openedx/tests/xblock_integration/test_recommender.py @@ -20,7 +20,7 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory -from lms.djangoapps.lms_xblock.runtime import quote_slashes +from openedx.core.lib.url_utils import quote_slashes class TestRecommender(SharedModuleStoreTestCase, LoginEnrollmentTestCase):