qalam_ims_lti 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/Changelog.txt +0 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +39 -0
  5. data/lib/ims/lis/context_type/handles.rb +10 -0
  6. data/lib/ims/lis/context_type/urns.rb +10 -0
  7. data/lib/ims/lis/roles/context/handles.rb +60 -0
  8. data/lib/ims/lis/roles/context/urns.rb +60 -0
  9. data/lib/ims/lis/roles/institution/handles.rb +22 -0
  10. data/lib/ims/lis/roles/institution/urns.rb +22 -0
  11. data/lib/ims/lis/roles/system/handles.rb +15 -0
  12. data/lib/ims/lis/roles/system/urns.rb +15 -0
  13. data/lib/ims/lis/statuses/simple_names.rb +9 -0
  14. data/lib/ims/lis/statuses/uris.rb +9 -0
  15. data/lib/ims/lis.rb +14 -0
  16. data/lib/ims/lti/converters/time_json_converter.rb +13 -0
  17. data/lib/ims/lti/converters.rb +5 -0
  18. data/lib/ims/lti/deprecated_role_checks.rb +52 -0
  19. data/lib/ims/lti/errors/authentication_failed_error.rb +11 -0
  20. data/lib/ims/lti/errors/invalid_lti_config_error.rb +4 -0
  21. data/lib/ims/lti/errors/invalid_tool_consumer_profile.rb +4 -0
  22. data/lib/ims/lti/errors/tool_proxy_registration_error.rb +15 -0
  23. data/lib/ims/lti/errors.rb +8 -0
  24. data/lib/ims/lti/extensions/canvas.rb +125 -0
  25. data/lib/ims/lti/extensions/content.rb +209 -0
  26. data/lib/ims/lti/extensions/outcome_data.rb +216 -0
  27. data/lib/ims/lti/extensions.rb +45 -0
  28. data/lib/ims/lti/launch_params.rb +166 -0
  29. data/lib/ims/lti/models/base_url_choice.rb +15 -0
  30. data/lib/ims/lti/models/base_url_selector.rb +5 -0
  31. data/lib/ims/lti/models/contact.rb +5 -0
  32. data/lib/ims/lti/models/content_item_container.rb +14 -0
  33. data/lib/ims/lti/models/content_item_placement.rb +20 -0
  34. data/lib/ims/lti/models/content_items/content_item.rb +32 -0
  35. data/lib/ims/lti/models/content_items/file_item.rb +15 -0
  36. data/lib/ims/lti/models/content_items/lti_link_item.rb +14 -0
  37. data/lib/ims/lti/models/content_items.rb +7 -0
  38. data/lib/ims/lti/models/icon_endpoint.rb +5 -0
  39. data/lib/ims/lti/models/icon_info.rb +6 -0
  40. data/lib/ims/lti/models/image.rb +7 -0
  41. data/lib/ims/lti/models/localized_name.rb +12 -0
  42. data/lib/ims/lti/models/localized_text.rb +12 -0
  43. data/lib/ims/lti/models/lti_model.rb +227 -0
  44. data/lib/ims/lti/models/membership_service/agent.rb +11 -0
  45. data/lib/ims/lti/models/membership_service/container.rb +11 -0
  46. data/lib/ims/lti/models/membership_service/context.rb +11 -0
  47. data/lib/ims/lti/models/membership_service/lis_membership_container.rb +13 -0
  48. data/lib/ims/lti/models/membership_service/lis_person.rb +14 -0
  49. data/lib/ims/lti/models/membership_service/membership.rb +14 -0
  50. data/lib/ims/lti/models/membership_service/organization.rb +14 -0
  51. data/lib/ims/lti/models/membership_service/page.rb +16 -0
  52. data/lib/ims/lti/models/membership_service/person.rb +13 -0
  53. data/lib/ims/lti/models/membership_service.rb +16 -0
  54. data/lib/ims/lti/models/message_handler.rb +14 -0
  55. data/lib/ims/lti/models/messages/basic_lti_launch_request.rb +24 -0
  56. data/lib/ims/lti/models/messages/content_item_selection.rb +32 -0
  57. data/lib/ims/lti/models/messages/content_item_selection_request.rb +26 -0
  58. data/lib/ims/lti/models/messages/message.rb +222 -0
  59. data/lib/ims/lti/models/messages/registration_request.rb +20 -0
  60. data/lib/ims/lti/models/messages/request_message.rb +12 -0
  61. data/lib/ims/lti/models/messages/tool_proxy_update_request.rb +15 -0
  62. data/lib/ims/lti/models/messages.rb +11 -0
  63. data/lib/ims/lti/models/parameter.rb +28 -0
  64. data/lib/ims/lti/models/product_family.rb +8 -0
  65. data/lib/ims/lti/models/product_info.rb +26 -0
  66. data/lib/ims/lti/models/product_instance.rb +10 -0
  67. data/lib/ims/lti/models/resource_handler.rb +22 -0
  68. data/lib/ims/lti/models/resource_type.rb +6 -0
  69. data/lib/ims/lti/models/rest_service.rb +30 -0
  70. data/lib/ims/lti/models/rest_service_profile.rb +15 -0
  71. data/lib/ims/lti/models/security_contract.rb +21 -0
  72. data/lib/ims/lti/models/security_profile.rb +10 -0
  73. data/lib/ims/lti/models/serializable.rb +12 -0
  74. data/lib/ims/lti/models/service_owner.rb +26 -0
  75. data/lib/ims/lti/models/service_provider.rb +11 -0
  76. data/lib/ims/lti/models/tool_consumer_profile.rb +45 -0
  77. data/lib/ims/lti/models/tool_profile.rb +35 -0
  78. data/lib/ims/lti/models/tool_proxy.rb +21 -0
  79. data/lib/ims/lti/models/tool_setting.rb +12 -0
  80. data/lib/ims/lti/models/tool_setting_container.rb +14 -0
  81. data/lib/ims/lti/models/vendor.rb +28 -0
  82. data/lib/ims/lti/models.rb +38 -0
  83. data/lib/ims/lti/outcome_request.rb +225 -0
  84. data/lib/ims/lti/outcome_response.rb +166 -0
  85. data/lib/ims/lti/request_validator.rb +56 -0
  86. data/lib/ims/lti/role_checks.rb +101 -0
  87. data/lib/ims/lti/serializers/base.rb +125 -0
  88. data/lib/ims/lti/serializers/membership_service/agent_serializer.rb +5 -0
  89. data/lib/ims/lti/serializers/membership_service/container_serializer.rb +6 -0
  90. data/lib/ims/lti/serializers/membership_service/context_serializer.rb +9 -0
  91. data/lib/ims/lti/serializers/membership_service/lis_membership_container_serializer.rb +9 -0
  92. data/lib/ims/lti/serializers/membership_service/lis_person_serializer.rb +12 -0
  93. data/lib/ims/lti/serializers/membership_service/membership_serializer.rb +7 -0
  94. data/lib/ims/lti/serializers/membership_service/organization_serializer.rb +8 -0
  95. data/lib/ims/lti/serializers/membership_service/page_serializer.rb +10 -0
  96. data/lib/ims/lti/serializers/membership_service/person_serializer.rb +8 -0
  97. data/lib/ims/lti/serializers/membership_service.rb +13 -0
  98. data/lib/ims/lti/serializers.rb +6 -0
  99. data/lib/ims/lti/services/authentication_service.rb +67 -0
  100. data/lib/ims/lti/services/message_authenticator.rb +80 -0
  101. data/lib/ims/lti/services/oauth2_client.rb +18 -0
  102. data/lib/ims/lti/services/tool_config.rb +223 -0
  103. data/lib/ims/lti/services/tool_consumer_profile_service.rb +16 -0
  104. data/lib/ims/lti/services/tool_proxy_registration_service.rb +84 -0
  105. data/lib/ims/lti/services/tool_proxy_validator.rb +182 -0
  106. data/lib/ims/lti/services.rb +11 -0
  107. data/lib/ims/lti/tool_base.rb +29 -0
  108. data/lib/ims/lti/tool_config.rb +231 -0
  109. data/lib/ims/lti/tool_consumer.rb +86 -0
  110. data/lib/ims/lti/tool_provider.rb +143 -0
  111. data/lib/ims/lti/version.rb +5 -0
  112. data/lib/ims/lti.rb +61 -0
  113. data/lib/ims.rb +4 -0
  114. metadata +379 -0
