aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal/params_wrapper.rb
blob: 7361946de57adb411cc7bc20962fd90b270b1dd2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# frozen_string_literal: true

require "active_support/core_ext/hash/slice"
require "active_support/core_ext/hash/except"
require "active_support/core_ext/module/anonymous"
require "action_dispatch/http/mime_type"

module ActionController
  # Wraps the parameters hash into a nested hash. This will allow clients to
  # submit requests without having to specify any root elements.
  #
  # This functionality is enabled in +config/initializers/wrap_parameters.rb+
  # and can be customized.
  #
  # You could also turn it on per controller by setting the format array to
  # a non-empty array:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
  #     end
  #
  # If you enable +ParamsWrapper+ for +:json+ format, instead of having to
  # send JSON parameters like this:
  #
  #     {"user": {"name": "Konata"}}
  #
  # You can send parameters like this:
  #
  #     {"name": "Konata"}
  #
  # And it will be wrapped into a nested hash with the key name matching the
  # controller's name. For example, if you're posting to +UsersController+,
  # your new +params+ hash will look like this:
  #
  #     {"name" => "Konata", "user" => {"name" => "Konata"}}
  #
  # You can also specify the key in which the parameters should be wrapped to,
  # and also the list of attributes it should wrap by using either +:include+ or
  # +:exclude+ options like this:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters :person, include: [:username, :password]
  #     end
  #
  # On Active Record models with no +:include+ or +:exclude+ option set,
  # it will only wrap the parameters returned by the class method
  # <tt>attribute_names</tt>.
  #
  # If you're going to pass the parameters to an +ActiveModel+ object (such as
  # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
  # the method instead. The +ParamsWrapper+ will actually try to determine the
  # list of attribute names from the model and only wrap those attributes:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters Person
  #     end
  #
  # You still could pass +:include+ and +:exclude+ to set the list of attributes
  # you want to wrap.
  #
  # By default, if you don't specify the key in which the parameters would be
  # wrapped to, +ParamsWrapper+ will actually try to determine if there's
  # a model related to it or not. This controller, for example:
  #
  #     class Admin::UsersController < ApplicationController
  #     end
  #
  # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
  # determine the wrapper key respectively. If both models don't exist,
  # it will then fallback to use +user+ as the key.
  module ParamsWrapper
    extend ActiveSupport::Concern

    EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)

    require "mutex_m"

    class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
      include Mutex_m

      def self.from_hash(hash)
        name    = hash[:name]
        format  = Array(hash[:format])
        include = hash[:include] && Array(hash[:include]).collect(&:to_s)
        exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
        new name, format, include, exclude, nil, nil
      end

      def initialize(name, format, include, exclude, klass, model) # :nodoc:
        super
        @include_set = include
        @name_set    = name
      end

      def model
        super || synchronize { super || self.model = _default_wrap_model }
      end

      def include
        return super if @include_set

        m = model
        synchronize do
          return super if @include_set

          @include_set = true

          unless super || exclude
            if m.respond_to?(:attribute_names) && m.attribute_names.any?
              if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
                self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s)
              else
                self.include = m.attribute_names
              end

              if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
                self.include += m.nested_attributes_options.keys.map do |key|
                  key.to_s.concat("_attributes")
                end
              end

              self.include
            end
          end
        end
      end

      def name
        return super if @name_set

        m = model
        synchronize do
          return super if @name_set

          @name_set = true

          unless super || klass.anonymous?
            self.name = m ? m.to_s.demodulize.underscore :
              klass.controller_name.singularize
          end
        end
      end

      private
        # Determine the wrapper model from the controller's name. By convention,
        # this could be done by trying to find the defined model that has the
        # same singular name as the controller. For example, +UsersController+
        # will try to find if the +User+ model exists.
        #
        # This method also does namespace lookup. Foo::Bar::UsersController will
        # try to find Foo::Bar::User, Foo::User and finally User.
        def _default_wrap_model
          return nil if klass.anonymous?
          model_name = klass.name.sub(/Controller$/, "").classify

          begin
            if model_klass = model_name.safe_constantize
              model_klass
            else
              namespaces = model_name.split("::")
              namespaces.delete_at(-2)
              break if namespaces.last == model_name
              model_name = namespaces.join("::")
            end
          end until model_klass

          model_klass
        end
    end

    included do
      class_attribute :_wrapper_options, default: Options.from_hash(format: [])
    end

    module ClassMethods
      def _set_wrapper_options(options)
        self._wrapper_options = Options.from_hash(options)
      end

      # Sets the name of the wrapper key, or the model which +ParamsWrapper+
      # would use to determine the attribute names from.
      #
      # ==== Examples
      #   wrap_parameters format: :xml
      #     # enables the parameter wrapper for XML format
      #
      #   wrap_parameters :person
      #     # wraps parameters into +params[:person]+ hash
      #
      #   wrap_parameters Person
      #     # wraps parameters by determining the wrapper key from Person class
      #     (+person+, in this case) and the list of attribute names
      #
      #   wrap_parameters include: [:username, :title]
      #     # wraps only +:username+ and +:title+ attributes from parameters.
      #
      #   wrap_parameters false
      #     # disables parameters wrapping for this controller altogether.
      #
      # ==== Options
      # * <tt>:format</tt> - The list of formats in which the parameters wrapper
      #   will be enabled.
      # * <tt>:include</tt> - The list of attribute names which parameters wrapper
      #   will wrap into a nested hash.
      # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
      #   will exclude from a nested hash.
      def wrap_parameters(name_or_model_or_options, options = {})
        model = nil

        case name_or_model_or_options
        when Hash
          options = name_or_model_or_options
        when false
          options = options.merge(format: [])
        when Symbol, String
          options = options.merge(name: name_or_model_or_options)
        else
          model = name_or_model_or_options
        end

        opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
        opts.model = model
        opts.klass = self

        self._wrapper_options = opts
      end

      # Sets the default wrapper key or model which will be used to determine
      # wrapper key and attribute names. Called automatically when the
      # module is inherited.
      def inherited(klass)
        if klass._wrapper_options.format.any?
          params = klass._wrapper_options.dup
          params.klass = klass
          klass._wrapper_options = params
        end
        super
      end
    end

    # Performs parameters wrapping upon the request. Called automatically
    # by the metal call stack.
    def process_action(*args)
      if _wrapper_enabled?
        wrapped_hash = _wrap_parameters request.request_parameters
        wrapped_keys = request.request_parameters.keys
        wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)

        # This will make the wrapped hash accessible from controller and view.
        request.parameters.merge! wrapped_hash
        request.request_parameters.merge! wrapped_hash

        # This will display the wrapped hash in the log file.
        request.filtered_parameters.merge! wrapped_filtered_hash
      end
    ensure
      # NOTE: Rescues all exceptions so they
      # may be caught in ActionController::Rescue.
      return super
    end

    private

      # Returns the wrapper key which will be used to store wrapped parameters.
      def _wrapper_key
        _wrapper_options.name
      end

      # Returns the list of enabled formats.
      def _wrapper_formats
        _wrapper_options.format
      end

      # Returns the list of parameters which will be selected for wrapped.
      def _wrap_parameters(parameters)
        { _wrapper_key => _extract_parameters(parameters) }
      end

      def _extract_parameters(parameters)
        if include_only = _wrapper_options.include
          parameters.slice(*include_only)
        else
          exclude = _wrapper_options.exclude || []
          parameters.except(*(exclude + EXCLUDE_PARAMETERS))
        end
      end

      # Checks if we should perform parameters wrapping.
      def _wrapper_enabled?
        return false unless request.has_content_type?

        ref = request.content_mime_type.ref
        _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
      end
  end
end