diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/strong_parameters.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/strong_parameters.rb | 375 |
1 files changed, 323 insertions, 52 deletions
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 8faa5f8a13..01bbd749c1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,7 +1,11 @@ -require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/string/filters' +require 'active_support/deprecation' require 'active_support/rescuable' +require 'action_dispatch/http/upload' +require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -16,14 +20,28 @@ module ActionController def initialize(param) # :nodoc: @param = param - super("param not found: #{param}") + super("param is missing or the value is empty: #{param}") + end + end + + # Raised when a supplied parameter is not expected. + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => ActionController::UnpermittedParameters: found unexpected keys: a, b + class UnpermittedParameters < IndexError + attr_reader :params # :nodoc: + + def initialize(params) # :nodoc: + @params = params + super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}") end end # == Action Controller \Parameters # # Allows to choose which attributes should be whitelisted for mass updating - # and thus prevent accidentally exposing that which shouldn’t be exposed. + # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter # as permitted and limit which attributes should be allowed for mass updating. @@ -41,13 +59,20 @@ module ActionController # permitted.class # => ActionController::Parameters # permitted.permitted? # => true # - # Person.first.update_attributes!(permitted) + # Person.first.update!(permitted) # # => #<Person id: 1, name: "Francesco", age: 22, role: "user"> # - # It provides a +permit_all_parameters+ option that controls the top-level - # behaviour of new instances. If it's +true+, all the parameters will be - # permitted by default. The default value for +permit_all_parameters+ - # option is +false+. + # It provides two options that controls the top-level behavior of new instances: + # + # * +permit_all_parameters+ - If it's +true+, all the parameters will be + # permitted by default. The default is +false+. + # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters + # that are not explicitly permitted are found. The values can be <tt>:log</tt> to + # write a message on the logger or <tt>:raise</tt> to raise + # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt> + # in test and development environments, +false+ otherwise. + # + # Examples: # # params = ActionController::Parameters.new # params.permitted? # => false @@ -57,7 +82,21 @@ module ActionController # params = ActionController::Parameters.new # params.permitted? # => true # - # <tt>ActionController::Parameters</tt> is inherited from + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => {} + # + # ActionController::Parameters.action_on_unpermitted_parameters = :raise + # + # params = ActionController::Parameters.new(a: "123", b: "456") + # params.permit(:c) + # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b + # + # Please note that these options *are not thread-safe*. In a multi-threaded + # environment they should only be set once at boot-time and never mutated at + # runtime. + # + # <tt>ActionController::Parameters</tt> inherits from # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. # @@ -66,6 +105,27 @@ module ActionController # params["key"] # => "value" class Parameters < ActiveSupport::HashWithIndifferentAccess cattr_accessor :permit_all_parameters, instance_accessor: false + cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false + + # By default, never raise an UnpermittedParameters exception if these + # params are present. The default includes both 'controller' and 'action' + # because they are added by Rails and should be of no concern. One way + # to change these is to specify `always_permitted_parameters` in your + # config. For instance: + # + # config.always_permitted_parameters = %w( controller action format ) + cattr_accessor :always_permitted_parameters + self.always_permitted_parameters = %w( controller action ) + + def self.const_missing(const_name) + super unless const_name == :NEVER_UNPERMITTED_PARAMS + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated. + Use `ActionController::Parameters.always_permitted_parameters` instead. + MSG + + always_permitted_parameters + end # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -88,6 +148,54 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Returns a safe +Hash+ representation of this parameter with all + # unpermitted keys removed. + # + # params = ActionController::Parameters.new({ + # name: 'Senjougahara Hitagi', + # oddity: 'Heavy stone crab' + # }) + # params.to_h # => {} + # + # safe_params = params.permit(:name) + # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} + def to_h + if permitted? + to_hash + else + slice(*self.class.always_permitted_parameters).permit!.to_h + end + end + + # Returns an unsafe, unfiltered +Hash+ representation of this parameter. + def to_unsafe_h + to_hash + end + alias_method :to_unsafe_hash, :to_unsafe_h + + # Convert all hashes in values into parameters, then yield each pair like + # the same way as <tt>Hash#each_pair</tt> + def each_pair(&block) + super do |key, value| + convert_hashes_to_parameters(key, value) + end + + super + end + + alias_method :each, :each_pair + + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + # + # Testing membership still loops, but it's going to be faster than our own + # loop that converts values. Also, we are not going to build a new array + # object per fetch. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -112,8 +220,9 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + Array.wrap(value).each do |v| + v.permit! if v.respond_to? :permit! + end end @permitted = true @@ -133,7 +242,12 @@ module ActionController # ActionController::Parameters.new(person: {}).require(:person) # # => ActionController::ParameterMissing: param not found: person def require(key) - self[key].presence || raise(ParameterMissing.new(key)) + value = self[key] + if value.present? || value == false + value + else + raise ParameterMissing.new(key) + end end # Alias of #require. @@ -151,6 +265,22 @@ module ActionController # permitted.has_key?(:age) # => true # permitted.has_key?(:role) # => false # + # Only permitted scalars pass the filter. For example, given + # + # params.permit(:name) + # + # +:name+ passes it is a key of +params+ whose associated value is of type + # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+, + # +Date+, +Time+, +DateTime+, +StringIO+, +IO+, + # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+. + # Otherwise, the key +:name+ is filtered out. + # + # You may declare that the parameter should be an array of permitted scalars + # by mapping it to an empty array: + # + # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) + # params.permit(tags: []) + # # You can also use +permit+ on nested parameters, like: # # params = ActionController::Parameters.new({ @@ -178,7 +308,7 @@ module ActionController # params = ActionController::Parameters.new({ # person: { # contact: { - # email: 'none@test.com' + # email: 'none@test.com', # phone: '555-1234' # } # } @@ -197,32 +327,15 @@ module ActionController filters.flatten.each do |filter| case filter - when Symbol, String then - if has_key?(filter) - _value = self[filter] - params[filter] = _value unless Hash === _value - end - keys.grep(/\A#{Regexp.escape(filter)}\(\d+[if]?\)\z/) { |key| params[key] = self[key] } + when Symbol, String + permitted_scalar_filter(params, filter) when Hash then - filter = filter.with_indifferent_access - - self.slice(*filter.keys).each do |key, values| - return unless values - - key = key.to_sym - - params[key] = each_element(values) do |value| - # filters are a Hash, so we expect value to be a Hash too - next if filter.is_a?(Hash) && !value.is_a?(Hash) - - value = self.class.new(value) if !value.respond_to?(:permit) - - value.permit(*Array.wrap(filter[key])) - end - end + hash_filter(params, filter) end end + unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters + params.permit! end @@ -248,7 +361,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -261,11 +374,56 @@ module ActionController # params.slice(:a, :b) # => {"a"=>1, "b"=>2} # params.slice(:d) # => {} def slice(*keys) - self.class.new(super).tap do |new_instance| - new_instance.instance_variable_set :@permitted, @permitted + new_instance_with_inherited_permitted_status(super) + end + + # Removes and returns the key/value pairs matching the given keys. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} + # params # => {"c"=>3} + def extract!(*keys) + new_instance_with_inherited_permitted_status(super) + end + + # Returns a new <tt>ActionController::Parameters</tt> with the results of + # running +block+ once for every value. The keys are unchanged. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.transform_values { |x| x * 2 } + # # => {"a"=>2, "b"=>4, "c"=>6} + def transform_values + if block_given? + new_instance_with_inherited_permitted_status(super) + else + super + end + end + + # This method is here only to make sure that the returned object has the + # correct +permitted+ status. It should not matter since the parent of + # this object is +HashWithIndifferentAccess+ + def transform_keys # :nodoc: + if block_given? + new_instance_with_inherited_permitted_status(super) + else + super end end + # Deletes and returns a key-value pair from +Parameters+ whose key is equal + # to key. If the key is not found, returns the default value. If the + # optional code block is given and the key is not found, pass in the key + # and return the result of block. + def delete(key, &block) + convert_hashes_to_parameters(key, super, false) + end + + # Equivalent to Hash#keep_if, but returns nil if no changes were made. + def select!(&block) + convert_value_to_parameters(super) + end + # Returns an exact copy of the <tt>ActionController::Parameters</tt> # instance. +permitted+ state is kept on the duped object. # @@ -276,24 +434,44 @@ module ActionController # copy_params.permitted? # => true def dup super.tap do |duplicate| - duplicate.instance_variable_set :@permitted, @permitted + duplicate.permitted = @permitted end end + protected + def permitted=(new_permitted) + @permitted = new_permitted + end + private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def new_instance_with_inherited_permitted_status(hash) + self.class.new(hash).tap do |new_instance| + new_instance.permitted = @permitted + end + end + + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end def each_element(object) if object.is_a?(Array) object.map { |el| yield el }.compact - elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ } + elsif fields_for_style?(object) hash = object.class.new object.each { |k,v| hash[k] = yield v } hash @@ -301,6 +479,105 @@ module ActionController yield object end end + + def fields_for_style?(object) + object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + + def unpermitted_parameters!(params) + unpermitted_keys = unpermitted_keys(params) + if unpermitted_keys.any? + case self.class.action_on_unpermitted_parameters + when :log + name = "unpermitted_parameters.action_controller" + ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys) + when :raise + raise ActionController::UnpermittedParameters.new(unpermitted_keys) + end + end + end + + def unpermitted_keys(params) + self.keys - params.keys - self.always_permitted_parameters + end + + # + # --- Filtering ---------------------------------------------------------- + # + + # This is a white list of permitted scalar types that includes the ones + # supported in XML and JSON requests. + # + # This list is in particular used to filter ordinary requests, String goes + # as first element to quickly short-circuit the common case. + # + # If you modify this collection please update the API of +permit+ above. + PERMITTED_SCALAR_TYPES = [ + String, + Symbol, + NilClass, + Numeric, + TrueClass, + FalseClass, + Date, + Time, + # DateTimes are Dates, we document the type but avoid the redundant check. + StringIO, + IO, + ActionDispatch::Http::UploadedFile, + Rack::Test::UploadedFile, + ] + + def permitted_scalar?(value) + PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)} + end + + def permitted_scalar_filter(params, key) + if has_key?(key) && permitted_scalar?(self[key]) + params[key] = self[key] + end + + keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k| + if permitted_scalar?(self[k]) + params[k] = self[k] + end + end + end + + def array_of_permitted_scalars?(value) + if value.is_a?(Array) + value.all? {|element| permitted_scalar?(element)} + end + end + + def array_of_permitted_scalars_filter(params, key) + if has_key?(key) && array_of_permitted_scalars?(self[key]) + params[key] = self[key] + end + end + + EMPTY_ARRAY = [] + def hash_filter(params, filter) + filter = filter.with_indifferent_access + + # Slicing filters out non-declared keys. + slice(*filter.keys).each do |key, value| + next unless value + + if filter[key] == EMPTY_ARRAY + # Declaration { comment_ids: [] }. + array_of_permitted_scalars_filter(params, key) + else + # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. + params[key] = each_element(value) do |element| + if element.is_a?(Hash) + element = self.class.new(element) unless element.respond_to?(:permit) + element.permit(*Array.wrap(filter[key])) + end + end + end + end + end end # == Strong \Parameters @@ -329,7 +606,7 @@ module ActionController # # into a 400 Bad Request reply. # def update # redirect_to current_account.people.find(params[:id]).tap { |person| - # person.update_attributes!(person_params) + # person.update!(person_params) # } # end # @@ -343,7 +620,7 @@ module ActionController # end # end # - # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you + # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you # will need to specify which nested attributes should be whitelisted. # # class Person @@ -374,12 +651,6 @@ module ActionController extend ActiveSupport::Concern include ActiveSupport::Rescuable - included do - rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| - render text: "Required parameter missing: #{parameter_missing_exception.param}", status: :bad_request - end - end - # Returns a new ActionController::Parameters object that # has been instantiated with the <tt>request.parameters</tt>. def params |