diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index c0c4feb55e..2a529e0ef3 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -14,6 +14,7 @@ import urllib from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.x_module import XModule +from xmodule.course_module import CourseDescriptor from pkg_resources import resource_string from xblock.core import String, Scope, List @@ -106,9 +107,9 @@ class LTIModule(LTIFields, XModule): - % for param_name, param_value in custom_parameters.items(): - - %endfor + + + @@ -129,13 +130,9 @@ class LTIModule(LTIFields, XModule): """ Renders parameters to template. """ # Obtains client_key and client_secret credentials from current course: - # course location example: u'i4x://blades/1/course/2013_Spring' - course = self.descriptor.system.load_item( - self.location.tag + '://' + - self.location.org + '/' + - self.location.course + - '/course' + - '/2013_Spring') + course_id = self.runtime.course_id + course_location = CourseDescriptor.id_to_location(course_id) + course = self.descriptor.runtime.modulestore.get_item(course_location) client_key, client_secret = '', '' for lti_passport in course.LTIs: try: @@ -147,15 +144,8 @@ class LTIModule(LTIFields, XModule): client_key, client_secret = key, secret break - # these params do not participate in oauth signing - params = { - 'launch_url': self.launch_url, - 'element_id': self.location.html_id(), - 'element_class': self.location.category, - } - # parsing custom parameters to dict - parsed_custom_parameters = {} + custom_parameters = {} for custom_parameter in self.custom_parameters: try: param_name, param_value = custom_parameter.split('=') @@ -164,17 +154,26 @@ class LTIModule(LTIFields, XModule): Should be "x=y" string.'.format(custom_parameter)) # LTI specs: 'custom_' should be prepended before each custom parameter - parsed_custom_parameters.update( + custom_parameters.update( {u'custom_' + unicode(param_name): unicode(param_value)} ) - params.update({'custom_parameters': parsed_custom_parameters}) - params.update(self.oauth_params( - parsed_custom_parameters, + input_fields = (self.oauth_params( + custom_parameters, client_key, client_secret )) - return self.system.render_template('lti.html', params) + + context = { + 'input_fields': input_fields, + + # these params do not participate in oauth signing + 'launch_url': self.launch_url, + 'element_id': self.location.html_id(), + 'element_class': self.location.category, + } + + return self.system.render_template('lti.html', context) def oauth_params(self, custom_parameters, client_key, client_secret): """Signs request and returns signature and oauth parameters. @@ -191,20 +190,19 @@ class LTIModule(LTIFields, XModule): client_secret=unicode(client_secret) ) - # @ned - why self.runtime.anonymous_student_id is None in dev env? user_id = self.runtime.anonymous_student_id - user_id = user_id if user_id else 'default_user_id' + assert user_id is not None # must have parameters for correct signing from LTI: body = { - 'user_id': user_id, - 'oauth_callback': 'about:blank', - 'lis_outcome_service_url': '', - 'lis_result_sourcedid': '', - 'launch_presentation_return_url': '', - 'lti_message_type': 'basic-lti-launch-request', - 'lti_version': 'LTI-1p0', - 'role': 'student' + u'user_id': user_id, + u'oauth_callback': u'about:blank', + u'lis_outcome_service_url': '', + u'lis_result_sourcedid': '', + u'launch_presentation_return_url': '', + u'lti_message_type': u'basic-lti-launch-request', + u'lti_version': 'LTI-1p0', + u'role': u'student' } # appending custom parameter for signing @@ -220,13 +218,11 @@ class LTIModule(LTIFields, XModule): headers=headers) params = headers['Authorization'] # parse headers to pass to template as part of context: - params = dict([param.strip().replace('"', '').split('=') for param in params.split('",')]) + params = dict([param.strip().replace('"', '').split('=') for param in params.split(',')]) params[u'oauth_nonce'] = params[u'OAuth oauth_nonce'] del params[u'OAuth oauth_nonce'] - params['user_id'] = body['user_id'] - # 0.14.2 (current) version of requests oauth library encodes signature, # with 'Content-Type': 'application/x-www-form-urlencoded' # so '='' becomes '%3D'. @@ -234,6 +230,8 @@ class LTIModule(LTIFields, XModule): # So we need to decode signature back: params[u'oauth_signature'] = urllib.unquote(params[u'oauth_signature']).decode('utf8') + # add lti parameters to oauth parameters for sending in form + params.update(body) return params diff --git a/lms/djangoapps/courseware/tests/test_lti.py b/lms/djangoapps/courseware/tests/test_lti.py index 86419300ad..cf74ea2660 100644 --- a/lms/djangoapps/courseware/tests/test_lti.py +++ b/lms/djangoapps/courseware/tests/test_lti.py @@ -1,53 +1,69 @@ -"""LTI test""" +"""LTI integration tests""" import requests from . import BaseTestXmodule +from collections import OrderedDict class TestLTI(BaseTestXmodule): - """Integration test for word cloud xmodule.""" + """ + Integration test for lti xmodule. + """ CATEGORY = "lti" def setUp(self): + """ + Mock oauth1 signing of requests library for testing. + """ super(TestLTI, self).setUp() mocked_noonce = u'135685044251684026041377608307' mocked_timestamp = u'1234567890' - mocked_signed_signature = u'my_signature%3D' + mocked_signature_after_sign = u'my_signature%3D' mocked_decoded_signature = u'my_signature=' self.correct_headers = { + u'oauth_callback': u'about:blank', + u'lis_outcome_service_url': '', + u'lis_result_sourcedid': '', + u'launch_presentation_return_url': '', + u'lti_message_type': u'basic-lti-launch-request', + u'lti_version': 'LTI-1p0', + u'oauth_nonce': mocked_noonce, u'oauth_timestamp': mocked_timestamp, u'oauth_consumer_key': u'', u'oauth_signature_method': u'HMAC-SHA1', u'oauth_version': u'1.0', - u'oauth_signature': mocked_decoded_signature} + u'user_id': self.runtime.anonymous_student_id, + u'role': u'student', + u'oauth_signature': mocked_decoded_signature + } saved_sign = requests.auth.Client.sign def mocked_sign(self, *args, **kwargs): """Mocked oauth1 sign function""" - # self is here: + # self is here: _, headers, _ = saved_sign(self, *args, **kwargs) # we should replace noonce, timestamp and signed_signature in headers: old = headers[u'Authorization'] - new = old[:19] + mocked_noonce + old[49:69] + mocked_timestamp + \ - old[79:179] + mocked_signed_signature + old[-1] - headers[u'Authorization'] = new + old_parsed = OrderedDict([param.strip().replace('"', '').split('=') for param in old.split(',')]) + old_parsed[u'OAuth oauth_nonce'] = mocked_noonce + old_parsed[u'oauth_timestamp'] = mocked_timestamp + old_parsed[u'oauth_signature'] = mocked_signature_after_sign + headers[u'Authorization'] = ', '.join([k+'="'+v+'"' for k, v in old_parsed.items()]) return None, headers, None requests.auth.Client.sign = mocked_sign def test_lti_constructor(self): """Make sure that all parameters extracted """ - fragment = self.runtime.render(self.item_module, None, 'student_view') + self.runtime.render_template = lambda template, context: context + generated_context = self.item_module.get_html() expected_context = { + 'input_fields': self.correct_headers, 'element_class': self.item_module.location.category, 'element_id': self.item_module.location.html_id(), - 'lti_url': '', # default value + 'launch_url': '', # default value } - self.correct_headers.update(expected_context) - self.assertEqual( - fragment.content, - self.runtime.render_template('lti.html', self.correct_headers) - ) + self.assertDictEqual(generated_context, expected_context) diff --git a/lms/templates/lti.html b/lms/templates/lti.html index 60bf00d176..3d97c8d808 100644 --- a/lms/templates/lti.html +++ b/lms/templates/lti.html @@ -11,22 +11,8 @@ target="ltiLaunchFrame" encType="application/x-www-form-urlencoded" > - - - - - - - - - - - - - - - % for param_name, param_value in custom_parameters.items(): + % for param_name, param_value in input_fields.items(): %endfor