aj-ims-lti 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Changelog +34 -0
- data/LICENSE +18 -0
- data/README.md +117 -0
- data/lib/ajims.rb +1 -0
- data/lib/ajims/lti.rb +51 -0
- data/lib/ajims/lti/exceptions.rb +15 -0
- data/lib/ajims/lti/extensions.rb +45 -0
- data/lib/ajims/lti/extensions/canvas.rb +122 -0
- data/lib/ajims/lti/extensions/content.rb +209 -0
- data/lib/ajims/lti/extensions/outcome_data.rb +185 -0
- data/lib/ajims/lti/launch_params.rb +159 -0
- data/lib/ajims/lti/outcome_request.rb +227 -0
- data/lib/ajims/lti/outcome_response.rb +165 -0
- data/lib/ajims/lti/request_validator.rb +50 -0
- data/lib/ajims/lti/tool_config.rb +225 -0
- data/lib/ajims/lti/tool_consumer.rb +95 -0
- data/lib/ajims/lti/tool_provider.rb +197 -0
- metadata +117 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module AJIMS::LTI
|
|
2
|
+
# Class for consuming/generating LTI Outcome Responses
|
|
3
|
+
#
|
|
4
|
+
# Response documentation: http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691
|
|
5
|
+
#
|
|
6
|
+
# Error code documentation: http://www.imsglobal.org/gws/gwsv1p0/imsgws_baseProfv1p0.html#1639667
|
|
7
|
+
#
|
|
8
|
+
# This class can be used by both Tool Providers and Tool Consumers. Each will
|
|
9
|
+
# use it a bit differently. The Tool Provider will use it parse the result of
|
|
10
|
+
# an OutcomeRequest to the Tool Consumer. A Tool Consumer will use it generate
|
|
11
|
+
# proper response XML to send back to a Tool Provider
|
|
12
|
+
#
|
|
13
|
+
# === Tool Provider Usage
|
|
14
|
+
# An OutcomeResponse will generally be created when POSTing an OutcomeRequest
|
|
15
|
+
# through a configured ToolProvider. See the ToolProvider documentation for
|
|
16
|
+
# typical usage.
|
|
17
|
+
#
|
|
18
|
+
# === Tool Consumer Usage
|
|
19
|
+
# When an outcome request is sent from a Tool Provider the body of the request
|
|
20
|
+
# is XML. This class parses that XML and provides a simple interface for
|
|
21
|
+
# accessing the information in the request. Typical usage would be:
|
|
22
|
+
#
|
|
23
|
+
# # create a new response and set the appropriate values
|
|
24
|
+
# res = IMS::LTI::OutcomeResponse.new
|
|
25
|
+
# res.message_ref_identifier = outcome_request.message_identifier
|
|
26
|
+
# res.operation = outcome_request.operation
|
|
27
|
+
# res.code_major = 'success'
|
|
28
|
+
# res.severity = 'status'
|
|
29
|
+
#
|
|
30
|
+
# # set a description (optional) and other information based on the type of response
|
|
31
|
+
# if outcome_request.replace_request?
|
|
32
|
+
# res.description = "Your old score of 0 has been replaced with #{outcome_request.score}"
|
|
33
|
+
# elsif outcome_request.read_request?
|
|
34
|
+
# res.description = "You score is 50"
|
|
35
|
+
# res.score = 50
|
|
36
|
+
# elsif outcome_request.delete_request?
|
|
37
|
+
# res.description = "You score has been cleared"
|
|
38
|
+
# else
|
|
39
|
+
# res.code_major = 'unsupported'
|
|
40
|
+
# res.severity = 'status'
|
|
41
|
+
# res.description = "#{outcome_request.operation} is not supported"
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# # the generated xml is returned to the Tool Provider
|
|
45
|
+
# res.generate_response_xml
|
|
46
|
+
#
|
|
47
|
+
class OutcomeResponse
|
|
48
|
+
include AJIMS::LTI::Extensions::Base
|
|
49
|
+
|
|
50
|
+
attr_accessor :request_type, :score, :message_identifier, :response_code,
|
|
51
|
+
:post_response, :code_major, :severity, :description, :operation,
|
|
52
|
+
:message_ref_identifier
|
|
53
|
+
|
|
54
|
+
CODE_MAJOR_CODES = %w{success processing failure unsupported}
|
|
55
|
+
SEVERITY_CODES = %w{status warning error}
|
|
56
|
+
|
|
57
|
+
# Create a new OutcomeResponse
|
|
58
|
+
#
|
|
59
|
+
# @param opts [Hash] initialization hash
|
|
60
|
+
def initialize(opts={})
|
|
61
|
+
opts.each_pair do |key, val|
|
|
62
|
+
self.send("#{key}=", val) if self.respond_to?("#{key}=")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Convenience method for creating a new OutcomeResponse from a response object
|
|
67
|
+
#
|
|
68
|
+
# req = IMS::LTI::OutcomeResponse.from_post_response(response)
|
|
69
|
+
def self.from_post_response(post_response)
|
|
70
|
+
response = OutcomeResponse.new
|
|
71
|
+
response.process_post_response(post_response)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_post_response(post_response)
|
|
75
|
+
self.post_response = post_response
|
|
76
|
+
self.response_code = post_response.code
|
|
77
|
+
xml = post_response.body
|
|
78
|
+
self.process_xml(xml)
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def success?
|
|
83
|
+
@code_major == 'success'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def processing?
|
|
87
|
+
@code_major == 'processing'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def failure?
|
|
91
|
+
@code_major == 'failure'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unsupported?
|
|
95
|
+
@code_major == 'unsupported'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def has_warning?
|
|
99
|
+
@severity == 'warning'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def has_error?
|
|
103
|
+
@severity == 'error'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Parse Outcome Response data from XML
|
|
107
|
+
def process_xml(xml)
|
|
108
|
+
doc = REXML::Document.new xml
|
|
109
|
+
@message_identifier = doc.text("//imsx_statusInfo/imsx_messageIdentifier").to_s
|
|
110
|
+
@code_major = doc.text("//imsx_statusInfo/imsx_codeMajor")
|
|
111
|
+
@code_major.downcase! if @code_major
|
|
112
|
+
@severity = doc.text("//imsx_statusInfo/imsx_severity")
|
|
113
|
+
@severity.downcase! if @severity
|
|
114
|
+
@description = doc.text("//imsx_statusInfo/imsx_description")
|
|
115
|
+
@description = @description.to_s if @description
|
|
116
|
+
@message_ref_identifier = doc.text("//imsx_statusInfo/imsx_messageRefIdentifier")
|
|
117
|
+
@operation = doc.text("//imsx_statusInfo/imsx_operationRefIdentifier")
|
|
118
|
+
@score = doc.text("//readResultResponse//resultScore/textString")
|
|
119
|
+
@score = @score.to_s if @score
|
|
120
|
+
rescue REXML::ParseException
|
|
121
|
+
raise AJIMS::LTI::XmlParseException.new(
|
|
122
|
+
xml,
|
|
123
|
+
"Failed to parse XML response from canvas"
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Generate XML based on the current configuration
|
|
128
|
+
# @return [String] The response xml
|
|
129
|
+
def generate_response_xml
|
|
130
|
+
builder = Builder::XmlMarkup.new
|
|
131
|
+
builder.instruct!
|
|
132
|
+
|
|
133
|
+
builder.imsx_POXEnvelopeResponse("xmlns" => "http://www.imsglobal.org/lis/oms1p0/pox") do |env|
|
|
134
|
+
env.imsx_POXHeader do |header|
|
|
135
|
+
header.imsx_POXResponseHeaderInfo do |info|
|
|
136
|
+
info.imsx_version "V1.0"
|
|
137
|
+
info.imsx_messageIdentifier @message_identifier || AJIMS::LTI::generate_identifier
|
|
138
|
+
info.imsx_statusInfo do |status|
|
|
139
|
+
status.imsx_codeMajor @code_major
|
|
140
|
+
status.imsx_severity @severity
|
|
141
|
+
status.imsx_description @description
|
|
142
|
+
status.imsx_messageRefIdentifier @message_ref_identifier
|
|
143
|
+
status.imsx_operationRefIdentifier @operation
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end #/header
|
|
147
|
+
env.imsx_POXBody do |body|
|
|
148
|
+
unless unsupported?
|
|
149
|
+
body.tag!(@operation + 'Response') do |request|
|
|
150
|
+
if @operation == OutcomeRequest::READ_REQUEST
|
|
151
|
+
request.result do |res|
|
|
152
|
+
res.resultScore do |res_score|
|
|
153
|
+
res_score.language "en" # 'en' represents the format of the number
|
|
154
|
+
res_score.textString @score.to_s
|
|
155
|
+
end
|
|
156
|
+
end #/result
|
|
157
|
+
end
|
|
158
|
+
end #/operationResponse
|
|
159
|
+
end
|
|
160
|
+
end #/body
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module AJIMS::LTI
|
|
2
|
+
# A mixin for OAuth request validation
|
|
3
|
+
module RequestValidator
|
|
4
|
+
|
|
5
|
+
attr_reader :oauth_signature_validator
|
|
6
|
+
|
|
7
|
+
# Validates and OAuth request using the OAuth Gem - https://github.com/oauth/oauth-ruby
|
|
8
|
+
#
|
|
9
|
+
# To validate the OAuth signatures you need to require the appropriate
|
|
10
|
+
# request proxy for your application. For example:
|
|
11
|
+
#
|
|
12
|
+
# # For a sinatra app:
|
|
13
|
+
# require 'oauth/request_proxy/rack_request'
|
|
14
|
+
#
|
|
15
|
+
# # For a rails app:
|
|
16
|
+
# require 'oauth/request_proxy/action_controller_request'
|
|
17
|
+
# @return [Bool] Whether the request was valid
|
|
18
|
+
def valid_request?(request, handle_error=true)
|
|
19
|
+
begin
|
|
20
|
+
@oauth_signature_validator = OAuth::Signature.build(request, :consumer_secret => @consumer_secret)
|
|
21
|
+
@oauth_signature_validator.verify() or raise OAuth::Unauthorized
|
|
22
|
+
true
|
|
23
|
+
rescue OAuth::Signature::UnknownSignatureMethod, OAuth::Unauthorized
|
|
24
|
+
if handle_error
|
|
25
|
+
false
|
|
26
|
+
else
|
|
27
|
+
raise $!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Check whether the OAuth-signed request is valid and throw error if not
|
|
33
|
+
#
|
|
34
|
+
# @return [Bool] Whether the request was valid
|
|
35
|
+
def valid_request!(request)
|
|
36
|
+
valid_request?(request, false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# convenience method for getting the oauth nonce from the request
|
|
40
|
+
def request_oauth_nonce
|
|
41
|
+
@oauth_signature_validator && @oauth_signature_validator.request.oauth_nonce
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# convenience method for getting the oauth timestamp from the request
|
|
45
|
+
def request_oauth_timestamp
|
|
46
|
+
@oauth_signature_validator && @oauth_signature_validator.request.oauth_timestamp
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
module AJIMS::LTI
|
|
2
|
+
# Class used to represent an LTI configuration
|
|
3
|
+
#
|
|
4
|
+
# It can create and read the Common Cartridge XML representation of LTI links
|
|
5
|
+
# as described here: http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649689
|
|
6
|
+
#
|
|
7
|
+
# == Usage
|
|
8
|
+
# To generate an XML configuration:
|
|
9
|
+
#
|
|
10
|
+
# # Create a config object and set some options
|
|
11
|
+
# tc = IMS::LTI::ToolConfig.new(:title => "Example Sinatra Tool Provider", :launch_url => url)
|
|
12
|
+
# tc.description = "This example LTI Tool Provider supports LIS Outcome pass-back."
|
|
13
|
+
#
|
|
14
|
+
# # generate the XML
|
|
15
|
+
# tc.to_xml
|
|
16
|
+
#
|
|
17
|
+
# Or to create a config object from an XML String:
|
|
18
|
+
#
|
|
19
|
+
# tc = IMS::LTI::ToolConfig.create_from_xml(xml)
|
|
20
|
+
class ToolConfig
|
|
21
|
+
attr_reader :custom_params, :extensions
|
|
22
|
+
|
|
23
|
+
attr_accessor :title, :description, :launch_url, :secure_launch_url,
|
|
24
|
+
:icon, :secure_icon, :cartridge_bundle, :cartridge_icon,
|
|
25
|
+
:vendor_code, :vendor_name, :vendor_description, :vendor_url,
|
|
26
|
+
:vendor_contact_email, :vendor_contact_name
|
|
27
|
+
|
|
28
|
+
# Create a new ToolConfig with the given options
|
|
29
|
+
#
|
|
30
|
+
# @param opts [Hash] The initial options for the ToolConfig
|
|
31
|
+
def initialize(opts={})
|
|
32
|
+
@custom_params = opts.delete("custom_params") || {}
|
|
33
|
+
@extensions = opts.delete("extensions") || {}
|
|
34
|
+
|
|
35
|
+
opts.each_pair do |key, val|
|
|
36
|
+
self.send("#{key}=", val) if self.respond_to?("#{key}=")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Create a ToolConfig from the given XML
|
|
41
|
+
#
|
|
42
|
+
# @param xml [String]
|
|
43
|
+
def self.create_from_xml(xml)
|
|
44
|
+
tc = ToolConfig.new
|
|
45
|
+
tc.process_xml(xml)
|
|
46
|
+
|
|
47
|
+
tc
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_custom_param(key, val)
|
|
51
|
+
@custom_params[key] = val
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get_custom_param(key)
|
|
55
|
+
@custom_params[key]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Set the extension parameters for a specific vendor
|
|
59
|
+
#
|
|
60
|
+
# @param ext_key [String] The identifier for the vendor-specific parameters
|
|
61
|
+
# @param ext_params [Hash] The parameters, this is allowed to be two-levels deep
|
|
62
|
+
def set_ext_params(ext_key, ext_params)
|
|
63
|
+
raise ArgumentError unless ext_params.is_a?(Hash)
|
|
64
|
+
@extensions[ext_key] = ext_params
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_ext_params(ext_key)
|
|
68
|
+
@extensions[ext_key]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def set_ext_param(ext_key, param_key, val)
|
|
72
|
+
@extensions[ext_key] ||= {}
|
|
73
|
+
@extensions[ext_key][param_key] = val
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def get_ext_param(ext_key, param_key)
|
|
77
|
+
@extensions[ext_key] && @extensions[ext_key][param_key]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Namespaces used for parsing configuration XML
|
|
81
|
+
LTI_NAMESPACES = {
|
|
82
|
+
"xmlns" => 'http://www.imsglobal.org/xsd/imslticc_v1p0',
|
|
83
|
+
"blti" => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0',
|
|
84
|
+
"lticm" => 'http://www.imsglobal.org/xsd/imslticm_v1p0',
|
|
85
|
+
"lticp" => 'http://www.imsglobal.org/xsd/imslticp_v1p0',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Parse tool configuration data out of the Common Cartridge LTI link XML
|
|
89
|
+
def process_xml(xml)
|
|
90
|
+
doc = REXML::Document.new xml
|
|
91
|
+
if root = REXML::XPath.first(doc, 'xmlns:cartridge_basiclti_link')
|
|
92
|
+
@title = get_node_text(root, 'blti:title')
|
|
93
|
+
@description = get_node_text(root, 'blti:description')
|
|
94
|
+
@launch_url = get_node_text(root, 'blti:launch_url')
|
|
95
|
+
@secure_launch_url = get_node_text(root, 'blti:secure_launch_url')
|
|
96
|
+
@icon = get_node_text(root, 'blti:icon')
|
|
97
|
+
@secure_icon = get_node_text(root, 'blti:secure_icon')
|
|
98
|
+
@cartridge_bundle = get_node_att(root, 'xmlns:cartridge_bundle', 'identifierref')
|
|
99
|
+
@cartridge_icon = get_node_att(root, 'xmlns:cartridge_icon', 'identifierref')
|
|
100
|
+
|
|
101
|
+
if vendor = REXML::XPath.first(root, 'blti:vendor')
|
|
102
|
+
@vendor_code = get_node_text(vendor, 'lticp:code')
|
|
103
|
+
@vendor_description = get_node_text(vendor, 'lticp:description')
|
|
104
|
+
@vendor_name = get_node_text(vendor, 'lticp:name')
|
|
105
|
+
@vendor_url = get_node_text(vendor, 'lticp:url')
|
|
106
|
+
@vendor_contact_email = get_node_text(vendor, '//lticp:contact/lticp:email')
|
|
107
|
+
@vendor_contact_name = get_node_text(vendor, '//lticp:contact/lticp:name')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if custom = REXML::XPath.first(root, 'blti:custom', LTI_NAMESPACES)
|
|
111
|
+
set_properties(@custom_params, custom)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
REXML::XPath.each(root, 'blti:extensions', LTI_NAMESPACES) do |vendor_ext_node|
|
|
115
|
+
platform = vendor_ext_node.attributes['platform']
|
|
116
|
+
properties = {}
|
|
117
|
+
set_properties(properties, vendor_ext_node)
|
|
118
|
+
REXML::XPath.each(vendor_ext_node, 'lticm:options', LTI_NAMESPACES) do |options_node|
|
|
119
|
+
opt_name = options_node.attributes['name']
|
|
120
|
+
options = {}
|
|
121
|
+
set_properties(options, options_node)
|
|
122
|
+
properties[opt_name] = options
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
self.set_ext_params(platform, properties)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Generate XML from the current settings
|
|
132
|
+
def to_xml(opts = {})
|
|
133
|
+
raise AJIMS::LTI::InvalidLTIConfigError, "A launch url is required for an LTI configuration." unless self.launch_url || self.secure_launch_url
|
|
134
|
+
|
|
135
|
+
builder = Builder::XmlMarkup.new(:indent => opts[:indent] || 0)
|
|
136
|
+
builder.instruct!
|
|
137
|
+
builder.cartridge_basiclti_link("xmlns" => "http://www.imsglobal.org/xsd/imslticc_v1p0",
|
|
138
|
+
"xmlns:blti" => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0',
|
|
139
|
+
"xmlns:lticm" => 'http://www.imsglobal.org/xsd/imslticm_v1p0',
|
|
140
|
+
"xmlns:lticp" => 'http://www.imsglobal.org/xsd/imslticp_v1p0',
|
|
141
|
+
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
|
142
|
+
"xsi:schemaLocation" => "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd"
|
|
143
|
+
) do |blti_node|
|
|
144
|
+
|
|
145
|
+
%w{title description launch_url secure_launch_url icon secure_icon}.each do |key|
|
|
146
|
+
blti_node.blti key.to_sym, self.send(key) if self.send(key)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
vendor_keys = %w{name code description url}
|
|
150
|
+
if vendor_keys.any?{|k|self.send("vendor_#{k}")} || vendor_contact_email
|
|
151
|
+
blti_node.blti :vendor do |v_node|
|
|
152
|
+
vendor_keys.each do |key|
|
|
153
|
+
v_node.lticp key.to_sym, self.send("vendor_#{key}") if self.send("vendor_#{key}")
|
|
154
|
+
end
|
|
155
|
+
if vendor_contact_email
|
|
156
|
+
v_node.lticp :contact do |c_node|
|
|
157
|
+
c_node.lticp :name, vendor_contact_name
|
|
158
|
+
c_node.lticp :email, vendor_contact_email
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if !@custom_params.empty?
|
|
165
|
+
blti_node.tag!("blti:custom") do |custom_node|
|
|
166
|
+
@custom_params.keys.sort.each do |key|
|
|
167
|
+
val = @custom_params[key]
|
|
168
|
+
custom_node.lticm :property, val, 'name' => key
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if !@extensions.empty?
|
|
174
|
+
@extensions.keys.sort.each do |ext_platform|
|
|
175
|
+
ext_params = @extensions[ext_platform]
|
|
176
|
+
blti_node.blti(:extensions, :platform => ext_platform) do |ext_node|
|
|
177
|
+
ext_params.keys.sort.each do |key|
|
|
178
|
+
val = ext_params[key]
|
|
179
|
+
if val.is_a?(Hash)
|
|
180
|
+
ext_node.lticm(:options, :name => key) do |type_node|
|
|
181
|
+
val.keys.sort.each do |p_key|
|
|
182
|
+
p_val = val[p_key]
|
|
183
|
+
type_node.lticm :property, p_val, 'name' => p_key
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
else
|
|
187
|
+
ext_node.lticm :property, val, 'name' => key
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
blti_node.cartridge_bundle(:identifierref => @cartridge_bundle) if @cartridge_bundle
|
|
195
|
+
blti_node.cartridge_icon(:identifierref => @cartridge_icon) if @cartridge_icon
|
|
196
|
+
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def get_node_text(node, path)
|
|
203
|
+
if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
|
|
204
|
+
val.text
|
|
205
|
+
else
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def get_node_att(node, path, att)
|
|
211
|
+
if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
|
|
212
|
+
val.attributes[att]
|
|
213
|
+
else
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def set_properties(hash, node)
|
|
219
|
+
REXML::XPath.each(node, 'lticm:property', LTI_NAMESPACES) do |prop|
|
|
220
|
+
hash[prop.attributes['name']] = prop.text
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module AJIMS::LTI
|
|
2
|
+
# Class for implementing an LTI Tool Consumer
|
|
3
|
+
class ToolConsumer
|
|
4
|
+
include AJIMS::LTI::Extensions::Base
|
|
5
|
+
include AJIMS::LTI::LaunchParams
|
|
6
|
+
include AJIMS::LTI::RequestValidator
|
|
7
|
+
|
|
8
|
+
attr_accessor :consumer_key, :consumer_secret, :launch_url, :timestamp, :nonce
|
|
9
|
+
|
|
10
|
+
# Create a new ToolConsumer
|
|
11
|
+
#
|
|
12
|
+
# @param consumer_key [String] The OAuth consumer key
|
|
13
|
+
# @param consumer_secret [String] The OAuth consumer secret
|
|
14
|
+
# @param params [Hash] Set the launch parameters as described in LaunchParams
|
|
15
|
+
def initialize(consumer_key, consumer_secret, params={})
|
|
16
|
+
@consumer_key = consumer_key
|
|
17
|
+
@consumer_secret = consumer_secret
|
|
18
|
+
@custom_params = {}
|
|
19
|
+
@ext_params = {}
|
|
20
|
+
@non_spec_params = {}
|
|
21
|
+
@launch_url = params['launch_url']
|
|
22
|
+
process_params(params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def process_post_request(post_request)
|
|
26
|
+
request = extend_outcome_request(OutcomeRequest.new)
|
|
27
|
+
request.process_post_request(post_request)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set launch data from a ToolConfig
|
|
31
|
+
#
|
|
32
|
+
# @param config [ToolConfig]
|
|
33
|
+
def set_config(config)
|
|
34
|
+
@launch_url ||= config.secure_launch_url
|
|
35
|
+
@launch_url ||= config.launch_url
|
|
36
|
+
# any parameters already set will take priority
|
|
37
|
+
@custom_params = config.custom_params.merge(@custom_params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if the required parameters for a tool launch are set
|
|
41
|
+
def has_required_params?
|
|
42
|
+
@consumer_key && @consumer_secret && @resource_link_id && @launch_url
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate the launch data including the necessary OAuth information
|
|
46
|
+
#
|
|
47
|
+
#
|
|
48
|
+
def generate_launch_data
|
|
49
|
+
raise AJIMS::LTI::InvalidLTIConfigError, "Not all required params set for tool launch" unless has_required_params?
|
|
50
|
+
|
|
51
|
+
params = self.to_params
|
|
52
|
+
params['lti_version'] ||= 'LTI-1p0'
|
|
53
|
+
params['lti_message_type'] ||= 'basic-lti-launch-request'
|
|
54
|
+
uri = URI.parse(@launch_url)
|
|
55
|
+
|
|
56
|
+
if uri.port == uri.default_port
|
|
57
|
+
host = uri.host
|
|
58
|
+
else
|
|
59
|
+
host = "#{uri.host}:#{uri.port}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, {
|
|
63
|
+
:site => "#{uri.scheme}://#{host}",
|
|
64
|
+
:signature_method => "HMAC-SHA1"
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
path = uri.path
|
|
68
|
+
path = '/' if path.empty?
|
|
69
|
+
if uri.query && uri.query != ''
|
|
70
|
+
CGI.parse(uri.query).each do |query_key, query_values|
|
|
71
|
+
unless params[query_key]
|
|
72
|
+
params[query_key] = query_values.first
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
options = {
|
|
77
|
+
:scheme => 'body',
|
|
78
|
+
:timestamp => @timestamp,
|
|
79
|
+
:nonce => @nonce
|
|
80
|
+
}
|
|
81
|
+
request = consumer.create_signed_request(:post, path, nil, options, params)
|
|
82
|
+
|
|
83
|
+
# the request is made by a html form in the user's browser, so we
|
|
84
|
+
# want to revert the escapage and return the hash of post parameters ready
|
|
85
|
+
# for embedding in a html view
|
|
86
|
+
hash = {}
|
|
87
|
+
request.body.split(/&/).each do |param|
|
|
88
|
+
key, val = param.split(/=/).map { |v| CGI.unescape(v) }
|
|
89
|
+
hash[key] = val
|
|
90
|
+
end
|
|
91
|
+
hash
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
end
|