403 lines
13 KiB
Ruby
403 lines
13 KiB
Ruby
class ContextExternalTool < ActiveRecord::Base
|
|
include Workflow
|
|
has_many :content_tags, :as => :content
|
|
has_many :assignments
|
|
belongs_to :context, :polymorphic => true
|
|
belongs_to :cloned_item
|
|
attr_accessible :privacy_level, :domain, :url, :shared_secret, :consumer_key,
|
|
:name, :description, :custom_fields, :custom_fields_string,
|
|
:course_navigation, :account_navigation, :user_navigation,
|
|
:resource_selection, :editor_button
|
|
validates_presence_of :name
|
|
validates_presence_of :consumer_key
|
|
validates_presence_of :shared_secret
|
|
validate :url_or_domain_is_set
|
|
serialize :settings
|
|
|
|
before_save :infer_defaults
|
|
|
|
workflow do
|
|
state :anonymous
|
|
state :name_only
|
|
state :public
|
|
state :deleted
|
|
end
|
|
|
|
set_policy do
|
|
given { |user, session| self.cached_context_grants_right?(user, session, :update) }
|
|
can :read and can :update and can :delete
|
|
end
|
|
|
|
def url_or_domain_is_set
|
|
if url.present? && domain.present?
|
|
errors.add(:url, t('url_or_domain_not_both', "Either the url or domain should be set, not both."))
|
|
errors.add(:domain, t('url_or_domain_not_both', "Either the url or domain should be set, not both."))
|
|
elsif url.blank? && domain.blank?
|
|
errors.add(:url, t('url_or_domain_required', "Either the url or domain should be set."))
|
|
errors.add(:domain, t('url_or_domain_required', "Either the url or domain should be set."))
|
|
end
|
|
end
|
|
|
|
def settings
|
|
read_attribute(:settings) || write_attribute(:settings, {})
|
|
end
|
|
|
|
def label_for(key, lang=nil)
|
|
labels = settings[key] && settings[key][:labels]
|
|
(labels && labels[lang]) || (settings[key] && settings[key][:text]) || name || "External Tool"
|
|
end
|
|
|
|
def readable_state
|
|
workflow_state.titleize
|
|
end
|
|
|
|
def privacy_level=(val)
|
|
if ['anonymous', 'name_only', 'public'].include?(val)
|
|
self.workflow_state = val
|
|
end
|
|
end
|
|
|
|
def privacy_level
|
|
self.workflow_state
|
|
end
|
|
|
|
def custom_fields_string
|
|
(settings[:custom_fields] || {}).map{|key, val|
|
|
"#{key}=#{val}"
|
|
}.join("\n")
|
|
end
|
|
|
|
def custom_fields_string=(str)
|
|
hash = {}
|
|
str.split(/\n/).each do |line|
|
|
key, val = line.split(/=/)
|
|
hash[key] = val if key.present? && val.present?
|
|
end
|
|
settings[:custom_fields] = hash
|
|
end
|
|
|
|
def custom_fields=(hash)
|
|
settings[:custom_fields] = hash if hash.is_a?(Hash)
|
|
end
|
|
|
|
def custom_fields
|
|
settings[:custom_fields]
|
|
end
|
|
|
|
def course_navigation=(hash)
|
|
tool_setting(:course_navigation, hash) { |nav_settings|
|
|
if hash[:visibility] == 'members' || hash[:visibility] == 'admins'
|
|
nav_settings[:visibility] = hash[:visibility]
|
|
end
|
|
nav_settings[:default] = !!hash[:default]
|
|
}
|
|
end
|
|
|
|
def account_navigation=(hash)
|
|
tool_setting(:account_navigation, hash)
|
|
end
|
|
|
|
def account_navigation
|
|
settings[:account_navigation]
|
|
end
|
|
|
|
def user_navigation=(hash)
|
|
tool_setting(:user_navigation, hash)
|
|
end
|
|
|
|
def user_navigation
|
|
settings[:user_navigation]
|
|
end
|
|
|
|
def resource_selection=(hash)
|
|
tool_setting(:resource_selection, hash, :selection_width, :selection_height)
|
|
end
|
|
|
|
def resource_selection
|
|
settings[:resource_selection]
|
|
end
|
|
|
|
def editor_button=(hash)
|
|
tool_setting(:editor_button, hash, :selection_width, :selection_height, :icon_url)
|
|
end
|
|
|
|
def editor_button
|
|
settings[:editor_button]
|
|
end
|
|
|
|
def shared_secret=(val)
|
|
write_attribute(:shared_secret, val) unless val.blank?
|
|
end
|
|
|
|
def infer_defaults
|
|
self.url = nil if url.blank?
|
|
self.domain = nil if domain.blank?
|
|
|
|
settings.delete(:user_navigation) if settings[:user_navigation] && (!settings[:user_navigation][:url])
|
|
settings.delete(:course_navigation) if settings[:course_navigation] && (!settings[:course_navigation][:url])
|
|
settings.delete(:account_navigation) if settings[:account_navigation] && (!settings[:account_navigation][:url])
|
|
settings.delete(:resource_selection) if settings[:resource_selection] && (!settings[:resource_selection][:url] || !settings[:resource_selection][:selection_width] || !settings[:resource_selection][:selection_height])
|
|
settings.delete(:editor_button) if settings[:editor_button] && (!settings[:editor_button][:url] || !settings[:editor_button][:icon_url])
|
|
[:resource_selection, :editor_button].each do |type|
|
|
if settings[type]
|
|
settings[type][:selection_width] = settings[type][:selection_width].to_i
|
|
settings[type][:selection_height] = settings[type][:selection_height].to_i
|
|
end
|
|
end
|
|
|
|
self.has_user_navigation = !!settings[:user_navigation]
|
|
self.has_course_navigation = !!settings[:course_navigation]
|
|
self.has_account_navigation = !!settings[:account_navigation]
|
|
self.has_resource_selection = !!settings[:resource_selection]
|
|
self.has_editor_button = !!settings[:editor_button]
|
|
true
|
|
end
|
|
|
|
def self.standardize_url(url)
|
|
return "" if url.empty?
|
|
url = "http://" + url unless url.match(/:\/\//)
|
|
res = URI.parse(url).normalize
|
|
res.query = res.query.split(/&/).sort.join('&') if !res.query.blank?
|
|
res.to_s
|
|
end
|
|
|
|
alias_method :destroy!, :destroy
|
|
def destroy
|
|
self.workflow_state = 'deleted'
|
|
save!
|
|
end
|
|
|
|
def include_email?
|
|
public?
|
|
end
|
|
|
|
def include_name?
|
|
name_only? || public?
|
|
end
|
|
|
|
def precedence
|
|
if domain
|
|
# Somebody tell me if we should be expecting more than
|
|
# 25 dots in a url host...
|
|
25 - domain.split(/\./).length
|
|
elsif url
|
|
25
|
|
else
|
|
26
|
|
end
|
|
end
|
|
|
|
def matches_url?(url)
|
|
if !defined?(@standard_url)
|
|
@standard_url = !self.url.blank? && ContextExternalTool.standardize_url(self.url)
|
|
end
|
|
return true if url == @standard_url
|
|
host = URI.parse(url).host rescue nil
|
|
!!(host && ('.' + host).match(/\.#{domain}\z/))
|
|
end
|
|
|
|
def self.all_tools_for(context)
|
|
contexts = []
|
|
tools = []
|
|
while context
|
|
if context.is_a?(Group)
|
|
contexts << context
|
|
context = context.context || context.account
|
|
elsif context.is_a?(Course)
|
|
contexts << context
|
|
context = context.account
|
|
elsif context.is_a?(Account)
|
|
contexts << context
|
|
context = context.parent_account
|
|
else
|
|
context = nil
|
|
end
|
|
end
|
|
return nil if contexts.empty?
|
|
contexts.each do |context|
|
|
tools += context.context_external_tools.active
|
|
end
|
|
tools.sort_by(&:name)
|
|
end
|
|
|
|
# Order of precedence: Basic LTI defines precedence as first
|
|
# checking for a match on domain. Subdomains count as a match
|
|
# on less-specific domains, but the most-specific domain will
|
|
# match first. So awesome.bob.example.com matches an
|
|
# external_tool with example.com as the domain, but only if
|
|
# there isn't another external_tool where awesome.bob.example.com
|
|
# or bob.example.com is set as the domain.
|
|
#
|
|
# If there is no domain match then check for an exact url match
|
|
# as configured by an admin. If there is still no match
|
|
# then check for a match on the current context (configured by
|
|
# the teacher).
|
|
def self.find_external_tool(url, context)
|
|
url = ContextExternalTool.standardize_url(url)
|
|
account_contexts = []
|
|
other_contexts = []
|
|
while context
|
|
if context.is_a?(Group)
|
|
other_contexts << context
|
|
context = context.context || context.account
|
|
elsif context.is_a?(Course)
|
|
other_contexts << context
|
|
context = context.account
|
|
elsif context.is_a?(Account)
|
|
account_contexts << context
|
|
context = context.parent_account
|
|
else
|
|
context = nil
|
|
end
|
|
end
|
|
return nil if account_contexts.empty? && other_contexts.empty?
|
|
account_contexts.each do |context|
|
|
res = context.context_external_tools.active.sort_by(&:precedence).detect{|tool| tool.domain && tool.matches_url?(url) }
|
|
return res if res
|
|
end
|
|
account_contexts.each do |context|
|
|
res = context.context_external_tools.active.sort_by(&:precedence).detect{|tool| tool.matches_url?(url) }
|
|
return res if res
|
|
end
|
|
other_contexts.reverse.each do |context|
|
|
res = context.context_external_tools.active.sort_by(&:precedence).detect{|tool| tool.matches_url?(url) }
|
|
return res if res
|
|
end
|
|
nil
|
|
end
|
|
|
|
named_scope :having_setting, lambda{|setting|
|
|
{:conditions => {"has_#{setting.to_s}" => true} }
|
|
}
|
|
|
|
def self.find_for(id, context, type)
|
|
tool = context.context_external_tools.having_setting(type).find_by_id(id)
|
|
if !tool && context.is_a?(Group)
|
|
context = context.context
|
|
tool = context.context_external_tools.having_setting(type).find_by_id(id)
|
|
end
|
|
if !tool
|
|
account_ids = context.account_chain_ids
|
|
tool = ContextExternalTool.having_setting(type).find_by_context_type_and_context_id_and_id('Account', account_ids, id)
|
|
end
|
|
raise ActiveRecord::RecordNotFound if !tool
|
|
tool
|
|
end
|
|
named_scope :active, :conditions => ['context_external_tools.workflow_state != ?', 'deleted']
|
|
|
|
def self.find_all_for(context, type)
|
|
tools = []
|
|
if !context.is_a?(Account) && context.respond_to?(:context_external_tools)
|
|
tools += context.context_external_tools.having_setting(type.to_s)
|
|
end
|
|
account_ids = context.account_chain_ids
|
|
tools += ContextExternalTool.having_setting(type.to_s).find_all_by_context_type_and_context_id('Account', account_ids)
|
|
end
|
|
|
|
def self.serialization_excludes; [:shared_secret,:settings]; end
|
|
|
|
def self.process_migration(data, migration)
|
|
tools = data['external_tools'] ? data['external_tools']: []
|
|
to_import = migration.to_import 'external_tools'
|
|
tools.each do |tool|
|
|
if tool['migration_id'] && (!to_import || to_import[tool['migration_id']])
|
|
item = import_from_migration(tool, migration.context)
|
|
migration.add_warning(t('external_tool_attention_needed', 'The security parameters for the external tool "%{tool_name}" need to be set in Course Settings.', :tool_name => item.name))
|
|
end
|
|
end
|
|
end
|
|
|
|
def set_custom_fields(hash, resource_type)
|
|
fields = resource_type ? settings[resource_type.to_sym][:custom_fields] : settings[:custom_fields]
|
|
(fields || {}).each do |key, val|
|
|
key = key.gsub(/[^\w]/, '_').downcase
|
|
if key.match(/^custom_/)
|
|
hash[key] = val
|
|
else
|
|
hash["custom_#{key}"] = val
|
|
end
|
|
end
|
|
end
|
|
|
|
def clone_for(context, dup=nil, options={})
|
|
if !self.cloned_item && !self.new_record?
|
|
self.cloned_item = ClonedItem.create(:original_item => self)
|
|
self.save!
|
|
end
|
|
existing = ContextExternalTool.active.find_by_context_type_and_context_id_and_id(context.class.to_s, context.id, self.id)
|
|
existing ||= ContextExternalTool.active.find_by_context_type_and_context_id_and_cloned_item_id(context.class.to_s, context.id, self.cloned_item_id)
|
|
return existing if existing && !options[:overwrite]
|
|
new_tool = existing
|
|
new_tool ||= ContextExternalTool.new
|
|
new_tool.context = context
|
|
new_tool.settings = self.settings.clone
|
|
[:name, :shared_secret, :url, :domain, :consumer_key, :workflow_state, :description].each do |att|
|
|
new_tool.write_attribute(att, self.read_attribute(att))
|
|
end
|
|
new_tool.cloned_item_id = self.cloned_item_id
|
|
|
|
new_tool
|
|
end
|
|
|
|
def resource_selection_settings
|
|
settings[:resource_selection]
|
|
end
|
|
|
|
def self.import_from_migration(hash, context, item=nil)
|
|
hash = hash.with_indifferent_access
|
|
return nil if hash[:migration_id] && hash[:external_tools_to_import] && !hash[:external_tools_to_import][hash[:migration_id]]
|
|
item ||= find_by_context_id_and_context_type_and_migration_id(context.id, context.class.to_s, hash[:migration_id]) if hash[:migration_id]
|
|
item ||= context.context_external_tools.new
|
|
item.migration_id = hash[:migration_id]
|
|
item.name = hash[:title]
|
|
item.description = hash[:description]
|
|
item.url = hash[:url] unless hash[:url].blank?
|
|
item.domain = hash[:domain] unless hash[:domain].blank?
|
|
item.privacy_level = hash[:privacy_level] || 'name_only'
|
|
item.consumer_key = 'fake'
|
|
item.shared_secret = 'fake'
|
|
item.settings = hash[:settings].with_indifferent_access if hash[:settings].is_a?(Hash)
|
|
if hash[:custom_fields].is_a? Hash
|
|
item.settings[:custom_fields] ||= {}
|
|
item.settings[:custom_fields].merge! hash[:custom_fields]
|
|
end
|
|
if hash[:extensions].is_a? Array
|
|
item.settings[:vendor_extensions] ||= []
|
|
hash[:extensions].each do |ext|
|
|
next unless ext[:custom_fields].is_a? Hash
|
|
if existing = item.settings[:vendor_extensions].find { |ve| ve[:platform] == ext[:platform] }
|
|
existing[:custom_fields] ||= {}
|
|
existing[:custom_fields].merge! ext[:custom_fields]
|
|
else
|
|
item.settings[:vendor_extensions] << {:platform => ext[:platform], :custom_fields => ext[:custom_fields]}
|
|
end
|
|
end
|
|
end
|
|
|
|
item.save!
|
|
context.imported_migration_items << item if context.imported_migration_items && item.new_record?
|
|
item
|
|
end
|
|
|
|
private
|
|
|
|
def tool_setting(setting, hash, *keys)
|
|
if !hash || !hash.is_a?(Hash)
|
|
settings.delete setting
|
|
return
|
|
else
|
|
settings[setting] = {}
|
|
end
|
|
|
|
settings[setting][:url] = hash[:url]
|
|
settings[setting][:text] = hash[:text] if hash[:text]
|
|
keys.each { |key| settings[setting][key] = hash[key] }
|
|
|
|
# if the type needs to do some validations for specific keys
|
|
yield settings[setting] if block_given?
|
|
|
|
settings[setting]
|
|
end
|
|
|
|
end
|