canvas-lms/lib/cc/importer/blti_converter.rb

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