lti2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,75 @@
1
+ require 'uri'
2
+ require 'oauth'
3
+ require_relative 'oauth_request'
4
+
5
+ module Lti2Commons
6
+ module Signer
7
+ # Creates an OAuth signed request using the OAuth Gem - https://github.com/oauth/oauth-ruby
8
+ #
9
+ # @param launch_url [String] Endpoint of service to be launched
10
+ # @param http_method [String] Http method ('get', 'post', 'put', 'delete')
11
+ # @param consumer_key [String] OAuth consumer key
12
+ # @param consumer_secret [String] OAuth consumer secret
13
+ # @param params [Hash] Non-auth parameters or oauth parameter default values
14
+ # oauth_timestamp => defaults to current time
15
+ # oauth_nonce => defaults to random number
16
+ # oauth_signature_method => defaults to HMAC-SHA1 (also RSA-SHA1 supported)
17
+ # @param body [String] Body content. Usually would include this for body-signing of non form-encoded data.
18
+ # @param content_type [String] HTTP CONTENT-TYPE header; defaults: 'application/x-www-form-urlencoded'
19
+ # @return [Request] Signed request
20
+ def create_signed_request(launch_url, http_method, consumer_key, consumer_secret, params = {},
21
+ body = nil, content_type = nil, accept = nil)
22
+ params['oauth_consumer_key'] = consumer_key
23
+ params['oauth_nonce'] = (rand * 10E12).to_i.to_s unless params.key? 'oauth_nonce'
24
+ params['oauth_signature_method'] = 'HMAC-SHA1' unless params.key? 'oauth_signature_method'
25
+ params['oauth_timestamp'] = Time.now.to_i.to_s unless params.key? 'oauth_timestamp'
26
+ params['oauth_version'] = '1.0' unless params.key? 'oauth_version'
27
+ params['oauth_callback'] = 'about:blank'
28
+
29
+ content_type = 'application/x-www-form-urlencoded' unless content_type
30
+
31
+ launch_url = URI.unescape(launch_url)
32
+ uri = URI.parse(launch_url)
33
+
34
+ # flatten in query string arrays
35
+ if uri.query && uri.query != ''
36
+ CGI.parse(uri.query).each do |query_key, query_values|
37
+ params[query_key] = query_values.first unless params[query_key]
38
+ end
39
+ end
40
+
41
+ final_uri = uri
42
+ uri = URI.parse(launch_url.split('?').first)
43
+
44
+ unless content_type == 'application/x-www-form-urlencoded'
45
+ params['oauth_body_hash'] = compute_oauth_body_hash body if body
46
+ end
47
+
48
+ request = OAuth::OAuthProxy::OAuthRequest.new(
49
+ 'method' => http_method.to_s.upcase,
50
+ 'uri' => uri,
51
+ 'parameters' => params,
52
+ 'final_uri' => final_uri
53
+ )
54
+
55
+ request.body = body
56
+ request.content_type = content_type
57
+ request.accept = accept
58
+
59
+ request.sign!(consumer_secret: consumer_secret)
60
+
61
+ # puts "Sender secret: #{consumer_secret}"
62
+ request
63
+ end
64
+
65
+ private
66
+
67
+ # Creates the value of an OAuth body hash
68
+ #
69
+ # @param launch_url [String] Content to be body signed
70
+ # @return [String] Signature base string (useful for debugging signature problems)
71
+ def compute_oauth_body_hash(content)
72
+ Base64.encode64(Digest::SHA1.digest(content.chomp)).gsub(/\n/, '')
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,87 @@
1
+ module Lti2Commons
2
+ module SubstitutionSupport
3
+ # Resolver resolves values by name from a variety of object sources.
4
+ # It's useful for variable substitution.
5
+ #
6
+ # The supported object sources are:
7
+ # Hash ::= value by key
8
+ # Proc ::= single-argument block evaluation
9
+ # Method ::= single-argument method obect evaluation
10
+ # Resolver ::= a nested resolver. useful for scoping resolvers;
11
+ # i.e. a constant, global inner resolver, but a one-time outer-resolver
12
+ # Object ::= value by dynamic instrospection of any object's accessor
13
+ #
14
+ # See the accompanying tester for numerous examples
15
+ class Resolver
16
+ attr_accessor :resolvers
17
+
18
+ def initialize
19
+ @resolver_hash = Hash.new
20
+ end
21
+
22
+ # Add some type of resolver object_source to the Resolver
23
+ #
24
+ # @param key [String] A dotted name with the leading zone indicating the object category
25
+ # @param resolver [ObjectSource] a raw object_source for resolving
26
+ # @returns [String] value. If no resolution, return the incoming name
27
+ #
28
+ def add_resolver(key, resolver)
29
+ if resolver.is_a? Resolver
30
+ # Resolvers themselves should always be generic
31
+ key_sym = :*
32
+ else
33
+ key_sym = key.to_sym
34
+ end
35
+ @resolver_hash[key_sym] = [] unless @resolver_hash.key? key_sym
36
+ @resolver_hash[key_sym] << resolver
37
+ end
38
+
39
+ def resolve(full_name)
40
+ full_name ||= ''
41
+ zones = full_name.split('.')
42
+ return full_name if zones[0].blank?
43
+ category = zones[0].to_sym
44
+ name = zones[1..-1].join('.')
45
+
46
+ # Find any hits within category
47
+ @resolver_hash.each_pair do |k, v|
48
+ if k == category
49
+ result = resolve_by_category(full_name, name, v)
50
+ return result if result
51
+ end
52
+ end
53
+
54
+ # Find any hits in global category
55
+ resolvers = @resolver_hash[:*]
56
+ result = resolve_by_category full_name, name, resolvers if resolvers
57
+ return result if result
58
+
59
+ "#{full_name}"
60
+ end
61
+
62
+ def to_s
63
+ "Resolver for [#{@resolver_hash.keys}]"
64
+ end
65
+
66
+ private
67
+
68
+ def resolve_by_category(full_name, name, resolvers)
69
+ resolvers.each do |resolver|
70
+ if resolver.is_a? Hash
71
+ value = resolver[name]
72
+ elsif resolver.is_a? Proc
73
+ value = resolver.call(name)
74
+ elsif resolver.is_a? Method
75
+ value = resolver.call(name)
76
+ elsif resolver.is_a? Resolver
77
+ value = resolver.resolve(full_name)
78
+ elsif resolver.is_a? Object
79
+ value = resolver.send(name)
80
+ end
81
+ return value if value
82
+ end
83
+ nil
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,28 @@
1
+ require 'digest'
2
+ require 'uuid'
3
+
4
+ module Lti2Commons
5
+ module Utils
6
+ def is_hash_intersect(node, constraint_hash)
7
+ # failed search if constraint_hash as invalid keys
8
+ return nil if (constraint_hash.keys - node.keys).length > 0
9
+ node.each_pair do |k, v|
10
+ if constraint_hash.key?(k)
11
+ return false unless v == constraint_hash[k]
12
+ end
13
+ end
14
+ true
15
+ end
16
+
17
+ def hash_to_query_string(hash)
18
+ hash.keys.inject('') do |query_string, key|
19
+ query_string << '&' unless key == hash.keys.first
20
+ query_string << "#{URI.encode(key.to_s)}=#{CGI.escape(hash[key])}"
21
+ end
22
+ end
23
+
24
+ def substitute_template_values_from_hash(source_string, prefix, suffix, hash)
25
+ hash.each { |k, v| source_string.sub!(prefix + k + suffix, v) }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Lti2Commons
2
+ VERSION = '0.0.8'
3
+ end
@@ -0,0 +1,163 @@
1
+ require 'stringio'
2
+
3
+ module Lti2Commons
4
+ module WireLogSupport
5
+ class WireLog
6
+ STATUS_CODES = {
7
+ 100 => 'Continue',
8
+ 101 => 'Switching Protocols',
9
+ 102 => 'Processing',
10
+
11
+ 200 => 'OK',
12
+ 201 => 'Created',
13
+ 202 => 'Accepted',
14
+ 203 => 'Non-Authoritative Information',
15
+ 204 => 'No Content',
16
+ 205 => 'Reset Content',
17
+ 206 => 'Partial Content',
18
+ 207 => 'Multi-Status',
19
+ 226 => 'IM Used',
20
+
21
+ 300 => 'Multiple Choices',
22
+ 301 => 'Moved Permanently',
23
+ 302 => 'Found',
24
+ 303 => 'See Other',
25
+ 304 => 'Not Modified',
26
+ 305 => 'Use Proxy',
27
+ 307 => 'Temporary Redirect',
28
+
29
+ 400 => 'Bad Request',
30
+ 401 => 'Unauthorized',
31
+ 402 => 'Payment Required',
32
+ 403 => 'Forbidden',
33
+ 404 => 'Not Found',
34
+ 405 => 'Method Not Allowed',
35
+ 406 => 'Not Acceptable',
36
+ 407 => 'Proxy Authentication Required',
37
+ 408 => 'Request Timeout',
38
+ 409 => 'Conflict',
39
+ 410 => 'Gone',
40
+ 411 => 'Length Required',
41
+ 412 => 'Precondition Failed',
42
+ 413 => 'Request Entity Too Large',
43
+ 414 => 'Request-URI Too Long',
44
+ 415 => 'Unsupported Media Type',
45
+ 416 => 'Requested Range Not Satisfiable',
46
+ 417 => 'Expectation Failed',
47
+ 422 => 'Unprocessable Entity',
48
+ 423 => 'Locked',
49
+ 424 => 'Failed Dependency',
50
+ 426 => 'Upgrade Required',
51
+
52
+ 500 => 'Internal Server Error',
53
+ 501 => 'Not Implemented',
54
+ 502 => 'Bad Gateway',
55
+ 503 => 'Service Unavailable',
56
+ 504 => 'Gateway Timeout',
57
+ 505 => 'HTTP Version Not Supported',
58
+ 507 => 'Insufficient Storage',
59
+ 510 => 'Not Extended'
60
+ }
61
+
62
+ attr_accessor :is_logging, :output_file_name
63
+
64
+ def initialize(wire_log_name, output_file, is_html_output = true)
65
+ @output_file_name = output_file
66
+ @is_logging = true
67
+ @wire_log_name = wire_log_name
68
+ @log_buffer = nil
69
+ @is_html_output = is_html_output
70
+ end
71
+
72
+ def clear_log
73
+ output_file = File.open(@output_file_name, 'a+')
74
+ output_file.truncate(0)
75
+ output_file.close
76
+ end
77
+
78
+ def flush(options = {})
79
+ output_file = File.open(@output_file_name, 'a+')
80
+ @log_buffer.rewind
81
+ buffer = @log_buffer.read
82
+ if @is_html_output
83
+ oldbuffer = buffer.dup
84
+ buffer = ''
85
+ oldbuffer.each_char do |c|
86
+ if c == '<'
87
+ buffer << '&lt;'
88
+ elsif c == '>'
89
+ buffer << '&gt;'
90
+ else
91
+ buffer << c
92
+ end
93
+ end
94
+ end
95
+ if options.key? :css_class
96
+ css_class = options[:css_class]
97
+ else
98
+ css_class = "#{@wire_log_name}"
99
+ end
100
+ output_file.puts("<div class=\"#{css_class}\"><pre>") if @is_html_output
101
+ output_file.write(buffer)
102
+ output_file.puts("\n</div></pre>") if @is_html_output
103
+ output_file.close
104
+ @log_buffer = nil
105
+ end
106
+
107
+ def log(s)
108
+ timestamp
109
+ raw_log("#{s}")
110
+ flush
111
+ end
112
+
113
+ def log_response(response, title = nil)
114
+ timestamp
115
+ raw_log(title.nil? ? 'Response' : "Response: #{title}")
116
+ raw_log("Status: #{response.code} #{STATUS_CODES[response.code]}")
117
+ headers = response.headers
118
+ unless headers.blank?
119
+ raw_log('Headers:')
120
+ headers.each { |k, v| raw_log("#{k}: #{v}") if k.downcase =~ /^content/ }
121
+ end
122
+
123
+ if response.body
124
+ # the following is expensive so do only when needed
125
+ raw_log('Body:') if @is_logging
126
+ begin
127
+ json_obj = JSON.load(response.body)
128
+ raw_log(JSON.pretty_generate(json_obj))
129
+ rescue
130
+ raw_log("#{response.body}")
131
+ end
132
+ end
133
+ newline
134
+ flush(css_class: "#{@wire_log_name}Response")
135
+ end
136
+
137
+ def newline
138
+ raw_log("\n")
139
+ end
140
+
141
+ def log_buffer
142
+ # put in the css header if file doesn't exist
143
+ unless File.size? @output_file_name
144
+ @output_file = File.open(@output_file_name, 'a')
145
+ @output_file.puts '<link rel="stylesheet" type="text/css" href="wirelog.css" />'
146
+ @output_file.puts ''
147
+ @output_file.close
148
+ end
149
+ @log_buffer = StringIO.new unless @log_buffer
150
+ @log_buffer
151
+ end
152
+
153
+ def raw_log(s)
154
+ @log_buffer = log_buffer
155
+ @log_buffer.puts(s)
156
+ end
157
+
158
+ def timestamp
159
+ raw_log(Time.new)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,255 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'json'
4
+ require 'jsonpath'
5
+
6
+ class TestTree < Test::Unit::TestCase
7
+ def setup
8
+ @json_str =
9
+ <<PROXY
10
+ {
11
+ "@context" : [
12
+ "http://www.imsglobal.org/imspurl/lti/v2/ctx/ToolProxy",
13
+ "http://purl.org/blackboard/ctx/v1/iconStyle"
14
+ ],
15
+ "@type" : "ToolProxy",
16
+ "@id" : "http://fabericious..com/ToolProxy/869e5ce5-214c-4e85-86c6-b99e8458a592",
17
+ "lti_version" : "LTI-2p0",
18
+ "tool_proxy_guid" : "869e5ce5-214c-4e85-86c6-b99e8458a592",
19
+ "tool_consumer_profile" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4",
20
+ "tool_profile" : {
21
+ "lti_version" : "LTI-2p0",
22
+ "product_instance" : {
23
+ "guid" : "fd75124a-140e-470f-944c-114d2d92bb40",
24
+ "product_info" : {
25
+ "product_name" : {
26
+ "default_value" : "Acme Assessments",
27
+ "key" : "tool.name"
28
+ },
29
+ "description" : {
30
+ "default_value" : "Acme Assessments provide an interactive test format.",
31
+ "key" : "tool.description"
32
+ },
33
+ "product_version" : "10.3",
34
+ "technical_description" : {
35
+ "default_value" : "Support provided for all LTI 1 extensions as well as LTI 2",
36
+ "key" : "tool.technical"
37
+ },
38
+ "product_family" : {
39
+ "code" : "assessment-tool",
40
+ "vendor" : {
41
+ "code" : "acme.com",
42
+ "name" : {
43
+ "default_value" : "Acme",
44
+ "key" : "tool.vendor.name"
45
+ },
46
+ "description" : {
47
+ "default_value" : "Acme is a leading provider of interactive tools for education",
48
+ "key" : "tool.vendor.description"
49
+ },
50
+ "website" : "http://acme.example.com",
51
+ "timestamp" : "2012-04-05T09:08:16-04:00",
52
+ "contact" : {
53
+ "email" : "info@example.com"
54
+ }
55
+ }
56
+ }
57
+ },
58
+ "support" : {
59
+ "email" : "helpdesk@example.com"
60
+ },
61
+ "service_provider" : {
62
+ "guid" : "18e7ea50-3d6d-4f6b-aff2-ed3ab577716c",
63
+ "provider_name" : {
64
+ "default_value" : "Acme Hosting",
65
+ "key" : "service_provider.name"
66
+ },
67
+ "description" : {
68
+ "default_value" : "Provider of high performance managed hosting environments",
69
+ "key" : "service_provider.description"
70
+ },
71
+ "support" : {
72
+ "email" : "support@acme-hosting.example.com"
73
+ },
74
+ "timestamp" : "2012-04-05T09:08:16-04:00"
75
+ }
76
+ },
77
+ "base_url_choice" : [
78
+ { "default_base_url" : "http://acme.example.com",
79
+ "secure_base_url" : "https://acme.example.com",
80
+ "selector" : {
81
+ "applies_to" : [
82
+ "IconEndpoint",
83
+ "MessageHandler"
84
+ ]
85
+ }
86
+ }
87
+ ],
88
+ "resource_handler" : [
89
+ {
90
+ "name" : {
91
+ "default_value" : "Acme Assessment",
92
+ "key" : "assessment.resource.name"
93
+ },
94
+ "description" : {
95
+ "default_value" : "An interactive assessment using the Acme scale.",
96
+ "key" : "assessment.resource.description"
97
+ },
98
+ "message" : {
99
+ "message_type" : "basic-lti-launch-request",
100
+ "path" : "/handler/launchRequest",
101
+ "capability" : [
102
+ "Result.autocreate",
103
+ "Result.sourcedGUID"
104
+ ],
105
+ "parameter" : [
106
+ { "name" : "result_id",
107
+ "variable" : "$Result.sourcedGUID"
108
+ },
109
+ { "name" : "discipline",
110
+ "fixed" : "chemistry"
111
+ }
112
+ ]
113
+ },
114
+ "icon_info" : [
115
+ {
116
+ "default_location" : {
117
+ "path" : "/images/bb/en/icon.png"
118
+ },
119
+ "key" : "iconStyle.default.path"
120
+ },
121
+ { "icon_style" : "BbListElementIcon",
122
+ "default_location" : {
123
+ "path" : "/images/bb/en/listElement.png"
124
+ },
125
+ "key" : "iconStyle.bb.listElement.path"
126
+ },
127
+ { "icon_style" : "BbPushButtonIcon",
128
+ "default_location" : {
129
+ "path" : "images/bb/en/pushButton.png"
130
+ },
131
+ "key" : "iconStyle.bb.pushButton.path"
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ },
137
+ "security_contract" : {
138
+ "shared_secret" : "ThisIsASecret!",
139
+ "tool_service" : [
140
+ { "@type" : "RestServiceProfile",
141
+ "service" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#ToolProxy.collection",
142
+ "action" : "POST"
143
+ },
144
+ { "@type" : "RestServiceProfile",
145
+ "service" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#ToolProxy.item",
146
+ "action" : [
147
+ "GET",
148
+ "PUT"
149
+ ]
150
+ },
151
+ { "@type" : "RestService",
152
+ "service" : "http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#Result.item",
153
+ "action" : [
154
+ "GET",
155
+ "PUT"
156
+ ]
157
+ }
158
+ ]
159
+ }
160
+ }
161
+ PROXY
162
+
163
+ @json_obj = JSON.parse(@json_str)
164
+ end
165
+
166
+ # asserts if expected_value is provided, else prints result
167
+ # useful for preparing new tests
168
+ def assert_node(jsonpath, expected_value=nil)
169
+ try_result = JsonPath.new(jsonpath).on(@json_obj)
170
+ if expected_value
171
+ assert_equal expected_value, try_result
172
+ else
173
+ puts "#{jsonpath}: #{try_result.inspect}"
174
+ end
175
+ end
176
+
177
+ # asserts if singleton expected_value is provided, else prints result
178
+ # useful for preparing new tests
179
+ def assert_first(jsonpath, expected_value=nil)
180
+ try_result = JsonPath.new(jsonpath).on(@json_obj).first
181
+ if expected_value
182
+ assert_equal expected_value, try_result
183
+ else
184
+ puts "#{jsonpath}: #{try_result.inspect}"
185
+ end
186
+ end
187
+
188
+ def test_path_on_json
189
+ # Note that same result for JSON string or loaded JSON object
190
+ assert_equal ["869e5ce5-214c-4e85-86c6-b99e8458a592"], JsonPath.new('tool_proxy_guid').on(@json_str)
191
+ assert_equal ["869e5ce5-214c-4e85-86c6-b99e8458a592"], JsonPath.new('tool_proxy_guid').on(@json_obj)
192
+ # assert_node 'tool_proxy_guid'
193
+ assert_node 'tool_proxy_guid', ["869e5ce5-214c-4e85-86c6-b99e8458a592"]
194
+ end
195
+
196
+ def test_first
197
+ assert_equal "869e5ce5-214c-4e85-86c6-b99e8458a592", JsonPath.new('tool_proxy_guid').on(@json_str).first
198
+ end
199
+
200
+ def test_basics
201
+ assert_node 'tool_proxy_guid', ["869e5ce5-214c-4e85-86c6-b99e8458a592"]
202
+ assert_node 'security_contract.shared_secret', ["ThisIsASecret!"]
203
+ # 2 dots goes through array(s)
204
+ assert_node 'tool_profile.base_url_choice..default_base_url', ["http://acme.example.com"]
205
+ assert_node 'lti_version', ["LTI-2p0"]
206
+ end
207
+
208
+ def test_arrays
209
+ assert_node "tool_profile.resource_handler[0].message.path", ["/handler/launchRequest"]
210
+ assert_node 'tool_profile.resource_handler[0].message.message_type', ["basic-lti-launch-request"]
211
+ end
212
+
213
+ def test_filter0
214
+ # resource_handler = JsonPath.new('tool_profile.resource_handler').on(@json_obj)
215
+ # assert_node 'tool_profile.resource_handler[?(true)]'
216
+ # assert_node 'tool_profile.resource_handler.[?(@["message"]["message_type"]=="basic-lti-launch-request")]'
217
+ #find matching message_type and dig down to get message.path
218
+ assert_equal "/handler/launchRequest",
219
+ JsonPath.new('tool_profile.resource_handler.[?(@["message"]["message_type"]=="basic-lti-launch-request")]').on(@json_obj).first['message']['path']
220
+
221
+ # do the same replying JsonPath
222
+ assert_equal "/handler/launchRequest", JsonPath.new('@..message..path').on(
223
+ JsonPath.new('tool_profile.resource_handler.[?(@["message"]["message_type"]=="basic-lti-launch-request")]').on(@json_obj)).first
224
+
225
+ end
226
+
227
+ def test_filter
228
+ assert_first 'security_contract.tool_service[0].action', "POST"
229
+ assert_first 'security_contract.tool_service[?(@["action"]=="POST")]',
230
+ {"service"=>"http://lms.example.com/profile/b6ffa601-ce1d-4549-9ccf-145670a964d4#ToolProxy.collection", "action"=>"POST", "@type"=>"RestServiceProfile"}
231
+ # assert_first 'security_contract.tool_service[?(@["action"]=="POST")]'
232
+ assert_equal "POST", JsonPath.new('security_contract.tool_service[?(@["action"]=="POST")]').on(@json_obj).first['action']
233
+ end
234
+
235
+ def test_enumerate
236
+ enum = JsonPath.new("$..*")[@json_obj]
237
+ counter = 0
238
+ enum.each {|node| counter += 1}
239
+ assert_equal 114, counter
240
+ end
241
+
242
+ def test_enumerate
243
+ root = JsonPath.new("$").on(@json_str).first
244
+ assert_equal ["http://acme.example.com"], JsonPath.new('tool_profile.base_url_choice..default_base_url').on(root)
245
+ end
246
+
247
+ def test_print
248
+ root = JsonPath.new("$").on(@json_str).first
249
+ # puts root.to_json
250
+ puts JSON.pretty_generate root
251
+ end
252
+
253
+ ARGV = ['', "--name", "test_print"]
254
+ Test::Unit::AutoRunner.run(false, nil, ARGV)
255
+ end