diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/strong_parameters.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/strong_parameters.rb | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb new file mode 100644 index 0000000000..bc27ecaa20 --- /dev/null +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -0,0 +1,573 @@ +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/array/wrap' +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. + # + # params = ActionController::Parameters.new(a: {}) + # params.fetch(:b) + # # => ActionController::ParameterMissing: param not found: b + # params.require(:a) + # # => ActionController::ParameterMissing: param not found: a + class ParameterMissing < KeyError + attr_reader :param # :nodoc: + + def initialize(param) # :nodoc: + @param = 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. + # 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. + # + # params = ActionController::Parameters.new({ + # person: { + # name: 'Francesco', + # age: 22, + # role: 'admin' + # } + # }) + # + # permitted = params.require(:person).permit(:name, :age) + # permitted # => {"name"=>"Francesco", "age"=>22} + # permitted.class # => ActionController::Parameters + # permitted.permitted? # => true + # + # Person.first.update!(permitted) + # # => #<Person id: 1, name: "Francesco", age: 22, role: "user"> + # + # 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 + # + # ActionController::Parameters.permit_all_parameters = true + # + # params = ActionController::Parameters.new + # params.permitted? # => true + # + # 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 + # + # <tt>ActionController::Parameters</tt> is inherited from + # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means + # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>. + # + # params = ActionController::Parameters.new(key: 'value') + # params[:key] # => "value" + # 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 "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\ + " has been deprecated. Use "\ + "`ActionController::Parameters.always_permitted_parameters` instead." + self.always_permitted_parameters + end + + # Returns a new instance of <tt>ActionController::Parameters</tt>. + # Also, sets the +permitted+ attribute to the default value of + # <tt>ActionController::Parameters.permit_all_parameters</tt>. + # + # class Person < ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => false + # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # + # ActionController::Parameters.permit_all_parameters = true + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => true + # Person.new(params) # => #<Person id: nil, name: "Francesco"> + def initialize(attributes = nil) + super(attributes) + @permitted = self.class.permit_all_parameters + end + + # 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 + # params.permitted? # => false + # params.permit! + # params.permitted? # => true + def permitted? + @permitted + end + + # Sets the +permitted+ attribute to +true+. This can be used to pass + # mass assignment. Returns +self+. + # + # class Person < ActiveRecord::Base + # end + # + # params = ActionController::Parameters.new(name: 'Francesco') + # params.permitted? # => false + # Person.new(params) # => ActiveModel::ForbiddenAttributesError + # params.permit! + # params.permitted? # => true + # Person.new(params) # => #<Person id: nil, name: "Francesco"> + def permit! + each_pair do |key, value| + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |v| + v.permit! if v.respond_to? :permit! + end + end + + @permitted = true + self + end + + # Ensures that a parameter is present. If it's present, returns + # the parameter at the given +key+, otherwise raises an + # <tt>ActionController::ParameterMissing</tt> error. + # + # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) + # # => {"name"=>"Francesco"} + # + # ActionController::Parameters.new(person: nil).require(:person) + # # => ActionController::ParameterMissing: param not found: person + # + # ActionController::Parameters.new(person: {}).require(:person) + # # => ActionController::ParameterMissing: param not found: person + def require(key) + value = self[key] + if value.present? || value == false + value + else + raise ParameterMissing.new(key) + end + end + + # Alias of #require. + alias :required :require + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # includes only the given +filters+ and sets the +permitted+ attribute + # for the object to +true+. This is useful for limiting which attributes + # should be allowed for mass updating. + # + # params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' }) + # permitted = params.require(:user).permit(:name, :age) + # permitted.permitted? # => true + # permitted.has_key?(:name) # => true + # 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({ + # person: { + # name: 'Francesco', + # age: 22, + # pets: [{ + # name: 'Purplish', + # category: 'dogs' + # }] + # } + # }) + # + # permitted = params.permit(person: [ :name, { pets: :name } ]) + # permitted.permitted? # => true + # permitted[:person][:name] # => "Francesco" + # permitted[:person][:age] # => nil + # permitted[:person][:pets][0][:name] # => "Purplish" + # permitted[:person][:pets][0][:category] # => nil + # + # Note that if you use +permit+ in a key that points to a hash, + # it won't allow all the hash. You also need to specify which + # attributes inside the hash should be whitelisted. + # + # params = ActionController::Parameters.new({ + # person: { + # contact: { + # email: 'none@test.com', + # phone: '555-1234' + # } + # } + # }) + # + # params.require(:person).permit(:contact) + # # => {} + # + # params.require(:person).permit(contact: :phone) + # # => {"contact"=>{"phone"=>"555-1234"}} + # + # params.require(:person).permit(contact: [ :email, :phone ]) + # # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}} + def permit(*filters) + params = self.class.new + + filters.flatten.each do |filter| + case filter + when Symbol, String + permitted_scalar_filter(params, filter) + when Hash then + hash_filter(params, filter) + end + end + + unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters + + params.permit! + end + + # Returns a parameter for the given +key+. If not found, + # returns +nil+. + # + # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params[:person] # => {"name"=>"Francesco"} + # params[:none] # => nil + def [](key) + convert_hashes_to_parameters(key, super) + end + + # Returns a parameter for the given +key+. If the +key+ + # can't be found, there are several options: With no other arguments, + # it will raise an <tt>ActionController::ParameterMissing</tt> error; + # if more arguments are given, then that will be returned; if a block + # is given, then that will be run and its result returned. + # + # params = ActionController::Parameters.new(person: { name: 'Francesco' }) + # params.fetch(:person) # => {"name"=>"Francesco"} + # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none + # params.fetch(:none, 'Francesco') # => "Francesco" + # params.fetch(:none) { 'Francesco' } # => "Francesco" + def fetch(key, *args) + convert_hashes_to_parameters(key, super, false) + rescue KeyError + raise ActionController::ParameterMissing.new(key) + end + + # Returns a new <tt>ActionController::Parameters</tt> instance that + # includes only the given +keys+. If the given +keys+ + # don't exist, returns an empty hash. + # + # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) + # params.slice(:a, :b) # => {"a"=>1, "b"=>2} + # params.slice(:d) # => {} + def slice(*keys) + self.class.new(super).tap do |new_instance| + new_instance.permitted = @permitted + end + end + + # Returns an exact copy of the <tt>ActionController::Parameters</tt> + # instance. +permitted+ state is kept on the duped object. + # + # params = ActionController::Parameters.new(a: 1) + # params.permit! + # params.permitted? # => true + # copy_params = params.dup # => {"a"=>1} + # copy_params.permitted? # => true + def dup + super.tap do |duplicate| + duplicate.permitted = @permitted + end + end + + protected + def permitted=(new_permitted) + @permitted = new_permitted + end + + private + 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 + self.class.new(value) + end + end + + def each_element(object) + if object.is_a?(Array) + object.map { |el| yield el }.compact + elsif fields_for_style?(object) + hash = object.class.new + object.each { |k,v| hash[k] = yield v } + hash + else + 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 + # + # It provides an interface for protecting attributes from end-user + # assignment. This makes Action Controller parameters forbidden + # to be used in Active Model mass assignment until they have been + # whitelisted. + # + # In addition, parameters can be marked as required and flow through a + # predefined raise/rescue flow to end up as a 400 Bad Request with no + # effort. + # + # class PeopleController < ActionController::Base + # # Using "Person.create(params[:person])" would raise an + # # ActiveModel::ForbiddenAttributes exception because it'd + # # be using mass assignment without an explicit permit step. + # # This is the recommended form: + # def create + # Person.create(person_params) + # end + # + # # This will pass with flying colors as long as there's a person key in the + # # parameters, otherwise it'll raise an ActionController::MissingParameter + # # exception, which will get caught by ActionController::Base and turned + # # into a 400 Bad Request reply. + # def update + # redirect_to current_account.people.find(params[:id]).tap { |person| + # person.update!(person_params) + # } + # end + # + # private + # # Using a private method to encapsulate the permissible parameters is + # # just a good pattern since you'll be able to reuse the same permit + # # list between create and update. Also, you can specialize this method + # # with per-user checking of permissible attributes. + # def person_params + # params.require(:person).permit(:name, :age) + # end + # end + # + # 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 + # has_many :pets + # accepts_nested_attributes_for :pets + # end + # + # class PeopleController < ActionController::Base + # def create + # Person.create(person_params) + # end + # + # ... + # + # private + # + # def person_params + # # It's mandatory to specify the nested attributes that should be whitelisted. + # # If you use `permit` with just the key that points to the nested attributes hash, + # # it will return an empty hash. + # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ]) + # end + # end + # + # See ActionController::Parameters.require and ActionController::Parameters.permit + # for more information. + module StrongParameters + extend ActiveSupport::Concern + include ActiveSupport::Rescuable + + # Returns a new ActionController::Parameters object that + # has been instantiated with the <tt>request.parameters</tt>. + def params + @_params ||= Parameters.new(request.parameters) + end + + # Assigns the given +value+ to the +params+ hash. If +value+ + # is a Hash, this will create an ActionController::Parameters + # object that has been instantiated with the given +value+ hash. + def params=(value) + @_params = value.is_a?(Hash) ? Parameters.new(value) : value + end + end +end |