canvas-lms/app/controllers/application_controller.rb

999 lines
42 KiB
Ruby

#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# 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
attr_accessor :active_tab
before_filter :set_locale
add_crumb "home", :root_path, :class => "home"
helper :all
filter_parameter_logging :password
include AuthenticationMethods
protect_from_forgery
before_filter :load_account, :load_user
before_filter :set_time_zone
before_filter :clear_cached_contexts
before_filter :set_page_view
after_filter :log_page_view
after_filter :discard_flash_if_xhr
after_filter :cache_buster
before_filter :fix_xhr_requests
before_filter :init_body_classes_and_active_tab
protected
def set_locale
# if params[:locale] is nil then I18n.default_locale will be used
I18n.locale = params[:locale] && I18n.available_locales.include?(params[:locale].to_sym) ? params[:locale] : nil
end
def init_body_classes_and_active_tab
@body_classes = []
active_tab = nil
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
end
# scopes all time objects to the user's specified time zone
def set_time_zone
if @current_user && !@current_user.time_zone.blank?
Time.zone = @current_user.time_zone
if Time.zone && Time.zone.name == "UTC" && @current_user.time_zone && @current_user.time_zone.match(/\s/)
Time.zone = @current_user.time_zone.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'] || Account.default
@files_domain = request.host != HostUrl.context_host(@domain_root_account) && request.host == HostUrl.file_host(@domain_root_account)
@domain_root_account
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)
context = context.user if context.is_a?(UserProfile)
klass = context.class.base_ar_class
name = name.to_s.sub(/context/, klass.name.underscore)
opts.unshift(context)
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
end
self.send name, *opts
end
def user_url(*opts)
opts[0] == @current_user && !current_user_is_site_admin? && !@current_user.grants_right?(@current_user, session, :view_statistics) ?
profile_url :
super
end
def tab_enabled?(id)
if @context && @context.respond_to?(:tabs_available) && !@context.tabs_available(@current_user, :include_hidden_unused => true).any?{|t| t[:id] == id }
if @context.is_a?(Account)
flash[:notice] = t "#application.notices.page_disabled_for_account", "That page has been disabled for this account"
elsif @context.is_a?(Course)
flash[:notice] = t "#application.notices.page_disabled_for_course", "That page has been disabled for this course"
elsif @context.is_a?(Group)
flash[:notice] = t "#application.notices.page_disabled_for_group", "That page has been disabled for this group"
else
flash[:notice] = t "#application.notices.page_disabled", "That page has been disabled"
end
redirect_to named_context_url(@context, :context_url)
return false
end
true
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, session, :update)
# render
# end
def authorized_action(object, *opts)
can_do = is_authorized_action?(object, *opts)
render_unauthorized_action(object) unless can_do
can_do
end
def is_authorized_action?(object, *opts)
user = opts.shift
action_session = nil
action_session ||= session
action_session = opts.shift if !opts[0].is_a?(Symbol) && !opts[0].is_a?(Array)
actions = Array(opts.shift)
can_do = false
begin
if object == @context && user == @current_user
@context_all_permissions ||= @context.grants_rights?(user, session, nil)
can_do = actions.any?{|a| @context_all_permissions[a] }
else
can_do = actions.any?{|a| object.grants_right?(user, action_session, a) }
end
rescue => e
logger.warn "#{object.inspect} raised an error while granting rights. #{e.inspect}"
end
can_do
end
def render_unauthorized_action(object=nil)
object ||= User.new
object.errors.add_to_base(t "#application.errors.unauthorized", "You are not authorized to perform this action")
respond_to do |format|
if !request.xhr?
flash[:notice] = t "#application.errors.unauthorized", "You are not authorized to perform this action"
end
@show_left_side = false
clear_crumbs
params = request.path_parameters
params[:format] = nil
@headers = !!@current_user if @headers != false
@files_domain = @account_domain && @account_domain.host_type == 'files'
format.html {
store_location if request.get?
return if !@current_user && initiate_delegated_login
render :template => "shared/unauthorized", :layout => "application", :status => :unauthorized
}
format.zip { redirect_to(url_for(params)) }
format.xml { render :xml => { 'status' => 'unauthorized' }, :status => :unauthorized }
format.json { render :json => { 'status' => 'unauthorized' }, :status => :unauthorized }
end
response.headers["Pragma"] = "no-cache"
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
end
# To be used as a before_filter, 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 request.path.match(/\A\/profile/)
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 clean_return_to(url)
return nil if !url
uri = URI.parse(url)
url = uri.path + (uri.query ? "?#{uri.query}" : "") + (uri.fragment ? "##{uri.fragment}" : "")
end
helper_method :clean_return_to
def return_to(url, fallback)
url = fallback if url.blank?
url = clean_return_to(url)
redirect_to url
end
MAX_ACCOUNT_LINEAGE_TO_SHOW_IN_CRUMBS = 3
# Can be used as a before_filter, 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_request? ?
Api.find(Course, params[:course_id]) : Course.find(params[:course_id])
params[:context_id] = params[:course_id]
params[:context_type] = "Course"
if @context && session[:enrollment_uuid_course_id] == @context.id
session[:enrollment_uuid_count] ||= 0
if session[:enrollment_uuid_count] > 4
session[:enrollment_uuid_count] = 0
self.extend(TextHelper)
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
session[:enrollment_uuid_count] += 1
end
@context_enrollment = @context.enrollments.find_all_by_user_id(@current_user.id).sort_by{|e| [e.state_sortable, e.rank_sortable] }.first if @context && @current_user
@context_membership = @context_enrollment
elsif params[:account_id] || (self.is_a?(AccountsController) && params[:account_id] = params[:id])
@context = Account.find(params[:account_id])
params[:context_id] = params[:account_id]
params[:context_type] = "Account"
@context_enrollment = @context.account_users.find_by_user_id(@current_user.id) if @context && @current_user
@context_membership = @context_enrollment
@account = @context
elsif params[:group_id]
@context = Group.find(params[:group_id])
params[:context_id] = params[:group_id]
params[:context_type] = "Group"
@context_enrollment = @context.group_memberships.find_by_user_id(@current_user.id) if @context && @current_user
@context_membership = @context_enrollment
elsif params[:user_id]
@context = User.find(params[:user_id])
params[:context_id] = params[:user_id]
params[:context_type] = "User"
@context_membership = @context if @context == @current_user
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/)
@context = @current_user
@context_membership = @context
end
if @context.try_rescue(:only_wiki_is_public) && params[:controller].match(/wiki/) && !@current_user && (!@context.is_a?(Course) || session[:enrollment_uuid_course_id] != @context.id)
@show_left_side = false
end
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
add_crumb(@context.short_name, named_context_url(@context, :context_url), :id => "crumb_#{@context.asset_string}") if @context && @context.respond_to?(:short_name)
end
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(include_groups = false)
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(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.
courses = @context.courses.active
groups = include_groups ? @context.groups.active : []
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)
courses = course_ids.empty? ? [] : courses.find_all_by_id(course_ids)
group_ids = only_contexts.select { |c| c.first == "Group" }.map(&:last)
groups = group_ids.empty? ? [] : groups.find_all_by_id(group_ids) if include_groups
end
@contexts.concat courses
@contexts.concat groups
end
if params[:include_contexts]
params[: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, nil, :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
# Retrieves all assignments for all contexts held in the @contexts variable.
# Also retrieves submissions and sorts the assignments based on
# their due dates and submission status for the given user.
def get_sorted_assignments
@assignment_groups = []
@upcoming_assignments = []
@assignments = []
@submissions = []
@overdue_assignments = []
@courses = @contexts.select{ |c| c.is_a?(Course) }
@just_viewing_one_course = @context.is_a?(Course) && @courses.length == 1
@context_codes = @courses.map(&:asset_string)
@context = @courses.first
if @just_viewing_one_course
@courses.each do |course|
# if there is just one context this will leave @groups set up for the view group by assignment group
@groups = course.assignment_groups.active(:include => :active_assignments)
assignments_for_this_course = @groups.map(&:active_assignments).flatten
@assignments += assignments_for_this_course
@upcoming_assignments += assignments_for_this_course.select{ |a|
a.due_at &&
a.due_at <= 1.weeks.from_now &&
a.due_at >= Time.now
}
log_asset_access("assignments:#{course.asset_string}", "assignments", "other")
end
else
@groups = AssignmentGroup.for_context_codes(@context_codes).active(:include => {:active_assignments => {:submissions => {}, :quiz => {}, :discussion_topic => {}} })
@assignments = Assignment.active.for_context_codes(@context_codes)
@courses.each do |course|
log_asset_access("assignments:#{course.asset_string}", "assignments", "other")
end
end
@upcoming_assignments = @assignments.select{|a|
a.due_at &&
a.due_at <= 1.weeks.from_now &&
a.due_at >= Time.now
}
@submissions = @current_user.submissions(:include => {:submission_comments => {}, :rubric_assessment => {}}).to_a if @current_user
@submissions_hash = {}
@submissions.each{|s|
@submissions_hash[s.assignment_id] = s
}
@ungraded_assignments = @assignments.select{|a|
a.grants_right?(@current_user, session, :grade) &&
a.expects_submission? &&
a.needs_grading_count > 0
}
@assignment_groups = @groups
@past_assignments = @assignments.select{ |a| a.due_at && a.due_at < Time.now }
@undated_assignments = @assignments.select{ |a| !a.due_at }
@past_assignments.each do |assignment|
submission = @submissions_hash[assignment.id]
if assignment.overdue? &&
assignment.expects_submission? &&
( !submission || (!submission.has_submission? && !submission.graded?) ) &&
assignment.grants_right?(@current_user, session, :submit)
@overdue_assignments << assignment
end
end
@future_assignments = @assignments - @past_assignments
if request.path.match(/\A\/assignments/)
if @future_assignments.length > 5
@future_assignments = @future_assignments.select{|a| a.due_at && a.due_at < 2.weeks.from_now }
else
@future_assignments = @future_assignments.select{|a| a.due_at && a.due_at < 4.weeks.from_now }
end
if @past_assignments.length > 5
@past_assignments = @past_assignments.select{|a| a.due_at && a.due_at > 2.weeks.ago }
else
@past_assignments = @past_assignments.select{|a| a.due_at && a.due_at > 4.weeks.ago }
end
@overdue_assignments = @overdue_assignments.select{|a| a.due_at && a.due_at > 2.weeks.ago }
@ungraded_assignments = @ungraded_assignments.select{|a| a.due_at && a.due_at > 2.weeks.ago }
end
[@assignments, @upcoming_assignments, @past_assignments, @overdue_assignments, @ungraded_assignments, @undated_assignments].map(&:sort!)
end
# Calculates the file storage quota for @context
def get_quota
@quota = 0
@quota_used = 0
return unless @context
@quota = Setting.get_cached('context_default_quota', 50.megabytes.to_s).to_i
@quota = @context.quota if (@context.respond_to?("quota") && @context.quota)
# TODO: remove this once values in the db have been migrated to be in bytes and not MB.
@quota = @quota.megabytes if @quota < 1.megabyte
@quota_used = 0
@context.attachments.active.select{|a| !a.root_attachment_id }.each do |a|
@quota_used += a.size || 0.0
end
end
# Renders a quota exceeded message if the @context's quota is exceeded
def quota_exceeded(redirect=nil)
redirect ||= root_url
get_quota
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", "Course storage quota exceeded"
elsif @context.is_a?(User)
error = t "#application.errors.quota_exceeded_user", "Course 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}}.to_json }
format.text {render :json => {:errors => {:base => error}}.to_json }
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.find_by_uuid(pieces[1]) 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.find_by_uuid(pieces[1]) 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::ContextTypes.const_defined?(@context_type)
@context_class = Context::ContextTypes.const_get(@context_type)
@context = @context_class.find_by_uuid(pieces[1]) 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_course", "The matching course 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."
@template_format = 'html'
@template.template_format = 'html'
render :text => @template.render(:file => "shared/unauthorized_feed", :layout => "layouts/application"), :status => :bad_request # :template => "shared/unauthorized_feed", :status => :bad_request
return false
end
@context
end
def discard_flash_if_xhr
flash.discard if request.xhr? || request.format.to_s == 'text/plain'
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
response.headers["Pragma"] = "no-cache"
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
end
def clear_cached_contexts
ActiveRecord::Base.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 (@developer_key && params[:user_request]) || (!@developer_key && @current_user && !request.xhr? && request.method == :get)
generate_page_view
end
end
def generate_page_view
@page_view = PageView.new(:url => request.url[0,255], :user => @current_user, :controller => request.path_parameters['controller'], :action => request.path_parameters['action'], :session_id => request.session_options[:id], :developer_key => @developer_key, :user_agent => request.headers['User-Agent'])
@page_view.interaction_seconds = 5
@page_view.user_request = true if params[:user_request] || (@current_user && !request.xhr? && request.method == :get)
@page_view.created_at = Time.now
@page_view.updated_at = Time.now
@page_before_render = Time.now.utc
@page_view.id = RequestContextGenerator.request_id
end
def generate_new_page_view
return true if !page_views_enabled?
generate_page_view
@page_view.generated_by_hand = true
end
def disable_page_views
@log_page_views = false
true
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.
def log_asset_access(asset, asset_category, asset_group=nil, level=nil, membership_type=nil)
return unless @current_user && @context && asset
@accessed_asset = {
:code => asset.is_a?(String) ? asset : asset.asset_string,
:group_code => asset_group.is_a?(String) ? asset_group : (asset_group.asset_string rescue 'unknown'),
:category => asset_category,
:membership_type => membership_type || (@context_membership && @context_membership.class.to_s rescue nil),
:level => level
}
end
def log_page_view
return true if !page_views_enabled?
if @current_user && @log_page_views != false
if @page_view && @page_view.generated_by_hand
elsif request.xhr? && params[:page_view_id]
if PageView.page_view_method != :db
@page_view = PageView.new { |p| p.request_id = params[:page_view_id] }
else
@page_view = PageView.find_by_request_id(params[:page_view_id])
if @page_view
response.headers["X-Canvas-Page-View-Id"] = @page_view.id.to_s
end
end
if @page_view
@page_view.do_update(params.slice(:interaction_seconds, :page_view_contributed))
@page_view_update = true
end
end
# 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 @current_user && @accessed_asset && (@accessed_asset[:level] == 'participate' || !@page_view_update)
@access = AssetUserAccess.find_by_user_id_and_asset_code(@current_user.id, @accessed_asset[:code])
@access ||= AssetUserAccess.create(:user => @current_user, :asset_code => @accessed_asset[:code])
@accessed_asset[:level] ||= 'view'
if @accessed_asset[:level] == 'view'
@access.view_score ||= 0
@access.view_score += 1
@access.action_level ||= 'view'
elsif @accessed_asset[:level] == 'participate'
@access.view_score ||= 0
@access.view_score += 1
@access.participate_score ||= 0
@access.participate_score += 1
@access.action_level = 'participate'
@page_view.participated = true if @page_view
elsif @accessed_asset[:level] == 'submit'
@access.participate_score ||= 0
@access.participate_score += 1
@access.action_level = 'participate'
@page_view.participated = true if @page_view
end
@access.asset_category ||= @accessed_asset[:category]
@access.asset_group_code ||= @accessed_asset[:group_code]
@access.membership_type ||= @accessed_asset[:membership_type]
@access.context = @context.is_a?(UserProfile) ? @context.user : @context
@access.summarized_at = nil
@access.save
@page_view.asset_user_access_id = @access.id if @page_view
@page_view_update = true
end
if @page_view && !request.xhr? && request.get? && (response.content_type || "").match(/html/)
@page_view.context ||= @context rescue nil
@page_view.account_id = @domain_root_account.id
@page_view.render_time ||= (Time.now.utc - @page_before_render) rescue nil
@page_view_update = true
end
if @page_view && @page_view_update
@page_view.store
end
else
@page_view.destroy if @page_view && !@page_view.new_record?
end
rescue => e
logger.error "Pageview error!"
raise e if Rails.env == 'development'
true
end
# Custom error catching and message rendering.
def rescue_action_in_public(exception)
response_code = response_code_for_rescue(exception)
begin
@status_code = interpret_status(response_code)
@status = @status_code
@status = 'AUT' if exception.is_a?(ActionController::InvalidAuthenticityToken)
type = 'default'
type = '404' if @status == '404 Not Found' && Rails.env == "production"
@error = ErrorReport.log_exception(type, exception, {
:url => request.url,
:user => @current_user,
:user_agent => request.headers['User-Agent'],
:request_context_id => RequestContextGenerator.request_id,
:account => @domain_root_account,
:request_method => request.method,
:format => request.format,
}.merge(ErrorReport.useful_http_env_stuff_from_request(request)))
@headers = nil
session[:last_error_id] = @error.id rescue nil
if request.xhr? || request.format == :text
render :json => {:errors => {:base => "Unexpected error, ID: #{@error.id rescue "unknown"}"}, :status => @status}, :status => @status_code
else
@status = '500' unless File.exists?(File.join('app', 'views', 'shared', 'errors', "#{@status.to_s[0,3]}_message.html.erb"))
render :template => "shared/errors/#{@status.to_s[0, 3]}_message.html.erb",
:layout => 'application', :status => @status, :locals => {:error => @error, :exception => exception, :status => @status}
end
rescue => e
# error generating the error page? failsafe.
render_optional_error_file response_code_for_rescue(exception)
ErrorReport.log_exception(:default, e)
end
end
def local_request?
false
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[:claim_course_uuid] = nil
session[:course_uuid] = nil
end
class InvalidDeveloperAPIKey < ActionController::InvalidAuthenticityToken #:nodoc:
end
rescue_responses['ApplicationController::InvalidDeveloperAPIKey'] = rescue_responses['ActionController::InvalidAuthenticityToken']
# Had to overwrite this method so we can say you don't need to have an
# authenticity_token if the request is coming from an api request.
# we also check for the session token not being set at all here, to catch
# those who have cookies disabled.
def verify_authenticity_token
params[request_forgery_protection_token] = params[request_forgery_protection_token].gsub(" ", "+") rescue nil
if params[:api_key] && api_request?
@developer_key = DeveloperKey.find_by_api_key(params[:api_key])
@developer_key || raise(InvalidDeveloperAPIKey)
elsif protect_against_forgery? &&
request.method != :get &&
verifiable_request_format?
if session[:_csrf_token].nil? && session.empty? && !request.xhr? && !api_request?
# the session should have the token stored by now, but doesn't? sounds
# like the user doesn't have cookies enabled.
redirect_to(login_url(:needs_cookies => '1'))
return false
else
raise(ActionController::InvalidAuthenticityToken) unless form_authenticity_token == form_authenticity_param
end
end
Rails.logger.warn("developer_key id: #{@developer_key.id}") if @developer_key
end
def api_request?
@api_request ||= !!request.path.match(/\A\/api\//)
end
def session_loaded?
session.send(:loaded?) rescue false
end
# Retrieving wiki pages needs to search either using the id or
# the page title. We've also got it in here to have more than one
# wiki per context, although we've never actually used that yet.
# And maybe we won't. See models/wiki_namespace.rb for more though.
def get_wiki_page
page_name = (params[:wiki_page_id] || params[:id] || (params[:wiki_page] && params[:wiki_page][:title]) || "front-page")
if(params[:format] && !['json', 'html'].include?(params[:format]))
page_name += ".#{params[:format]}"
params[:format] = 'html'
end
return @page if @page
@namespace = WikiNamespace.default_for_context(@context)
@wiki = @namespace.wiki
if params[:action] != 'create'
@page = @wiki.wiki_pages.deleted_last.find_by_url(page_name.to_s) ||
@wiki.wiki_pages.deleted_last.find_by_url(page_name.to_s.to_url) ||
@wiki.wiki_pages.find_by_id(page_name.to_i)
end
@page ||= @wiki.wiki_pages.build(
:title => page_name.titleize,
:url => page_name.to_url
)
@page.current_namespace = @namespace
if page_name == "front-page" && @page.new_record?
@page.body = t "#application.wiki_front_page_default_content_course", "Welcome to your new course wiki!" if @context.is_a?(Course)
@page.body = t "#application.wiki_front_page_default_content_group", "Welcome to your new group wiki!" if @context.is_a?(Group)
end
end
def context_wiki_page_url
page_name = @page.url
namespace = WikiNamespace.find_by_wiki_id_and_context_id_and_context_type(@page.wiki_id, @context.id, @context.class.to_s)
page_name = namespace.namespace + page_name if namespace && !namespace.default?
named_context_url(@context, :context_wiki_page_url, page_name)
end
def content_tag_redirect(context, tag, error_redirect_symbol)
if tag.content_type == 'Assignment'
redirect_to named_context_url(context, :context_assignment_url, tag.content_id)
elsif tag.content_type == 'WikiPage'
redirect_to named_context_url(context, :context_wiki_page_url, tag.content.url)
elsif tag.content_type == 'Attachment'
redirect_to named_context_url(context, :context_file_url, tag.content_id)
elsif tag.content_type == 'Quiz'
redirect_to named_context_url(context, :context_quiz_url, tag.content_id)
elsif tag.content_type == 'DiscussionTopic'
redirect_to named_context_url(context, :context_discussion_topic_url, tag.content_id)
elsif tag.content_type == 'ExternalUrl'
@tag = tag
@module = tag.context_module
tag.context_module_action(@current_user, :read)
render :template => 'context_modules/url_show'
elsif tag.content_type == 'ContextExternalTool'
@tag = tag
@tool = ContextExternalTool.find_external_tool(tag.url, context)
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
render :template => 'external_tools/tool_show'
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] ||= {}
options[:anchor] ||= {}
contexts_to_link_to = Array(contexts_to_link_to)
if !contexts_to_link_to.empty? && options[:anchor].is_a?(Hash)
options[:anchor][:show] = contexts_to_link_to.collect{ |c|
"group_#{c.class.to_s.downcase}_#{c.id}"
}.join(',')
options[:anchor] = options[:anchor].to_json
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].merge(options[:anchor].empty? ? {} : {
:anchor => options[:anchor].unpack('H*').first # calendar anchor is hex encoded
})
)
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 safe_domain_file_url(attachment, host=nil, verifier = nil) # TODO: generalize this
res = "#{request.protocol}#{host || HostUrl.file_host(@domain_root_account || Account.default)}"
ts, sig = @current_user && @current_user.access_verifier
if @context
res += named_context_url(@context, :context_file_url, attachment)
res += '/' + URI.escape(attachment.full_display_path)
else
res += file_download_url(attachment, :only_path => true)
end
# 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.
res += "?user_id=#{(@current_user ? @current_user.id : nil)}&ts=#{ts}&sf_verifier=#{sig}"
if verifier.present?
res += "&verifier=#{verifier}"
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 == :twitter
!!Twitter.config
elsif feature == :facebook
!!Facebook.config
elsif feature == :linked_in
!!LinkedIn.config
elsif feature == :google_docs
!!GoogleDocs.config
elsif feature == :etherpad
!!EtherpadCollaboration.config
elsif feature == :kaltura
!!Kaltura::ClientV3.config
elsif feature == :web_conferences
!!WebConference.config
elsif feature == :tinychat
!!Tinychat.config
elsif feature == :scribd
!!ScribdAPI.config
elsif feature == :lockdown_browser
Canvas::Plugin.all_for_tag(:lockdown_browser).any? { |p| p.settings[:enabled] }
else
!Rails.env.production? || (@current_user && current_user_is_site_admin?)
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.to_s}_#{rand.to_s}")}"
else
session[:temporary_user_code]
end
end
def require_account_management(on_root_account = false)
if (@context.root_account != nil && 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
# This before_filter can be used to limit access to only site admins.
# This checks if the user is an admin of the 'Site Admin' account, and has the
# site_admin permission.
def require_site_admin
require_site_admin_with_permission(:site_admin)
end
helper_method :current_user_is_site_admin?
def require_site_admin_with_permission(permission)
if session[:become_user_id]
session[:become_user_id] = nil
@current_user = @real_current_user
end
unless current_user_is_site_admin?(permission)
flash[:error] = t "#application.errors.permission_denied", "You don't have permission to access that page"
redirect_to root_url
return false
end
end
# This checks if the user is an admin of the 'Site Admin' account, and has the
# specified permission.
def current_user_is_site_admin?(permission = :site_admin)
user_is_site_admin?(@current_user)
end
helper_method :current_user_is_site_admin?
def user_is_site_admin?(user, permission = :site_admin)
Account.site_admin.grants_right?(user, session, permission)
end
helper_method :user_is_site_admin?
def page_views_enabled?
PageView.page_views_enabled?
end
helper_method :page_views_enabled?
# calls send_file if the io has a local file, or send_data otherwise
# make sure to rewind the io first, if necessary
def send_file_or_data(io, opts = {})
cancel_cache_buster
if io.respond_to?(:path) && io.path.present? && File.file?(io.path)
send_file(io.path, opts)
else
send_data(io, opts)
end
end
def verified_file_download_url(attachment, *opts)
file_download_url(attachment, :verifier => attachment.uuid, *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)
end
helper_method :user_content
end