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
|