aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal/params_wrapper.rb
blob: 93241fc05639060d46f728c84de91297264f70f7 (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
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/module/anonymous'
require 'action_dispatch/http/mime_types'

module ActionController
  # Wraps parameters hash into nested hash. This will allow client to submit
  # POST request without having to specify a root element in it.
  #
  # By default this functionality won't be enabled. You can enable
  # it globally by setting +ActionController::Base.wrap_parameters+:
  #
  #     ActionController::Base.wrap_parameters = [:json]
  #
  # You could also turn it on per controller by setting the format array to
  # non-empty array:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters :format => [:json, :xml]
  #     end
  #
  # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to
  # send JSON parameters like this:
  #
  #     {"user": {"name": "Konata"}}
  #
  # You can now just send a parameters like this:
  #
  #     {"name": "Konata"}
  #
  # And it will be wrapped into a nested hash with the key name matching
  # 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 +:only+ or
  # +:except+ options like this:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters :person, :only => [:username, :password]
  #     end
  #
  # If you're going to pass the parameters to an +ActiveModel+ object (such as
  # +User.new(params[:user])+), 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 +:only+ and +:except+ 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 +Admin::User+ or +User+ model exists, and use it to
  # determine the wrapper key respectively. If both of the model doesn't exists,
  # it will then fallback to use +user+ as the key.
  module ParamsWrapper
    extend ActiveSupport::Concern

    EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)

    included do
      class_attribute :_wrapper_options
      self._wrapper_options = {:format => []}
    end

    module ClassMethods
      # 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 parmeter wrapper for XML format
      #
      #   wrap_parameters :person
      #     # wraps parameters into +params[:person]+ hash
      #
      #   wrap_parameters Person
      #     # wraps parameters by determine the wrapper key from Person class
      #     (+person+, in this case) and the list of attribute names
      #
      #   wrap_parameters :only => [:username, :title]
      #     # wraps only +:username+ and +:title+ attributes from parameters.
      #
      #   wrap_parameters false
      #     # disable parameters wrapping for this controller altogether.
      #
      # ==== Options
      # * <tt>:format</tt> - The list of formats in which the parameters wrapper
      #   will be enabled.
      # * <tt>:only</tt> - The list of attribute names which parameters wrapper
      #   will wrap into a nested hash.
      # * <tt>:except</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

        _set_wrapper_defaults(_wrapper_options.slice(:format).merge(options), model)
      end

      # Sets the default wrapper key or model which will be used to determine
      # wrapper key and attribute names. Will be called automatically when the
      # module is inherited.
      def inherited(klass)
        if klass._wrapper_options[:format].present?
          klass._set_wrapper_defaults(klass._wrapper_options.slice(:format))
        end
        super
      end

      protected

      # 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 singularize 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 #:nodoc:
        return nil if self.anonymous?

        model_name = self.name.sub(/Controller$/, '').singularize

        begin
          model_klass = model_name.constantize
        rescue NameError, ArgumentError => e
          if e.message =~ /is not missing constant|uninitialized constant #{model_name}/
            namespaces = model_name.split("::")
            namespaces.delete_at(-2)
            break if namespaces.last == model_name
            model_name = namespaces.join("::")
          else
            raise
          end
        end until model_klass

        model_klass
      end

      def _set_wrapper_defaults(options, model=nil)
        options = options.dup

        unless options[:only] || options[:except]
          model ||= _default_wrap_model
          if model.respond_to?(:attribute_names) && model.attribute_names.present?
            options[:only] = model.attribute_names
          end
        end

        unless options[:name] || self.anonymous?
          model ||= _default_wrap_model
          options[:name] = model ? model.to_s.demodulize.underscore :
            controller_name.singularize
        end

        options[:only]   = Array.wrap(options[:only]).collect(&:to_s)   if options[:only]
        options[:except] = Array.wrap(options[:except]).collect(&:to_s) if options[:except]
        options[:format] = Array.wrap(options[:format])

        self._wrapper_options = options
      end
    end

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

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

        # This will make the wrapped hash displayed in the log file
        request.filtered_parameters.merge! wrapped_filtered_hash
      end
      super
    end

    private

      # Returns the wrapper key which will use to stored 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)
        value = if only = _wrapper_options[:only]
          parameters.slice(*only)
        else
          except = _wrapper_options[:except] || []
          parameters.except(*(except + EXCLUDE_PARAMETERS))
        end

        { _wrapper_key => value }
      end

      # Checks if we should perform parameters wrapping.
      def _wrapper_enabled?
        ref = request.content_mime_type.try(:ref)
        _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
      end
  end
end