Merge branch 'master' into refactor_proforma_import_export
This commit is contained in:
@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
|
||||
after_action :verify_authorized, except: %i[welcome]
|
||||
around_action :mnemosyne_trace
|
||||
around_action :switch_locale
|
||||
before_action :set_sentry_context, :allow_iframe_requests, :load_embed_options
|
||||
before_action :set_sentry_context, :load_embed_options
|
||||
protect_from_forgery(with: :exception, prepend: true)
|
||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
|
||||
@ -40,7 +40,10 @@ class ApplicationController < ActionController::Base
|
||||
token = AuthenticationToken.find_by(shared_secret: params[:token])
|
||||
return unless token
|
||||
|
||||
auto_login(token.user) if token.expire_at.future?
|
||||
if token.expire_at.future?
|
||||
token.update(expire_at: Time.zone.now)
|
||||
auto_login(token.user)
|
||||
end
|
||||
end
|
||||
|
||||
def set_sentry_context
|
||||
@ -93,10 +96,6 @@ class ApplicationController < ActionController::Base
|
||||
# Show root page
|
||||
end
|
||||
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
|
||||
def load_embed_options
|
||||
@embed_options = if session[:embed_options].present? && session[:embed_options].is_a?(Hash)
|
||||
session[:embed_options].symbolize_keys
|
||||
|
@ -10,6 +10,15 @@ module CodeOcean
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def show_protected_upload
|
||||
@file = CodeOcean::File.find(params[:id])
|
||||
authorize!
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download] || @file.name_with_extension != params[:filename]
|
||||
|
||||
real_location = Pathname(@file.native_file.current_path).realpath
|
||||
send_file(real_location, type: @file.native_file.content_type, filename: @file.name_with_extension, disposition: 'attachment')
|
||||
end
|
||||
|
||||
def create
|
||||
@file = CodeOcean::File.new(file_params)
|
||||
if @file.file_template_id
|
||||
|
@ -5,8 +5,14 @@ module FileParameters
|
||||
if exercise && params
|
||||
params.reject do |_, file_attributes|
|
||||
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
|
||||
next true if file.nil? || file.hidden || file.read_only
|
||||
# avoid that public files from other contexts can be created
|
||||
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id) || (file.context_type == 'CommunitySolution' && controller_name != 'community_solutions')
|
||||
# `next` is similar to an early return and will proceed with the next iteration of the loop
|
||||
next true if file.context_type == 'Exercise' && file.context_id != exercise.id
|
||||
next true if file.context_type == 'Submission' && file.context.user != current_user
|
||||
next true if file.context_type == 'CommunitySolution' && controller_name != 'community_solutions'
|
||||
|
||||
false
|
||||
end
|
||||
else
|
||||
[]
|
||||
|
@ -22,6 +22,8 @@ class ExercisesController < ApplicationController
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise || @exercises)
|
||||
end
|
||||
@ -294,8 +296,6 @@ class ExercisesController < ApplicationController
|
||||
private :update_exercise_tips
|
||||
|
||||
def implement
|
||||
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO: TESTESTEST
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
user_solved_exercise = @exercise.solved_by?(current_user)
|
||||
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
|
||||
Time.zone.now.beginning_of_day).count
|
||||
@ -324,6 +324,8 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
@embed_options[:disable_score] = true unless @exercise.teacher_defined_assessment?
|
||||
|
||||
@hide_rfc_button = @embed_options[:disable_rfc]
|
||||
|
||||
@search = Search.new
|
||||
@ -432,6 +434,19 @@ class ExercisesController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def not_authorized_for_exercise(_exception)
|
||||
return render_not_authorized unless current_user
|
||||
return render_not_authorized unless %w[implement working_times intervention search reload].include?(action_name)
|
||||
|
||||
if current_user.admin? || current_user.teacher?
|
||||
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished?
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
else
|
||||
render_not_authorized
|
||||
end
|
||||
end
|
||||
private :not_authorized_for_exercise
|
||||
|
||||
def set_execution_environments
|
||||
@execution_environments = ExecutionEnvironment.all.order(:name)
|
||||
end
|
||||
|
@ -9,7 +9,7 @@ class ExternalUsersController < ApplicationController
|
||||
private :authorize!
|
||||
|
||||
def index
|
||||
@search = ExternalUser.ransack(params[:q])
|
||||
@search = ExternalUser.ransack(params[:q], {auth_object: current_user})
|
||||
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
@ -43,15 +43,15 @@ class ExternalUsersController < ApplicationController
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE user_id = #{@user.id}
|
||||
WHERE #{ExternalUser.sanitize_sql(['user_id = ?', @user.id])}
|
||||
AND user_type = 'ExternalUser'
|
||||
#{current_user.admin? ? '' : "AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"}
|
||||
#{current_user.admin? ? '' : "AND #{ExternalUser.sanitize_sql(['study_group_id IN (?)', current_user.study_groups.pluck(:id)])} AND cause = 'submit'"}
|
||||
GROUP BY exercise_id,
|
||||
user_id,
|
||||
id
|
||||
) AS foo
|
||||
) AS bar
|
||||
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND et.tag_id = #{tag} "}
|
||||
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND #{ExternalUser.sanitize_sql(['et.tag_id = ?', tag])}"}
|
||||
GROUP BY user_id,
|
||||
bar.exercise_id;
|
||||
"
|
||||
@ -60,10 +60,14 @@ class ExternalUsersController < ApplicationController
|
||||
def statistics
|
||||
@user = ExternalUser.find(params[:id])
|
||||
authorize!
|
||||
if params[:tag].present?
|
||||
tag = Tag.find(params[:tag])
|
||||
authorize(tag, :show?)
|
||||
end
|
||||
|
||||
statistics = {}
|
||||
|
||||
ApplicationRecord.connection.execute(working_time_query(params[:tag])).each do |tuple|
|
||||
ApplicationRecord.connection.execute(working_time_query(tag&.id)).each do |tuple|
|
||||
statistics[tuple['exercise_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
|
@ -67,7 +67,7 @@ class InternalUsersController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
@search = InternalUser.ransack(params[:q])
|
||||
@search = InternalUser.ransack(params[:q], {auth_object: current_user})
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
@ -70,12 +70,9 @@ class ProxyExercisesController < ApplicationController
|
||||
|
||||
def show
|
||||
@search = @proxy_exercise.exercises.ransack
|
||||
@exercises = @proxy_exercise.exercises.ransack.result.order(:title) # @search.result.order(:title)
|
||||
@exercises = @proxy_exercise.exercises.ransack.result.order(:title)
|
||||
end
|
||||
|
||||
# we might want to think about auth here
|
||||
def reload; end
|
||||
|
||||
def update
|
||||
myparams = proxy_exercise_params
|
||||
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].compact_blank)
|
||||
|
@ -12,7 +12,17 @@ class SubmissionsController < ApplicationController
|
||||
before_action :set_testrun, only: %i[run score test]
|
||||
before_action :set_files, only: %i[download show]
|
||||
before_action :set_files_and_specific_file, only: %i[download_file render_file run test]
|
||||
before_action :set_mime_type, only: %i[download_file render_file]
|
||||
before_action :set_content_type_nosniff, only: %i[download download_file render_file]
|
||||
|
||||
# Overwrite the CSP header for the :render_file action
|
||||
content_security_policy only: :render_file do |policy|
|
||||
policy.img_src :none
|
||||
policy.script_src :none
|
||||
policy.font_src :none
|
||||
policy.style_src :none
|
||||
policy.connect_src :none
|
||||
policy.form_action :none
|
||||
end
|
||||
|
||||
def create
|
||||
@submission = Submission.new(submission_params)
|
||||
@ -56,7 +66,11 @@ class SubmissionsController < ApplicationController
|
||||
def download_file
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
|
||||
|
||||
send_data(@file.read, filename: @file.name_with_extension)
|
||||
if @file.native_file?
|
||||
redirect_to protected_upload_path(id: @file.id, filename: @file.name_with_extension)
|
||||
else
|
||||
send_data(@file.content, filename: @file.name_with_extension, disposition: 'attachment')
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@ -66,13 +80,13 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def render_file
|
||||
if @file.native_file?
|
||||
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
|
||||
else
|
||||
render(plain: @file.content)
|
||||
end
|
||||
# If a file should not be downloaded, it should not be rendered either
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
|
||||
|
||||
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def run
|
||||
# These method-local socket variables are required in order to use one socket
|
||||
# in the callbacks of the other socket. As the callbacks for the client socket
|
||||
@ -83,7 +97,7 @@ class SubmissionsController < ApplicationController
|
||||
client_socket = tubesock
|
||||
|
||||
client_socket.onopen do |_event|
|
||||
kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
return kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
end
|
||||
|
||||
client_socket.onclose do |_event|
|
||||
@ -167,7 +181,8 @@ class SubmissionsController < ApplicationController
|
||||
@testrun[:status] = :failed
|
||||
"\n#{t('exercises.implement.exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
|
||||
end
|
||||
send_and_store client_socket, {cmd: :write, stream: :stdout, data: "#{exit_statement}\n"}
|
||||
stream = @testrun[:status] == :ok ? :stdout : :stderr
|
||||
send_and_store client_socket, {cmd: :write, stream: stream, data: "#{exit_statement}\n"}
|
||||
if exit_code == 137
|
||||
send_and_store client_socket, {cmd: :status, status: :out_of_memory}
|
||||
@testrun[:status] = :out_of_memory
|
||||
@ -194,12 +209,13 @@ class SubmissionsController < ApplicationController
|
||||
ensure
|
||||
save_testrun_output 'run'
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity:
|
||||
|
||||
def score
|
||||
hijack do |tubesock|
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_score]
|
||||
return kill_client_socket(tubesock) if @embed_options[:disable_score] || !@submission.exercise.teacher_defined_assessment?
|
||||
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.calculate_score))
|
||||
@ -226,7 +242,7 @@ class SubmissionsController < ApplicationController
|
||||
hijack do |tubesock|
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
return kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.test(@file)))
|
||||
@ -363,9 +379,9 @@ class SubmissionsController < ApplicationController
|
||||
@files = @submission.collect_files.select(&:visible)
|
||||
end
|
||||
|
||||
def set_mime_type
|
||||
@mime_type = Mime::Type.lookup_by_extension(@file.file_type.file_extension.gsub(/^\./, ''))
|
||||
response.headers['Content-Type'] = @mime_type.to_s
|
||||
def set_content_type_nosniff
|
||||
# When sending a file, we want to ensure that browsers follow our Content-Type header
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
end
|
||||
|
||||
def set_submission
|
||||
|
@ -74,7 +74,7 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
|
||||
def update
|
||||
submission = begin
|
||||
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').final.first
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
@ -127,14 +127,16 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
user_type = current_user.class.name
|
||||
latest_submission = Submission
|
||||
.where(user_id: user_id, user_type: user_type, exercise_id: exercise_id)
|
||||
.order(created_at: :desc).first
|
||||
.order(created_at: :desc).final.first
|
||||
|
||||
authorize(latest_submission, :show?)
|
||||
|
||||
params[:user_exercise_feedback]
|
||||
.permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime)
|
||||
.merge(user_id: user_id,
|
||||
user_type: user_type,
|
||||
submission: latest_submission,
|
||||
normalized_score: latest_submission.normalized_score)
|
||||
normalized_score: latest_submission&.normalized_score)
|
||||
end
|
||||
|
||||
def validate_inputs(uef_params)
|
||||
|
Reference in New Issue
Block a user