lti2 0.0.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.
@@ -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