Files
codeocean/app/controllers/concerns/lti.rb
Sebastian Serth a0d8b30ef2 Implement support for some basic embed options for work sheets via LTI
This commit also fixes an issue with the flash messages being positioned too high and displayed for too long
2018-12-11 14:29:36 +01:00

179 lines
6.3 KiB
Ruby

require 'oauth/request_proxy/rack_request'
module Lti
extend ActiveSupport::Concern
include LtiHelper
MAXIMUM_SCORE = 1
MAXIMUM_SESSION_AGE = 60.minutes
SESSION_PARAMETERS = %w(launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid)
def build_tool_provider(options = {})
if options[:consumer] && options[:parameters]
IMS::LTI::ToolProvider.new(options[:consumer].oauth_key, options[:consumer].oauth_secret, options[:parameters])
end
end
private :build_tool_provider
# exercise_id.nil? ==> the user has logged out. All session data is to be destroyed
# exercise_id.exists? ==> the user has submitted the results of an exercise to the consumer.
# Only the lti_parameters are deleted.
def clear_lti_session_data(exercise_id = nil, user_id = nil, consumer_id = nil)
if (exercise_id.nil?)
session.delete(:consumer_id)
session.delete(:external_user_id)
session.delete(:embed_options)
else
LtiParameter.where(consumers_id: consumer_id,
external_users_id: user_id,
exercises_id: exercise_id).destroy_all
end
end
private :clear_lti_session_data
def consumer_return_url(provider, options = {})
consumer_return_url = provider.try(:launch_presentation_return_url) || params[:launch_presentation_return_url]
consumer_return_url += "?#{options.to_query}" if consumer_return_url && options.present?
consumer_return_url
end
def external_user_email(provider)
provider.lis_person_contact_email_primary
end
private :external_user_email
def external_user_name(provider)
# save person_name_full if supplied. this is the display_name, if it is set.
# else only save the firstname, we don't want lastnames (family names)
if provider.lis_person_name_full
provider.lis_person_name_full
else
provider.lis_person_name_given
end
end
private :external_user_name
def refuse_lti_launch(options = {})
return_to_consumer(lti_errorlog: options[:message], lti_errormsg: t('sessions.oauth.failure'))
end
private :refuse_lti_launch
def require_oauth_parameters
refuse_lti_launch(message: t('sessions.oauth.missing_parameters')) unless params[:oauth_consumer_key] && params[:oauth_signature]
end
private :require_oauth_parameters
def require_unique_oauth_nonce
refuse_lti_launch(message: t('sessions.oauth.used_nonce')) if NonceStore.has?(params[:oauth_nonce])
end
private :require_unique_oauth_nonce
def require_valid_consumer_key
@consumer = Consumer.find_by(oauth_key: params[:oauth_consumer_key])
refuse_lti_launch(message: t('sessions.oauth.invalid_consumer')) unless @consumer
end
private :require_valid_consumer_key
def require_valid_exercise_token
proxy_exercise = ProxyExercise.find_by(token: params[:custom_token])
unless proxy_exercise.nil?
@exercise = proxy_exercise.get_matching_exercise(@current_user)
else
@exercise = Exercise.find_by(token: params[:custom_token])
end
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
end
private :require_valid_exercise_token
def require_valid_oauth_signature
@provider = build_tool_provider(consumer: @consumer, parameters: params)
refuse_lti_launch(message: t('sessions.oauth.invalid_signature')) unless @provider.valid_request?(request)
end
private :require_valid_oauth_signature
def return_to_consumer(options = {})
consumer_return_url = @provider.try(:launch_presentation_return_url) || params[:launch_presentation_return_url]
if consumer_return_url
consumer_return_url += "?#{options.to_query}" if options.present?
redirect_to(consumer_return_url)
else
flash[:danger] = options[:lti_errormsg]
flash[:info] = options[:lti_msg]
redirect_to(:root)
end
end
private :return_to_consumer
def send_score(exercise_id, score, user_id)
::NewRelic::Agent.add_custom_attributes({ score: score, session: session })
fail(Error, "Score #{score} must be between 0 and #{MAXIMUM_SCORE}!") unless (0..MAXIMUM_SCORE).include?(score)
if session[:consumer_id]
lti_parameter = LtiParameter.where(consumers_id: session[:consumer_id],
external_users_id: user_id,
exercises_id: exercise_id).first
consumer = Consumer.find_by(id: session[:consumer_id])
provider = build_tool_provider(consumer: consumer, parameters: lti_parameter.lti_parameters)
end
if provider.nil?
{status: 'error'}
elsif provider.outcome_service?
response = provider.post_replace_result!(score)
{code: response.response_code, message: response.post_response.body, status: response.code_major}
else
{status: 'unsupported'}
end
end
private :send_score
def set_current_user
@current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id)
@current_user.update(email: external_user_email(@provider), name: external_user_name(@provider))
end
private :set_current_user
def set_embedding_options
@embed_options = {}
[:hide_navbar,
:hide_exercise_description,
:disable_run,
:disable_score,
:disable_rfc,
:disable_interventions,
:hide_sidebar,
:read_only,
:hide_test_results,
:disable_hints].each do |option|
value = params["custom_embed_options_#{option}".to_sym] == 'true'
# Optimize storage and save only those that are true, the session cookie is limited to 4KB
@embed_options[option] = value if value.present?
end
session[:embed_options] = @embed_options
end
private :set_embedding_options
def store_lti_session_data(options = {})
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
external_users_id: @current_user.id,
exercises_id: @exercise.id)
lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).permit!.to_h
lti_parameters.save!
@lti_parameters = lti_parameters
session[:consumer_id] = options[:consumer].id
session[:external_user_id] = @current_user.id
end
private :store_lti_session_data
def store_nonce(nonce)
NonceStore.add(nonce)
end
private :store_nonce
class Error < RuntimeError
end
end