826 lines
33 KiB
Ruby
826 lines
33 KiB
Ruby
#
|
|
# Copyright (C) 2011 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/>.
|
|
#
|
|
|
|
# @API External Tools
|
|
# API for accessing and configuring external tools on accounts and courses.
|
|
# "External tools" are IMS LTI links: http://www.imsglobal.org/developers/LTI/index.cfm
|
|
class ExternalToolsController < ApplicationController
|
|
before_filter :require_context
|
|
before_filter :require_access_to_context, except: [:index, :sessionless_launch]
|
|
before_filter :require_user, only: [:generate_sessionless_launch]
|
|
before_filter :get_context, :only => [:retrieve, :show, :resource_selection]
|
|
include Api::V1::ExternalTools
|
|
|
|
REDIS_PREFIX = 'external_tool:sessionless_launch:'
|
|
|
|
# @API List external tools
|
|
# Returns the paginated list of external tools for the current context.
|
|
# See the get request docs for a single tool for a list of properties on an external tool.
|
|
#
|
|
# @argument search_term [String]
|
|
# The partial name of the tools to match and return.
|
|
#
|
|
# @argument selectable [Boolean]
|
|
# If true, then only tools that are meant to be selectable are returned
|
|
#
|
|
# @example_response
|
|
# [
|
|
# {
|
|
# "id":1,
|
|
# "name":"BLTI Example",
|
|
# "description":"This is for cool things"
|
|
# "url":"http://www.example.com/ims/lti",
|
|
# "domain":null,
|
|
# "privacy_level":anonymous
|
|
# "consumer_key":null,
|
|
# "created_at":"2037-07-21T13:29:31Z",
|
|
# "updated_at":"2037-07-28T19:38:31Z",
|
|
# "custom_fields":{"key":"value"},
|
|
# "account_navigation":{"url":"...", "text":"..."},
|
|
# "user_navigation":{"url":"...", "text":"..."},
|
|
# "course_navigation":{"url":"...", "text":"...", "visibility":"members", "default":true},
|
|
# "editor_button":{"url":"...", "text":"...", "selection_width":50, "selection_height":50, "icon_url":"..."},
|
|
# "resource_selection":{"url":"...", "text":"...", "selection_width":50, "selection_height":50}
|
|
# },
|
|
# {
|
|
# "id":2,
|
|
# "name":"Another BLTI Example",
|
|
# "description":"This one isn't very cool."
|
|
# "url":null,
|
|
# "domain":"example.com",
|
|
# "privacy_level":anonymous
|
|
# "consumer_key":null,
|
|
# "created_at":"2037-07-21T13:29:31Z",
|
|
# "updated_at":"2037-07-28T19:38:31Z"
|
|
# }
|
|
# ]
|
|
def index
|
|
if authorized_action(@context, @current_user, :update)
|
|
if params[:include_parents]
|
|
@tools = ContextExternalTool.all_tools_for(@context, :user => (params[:include_personal] ? @current_user : nil))
|
|
else
|
|
@tools = @context.context_external_tools.active
|
|
end
|
|
@tools = ContextExternalTool.search_by_attribute(@tools, :name, params[:search_term])
|
|
|
|
if Canvas::Plugin.value_to_boolean(params[:selectable])
|
|
@tools = @tools.select{|t| t.selectable }
|
|
end
|
|
respond_to do |format|
|
|
@tools = Api.paginate(@tools, self, tool_pagination_url)
|
|
format.json { render :json => external_tools_json(@tools, @context, @current_user, session) }
|
|
end
|
|
end
|
|
end
|
|
|
|
def homework_submissions
|
|
@tools = ContextExternalTool.all_tools_for(@context, :user => @current_user, :type => :has_homework_submission)
|
|
respond_to do |format|
|
|
format.json { render :json => external_tools_json(@tools, @context, @current_user, session) }
|
|
end
|
|
end
|
|
|
|
def finished
|
|
@headers = false
|
|
end
|
|
|
|
def retrieve
|
|
@tool = ContextExternalTool.find_external_tool(params[:url], @context)
|
|
if !@tool
|
|
flash[:error] = t "#application.errors.invalid_external_tool", "Couldn't find valid settings for this link"
|
|
redirect_to named_context_url(@context, :context_url)
|
|
return
|
|
end
|
|
add_crumb(@context.name, named_context_url(@context, :context_url))
|
|
|
|
@lti_launch = @tool.settings['post_only'] ? Lti::Launch.new(post_only: true) : Lti::Launch.new
|
|
|
|
opts = {
|
|
resource_type: @resource_type,
|
|
launch_url: params[:url]
|
|
}
|
|
adapter = Lti::LtiOutboundAdapter.new(@tool, @current_user, @context).prepare_tool_launch(url_for(@context), variable_expander, opts)
|
|
@lti_launch.params = adapter.generate_post_payload
|
|
|
|
@lti_launch.resource_url = params[:url]
|
|
@lti_launch.link_text = @tool.name
|
|
@lti_launch.analytics_id = @tool.tool_id
|
|
|
|
display_type = 'borderless' if params['borderless']
|
|
render Lti::AppUtil.display_template(display_type, display_override: params[:display])
|
|
end
|
|
|
|
# @API Get a sessionless launch url for an external tool.
|
|
# Returns a sessionless launch url for an external tool.
|
|
#
|
|
# Either the id or url must be provided.
|
|
#
|
|
# @argument id [String]
|
|
# The external id of the tool to launch.
|
|
#
|
|
# @argument url [String]
|
|
# The LTI launch url for the external tool.
|
|
#
|
|
# @argument assignment_id [String]
|
|
# The assignment id for an assignment launch.
|
|
#
|
|
# @argument launch_type [String]
|
|
# The type of launch to perform on the external tool.
|
|
#
|
|
# @response_field id The id for the external tool to be launched.
|
|
# @response_field name The name of the external tool to be launched.
|
|
# @response_field url The url to load to launch the external tool for the user.
|
|
def generate_sessionless_launch
|
|
# prerequisite checks
|
|
unless Canvas.redis_enabled?
|
|
@context.errors.add(:redis, 'Redis is not enabled, but is required for sessionless LTI launch')
|
|
render :json => @context.errors, :status => :service_unavailable
|
|
return
|
|
end
|
|
|
|
tool_id = params[:id]
|
|
launch_url = params[:url]
|
|
|
|
#extra permissions for assignments
|
|
assignment = nil
|
|
if params[:launch_type] == 'assessment'
|
|
unless params[:assignment_id]
|
|
@context.errors.add(:assignment_id, 'An assignment id must be provided for assessment LTI launch')
|
|
render :json => @context.errors, :status => :bad_request
|
|
return
|
|
end
|
|
|
|
assignment = @context.assignments.where(id: params[:assignment_id]).first
|
|
unless assignment
|
|
@context.errors.add(:assignment_id, 'The assignment was not found in this course')
|
|
render :json => @context.errors, :status => :bad_request
|
|
return
|
|
end
|
|
|
|
unless assignment.external_tool_tag
|
|
@context.errors.add(:assignment_id, 'The assignment must have an external tool tag')
|
|
render :json => @context.errors, :status => :bad_request
|
|
return
|
|
end
|
|
|
|
return unless authorized_action(assignment, @current_user, :read)
|
|
|
|
launch_url = assignment.external_tool_tag.url
|
|
end
|
|
|
|
unless tool_id || launch_url
|
|
@context.errors.add(:id, 'An id or a url must be provided')
|
|
@context.errors.add(:url, 'An id or a url must be provided')
|
|
render :json => @context.errors, :status => :bad_request
|
|
return
|
|
end
|
|
|
|
# locate the tool
|
|
if launch_url
|
|
@tool = ContextExternalTool.find_external_tool(launch_url, @context, tool_id)
|
|
else
|
|
return unless find_tool(tool_id, params[:launch_type])
|
|
end
|
|
if !@tool
|
|
flash[:error] = t "#application.errors.invalid_external_tool", "Couldn't find valid settings for this link"
|
|
redirect_to named_context_url(@context, :context_url)
|
|
return
|
|
end
|
|
|
|
# generate the launch
|
|
opts = {
|
|
launch_url: launch_url,
|
|
resource_type: params[:launch_type]
|
|
}
|
|
|
|
adapter = Lti::LtiOutboundAdapter.new(@tool, @current_user, @context).prepare_tool_launch(url_for(@context), variable_expander(assignment: assignment), opts)
|
|
|
|
launch_settings = {
|
|
'launch_url' => adapter.launch_url,
|
|
'tool_name' => @tool.name,
|
|
'analytics_id' => @tool.tool_id
|
|
}
|
|
|
|
if assignment
|
|
launch_settings['tool_settings'] = adapter.generate_post_payload_for_assignment(assignment, lti_grade_passback_api_url(@tool), blti_legacy_grade_passback_api_url(@tool), lti_turnitin_outcomes_placement_url(@tool.id))
|
|
else
|
|
launch_settings['tool_settings'] = adapter.generate_post_payload
|
|
end
|
|
|
|
# store the launch settings and return to the user
|
|
verifier = SecureRandom.hex(64)
|
|
Canvas.redis.setex("#{@context.class.name}:#{REDIS_PREFIX}#{verifier}", 5.minutes, launch_settings.to_json)
|
|
|
|
if @context.is_a?(Account)
|
|
uri = URI(account_external_tools_sessionless_launch_url(@context))
|
|
else
|
|
uri = URI(course_external_tools_sessionless_launch_url(@context))
|
|
end
|
|
uri.query = {:verifier => verifier}.to_query
|
|
|
|
render :json => {:id => @tool.id, :name => @tool.name, :url => uri.to_s}
|
|
end
|
|
|
|
def sessionless_launch
|
|
if Canvas.redis_enabled?
|
|
redis_key = "#{@context.class.name}:#{REDIS_PREFIX}#{params[:verifier]}"
|
|
launch_settings = Canvas.redis.get(redis_key)
|
|
Canvas.redis.del(redis_key)
|
|
end
|
|
unless launch_settings
|
|
render :text => t(:cannot_locate_launch_request, 'Cannot locate launch request, please try again.'), :status => :not_found
|
|
return
|
|
end
|
|
|
|
launch_settings = JSON.parse(launch_settings)
|
|
|
|
@lti_launch = launch_settings['tool_settings']['post_only'] ? Lti::Launch.new(post_only: true) : Lti::Launch.new
|
|
@lti_launch.params = launch_settings['tool_settings']
|
|
@lti_launch.resource_url = launch_settings['launch_url']
|
|
@lti_launch.link_text = launch_settings['tool_name']
|
|
@lti_launch.analytics_id = launch_settings['analytics_id']
|
|
|
|
render Lti::AppUtil.display_template('borderless')
|
|
end
|
|
|
|
# @API Get a single external tool
|
|
# Returns the specified external tool.
|
|
#
|
|
# @response_field id The unique identifier for the tool
|
|
# @response_field name The name of the tool
|
|
# @response_field description A description of the tool
|
|
# @response_field url The url to match links against
|
|
# @response_field domain The domain to match links against
|
|
# @response_field privacy_level What information to send to the external tool, "anonymous", "name_only", "public"
|
|
# @response_field consumer_key The consumer key used by the tool (The associated shared secret is not returned)
|
|
# @response_field created_at Timestamp of creation
|
|
# @response_field updated_at Timestamp of last update
|
|
# @response_field custom_fields Custom fields that will be sent to the tool consumer
|
|
# @response_field account_navigation The configuration for account navigation links (see create API for values)
|
|
# @response_field user_navigation The configuration for user navigation links (see create API for values)
|
|
# @response_field course_navigation The configuration for course navigation links (see create API for values)
|
|
# @response_field editor_button The configuration for a WYSIWYG editor button (see create API for values)
|
|
# @response_field resource_selection The configuration for a resource selector in modules (see create API for values)
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "id":1,
|
|
# "name":"BLTI Example",
|
|
# "description":"This is for cool things"
|
|
# "url":"http://www.example.com/ims/lti",
|
|
# "domain":null,
|
|
# "privacy_level":anonymous
|
|
# "consumer_key":null,
|
|
# "created_at":"2037-07-21T13:29:31Z",
|
|
# "updated_at":"2037-07-28T19:38:31Z",
|
|
# "custom_fields":{"key":"value"},
|
|
# "account_navigation":{"url":"...", "text":"..."},
|
|
# "user_navigation":{"url":"...", "text":"..."},
|
|
# "course_navigation":{"url":"...", "text":"...", "visibility":"members", "default":true},
|
|
# "editor_button":{"url":"...", "selection_width":50, "selection_height":50, "icon_url":"..."},
|
|
# "resource_selection":{"url":"...", "selection_width":50, "selection_height":50}
|
|
# }
|
|
def show
|
|
if api_request?
|
|
if tool = @context.context_external_tools.active.where(id: params[:external_tool_id]).first
|
|
render :json => external_tool_json(tool, @context, @current_user, session)
|
|
else
|
|
raise(ActiveRecord::RecordNotFound, "Couldn't find external tool with API id '#{params[:external_tool_id]}'")
|
|
end
|
|
else
|
|
placement = params[:placement] || params[:launch_type] || "#{@context.class.base_class.to_s.downcase}_navigation"
|
|
if find_tool(params[:id], placement)
|
|
|
|
log_asset_access(@tool, "external_tools", "external_tools")
|
|
|
|
@return_url = named_context_url(@context, :context_external_content_success_url, 'external_tool_redirect', {include_host: true})
|
|
@redirect_return = true
|
|
|
|
success_url = tool_return_success_url(placement)
|
|
cancel_url = tool_return_cancel_url(placement) || success_url
|
|
js_env(:redirect_return_success_url => success_url,
|
|
:redirect_return_cancel_url => cancel_url)
|
|
js_env(:course_id => @context.id) if @context.is_a?(Course)
|
|
|
|
@active_tab = @tool.asset_string
|
|
@show_embedded_chat = false if @tool.tool_id == 'chat'
|
|
|
|
@lti_launch = lti_launch(@tool, placement)
|
|
render Lti::AppUtil.display_template(@tool.display_type(placement), display_override: params[:display])
|
|
end
|
|
add_crumb(@context.name, named_context_url(@context, :context_url))
|
|
end
|
|
end
|
|
|
|
def tool_return_success_url(selection_type=nil)
|
|
case @context
|
|
when Course
|
|
case selection_type
|
|
when "course_settings_sub_navigation"
|
|
course_settings_url(@context)
|
|
else
|
|
course_url(@context)
|
|
end
|
|
when Account
|
|
case selection_type
|
|
when "global_navigation"
|
|
dashboard_url
|
|
else
|
|
account_url(@context)
|
|
end
|
|
else
|
|
dashboard_url
|
|
end
|
|
end
|
|
|
|
def tool_return_cancel_url(selection_type)
|
|
case @context
|
|
when Course
|
|
if selection_type == "course_home_sub_navigation"
|
|
course_url(@context)
|
|
end
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def resource_selection
|
|
add_crumb(@context.name, named_context_url(@context, :context_url))
|
|
placement = params[:placement] || params[:launch_type]
|
|
selection_type = placement || 'resource_selection'
|
|
selection_type = 'editor_button' if params[:editor]
|
|
selection_type = 'homework_submission' if params[:homework]
|
|
|
|
@return_url = named_context_url(@context, :context_external_content_success_url, 'external_tool_dialog', {include_host: true})
|
|
@headers = false
|
|
|
|
tool = find_tool(params[:external_tool_id], selection_type)
|
|
if tool
|
|
@lti_launch = lti_launch(@tool, selection_type)
|
|
render Lti::AppUtil.display_template('borderless')
|
|
end
|
|
end
|
|
|
|
def find_tool(id, selection_type)
|
|
if selection_type.nil? || ContextExternalTool::EXTENSION_TYPES.include?(selection_type.to_sym)
|
|
@tool = ContextExternalTool.find_for(id, @context, selection_type, false)
|
|
end
|
|
|
|
if !@tool
|
|
flash[:error] = t "#application.errors.invalid_external_tool_id", "Couldn't find valid settings for this tool"
|
|
redirect_to named_context_url(@context, :context_url)
|
|
end
|
|
|
|
@tool
|
|
end
|
|
protected :find_tool
|
|
|
|
def lti_launch(tool, selection_type = nil)
|
|
@return_url ||= url_for(@context)
|
|
message_type = tool.extension_setting(selection_type, 'message_type') if selection_type
|
|
case message_type
|
|
when 'ContentItemSelectionResponse', 'ContentItemSelection'
|
|
#ContentItemSelectionResponse is deprecated, use ContentItemSelection instead
|
|
content_item_selection(tool, selection_type, create_content_item_response, message_type)
|
|
when 'ContentItemSelectionRequest'
|
|
content_item_selection_request(tool, selection_type)
|
|
else
|
|
basic_lti_launch_request(tool, selection_type)
|
|
end
|
|
end
|
|
protected :lti_launch
|
|
|
|
def basic_lti_launch_request(tool, selection_type = nil)
|
|
lti_launch = tool.settings['post_only'] ? Lti::Launch.new(post_only: true) : Lti::Launch.new
|
|
|
|
opts = {
|
|
resource_type: selection_type,
|
|
selected_html: params[:selection]
|
|
}
|
|
assignment = selection_type == 'homework_submission' && @context.assignments.active.find(params[:assignment_id])
|
|
|
|
adapter = Lti::LtiOutboundAdapter.new(tool, @current_user, @context).prepare_tool_launch(@return_url, variable_expander(assignment: assignment, tool: tool), opts)
|
|
lti_launch.params = assignment ? adapter.generate_post_payload_for_homework_submission(assignment) : adapter.generate_post_payload
|
|
|
|
lti_launch.resource_url = adapter.launch_url
|
|
lti_launch.link_text = selection_type ? tool.label_for(selection_type.to_sym, I18n.locale) : tool.default_label
|
|
lti_launch.analytics_id = tool.tool_id
|
|
lti_launch
|
|
end
|
|
protected :basic_lti_launch_request
|
|
|
|
def content_item_selection(tool, placement, content_item_response, message_type)
|
|
params = default_lti_params.merge(
|
|
{
|
|
#required params
|
|
lti_message_type: message_type,
|
|
lti_version: 'LTI-1p0',
|
|
resource_link_id: Lti::Asset.opaque_identifier_for(@context),
|
|
content_items: content_item_response.to_json(lti_message_type: message_type),
|
|
launch_presentation_return_url: @return_url,
|
|
context_title: @context.name,
|
|
tool_consumer_instance_name: @domain_root_account.name,
|
|
tool_consumer_instance_contact_email: HostUrl.outgoing_email_address,
|
|
}).merge(variable_expander(tool: tool, attachment: content_item_response.file).expand_variables!(tool.set_custom_fields(placement)))
|
|
|
|
lti_launch = @tool.settings['post_only'] ? Lti::Launch.new(post_only: true) : Lti::Launch.new
|
|
lti_launch.resource_url = tool.extension_setting(placement, :url)
|
|
lti_launch.params = LtiOutbound::ToolLaunch.generate_params(params, lti_launch.resource_url, tool.consumer_key, tool.shared_secret,
|
|
disable_lti_post_only: @context.root_account.feature_enabled?(:disable_lti_post_only))
|
|
lti_launch.link_text = tool.label_for(placement.to_sym)
|
|
lti_launch.analytics_id = tool.tool_id
|
|
|
|
lti_launch
|
|
end
|
|
protected :content_item_selection
|
|
|
|
def create_content_item_response
|
|
media_types = params.select { |param| Lti::ContentItemResponse::MEDIA_TYPES.include? param.to_sym }
|
|
begin
|
|
Lti::ContentItemResponse.new(@context, self, @current_user, media_types, params["export_type"])
|
|
rescue Lti::UnauthorizedError
|
|
render_unauthorized_action
|
|
rescue Lti::UnsupportedExportTypeError
|
|
#Legacy API behavior does nothing if the export type is unsupported
|
|
end
|
|
end
|
|
|
|
protected :create_content_item_response
|
|
|
|
# Do an official content-item request as specified: http://www.imsglobal.org/LTI/services/ltiCIv1p0pd/ltiCIv1p0pd.html
|
|
def content_item_selection_request(tool, placement)
|
|
extra_params = {}
|
|
return_url = named_context_url(@context, :context_external_content_success_url, 'external_tool_dialog', {include_host: true})
|
|
|
|
# choose accepted return types based on placement
|
|
# todo, make return types configurable at installation?
|
|
case placement
|
|
when 'migration_selection'
|
|
accept_media_types = 'application/vnd.ims.imsccv1p1,application/vnd.ims.imsccv1p2,application/vnd.ims.imsccv1p3,application/zip,application/xml'
|
|
accept_presentation_document_targets = 'download'
|
|
extra_params[:accept_copy_advice] = true
|
|
extra_params[:ext_content_file_extensions] = 'zip,imscc,mbz,xml'
|
|
when 'editor_button'
|
|
accept_media_types = 'image/*,text/html,application/vnd.ims.lti.v1.ltilink,*/*'
|
|
accept_presentation_document_targets = 'embed,frame,iframe,window'
|
|
when 'resource_selection', 'link_selection', 'assignment_selection'
|
|
accept_media_types = 'application/vnd.ims.lti.v1.ltilink'
|
|
accept_presentation_document_targets = 'frame,window'
|
|
when 'homework_submission'
|
|
assignment = @context.assignments.active.find(params[:assignment_id])
|
|
accept_media_types = '*/*'
|
|
accept_presentation_document_targets = 'none'
|
|
extra_params[:accept_copy_advice] = true
|
|
accept_media_types = assignment.allowed_extensions.map { |ext| MimetypeFu::EXTENSIONS[ext] }.compact.join(',') if assignment.allowed_extensions.present?
|
|
else
|
|
# todo: we _could_, if configured, have any other placements return to the content migration page...
|
|
raise "Content-Item not supported at this placement"
|
|
end
|
|
|
|
params = default_lti_params.merge({
|
|
#required params
|
|
lti_message_type: 'ContentItemSelectionRequest',
|
|
lti_version: 'LTI-1p0',
|
|
accept_media_types: accept_media_types,
|
|
accept_presentation_document_targets: accept_presentation_document_targets,
|
|
content_item_return_url: return_url,
|
|
#optional params
|
|
accept_multiple: false,
|
|
accept_unsigned: true,
|
|
auto_create: false,
|
|
context_title: @context.name,
|
|
}).merge(extra_params).merge(variable_expander(tool:tool).expand_variables!(tool.set_custom_fields(placement)))
|
|
|
|
lti_launch = @tool.settings['post_only'] ? Lti::Launch.new(post_only: true) : Lti::Launch.new
|
|
lti_launch.resource_url = tool.extension_setting(placement, :url)
|
|
lti_launch.params = LtiOutbound::ToolLaunch.generate_params(params, lti_launch.resource_url, tool.consumer_key, tool.shared_secret,
|
|
disable_lti_post_only: @context.root_account.feature_enabled?(:disable_lti_post_only))
|
|
lti_launch.link_text = tool.label_for(placement.to_sym, I18n.locale)
|
|
lti_launch.analytics_id = tool.tool_id
|
|
|
|
lti_launch
|
|
end
|
|
protected :content_item_selection_request
|
|
|
|
# @API Create an external tool
|
|
# Create an external tool in the specified course/account.
|
|
# The created tool will be returned, see the "show" endpoint for an example.
|
|
#
|
|
# @argument name [Required, String]
|
|
# The name of the tool
|
|
#
|
|
# @argument privacy_level [Required, String, "anonymous"|"name_only"|"public"]
|
|
# What information to send to the external tool.
|
|
#
|
|
# @argument consumer_key [Required, String]
|
|
# The consumer key for the external tool
|
|
#
|
|
# @argument shared_secret [Required, String]
|
|
# The shared secret with the external tool
|
|
#
|
|
# @argument description [String]
|
|
# A description of the tool
|
|
#
|
|
# @argument url [String]
|
|
# The url to match links against. Either "url" or "domain" should be set,
|
|
# not both.
|
|
#
|
|
# @argument domain [String]
|
|
# The domain to match links against. Either "url" or "domain" should be
|
|
# set, not both.
|
|
#
|
|
# @argument icon_url [String]
|
|
# The url of the icon to show for this tool
|
|
#
|
|
# @argument text [String]
|
|
# The default text to show for this tool
|
|
#
|
|
# @argument not_selectable [Boolean]
|
|
# Default: false, if set to true the tool won't show up in the external tool
|
|
# selection UI in modules and assignments
|
|
#
|
|
# @argument custom_fields [String]
|
|
# Custom fields that will be sent to the tool consumer, specified as
|
|
# custom_fields[field_name]
|
|
#
|
|
# @argument account_navigation[url] [String]
|
|
# The url of the external tool for account navigation
|
|
#
|
|
# @argument account_navigation[enabled] [Boolean]
|
|
# Set this to enable this feature
|
|
#
|
|
# @argument account_navigation[text] [String]
|
|
# The text that will show on the left-tab in the account navigation
|
|
#
|
|
# @argument user_navigation[url] [String]
|
|
# The url of the external tool for user navigation
|
|
#
|
|
# @argument user_navigation[enabled] [Boolean]
|
|
# Set this to enable this feature
|
|
#
|
|
# @argument user_navigation[text] [String]
|
|
# The text that will show on the left-tab in the user navigation
|
|
#
|
|
# @argument course_navigation[url] [String]
|
|
# The url of the external tool for course navigation
|
|
#
|
|
# @argument course_navigation[enabled] [Boolean]
|
|
# Set this to enable this feature
|
|
#
|
|
# @argument course_navigation[text] [String]
|
|
# The text that will show on the left-tab in the course navigation
|
|
#
|
|
# @argument course_navigation[visibility] [String, "admins"|"members"]
|
|
# Who will see the navigation tab. "admins" for course admins, "members" for
|
|
# students, null for everyone
|
|
#
|
|
# @argument course_navigation[default] [Boolean]
|
|
# Whether the navigation option will show in the course by default or
|
|
# whether the teacher will have to explicitly enable it
|
|
#
|
|
# @argument editor_button[url] [String]
|
|
# The url of the external tool
|
|
#
|
|
# @argument editor_button[enabled] [Boolean]
|
|
# Set this to enable this feature
|
|
#
|
|
# @argument editor_button[icon_url] [String]
|
|
# The url of the icon to show in the WYSIWYG editor
|
|
#
|
|
# @argument editor_button[selection_width] [String]
|
|
# The width of the dialog the tool is launched in
|
|
#
|
|
# @argument editor_button[selection_height] [String]
|
|
# The height of the dialog the tool is launched in
|
|
#
|
|
# @argument resource_selection[url] [String]
|
|
# The url of the external tool
|
|
#
|
|
# @argument resource_selection[enabled] [Boolean]
|
|
# Set this to enable this feature
|
|
#
|
|
# @argument resource_selection[icon_url] [String]
|
|
# The url of the icon to show in the module external tool list
|
|
#
|
|
# @argument resource_selection[selection_width] [String]
|
|
# The width of the dialog the tool is launched in
|
|
#
|
|
# @argument resource_selection[selection_height] [String]
|
|
# The height of the dialog the tool is launched in
|
|
#
|
|
# @argument config_type [String]
|
|
# Configuration can be passed in as CC xml instead of using query
|
|
# parameters. If this value is "by_url" or "by_xml" then an xml
|
|
# configuration will be expected in either the "config_xml" or "config_url"
|
|
# parameter. Note that the name parameter overrides the tool name provided
|
|
# in the xml
|
|
#
|
|
# @argument config_xml [String]
|
|
# XML tool configuration, as specified in the CC xml specification. This is
|
|
# required if "config_type" is set to "by_xml"
|
|
#
|
|
# @argument config_url [String]
|
|
# URL where the server can retrieve an XML tool configuration, as specified
|
|
# in the CC xml specification. This is required if "config_type" is set to
|
|
# "by_url"
|
|
#
|
|
# @example_request
|
|
#
|
|
# This would create a tool on this course with two custom fields and a course navigation tab
|
|
# curl 'https://<canvas>/api/v1/courses/<course_id>/external_tools' \
|
|
# -H "Authorization: Bearer <token>" \
|
|
# -F 'name=LTI Example' \
|
|
# -F 'consumer_key=asdfg' \
|
|
# -F 'shared_secret=lkjh' \
|
|
# -F 'url=https://example.com/ims/lti' \
|
|
# -F 'privacy_level=name_only' \
|
|
# -F 'custom_fields[key1]=value1' \
|
|
# -F 'custom_fields[key2]=value2' \
|
|
# -F 'course_navigation[text]=Course Materials' \
|
|
# -F 'course_navigation[default]=false'
|
|
# -F 'course_navigation[enabled]=true'
|
|
#
|
|
# @example_request
|
|
#
|
|
# This would create a tool on the account with navigation for the user profile page
|
|
# curl 'https://<canvas>/api/v1/accounts/<account_id>/external_tools' \
|
|
# -H "Authorization: Bearer <token>" \
|
|
# -F 'name=LTI Example' \
|
|
# -F 'consumer_key=asdfg' \
|
|
# -F 'shared_secret=lkjh' \
|
|
# -F 'url=https://example.com/ims/lti' \
|
|
# -F 'privacy_level=name_only' \
|
|
# -F 'user_navigation[url]=https://example.com/ims/lti/user_endpoint' \
|
|
# -F 'user_navigation[text]=Something Cool'
|
|
# -F 'user_navigation[enabled]=true'
|
|
#
|
|
# @example_request
|
|
#
|
|
# This would create a tool on the account with configuration pulled from an external URL
|
|
# curl 'https://<canvas>/api/v1/accounts/<account_id>/external_tools' \
|
|
# -H "Authorization: Bearer <token>" \
|
|
# -F 'name=LTI Example' \
|
|
# -F 'consumer_key=asdfg' \
|
|
# -F 'shared_secret=lkjh' \
|
|
# -F 'config_type=by_url' \
|
|
# -F 'config_url=https://example.com/ims/lti/tool_config.xml'
|
|
def create
|
|
if authorized_action(@context, @current_user, :update)
|
|
@tool = @context.context_external_tools.new
|
|
set_tool_attributes(@tool, params[:external_tool] || params)
|
|
respond_to do |format|
|
|
if @tool.save
|
|
invalidate_nav_tabs_cache(@tool)
|
|
if api_request?
|
|
format.json { render :json => external_tool_json(@tool, @context, @current_user, session) }
|
|
else
|
|
format.json { render :json => @tool.as_json(:methods => [:readable_state, :custom_fields_string, :vendor_help_link], :include_root => false) }
|
|
end
|
|
else
|
|
format.json { render :json => @tool.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Edit an external tool
|
|
# Update the specified external tool. Uses same parameters as create
|
|
#
|
|
# @example_request
|
|
#
|
|
# This would update the specified keys on this external tool
|
|
# curl -X PUT 'https://<canvas>/api/v1/courses/<course_id>/external_tools/<external_tool_id>' \
|
|
# -H "Authorization: Bearer <token>" \
|
|
# -F 'name=Public Example' \
|
|
# -F 'privacy_level=public'
|
|
def update
|
|
@tool = @context.context_external_tools.active.find(params[:id] || params[:external_tool_id])
|
|
if authorized_action(@tool, @current_user, :update)
|
|
respond_to do |format|
|
|
set_tool_attributes(@tool, params[:external_tool] || params)
|
|
if @tool.save
|
|
invalidate_nav_tabs_cache(@tool)
|
|
if api_request?
|
|
format.json { render :json => external_tool_json(@tool, @context, @current_user, session) }
|
|
else
|
|
format.json { render :json => @tool.as_json(:methods => [:readable_state, :custom_fields_string], :include_root => false) }
|
|
end
|
|
else
|
|
format.json { render :json => @tool.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Delete an external tool
|
|
# Remove the specified external tool
|
|
#
|
|
# @example_request
|
|
#
|
|
# This would delete the specified external tool
|
|
# curl -X DELETE 'https://<canvas>/api/v1/courses/<course_id>/external_tools/<external_tool_id>' \
|
|
# -H "Authorization: Bearer <token>"
|
|
def destroy
|
|
@tool = @context.context_external_tools.active.find(params[:id] || params[:external_tool_id])
|
|
if authorized_action(@tool, @current_user, :delete)
|
|
respond_to do |format|
|
|
if @tool.destroy
|
|
if api_request?
|
|
invalidate_nav_tabs_cache(@tool)
|
|
format.json { render :json => external_tool_json(@tool, @context, @current_user, session) }
|
|
else
|
|
format.json { render :json => @tool.as_json(:methods => [:readable_state, :custom_fields_string], :include_root => false) }
|
|
end
|
|
else
|
|
format.json { render :json => @tool.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def jwt_token
|
|
tool = ContextExternalTool.find(params['tool_id']) if params['tool_id']
|
|
|
|
if params['tool_launch_url'] && tool.nil?
|
|
tool = ContextExternalTool.find_external_tool(params['tool_launch_url'], @context)
|
|
end
|
|
|
|
raise ActiveRecord::RecordNotFound if tool.nil?
|
|
|
|
launch = lti_launch(tool)
|
|
params = launch.params.reject {|p| p.starts_with?('oauth_')}
|
|
params[:consumer_key] = tool.consumer_key
|
|
params[:iat] = Time.zone.now.to_i
|
|
|
|
render json: {jwt_token: Canvas::Security.create_jwt(params, nil, tool.shared_secret)}
|
|
end
|
|
|
|
private
|
|
|
|
def set_tool_attributes(tool, params)
|
|
attrs = ContextExternalTool::EXTENSION_TYPES
|
|
attrs += [:name, :description, :url, :icon_url, :canvas_icon_class, :domain, :privacy_level, :consumer_key, :shared_secret,
|
|
:custom_fields, :custom_fields_string, :text, :config_type, :config_url, :config_xml, :not_selectable, :app_center_id]
|
|
attrs.each do |prop|
|
|
tool.send("#{prop}=", params[prop]) if params.has_key?(prop)
|
|
end
|
|
end
|
|
|
|
def invalidate_nav_tabs_cache(tool)
|
|
if tool.has_placement?(:user_navigation) || tool.has_placement?(:course_navigation) || tool.has_placement?(:account_navigation)
|
|
Lti::NavigationCache.new(@domain_root_account).invalidate_cache_key
|
|
end
|
|
end
|
|
|
|
def require_access_to_context
|
|
if @context.is_a?(Account)
|
|
require_user
|
|
elsif !@context.grants_right?(@current_user, session, :read)
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
def variable_expander(opts = {})
|
|
default_opts = {
|
|
current_user: @current_user,
|
|
current_pseudonym: @current_pseudonym,
|
|
tool: @tool }
|
|
Lti::VariableExpander.new(@domain_root_account, @context, self, default_opts.merge(opts))
|
|
end
|
|
|
|
def default_lti_params
|
|
lti_helper = Lti::SubstitutionsHelper.new(@context, @domain_root_account, @current_user)
|
|
|
|
params = {
|
|
context_id: Lti::Asset.opaque_identifier_for(@context),
|
|
tool_consumer_instance_guid: @domain_root_account.lti_guid,
|
|
roles: lti_helper.current_lis_roles,
|
|
launch_presentation_locale: I18n.locale || I18n.default_locale.to_s,
|
|
launch_presentation_document_target: 'iframe',
|
|
ext_roles: lti_helper.all_roles,
|
|
# launch_presentation_width:,
|
|
# launch_presentation_height:,
|
|
# launch_presentation_return_url: return_url,
|
|
}
|
|
|
|
params.merge!(user_id: Lti::Asset.opaque_identifier_for(@current_user)) if @current_user
|
|
params
|
|
end
|
|
|
|
end
|