jk-mil-ims-lti 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+
2
+ module IMS::LTI
3
+ module Extensions
4
+
5
+ # Base functionality for creating LTI extension modules
6
+ # See the test for this class for a simple example of how to create an extension module
7
+ module Base
8
+ def outcome_request_extensions
9
+ []
10
+ end
11
+
12
+ def outcome_response_extensions
13
+ []
14
+ end
15
+
16
+ def extend_outcome_request(request)
17
+ outcome_request_extensions.each do |ext|
18
+ request.extend(ext)
19
+ end
20
+ request
21
+ end
22
+
23
+ def extend_outcome_response(response)
24
+ outcome_response_extensions.each do |ext|
25
+ response.extend(ext)
26
+ end
27
+ response
28
+ end
29
+ end
30
+
31
+ module ExtensionBase
32
+ def outcome_request_extensions
33
+ super
34
+ end
35
+
36
+ def outcome_response_extensions
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ require 'ims/lti/extensions/outcome_data'
44
+ require 'ims/lti/extensions/content'
45
+ require 'ims/lti/extensions/canvas'
@@ -0,0 +1,159 @@
1
+ module IMS::LTI
2
+ # Mixin module for managing LTI Launch Data
3
+ #
4
+ # Launch data documentation:
5
+ # http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649684
6
+ module LaunchParams
7
+
8
+ # List of the standard launch parameters for an LTI launch
9
+ LAUNCH_DATA_PARAMETERS = %w{
10
+ context_id
11
+ context_label
12
+ context_title
13
+ context_type
14
+ launch_presentation_css_url
15
+ launch_presentation_document_target
16
+ launch_presentation_height
17
+ launch_presentation_locale
18
+ launch_presentation_return_url
19
+ launch_presentation_width
20
+ lis_course_offering_sourcedid
21
+ lis_course_section_sourcedid
22
+ lis_outcome_service_url
23
+ lis_person_contact_email_primary
24
+ lis_person_name_family
25
+ lis_person_name_full
26
+ lis_person_name_given
27
+ lis_person_sourcedid
28
+ lis_result_sourcedid
29
+ lti_message_type
30
+ lti_version
31
+ oauth_callback
32
+ oauth_consumer_key
33
+ oauth_nonce
34
+ oauth_signature
35
+ oauth_signature_method
36
+ oauth_timestamp
37
+ oauth_version
38
+ resource_link_description
39
+ resource_link_id
40
+ resource_link_title
41
+ roles
42
+ tool_consumer_info_product_family_code
43
+ tool_consumer_info_version
44
+ tool_consumer_instance_contact_email
45
+ tool_consumer_instance_description
46
+ tool_consumer_instance_guid
47
+ tool_consumer_instance_name
48
+ tool_consumer_instance_url
49
+ user_id
50
+ user_image
51
+ }
52
+
53
+ LAUNCH_DATA_PARAMETERS.each { |p| attr_accessor p }
54
+
55
+ # Hash of custom parameters, the keys will be prepended with "custom_" at launch
56
+ attr_accessor :custom_params
57
+
58
+ # Hash of extension parameters, the keys will be prepended with "ext_" at launch
59
+ attr_accessor :ext_params
60
+
61
+ # Hash of parameters to add to the launch. These keys will not be prepended
62
+ # with any value at launch
63
+ attr_accessor :non_spec_params
64
+
65
+ # Set the roles for the current launch
66
+ #
67
+ # Full list of roles can be found here:
68
+ # http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649700
69
+ #
70
+ # LIS roles include:
71
+ # * Student
72
+ # * Faculty
73
+ # * Member
74
+ # * Learner
75
+ # * Instructor
76
+ # * Mentor
77
+ # * Staff
78
+ # * Alumni
79
+ # * ProspectiveStudent
80
+ # * Guest
81
+ # * Other
82
+ # * Administrator
83
+ # * Observer
84
+ # * None
85
+ #
86
+ # @param roles_list [String,Array] An Array or comma-separated String of roles
87
+ def roles=(roles_list)
88
+ if roles_list
89
+ if roles_list.is_a?(Array)
90
+ @roles = roles_list
91
+ else
92
+ @roles = roles_list.split(",").map(&:downcase)
93
+ end
94
+ else
95
+ @roles = nil
96
+ end
97
+ end
98
+
99
+ def set_custom_param(key, val)
100
+ @custom_params[key] = val
101
+ end
102
+
103
+ def get_custom_param(key)
104
+ @custom_params[key]
105
+ end
106
+
107
+ def set_non_spec_param(key, val)
108
+ @non_spec_params[key] = val
109
+ end
110
+
111
+ def get_non_spec_param(key)
112
+ @non_spec_params[key]
113
+ end
114
+
115
+ def set_ext_param(key, val)
116
+ @ext_params[key] = val
117
+ end
118
+
119
+ def get_ext_param(key)
120
+ @ext_params[key]
121
+ end
122
+
123
+ # Create a new Hash with all launch data. Custom/Extension keys will have the
124
+ # appropriate value prepended to the keys and the roles are set as a comma
125
+ # separated String
126
+ def to_params
127
+ params = launch_data_hash.merge(add_key_prefix(@custom_params, 'custom')).merge(add_key_prefix(@ext_params, 'ext')).merge(@non_spec_params)
128
+ params["roles"] = @roles.join(",") if @roles
129
+ params
130
+ end
131
+
132
+ # Populates the launch data from a Hash
133
+ #
134
+ # Only keys in LAUNCH_DATA_PARAMETERS and that start with 'custom_' or 'ext_'
135
+ # will be pulled from the provided Hash
136
+ def process_params(params)
137
+ params.each_pair do |key, val|
138
+ if LAUNCH_DATA_PARAMETERS.member?(key)
139
+ self.send("#{key}=", val)
140
+ elsif key =~ /custom_(.*)/
141
+ @custom_params[$1] = val
142
+ elsif key =~ /ext_(.*)/
143
+ @ext_params[$1] = val
144
+ end
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def launch_data_hash
151
+ LAUNCH_DATA_PARAMETERS.inject({}) { |h, k| h[k] = self.send(k) if self.send(k); h }
152
+ end
153
+
154
+ def add_key_prefix(hash, prefix)
155
+ hash.keys.inject({}) { |h, k| h["#{prefix}_#{k}"] = hash[k]; h }
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,216 @@
1
+ module IMS::LTI
2
+ # Class for consuming/generating LTI Outcome Requests
3
+ #
4
+ # Outcome Request documentation: http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691
5
+ #
6
+ # This class can be used by both Tool Providers and Tool Consumers. Each will
7
+ # use it a bit differently. The Tool Provider will use it to POST an OAuth-signed
8
+ # request to a TC. A Tool Consumer will use it to parse such a request from a TP.
9
+ #
10
+ # === Tool Provider Usage
11
+ # An OutcomeRequest will generally be created through a configured ToolProvider
12
+ # object. See the ToolProvider documentation.
13
+ #
14
+ # === Tool Consumer Usage
15
+ # When an outcome request is sent from a TP the body of the request is XML.
16
+ # This class parses that XML and provides a simple interface for accessing the
17
+ # information in the request. Typical usage would be:
18
+ #
19
+ # # create an OutcomeRequest from the request object
20
+ # req = IMS::LTI::OutcomeRequest.from_post_request(request)
21
+ #
22
+ # # access the source id to identify the user who's grade you'd like to access
23
+ # req.lis_result_sourcedid
24
+ #
25
+ # # process the request
26
+ # if req.replace_request?
27
+ # # set a new score for the user
28
+ # elsif req.read_request?
29
+ # # return the score for the user
30
+ # elsif req.delete_request?
31
+ # # clear the score for the user
32
+ # else
33
+ # # return an unsupported OutcomeResponse
34
+ # end
35
+ class OutcomeRequest
36
+ include IMS::LTI::Extensions::Base
37
+
38
+ REPLACE_REQUEST = 'replaceResult'
39
+ DELETE_REQUEST = 'deleteResult'
40
+ READ_REQUEST = 'readResult'
41
+
42
+ attr_accessor :operation, :score, :outcome_response, :message_identifier,
43
+ :lis_outcome_service_url, :lis_result_sourcedid,
44
+ :consumer_key, :consumer_secret, :post_request
45
+
46
+ # Create a new OutcomeRequest
47
+ #
48
+ # @param opts [Hash] initialization hash
49
+ def initialize(opts={})
50
+ opts.each_pair do |key, val|
51
+ self.send("#{key}=", val) if self.respond_to?("#{key}=")
52
+ end
53
+ end
54
+
55
+ # Convenience method for creating a new OutcomeRequest from a request object
56
+ #
57
+ # req = IMS::LTI::OutcomeRequest.from_post_request(request)
58
+ def self.from_post_request(post_request)
59
+ request = OutcomeRequest.new
60
+ request.process_post_request(post_request)
61
+ end
62
+
63
+ def process_post_request(post_request)
64
+ self.post_request = post_request
65
+ if post_request.body.respond_to?(:read)
66
+ xml = post_request.body.read
67
+ post_request.body.rewind
68
+ else
69
+ xml = post_request.body
70
+ end
71
+ self.process_xml(xml)
72
+ self
73
+ end
74
+
75
+ # POSTs the given score to the Tool Consumer with a replaceResult
76
+ #
77
+ # @return [OutcomeResponse] The response from the Tool Consumer
78
+ def post_replace_result!(score)
79
+ @operation = REPLACE_REQUEST
80
+ @score = score
81
+ post_outcome_request
82
+ end
83
+
84
+ # POSTs a deleteResult to the Tool Consumer
85
+ #
86
+ # @return [OutcomeResponse] The response from the Tool Consumer
87
+ def post_delete_result!
88
+ @operation = DELETE_REQUEST
89
+ post_outcome_request
90
+ end
91
+
92
+ # POSTs a readResult to the Tool Consumer
93
+ #
94
+ # @return [OutcomeResponse] The response from the Tool Consumer
95
+ def post_read_result!
96
+ @operation = READ_REQUEST
97
+ post_outcome_request
98
+ end
99
+
100
+ # Check whether this request is a replaceResult request
101
+ def replace_request?
102
+ @operation == REPLACE_REQUEST
103
+ end
104
+
105
+ # Check whether this request is a deleteResult request
106
+ def delete_request?
107
+ @operation == DELETE_REQUEST
108
+ end
109
+
110
+ # Check whether this request is a readResult request
111
+ def read_request?
112
+ @operation == READ_REQUEST
113
+ end
114
+
115
+ # Check whether the last outcome POST was successful
116
+ def outcome_post_successful?
117
+ @outcome_response && @outcome_response.success?
118
+ end
119
+
120
+ # POST an OAuth signed request to the Tool Consumer
121
+ #
122
+ # @return [OutcomeResponse] The response from the Tool Consumer
123
+ def post_outcome_request
124
+ raise IMS::LTI::InvalidLTIConfigError, "" unless has_required_attributes?
125
+
126
+ consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret)
127
+ token = OAuth::AccessToken.new(consumer)
128
+ res = token.post(
129
+ @lis_outcome_service_url,
130
+ generate_request_xml,
131
+ 'Content-Type' => 'application/xml'
132
+ )
133
+
134
+ puts "jake @lis_outcome_service_url #{@lis_outcome_service_url}"
135
+ puts "jake res.inspect #{res.inspect}"
136
+
137
+ @outcome_response = extend_outcome_response(OutcomeResponse.new)
138
+ @outcome_response.process_post_response(res)
139
+ end
140
+
141
+ # Parse Outcome Request data from XML
142
+ def process_xml(xml)
143
+ doc = REXML::Document.new xml
144
+ @message_identifier = doc.text("//imsx_POXRequestHeaderInfo/imsx_messageIdentifier")
145
+ @lis_result_sourcedid = doc.text("//resultRecord/sourcedGUID/sourcedId")
146
+
147
+ if REXML::XPath.first(doc, "//deleteResultRequest")
148
+ @operation = DELETE_REQUEST
149
+ elsif REXML::XPath.first(doc, "//readResultRequest")
150
+ @operation = READ_REQUEST
151
+ elsif REXML::XPath.first(doc, "//replaceResultRequest")
152
+ @operation = REPLACE_REQUEST
153
+ @score = doc.get_text("//resultRecord/result/resultScore/textString")
154
+ end
155
+
156
+ puts "jake doc #{doc}"
157
+
158
+ extention_process_xml(doc)
159
+ end
160
+
161
+ private
162
+
163
+ def extention_process_xml(doc)
164
+ end
165
+
166
+ def has_result_data?
167
+ !!@score
168
+ end
169
+
170
+ def results(node)
171
+ return unless has_result_data?
172
+
173
+ node.result do |res|
174
+ result_values(res)
175
+ end
176
+ end
177
+
178
+ def result_values(node)
179
+ if @score
180
+ node.resultScore do |res_score|
181
+ res_score.language "en" # 'en' represents the format of the number
182
+ res_score.textString @score.to_s
183
+ end
184
+ end
185
+ end
186
+
187
+ def has_required_attributes?
188
+ @consumer_key && @consumer_secret && @lis_outcome_service_url && @lis_result_sourcedid && @operation
189
+ end
190
+
191
+ def generate_request_xml
192
+ builder = Builder::XmlMarkup.new #(:indent=>2)
193
+ builder.instruct!
194
+
195
+ builder.imsx_POXEnvelopeRequest("xmlns" => "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0") do |env|
196
+ env.imsx_POXHeader do |header|
197
+ header.imsx_POXRequestHeaderInfo do |info|
198
+ info.imsx_version "V1.0"
199
+ info.imsx_messageIdentifier @message_identifier || IMS::LTI::generate_identifier
200
+ end
201
+ end
202
+ env.imsx_POXBody do |body|
203
+ body.tag!(@operation + 'Request') do |request|
204
+ request.resultRecord do |record|
205
+ record.sourcedGUID do |guid|
206
+ guid.sourcedId @lis_result_sourcedid
207
+ end
208
+ results(record)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ end
216
+ end
@@ -0,0 +1,167 @@
1
+ module IMS::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 IMS::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
+ puts "jake post_response #{post_response}"
76
+
77
+ self.post_response = post_response
78
+ self.response_code = post_response.code
79
+ xml = post_response.body
80
+ self.process_xml(xml)
81
+ self
82
+ end
83
+
84
+ def success?
85
+ @code_major == 'success'
86
+ end
87
+
88
+ def processing?
89
+ @code_major == 'processing'
90
+ end
91
+
92
+ def failure?
93
+ @code_major == 'failure'
94
+ end
95
+
96
+ def unsupported?
97
+ @code_major == 'unsupported'
98
+ end
99
+
100
+ def has_warning?
101
+ @severity == 'warning'
102
+ end
103
+
104
+ def has_error?
105
+ @severity == 'error'
106
+ end
107
+
108
+ # Parse Outcome Response data from XML
109
+ def process_xml(xml)
110
+ puts "jake response xml #{xml}"
111
+
112
+ doc = REXML::Document.new xml
113
+ @message_identifier = doc.text('//imsx_statusInfo/imsx_messageIdentifier').to_s
114
+ @code_major = doc.text('//imsx_statusInfo/imsx_codeMajor')
115
+ @code_major.downcase! if @code_major
116
+ @severity = doc.text('//imsx_statusInfo/imsx_severity')
117
+ @severity.downcase! if @severity
118
+ @description = doc.text('//imsx_statusInfo/imsx_description')
119
+ @description = @description.to_s if @description
120
+ @message_ref_identifier = doc.text('//imsx_statusInfo/imsx_messageRefIdentifier')
121
+ @operation = doc.text('//imsx_statusInfo/imsx_operationRefIdentifier')
122
+ @score = doc.text('//readResultResponse//resultScore/textString')
123
+
124
+ @score = @score.to_s if @score
125
+ rescue
126
+ raise IMS::LTI::InvalidLTIResponseData, ''
127
+ end
128
+
129
+ # Generate XML based on the current configuration
130
+ # @return [String] The response xml
131
+ def generate_response_xml
132
+ builder = Builder::XmlMarkup.new
133
+ builder.instruct!
134
+
135
+ builder.imsx_POXEnvelopeResponse("xmlns" => "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0") do |env|
136
+ env.imsx_POXHeader do |header|
137
+ header.imsx_POXResponseHeaderInfo do |info|
138
+ info.imsx_version "V1.0"
139
+ info.imsx_messageIdentifier @message_identifier || IMS::LTI::generate_identifier
140
+ info.imsx_statusInfo do |status|
141
+ status.imsx_codeMajor @code_major
142
+ status.imsx_severity @severity
143
+ status.imsx_description @description
144
+ status.imsx_messageRefIdentifier @message_ref_identifier
145
+ status.imsx_operationRefIdentifier @operation
146
+ end
147
+ end
148
+ end #/header
149
+ env.imsx_POXBody do |body|
150
+ unless unsupported?
151
+ body.tag!(@operation + 'Response') do |request|
152
+ if @operation == OutcomeRequest::READ_REQUEST
153
+ request.result do |res|
154
+ res.resultScore do |res_score|
155
+ res_score.language "en" # 'en' represents the format of the number
156
+ res_score.textString @score.to_s
157
+ end
158
+ end #/result
159
+ end
160
+ end #/operationResponse
161
+ end
162
+ end #/body
163
+ end
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,50 @@
1
+ module IMS::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