@@ -0,0 +1,84 @@
1
+ module IMS::LTI::Services
2
+ class ToolProxyRegistrationService
3
+ def initialize(registration_request)
4
+ @registration_request = registration_request
5
+ end
6
+
7
+ def tool_consumer_profile
8
+ return @tool_consumer_profile if @tool_consumer_profile
9
+
10
+ connection = Faraday.new
11
+ response = connection.get(@registration_request.tc_profile_url)
12
+ @tool_consumer_profile = IMS::LTI::Models::ToolConsumerProfile.new.from_json(response.body)
13
+ end
14
+
15
+ def service_profiles
16
+ tool_consumer_profile.services_offered.map(&:profile)
17
+ end
18
+
19
+ def register_tool_proxy(tool_proxy, reregistration_confirm_url = nil, shared_secret = nil)
20
+ service = tool_consumer_profile.services_offered.find { |s| s.formats.include?('application/vnd.ims.lti.v2.toolproxy+json') && s.actions.include?('POST') }
21
+
22
+ SimpleOAuth::Header::ATTRIBUTE_KEYS << :body_hash unless SimpleOAuth::Header::ATTRIBUTE_KEYS.include? :body_hash
23
+ tool_proxy_json = tool_proxy.to_json
24
+ body_hash = Digest::SHA1.base64digest tool_proxy_json
25
+
26
+ if reregistration?
27
+ consumer_key = tool_proxy.tool_proxy_guid
28
+ consumer_secret = shared_secret
29
+ else
30
+ consumer_key = @registration_request.reg_key
31
+ consumer_secret = @registration_request.reg_password
32
+ end
33
+
34
+ conn = Faraday.new do |conn|
35
+ conn.request :oauth, {:consumer_key => consumer_key, :consumer_secret => consumer_secret, :body_hash => body_hash}
36
+ conn.adapter :net_http
37
+ end
38
+
39
+ response = conn.post do |req|
40
+ req.url service.endpoint
41
+ req.headers['Content-Type'] = 'application/vnd.ims.lti.v2.toolproxy+json'
42
+ req.headers['VND-IMS-CONFIRM-URL'] = reregistration_confirm_url if reregistration_confirm_url
43
+ req.body = tool_proxy_json
44
+ end
45
+
46
+ if response.status == 201 || (response.status == 200 && reregistration?)
47
+ IMS::LTI::Models::ToolProxy.new.from_json(tool_proxy.to_json).from_json(response.body)
48
+ else
49
+ raise IMS::LTI::Errors::ToolProxyRegistrationError.new(response.status, response.body)
50
+ end
51
+ end
52
+
53
+
54
+ def remove_invalid_capabilities!(message_handler)
55
+ {
56
+ invalid_capabilities: remove_capabilites!(message_handler),
57
+ invalid_parameters: remove_params!(message_handler)
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def remove_params!(message_handler)
64
+ orig_parameters = message_handler.parameter || []
65
+ parameters = orig_parameters.select {|p| p.fixed? || tool_consumer_profile.capability_offered.include?(p.variable)}
66
+ message_handler.parameter = parameters
67
+ orig_parameters - parameters
68
+ end
69
+
70
+ def remove_capabilites!(message_handler)
71
+ orig_capabilities = message_handler.enabled_capability || []
72
+ capabilites = orig_capabilities & tool_consumer_profile.capability_offered
73
+ capabilites.reject! { |cap| IMS::LTI::Models::ToolConsumerProfile::MESSAGING_CAPABILITIES.include? cap }
74
+ message_handler.enabled_capability = capabilites
75
+ orig_capabilities - capabilites
76
+ end
77
+
78
+ def reregistration?
79
+ @registration_request.is_a?(IMS::LTI::Models::Messages::ToolProxyUpdateRequest)
80
+ end
81
+
82
+
83
+ end
84
+ end
@@ -0,0 +1,182 @@
1
+ module IMS::LTI::Services
2
+ class ToolProxyValidator
3
+
4
+ attr_reader :tool_proxy
5
+
6
+ def initialize(tool_proxy)
7
+ @tool_proxy = tool_proxy
8
+ end
9
+
10
+ def tool_consumer_profile
11
+ return @tool_consumer_profile if @tool_consumer_profile
12
+
13
+ connection = Faraday.new
14
+ response = connection.get(tool_proxy.tool_consumer_profile)
15
+ @tool_consumer_profile = IMS::LTI::Models::ToolConsumerProfile.new.from_json(response.body)
16
+ @tool_consumer_profile
17
+ end
18
+
19
+ def tool_consumer_profile=(tcp)
20
+ tcp = IMS::LTI::Models::ToolConsumerProfile.from_json(tcp) unless tcp.is_a?(IMS::LTI::Models::ToolConsumerProfile)
21
+ if tool_proxy.tool_consumer_profile != tcp.id
22
+ raise IMS::LTI::Errors::InvalidToolConsumerProfile, "Tool Consumer Profile @id doesn't match the Tool Proxy"
23
+ end
24
+ @tool_consumer_profile = tcp
25
+ end
26
+
27
+ def capabilities_offered
28
+ tool_consumer_profile.capabilities_offered
29
+ end
30
+
31
+ def invalid_services
32
+ services = tool_proxy.security_contract.services
33
+ services_used = services.each_with_object({}) do |s, hash|
34
+ hash[s.service.split('#').last.strip] = s.actions
35
+ hash
36
+ end
37
+ services_offered = tool_consumer_profile.services_offered.each_with_object({}) do |s, hash|
38
+ hash[s.id.split(':').last.split('#').last.strip] = s.actions
39
+ hash
40
+ end
41
+ invalid_services = services_used.each_with_object({}) do |(id, actions), hash|
42
+ if services_offered.keys.include?(id)
43
+ actions_used = normalize_strings(*services_offered[id])
44
+ actions_offered = normalize_strings(*actions)
45
+ invalid_actions = actions_offered - actions_used
46
+ hash[id] = invalid_actions unless invalid_actions.empty?
47
+ else
48
+ hash[id] = actions
49
+ end
50
+ hash
51
+ end
52
+ invalid_services
53
+ end
54
+
55
+ def invalid_message_handlers
56
+ ret_val = {}
57
+ tool_profile = tool_proxy.tool_profile
58
+ #singleton_message_handlers = tool_profile.messages
59
+ invalid_rhs = validate_resource_handlers(tool_profile.resource_handlers)
60
+ ret_val[:resource_handlers] = invalid_rhs unless invalid_rhs.empty?
61
+ invalid_singleton_message_handlers = validate_singleton_message_handlers(tool_profile.messages)
62
+ ret_val[:singleton_message_handlers] = invalid_singleton_message_handlers unless invalid_singleton_message_handlers.empty?
63
+ ret_val
64
+ end
65
+
66
+ def invalid_capabilities
67
+ tool_proxy.enabled_capabilities - capabilities_offered
68
+ end
69
+
70
+ def invalid_security_contract
71
+ ret_val = {}
72
+
73
+ is_split_secret_capable = tool_proxy.enabled_capabilities.include?('Security.splitSecret')
74
+ has_shared_secret = tool_proxy.security_contract.shared_secret != nil && !tool_proxy.security_contract.shared_secret.empty?
75
+ has_split_secret = tool_proxy.security_contract.tp_half_shared_secret != nil && !tool_proxy.security_contract.tp_half_shared_secret.empty?
76
+
77
+ if is_split_secret_capable
78
+ ret_val[:missing_secret] = :tp_half_shared_secret unless has_split_secret
79
+ ret_val[:invalid_secret_type] = :shared_secret if has_shared_secret
80
+ else
81
+ ret_val[:missing_secret] = :shared_secret unless has_shared_secret
82
+ ret_val[:invalid_secret_type] = :tp_half_shared_secret if has_split_secret
83
+ end
84
+
85
+ ret_val
86
+ end
87
+
88
+ def invalid_security_profiles
89
+ security_profiles = tool_proxy.tool_profile.security_profiles
90
+ array = security_profiles.each_with_object([]) do |sp, array|
91
+ tcp_sp = tool_consumer_profile.security_profile_by_name(security_profile_name: sp.security_profile_name)
92
+ if tcp_sp
93
+ supported_algorithms = sp.digest_algorithms & tcp_sp.digest_algorithms
94
+ unsupported_algorithms = sp.digest_algorithms - supported_algorithms
95
+ unless unsupported_algorithms.empty?
96
+ array << { name: sp.security_profile_name, algorithms: unsupported_algorithms }
97
+ end
98
+ else
99
+ array << { name: sp.security_profile_name }
100
+ end
101
+ end
102
+ array
103
+ end
104
+
105
+ def errors
106
+ ret_val = {}
107
+ ret_val[:invalid_security_contract] = invalid_security_contract unless invalid_security_contract.empty?
108
+ ret_val[:invalid_capabilities] = invalid_capabilities unless invalid_capabilities.empty?
109
+ ret_val[:invalid_message_handlers] = invalid_message_handlers unless invalid_message_handlers.empty?
110
+ ret_val[:invalid_services] = invalid_services unless invalid_services.empty?
111
+ ret_val[:invalid_security_profiles] = invalid_security_profiles unless invalid_security_profiles.empty?
112
+ ret_val
113
+ end
114
+
115
+ def valid?
116
+ errors.keys.empty?
117
+ end
118
+
119
+ private
120
+
121
+ def normalize_strings(string, *strings)
122
+ strings.push(string)
123
+ normalized = strings.map {|s| s.upcase.strip}
124
+ normalized
125
+ end
126
+
127
+ def validate_message_handlers(message_handlers)
128
+ message_handlers.each_with_object([]) do |mh, array|
129
+ invalid_capabilities = mh.enabled_capabilities - capabilities_offered
130
+ invalid_parameters = validate_parameters(mh.parameters)
131
+ if !invalid_parameters.empty? || !invalid_capabilities.empty?
132
+ hash = { message_type: mh.message_type, }
133
+ hash[:invalid_capabilities] = invalid_capabilities unless invalid_capabilities.empty?
134
+ hash[:invalid_parameters] = invalid_parameters unless invalid_parameters.empty?
135
+ array << hash
136
+ end
137
+ array
138
+ end
139
+ end
140
+
141
+ def validate_parameters(parameters)
142
+ parameters.each_with_object([]) do |p, array|
143
+ if !p.fixed? && !capabilities_offered.include?(p.variable)
144
+ array << { name: p.name, variable: p.variable }
145
+ end
146
+ end
147
+ end
148
+
149
+ def validate_message_types(message_handlers)
150
+ message_handlers.each_with_object([]) do |mh, array|
151
+ array << mh.message_type unless capabilities_offered.include?(mh.message_type)
152
+ array
153
+ end
154
+ end
155
+
156
+ def validate_resource_handlers(resource_handlers)
157
+ resource_handlers.each_with_object([]) do |rh, array|
158
+ invalid_message_types = validate_message_types(rh.messages)
159
+ invalid_mhs = validate_message_handlers(rh.messages)
160
+ if !invalid_mhs.empty? || !invalid_message_types.empty?
161
+ hash = {
162
+ code: rh.resource_type.code,
163
+ }
164
+ hash[:messages] = invalid_mhs unless invalid_mhs.empty?
165
+ hash[:invalid_message_types] = invalid_message_types unless invalid_message_types.empty?
166
+ array << hash
167
+ end
168
+ array
169
+ end
170
+ end
171
+
172
+ def validate_singleton_message_handlers(message_handlers)
173
+ hash = {}
174
+ invalid_messages = validate_message_handlers(message_handlers)
175
+ invalid_message_types = validate_message_types(message_handlers)
176
+ hash[:messages] = invalid_messages unless invalid_messages.empty?
177
+ hash[:invalid_message_types] = invalid_message_types unless invalid_message_types.empty?
178
+ hash
179
+ end
180
+
181
+ end
182
+ end
@@ -0,0 +1,11 @@
1
+ module IMS::LTI
2
+ module Services
3
+ require_relative 'services/oauth2_client'
4
+ require_relative 'services/tool_proxy_registration_service'
5
+ require_relative 'services/tool_config'
6
+ require_relative 'services/tool_proxy_validator'
7
+ require_relative 'services/message_authenticator'
8
+ require_relative 'services/tool_consumer_profile_service'
9
+ require_relative 'services/authentication_service'
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module IMS::LTI
2
+ class ToolBase
3
+ include IMS::LTI::Extensions::Base
4
+ include IMS::LTI::LaunchParams
5
+ include IMS::LTI::RequestValidator
6
+
7
+ # OAuth credentials
8
+ attr_accessor :consumer_key, :consumer_secret
9
+
10
+ def initialize(consumer_key, consumer_secret, params={})
11
+ @consumer_key = consumer_key
12
+ @consumer_secret = consumer_secret
13
+ @custom_params = {}
14
+ @ext_params = {}
15
+ @non_spec_params = {}
16
+ process_params(params)
17
+ end
18
+
19
+ # Convenience method for doing oauth signed requests to services that
20
+ # aren't supported by this library
21
+ def post_service_request(url, content_type, body)
22
+ IMS::LTI::post_service_request(@consumer_key,
23
+ @consumer_secret,
24
+ url,
25
+ content_type,
26
+ body)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,231 @@
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
+ set_options(properties, vendor_ext_node)
119
+ self.set_ext_params(platform, properties)
120
+ end
121
+
122
+ end
123
+ end
124
+
125
+ # Generate XML from the current settings
126
+ def to_xml(opts = {})
127
+ raise IMS::LTI::InvalidLTIConfigError, "A launch url is required for an LTI configuration." unless self.launch_url || self.secure_launch_url
128
+
129
+ builder = Builder::XmlMarkup.new(:indent => opts[:indent] || 0)
130
+ builder.instruct!
131
+ builder.cartridge_basiclti_link("xmlns" => "http://www.imsglobal.org/xsd/imslticc_v1p0",
132
+ "xmlns:blti" => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0',
133
+ "xmlns:lticm" => 'http://www.imsglobal.org/xsd/imslticm_v1p0',
134
+ "xmlns:lticp" => 'http://www.imsglobal.org/xsd/imslticp_v1p0',
135
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
136
+ "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"
137
+ ) do |blti_node|
138
+
139
+ %w{title description launch_url secure_launch_url icon secure_icon}.each do |key|
140
+ blti_node.blti key.to_sym, self.send(key) if self.send(key)
141
+ end
142
+
143
+ vendor_keys = %w{name code description url}
144
+ if vendor_keys.any?{|k|self.send("vendor_#{k}")} || vendor_contact_email
145
+ blti_node.blti :vendor do |v_node|
146
+ vendor_keys.each do |key|
147
+ v_node.lticp key.to_sym, self.send("vendor_#{key}") if self.send("vendor_#{key}")
148
+ end
149
+ if vendor_contact_email
150
+ v_node.lticp :contact do |c_node|
151
+ c_node.lticp :name, vendor_contact_name
152
+ c_node.lticp :email, vendor_contact_email
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ if !@custom_params.empty?
159
+ blti_node.tag!("blti:custom") do |custom_node|
160
+ @custom_params.keys.sort.each do |key|
161
+ val = @custom_params[key]
162
+ custom_node.lticm :property, val, 'name' => key
163
+ end
164
+ end
165
+ end
166
+
167
+ if !@extensions.empty?
168
+ @extensions.keys.sort.each do |ext_platform|
169
+ ext_params = @extensions[ext_platform]
170
+ blti_node.blti(:extensions, :platform => ext_platform) do |ext_node|
171
+ ext_params.keys.sort.each do |key|
172
+ nest_xml(ext_node, key, ext_params[key])
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ blti_node.cartridge_bundle(:identifierref => @cartridge_bundle) if @cartridge_bundle
179
+ blti_node.cartridge_icon(:identifierref => @cartridge_icon) if @cartridge_icon
180
+
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def nest_xml(ext_node, key, value)
187
+ if value.is_a?(Hash)
188
+ ext_node.lticm(:options, :name => key) do |type_node|
189
+ value.keys.sort.each do |sub_key|
190
+ nest_xml(type_node, sub_key, value[sub_key])
191
+ end
192
+ end
193
+ else
194
+ ext_node.lticm :property, value, 'name' => key
195
+ end
196
+ end
197
+
198
+ def get_node_text(node, path)
199
+ if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
200
+ val.text
201
+ else
202
+ nil
203
+ end
204
+ end
205
+
206
+ def get_node_att(node, path, att)
207
+ if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
208
+ val.attributes[att]
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+ def set_properties(hash, node)
215
+ REXML::XPath.each(node, 'lticm:property', LTI_NAMESPACES) do |prop|
216
+ hash[prop.attributes['name']] = prop.text
217
+ end
218
+ end
219
+
220
+ def set_options(hash, node)
221
+ REXML::XPath.each(node, 'lticm:options', LTI_NAMESPACES) do |options_node|
222
+ opt_name = options_node.attributes['name']
223
+ options = {}
224
+ set_properties(options, options_node)
225
+ set_options(options, options_node)
226
+ hash[opt_name] = options
227
+ end
228
+ end
229
+
230
+ end
231
+ end
@@ -0,0 +1,86 @@
1
+ module IMS::LTI
2
+ # Class for implementing an LTI Tool Consumer
3
+ class ToolConsumer < ToolBase
4
+ attr_accessor :launch_url, :timestamp, :nonce
5
+
6
+ # Create a new ToolConsumer
7
+ #
8
+ # @param consumer_key [String] The OAuth consumer key
9
+ # @param consumer_secret [String] The OAuth consumer secret
10
+ # @param params [Hash] Set the launch parameters as described in LaunchParams
11
+ def initialize(consumer_key, consumer_secret, params={})
12
+ super(consumer_key, consumer_secret, params)
13
+ @launch_url = params['launch_url']
14
+ end
15
+
16
+ def process_post_request(post_request)
17
+ request = extend_outcome_request(OutcomeRequest.new)
18
+ request.process_post_request(post_request)
19
+ end
20
+
21
+ # Set launch data from a ToolConfig
22
+ #
23
+ # @param config [ToolConfig]
24
+ def set_config(config)
25
+ @launch_url ||= config.secure_launch_url
26
+ @launch_url ||= config.launch_url
27
+ # any parameters already set will take priority
28
+ @custom_params = config.custom_params.merge(@custom_params)
29
+ end
30
+
31
+ # Check if the required parameters for a tool launch are set
32
+ def has_required_params?
33
+ @consumer_key && @consumer_secret && @resource_link_id && @launch_url
34
+ end
35
+
36
+ # Generate the launch data including the necessary OAuth information
37
+ #
38
+ #
39
+ def generate_launch_data
40
+ raise IMS::LTI::InvalidLTIConfigError, "Not all required params set for tool launch" unless has_required_params?
41
+
42
+ params = self.to_params
43
+ params['lti_version'] ||= 'LTI-1p0'
44
+ params['lti_message_type'] ||= 'basic-lti-launch-request'
45
+ uri = URI.parse(@launch_url)
46
+
47
+ if uri.port == uri.default_port
48
+ host = uri.host
49
+ else
50
+ host = "#{uri.host}:#{uri.port}"
51
+ end
52
+
53
+ consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, {
54
+ :site => "#{uri.scheme}://#{host}",
55
+ :signature_method => "HMAC-SHA1"
56
+ })
57
+
58
+ path = uri.path
59
+ path = '/' if path.empty?
60
+ if uri.query && uri.query != ''
61
+ CGI.parse(uri.query).each do |query_key, query_values|
62
+ unless params[query_key]
63
+ params[query_key] = query_values.first
64
+ end
65
+ end
66
+ end
67
+ options = {
68
+ :scheme => 'body',
69
+ :timestamp => @timestamp,
70
+ :nonce => @nonce
71
+ }
72
+ request = consumer.create_signed_request(:post, path, nil, options, params)
73
+
74
+ # the request is made by a html form in the user's browser, so we
75
+ # want to revert the escapage and return the hash of post parameters ready
76
+ # for embedding in a html view
77
+ hash = {}
78
+ request.body.split(/&/).each do |param|
79
+ key, val = param.split(/=/).map { |v| CGI.unescape(v) }
80
+ hash[key] = val
81
+ end
82
+ hash
83
+ end
84
+
85
+ end
86
+ end