Files
edx-platform/common/lib/xmodule/xmodule/lti_module.py
Alexander Kryklia 1d03f4aab2 Revert "reformat for readability"
This reverts commit acd23ad933.
2013-09-18 13:40:22 +03:00

262 lines
9.8 KiB
Python

"""
Module that allows to insert LTI tools to page.
Protocol is oauth1, LTI version is 1.1.1:
http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html
"""
import logging
import oauthlib.oauth1
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
log = logging.getLogger(__name__)
class LTIError(Exception):
pass
class LTIFields(object):
"""
Fields to define and obtain LTI tool from provider are set here,
except credentials, which should be set in course settings::
`lti_id` is id to connect tool with credentials in course settings.
`launch_url` is launch url of tool.
`custom_parameters` are additional parameters to navigate to proper book and book page.
For example, for Vitalsource provider, `launch_url` should be
*https://bc-staging.vitalsource.com/books/book*,
and to get to proper book and book page, you should set custom parameters as::
vbid=put_book_id_here
book_location=page/put_page_number_here
Default non-empty url for `launch_url` is needed due to oauthlib demand (url scheme should be presented)::
https://github.com/idan/oauthlib/blob/master/oauthlib/oauth1/rfc5849/signature.py#L136
"""
lti_id = String(help="Id of the tool", default='', scope=Scope.settings)
launch_url = String(help="URL of the tool", default='http://www.example.com', scope=Scope.settings)
custom_parameters = List(help="Custom parameters (vbid, book_location, etc..)", scope=Scope.settings)
class LTIModule(LTIFields, XModule):
'''
Module provides LTI integration to course.
Except usual xmodule structure it proceeds with oauth signing.
How it works::
1. Get credentials from course settings.
2. There is minimal set of parameters need to be signed (presented for Vitalsource)::
user_id
oauth_callback
lis_outcome_service_url
lis_result_sourcedid
launch_presentation_return_url
lti_message_type
lti_version
role
*+ all custom parameters*
These parameters should be encoded and signed by *oauth1* together with
`launch_url` and *POST* request type.
3. Signing proceeds with client key/secret pair obtained from course settings.
That pair should be obtained from LTI provider and set into course settings by course author.
After that signature and other oauth data are generated.
Oauth data which is generated after signing is usual::
oauth_callback
oauth_nonce
oauth_consumer_key
oauth_signature_method
oauth_timestamp
oauth_version
4. All that data is passed to form and sent to LTI provider server by browser via
autosubmit via javascript.
Form example::
<form
action="${launch_url}"
name="ltiLaunchForm"
class="ltiLaunchForm"
method="post"
target="ltiLaunchFrame"
encType="application/x-www-form-urlencoded"
>
<input name="launch_presentation_return_url" value="" />
<input name="lis_outcome_service_url" value="" />
<input name="lis_result_sourcedid" value="" />
<input name="lti_message_type" value="basic-lti-launch-request" />
<input name="lti_version" value="LTI-1p0" />
<input name="oauth_callback" value="about:blank" />
<input name="oauth_consumer_key" value="${oauth_consumer_key}" />
<input name="oauth_nonce" value="${oauth_nonce}" />
<input name="oauth_signature_method" value="HMAC-SHA1" />
<input name="oauth_timestamp" value="${oauth_timestamp}" />
<input name="oauth_version" value="1.0" />
<input name="user_id" value="${user_id}" />
<input name="role" value="student" />
<input name="oauth_signature" value="${oauth_signature}" />
<input name="custom_1" value="${custom_param_1_value}" />
<input name="custom_2" value="${custom_param_2_value}" />
<input name="custom_..." value="${custom_param_..._value}" />
<input type="submit" value="Press to Launch" />
</form>
5. LTI provider has same secret key and it signs data string via *oauth1* and compares signatures.
If signatures are correct, LTI provider redirects iframe source to LTI tool web page,
and LTI tool is rendered to iframe inside course.
Otherwise error message from LTI provider is generated.
'''
js = {'js': [resource_string(__name__, 'js/src/lti/lti.js')]}
css = {'scss': [resource_string(__name__, 'css/lti/lti.scss')]}
js_module_name = "LTI"
def get_html(self):
"""
Renders parameters to template.
"""
# Obtains client_key and client_secret credentials from current course:
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.lti_passports:
try:
lti_id, key, secret = lti_passport.split(':')
except ValueError:
raise LTIError('Could not parse LTI passport: {0!r}. \
Should be "id:key:secret" string.'.format(lti_passport))
if lti_id == self.lti_id:
client_key, client_secret = key, secret
break
# parsing custom parameters to dict
custom_parameters = {}
for custom_parameter in self.custom_parameters:
try:
param_name, param_value = custom_parameter.split('=', 1)
except ValueError:
raise LTIError('Could not parse custom parameter: {0!r}. \
Should be "x=y" string.'.format(custom_parameter))
# LTI specs: 'custom_' should be prepended before each custom parameter
custom_parameters[u'custom_' + unicode(param_name)] = unicode(param_value)
input_fields = self.oauth_params(
custom_parameters,
client_key,
client_secret
)
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.
`custom_paramters` is dict of parsed `custom_parameter` field
`client_key` and `client_secret` are LTI tool credentials.
Also *anonymous student id* is passed to template and therefore to LTI provider.
"""
client = oauthlib.oauth1.Client(
client_key=unicode(client_key),
client_secret=unicode(client_secret)
)
user_id = self.runtime.anonymous_student_id
assert user_id is not None
# must have parameters for correct signing from LTI:
body = {
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
body.update(custom_parameters)
headers = {
# This is needed for body encoding:
'Content-Type': 'application/x-www-form-urlencoded',
}
try:
__, headers, __ = client.sign(
unicode(self.launch_url),
http_method=u'POST',
body=body,
headers=headers)
except ValueError: # scheme not in url
#https://github.com/idan/oauthlib/blob/master/oauthlib/oauth1/rfc5849/signature.py#L136
#Stubbing headers for now:
headers = {
u'Content-Type': u'application/x-www-form-urlencoded',
u'Authorization': u'OAuth oauth_nonce="80966668944732164491378916897", \
oauth_timestamp="1378916897", oauth_version="1.0", oauth_signature_method="HMAC-SHA1", \
oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'}
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[u'oauth_nonce'] = params[u'OAuth oauth_nonce']
del params[u'OAuth oauth_nonce']
# oauthlib encodes signature with
# 'Content-Type': 'application/x-www-form-urlencoded'
# so '='' becomes '%3D'.
# We send form via browser, so browser will encode it again,
# 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
class LTIModuleDescriptor(LTIFields, MetadataOnlyEditingDescriptor):
"""
LTIModuleDescriptor provides no export/import to xml.
"""
module_class = LTIModule