mil-ims-lti 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,225 @@
1
+ module IMS::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 IMS::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}.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 IMS::LTI
2
+ # Class for implementing an LTI Tool Consumer
3
+ class ToolConsumer
4
+ include IMS::LTI::Extensions::Base
5
+ include IMS::LTI::LaunchParams
6
+ include IMS::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 IMS::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-1.0'
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
@@ -0,0 +1,197 @@
1
+ module IMS::LTI
2
+
3
+ # Class for implementing an LTI Tool Provider
4
+ #
5
+ # # Initialize TP object with OAuth creds and post parameters
6
+ # provider = IMS::LTI::ToolProvider.new(consumer_key, consumer_secret, params)
7
+ #
8
+ # # Verify OAuth signature by passing the request object
9
+ # if provider.valid_request?(request)
10
+ # # success
11
+ # else
12
+ # # handle invalid OAuth
13
+ # end
14
+ #
15
+ # if provider.outcome_service?
16
+ # # ready for grade write-back
17
+ # else
18
+ # # normal tool launch without grade write-back
19
+ # end
20
+ #
21
+ # If the tool was launch as an outcome service you can POST a score to the TC.
22
+ # The POST calls all return an OutcomeResponse object which can be used to
23
+ # handle the response appropriately.
24
+ #
25
+ # # post the score to the TC, score should be a float >= 0.0 and <= 1.0
26
+ # # this returns an OutcomeResponse object
27
+ # response = provider.post_replace_result!(score)
28
+ # if response.success?
29
+ # # grade write worked
30
+ # elsif response.processing?
31
+ # elsif response.unsupported?
32
+ # else
33
+ # # failed
34
+ # end
35
+
36
+ class ToolProvider
37
+ include IMS::LTI::Extensions::Base
38
+ include IMS::LTI::LaunchParams
39
+ include IMS::LTI::RequestValidator
40
+
41
+ # OAuth credentials
42
+ attr_accessor :consumer_key, :consumer_secret
43
+ # List of outcome requests made through this instance
44
+ attr_accessor :outcome_requests
45
+ # Message to be sent back to the ToolConsumer when the user returns
46
+ attr_accessor :lti_errormsg, :lti_errorlog, :lti_msg, :lti_log
47
+
48
+ # Create a new ToolProvider
49
+ #
50
+ # @param consumer_key [String] The OAuth consumer key
51
+ # @param consumer_secret [String] The OAuth consumer secret
52
+ # @param params [Hash] Set the launch parameters as described in LaunchParams
53
+ def initialize(consumer_key, consumer_secret, params={})
54
+ @consumer_key = consumer_key
55
+ @consumer_secret = consumer_secret
56
+ @custom_params = {}
57
+ @ext_params = {}
58
+ @non_spec_params = {}
59
+ @outcome_requests = []
60
+ process_params(params)
61
+ end
62
+
63
+ # Check whether the Launch Parameters have a role
64
+ def has_role?(role)
65
+ role = role.downcase
66
+ @roles && @roles.any?{|r| r.index(role)}
67
+ end
68
+
69
+ # Convenience method for checking if the user has 'learner' or 'student' role
70
+ def student?
71
+ has_role?('learner') || has_role?('student')
72
+ end
73
+
74
+ # Convenience method for checking if the user has 'instructor' or 'faculty' or 'staff' role
75
+ def instructor?
76
+ has_role?('instructor') || has_role?('faculty') || has_role?('staff')
77
+ end
78
+
79
+ # Convenience method for checking if the user has 'contentdeveloper' role
80
+ def content_developer?
81
+ has_role?('ContentDeveloper')
82
+ end
83
+
84
+ # Convenience method for checking if the user has 'Member' role
85
+ def member?
86
+ has_role?('Member')
87
+ end
88
+
89
+ # Convenience method for checking if the user has 'Manager' role
90
+ def manager?
91
+ has_role?('Manager')
92
+ end
93
+
94
+ # Convenience method for checking if the user has 'Mentor' role
95
+ def mentor?
96
+ has_role?('Mentor')
97
+ end
98
+
99
+ # Convenience method for checking if the user has 'administrator' role
100
+ def admin?
101
+ has_role?('administrator')
102
+ end
103
+
104
+ # Convenience method for checking if the user has 'TeachingAssistant' role
105
+ def ta?
106
+ has_role?('TeachingAssistant')
107
+ end
108
+
109
+ # Check if the request was an LTI Launch Request
110
+ def launch_request?
111
+ lti_message_type == 'basic-lti-launch-request'
112
+ end
113
+
114
+ # Check if the Tool Launch expects an Outcome Result
115
+ def outcome_service?
116
+ !!(lis_outcome_service_url && lis_result_sourcedid)
117
+ end
118
+
119
+ # Return the full, given, or family name if set
120
+ def username(default=nil)
121
+ lis_person_name_given || lis_person_name_family || lis_person_name_full || default
122
+ end
123
+
124
+ # POSTs the given score to the Tool Consumer with a replaceResult
125
+ #
126
+ # Creates a new OutcomeRequest object and stores it in @outcome_requests
127
+ #
128
+ # @return [OutcomeResponse] the response from the Tool Consumer
129
+ def post_replace_result!(score)
130
+ new_request.post_replace_result!(score)
131
+ end
132
+
133
+ # POSTs a delete request to the Tool Consumer
134
+ #
135
+ # Creates a new OutcomeRequest object and stores it in @outcome_requests
136
+ #
137
+ # @return [OutcomeResponse] the response from the Tool Consumer
138
+ def post_delete_result!
139
+ new_request.post_delete_result!
140
+ end
141
+
142
+ # POSTs the given score to the Tool Consumer with a replaceResult, the
143
+ # returned OutcomeResponse will have the score
144
+ #
145
+ # Creates a new OutcomeRequest object and stores it in @outcome_requests
146
+ #
147
+ # @return [OutcomeResponse] the response from the Tool Consumer
148
+ def post_read_result!
149
+ new_request.post_read_result!
150
+ end
151
+
152
+ # Returns the most recent OutcomeRequest
153
+ def last_outcome_request
154
+ @outcome_requests.last
155
+ end
156
+
157
+ # Convenience method for whether the last OutcomeRequest was successful
158
+ def last_outcome_success?
159
+ last_outcome_request && last_outcome_request.outcome_post_successful?
160
+ end
161
+
162
+ # If the Tool Consumer sent a URL for the user to return to this will add
163
+ # any set messages to the URL.
164
+ #
165
+ # Example:
166
+ #
167
+ # tc = IMS::LTI::tc.new
168
+ # tc.launch_presentation_return_url = "http://example.com/return"
169
+ # tc.lti_msg = "hi there"
170
+ # tc.lti_errorlog = "error happens"
171
+ #
172
+ # tc.build_return_url # => "http://example.com/return?lti_msg=hi%20there&lti_errorlog=error%20happens"
173
+ def build_return_url
174
+ return nil unless launch_presentation_return_url
175
+ messages = []
176
+ %w{lti_errormsg lti_errorlog lti_msg lti_log}.each do |m|
177
+ if message = self.send(m)
178
+ messages << "#{m}=#{URI.escape(message)}"
179
+ end
180
+ end
181
+ q_string = messages.any? ? ("?" + messages.join("&")) : ''
182
+ launch_presentation_return_url + q_string
183
+ end
184
+
185
+ private
186
+
187
+ def new_request
188
+ @outcome_requests << OutcomeRequest.new(:consumer_key => @consumer_key,
189
+ :consumer_secret => @consumer_secret,
190
+ :lis_outcome_service_url => lis_outcome_service_url,
191
+ :lis_result_sourcedid =>lis_result_sourcedid)
192
+
193
+ extend_outcome_request(@outcome_requests.last)
194
+ end
195
+
196
+ end
197
+ end