197 lines
6.3 KiB
Ruby
197 lines
6.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#
|
|
# Copyright (C) 2011 - present Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
require "nokogiri"
|
|
|
|
module CC::Importer
|
|
class BLTIConverter
|
|
class CCImportError < RuntimeError; end
|
|
include CC::Importer
|
|
|
|
def get_blti_resources(manifest)
|
|
blti_resources = []
|
|
|
|
manifest.css("resource[type=#{BASIC_LTI}]").each do |r_node|
|
|
res = {}
|
|
res[:migration_id] = r_node["identifier"]
|
|
res[:href] = r_node["href"]
|
|
res[:files] = []
|
|
r_node.css("file").each do |file_node|
|
|
res[:files] << { href: file_node[:href] }
|
|
end
|
|
|
|
blti_resources << res
|
|
end
|
|
|
|
blti_resources
|
|
end
|
|
|
|
def convert_blti_links(blti_resources, converter)
|
|
tools = []
|
|
|
|
blti_resources.each do |res|
|
|
path = res[:href] || (res[:files]&.first && res[:files].first[:href])
|
|
path = converter.get_full_path(path)
|
|
|
|
next unless File.exist?(path)
|
|
|
|
doc = open_file_xml(path)
|
|
tool = convert_blti_link(doc)
|
|
tool[:migration_id] = res[:migration_id]
|
|
res[:url] = tool[:url] # for the organization item to reference
|
|
|
|
tools << tool
|
|
end
|
|
|
|
tools
|
|
end
|
|
|
|
def convert_blti_link(doc)
|
|
blti = get_blti_namespace(doc)
|
|
blti = nil unless doc.namespaces["xmlns:#{blti}"]
|
|
link_css_path = "cartridge_basiclti_link"
|
|
tool = {}
|
|
tool[:description] = get_node_val(doc, "#{link_css_path} > #{blti}|description")&.strip
|
|
tool[:title] = get_node_val(doc, "#{link_css_path} > #{blti}|title")&.strip
|
|
tool[:url] = get_node_val(doc, "#{link_css_path} > #{blti}|secure_launch_url")
|
|
tool[:url] ||= get_node_val(doc, "#{link_css_path} > #{blti}|launch_url")
|
|
tool[:url] &&= tool[:url].strip
|
|
if (custom_node = doc.css("#{link_css_path} > #{blti}|custom").first)
|
|
tool[:custom_fields] = get_custom_properties(custom_node)
|
|
end
|
|
tool[:custom_fields] ||= {}
|
|
|
|
doc.css("#{link_css_path} > #{blti}|extensions").each do |extension|
|
|
tool[:extensions] ||= []
|
|
ext = {}
|
|
ext[:platform] = extension["platform"]
|
|
ext[:custom_fields] = get_custom_properties(extension)
|
|
|
|
if ext[:platform] == CANVAS_PLATFORM
|
|
tool[:privacy_level] = ext[:custom_fields].delete "privacy_level"
|
|
tool[:not_selectable] = ext[:custom_fields].delete "not_selectable"
|
|
tool[:domain] = ext[:custom_fields].delete "domain"
|
|
tool[:consumer_key] = ext[:custom_fields].delete "consumer_key"
|
|
tool[:shared_secret] = ext[:custom_fields].delete "shared_secret"
|
|
tool[:tool_id] = ext[:custom_fields].delete "tool_id"
|
|
tool[:lti_version] = ext[:custom_fields].delete "lti_version"
|
|
if (tool[:assignment_points_possible] = ext[:custom_fields].delete("outcome"))
|
|
tool[:assignment_points_possible] = tool[:assignment_points_possible].to_f
|
|
end
|
|
tool[:settings] = ext[:custom_fields]
|
|
else
|
|
tool[:extensions] << ext
|
|
end
|
|
end
|
|
if (icon = get_node_val(doc, "#{link_css_path} > #{blti}|icon"))
|
|
tool[:settings] ||= {}
|
|
tool[:settings][:icon_url] = icon.strip
|
|
end
|
|
tool
|
|
end
|
|
|
|
def convert_blti_xml(xml)
|
|
doc = create_xml_doc(xml)
|
|
unless doc.namespaces.to_s.downcase.include? "imsglobal"
|
|
raise CCImportError, I18n.t("Invalid XML Configuration")
|
|
end
|
|
|
|
begin
|
|
tool = convert_blti_link(doc)
|
|
check_for_unescaped_url_properties(tool) if tool
|
|
rescue Nokogiri::XML::XPath::SyntaxError
|
|
raise CCImportError, I18n.t(:invalid_xml_syntax, "Invalid xml syntax")
|
|
end
|
|
tool
|
|
end
|
|
|
|
def check_for_unescaped_url_properties(obj)
|
|
# Recursively look for properties named 'url'
|
|
case obj
|
|
when Hash
|
|
obj.select { |k, v| k.to_s == "url" && v.is_a?(String) }
|
|
.each_value { |v| check_for_unescaped_url(v) }
|
|
obj.each_value { |v| check_for_unescaped_url_properties(v) }
|
|
when Array
|
|
obj.each { |o| check_for_unescaped_url_properties(o) }
|
|
end
|
|
end
|
|
|
|
def check_for_unescaped_url(url)
|
|
if /(.*[^=]*\?*=)[^&;]*=/.match?(url)
|
|
raise CCImportError, I18n.t(:invalid_url_in_xml, "Invalid url in xml. Ampersands must be escaped.")
|
|
end
|
|
end
|
|
|
|
def retrieve_and_convert_blti_url(url)
|
|
response = CanvasHttp.get(url, redirect_limit: 10)
|
|
config_xml = response.body
|
|
convert_blti_xml(config_xml)
|
|
rescue Timeout::Error
|
|
raise CCImportError, I18n.t(:retrieve_timeout, "could not retrieve configuration, the server response timed out")
|
|
end
|
|
|
|
def get_custom_properties(node)
|
|
props = {}
|
|
node.children.each do |property|
|
|
next if property.name == "text"
|
|
|
|
case property.name
|
|
when "property"
|
|
props[property["name"]] = property.text.strip
|
|
when "options"
|
|
props[property["name"]] = get_custom_properties(property)
|
|
when "custom"
|
|
props[:custom_fields] = get_custom_properties(property)
|
|
end
|
|
end
|
|
props
|
|
end
|
|
|
|
def get_blti_namespace(doc)
|
|
doc.namespaces.each_pair do |key, val|
|
|
if val == BLTI_NAMESPACE
|
|
return key.gsub("xmlns:", "")
|
|
end
|
|
end
|
|
"blti"
|
|
end
|
|
|
|
def create_assignments_from_lti_links(lti_tools)
|
|
asmnts = []
|
|
|
|
lti_tools.each do |tool|
|
|
next unless tool[:assignment_points_possible]
|
|
|
|
asmnt = { migration_id: tool[:migration_id] }
|
|
asmnt[:title] = tool[:title]
|
|
asmnt[:description] = tool[:description]
|
|
asmnt[:submission_format] = "external_tool"
|
|
asmnt[:external_tool_url] = tool[:url]
|
|
asmnt[:grading_type] = "points"
|
|
asmnt[:points_possible] = tool[:assignment_points_possible]
|
|
asmnts << asmnt
|
|
end
|
|
|
|
asmnts
|
|
end
|
|
end
|
|
end
|