1487 lines
58 KiB
Ruby
1487 lines
58 KiB
Ruby
#
|
|
# Copyright (C) 2011 - 2012 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
|
|
|
|
include Api
|
|
include LocaleSelection
|
|
include Api::V1::User
|
|
around_filter :set_locale
|
|
|
|
helper :all
|
|
|
|
include AuthenticationMethods
|
|
protect_from_forgery
|
|
# load_user checks masquerading permissions, so this needs to be cleared first
|
|
before_filter :clear_cached_contexts
|
|
before_filter :load_account, :load_user
|
|
before_filter :check_pending_otp
|
|
before_filter :set_user_id_header
|
|
before_filter :set_time_zone
|
|
before_filter :set_page_view
|
|
after_filter :log_page_view
|
|
after_filter :discard_flash_if_xhr
|
|
after_filter :cache_buster
|
|
# 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_filter :set_user_id_header
|
|
before_filter :fix_xhr_requests
|
|
before_filter :init_body_classes
|
|
after_filter :set_response_headers
|
|
after_filter :update_enrollment_last_activity_at
|
|
|
|
add_crumb(proc { %Q{<i title="#{I18n.t('links.dashboard', "My Dashboard")}" class="icon-home standalone-icon"></i>}.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 = {})
|
|
# set some defaults
|
|
unless @js_env
|
|
@js_env = {
|
|
:current_user_id => @current_user.try(:id),
|
|
:current_user => user_display_json(@current_user, :profile),
|
|
:current_user_roles => @current_user.try(:roles),
|
|
:context_asset_string => @context.try(:asset_string),
|
|
:AUTHENTICITY_TOKEN => form_authenticity_token,
|
|
:files_domain => HostUrl.file_host(@domain_root_account || Account.default, request.host_with_port)
|
|
}
|
|
@js_env[:IS_LARGE_ROSTER] = true if @context.respond_to?(:large_roster?) && @context.large_roster?
|
|
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
|
|
|
|
protected
|
|
|
|
def assign_localizer
|
|
I18n.localizer = lambda {
|
|
infer_locale :context => @context,
|
|
:user => @current_user,
|
|
:root_account => @domain_root_account,
|
|
:accept_language => request.headers['Accept-Language']
|
|
}
|
|
end
|
|
|
|
def set_locale
|
|
assign_localizer
|
|
yield if block_given?
|
|
ensure
|
|
I18n.localizer = nil
|
|
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
|
|
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'] || 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
|
|
headers['X-UA-Compatible'] = 'IE=edge,chrome=1'
|
|
# 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_cached('block_html_frames', 'true') == 'true' && !@embeddable
|
|
headers['X-Frame-Options'] = 'SAMEORIGIN'
|
|
end
|
|
true
|
|
end
|
|
|
|
def files_domain?
|
|
!!@files_domain
|
|
end
|
|
|
|
def check_pending_otp
|
|
if session[:pending_otp] && !(params[:action] == 'otp_login' && request.method == :post)
|
|
reset_session
|
|
redirect_to login_url
|
|
end
|
|
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_ar_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
|
|
end
|
|
self.send name, *opts
|
|
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)
|
|
return true unless @context && @context.respond_to?(:tabs_available)
|
|
tabs = @context.tabs_available(@current_user,
|
|
:session => session,
|
|
:include_hidden_unused => true,
|
|
:root_account => @domain_root_account)
|
|
valid = tabs.any?{|t| t[:id] == id }
|
|
render_tab_disabled unless valid
|
|
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
|
|
|
|
# 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 = object.grants_rights?(user, action_session, *actions).values.any?
|
|
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.generic", "You are not authorized to perform this action")
|
|
respond_to do |format|
|
|
@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
|
|
return if !@current_user && initiate_delegated_login(request.host_with_port)
|
|
if @context.is_a?(Course) && @context_enrollment
|
|
start_date = @context_enrollment.enrollment_dates.map(&:first).compact.min if @context_enrollment.state_based_on_date == :inactive
|
|
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
|
|
|
|
@is_delegated = delegated_authentication_url?
|
|
render :template => "shared/unauthorized", :layout => "application", :status => :unauthorized
|
|
}
|
|
format.zip { redirect_to(url_for(params)) }
|
|
format.json { render_json_unauthorized }
|
|
end
|
|
response.headers["Pragma"] = "no-cache"
|
|
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
|
end
|
|
|
|
def delegated_authentication_url?
|
|
@domain_root_account.delegated_authentication? &&
|
|
!@domain_root_account.ldap_authentication? &&
|
|
!params[:canvas_login]
|
|
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
|
|
|
|
helper_method :clean_return_to
|
|
|
|
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.active, params[:course_id]) : Course.active.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, e.id] }.first if @context && @current_user
|
|
@context_membership = @context_enrollment
|
|
elsif params[:account_id] || (self.is_a?(AccountsController) && params[:account_id] = params[:id])
|
|
case params[:account_id]
|
|
when 'self'
|
|
@context = @domain_root_account
|
|
when 'default'
|
|
@context = Account.default
|
|
when 'site_admin'
|
|
@context = Account.site_admin
|
|
else
|
|
@context = api_request? ?
|
|
api_find(Account, params[:account_id]) : Account.find(params[:account_id])
|
|
end
|
|
params[:context_id] = @context.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] || (self.is_a?(UsersController) && params[:user_id] = params[:id])
|
|
case params[:user_id]
|
|
when 'self'
|
|
@context = @current_user
|
|
else
|
|
@context = api_request? ? api_find(User, params[:user_id]) : User.find(params[:user_id])
|
|
end
|
|
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_request? ? api_find(CourseSection, params[:course_section_id]) : CourseSection.find(params[:course_section_id])
|
|
elsif params[:collection_item_id]
|
|
params[:context_id] = params[:collection_item_id]
|
|
params[:context_type] = 'CollectionItem'
|
|
@context = CollectionItem.find(params[:collection_item_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/)
|
|
@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
|
|
set_badge_counts_for(@context, @current_user, @current_enrollment)
|
|
assign_localizer if @context.present?
|
|
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.current_enrollments.select { |e| e.state_based_on_date == :active }.map(&:course).uniq
|
|
groups = include_groups ? @context.current_groups : []
|
|
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.select { |c| course_ids.include?(c.id) }
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
# 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
|
|
@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
|
|
@groups = @courses.first.assignment_groups.active.includes(:active_assignments)
|
|
@assignments = @groups.map(&:active_assignments).flatten
|
|
else
|
|
@groups = AssignmentGroup.for_context_codes(@context_codes).active
|
|
@assignments = Assignment.active.for_course(@courses.map(&:id))
|
|
end
|
|
@assignment_groups = @groups
|
|
|
|
@courses.each { |course| log_course(course) }
|
|
|
|
@submissions = @current_user.try(:submissions).to_a
|
|
@submissions.each{ |s| s.mute if s.muted_assignment? }
|
|
|
|
@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
|
|
})
|
|
|
|
@past_assignments = sorted.past
|
|
@undated_assignments = sorted.undated
|
|
@ungraded_assignments = sorted.ungraded
|
|
@upcoming_assignments = sorted.upcoming
|
|
@future_assignments = sorted.future
|
|
@overdue_assignments = sorted.overdue
|
|
|
|
condense_assignments if requesting_main_assignments_page?
|
|
|
|
categorized_assignments.each(&:sort!)
|
|
end
|
|
|
|
def categorized_assignments
|
|
[
|
|
@assignments,
|
|
@upcoming_assignments,
|
|
@past_assignments,
|
|
@ungraded_assignments,
|
|
@undated_assignments,
|
|
@future_assignments
|
|
]
|
|
end
|
|
|
|
def condense_assignments
|
|
num_weeks = @future_assignments.length > 5 ? 2 : 4
|
|
@future_assignments = SortsAssignments.up_to(@future_assignments, num_weeks.weeks.from_now)
|
|
num_weeks = @past_assignments.length < 5 ? 2 : 4
|
|
@past_assignments = SortsAssignments.down_to(@past_assignments, num_weeks.weeks.ago)
|
|
|
|
@overdue_assignments = SortsAssignments.down_to(@overdue_assignments, 4.weeks.ago)
|
|
@ungraded_assignments = SortsAssignments.down_to(@ungraded_assignments, 4.weeks.ago)
|
|
end
|
|
|
|
def log_course(course)
|
|
log_asset_access("assignments:#{course.asset_string}", "assignments", "other")
|
|
end
|
|
|
|
def requesting_main_assignments_page?
|
|
request.path.match(/\A\/assignments/)
|
|
end
|
|
|
|
# Calculates the file storage quota for @context
|
|
def get_quota
|
|
quota_params = Attachment.get_quota(@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(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", "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}}.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.active.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_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."
|
|
@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 || request.xhr? || api_request?
|
|
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
|
|
attributes = { :user => @current_user, :developer_key => @developer_key, :real_user => @real_current_user }
|
|
@page_view = PageView.generate(request, attributes)
|
|
@page_view.user_request = true if params[:user_request] || (@current_user && !request.xhr? && request.method == :get)
|
|
@page_before_render = Time.now.utc
|
|
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
|
|
|
|
def update_enrollment_last_activity_at
|
|
if @context.is_a?(Course) && @context_enrollment
|
|
@context_enrollment.record_recent_activity
|
|
end
|
|
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 request.xhr? && params[:page_view_id] && !(@page_view && @page_view.generated_by_hand)
|
|
@page_view = PageView.for_request_id(params[:page_view_id])
|
|
if @page_view
|
|
response.headers["X-Canvas-Page-View-Id"] = @page_view.id.to_s if @page_view.id
|
|
@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_or_initialize_by_user_id_and_asset_code(@current_user.id, @accessed_asset[:code])
|
|
@accessed_asset[:level] ||= 'view'
|
|
access_context = @context.is_a?(UserProfile) ? @context.user : @context
|
|
@access.log access_context, @accessed_asset
|
|
|
|
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
|
|
if @page_view && !request.xhr? && request.get? && (response.content_type || "").match(/html/)
|
|
@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.context = @context if !@page_view.context_id
|
|
@page_view.account_id = @domain_root_account.id
|
|
@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'
|
|
|
|
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)))
|
|
|
|
if api_request?
|
|
rescue_action_in_api(exception, error)
|
|
else
|
|
render_rescue_action(exception, error, status, status_code)
|
|
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 render_rescue_action(exception, error, status, status_code)
|
|
clear_crumbs
|
|
@headers = nil
|
|
session[:last_error_id] = error.id rescue nil
|
|
if request.xhr? || request.format == :text
|
|
render :status => status_code, :json => {
|
|
:errors => {
|
|
:base => "Unexpected error, ID: #{error.id rescue "unknown"}"
|
|
},
|
|
:status => status
|
|
}
|
|
else
|
|
erbfile = "#{status.to_s[0,3]}_message.html.erb"
|
|
erbpath = File.join('app', 'views', 'shared', 'errors', erbfile)
|
|
erbfile = "500_message.html.erb" unless File.exists?(erbpath)
|
|
@status_code = status_code
|
|
render :template => "shared/errors/#{erbfile}",
|
|
:layout => 'application',
|
|
:status => status,
|
|
:locals => {
|
|
:error => error,
|
|
:exception => exception,
|
|
:status => status
|
|
}
|
|
end
|
|
end
|
|
|
|
if Rails.version < "3.0"
|
|
rescue_responses['AuthenticationMethods::AccessTokenError'] = 401
|
|
else
|
|
ActionDispatch::ShowExceptions.rescue_responses['AuthenticationMethods::AccessTokenError'] = 401
|
|
end
|
|
|
|
def rescue_action_in_api(exception, error_report)
|
|
status_code = response_code_for_rescue(exception) || 500
|
|
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 = { :status => status_code_string }
|
|
if error_report.try(:id)
|
|
data[:error_report_id] = error_report.id
|
|
end
|
|
|
|
# inject exception-specific data into the response
|
|
case exception
|
|
when ActiveRecord::RecordNotFound
|
|
data[:message] = 'The specified resource does not exist.'
|
|
when AuthenticationMethods::AccessTokenError
|
|
add_www_authenticate_header
|
|
data[:message] = 'Invalid access token.'
|
|
end
|
|
|
|
data[:message] ||= "An error occurred."
|
|
render :json => data, :status => status_code
|
|
end
|
|
|
|
def rescue_action_locally(exception)
|
|
if api_request?
|
|
# 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_api(exception, nil)
|
|
else
|
|
super
|
|
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.delete(:claim_course_uuid)
|
|
session.delete(:course_uuid)
|
|
end
|
|
|
|
# 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
|
|
token = params[request_forgery_protection_token].try(:gsub, " ", "+")
|
|
params[request_forgery_protection_token] = token if token
|
|
|
|
if protect_against_forgery? &&
|
|
!request.get? &&
|
|
!api_request?
|
|
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) || (form_authenticity_token == request.headers['X-CSRF-Token'])
|
|
end
|
|
end
|
|
Rails.logger.warn("developer_key id: #{@developer_key.id}") if @developer_key
|
|
end
|
|
|
|
API_REQUEST_REGEX = %r{\A/api/v\d}
|
|
|
|
def api_request?
|
|
@api_request ||= !!request.path.match(API_REQUEST_REGEX)
|
|
end
|
|
|
|
def session_loaded?
|
|
session.send(:loaded?) rescue false
|
|
end
|
|
|
|
# Retrieving wiki pages needs to search either using the id or
|
|
# the page title.
|
|
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
|
|
@wiki = @context.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
|
|
)
|
|
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
|
|
named_context_url(@context, :context_wiki_page_url, page_name)
|
|
end
|
|
|
|
def content_tag_redirect(context, tag, error_redirect_symbol)
|
|
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 named_context_url(context, :context_wiki_page_url, tag.content.url, 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 == 'ExternalUrl'
|
|
@tag = tag
|
|
@module = tag.context_module
|
|
tag.context_module_action(@current_user, :read) unless tag.locked_for? @current_user
|
|
render :template => 'context_modules/url_show'
|
|
elsif tag.content_type == 'ContextExternalTool'
|
|
@tag = tag
|
|
if @tag.context.is_a?(Assignment)
|
|
@assignment = @tag.context
|
|
@resource_title = @assignment.title
|
|
else
|
|
@resource_title = @tag.title
|
|
end
|
|
@resource_url = @tag.url
|
|
@opaque_id = @tag.opaque_identifier(:asset_string)
|
|
@tool = ContextExternalTool.find_external_tool(tag.url, context, tag.content_id)
|
|
@target = '_blank' if tag.new_tab
|
|
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
|
|
return unless require_user
|
|
@return_url = named_context_url(@context, :context_external_tool_finished_url, @tool.id, :include_host => true)
|
|
@launch = BasicLTI::ToolLaunch.new(:url => @resource_url, :tool => @tool, :user => @current_user, :context => @context, :link_code => @opaque_id, :return_url => @return_url)
|
|
if @assignment && @context.includes_student?(@current_user)
|
|
@launch.for_assignment!(@tag.context, lti_grade_passback_api_url(@tool), blti_legacy_grade_passback_api_url(@tool))
|
|
end
|
|
@tool_settings = @launch.generate
|
|
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 event = options.delete(:event)
|
|
options[:query][:event_id] = event.id
|
|
end
|
|
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 conversations_path(params={})
|
|
hash = params.keys.empty? ? '' : "##{params.to_json.unpack('H*').first}"
|
|
"/conversations#{hash}"
|
|
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_ar_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 == :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 == :crocodoc
|
|
!!Canvas::Crocodoc.config
|
|
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 show_new_dashboard?
|
|
@current_user && @current_user.preferences[:new_dashboard]
|
|
end
|
|
|
|
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? && 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)
|
|
unless Account.site_admin.grants_right?(@current_user, permission)
|
|
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
|
|
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 :template => "shared/registration_incomplete", :layout => "application", :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 => params[:registration_success] && @current_user.pre_registered?, :USER_EMAIL => @current_user.email
|
|
end
|
|
end
|
|
|
|
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, request.host_with_port)
|
|
end
|
|
helper_method :user_content
|
|
|
|
def find_bank(id, check_context_chain=true)
|
|
bank = @context.assessment_question_banks.active.find_by_id(id) || @current_user.assessment_question_banks.active.find_by_id(id)
|
|
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.find_by_id(id)
|
|
end
|
|
yield if block_given? && (@bank = bank)
|
|
bank
|
|
end
|
|
|
|
# refs #6632 -- once the speed grader ipad app is upgraded, we can remove these exceptions
|
|
SKIP_JSON_CSRF_REGEX = %r{\A(?:/login|/logout|/dashboard/comment_session)}
|
|
def prepend_json_csrf?
|
|
request.get? && in_app? && !request.path.match(SKIP_JSON_CSRF_REGEX)
|
|
end
|
|
|
|
def in_app?
|
|
@pseudonym_session && !@pseudonym_session.used_basic_auth?
|
|
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 reset_session
|
|
# when doing login/logout via ajax, we need to have the new csrf token
|
|
# for subsequent requests.
|
|
@resend_csrf_token_if_json = true
|
|
super
|
|
end
|
|
|
|
def set_layout_options
|
|
@embedded_view = params[:embedded]
|
|
@headers = false if params[:no_headers]
|
|
(@body_classes ||= []) << 'embedded' if @embedded_view
|
|
end
|
|
|
|
def render(options = nil, extra_options = {}, &block)
|
|
set_layout_options
|
|
if options && options.key?(:json)
|
|
json = options.delete(:json)
|
|
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
|
|
# 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
|
|
|
|
if @resend_csrf_token_if_json
|
|
response.headers['X-CSRF-Token'] = form_authenticity_token
|
|
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[:text] = json
|
|
else
|
|
options[:json] = json
|
|
end
|
|
end
|
|
super
|
|
end
|
|
|
|
def jammit_css_bundles; @jammit_css_bundles ||= []; end
|
|
helper_method :jammit_css_bundles
|
|
|
|
def jammit_css(*args)
|
|
opts = (args.last.is_a?(Hash) ? args.pop : {})
|
|
Array(args).flatten.each do |bundle|
|
|
jammit_css_bundles << [bundle, opts[:plugin]] unless jammit_css_bundles.include? [bundle, opts[:plugin]]
|
|
end
|
|
nil
|
|
end
|
|
helper_method :jammit_css
|
|
|
|
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 :gradebook2
|
|
#
|
|
# 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 :gradebook2, :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(@current_user)
|
|
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 => unsupported_browser, :classes => 'unsupported_browser'}
|
|
end
|
|
if error = flash.delete(:error)
|
|
notices << {:type => 'error', :content => error}
|
|
end
|
|
if warning = flash.delete(:warning)
|
|
notices << {:type => 'warning', :content => warning}
|
|
end
|
|
if notice = (flash[:html_notice] ? flash.delete(:html_notice).html_safe : flash.delete(:notice))
|
|
notices << {:type => 'success', :content => notice}
|
|
end
|
|
notices
|
|
end
|
|
end
|
|
helper_method :flash_notices
|
|
|
|
def unsupported_browser
|
|
t("#application.warnings.unsupported_browser", "Your browser does not meet the minimum requirements for Canvas. Please visit the *Canvas Guides* for a complete list of supported browsers.", :wrapper => @template.link_to('\1', 'http://guides.instructure.com/s/2204/m/4214/l/41056-which-browsers-does-canvas-support'))
|
|
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 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
|
|
known_user = viewer.load_messageable_user(profile.user)
|
|
common_courses = []
|
|
common_groups = []
|
|
if viewer != profile.user
|
|
if known_user
|
|
common_courses = known_user.common_courses.map do |course_id, roles|
|
|
next if course_id.zero?
|
|
c = course_json(Course.find(course_id), @current_user, session, ['html_url'], false)
|
|
c[:roles] = roles.map { |role| Enrollment.readable_type(role) }
|
|
c
|
|
end.compact
|
|
common_groups = known_user.common_groups.map do |group_id, roles|
|
|
next if group_id.zero?
|
|
g = group_json(Group.find(group_id), @current_user, session, :include => ['html_url'])
|
|
# in the future groups will have more roles and we'll need soemthing similar to
|
|
# the roles.map above in courses
|
|
g[:roles] = [t('#group.memeber', "Member")]
|
|
g
|
|
end.compact
|
|
end
|
|
end
|
|
data[:common_contexts] = [] + common_courses + common_groups
|
|
data[:known_user] = known_user
|
|
data
|
|
end
|
|
|
|
unless CANVAS_RAILS3
|
|
filter_parameter_logging *LoggingFilter.filtered_parameters
|
|
end
|
|
|
|
# filter out sensitive parameters in the query string as well when logging
|
|
# the rails "Completed in XXms" line.
|
|
# this is fixed in Rails 3.x
|
|
def complete_request_uri
|
|
uri = LoggingFilter.filter_uri(request.request_uri)
|
|
"#{request.protocol}#{request.host}#{uri}"
|
|
end
|
|
|
|
def self.batch_jobs_in_actions(opts = {})
|
|
batch_opts = opts.delete(:batch)
|
|
around_filter(opts) do |controller, action|
|
|
Delayed::Batch.serial_batch(batch_opts || {}) do
|
|
action.call
|
|
end
|
|
end
|
|
end
|
|
end
|