diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 31683e86808..2a0206c8754 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -340,7 +340,7 @@ class ApplicationController < ActionController::Base when 'self' @context = @current_user else - @context = User.find(params[:user_id]) + @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" @@ -348,7 +348,7 @@ class ApplicationController < ActionController::Base elsif params[:course_section_id] params[:context_id] = params[:course_section_id] params[:context_type] = "CourseSection" - @context = CourseSection.find(params[:course_section_id]) + @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' diff --git a/app/controllers/external_tools_controller.rb b/app/controllers/external_tools_controller.rb index f18573d008c..32548fa23b8 100644 --- a/app/controllers/external_tools_controller.rb +++ b/app/controllers/external_tools_controller.rb @@ -178,12 +178,12 @@ class ExternalToolsController < ApplicationController redirect_to named_context_url(@context, :context_url) return end + @resource_title = @tool.label_for(selection_type.to_sym) - @resource_url = @tool.settings[selection_type.to_sym][:url] - @opaque_id = @context.opaque_identifier(:asset_string) - @resource_type = selection_type @return_url ||= url_for(@context) - @launch = BasicLTI::ToolLaunch.new(:url => @resource_url, :tool => @tool, :user => @current_user, :context => @context, :link_code => @opaque_id, :return_url => @return_url, :resource_type => @resource_type) + @launch = @tool.create_launch(@context, @current_user, @return_url, selection_type) + @resource_url = @launch.url + @tool_settings = @launch.generate render :template => 'external_tools/tool_show' end @@ -200,20 +200,28 @@ class ExternalToolsController < ApplicationController # @argument description [string] [optional] A description of the tool # @argument url [string] [optional] The url to match links against. Either "url" or "domain" should be set, not both. # @argument domain [string] [optional] The domain to match links against. Either "url" or "domain" should be set, not both. + # @argument icon_url [string] [optional] The url of the icon to show for this tool + # @argument text [string] [optional] The default text to show for this tool # @argument custom_fields [string] [optional] Custom fields that will be sent to the tool consumer, specified as custom_fields[field_name] # @argument account_navigation[url] [string] [optional] The url of the external tool for account navigation + # @argument account_navigation[enabled] [boolean] [optional] Set this to enable this feature # @argument account_navigation[text] [string] [optional] The text that will show on the left-tab in the account navigation # @argument user_navigation[url] [string] [optional] The url of the external tool for user navigation + # @argument user_navigation[enabled] [boolean] [optional] Set this to enable this feature # @argument user_navigation[text] [string] [optional] The text that will show on the left-tab in the user navigation # @argument course_navigation[url] [string] [optional] The url of the external tool for course navigation + # @argument course_navigation[enabled] [boolean] [optional] Set this to enable this feature # @argument course_navigation[text] [string] [optional] The text that will show on the left-tab in the course navigation # @argument course_navigation[visibility] [string] [optional] Who will see the navigation tab. "admins" for course admins, "members" for students, null for everyone # @argument course_navigation[default] [boolean] [optional] Whether the navigation option will show in the course by default or whether the teacher will have to explicitly enable it # @argument editor_button[url] [string] [optional] The url of the external tool + # @argument editor_button[enabled] [boolean] [optional] Set this to enable this feature # @argument editor_button[icon_url] [string] [optional] The url of the icon to show in the WYSIWYG editor # @argument editor_button[selection_width] [string] [optional] The width of the dialog the tool is launched in # @argument editor_button[selection_height] [string] [optional] The height of the dialog the tool is launched in # @argument resource_selection[url] [string] [optional] The url of the external tool + # @argument resource_selection[enabled] [boolean] [optional] Set this to enable this feature + # @argument resource_selection[icon_url] [string] [optional] The url of the icon to show in the module external tool list # @argument resource_selection[selection_width] [string] [optional] The width of the dialog the tool is launched in # @argument resource_selection[selection_height] [string] [optional] The height of the dialog the tool is launched in # @argument config_type [string] [optional] Configuration can be passed in as CC xml instead of using query parameters. If this value is "by_url" or "by_xml" then an xml configuration will be expected in either the "config_xml" or "config_url" parameter. Note that the name parameter overrides the tool name provided in the xml @@ -228,14 +236,14 @@ class ExternalToolsController < ApplicationController # -F 'name=LTI Example' \ # -F 'consumer_key=asdfg' \ # -F 'shared_secret=lkjh' \ - # -F 'url=http://example.com/ims/lti' \ + # -F 'url=https://example.com/ims/lti' \ # -F 'privacy_level=name_only' \ # -F 'custom_fields[key1]=value1' \ # -F 'custom_fields[key2]=value2' \ - # -F 'course_navigation[url]=http://example.com/ims/lti/course_endpoint' \ - # -F 'course_navigation[text]=Course Materials' \ + # -F 'course_navigation[text]=Course Materials' \ # -F 'course_navigation[default]=false' - # + # -F 'course_navigation[enabled]=true' + # # @example_request # # This would create a tool on the account with navigation for the user profile page @@ -244,11 +252,12 @@ class ExternalToolsController < ApplicationController # -F 'name=LTI Example' \ # -F 'consumer_key=asdfg' \ # -F 'shared_secret=lkjh' \ - # -F 'url=http://example.com/ims/lti' \ + # -F 'url=https://example.com/ims/lti' \ # -F 'privacy_level=name_only' \ - # -F 'user_navigation[url]=http://example.com/ims/lti/user_endpoint' \ - # -F 'user_navigation[text]=Soemthing Cool' - # + # -F 'user_navigation[url]=https://example.com/ims/lti/user_endpoint' \ + # -F 'user_navigation[text]=Something Cool' + # -F 'user_navigation[enabled]=true' + # # @example_request # # This would create a tool on the account with configuration pulled from an external URL @@ -258,7 +267,7 @@ class ExternalToolsController < ApplicationController # -F 'consumer_key=asdfg' \ # -F 'shared_secret=lkjh' \ # -F 'config_type=by_url' \ - # -F 'config_url=http://example.com/ims/lti/tool_config.xml' + # -F 'config_url=https://example.com/ims/lti/tool_config.xml' def create if authorized_action(@context, @current_user, :update) @tool = @context.context_external_tools.new @@ -327,9 +336,9 @@ class ExternalToolsController < ApplicationController private def set_tool_attributes(tool, params) - [:name, :description, :url, :domain, :privacy_level, :consumer_key, :shared_secret, + [:name, :description, :url, :icon_url, :domain, :privacy_level, :consumer_key, :shared_secret, :custom_fields, :custom_fields_string, :account_navigation, :user_navigation, - :course_navigation, :editor_button, :resource_selection, + :course_navigation, :editor_button, :resource_selection, :text, :config_type, :config_url, :config_xml].each do |prop| tool.send("#{prop}=", params[prop]) if params.has_key?(prop) end diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index af6ccf96b0f..846622d5211 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -323,7 +323,9 @@ class FilesController < ApplicationController protected :send_attachment def send_stored_file(attachment, inline=true, redirect_to_s3=false) - attachment.context_module_action(@current_user, :read) if @current_user && !params[:preview] + user = @current_user + user ||= User.find_by_id(params[:user_id]) if params[:user_id].present? + attachment.context_module_action(user, :read) if user && !params[:preview] log_asset_access(@attachment, "files", "files") unless params[:preview] if safer_domain_available? redirect_to safe_domain_file_url(attachment, @safer_domain_host, params[:verifier], !inline) diff --git a/app/controllers/pseudonym_sessions_controller.rb b/app/controllers/pseudonym_sessions_controller.rb index 5bd249e03fc..ae03fa9f06e 100644 --- a/app/controllers/pseudonym_sessions_controller.rb +++ b/app/controllers/pseudonym_sessions_controller.rb @@ -113,20 +113,20 @@ class PseudonymSessionsController < ApplicationController @pseudonym_session.remote_ip = request.remote_ip found = @pseudonym_session.save - if @pseudonym_session.too_many_attempts? + if !found && params[:pseudonym_session] + pseudonym = Pseudonym.authenticate(params[:pseudonym_session], @domain_root_account.trusted_account_ids, request.remote_ip) + if pseudonym && pseudonym != :too_many_attempts + @pseudonym_session = PseudonymSession.new(pseudonym, params[:pseudonym_session][:remember_me] == "1") + found = @pseudonym_session.save + end + end + + if pseudonym == :too_many_attempts || @pseudonym_session.too_many_attempts? flash[:error] = t 'errors.max_attempts', "Too many failed login attempts. Please try again later or contact your system administrator." redirect_to login_url return end - if !found && params[:pseudonym_session] - if pseudonym = Pseudonym.authenticate(params[:pseudonym_session], @domain_root_account.trusted_account_ids) - @pseudonym_session = PseudonymSession.new(pseudonym, params[:pseudonym_session][:remember_me] == "1") - @pseudonym_session.save - found = true - end - end - @pseudonym = @pseudonym_session && @pseudonym_session.record # If the user's account has been deleted, feel free to share that information if @pseudonym && (!@pseudonym.user || @pseudonym.user.unavailable?) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fb850e3902b..834ccd8ffd2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -481,8 +481,8 @@ module ApplicationHelper { :name => tool.label_for(:editor_button, nil), :id => tool.id, - :url => tool.settings[:editor_button][:url], - :icon_url => tool.settings[:editor_button][:icon_url], + :url => tool.settings[:editor_button][:url] || tool.url, + :icon_url => tool.settings[:editor_button][:icon_url] || tool.settings[:icon_url], :width => tool.settings[:editor_button][:selection_width], :height => tool.settings[:editor_button][:selection_height] } diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 77fc47a4490..0cbd8d2db4b 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -1588,6 +1588,11 @@ class Assignment < ActiveRecord::Base assoc = rubric.associate_with(item, context, :purpose => 'grading') assoc.use_for_grading = !!hash[:rubric_use_for_grading] if hash.has_key?(:rubric_use_for_grading) assoc.hide_score_total = !!hash[:rubric_hide_score_total] if hash.has_key?(:rubric_hide_score_total) + if hash[:saved_rubric_comments] + assoc.summary_data ||= {} + assoc.summary_data[:saved_comments] ||= {} + assoc.summary_data[:saved_comments] = hash[:saved_rubric_comments] + end assoc.save end end diff --git a/app/models/context_external_tool.rb b/app/models/context_external_tool.rb index 5bcc0ec2cf2..960b19dfc55 100644 --- a/app/models/context_external_tool.rb +++ b/app/models/context_external_tool.rb @@ -26,8 +26,26 @@ class ContextExternalTool < ActiveRecord::Base state :public state :deleted end - - set_policy do + + def create_launch(context, user, return_url, selection_type=nil) + if selection_type + if self.settings[selection_type.to_sym] + resource_url = self.settings[selection_type.to_sym][:url] + else + raise t('no_selection_type', "This tool has no selection type %{type}", :type => selection_type) + end + end + resource_url ||= self.url + BasicLTI::ToolLaunch.new(:url => resource_url, + :tool => self, + :user => user, + :context => context, + :link_code => context.opaque_identifier(:asset_string), + :return_url => return_url, + :resource_type => selection_type) + end + + set_policy do given { |user, session| self.cached_context_grants_right?(user, session, :update) } can :read and can :update and can :delete end @@ -51,9 +69,12 @@ class ContextExternalTool < ActiveRecord::Base def label_for(key, lang=nil) labels = settings[key] && settings[key][:labels] - (labels && labels[lang]) || + labels2 = settings[:labels] + (labels && labels[lang]) || (labels && lang && labels[lang.split('-').first]) || - (settings[key] && settings[key][:text]) || + (settings[key] && settings[key][:text]) || + (labels2 && labels2[lang]) || + (labels2 && lang && labels2[lang.split('-').first]) || settings[:text] || name || "External Tool" end @@ -169,7 +190,7 @@ class ContextExternalTool < ActiveRecord::Base end def resource_selection=(hash) - tool_setting(:resource_selection, hash, :selection_width, :selection_height) + tool_setting(:resource_selection, hash, :selection_width, :selection_height, :icon_url) end def resource_selection @@ -183,6 +204,22 @@ class ContextExternalTool < ActiveRecord::Base def editor_button settings[:editor_button] end + + def icon_url=(i_url) + settings[:icon_url] = i_url + end + + def icon_url + settings[:icon_url] + end + + def text=(val) + settings[:text] = val + end + + def text + settings[:text] + end def shared_secret=(val) write_attribute(:shared_secret, val) unless val.blank? @@ -191,19 +228,24 @@ class ContextExternalTool < ActiveRecord::Base def infer_defaults self.url = nil if url.blank? self.domain = nil if domain.blank? - - settings.delete(:user_navigation) if settings[:user_navigation] && (!settings[:user_navigation][:url]) - settings.delete(:course_navigation) if settings[:course_navigation] && (!settings[:course_navigation][:url]) - settings.delete(:account_navigation) if settings[:account_navigation] && (!settings[:account_navigation][:url]) - settings.delete(:resource_selection) if settings[:resource_selection] && (!settings[:resource_selection][:url] || !settings[:resource_selection][:selection_width] || !settings[:resource_selection][:selection_height]) - settings.delete(:editor_button) if settings[:editor_button] && (!settings[:editor_button][:url] || !settings[:editor_button][:icon_url]) - settings[:icon_url] ||= settings[:editor_button][:icon_url] if settings[:editor_button] && settings[:editor_button][:icon_url] + [:resource_selection, :editor_button].each do |type| if settings[type] - settings[type][:selection_width] = settings[type][:selection_width].to_i - settings[type][:selection_height] = settings[type][:selection_height].to_i + settings[:icon_url] ||= settings[type][:icon_url] if settings[type][:icon_url] + settings[type][:selection_width] = settings[type][:selection_width].to_i if settings[type][:selection_width] + settings[type][:selection_height] = settings[type][:selection_height].to_i if settings[type][:selection_height] end end + [:course_navigation, :account_navigation, :user_navigation, :resource_selection, :editor_button].each do |type| + if settings[type] + if !(settings[type][:url] || self.url) || (settings[type].has_key?(:enabled) && !settings[type][:enabled]) + settings.delete(type) + end + end + end + + settings.delete(:resource_selection) if settings[:resource_selection] && (!settings[:resource_selection][:selection_width] || !settings[:resource_selection][:selection_height]) + settings.delete(:editor_button) if settings[:editor_button] && !settings[:icon_url] self.has_user_navigation = !!settings[:user_navigation] self.has_course_navigation = !!settings[:course_navigation] @@ -369,19 +411,23 @@ class ContextExternalTool < ActiveRecord::Base end end end - + + # sets the custom fields from the main tool settings, and any on individual resource type settings def set_custom_fields(hash, resource_type) - fields = resource_type ? settings[resource_type.to_sym][:custom_fields] : settings[:custom_fields] - (fields || {}).each do |key, val| - key = key.gsub(/[^\w]/, '_').downcase - if key.match(/^custom_/) - hash[key] = val - else - hash["custom_#{key}"] = val + fields = [settings[:custom_fields] || {}] + fields << (settings[resource_type.to_sym][:custom_fields] || {}) if resource_type && settings[resource_type.to_sym] + fields.each do |field_set| + field_set.each do |key, val| + key = key.to_s.gsub(/[^\w]/, '_').downcase + if key.match(/^custom_/) + hash[key] = val + else + hash["custom_#{key}"] = val + end end end end - + def clone_for(context, dup=nil, options={}) if !self.cloned_item && !self.new_record? self.cloned_item = ClonedItem.create(:original_item => self) @@ -453,9 +499,11 @@ class ContextExternalTool < ActiveRecord::Base settings[setting] = {} end - settings[setting][:url] = hash[:url] + settings[setting][:url] = hash[:url] if hash[:url] settings[setting][:text] = hash[:text] if hash[:text] - keys.each { |key| settings[setting][key] = hash[key] } + settings[setting][:custom_fields] = hash[:custom_fields] if hash[:custom_fields] + settings[setting][:enabled] = Canvas::Plugin.value_to_boolean(hash[:enabled]) if hash.has_key?(:enabled) + keys.each { |key| settings[setting][key] = hash[key] if hash.has_key?(key) } # if the type needs to do some validations for specific keys yield settings[setting] if block_given? diff --git a/app/models/course.rb b/app/models/course.rb index 5edafd08094..775c66ed401 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -2712,6 +2712,9 @@ class Course < ActiveRecord::Base self.attributes.delete_if{|k,v| [:id, :created_at, :updated_at, :syllabus_body, :wiki_id, :default_view, :tab_configuration].include?(k.to_sym) }.each do |key, val| new_course.write_attribute(key, val) end + # there's a unique constraint on this, so we need to clear it out + self.self_enrollment_code = nil + self.self_enrollment = false # The order here is important; we have to set our sis id to nil and save first # so that the new course can be saved, then we need the new course saved to # get its id to move over sections and enrollments. Setting this course to diff --git a/app/models/pseudonym.rb b/app/models/pseudonym.rb index c604edba2a3..b9b33c9a85a 100644 --- a/app/models/pseudonym.rb +++ b/app/models/pseudonym.rb @@ -376,22 +376,28 @@ class Pseudonym < ActiveRecord::Base def self.serialization_excludes; [:crypted_password, :password_salt, :reset_password_token, :persistence_token, :single_access_token, :perishable_token, :sis_ssha]; end - def self.find_all_by_arbitrary_credentials(credentials, account_ids) + def self.find_all_by_arbitrary_credentials(credentials, account_ids, remote_ip) return [] if credentials[:unique_id].blank? || credentials[:password].blank? - Shard.partition_by_shard(account_ids) do |account_ids| + too_many_attempts = false + pseudonyms = Shard.partition_by_shard(account_ids) do |account_ids| active. by_unique_id(credentials[:unique_id]). where(:account_id => account_ids). all(:include => :user). select { |p| - p.valid_arbitrary_credentials?(credentials[:password]) + valid = p.valid_arbitrary_credentials?(credentials[:password]) + too_many_attempts = true if p.audit_login(remote_ip, valid) == :too_many_attempts + valid } end + return :too_many_attempts if too_many_attempts + pseudonyms end - def self.authenticate(credentials, account_ids) - pseudonyms = find_all_by_arbitrary_credentials(credentials, account_ids) + def self.authenticate(credentials, account_ids, remote_ip = nil) + pseudonyms = find_all_by_arbitrary_credentials(credentials, account_ids, remote_ip) + return :too_many_attempts if pseudonyms == :too_many_attempts site_admin = pseudonyms.find { |p| p.account_id == Account.site_admin.id } # only log them in if these credentials match a single user OR if it matched site admin if pseudonyms.map(&:user).uniq.length == 1 || site_admin @@ -399,4 +405,15 @@ class Pseudonym < ActiveRecord::Base site_admin || pseudonyms.first end end + + def audit_login(remote_ip, valid_password) + return :too_many_attempts unless Canvas::Security.allow_login_attempt?(self, remote_ip) + + if valid_password + Canvas::Security.successful_login!(self, remote_ip) + else + Canvas::Security.failed_login!(self, remote_ip) + end + nil + end end diff --git a/app/models/pseudonym_session.rb b/app/models/pseudonym_session.rb index 3d9496a1ce5..8692ae73fac 100644 --- a/app/models/pseudonym_session.rb +++ b/app/models/pseudonym_session.rb @@ -99,17 +99,11 @@ class PseudonymSession < Authlogic::Session::Base super # have to call super first, as that's what loads attempted_record - if !Canvas::Security.allow_login_attempt?(attempted_record, remote_ip) + if too_many_attempts? || attempted_record.try(:audit_login, remote_ip, !invalid_password?) == :too_many_attempts self.too_many_attempts = true errors.add(password_field, I18n.t('errors.max_attempts', 'Too many failed login attempts. Please try again later or contact your system administrator.')) return end - - if invalid_password? - Canvas::Security.failed_login!(attempted_record, remote_ip) - else - Canvas::Security.successful_login!(attempted_record, remote_ip) - end end def too_many_attempts? diff --git a/app/views/discussion_topics/show.html.erb b/app/views/discussion_topics/show.html.erb index de3520aa53b..6be630c89be 100644 --- a/app/views/discussion_topics/show.html.erb +++ b/app/views/discussion_topics/show.html.erb @@ -53,7 +53,8 @@

<%= @topic.title %>

<% end %> <% if @locked %> - <%= @locked.is_a?(Hash) ? lock_explanation(@locked, 'topic', @context) : t(:locked_topic, "This topic is currently locked.") %> + <% use_context = (@context.is_a?(Group)) ? @context.context : @context %> + <%= @locked.is_a?(Hash) ? lock_explanation(@locked, 'topic', use_context) : t(:locked_topic, "This topic is currently locked.") %> <% else %> <% js_bundle :discussion diff --git a/app/views/submissions/show_preview.html.erb b/app/views/submissions/show_preview.html.erb index 96cbc7d1427..e5aa1977951 100644 --- a/app/views/submissions/show_preview.html.erb +++ b/app/views/submissions/show_preview.html.erb @@ -78,7 +78,7 @@