canvas-lms/app/controllers/application_controller.rb

2249 lines
86 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
# 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/>.
#
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
class << self
[:before, :after, :around,
:skip_before, :skip_after, :skip_around,
:prepend_before, :prepend_after, :prepend_around].each do |type|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{type}_filter(*)
raise "Please use #{type}_action instead of #{type}_filter"
end
RUBY
end
end
attr_accessor :active_tab
attr_reader :context
include Api
include LocaleSelection
include Api::V1::User
include Api::V1::WikiPage
include LegalInformationHelper
around_action :set_locale
around_action :enable_request_cache
around_action :batch_statsd
helper :all
include AuthenticationMethods
include Canvas::RequestForgeryProtection
protect_from_forgery with: :exception
# load_user checks masquerading permissions, so this needs to be cleared first
before_action :clear_cached_contexts
prepend_before_action :load_user, :load_account
# make sure authlogic is before load_user
skip_before_action :activate_authlogic
prepend_before_action :activate_authlogic
before_action :check_pending_otp
before_action :set_user_id_header
before_action :set_time_zone
before_action :set_page_view
before_action :require_reacceptance_of_terms
before_action :clear_policy_cache
before_action :setup_live_events_context
after_action :log_page_view
after_action :discard_flash_if_xhr
after_action :cache_buster
before_action :initiate_session_from_token
# Yes, we're calling this before and after so that we get the user id logged
# on events that log someone in and log someone out.
after_action :set_user_id_header
before_action :fix_xhr_requests
before_action :init_body_classes
after_action :set_response_headers
after_action :update_enrollment_last_activity_at
add_crumb(proc {
title = I18n.t('links.dashboard', 'My Dashboard')
crumb = <<-END
<i class="icon-home"
title="#{title}">
<span class="screenreader-only">#{title}</span>
</i>
END
crumb.html_safe
}, :root_path, class: 'home')
##
# Sends data from rails to JavaScript
#
# The data you send will eventually make its way into the view by simply
# calling `to_json` on the data.
#
# It won't allow you to overwrite a key that has already been set
#
# Please use *ALL_CAPS* for keys since these are considered constants
# Also, please don't name it stuff from JavaScript's Object.prototype
# like `hasOwnProperty`, `constructor`, `__defineProperty__` etc.
#
# This method is available in controllers and views
#
# example:
#
# # ruby
# js_env :FOO_BAR => [1,2,3], :COURSE => @course
#
# # coffeescript
# require ['ENV'], (ENV) ->
# ENV.FOO_BAR #> [1,2,3]
#
def js_env(hash = {})
return {} unless request.format.html?
# set some defaults
unless @js_env
editor_css = view_context.stylesheet_path(css_url_for('what_gets_loaded_inside_the_tinymce_editor'))
@js_env = {
ASSET_HOST: Canvas::Cdn.config.host,
active_brand_config: active_brand_config.try(:md5),
active_brand_config_json_url: active_brand_config_json_url,
url_to_what_gets_loaded_inside_the_tinymce_editor_css: editor_css,
current_user_id: @current_user.try(:id),
current_user: Rails.cache.fetch(['user_display_json', @current_user].cache_key, :expires_in => 1.hour) { user_display_json(@current_user, :profile) },
current_user_roles: @current_user.try(:roles, @domain_root_account),
current_user_disabled_inbox: @current_user.try(:disabled_inbox?),
files_domain: HostUrl.file_host(@domain_root_account || Account.default, request.host_with_port),
DOMAIN_ROOT_ACCOUNT_ID: @domain_root_account.try(:global_id),
k12: k12?,
help_link_name: help_link_name,
help_link_icon: help_link_icon,
use_high_contrast: @current_user.try(:prefers_high_contrast?),
SETTINGS: {
open_registration: @domain_root_account.try(:open_registration?),
eportfolios_enabled: (@domain_root_account && @domain_root_account.settings[:enable_eportfolios] != false), # checking all user root accounts is slow
collapse_global_nav: @current_user.try(:collapse_global_nav?),
show_feedback_link: show_feedback_link?,
enable_profiles: (@domain_root_account && @domain_root_account.settings[:enable_profiles] != false)
},
}
@js_env[:page_view_update_url] = page_view_path(@page_view.id, page_view_token: @page_view.token) if @page_view
@js_env[:IS_LARGE_ROSTER] = true if !@js_env[:IS_LARGE_ROSTER] && @context.respond_to?(:large_roster?) && @context.large_roster?
@js_env[:context_asset_string] = @context.try(:asset_string) if !@js_env[:context_asset_string]
@js_env[:ping_url] = polymorphic_url([:api_v1, @context, :ping]) if @context.is_a?(Course)
@js_env[:TIMEZONE] = Time.zone.tzinfo.identifier if !@js_env[:TIMEZONE]
@js_env[:CONTEXT_TIMEZONE] = @context.time_zone.tzinfo.identifier if !@js_env[:CONTEXT_TIMEZONE] && @context.respond_to?(:time_zone) && @context.time_zone.present?
unless @js_env[:LOCALE]
@js_env[:LOCALE] = I18n.locale.to_s
@js_env[:BIGEASY_LOCALE] = I18n.bigeasy_locale
@js_env[:FULLCALENDAR_LOCALE] = I18n.fullcalendar_locale
@js_env[:MOMENT_LOCALE] = I18n.moment_locale
end
@js_env[:lolcalize] = true if ENV['LOLCALIZE']
end
hash.each do |k,v|
if @js_env[k]
raise "js_env key #{k} is already taken"
else
@js_env[k] = v
end
end
@js_env
end
helper_method :js_env
# add keys to JS environment necessary for the RCE at the given risk level
def rce_js_env(risk_level, root_account: @domain_root_account, domain: request.env['HTTP_HOST'], context: @context)
rce_env_hash = Services::RichContent.env_for(root_account,
risk_level: risk_level,
user: @current_user,
domain: domain,
real_user: @real_current_user,
context: context)
js_env(rce_env_hash)
end
helper_method :rce_js_env
def conditional_release_js_env(assignment = nil, includes: [])
return unless ConditionalRelease::Service.enabled_in_context?(@context)
cr_env = ConditionalRelease::Service.env_for(
@context,
@current_user,
session: session,
assignment: assignment,
domain: request.env['HTTP_HOST'],
real_user: @real_current_user,
includes: includes
)
js_env(cr_env)
end
helper_method :conditional_release_js_env
def external_tools_display_hashes(type, context=@context, custom_settings=[])
return [] if context.is_a?(Group)
context = context.account if context.is_a?(User)
tools = ContextExternalTool.all_tools_for(context, {:placements => type,
:root_account => @domain_root_account, :current_user => @current_user}).to_a
tools.select!{|tool| ContextExternalTool.visible?(tool.extension_setting(type)['visibility'], @current_user, context, session)}
tools.map do |tool|
external_tool_display_hash(tool, type, {}, context, custom_settings)
end
end
def external_tool_display_hash(tool, type, url_params={}, context=@context, custom_settings=[])
url_params = {
id: tool.id,
launch_type: type
}.merge(url_params)
hash = {
:title => tool.label_for(type, I18n.locale),
:base_url => polymorphic_url([context, :external_tool], url_params)
}
extension_settings = [:icon_url, :canvas_icon_class] | custom_settings
extension_settings.each do |setting|
hash[setting] = tool.extension_setting(type, setting)
end
hash
end
helper_method :external_tool_display_hash
def k12?
@domain_root_account && @domain_root_account.feature_enabled?(:k12)
end
helper_method :k12?
def grading_periods?
!!@context.try(:grading_periods?)
end
helper_method :grading_periods?
def master_courses?
@domain_root_account && @domain_root_account.feature_enabled?(:master_courses)
end
helper_method :master_courses?
def setup_master_course_restrictions(objects, course)
return unless master_courses? && course.is_a?(Course) && course.grants_right?(@current_user, session, :read_as_admin)
if MasterCourses::MasterTemplate.is_master_course?(course)
MasterCourses::Restrictor.preload_default_template_restrictions(objects, course)
return :master # return master/child status
elsif MasterCourses::ChildSubscription.is_child_course?(course)
MasterCourses::Restrictor.preload_child_restrictions(objects)
return :child
end
end
helper_method :setup_master_course_restrictions
def set_master_course_js_env_data(object, course)
return unless object.respond_to?(:master_course_api_restriction_data) && object.persisted?
status = setup_master_course_restrictions([object], course)
return unless status
# we might have to include more information about the object here to make it easier to plug a common component in
data = object.master_course_api_restriction_data(status)
if status == :master
data[:default_restrictions] = MasterCourses::MasterTemplate.full_template_for(course).default_restrictions_for(object)
end
js_env(:MASTER_COURSE_DATA => data)
end
helper_method :set_master_course_js_env_data
def load_blueprint_courses_ui
return unless @context && @context.is_a?(Course) && master_courses? && @context.grants_right?(@current_user, :manage)
is_child = MasterCourses::ChildSubscription.is_child_course?(@context)
is_master = MasterCourses::MasterTemplate.is_master_course?(@context)
return unless is_master || is_child
js_bundle :blueprint_courses
css_bundle :blueprint_courses
master_course = is_master ? @context : @context.master_course_subscriptions.active.first.master_template.course
js_env :DEBUG_BLUEPRINT_COURSES => Rails.env.development? || Rails.env.test?
js_env :BLUEPRINT_COURSES_DATA => {
isMasterCourse: is_master,
isChildCourse: is_child,
accountId: @context.account.id,
masterCourse: master_course.slice(:id, :name, :enrollment_term_id),
course: @context.slice(:id, :name, :enrollment_term_id),
subAccounts: @context.account.sub_accounts.pluck(:id, :name).map{|id, name| {id: id, name: name}},
terms: @context.account.root_account.enrollment_terms.active.pluck(:id, :name).map{|id, name| {id: id, name: name}},
canManageCourse: MasterCourses::MasterTemplate.is_master_course?(@context) && @context.account.grants_right?(@current_user, :manage_master_courses)
}
if is_master && js_env.key?(:NEW_USER_TUTORIALS)
js_env[:NEW_USER_TUTORIALS][:is_enabled] = false
end
end
helper_method :load_blueprint_courses_ui
def editing_restricted?(content, edit_type=:any)
return false unless master_courses? && content.respond_to?(:editing_restricted?)
content.editing_restricted?(edit_type)
end
helper_method :editing_restricted?
def tool_dimensions
tool_dimensions = {selection_width: '100%', selection_height: '100%'}
tool_dimensions.each do |k, v|
tool_dimensions[k] = @tool.settings[k] || v
tool_dimensions[k] = tool_dimensions[k].to_s << 'px' unless tool_dimensions[k].to_s =~ /%|px/
end
tool_dimensions
end
private :tool_dimensions
# Reject the request by halting the execution of the current handler
# and returning a helpful error message (and HTTP status code).
#
# @param [String] cause
# The reason the request is rejected for.
# @param [Optional, Integer|Symbol, Default :bad_request] status
# HTTP status code or symbol.
def reject!(cause, status=:bad_request)
raise RequestError.new(cause, status)
end
# returns the user actually logged into canvas, even if they're currently masquerading
#
# This is used by the google docs integration, among other things --
# having @real_current_user first ensures that a masquerading user never sees the
# masqueradee's files, but in general you may want to block access to google
# docs for masqueraders earlier in the request
def logged_in_user
@real_current_user || @current_user
end
def not_fake_student_user
@current_user && @current_user.fake_student? ? logged_in_user : @current_user
end
def rescue_action_dispatch_exception
rescue_action_in_public(request.env['action_dispatch.exception'])
end
# used to generate context-specific urls without having to
# check which type of context it is everywhere
def named_context_url(context, name, *opts)
if context.is_a?(UserProfile)
name = name.to_s.sub(/context/, "profile")
else
klass = context.class.base_class
name = name.to_s.sub(/context/, klass.name.underscore)
opts.unshift(context)
end
opts.push({}) unless opts[-1].is_a?(Hash)
include_host = opts[-1].delete(:include_host)
if !include_host
opts[-1][:host] = context.host_name rescue nil
opts[-1][:only_path] = true unless name.end_with?("_path")
end
self.send name, *opts
end
def self.promote_view_path(path)
self.view_paths = self.view_paths.to_ary.reject{ |p| p.to_s == path }
prepend_view_path(path)
end
protected
# we track the cost of each request in RequestThrottle in order
# to rate limit clients that are abusing the API. Some actions consume
# time or resources that are not well represented by simple time/cpu
# benchmarks, so you can use this method to increase the perceived cost
# of a request by an arbitrary amount. For an anchor, rate limiting
# kicks in when a user has exceeded 600 arbitrary units of cost (it's
# a leaky bucket, go see RequestThrottle), so using an 'amount'
# param of 600, for example, would max out the bucket immediately
def increment_request_cost(amount)
current_cost = request.env['extra-request-cost'] || 0
request.env['extra-request-cost'] = current_cost + amount
end
def assign_localizer
I18n.localizer = lambda {
infer_locale :context => @context,
:user => not_fake_student_user,
:root_account => @domain_root_account,
:session_locale => session[:locale],
:accept_language => request.headers['Accept-Language']
}
end
def set_locale
store_session_locale
assign_localizer
yield if block_given?
ensure
I18n.localizer = nil
end
def enable_request_cache(&block)
RequestCache.enable(&block)
end
def batch_statsd(&block)
CanvasStatsd::Statsd.batch(&block)
end
def store_session_locale
return unless locale = params[:session_locale]
supported_locales = I18n.available_locales.map(&:to_s)
session[:locale] = locale if supported_locales.include? locale
end
def init_body_classes
@body_classes = []
end
def set_user_id_header
headers['X-Canvas-User-Id'] ||= @current_user.global_id.to_s if @current_user
headers['X-Canvas-Real-User-Id'] ||= @real_current_user.global_id.to_s if @real_current_user
end
# make things requested from jQuery go to the "format.js" part of the "respond_to do |format|" block
# see http://codetunes.com/2009/01/31/rails-222-ajax-and-respond_to/ for why
def fix_xhr_requests
request.format = :js if request.xhr? && request.format == :html && !params[:html_xhr]
end
# scopes all time objects to the user's specified time zone
def set_time_zone
user = not_fake_student_user
if user && !user.time_zone.blank?
Time.zone = user.time_zone
if Time.zone && Time.zone.name == "UTC" && user.time_zone && user.time_zone.name.match(/\s/)
Time.zone = user.time_zone.name.split(/\s/)[1..-1].join(" ") rescue nil
end
else
Time.zone = @domain_root_account && @domain_root_account.default_time_zone
end
end
# retrieves the root account for the given domain
def load_account
@domain_root_account = request.env['canvas.domain_root_account'] || LoadAccount.default_domain_root_account
@files_domain = request.host_with_port != HostUrl.context_host(@domain_root_account) && HostUrl.is_file_host?(request.host_with_port)
@domain_root_account
end
def set_response_headers
# we can't block frames on the files domain, since files domain requests
# are typically embedded in an iframe in canvas, but the hostname is
# different
if !files_domain? && Setting.get('block_html_frames', 'true') == 'true' && !@embeddable
headers['X-Frame-Options'] = 'SAMEORIGIN'
end
RequestContextGenerator.store_request_meta(request, @context)
true
end
def files_domain?
!!@files_domain
end
def check_pending_otp
if session[:pending_otp] && params[:controller] != 'login/otp'
return render plain: "Please finish logging in", status: 403 if request.xhr?
reset_session
redirect_to login_url
end
end
def user_url(*opts)
opts[0] == @current_user && !@current_user.grants_right?(@current_user, session, :view_statistics) ?
user_profile_url(@current_user) :
super
end
def tab_enabled?(id, opts = {})
return true unless @context && @context.respond_to?(:tabs_available)
tabs = Rails.cache.fetch(['tabs_available', @context, @current_user, @domain_root_account,
session[:enrollment_uuid]].cache_key, expires_in: 1.hour) do
@context.tabs_available(@current_user,
:session => session, :include_hidden_unused => true, :root_account => @domain_root_account)
end
valid = tabs.any?{|t| t[:id] == id }
render_tab_disabled unless valid || opts[:no_render]
return valid
end
def render_tab_disabled
msg = tab_disabled_message(@context)
respond_to do |format|
format.html {
flash[:notice] = msg
redirect_to named_context_url(@context, :context_url)
}
format.json {
render :json => { :message => msg }, :status => :not_found
}
end
end
def tab_disabled_message(context)
if context.is_a?(Account)
t "#application.notices.page_disabled_for_account", "That page has been disabled for this account"
elsif context.is_a?(Course)
t "#application.notices.page_disabled_for_course", "That page has been disabled for this course"
elsif context.is_a?(Group)
t "#application.notices.page_disabled_for_group", "That page has been disabled for this group"
else
t "#application.notices.page_disabled", "That page has been disabled"
end
end
def require_password_session
if session[:used_remember_me_token]
flash[:warning] = t "#application.warnings.please_log_in", "For security purposes, please enter your password to continue"
store_location
redirect_to login_url
return false
end
true
end
def run_login_hooks
LoginHooks.run_hooks(request)
end
# checks the authorization policy for the given object using
# the vendor/plugins/adheres_to_policy plugin. If authorized,
# returns true, otherwise renders unauthorized messages and returns
# false. To be used as follows:
# if authorized_action(object, @current_user, :update)
# render
# end
def authorized_action(object, actor, rights)
can_do = object.grants_any_right?(actor, session, *Array(rights))
render_unauthorized_action unless can_do
can_do
end
alias :authorized_action? :authorized_action
def fix_ms_office_redirects
if ms_office?
# Office will follow 302's internally, until it gets to a 200. _then_ it will pop it out
# to a web browser - but you've lost your cookies! This breaks not only store_location,
# but in the case of delegated authentication where the provider does an additional
# redirect storing important information in session, makes it impossible to log in at all
render plain: '', status: 200
return false
end
true
end
def render_unauthorized_action
respond_to do |format|
@show_left_side = false
clear_crumbs
path_params = request.path_parameters
path_params[:format] = nil
@headers = !!@current_user if @headers != false
@files_domain = @account_domain && @account_domain.host_type == 'files'
format.html {
return unless fix_ms_office_redirects
store_location
return redirect_to login_url(params.permit(:authentication_provider)) if !@files_domain && !@current_user
if @context.is_a?(Course) && @context_enrollment
if @context_enrollment.inactive?
start_date = @context_enrollment.available_at
end
if @context.claimed?
@unauthorized_message = t('#application.errors.unauthorized.unpublished', "This course has not been published by the instructor yet.")
@unauthorized_reason = :unpublished
elsif start_date && start_date > Time.now.utc
@unauthorized_message = t('#application.errors.unauthorized.not_started_yet', "The course you are trying to access has not started yet. It will start %{date}.", :date => TextHelper.date_string(start_date))
@unauthorized_reason = :unpublished
end
end
render "shared/unauthorized", status: :unauthorized
}
format.zip { redirect_to(url_for(path_params)) }
format.json { render_json_unauthorized }
format.all { render plain: 'Unauthorized', status: :unauthorized }
end
set_no_cache_headers
end
# To be used as a before_action, requires controller or controller actions
# to have their urls scoped to a context in order to be valid.
# So /courses/5/assignments or groups/1/assignments would be valid, but
# not /assignments
def require_context
get_context
if !@context
if @context_is_current_user
store_location
redirect_to login_url
elsif params[:context_id]
raise ActiveRecord::RecordNotFound.new("Cannot find #{params[:context_type] || 'Context'} for ID: #{params[:context_id]}")
else
raise ActiveRecord::RecordNotFound.new("Context is required, but none found")
end
end
return @context != nil
end
def require_context_and_read_access
require_context && authorized_action(@context, @current_user, :read)
end
helper_method :clean_return_to
def require_account_context
require_context_type(Account)
end
def require_course_context
require_context_type(Course)
end
def require_context_type(klass)
unless require_context && @context.is_a?(klass)
raise ActiveRecord::RecordNotFound.new("Context must be of type '#{klass}'")
end
true
end
MAX_ACCOUNT_LINEAGE_TO_SHOW_IN_CRUMBS = 3
# Can be used as a before_action, or just called from controller code.
# Assigns the variable @context to whatever context the url is scoped
# to. So /courses/5/assignments would have a @context=Course.find(5).
# Also assigns @context_membership to the membership type of @current_user
# if @current_user is a member of the context.
def get_context
unless @context
if params[:course_id]
@context = api_find(Course.active, params[:course_id])
@context.root_account = @domain_root_account if @context.root_account_id == @domain_root_account.id # no sense in refetching it
params[:context_id] = params[:course_id]
params[:context_type] = "Course"
if @context && @current_user
@context_enrollment = @context.enrollments.where(user_id: @current_user).joins(:enrollment_state).
order(Enrollment.state_by_date_rank_sql, Enrollment.type_rank_sql).readonly(false).first
end
@context_membership = @context_enrollment
check_for_readonly_enrollment_state
elsif params[:account_id] || (self.is_a?(AccountsController) && params[:account_id] = params[:id])
@context = api_find(Account, params[:account_id])
params[:context_id] = @context.id
params[:context_type] = "Account"
@context_enrollment = @context.account_users.active.where(user_id: @current_user.id).first if @context && @current_user
@context_membership = @context_enrollment
@account = @context
elsif params[:group_id]
@context = api_find(Group.active, params[:group_id])
params[:context_id] = params[:group_id]
params[:context_type] = "Group"
@context_enrollment = @context.group_memberships.where(user_id: @current_user).first if @context && @current_user
@context_membership = @context_enrollment
elsif params[:user_id] || (self.is_a?(UsersController) && params[:user_id] = params[:id])
@context = api_find(User, params[:user_id])
params[:context_id] = params[:user_id]
params[:context_type] = "User"
@context_membership = @context if @context == @current_user
elsif params[:course_section_id] || (self.is_a?(SectionsController) && params[:course_section_id] = params[:id])
params[:context_id] = params[:course_section_id]
params[:context_type] = "CourseSection"
@context = api_find(CourseSection, params[:course_section_id])
elsif request.path.match(/\A\/profile/) || request.path == '/' || request.path.match(/\A\/dashboard\/files/) || request.path.match(/\A\/calendar/) || request.path.match(/\A\/assignments/) || request.path.match(/\A\/files/) || request.path == '/api/v1/calendar_events/visible_contexts'
# ^ this should be split out into things on the individual controllers
@context_is_current_user = true
@context = @current_user
@context_membership = @context
end
assign_localizer if @context.present?
if request.format.html?
if @context.is_a?(Account) && !@context.root_account?
account_chain = @context.account_chain.to_a.select {|a| a.grants_right?(@current_user, session, :read) }
account_chain.slice!(0) # the first element is the current context
count = account_chain.length
account_chain.reverse.each_with_index do |a, idx|
if idx == 1 && count >= MAX_ACCOUNT_LINEAGE_TO_SHOW_IN_CRUMBS
add_crumb(I18n.t('#lib.text_helper.ellipsis', '...'), nil)
elsif count >= MAX_ACCOUNT_LINEAGE_TO_SHOW_IN_CRUMBS && idx > 0 && idx <= count - MAX_ACCOUNT_LINEAGE_TO_SHOW_IN_CRUMBS
next
else
add_crumb(a.short_name, account_url(a.id), :id => "crumb_#{a.asset_string}")
end
end
end
if @context && @context.respond_to?(:short_name)
crumb_url = named_context_url(@context, :context_url) if @context.grants_right?(@current_user, session, :read)
add_crumb(@context.nickname_for(@current_user, :short_name), crumb_url)
end
@set_badge_counts = true
end
end
# There is lots of interesting information set up in here, that we want
# to place into the live events context.
setup_live_events_context
end
# This is used by a number of actions to retrieve a list of all contexts
# associated with the given context. If the context is a user then it will
# include all the user's current contexts.
# Assigns it to the variable @contexts
def get_all_pertinent_contexts(opts = {})
return if @already_ran_get_all_pertinent_contexts
@already_ran_get_all_pertinent_contexts = true
raise(ArgumentError, "Need a starting context") if @context.nil?
@contexts = [@context]
only_contexts = ActiveRecord::Base.parse_asset_string_list(opts[:only_contexts] || params[:only_contexts])
if @context && @context.is_a?(User)
# we already know the user can read these courses and groups, so skip
# the grants_right? check to avoid querying for the various memberships
# again.
enrollment_scope = Enrollment.for_user(@context).current.active_by_date
include_groups = !!opts[:include_groups]
group_ids = nil
courses = []
if only_contexts.present?
# find only those courses and groups passed in the only_contexts
# parameter, but still scoped by user so we know they have rights to
# view them.
course_ids = only_contexts.select { |c| c.first == "Course" }.map(&:last)
unless course_ids.empty?
courses = Course.where(:id => course_ids).where(:id => enrollment_scope.select(:course_id)).to_a
end
if include_groups
group_ids = only_contexts.select { |c| c.first == "Group" }.map(&:last)
include_groups = false if group_ids.empty?
end
else
courses = Course.shard(opts[:cross_shard] ? @context.in_region_associated_shards : Shard.current).
where(:id => enrollment_scope.select(:course_id)).to_a
end
groups = []
if include_groups
if group_ids
Shard.partition_by_shard(group_ids) do |shard_group_ids|
groups += @context.current_groups.shard(Shard.current).where(:id => shard_group_ids).to_a
end
else
groups = @context.current_groups.shard(opts[:cross_shard] ? @context.in_region_associated_shards : Shard.current).to_a
end
end
groups.reject!{|g| g.context_type == "Course" && g.context.concluded?}
if opts[:favorites_first]
favorite_course_ids = @context.favorite_context_ids("Course")
courses = courses.sort_by {|c| [favorite_course_ids.include?(c.id) ? 0 : 1, Canvas::ICU.collation_key(c.name)]}
end
@contexts.concat courses
@contexts.concat groups
end
include_contexts = opts[:include_contexts] || params[:include_contexts]
if include_contexts
include_contexts.split(",").each do |include_context|
# don't load it again if we've already got it
next if @contexts.any? { |c| c.asset_string == include_context }
context = Context.find_by_asset_string(include_context)
@contexts << context if context && context.grants_right?(@current_user, session, :read)
end
end
@contexts = @contexts.uniq
Course.require_assignment_groups(@contexts)
@context_enrollment = @context.membership_for_user(@current_user) if @context.respond_to?(:membership_for_user)
@context_membership = @context_enrollment
end
def check_for_readonly_enrollment_state
return unless request.format.html?
if @context_enrollment && @context_enrollment.is_a?(Enrollment) && ['invited', 'active'].include?(@context_enrollment.workflow_state) && action_name != "enrollment_invitation"
state = @context_enrollment.state_based_on_date
case state
when :invited
if @context_enrollment.available_at
flash[:html_notice] = mt "#application.notices.need_to_accept_future_enrollment",
"You'll need to [accept the enrollment invitation](%{url}) before you can fully participate in this course, starting on %{date}.",
:url => course_url(@context),:date => datetime_string(@context_enrollment.available_at)
else
flash[:html_notice] = mt "#application.notices.need_to_accept_enrollment",
"You'll need to [accept the enrollment invitation](%{url}) before you can fully participate in this course.", :url => course_url(@context)
end
when :accepted
flash[:html_notice] = t("This course hasnt started yet. You will not be able to participate in this course until %{date}.", :date => datetime_string(@context_enrollment.available_at))
end
end
end
def set_badge_counts_for(context, user, enrollment=nil)
return if @js_env && @js_env[:badge_counts].present?
return unless context.present? && user.present?
return unless context.respond_to?(:content_participation_counts) # just Course and Group so far
js_env(:badge_counts => badge_counts_for(context, user, enrollment))
end
helper_method :set_badge_counts_for
def badge_counts_for(context, user, enrollment=nil)
badge_counts = {}
['Submission'].each do |type|
participation_count = context.content_participation_counts.
where(:user_id => user.id, :content_type => type).first
participation_count ||= ContentParticipationCount.create_or_update({
:context => context,
:user => user,
:content_type => type,
})
badge_counts[type.underscore.pluralize] = participation_count.unread_count
end
badge_counts
end
def get_upcoming_assignments(course)
assignments = AssignmentGroup.visible_assignments(
@current_user,
course,
course.assignment_groups.active
).to_a
log_course(course)
if @current_user
submissions = @current_user.submissions.shard(@current_user).to_a
submissions.each{ |s| s.mute if s.muted_assignment? }
else
submissions = []
end
assignments.map! {|a| a.overridden_for(@current_user)}
sorted = SortsAssignments.by_due_date({
:assignments => assignments,
:user => @current_user,
:session => session,
:upcoming_limit => 1.week.from_now,
:submissions => submissions
})
sorted.upcoming.sort
end
def log_course(course)
log_asset_access([ "assignments", course ], "assignments", "other")
end
# Calculates the file storage quota for @context
def get_quota(context=nil)
quota_params = Attachment.get_quota(context || @context)
@quota = quota_params[:quota]
@quota_used = quota_params[:quota_used]
end
# Renders a quota exceeded message if the @context's quota is exceeded
def quota_exceeded(context=nil, redirect=nil)
context ||= @context
redirect ||= root_url
get_quota(context)
if response.body.size + @quota_used > @quota
if context.is_a?(Account)
error = t "#application.errors.quota_exceeded_account", "Account storage quota exceeded"
elsif context.is_a?(Course)
error = t "#application.errors.quota_exceeded_course", "Course storage quota exceeded"
elsif context.is_a?(Group)
error = t "#application.errors.quota_exceeded_group", "Group storage quota exceeded"
elsif context.is_a?(User)
error = t "#application.errors.quota_exceeded_user", "User storage quota exceeded"
else
error = t "#application.errors.quota_exceeded", "Storage quota exceeded"
end
respond_to do |format|
flash[:error] = error unless request.format.to_s == "text/plain"
format.html {redirect_to redirect }
format.json {render :json => {:errors => {:base => error}}, :status => :bad_request }
format.text {render :json => {:errors => {:base => error}}, :status => :bad_request }
end
return true
end
false
end
# Used to retrieve the context from a :feed_code parameter. These
# :feed_code attributes are keyed off the object type and the object's
# uuid. Using the uuid attribute gives us an unguessable url so
# that we can offer the feeds without requiring password authentication.
def get_feed_context(opts={})
pieces = params[:feed_code].split("_", 2)
if params[:feed_code].match(/\Agroup_membership/)
pieces = ["group_membership", params[:feed_code].split("_", 3)[-1]]
end
@context = nil
@problem = nil
if pieces[0] == "enrollment"
@enrollment = Enrollment.where(uuid: pieces[1]).first if pieces[1]
@context_type = "Course"
if !@enrollment
@problem = t "#application.errors.mismatched_verification_code", "The verification code does not match any currently enrolled user."
elsif @enrollment.course && !@enrollment.course.available?
@problem = t "#application.errors.feed_unpublished_course", "Feeds for this course cannot be accessed until it is published."
end
@context = @enrollment.course unless @problem
@current_user = @enrollment.user unless @problem
elsif pieces[0] == 'group_membership'
@membership = GroupMembership.active.where(uuid: pieces[1]).first if pieces[1]
@context_type = "Group"
if !@membership
@problem = t "#application.errors.mismatched_verification_code", "The verification code does not match any currently enrolled user."
elsif @membership.group && !@membership.group.available?
@problem = t "#application.errors.feed_unpublished_group", "Feeds for this group cannot be accessed until it is published."
end
@context = @membership.group unless @problem
@current_user = @membership.user unless @problem
else
@context_type = pieces[0].classify
if Context::CONTEXT_TYPES.include?(@context_type.to_sym)
@context_class = Object.const_get(@context_type, false)
@context = @context_class.where(uuid: pieces[1]).first if pieces[1]
end
if !@context
@problem = t "#application.errors.invalid_verification_code", "The verification code is invalid."
elsif (!@context.is_public rescue false) && (!@context.respond_to?(:uuid) || pieces[1] != @context.uuid)
if @context_type == 'course'
@problem = t "#application.errors.feed_private_course", "The matching course has gone private, so public feeds like this one will no longer be visible."
elsif @context_type == 'group'
@problem = t "#application.errors.feed_private_group", "The matching group has gone private, so public feeds like this one will no longer be visible."
else
@problem = t "#application.errors.feed_private", "The matching context has gone private, so public feeds like this one will no longer be visible."
end
end
@context = nil if @problem
@current_user = @context if @context.is_a?(User)
end
if !@context || (opts[:only] && !opts[:only].include?(@context.class.to_s.underscore.to_sym))
@problem ||= t("#application.errors.invalid_feed_parameters", "Invalid feed parameters.") if (opts[:only] && !opts[:only].include?(@context.class.to_s.underscore.to_sym))
@problem ||= t "#application.errors.feed_not_found", "Could not find feed."
render template: "shared/unauthorized_feed", status: :bad_request, formats: [:html]
return false
end
@context
end
def discard_flash_if_xhr
if request.xhr? || request.format.to_s == 'text/plain'
flash.discard
end
end
def cancel_cache_buster
@cancel_cache_buster = true
end
def cache_buster
# Annoying problem. If I set the cache-control to anything other than "no-cache, no-store"
# then the local cache is used when the user clicks the 'back' button. I don't know how
# to tell the browser to ALWAYS check back other than to disable caching...
return true if @cancel_cache_buster || request.xhr? || api_request?
set_no_cache_headers
end
def initiate_session_from_token
# Login from a token generated via API
if params[:session_token]
token = SessionToken.parse(params[:session_token])
if token&.valid?
pseudonym = Pseudonym.active.find_by(id: token.pseudonym_id)
if pseudonym
unless pseudonym.works_for_account?(@domain_root_account, true)
# if the logged in pseudonym doesn't work, we can only switch to another pseudonym
# that does work if it's the same password, and it's not a managed pseudonym
alternates = pseudonym.user.all_active_pseudonyms.select { |p|
!p.managed_password? &&
p.works_for_account?(@domain_root_account, true) &&
p.password_salt == pseudonym.password_salt &&
p.crypted_password == pseudonym.crypted_password }
# prefer a site admin pseudonym, then a pseudonym in this account, and then any old
# pseudonym
pseudonym = alternates.find { |p| p.account_id == Account.site_admin.id }
pseudonym ||= alternates.find { |p| p.account_id == @domain_root_account.id }
pseudonym ||= alternates.first
end
if pseudonym && pseudonym != @current_pseudonym
return_to = session.delete(:return_to)
reset_session_saving_keys(:oauth2)
PseudonymSession.create!(pseudonym)
session[:used_remember_me_token] = true if token.used_remember_me_token
end
if pseudonym && token.current_user_id
target_user = User.find(token.current_user_id)
session[:become_user_id] = token.current_user_id if target_user.can_masquerade?(pseudonym.user, @domain_root_account)
end
end
return redirect_to return_to if return_to
if (oauth = session[:oauth2])
provider = Canvas::Oauth::Provider.new(oauth[:client_id], oauth[:redirect_uri], oauth[:scopes], oauth[:purpose])
return redirect_to Canvas::Oauth::Provider.confirmation_redirect(self, provider, pseudonym.user)
end
# do one final redirect to get the token out of the URL
redirect_to remove_query_params(request.original_url, 'session_token')
end
end
end
def remove_query_params(url, *params)
uri = URI.parse(url)
return url unless uri.query
qs = Rack::Utils.parse_query(uri.query)
qs.except!(*params)
uri.query = qs.empty? ? nil : Rack::Utils.build_query(qs)
uri.to_s
end
def set_no_cache_headers
response.headers["Pragma"] = "no-cache"
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
end
def clear_cached_contexts
RoleOverride.clear_cached_contexts
end
def set_page_view
return true if !page_views_enabled?
ENV['RAILS_HOST_WITH_PORT'] ||= request.host_with_port rescue nil
# We only record page_views for html page requests coming from within the
# app, or if coming from a developer api request and specified as a
# page_view.
if @current_user && !request.xhr? && request.get?
generate_page_view
end
end
def require_reacceptance_of_terms
if session[:require_terms] && request.get? && !api_request? && !verified_file_request?
render "shared/terms_required", status: :unauthorized
false
end
end
def clear_policy_cache
AdheresToPolicy::Cache.clear
end
def generate_page_view(user=@current_user)
attributes = { :user => user, :real_user => @real_current_user }
@page_view = PageView.generate(request, attributes)
@page_view.user_request = true if params[:user_request] || (user && !request.xhr? && request.get?)
@page_before_render = Time.now.utc
end
def disable_page_views
@log_page_views = false
true
end
def update_enrollment_last_activity_at
activity = Enrollment::RecentActivity.new(@context_enrollment, @context)
activity.record_for_access(response)
end
# Asset accesses are used for generating usage statistics. This is how
# we say, "the user just downloaded this file" or "the user just
# viewed this wiki page". We can then after-the-fact build statistics
# and reports from these accesses. This is currently being used
# to generate access reports per student per course.
#
# If asset is an AR model, then its asset_string will be used. If it's an array,
# it should look like [ "subtype", context ], like [ "pages", course ].
def log_asset_access(asset, asset_category, asset_group=nil, level=nil, membership_type=nil, overwrite:true)
user = @current_user
user ||= User.where(id: session['file_access_user_id']).first if session['file_access_user_id'].present?
return unless user && @context && asset
return if asset.respond_to?(:new_record?) && asset.new_record?
code = if asset.is_a?(Array)
"#{asset[0]}:#{asset[1].asset_string}"
else
asset.asset_string
end
membership_type ||= @context_membership && @context_membership.class.to_s
group_code = if asset_group.is_a?(String)
asset_group
elsif asset_group.respond_to?(:asset_string)
asset_group.asset_string
else
'unknown'
end
if !@accessed_asset || overwrite
@accessed_asset = {
:user => user,
:code => code,
:group_code => group_code,
:category => asset_category,
:membership_type => membership_type,
:level => level
}
end
Canvas::LiveEvents.asset_access(asset, asset_category, membership_type, level)
@accessed_asset
end
def log_page_view
return true if !page_views_enabled?
user = @current_user || (@accessed_asset && @accessed_asset[:user])
if user && @log_page_views != false
add_interaction_seconds
log_participation(user)
log_gets
finalize_page_view
else
@page_view.destroy if @page_view && !@page_view.new_record?
end
rescue StandardError, CassandraCQL::Error::InvalidRequestException => e
Canvas::Errors.capture_exception(:page_view, e)
logger.error "Pageview error!"
raise e if Rails.env.development?
true
end
def add_interaction_seconds
updated_fields = params.slice(:interaction_seconds)
if request.xhr? && params[:page_view_token] && !updated_fields.empty? && !(@page_view && @page_view.generated_by_hand)
RequestContextGenerator.store_interaction_seconds_update(params[:page_view_token], updated_fields[:interaction_seconds])
page_view_info = PageView.decode_token(params[:page_view_token])
@page_view = PageView.find_for_update(page_view_info[:request_id])
if @page_view
if @page_view.id
response.headers["X-Canvas-Page-View-Update-Url"] = page_view_path(
@page_view.id, page_view_token: @page_view.token)
end
@page_view.do_update(updated_fields)
@page_view_update = true
end
end
end
def log_participation(user)
# If we're logging the asset access, and it's either a participatory action
# or it's not an update to an already-existing page_view. We check to make sure
# it's not an update because if the page_view already existed, we don't want to
# double-count it as multiple views when it's really just a single view.
if @accessed_asset && (@accessed_asset[:level] == 'participate' || !@page_view_update)
@access = AssetUserAccess.where(user_id: user.id, asset_code: @accessed_asset[:code]).first_or_initialize
@accessed_asset[:level] ||= 'view'
@access.log @context, @accessed_asset
if @page_view.nil? && page_views_enabled? && %w{participate submit}.include?(@accessed_asset[:level])
generate_page_view(user)
end
if @page_view
@page_view.participated = %w{participate submit}.include?(@accessed_asset[:level])
@page_view.asset_user_access = @access
end
@page_view_update = true
end
end
def log_gets
if @page_view && !request.xhr? && request.get? && (((response.content_type || "").to_s.match(/html/)) ||
(Setting.get('create_get_api_page_views', 'true') == 'true') && api_request?)
@page_view.render_time ||= (Time.now.utc - @page_before_render) rescue nil
@page_view_update = true
end
end
def finalize_page_view
if @page_view && @page_view_update
@page_view.context = @context if !@page_view.context_id && PageView::CONTEXT_TYPES.include?(@context.class.name)
@page_view.account_id = @domain_root_account.id
@page_view.developer_key_id = @access_token.try(:developer_key_id)
@page_view.store
RequestContextGenerator.store_page_view_meta(@page_view)
end
end
rescue_from Exception, :with => :rescue_exception
# analogous to rescue_action_without_handler from ActionPack 2.3
def rescue_exception(exception)
ActiveSupport::Deprecation.silence do
message = "\n#{exception.class} (#{exception.message}):\n"
message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
message << " " << exception.backtrace.join("\n ")
logger.fatal("#{message}\n\n")
end
if config.consider_all_requests_local
rescue_action_locally(exception)
else
rescue_action_in_public(exception)
end
end
def interpret_status(code)
message = Rack::Utils::HTTP_STATUS_CODES[code]
code, message = [500, Rack::Utils::HTTP_STATUS_CODES[500]] unless message
"#{code} #{message}"
end
def response_code_for_rescue(exception)
ActionDispatch::ExceptionWrapper.status_code_for_exception(exception.class.name)
end
def render_optional_error_file(status)
path = "#{Rails.public_path}/#{status.to_s[0,3]}"
if File.exist?(path)
render :file => path, :status => status, :content_type => Mime::HTML, :layout => false, :formats => [:html]
else
head status
end
end
# Custom error catching and message rendering.
def rescue_action_in_public(exception)
response_code = exception.response_status if exception.respond_to?(:response_status)
@show_left_side = exception.show_left_side if exception.respond_to?(:show_left_side)
response_code ||= response_code_for_rescue(exception) || 500
begin
status_code = interpret_status(response_code)
status = status_code
status = 'AUT' if exception.is_a?(ActionController::InvalidAuthenticityToken)
type = nil
type = '404' if status == '404 Not Found'
# TODO: get rid of exceptions that implement this "skip_error_report?" thing, instead
# use the initializer in config/initializers/errors.rb to configure
# exceptions we want skipped
unless exception.respond_to?(:skip_error_report?) && exception.skip_error_report?
opts = {type: type}
opts[:canvas_error_info] = exception.canvas_error_info if exception.respond_to?(:canvas_error_info)
info = Canvas::Errors::Info.new(request, @domain_root_account, @current_user, opts)
error_info = info.to_h
error_info[:tags][:response_code] = response_code
capture_outputs = Canvas::Errors.capture(exception, error_info)
error = nil
if capture_outputs[:error_report]
error = ErrorReport.find(capture_outputs[:error_report])
end
end
if api_request?
rescue_action_in_api(exception, error, response_code)
else
render_rescue_action(exception, error, status, status_code)
end
rescue => e
# error generating the error page? failsafe.
Canvas::Errors.capture(e)
render_optional_error_file response_code_for_rescue(exception)
end
end
def render_xhr_exception(error, message = nil, status = "500 Internal Server Error", status_code = 500)
message ||= "Unexpected error, ID: #{error.id rescue "unknown"}"
render status: status_code, json: {
errors: {
base: message
},
status: status
}
end
def render_rescue_action(exception, error, status, status_code)
clear_crumbs
@headers = nil
load_account unless @domain_root_account
session[:last_error_id] = error.id rescue nil
if request.xhr? || request.format == :text
message = exception.xhr_message if exception.respond_to?(:xhr_message)
render_xhr_exception(error, message, status, status_code)
elsif exception.is_a?(ActionController::InvalidAuthenticityToken) && cookies[:_csrf_token].blank?
redirect_to login_url(needs_cookies: '1')
reset_session
return
else
request.format = :html
template = exception.error_template if exception.respond_to?(:error_template)
unless template
template = "shared/errors/#{status.to_s[0,3]}_message"
erbpath = Rails.root.join('app', 'views', "#{template}.html.erb")
template = "shared/errors/500_message" unless erbpath.file?
end
@status_code = status_code
message = exception.is_a?(RequestError) ? exception.message : nil
render template: template,
layout: 'application',
status: status_code,
formats: [:html],
locals: {
error: error,
exception: exception,
status: status,
message: message,
}
end
end
def rescue_action_in_api(exception, error_report, response_code)
data = exception.error_json if exception.respond_to?(:error_json)
data ||= api_error_json(exception, response_code)
if error_report.try(:id)
data[:error_report_id] = error_report.id
end
render :json => data, :status => response_code
end
def api_error_json(exception, status_code)
case exception
when ActiveRecord::RecordInvalid
errors = exception.record.errors
errors.set_reporter(:hash, Api::Errors::Reporter)
data = errors.to_hash
when Api::Error
errors = ActiveModel::BetterErrors::Errors.new(nil)
errors.error_collection.add(:base, exception.error_id, message: exception.message)
errors.set_reporter(:hash, Api::Errors::Reporter)
data = errors.to_hash
when ActiveRecord::RecordNotFound
data = { errors: [{message: 'The specified resource does not exist.'}] }
when AuthenticationMethods::AccessTokenError
add_www_authenticate_header
data = { errors: [{message: 'Invalid access token.'}] }
when ActionController::ParameterMissing
data = { errors: [{message: "#{exception.param} is missing"}] }
when BasicLTI::BasicOutcomes::Unauthorized,
BasicLTI::BasicOutcomes::InvalidRequest
data = { errors: [{message: exception.message}] }
else
if status_code.is_a?(Symbol)
status_code_string = status_code.to_s
else
# we want to return a status string of the form "not_found", so take the rails-style "Not Found" and tweak it
status_code_string = interpret_status(status_code).sub(/\d\d\d /, '').gsub(' ', '').underscore
end
data = { errors: [{message: "An error occurred.", error_code: status_code_string}] }
end
data
end
def rescue_action_locally(exception)
if api_request? or exception.is_a? RequestError
# we want api requests to behave the same on error locally as in prod, to
# ease testing and development. you can still view the backtrace, etc, in
# the logs.
rescue_action_in_public(exception)
else
super
end
end
def claim_session_course(course, user, state=nil)
e = course.claim_with_teacher(user)
session[:claimed_enrollment_uuids] ||= []
session[:claimed_enrollment_uuids] << e.uuid
session[:claimed_enrollment_uuids].uniq!
flash[:notice] = t "#application.notices.first_teacher", "This course is now claimed, and you've been registered as its first teacher."
if !@current_user && state == :just_registered
flash[:notice] = t "#application.notices.first_teacher_with_email", "This course is now claimed, and you've been registered as its first teacher. You should receive an email shortly to complete the registration process."
end
session[:claimed_course_uuids] ||= []
session[:claimed_course_uuids] << course.uuid
session[:claimed_course_uuids].uniq!
session.delete(:claim_course_uuid)
session.delete(:course_uuid)
end
API_REQUEST_REGEX = %r{\A/api/}
def api_request?
@api_request ||= !!request.path.match(API_REQUEST_REGEX)
end
def verified_file_request?
params[:controller] == 'files' && params[:action] == 'show' && params[:verifier].present?
end
# Retrieving wiki pages needs to search either using the id or
# the page title.
def get_wiki_page
@wiki = @context.wiki
@page_name = params[:wiki_page_id] || params[:id] || (params[:wiki_page] && params[:wiki_page][:title])
if(params[:format] && !['json', 'html'].include?(params[:format]))
@page_name += ".#{params[:format]}"
params[:format] = 'html'
end
return if @page || !@page_name
@page = @wiki.find_page(@page_name) if params[:action] != 'create'
unless @page
if params[:titleize].present? && !value_to_boolean(params[:titleize])
@page_name = CGI.unescape(@page_name)
@page = @wiki.build_wiki_page(@current_user, :title => @page_name)
else
@page = @wiki.build_wiki_page(@current_user, :url => @page_name)
end
end
end
def content_tag_redirect(context, tag, error_redirect_symbol, tag_type=nil)
url_params = { :module_item_id => tag.id }
if tag.content_type == 'Assignment'
redirect_to named_context_url(context, :context_assignment_url, tag.content_id, url_params)
elsif tag.content_type == 'WikiPage'
redirect_to polymorphic_url([context, tag.content], url_params)
elsif tag.content_type == 'Attachment'
redirect_to named_context_url(context, :context_file_url, tag.content_id, url_params)
elsif tag.content_type_quiz?
redirect_to named_context_url(context, :context_quiz_url, tag.content_id, url_params)
elsif tag.content_type == 'DiscussionTopic'
redirect_to named_context_url(context, :context_discussion_topic_url, tag.content_id, url_params)
elsif tag.content_type == 'Rubric'
redirect_to named_context_url(context, :context_rubric_url, tag.content_id, url_params)
elsif tag.content_type == 'AssessmentQuestionBank'
redirect_to named_context_url(context, :context_question_bank_url, tag.content_id, url_params)
elsif tag.content_type == 'Lti::MessageHandler'
url_params[:module_item_id] = params[:module_item_id] if params[:module_item_id]
url_params[:resource_link_fragment] = "ContentTag:#{tag.id}"
redirect_to named_context_url(context, :context_basic_lti_launch_request_url, tag.content_id, url_params)
elsif tag.content_type == 'ExternalUrl'
@tag = tag
@module = tag.context_module
log_asset_access(@tag, "external_urls", "external_urls")
if tag.locked_for? @current_user
render 'context_modules/lock_explanation'
else
tag.context_module_action(@current_user, :read)
render 'context_modules/url_show'
end
elsif tag.content_type == 'ContextExternalTool'
@tag = tag
if tag.locked_for? @current_user
return render 'context_modules/lock_explanation'
end
if @tag.context.is_a?(Assignment)
@assignment = @tag.context
@resource_title = @assignment.title
@module_tag = @context.context_module_tags.not_deleted.find(params[:module_item_id]) if params[:module_item_id]
else
@module_tag = @tag
@resource_title = @tag.title
end
@resource_url = @tag.url
@tool = ContextExternalTool.find_external_tool(tag.url, context, tag.content_id)
tag.context_module_action(@current_user, :read)
if !@tool
flash[:error] = t "#application.errors.invalid_external_tool", "Couldn't find valid settings for this link"
redirect_to named_context_url(context, error_redirect_symbol)
else
log_asset_access(@tool, "external_tools", "external_tools", overwrite: false)
@opaque_id = @tool.opaque_identifier_for(@tag)
launch_settings = @tool.settings['post_only'] ? {post_only: true, tool_dimensions: tool_dimensions} : {tool_dimensions: tool_dimensions}
@lti_launch = Lti::Launch.new(launch_settings)
success_url = case tag_type
when :assignments
named_context_url(@context, :context_assignments_url, include_host: true)
when :modules
named_context_url(@context, :context_context_modules_url, include_host: true)
else
named_context_url(@context, :context_url, include_host: true)
end
if tag.new_tab
@lti_launch.launch_type = 'window'
@return_url = success_url
else
if @context
@return_url = named_context_url(@context, :context_external_content_success_url, 'external_tool_redirect', include_host: true)
else
@return_url = external_content_success_url('external_tool_redirect')
end
@redirect_return = true
js_env(:redirect_return_success_url => success_url,
:redirect_return_cancel_url => success_url)
end
opts = {
launch_url: @resource_url,
link_code: @opaque_id,
overrides: {'resource_link_title' => @resource_title},
}
variable_expander = Lti::VariableExpander.new(@domain_root_account, @context, self,{
current_user: @current_user,
current_pseudonym: @current_pseudonym,
content_tag: @module_tag || tag,
assignment: @assignment,
tool: @tool})
adapter = Lti::LtiOutboundAdapter.new(@tool, @current_user, @context).prepare_tool_launch(@return_url, variable_expander, opts)
if tag.try(:context_module)
add_crumb tag.context_module.name, context_url(@context, :context_context_modules_url)
end
if @assignment
return unless require_user
add_crumb(@resource_title)
@prepend_template = 'assignments/description'
@lti_launch.params = 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
@lti_launch.params = adapter.generate_post_payload
end
@lti_launch.resource_url = @resource_url
@lti_launch.link_text = @resource_title
@lti_launch.analytics_id = @tool.tool_id
@append_template = 'context_modules/tool_sequence_footer'
render Lti::AppUtil.display_template(params['display'])
end
else
flash[:error] = t "#application.errors.invalid_tag_type", "Didn't recognize the item type for this tag"
redirect_to named_context_url(context, error_redirect_symbol)
end
end
# pass it a context or an array of contexts and it will give you a link to the
# person's calendar with only those things checked.
def calendar_url_for(contexts_to_link_to = nil, options={})
options[:query] ||= {}
contexts_to_link_to = Array(contexts_to_link_to)
if event = options.delete(:event)
options[:query][:event_id] = event.id
end
options[:query][:include_contexts] = contexts_to_link_to.map{|c| c.asset_string}.join(",") unless contexts_to_link_to.empty?
calendar_url(options[:query])
end
# pass it a context or an array of contexts and it will give you a link to the
# person's files browser for the supplied contexts.
def files_url_for(contexts_to_link_to = nil, options={})
options[:query] ||= {}
contexts_to_link_to = Array(contexts_to_link_to)
unless contexts_to_link_to.empty?
options[:anchor] = "#{contexts_to_link_to.first.asset_string}"
end
options[:query][:include_contexts] = contexts_to_link_to.map{|c| c.asset_string}.join(",") unless contexts_to_link_to.empty?
url_for(
options[:query].merge({
:controller => 'files',
:action => "full_index",
}.merge(options[:anchor].empty? ? {} : {
:anchor => options[:anchor]
})
)
)
end
helper_method :calendar_url_for, :files_url_for
def conversations_path(params={})
if @current_user and @current_user.use_new_conversations?
query_string = params.slice(:context_id, :user_id, :user_name).inject([]) do |res, (k, v)|
res << "#{k}=#{v}"
res
end.join('&')
"/conversations?#{query_string}"
else
hash = params.keys.empty? ? '' : "##{params.to_json.unpack('H*').first}"
"/conversations#{hash}"
end
end
helper_method :conversations_path
# escape everything but slashes, see http://code.google.com/p/phusion-passenger/issues/detail?id=113
FILE_PATH_ESCAPE_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}/]")
def safe_domain_file_url(attachment, host_and_shard=nil, verifier = nil, download = false) # TODO: generalize this
if !host_and_shard
host_and_shard = HostUrl.file_host_with_shard(@domain_root_account || Account.default, request.host_with_port)
end
host, shard = host_and_shard
res = "#{request.protocol}#{host}"
shard.activate do
ts, sig = @current_user && @current_user.access_verifier
# add parameters so that the other domain can create a session that
# will authorize file access but not full app access. We need this in
# case there are relative URLs in the file that point to other pieces
# of content.
opts = { :user_id => @current_user.try(:id), :ts => ts, :sf_verifier => sig }
opts[:verifier] = verifier if verifier.present?
if download
# download "for realz, dude" (see later comments about :download)
opts[:download_frd] = 1
else
# don't set :download here, because file_download_url won't like it. see
# comment below for why we'd want to set :download
opts[:inline] = 1
end
if @context && Attachment.relative_context?(@context.class.base_class) && @context == attachment.context
# so yeah, this is right. :inline=>1 wants :download=>1 to go along with
# it, so we're setting :download=>1 *because* we want to display inline.
opts[:download] = 1 unless download
# if the context is one that supports relative paths (which requires extra
# routes and stuff), then we'll build an actual named_context_url with the
# params for show_relative
res += named_context_url(@context, :context_file_url, attachment)
res += '/' + URI.escape(attachment.full_display_path, FILE_PATH_ESCAPE_PATTERN)
res += '?' + opts.to_query
else
# otherwise, just redirect to /files/:id
res += file_download_url(attachment, opts.merge(:only_path => true))
end
end
res
end
helper_method :safe_domain_file_url
def feature_enabled?(feature)
@features_enabled ||= {}
feature = feature.to_sym
return @features_enabled[feature] if @features_enabled[feature] != nil
@features_enabled[feature] ||= begin
if [:question_banks].include?(feature)
true
elsif feature == :yo
Canvas::Plugin.find(:yo).try(:enabled?)
elsif feature == :twitter
!!Twitter::Connection.config
elsif feature == :linked_in
!!LinkedIn::Connection.config
elsif feature == :diigo
!!Diigo::Connection.config
elsif feature == :google_drive
Canvas::Plugin.find(:google_drive).try(:enabled?)
elsif feature == :etherpad
!!EtherpadCollaboration.config
elsif feature == :kaltura
!!CanvasKaltura::ClientV3.config
elsif feature == :web_conferences
!!WebConference.config
elsif feature == :crocodoc
!!Canvas::Crocodoc.config
elsif feature == :vericite
Canvas::Plugin.find(:vericite).try(:enabled?)
elsif feature == :lockdown_browser
Canvas::Plugin.all_for_tag(:lockdown_browser).any? { |p| p.settings[:enabled] }
else
false
end
end
end
helper_method :feature_enabled?
def service_enabled?(service)
@domain_root_account && @domain_root_account.service_enabled?(service)
end
helper_method :service_enabled?
def feature_and_service_enabled?(feature)
feature_enabled?(feature) && service_enabled?(feature)
end
helper_method :feature_and_service_enabled?
def temporary_user_code(generate=true)
if generate
session[:temporary_user_code] ||= "tmp_#{Digest::MD5.hexdigest("#{Time.now.to_i}_#{rand}")}"
else
session[:temporary_user_code]
end
end
def require_account_management(on_root_account = false)
if (!@context.root_account? && on_root_account) || !@context.is_a?(Account)
redirect_to named_context_url(@context, :context_url)
return false
else
return false unless authorized_action(@context, @current_user, :manage_account_settings)
end
end
def require_root_account_management
require_account_management(true)
end
def require_site_admin_with_permission(permission)
require_context_with_permission(Account.site_admin, permission)
end
def require_context_with_permission(context, permission)
unless context.grants_right?(@current_user, permission)
respond_to do |format|
format.html do
if @current_user
flash[:error] = t "#application.errors.permission_denied", "You don't have permission to access that page"
redirect_to root_url
else
redirect_to_login
end
end
format.json do
render_json_unauthorized
end
end
return false
end
end
def require_registered_user
return false if require_user == false
unless @current_user.registered?
respond_to do |format|
format.html { render "shared/registration_incomplete", status: :unauthorized }
format.json { render :json => { 'status' => 'unauthorized', 'message' => t('#errors.registration_incomplete', 'You need to confirm your email address before you can view this page') }, :status => :unauthorized }
end
return false
end
end
def check_incomplete_registration
if @current_user
js_env :INCOMPLETE_REGISTRATION => incomplete_registration?, :USER_EMAIL => @current_user.email
end
end
def incomplete_registration?
@current_user && params[:registration_success] && @current_user.pre_registered?
end
helper_method :incomplete_registration?
def page_views_enabled?
PageView.page_views_enabled?
end
helper_method :page_views_enabled?
def verified_file_download_url(attachment, context = nil, permission_map_id = nil, *opts)
verifier = Attachments::Verification.new(attachment).verifier_for_user(@current_user,
context: context.try(:asset_string), permission_map_id: permission_map_id)
file_download_url(attachment, { :verifier => verifier }, *opts)
end
helper_method :verified_file_download_url
def user_content(str, cache_key = nil)
return nil unless str
return str.html_safe unless str.match(/object|embed|equation_image/)
UserContent.escape(str, request.host_with_port)
end
helper_method :user_content
def public_user_content(str, context=@context, user=@current_user, is_public=false)
return nil unless str
rewriter = UserContent::HtmlRewriter.new(context, user)
rewriter.set_handler('files') do |match|
UserContent::FilesHandler.new(
match: match,
context: context,
user: user,
preloaded_attachments: {},
is_public: is_public
).processed_url
end
UserContent.escape(rewriter.translate_content(str), request.host_with_port)
end
helper_method :public_user_content
def find_bank(id, check_context_chain=true)
bank = @context.assessment_question_banks.active.where(id: id).first || @current_user.assessment_question_banks.active.where(id: id).first
if bank
(block_given? ?
authorized_action(bank, @current_user, :read) :
bank.grants_right?(@current_user, session, :read)) or return nil
elsif check_context_chain
(block_given? ?
authorized_action(@context, @current_user, :read_question_banks) :
@context.grants_right?(@current_user, session, :read_question_banks)) or return nil
bank = @context.inherited_assessment_question_banks.where(id: id).first
end
yield if block_given? && (@bank = bank)
bank
end
def prepend_json_csrf?
requested_json = request.headers['Accept'] =~ %r{application/json}
request.get? && !requested_json && in_app?
end
def in_app?
@pseudonym_session
end
def json_as_text?
(request.headers['CONTENT_TYPE'].to_s =~ %r{multipart/form-data}) &&
(params[:format].to_s != 'json' || in_app?)
end
def params_are_integers?(*check_params)
begin
check_params.each{ |p| Integer(params[p]) }
rescue ArgumentError
return false
end
true
end
def destroy_session
logger.info "Destroying session: #{session[:session_id]}"
@pseudonym_session.destroy rescue true
reset_session
end
def logout_current_user
@current_user.try(:stamp_logout_time!)
destroy_session
end
def set_layout_options
@embedded_view = params[:embedded]
@headers = false if params[:no_headers]
(@body_classes ||= []) << 'embedded' if @embedded_view
end
def stringify_json_ids?
request.headers['Accept'] =~ %r{application/json\+canvas-string-ids}
end
def json_cast(obj)
obj = obj.as_json if obj.respond_to?(:as_json) unless obj.is_a?(Hash) || obj.is_a?(Array)
stringify_json_ids? ? StringifyIds.recursively_stringify_ids(obj) : obj
end
def render(options = nil, extra_options = {}, &block)
set_layout_options
if options.is_a?(Hash) && options.key?(:json)
json = options.delete(:json)
unless json.is_a?(String)
json_cast(json)
json = ActiveSupport::JSON.encode(json)
end
# prepend our CSRF protection to the JSON response, unless this is an API
# call that didn't use session auth, or a non-GET request.
if prepend_json_csrf?
json = "while(1);#{json}"
end
# fix for some browsers not properly handling json responses to multipart
# file upload forms and s3 upload success redirects -- we'll respond with text instead.
if options[:as_text] || json_as_text?
options[:html] = json.html_safe
else
options[:json] = json
end
end
super
end
# flash is normally only preserved for one redirect; make sure we carry
# it along in case there are more
def redirect_to(*)
flash.keep
super
end
def css_bundles
@css_bundles ||= []
end
helper_method :css_bundles
def css_bundle(*args)
opts = (args.last.is_a?(Hash) ? args.pop : {})
Array(args).flatten.each do |bundle|
css_bundles << [bundle, opts[:plugin]] unless css_bundles.include? [bundle, opts[:plugin]]
end
nil
end
helper_method :css_bundle
def js_bundles; @js_bundles ||= []; end
helper_method :js_bundles
# Use this method to place a bundle on the page, note that the end goal here
# is to only ever include one bundle per page load, so use this with care and
# ensure that the bundle you are requiring isn't simply a dependency of some
# other bundle.
#
# Bundles are defined in app/coffeescripts/bundles/<bundle>.coffee
#
# usage: js_bundle :gradebook
#
# Only allows multiple arguments to support old usage of jammit_js
#
# Optional :plugin named parameter allows you to specify a plugin which
# contains the bundle. Example:
#
# js_bundle :gradebook, :plugin => :my_feature
#
# will look for the bundle in
# /plugins/my_feature/(optimized|javascripts)/compiled/bundles/ rather than
# /(optimized|javascripts)/compiled/bundles/
def js_bundle(*args)
opts = (args.last.is_a?(Hash) ? args.pop : {})
Array(args).flatten.each do |bundle|
js_bundles << [bundle, opts[:plugin]] unless js_bundles.include? [bundle, opts[:plugin]]
end
nil
end
helper_method :js_bundle
def get_course_from_section
if params[:section_id]
@section = api_find(CourseSection, params.delete(:section_id))
params[:course_id] = @section.course_id
end
end
def reject_student_view_student
return unless @current_user && @current_user.fake_student?
@unauthorized_message ||= t('#application.errors.student_view_unauthorized', "You cannot access this functionality in student view.")
render_unauthorized_action
end
def set_site_admin_context
@context = Account.site_admin
add_crumb t('#crumbs.site_admin', "Site Admin"), url_for(Account.site_admin)
end
def flash_notices
@notices ||= begin
notices = []
if !browser_supported? && !@embedded_view && !cookies['unsupported_browser_dismissed']
notices << {:type => 'warning', :content => {html: unsupported_browser}, :classes => 'unsupported_browser'}
end
if error = flash[:error]
flash.delete(:error)
notices << {:type => 'error', :content => error, :icon => 'warning'}
end
if warning = flash[:warning]
flash.delete(:warning)
notices << {:type => 'warning', :content => warning, :icon => 'warning'}
end
if info = flash[:info]
flash.delete(:info)
notices << {:type => 'info', :content => info, :icon => 'info'}
end
if notice = (flash[:html_notice] ? {html: flash[:html_notice]} : flash[:notice])
if flash[:html_notice]
flash.delete(:html_notice)
else
flash.delete(:notice)
end
notices << {:type => 'success', :content => notice, :icon => 'check'}
end
notices
end
end
helper_method :flash_notices
def unsupported_browser
t("Your browser does not meet the minimum requirements for Canvas. Please visit the *Canvas Community* for a complete list of supported browsers.", :wrapper => view_context.link_to('\1', 'https://community.canvaslms.com/docs/DOC-1284'))
end
def browser_supported?
# the user_agent gem likes to (ab)use objects and metaprogramming, so
# we just do this check once per session. or maybe more than once, if
# you upgrade your browser and it treats session cookie expiration
# rules as a suggestion
key = request.user_agent.to_s.sum # keep cookie size in check. a legitimate collision here would be 1. extremely unlikely and 2. not a big deal
if key != session[:browser_key]
session[:browser_key] = key
session[:browser_supported] = Browser.supported?(request.user_agent)
end
session[:browser_supported]
end
def mobile_device?
params[:mobile] || request.user_agent.to_s =~ /ipod|iphone|ipad|Android/i
end
def ms_office?
!!(request.user_agent.to_s =~ /ms-office/) ||
!!(request.user_agent.to_s =~ %r{Word/\d+\.\d+})
end
def profile_data(profile, viewer, session, includes)
extend Api::V1::UserProfile
extend Api::V1::Course
extend Api::V1::Group
includes ||= []
data = user_profile_json(profile, viewer, session, includes, profile)
data[:can_edit] = viewer == profile.user
data[:can_edit_name] = data[:can_edit] && profile.user.user_can_edit_name?
data[:known_user] = viewer.address_book.known_user(profile.user)
if data[:known_user] && viewer != profile.user
common_courses = viewer.address_book.common_courses(profile.user)
common_groups = viewer.address_book.common_groups(profile.user)
else
common_courses = {}
common_groups = {}
end
data[:common_contexts] = common_contexts(common_courses, common_groups, @current_user, session)
data
end
def common_contexts(common_courses, common_groups, current_user, session)
courses = Course.where(id: common_courses.keys).to_a
groups = Group.where(id: common_groups.keys).to_a
common_courses = courses.map do |course|
course_json(course, current_user, session, ['html_url'], false).merge({
roles: common_courses[course.id].map { |role| Enrollment.readable_type(role) }
})
end
common_groups = groups.map do |group|
group_json(group, current_user, session, include: ['html_url']).merge({
# in the future groups will have more roles and we'll need soemthing similar to
# the roles.map above in courses
roles: [t('#group.memeber', "Member")]
})
end
common_courses + common_groups
end
def self.batch_jobs_in_actions(opts = {})
batch_opts = opts.delete(:batch)
around_action(opts) do |controller, action|
Delayed::Batch.serial_batch(batch_opts || {}) do
action.call
end
end
end
def not_found
raise ActionController::RoutingError.new('Not Found')
end
def set_js_rights(objtypes = nil)
objtypes ||= js_rights if respond_to?(:js_rights)
if objtypes
hash = {}
objtypes.each do |instance_symbol|
instance_name = instance_symbol.to_s
obj = instance_variable_get("@#{instance_name}")
policy = obj.check_policy(@current_user, session) unless obj.nil? || !obj.respond_to?(:check_policy)
hash["#{instance_name.upcase}_RIGHTS".to_sym] = HashWithIndifferentAccess[policy.map { |right| [right, true] }] unless policy.nil?
end
js_env hash
end
end
def set_js_wiki_data(opts = {})
hash = {}
hash[:DEFAULT_EDITING_ROLES] = @context.default_wiki_editing_roles if @context.respond_to?(:default_wiki_editing_roles)
hash[:WIKI_PAGES_PATH] = polymorphic_path([@context, :wiki_pages])
if opts[:course_home]
hash[:COURSE_HOME] = true
hash[:COURSE_TITLE] = @context.name
end
if @page
if @context.wiki.grants_right?(@current_user, :manage)
mc_status = setup_master_course_restrictions(@page, @context)
end
hash[:WIKI_PAGE] = wiki_page_json(@page, @current_user, session, true, :deep_check_if_needed => true, :master_course_status => mc_status)
hash[:WIKI_PAGE_REVISION] = (current_version = @page.versions.current) ? StringifyIds.stringify_id(current_version.number) : nil
hash[:WIKI_PAGE_SHOW_PATH] = named_context_url(@context, :context_wiki_page_path, @page)
hash[:WIKI_PAGE_EDIT_PATH] = named_context_url(@context, :edit_context_wiki_page_path, @page)
hash[:WIKI_PAGE_HISTORY_PATH] = named_context_url(@context, :context_wiki_page_revisions_path, @page)
end
if @context.is_a?(Course) && @context.grants_right?(@current_user, session, :read)
hash[:COURSE_ID] = @context.id.to_s
hash[:MODULES_PATH] = polymorphic_path([@context, :context_modules])
end
js_env hash
end
def set_js_assignment_data
rights = [:manage_assignments, :manage_grades, :read_grades, :manage]
permissions = @context.rights_status(@current_user, *rights)
permissions[:manage_course] = permissions[:manage]
permissions[:manage] = permissions[:manage_assignments]
js_env({
:URLS => {
:new_assignment_url => new_polymorphic_url([@context, :assignment]),
:course_url => api_v1_course_url(@context),
:sort_url => reorder_course_assignment_groups_url(@context),
:assignment_sort_base_url => course_assignment_groups_url(@context),
:context_modules_url => api_v1_course_context_modules_path(@context),
:course_student_submissions_url => api_v1_course_student_submissions_url(@context)
},
:POST_TO_SIS => Assignment.sis_grade_export_enabled?(@context),
:PERMISSIONS => permissions,
:HAS_GRADING_PERIODS => @context.grading_periods?,
:VALID_DATE_RANGE => CourseDateRange.new(@context),
:assignment_menu_tools => external_tools_display_hashes(:assignment_menu),
:discussion_topic_menu_tools => external_tools_display_hashes(:discussion_topic_menu),
:quiz_menu_tools => external_tools_display_hashes(:quiz_menu),
:current_user_has_been_observer_in_this_course => @context.user_has_been_observer?(@current_user),
:observed_student_ids => ObserverEnrollment.observed_student_ids(@context, @current_user),
})
conditional_release_js_env(includes: :active_rules)
if @context.grading_periods?
js_env(:active_grading_periods => GradingPeriod.json_for(@context, @current_user))
end
end
def self.google_drive_timeout
Setting.get('google_drive_timeout', 30).to_i
end
def google_drive_connection
return @google_drive_connection if @google_drive_connection
## @real_current_user first ensures that a masquerading user never sees the
## masqueradee's files, but in general you may want to block access to google
## docs for masqueraders earlier in the request
if logged_in_user
refresh_token, access_token = Rails.cache.fetch(['google_drive_tokens', logged_in_user].cache_key) do
service = logged_in_user.user_services.where(service: "google_drive").first
service && [service.token, service.secret]
end
else
refresh_token = session[:oauth_gdrive_refresh_token]
access_token = session[:oauth_gdrive_access_token]
end
@google_drive_connection = GoogleDrive::Connection.new(refresh_token, access_token, ApplicationController.google_drive_timeout)
end
def google_drive_client(refresh_token=nil, access_token=nil)
settings = Canvas::Plugin.find(:google_drive).try(:settings) || {}
client_secrets = {
client_id: settings[:client_id],
client_secret: settings[:client_secret_dec],
redirect_uri: settings[:redirect_uri]
}.with_indifferent_access
GoogleDrive::Client.create(client_secrets, refresh_token, access_token)
end
def user_has_google_drive
@user_has_google_drive ||= google_drive_connection.authorized?
end
def self.region
nil
end
def show_request_delete_account
false
end
helper_method :show_request_delete_account
def request_delete_account_link
nil
end
helper_method :request_delete_account_link
def setup_live_events_context
ctx = {}
if @domain_root_account
ctx[:root_account_uuid] = @domain_root_account.uuid
ctx[:root_account_id] = @domain_root_account.global_id
ctx[:root_account_lti_guid] = @domain_root_account.lti_guid
end
if @current_pseudonym
ctx[:user_login] = @current_pseudonym.unique_id
ctx[:user_account_id] = @current_pseudonym.global_account_id
ctx[:user_sis_id] = @current_pseudonym.sis_user_id
end
ctx[:user_id] = @current_user.global_id if @current_user
ctx[:real_user_id] = @real_current_user.global_id if @real_current_user
ctx[:context_type] = @context.class.to_s if @context
ctx[:context_id] = @context.global_id if @context
if @context_membership
ctx[:context_role] =
if @context_membership.respond_to?(:role)
@context_membership.role.name
elsif @context_membership.respond_to?(:type)
@context_membership.type
else
@context_membership.class.to_s
end
end
if tctx = Thread.current[:context]
ctx[:request_id] = tctx[:request_id]
ctx[:session_id] = tctx[:session_id]
end
ctx[:hostname] = request.host
ctx[:user_agent] = request.headers['User-Agent']
ctx[:producer] = 'canvas'
StringifyIds.recursively_stringify_ids(ctx)
LiveEvents.set_context(ctx)
end
def teardown_live_events_context
LiveEvents.clear_context!
end
end