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,125 @@
1
+ module IMS::LTI::Serializers
2
+ class Base
3
+ class Filter
4
+ def initialize
5
+ @optionals = Set.new
6
+ @keys = {}
7
+ @serializables = Set.new
8
+ @serializable_lists = Set.new
9
+ end
10
+
11
+ def add_filter(name, opts={})
12
+ @optionals.add(name) if opts[:optional]
13
+ @keys[name] = opts[:key] if opts[:key]
14
+ @serializables.add(name) if opts[:serializable]
15
+ @serializable_lists.add(name) if opts[:list_of_serializables]
16
+ end
17
+
18
+ def options_for_attribute(attribute)
19
+ options = {}
20
+ options[:optional] = @optionals.include?(attribute) ? true : false
21
+ options[:key] = @keys[attribute]
22
+ options[:has_serializable] = @serializables.include?(attribute) ? true : false
23
+ options[:has_list_of_serializables] = @serializable_lists.include?(attribute) ? true : false
24
+ options
25
+ end
26
+
27
+ def filter(hash)
28
+ hash = optionals(hash)
29
+ hash = serializables(hash)
30
+ hash = serializable_lists(hash)
31
+ keys(hash) # Needs to happen last
32
+ end
33
+
34
+ private
35
+ def optionals(hash)
36
+ hash.reject { |k, v| @optionals.include?(k) && v.nil? }
37
+ end
38
+
39
+ def serializables(hash)
40
+ hash.reduce({}) do |memo, (k, v)|
41
+ memo[k] = @serializables.include?(k) ? serialize_serializable(v) : v
42
+ memo
43
+ end
44
+ end
45
+
46
+ def serialize_serializable(serializable)
47
+ return if serializable.nil?
48
+ serializable.as_json
49
+ end
50
+
51
+ def serializable_lists(hash)
52
+ hash.reduce({}) do |memo, (k, v)|
53
+ memo[k] = @serializable_lists.include?(k) ? serialize_serializable_list(v) : v
54
+ memo
55
+ end
56
+ end
57
+
58
+ def serialize_serializable_list(list)
59
+ return if list.nil?
60
+ list.map(&:as_json)
61
+ end
62
+
63
+ def keys(hash)
64
+ hash.reduce({}) do |memo, (k, v)|
65
+ key = @keys.include?(k) ? @keys[k] : k
66
+ memo[key] = v
67
+ memo
68
+ end
69
+ end
70
+ end
71
+
72
+ @filter = Filter.new
73
+ @attributes = Set.new
74
+
75
+ def self.inherited(subclass)
76
+ subclass.instance_variable_set(:@filter, Filter.new)
77
+ subclass.instance_variable_set(:@attributes, Set.new)
78
+ end
79
+
80
+ def self.set_attribute(name, opts={})
81
+ @attributes.add(name)
82
+ @filter.add_filter(name, opts)
83
+ end
84
+
85
+ def self.set_attributes(*names)
86
+ names.each { |name| set_attribute(name) }
87
+ end
88
+
89
+ def self.has_serializable(name, opts={})
90
+ opts[:serializable] = true
91
+ opts[:list_of_serializables] = false
92
+ set_attribute(name, opts)
93
+ end
94
+
95
+ def self.has_list_of_serializables(name, opts={})
96
+ opts[:list_of_serializables] = true
97
+ opts[:serializable] = false
98
+ set_attribute(name, opts)
99
+ end
100
+
101
+ def self.as_json(obj)
102
+ @filter.filter(base_hash(obj))
103
+ end
104
+
105
+ def self.to_json(obj)
106
+ as_json(obj).to_json
107
+ end
108
+
109
+ def self.options_for_attribute(attribute)
110
+ @filter.options_for_attribute(attribute)
111
+ end
112
+
113
+ def self.attributes
114
+ @attributes
115
+ end
116
+
117
+ private
118
+
119
+ def self.base_hash(obj)
120
+ h = {}
121
+ @attributes.each { |attribute| h[attribute] = obj.send(attribute) }
122
+ h
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,5 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class AgentSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class ContainerSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :membership_predicate, key: :membershipPredicate
4
+ has_serializable :membership_subject, key: :membershipSubject
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class ContextSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ set_attribute :name
5
+ set_attribute :type, key: :@type
6
+ set_attribute :context_id, key: :contextId
7
+ has_list_of_serializables :membership
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class LISMembershipContainerSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :membership_predicate, key: :membershipPredicate
4
+ has_serializable :membership_subject, key: :membershipSubject
5
+ set_attribute :id, key: :@id
6
+ set_attribute :context, key: :@context
7
+ set_attribute :type, key: :@type
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class LISPersonSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ set_attributes :name, :img, :email
5
+ set_attribute :family_name, key: :familyName
6
+ set_attribute :given_name, key: :givenName
7
+ set_attribute :result_sourced_id, key: :resultSourcedId
8
+ set_attribute :sourced_id, key: :sourcedId
9
+ set_attribute :user_id, key: :userId
10
+ set_attribute :sis_id, key: :sisId, optional: true
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class MembershipSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ set_attributes :status, :role
5
+ has_serializable :member
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class OrganizationSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ set_attribute :name
5
+ set_attribute :type, key: :@type
6
+ has_list_of_serializables :membership
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class PageSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ set_attribute :type, key: :@type
5
+ set_attribute :context, key: :@context
6
+ set_attribute :differences
7
+ set_attribute :next_page, key: :nextPage
8
+ has_serializable :page_of, key: :pageOf
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module IMS::LTI::Serializers::MembershipService
2
+ class PersonSerializer < IMS::LTI::Serializers::Base
3
+ set_attribute :id, key: :@id
4
+ set_attributes :name, :img
5
+ set_attribute :family_name, key: :familyName
6
+ set_attribute :given_name, key: :givenName
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module IMS::LTI::Serializers
2
+ module MembershipService
3
+ require_relative 'membership_service/agent_serializer'
4
+ require_relative 'membership_service/container_serializer'
5
+ require_relative 'membership_service/context_serializer'
6
+ require_relative 'membership_service/lis_membership_container_serializer'
7
+ require_relative 'membership_service/lis_person_serializer'
8
+ require_relative 'membership_service/membership_serializer'
9
+ require_relative 'membership_service/organization_serializer'
10
+ require_relative 'membership_service/page_serializer'
11
+ require_relative 'membership_service/person_serializer'
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module IMS::LTI
2
+ module Serializers
3
+ require_relative 'serializers/base'
4
+ require_relative 'serializers/membership_service'
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ module IMS::LTI::Services
2
+ class AuthenticationService
3
+
4
+ attr_accessor :connection, :iss, :aud, :sub, :secret, :grant_type,
5
+ :additional_claims, :additional_params
6
+ attr_writer :secret
7
+
8
+ def initialize(iss:, aud:, sub:, secret:)
9
+ @iss = iss
10
+ @aud = aud
11
+ @sub = sub
12
+ @secret = secret
13
+ @additional_claims = {}
14
+ @additional_params = {}
15
+ @grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
16
+ end
17
+
18
+ def connection
19
+ @connection ||= Faraday.new
20
+ end
21
+
22
+ def access_token
23
+ access_token_request['access_token']
24
+ end
25
+
26
+ def expiration
27
+ expires_in = access_token_request['expires_in'].to_i
28
+ @_response_time + expires_in
29
+ end
30
+
31
+ def expired?
32
+ expiration < Time.now
33
+ end
34
+
35
+ def invalidate!
36
+ @_access_token_request = nil
37
+ @_response_time = nil
38
+ end
39
+
40
+ private
41
+
42
+ def access_token_request
43
+ @_access_token_request ||= begin
44
+ assertion = JSON::JWT.new(
45
+ iss: iss,
46
+ sub: sub,
47
+ aud: aud.to_s,
48
+ iat: Time.now.to_i,
49
+ exp: 1.minute.from_now,
50
+ jti: SecureRandom.uuid
51
+ )
52
+ assertion.merge!(@additional_claims)
53
+ assertion = assertion.sign(@secret, :HS256).to_s
54
+ body = {
55
+ grant_type: grant_type,
56
+ assertion: assertion
57
+ }
58
+ body.merge!(@additional_params)
59
+ response = connection.post(aud, body)
60
+ raise IMS::LTI::Errors::AuthenticationFailedError.new(response: response) unless response.success?
61
+ @_response_time = Time.now
62
+ response.body
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,80 @@
1
+ module IMS::LTI::Services
2
+ class MessageAuthenticator
3
+
4
+ attr_reader :launch_url, :params, :message, :simple_oauth_header, :signature, :consumer_key
5
+
6
+ def initialize(launch_url, params, secret)
7
+ @launch_url = launch_url
8
+ @params = params
9
+ @options, @parsed_params = parse_params(params)
10
+ @consumer_key = @options[:consumer_key]
11
+ @signature = @parsed_params.delete(:oauth_signature)
12
+ @secret = secret
13
+ end
14
+
15
+
16
+ def valid_signature?
17
+ message.jwt ? valid_jwt? : simple_oauth_header.valid?(signature: signature)
18
+ end
19
+
20
+ def message
21
+ @message ||= begin
22
+ m = IMS::LTI::Models::Messages::Message.generate(params)
23
+ m.launch_url = launch_url
24
+ m
25
+ end
26
+ end
27
+
28
+ def simple_oauth_header
29
+ @simple_oauth_header ||= begin
30
+ @simple_oauth_header = SimpleOAuth::Header.new(
31
+ :post, launch_url,
32
+ @parsed_params,
33
+ @options.merge(
34
+ {
35
+ consumer_key: consumer_key,
36
+ consumer_secret: @secret
37
+ }
38
+ )
39
+ )
40
+ @simple_oauth_header
41
+ end
42
+ end
43
+
44
+ def base_string
45
+ simple_oauth_header.send(:signature_base)
46
+ end
47
+
48
+ def signed_params
49
+ simple_oauth_header.signed_attributes.merge(@parsed_params)
50
+ end
51
+
52
+
53
+ private
54
+
55
+ def valid_jwt?
56
+ begin
57
+ jwt = JSON::JWT.decode(message.jwt, @secret)
58
+ aud1 = Addressable::URI.parse(jwt['aud'])
59
+ aud2 = Addressable::URI.parse(launch_url)
60
+ [aud1, aud2].each{ |aud| aud.fragment = '' }
61
+ aud1.normalize == aud2.normalize
62
+ rescue JSON::JWS::VerificationFailed
63
+ false
64
+ end
65
+ end
66
+
67
+ def parse_params(params)
68
+ params.inject([{}, {}]) do |array, (k, v)|
69
+ attr = k.to_s.sub('oauth_', '').to_sym
70
+ if SimpleOAuth::Header::ATTRIBUTE_KEYS.include?(attr)
71
+ array[0][attr] = v
72
+ else
73
+ array[1][k.to_sym] = v
74
+ end
75
+ array
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,18 @@
1
+ module IMS::LTI::Services
2
+
3
+ class OAuth2Client
4
+ attr_accessor :token, :base_url
5
+ attr_writer :connection
6
+
7
+ def initialize(token:, base_url: nil)
8
+ @base_url = base_url
9
+ @token = token
10
+ end
11
+
12
+ def connection
13
+ @connection ||= Faraday.new base_url do |conn|
14
+ conn.authorization :Bearer, token
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,223 @@
1
+ module IMS::LTI::Services
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::Services::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::Services::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
+ builder = Builder::XmlMarkup.new(:indent => opts[:indent] || 0)
134
+ builder.instruct!
135
+ builder.cartridge_basiclti_link("xmlns" => "http://www.imsglobal.org/xsd/imslticc_v1p0",
136
+ "xmlns:blti" => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0',
137
+ "xmlns:lticm" => 'http://www.imsglobal.org/xsd/imslticm_v1p0',
138
+ "xmlns:lticp" => 'http://www.imsglobal.org/xsd/imslticp_v1p0',
139
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
140
+ "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"
141
+ ) do |blti_node|
142
+
143
+ %w{title description launch_url secure_launch_url icon secure_icon}.each do |key|
144
+ blti_node.blti key.to_sym, self.send(key) if self.send(key)
145
+ end
146
+
147
+ vendor_keys = %w{name code description url}
148
+ if vendor_keys.any? { |k| self.send("vendor_#{k}") } || vendor_contact_email
149
+ blti_node.blti :vendor do |v_node|
150
+ vendor_keys.each do |key|
151
+ v_node.lticp key.to_sym, self.send("vendor_#{key}") if self.send("vendor_#{key}")
152
+ end
153
+ if vendor_contact_email
154
+ v_node.lticp :contact do |c_node|
155
+ c_node.lticp :name, vendor_contact_name
156
+ c_node.lticp :email, vendor_contact_email
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ if !@custom_params.empty?
163
+ blti_node.tag!("blti:custom") do |custom_node|
164
+ @custom_params.keys.sort.each do |key|
165
+ val = @custom_params[key]
166
+ custom_node.lticm :property, val, 'name' => key
167
+ end
168
+ end
169
+ end
170
+
171
+ if !@extensions.empty?
172
+ @extensions.keys.sort.each do |ext_platform|
173
+ ext_params = @extensions[ext_platform]
174
+ blti_node.blti(:extensions, :platform => ext_platform) do |ext_node|
175
+ ext_params.keys.sort.each do |key|
176
+ val = ext_params[key]
177
+ if val.is_a?(Hash)
178
+ ext_node.lticm(:options, :name => key) do |type_node|
179
+ val.keys.sort.each do |p_key|
180
+ p_val = val[p_key]
181
+ type_node.lticm :property, p_val, 'name' => p_key
182
+ end
183
+ end
184
+ else
185
+ ext_node.lticm :property, val, 'name' => key
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ blti_node.cartridge_bundle(:identifierref => @cartridge_bundle) if @cartridge_bundle
193
+ blti_node.cartridge_icon(:identifierref => @cartridge_icon) if @cartridge_icon
194
+
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ def get_node_text(node, path)
201
+ if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
202
+ val.text
203
+ else
204
+ nil
205
+ end
206
+ end
207
+
208
+ def get_node_att(node, path, att)
209
+ if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
210
+ val.attributes[att]
211
+ else
212
+ nil
213
+ end
214
+ end
215
+
216
+ def set_properties(hash, node)
217
+ REXML::XPath.each(node, 'lticm:property', LTI_NAMESPACES) do |prop|
218
+ hash[prop.attributes['name']] = prop.text
219
+ end
220
+ end
221
+
222
+ end
223
+ end
@@ -0,0 +1,16 @@
1
+ module IMS::LTI::Services
2
+ class ToolConsumerProfileService
3
+
4
+ attr_accessor :tcp
5
+
6
+ def initialize(tool_consumer_profile)
7
+ @tcp = tool_consumer_profile
8
+ end
9
+
10
+ def supports_capabilities?(capability, *capabilities)
11
+ capabilities.unshift(capability)
12
+ (capabilities - tcp.capabilities_offered).empty?
13
+ end
14
+
15
+ end
16
+ end