strong_parameters.rb 12.4 KB
Newer Older
1 2 3 4 5
require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/rescuable'

module ActionController
6 7 8 9
  # Raised when a required parameter is missing.
  #
  #   params = ActionController::Parameters.new(a: {})
  #   params.fetch(:b)
10
  #   # => ActionController::ParameterMissing: param not found: b
11
  #   params.require(:a)
12
  #   # => ActionController::ParameterMissing: param not found: a
13
  class ParameterMissing < KeyError
14
    attr_reader :param # :nodoc:
15

16
    def initialize(param) # :nodoc:
17
      @param = param
18
      super("param not found: #{param}")
19 20 21
    end
  end

22 23 24 25 26 27
  # == 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
28
  # as permitted and limit which attributes should be allowed for mass updating.
29 30 31 32 33 34 35 36 37 38 39 40 41 42
  #
  #   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
  #
43 44
  #   Person.first.update_attributes!(permitted)
  #   # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
45
  #
46 47 48 49
  # 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+.
50 51
  #
  #   params = ActionController::Parameters.new
52
  #   params.permitted? # => false
53 54 55 56 57 58 59 60 61 62 63 64 65
  #
  #   ActionController::Parameters.permit_all_parameters = true
  #
  #   params = ActionController::Parameters.new
  #   params.permitted? # => true
  #
  # <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"
66
  class Parameters < ActiveSupport::HashWithIndifferentAccess
67
    cattr_accessor :permit_all_parameters, instance_accessor: false
68
    attr_accessor :permitted # :nodoc:
69

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
    # 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
    #     include 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">
87 88
    def initialize(attributes = nil)
      super(attributes)
89
      @permitted = self.class.permit_all_parameters
90 91
    end

92 93 94 95 96 97 98 99 100 101
    # 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

102 103 104 105 106 107 108 109 110 111 112 113
    # 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">
114
    def permit!
115 116 117 118 119
      each_pair do |key, value|
        convert_hashes_to_parameters(key, value)
        self[key].permit! if self[key].respond_to? :permit!
      end

120 121 122 123
      @permitted = true
      self
    end

124 125 126 127 128 129 130 131
    # 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)
132
    #   # => ActionController::ParameterMissing: param not found: person
133 134
    #
    #   ActionController::Parameters.new(person: {}).require(:person)
135
    #   # => ActionController::ParameterMissing: param not found: person
136 137 138 139
    def require(key)
      self[key].presence || raise(ParameterMissing.new(key))
    end

140
    # Alias of #require.
141 142
    alias :required :require

143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    # Returns a new <tt>ActionController::Parameters</tt> instance that
    # includes only the given +filters+ and sets the +permitted+ 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
    #
    # You can also use +permit+ on nested parameters, like:
    #
    #   params = ActionController::Parameters.new({
    #     person: {
    #       name: 'Francesco',
    #       age:  22,
    #       pets: [{
    #         name: 'Purplish',
    #         category: 'dogs'
    #       }]
    #     }
    #   })
    #
168
    #   permitted = params.permit(person: [ :name, { pets: :name } ])
169 170 171 172 173
    #   permitted.permitted?                    # => true
    #   permitted[:person][:name]               # => "Francesco"
    #   permitted[:person][:age]                # => nil
    #   permitted[:person][:pets][0][:name]     # => "Purplish"
    #   permitted[:person][:pets][0][:category] # => nil
174 175 176 177 178 179
    def permit(*filters)
      params = self.class.new

      filters.each do |filter|
        case filter
        when Symbol, String then
180 181 182 183
          if has_key?(filter)
            value = self[filter]
            params[filter] = value unless Hash === value
          end
184
          keys.grep(/\A#{Regexp.escape(filter)}\(\di\)\z/) { |key| params[key] = self[key] }
185
        when Hash then
186 187
          self.slice(*filter.keys).each do |key, values|
            return unless values
188 189 190

            key = key.to_sym

191
            params[key] = each_element(values) do |value|
192 193 194 195 196 197 198 199 200 201 202 203 204 205
              # 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
        end
      end

      params.permit!
    end

206 207 208 209 210 211
    # 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
212 213 214 215
    def [](key)
      convert_hashes_to_parameters(key, super)
    end

216 217 218 219 220 221 222 223
    # 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"}
224
    #   params.fetch(:none)           # => ActionController::ParameterMissing: param not found: none
225 226
    #   params.fetch(:none, 'Francesco')    # => "Francesco"
    #   params.fetch(:none) { 'Francesco' } # => "Francesco"
227 228 229 230 231 232
    def fetch(key, *args)
      convert_hashes_to_parameters(key, super)
    rescue KeyError
      raise ActionController::ParameterMissing.new(key)
    end

233 234 235 236 237 238 239
    # 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)     # => {}
240 241 242 243
    def slice(*keys)
      self.class.new(super)
    end

244 245 246 247 248 249 250 251
    # 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
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
    def dup
      super.tap do |duplicate|
        duplicate.instance_variable_set :@permitted, @permitted
      end
    end

    private
      def convert_hashes_to_parameters(key, value)
        if value.is_a?(Parameters) || !value.is_a?(Hash)
          value
        else
          # Convert to Parameters on first access
          self[key] = self.class.new(value)
        end
      end

      def each_element(object)
        if object.is_a?(Array)
          object.map { |el| yield el }.compact
271 272 273 274
        elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ }
          hash = object.class.new
          object.each { |k,v| hash[k] = yield v }
          hash
275 276 277 278 279 280
        else
          yield object
        end
      end
  end

281
  # == Strong \Parameters
282
  #
283 284 285
  # 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
286 287 288 289 290 291 292
  # 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
293 294 295 296
  #     # 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:
297
  #     def create
298
  #       Person.create(person_params)
299 300 301
  #     end
  #
  #     # This will pass with flying colors as long as there's a person key in the
302
  #     # parameters, otherwise it'll raise an ActionController::MissingParameter
303
  #     # exception, which will get caught by ActionController::Base and turned
304
  #     # into a 400 Bad Request reply.
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
  #     def update
  #       redirect_to current_account.people.find(params[:id]).tap { |person|
  #         person.update_attributes!(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
  #
  # See ActionController::Parameters.require and ActionController::Parameters.permit
  # for more information.
323 324 325 326 327 328 329 330 331 332
  module StrongParameters
    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

333 334
    # Returns a new ActionController::Parameters object that
    # has been instantiated with the <tt>request.parameters</tt>.
335 336 337 338
    def params
      @_params ||= Parameters.new(request.parameters)
    end

339 340 341 342 343
    # 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
344 345 346
    end
  end
end