diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 05ac847caac09..16bf9f61dd8f7 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -531,6 +531,140 @@ def require(key) alias :required :require + # `mandate` is the preferred way to require and permit parameters. + # It is similar to calling `permit` and `require` in sequence, but it avoids some + # potential pitfalls, such as NoMethodErrors that can be triggered by external + # users passing unexpected scalar or non-scalar types. + # + # `mandate` can be thought of as a shortcut for `params.permit(filters).require(filters)` + # with the additional special behavior of requiring `filetrs.keys` if `filters` is a hash. + # This can be convenient for the standard format of params from rails forms. + # + # # given a usual format of params from a form + # params = ActionController::Parameters.new(user: { name: "Martin", age: 40, role: "admin" }) + # # instead of this (not recommended) + # permitted = params.require(:user).permit(:name, :age) + # # do this (recommended) + # permitted = params.mandate(user: [:name, :age]) + # + # In the example, `mandate` will require the `:user` key and permit the + # `:name` and `:age` keys, and return the params `{ name: "Martin", age: 40 }`. + # It is functionally the same as the following: + # + # permitted = params.permit(user: [:name, :age]).require(:user) + # + # `mandate` aims to correct a potential error that can be triggered when an end user + # alters the params. In the following example, the params are altered by the user in + # order to cause a 500 error. + # + # params = ActionController::Parameters.new(user: "Hax0r") + # params.require(:user).permit(:name, :age) # causes a 500 error + # # => NoMethodError: undefined method `permit' for an instance of String + # params.mandate(user: [:name, :age]) # expected behavior, handled correctly by rails + # # => ActionController::ParameterMissing: param is missing or the value is empty: user + # + # Like `permit`, `mandate` only allows permitted scalars to pass the filter. + # For example, + # + # params.mandate(:name) + # + # If `:name` is a `Hash` or `Array`, ActionController::ParameterMissing is raised. + # + # `:name` is permitted if 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`. + # + # You can also `mandate` nested parameters just like permit. The result will + # `require(filters.keys)` on the permitted parameters. + # + # params = ActionController::Parameters.new({ + # person: { + # name: "Francesco", + # age: 22, + # pets: [{ + # name: "Purplish", + # category: "dogs" + # }] + # } + # }) + # + # permitted = params.mandate(person: [ :name, { pets: :name } ]) + # permitted.permitted? # => true + # permitted[:name] # => "Francesco" + # permitted[:age] # => nil + # permitted[:pets][0][:name] # => "Purplish" + # permitted[:pets][0][:category] # => nil + # + # 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"]) + # permitted = params.mandate(tags: []) + # permitted.permitted? # => true + # permitted.is_a?(Array) # => true + # permitted.size # => 2 + # + # Like `permit`, `mandate` can be used to permit nested parameters. + # Be careful because this opens the door to arbitrary input. In this case, + # `permit` ensures values in the returned structure are permitted scalars and + # filters out anything else. For example: + # + # params = ActionController::Parameters.new(user: { name: "Martin", age: 40, hax: [] }) + # permitted = params.mandate(user: {}) + # permitted.permitted? # => true + # permitted.has_key?(:name) # => true + # permitted.has_key?(:age) # => true + # permitted.has_key?(:hax) # => false + # + # Note that if you use `mandate` on a key that points to a hash, it will not + # allow the hash. You also need to specify which attributes inside the hash + # should be permitted. + # + # params = ActionController::Parameters.new({ + # person: { + # contact: { + # email: "none@test.com", + # phone: "555-1234" + # } + # } + # }) + # + # params.mandate(person: :contact) # wrong + # # => ActionController::ParameterMissing: param is missing or the value is empty: person + # + # params.mandate(person: { contact: :phone }) # correct + # # => ##"555-1234"} permitted: true>} permitted: true> + # + # params.mandate(person: { contact: [ :email, :phone ] }) # correct + # # => ##"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true> + # + # Use `mandate` to require multiple top level scalar parameters. + # The keys must be passed as an array, similar to using `require`. + # Note that this is different than `permit` which allows multiple keys + # to be passed as arguments instead of as an array. + # + # params = ActionController::Parameters.new(name: "Martin", age: 40) + # name, age = params.mandate(:name, :age) # wrong + # # => ArgumentError: wrong number of arguments (given 2, expected 1) + # name, age = params.mandate([:name, :age]) # correct + # name # => "Martin" + # age # => 40 + # + # When called with a hash with multiple keys, `mandate` will behave as + # described above, requiring all the keys of the hash and returning the value + # at each key in the order they are given in the filters argument. + # + # params = ActionController::Parameters.new(subject: { name: "Martin" }, object: { pie: "pumpkin" }) + # subject, object = params.mandate(subject: [:name], object: [:pie]) + # subject # => #"Martin"} permitted: true> + # object # => #"pumpkin"} permitted: true> + # + def mandate(filters) + keys = filters.respond_to?(:keys) ? filters.keys : filters + permit(filters).require(keys) + end + # Returns a new `ActionController::Parameters` 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