durable_parameters 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +853 -0
- data/Rakefile +29 -0
- data/app/params/account_params.rb.example +38 -0
- data/app/params/application_params.rb +16 -0
- data/lib/durable_parameters/adapters/hanami.rb +138 -0
- data/lib/durable_parameters/adapters/rage.rb +124 -0
- data/lib/durable_parameters/adapters/rails.rb +280 -0
- data/lib/durable_parameters/adapters/sinatra.rb +91 -0
- data/lib/durable_parameters/core/application_params.rb +334 -0
- data/lib/durable_parameters/core/configuration.rb +83 -0
- data/lib/durable_parameters/core/forbidden_attributes_protection.rb +48 -0
- data/lib/durable_parameters/core/parameters.rb +643 -0
- data/lib/durable_parameters/core/params_registry.rb +110 -0
- data/lib/durable_parameters/core.rb +15 -0
- data/lib/durable_parameters/log_subscriber.rb +34 -0
- data/lib/durable_parameters/railtie.rb +65 -0
- data/lib/durable_parameters/version.rb +7 -0
- data/lib/durable_parameters.rb +41 -0
- data/lib/generators/rails/USAGE +12 -0
- data/lib/generators/rails/durable_parameters_controller_generator.rb +17 -0
- data/lib/generators/rails/templates/controller.rb +94 -0
- data/lib/legacy/action_controller/application_params.rb +235 -0
- data/lib/legacy/action_controller/parameters.rb +524 -0
- data/lib/legacy/action_controller/params_registry.rb +108 -0
- data/lib/legacy/active_model/forbidden_attributes_protection.rb +40 -0
- data/test/action_controller_required_params_test.rb +36 -0
- data/test/action_controller_tainted_params_test.rb +29 -0
- data/test/active_model_mass_assignment_taint_protection_test.rb +25 -0
- data/test/application_params_array_test.rb +245 -0
- data/test/application_params_edge_cases_test.rb +361 -0
- data/test/application_params_test.rb +893 -0
- data/test/controller_generator_test.rb +31 -0
- data/test/core_parameters_test.rb +2376 -0
- data/test/durable_parameters_test.rb +115 -0
- data/test/enhanced_error_messages_test.rb +120 -0
- data/test/gemfiles/Gemfile.rails-3.0.x +14 -0
- data/test/gemfiles/Gemfile.rails-3.1.x +14 -0
- data/test/gemfiles/Gemfile.rails-3.2.x +14 -0
- data/test/log_on_unpermitted_params_test.rb +49 -0
- data/test/metadata_validation_test.rb +294 -0
- data/test/multi_parameter_attributes_test.rb +38 -0
- data/test/parameters_core_methods_test.rb +503 -0
- data/test/parameters_integration_test.rb +553 -0
- data/test/parameters_permit_test.rb +491 -0
- data/test/parameters_require_test.rb +9 -0
- data/test/parameters_taint_test.rb +98 -0
- data/test/params_registry_concurrency_test.rb +422 -0
- data/test/params_registry_test.rb +112 -0
- data/test/permit_by_model_test.rb +227 -0
- data/test/raise_on_unpermitted_params_test.rb +32 -0
- data/test/test_helper.rb +38 -0
- data/test/transform_params_edge_cases_test.rb +526 -0
- data/test/transformation_test.rb +360 -0
- metadata +223 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'bigdecimal'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
module StrongParameters
|
|
8
|
+
module Core
|
|
9
|
+
# Exception raised when a required parameter is missing or empty.
|
|
10
|
+
#
|
|
11
|
+
# This exception provides helpful context about what went wrong and suggests
|
|
12
|
+
# how to fix it, making debugging easier for developers.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# params = Parameters.new({})
|
|
16
|
+
# params.require(:user)
|
|
17
|
+
# # => ParameterMissing: param is missing or the value is empty: user
|
|
18
|
+
# #
|
|
19
|
+
# # Expected to find parameter 'user' in the request, but it was missing.
|
|
20
|
+
# #
|
|
21
|
+
# # Make sure your request includes this parameter. For example:
|
|
22
|
+
# # POST /users with { "user": { ... } }
|
|
23
|
+
class ParameterMissing < IndexError
|
|
24
|
+
# @return [Symbol, String] the name of the missing parameter
|
|
25
|
+
attr_reader :param
|
|
26
|
+
|
|
27
|
+
# Initialize a new ParameterMissing exception
|
|
28
|
+
#
|
|
29
|
+
# @param param [Symbol, String] the name of the missing parameter
|
|
30
|
+
# @param context [Hash] optional context for better error messages
|
|
31
|
+
# @option context [Array<String>] :available_keys keys that were present
|
|
32
|
+
def initialize(param, context = {})
|
|
33
|
+
@param = param
|
|
34
|
+
message = build_message(param, context)
|
|
35
|
+
super(message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_message(param, context)
|
|
41
|
+
msg = "param is missing or the value is empty: #{param}"
|
|
42
|
+
|
|
43
|
+
if context[:available_keys]&.any?
|
|
44
|
+
msg += "\n\n"
|
|
45
|
+
msg += "Available keys: #{context[:available_keys].join(', ')}"
|
|
46
|
+
msg += "\n"
|
|
47
|
+
|
|
48
|
+
# Suggest similar keys if available
|
|
49
|
+
similar = find_similar_keys(param.to_s, context[:available_keys])
|
|
50
|
+
if similar.any?
|
|
51
|
+
msg += "\nDid you mean? #{similar.join(', ')}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
msg
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_similar_keys(param, available_keys)
|
|
59
|
+
return [] unless available_keys
|
|
60
|
+
|
|
61
|
+
param_str = param.to_s.downcase
|
|
62
|
+
available_keys.select do |key|
|
|
63
|
+
key_str = key.to_s.downcase
|
|
64
|
+
# Simple similarity check: starts with same letter or contains param
|
|
65
|
+
key_str[0] == param_str[0] || key_str.include?(param_str) || param_str.include?(key_str)
|
|
66
|
+
end.take(3)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Exception raised when unpermitted parameters are detected and configured to raise.
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# params.permit(:name)
|
|
74
|
+
# # With params containing :admin => true
|
|
75
|
+
# # => StrongParameters::Core::UnpermittedParameters: found unpermitted parameters: admin
|
|
76
|
+
class UnpermittedParameters < IndexError
|
|
77
|
+
# @return [Array<String>] the names of the unpermitted parameters
|
|
78
|
+
attr_reader :params
|
|
79
|
+
|
|
80
|
+
# Initialize a new UnpermittedParameters exception
|
|
81
|
+
#
|
|
82
|
+
# @param params [Array<String>] the names of the unpermitted parameters
|
|
83
|
+
def initialize(params)
|
|
84
|
+
@params = params
|
|
85
|
+
super("found unpermitted parameters: #{params.join(', ')}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Core Parameters implementation - framework-agnostic strong parameters.
|
|
90
|
+
#
|
|
91
|
+
# This class provides a whitelist-based approach to mass assignment protection,
|
|
92
|
+
# requiring explicit permission for parameters before they can be used.
|
|
93
|
+
#
|
|
94
|
+
# @example Basic usage
|
|
95
|
+
# params = StrongParameters::Core::Parameters.new(user: { name: 'John', admin: true })
|
|
96
|
+
# params.require(:user).permit(:name)
|
|
97
|
+
# # => <StrongParameters::Core::Parameters {"name"=>"John"} permitted: true>
|
|
98
|
+
class Parameters < Hash
|
|
99
|
+
# @return [Boolean] whether this Parameters object is permitted
|
|
100
|
+
attr_accessor :permitted
|
|
101
|
+
alias permitted? permitted
|
|
102
|
+
|
|
103
|
+
# @return [Symbol, String, nil] the key used in the last require() call
|
|
104
|
+
attr_accessor :required_key
|
|
105
|
+
|
|
106
|
+
class << self
|
|
107
|
+
# @return [Symbol, nil] action to take on unpermitted parameters (:log, :raise, or nil/false)
|
|
108
|
+
attr_accessor :action_on_unpermitted_parameters
|
|
109
|
+
|
|
110
|
+
# @return [Proc, nil] notification handler for unpermitted parameters
|
|
111
|
+
attr_accessor :unpermitted_notification_handler
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Parameters that are never considered unpermitted.
|
|
115
|
+
# These are added by frameworks and are of no concern for security.
|
|
116
|
+
NEVER_UNPERMITTED_PARAMS = %w[controller action].freeze
|
|
117
|
+
|
|
118
|
+
# Initialize a new Parameters object.
|
|
119
|
+
#
|
|
120
|
+
# @param attributes [Hash, nil] the initial attributes
|
|
121
|
+
def initialize(attributes = nil)
|
|
122
|
+
super()
|
|
123
|
+
@permitted = false
|
|
124
|
+
@required_key = nil
|
|
125
|
+
update(deep_normalize_keys(attributes)) if attributes
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Mark this Parameters object and all nested Parameters as permitted.
|
|
129
|
+
#
|
|
130
|
+
# Use with extreme caution as this allows all current and future attributes
|
|
131
|
+
# to be mass-assigned. Should only be used when you fully trust the source
|
|
132
|
+
# of the parameters.
|
|
133
|
+
#
|
|
134
|
+
# @return [Parameters] self
|
|
135
|
+
# @example
|
|
136
|
+
# params.require(:log_entry).permit!
|
|
137
|
+
def permit!
|
|
138
|
+
each_pair do |key, value|
|
|
139
|
+
value = convert_hashes_to_parameters(key, value)
|
|
140
|
+
wrap_array(value).each do |item|
|
|
141
|
+
item.permit! if item.respond_to?(:permit!)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@permitted = true
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Ensure that a parameter is present and not empty.
|
|
150
|
+
#
|
|
151
|
+
# If the parameter is missing or empty, raises ParameterMissing exception
|
|
152
|
+
# with helpful context about available keys and suggestions.
|
|
153
|
+
# If the parameter is present and is a Parameters object, tracks the key
|
|
154
|
+
# for later use by transform_params.
|
|
155
|
+
#
|
|
156
|
+
# @param key [Symbol, String] the parameter key to require
|
|
157
|
+
# @return [Object] the parameter value
|
|
158
|
+
# @raise [ParameterMissing] if the parameter is missing or empty
|
|
159
|
+
#
|
|
160
|
+
# @example Basic usage
|
|
161
|
+
# params.require(:user) # => returns params[:user] or raises ParameterMissing
|
|
162
|
+
#
|
|
163
|
+
# @example Chaining with permit
|
|
164
|
+
# params.require(:user).permit(:name, :email)
|
|
165
|
+
#
|
|
166
|
+
# @example Error with helpful context
|
|
167
|
+
# params = Parameters.new(usr: {name: 'John'})
|
|
168
|
+
# params.require(:user)
|
|
169
|
+
# # => ParameterMissing: param is missing or the value is empty: user
|
|
170
|
+
# #
|
|
171
|
+
# # Available keys: usr
|
|
172
|
+
# # Did you mean? usr
|
|
173
|
+
def require(key)
|
|
174
|
+
key = normalize_key(key)
|
|
175
|
+
value = self[key]
|
|
176
|
+
value = nil if value.respond_to?(:empty?) && value.empty?
|
|
177
|
+
|
|
178
|
+
if value.nil?
|
|
179
|
+
# Provide helpful context in error message
|
|
180
|
+
context = { available_keys: keys }
|
|
181
|
+
raise ParameterMissing.new(key, context)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Track the required key so transform_params can infer the params class
|
|
185
|
+
if value.is_a?(Parameters) && value.required_key.nil?
|
|
186
|
+
value.required_key = @required_key || key.to_sym
|
|
187
|
+
end
|
|
188
|
+
value
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
alias required require
|
|
192
|
+
|
|
193
|
+
# Create a new Parameters object with only the specified keys permitted.
|
|
194
|
+
#
|
|
195
|
+
# Filters can be symbols/strings for scalar values, or hashes for nested
|
|
196
|
+
# parameters. Only explicitly permitted parameters will be included in
|
|
197
|
+
# the returned object.
|
|
198
|
+
#
|
|
199
|
+
# @param filters [Array<Symbol, String, Hash>] the keys to permit
|
|
200
|
+
# @return [Parameters] a new Parameters object containing only permitted keys
|
|
201
|
+
# @example Permit scalar values
|
|
202
|
+
# params.permit(:name, :age)
|
|
203
|
+
# @example Permit nested parameters
|
|
204
|
+
# params.permit(:name, emails: [], friends: [:name, { family: [:name] }])
|
|
205
|
+
def permit(*filters)
|
|
206
|
+
params = self.class.new
|
|
207
|
+
|
|
208
|
+
filters.flatten.each do |filter|
|
|
209
|
+
case filter
|
|
210
|
+
when Symbol, String
|
|
211
|
+
permitted_scalar_filter(params, filter)
|
|
212
|
+
when Hash
|
|
213
|
+
hash_filter(params, filter)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
|
|
218
|
+
|
|
219
|
+
params.permit!
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Transform and permit parameters using a declarative params class.
|
|
223
|
+
#
|
|
224
|
+
# This method provides a declarative approach to parameter filtering using
|
|
225
|
+
# params classes. It automatically looks up the appropriate params class
|
|
226
|
+
# based on the required_key if not explicitly provided.
|
|
227
|
+
#
|
|
228
|
+
# Transformations defined in the params class are applied before filtering.
|
|
229
|
+
# This allows you to normalize, validate, or modify parameter values based
|
|
230
|
+
# on metadata like the current user or action.
|
|
231
|
+
#
|
|
232
|
+
# @param params_class [Class, Symbol, nil] optional params class or :__infer__ (default)
|
|
233
|
+
# - Class: Use this specific params class (e.g., UserParams)
|
|
234
|
+
# - :__infer__: Infer from required_key (e.g., :user -> UserParams)
|
|
235
|
+
# - nil: Skip lookup and return empty permitted params
|
|
236
|
+
# @param options [Hash] metadata and configuration options
|
|
237
|
+
# @option options [Symbol, String] :action Action name for action-specific filtering
|
|
238
|
+
# @option options [Array<Symbol>] :additional_attrs Additional attributes to permit
|
|
239
|
+
# @option options [Object] :current_user Current user (always allowed, no declaration needed)
|
|
240
|
+
# @return [Parameters] permitted parameters with only allowed attributes
|
|
241
|
+
# @raise [ArgumentError] if undeclared metadata keys are passed
|
|
242
|
+
#
|
|
243
|
+
# @example Basic usage with inference
|
|
244
|
+
# params.require(:user).transform_params
|
|
245
|
+
# # Looks up UserParams automatically
|
|
246
|
+
#
|
|
247
|
+
# @example Explicit params class
|
|
248
|
+
# params.require(:user).transform_params(AdminUserParams)
|
|
249
|
+
#
|
|
250
|
+
# @example With action-specific filtering
|
|
251
|
+
# params.require(:post).transform_params(action: :create)
|
|
252
|
+
#
|
|
253
|
+
# @example With metadata for transformations
|
|
254
|
+
# params.require(:user).transform_params(current_user: current_user)
|
|
255
|
+
# # Transformations can access current_user
|
|
256
|
+
#
|
|
257
|
+
# @example With additional attributes
|
|
258
|
+
# params.require(:user).transform_params(additional_attrs: [:temp_token])
|
|
259
|
+
def transform_params(params_class = :__infer__, **options)
|
|
260
|
+
# Extract known options (these don't need to be declared as metadata)
|
|
261
|
+
action = options[:action]
|
|
262
|
+
additional_attrs = options[:additional_attrs] || []
|
|
263
|
+
|
|
264
|
+
# Infer params class from required_key if not explicitly provided
|
|
265
|
+
if params_class == :__infer__
|
|
266
|
+
if @required_key
|
|
267
|
+
params_class = ParamsRegistry.lookup(@required_key)
|
|
268
|
+
else
|
|
269
|
+
# If no required_key and no explicit params_class, return empty permitted params
|
|
270
|
+
return self.class.new.permit!
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Handle case where params_class is nil (explicitly passed or not registered)
|
|
275
|
+
if params_class.nil?
|
|
276
|
+
return self.class.new.permit!
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Validate metadata keys (excluding known options)
|
|
280
|
+
metadata_keys = options.keys - [:action, :additional_attrs]
|
|
281
|
+
validate_metadata_keys!(params_class, metadata_keys)
|
|
282
|
+
|
|
283
|
+
# Apply transformations first (before filtering)
|
|
284
|
+
# Pass all options as metadata to the transformations
|
|
285
|
+
transformed_hash = params_class.apply_transformations(self, options)
|
|
286
|
+
|
|
287
|
+
# Create a new Parameters object from the transformed hash
|
|
288
|
+
transformed_params = self.class.new(transformed_hash)
|
|
289
|
+
|
|
290
|
+
# Get permitted attributes and apply them
|
|
291
|
+
permitted_attrs = params_class.permitted_attributes(action: action) || []
|
|
292
|
+
permitted_attrs += additional_attrs
|
|
293
|
+
transformed_params.permit(*permitted_attrs)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Legacy alias for backwards compatibility.
|
|
297
|
+
#
|
|
298
|
+
# @deprecated Use {#transform_params} instead
|
|
299
|
+
# @param model_name [Symbol, String] the model name to look up
|
|
300
|
+
# @param action [Symbol, String, nil] optional action name
|
|
301
|
+
# @param additional_attrs [Array<Symbol>] additional attributes to permit
|
|
302
|
+
# @return [Parameters] permitted parameters
|
|
303
|
+
def permit_by_model(model_name, action: nil, additional_attrs: [])
|
|
304
|
+
params_class = ParamsRegistry.lookup(model_name)
|
|
305
|
+
transform_params(params_class, action: action, additional_attrs: additional_attrs)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Access a parameter value by key, converting hashes to Parameters objects.
|
|
309
|
+
#
|
|
310
|
+
# @param key [Symbol, String] the parameter key
|
|
311
|
+
# @return [Object] the parameter value
|
|
312
|
+
def [](key)
|
|
313
|
+
convert_hashes_to_parameters(normalize_key(key), super(normalize_key(key)))
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Set a parameter value by key.
|
|
317
|
+
#
|
|
318
|
+
# @param key [Symbol, String] the parameter key
|
|
319
|
+
# @param value [Object] the parameter value
|
|
320
|
+
def []=(key, value)
|
|
321
|
+
super(normalize_key(key), value)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Fetch a parameter value by key, raising ParameterMissing if not found.
|
|
325
|
+
#
|
|
326
|
+
# @param key [Symbol, String] the parameter key
|
|
327
|
+
# @param args additional arguments passed to Hash#fetch
|
|
328
|
+
# @return [Object] the parameter value
|
|
329
|
+
# @raise [ParameterMissing] if the key is not found
|
|
330
|
+
def fetch(key, *args)
|
|
331
|
+
key = normalize_key(key)
|
|
332
|
+
convert_hashes_to_parameters(key, super(key, *args), false)
|
|
333
|
+
rescue KeyError, IndexError
|
|
334
|
+
raise ParameterMissing.new(key)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Check if a key exists in the parameters.
|
|
338
|
+
#
|
|
339
|
+
# @param key [Symbol, String] the parameter key
|
|
340
|
+
# @return [Boolean] true if the key exists
|
|
341
|
+
def has_key?(key)
|
|
342
|
+
super(normalize_key(key))
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
alias key? has_key?
|
|
346
|
+
alias include? has_key?
|
|
347
|
+
|
|
348
|
+
# Delete a key from the parameters.
|
|
349
|
+
#
|
|
350
|
+
# @param key [Symbol, String] the parameter key
|
|
351
|
+
# @return [Object] the deleted value
|
|
352
|
+
def delete(key)
|
|
353
|
+
super(normalize_key(key))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Create a new Parameters object containing only the specified keys.
|
|
357
|
+
#
|
|
358
|
+
# @param keys [Array<Symbol, String>] the keys to include
|
|
359
|
+
# @return [Parameters] a new Parameters object with only the specified keys
|
|
360
|
+
# @example
|
|
361
|
+
# params.slice(:name, :email)
|
|
362
|
+
def slice(*keys)
|
|
363
|
+
normalized_keys = keys.map { |k| normalize_key(k) }
|
|
364
|
+
sliced = select { |k, v| normalized_keys.include?(k) }
|
|
365
|
+
|
|
366
|
+
self.class.new(sliced).tap do |new_instance|
|
|
367
|
+
new_instance.instance_variable_set(:@permitted, @permitted)
|
|
368
|
+
new_instance.instance_variable_set(:@required_key, @required_key)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Create a new Parameters object excluding the specified keys.
|
|
373
|
+
#
|
|
374
|
+
# @param keys [Array<Symbol, String>] the keys to exclude
|
|
375
|
+
# @return [Parameters] a new Parameters object without the specified keys
|
|
376
|
+
# @example
|
|
377
|
+
# params.except(:password, :password_confirmation)
|
|
378
|
+
def except(*keys)
|
|
379
|
+
# Return a new Parameters instance excluding the specified keys
|
|
380
|
+
excepted = dup
|
|
381
|
+
keys.each { |key| excepted.delete(key) }
|
|
382
|
+
excepted
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Create a duplicate of this Parameters object.
|
|
386
|
+
#
|
|
387
|
+
# @return [Parameters] a new Parameters object with the same data and state
|
|
388
|
+
def dup
|
|
389
|
+
self.class.new(self).tap do |duplicate|
|
|
390
|
+
duplicate.instance_variable_set(:@permitted, @permitted)
|
|
391
|
+
duplicate.instance_variable_set(:@required_key, @required_key)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Convert to a regular Hash.
|
|
396
|
+
#
|
|
397
|
+
# @return [Hash] a regular hash with string keys
|
|
398
|
+
def to_h
|
|
399
|
+
each_with_object({}) do |(key, value), hash|
|
|
400
|
+
hash[key.to_s] = value.is_a?(Parameters) ? value.to_h : value
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Convert to an unsafe hash (without permission checking).
|
|
405
|
+
#
|
|
406
|
+
# @return [Hash] a regular hash
|
|
407
|
+
def to_unsafe_h
|
|
408
|
+
to_h
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
alias to_hash to_h
|
|
412
|
+
|
|
413
|
+
protected
|
|
414
|
+
|
|
415
|
+
def convert_value(value)
|
|
416
|
+
if value.is_a?(Hash) && !value.is_a?(Parameters)
|
|
417
|
+
self.class.new(value)
|
|
418
|
+
elsif value.is_a?(Array)
|
|
419
|
+
value.map { |e| convert_value(e) }
|
|
420
|
+
else
|
|
421
|
+
value
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private
|
|
426
|
+
|
|
427
|
+
def normalize_key(key)
|
|
428
|
+
key.to_s
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def deep_normalize_keys(hash)
|
|
432
|
+
return hash unless hash.is_a?(Hash)
|
|
433
|
+
|
|
434
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
435
|
+
normalized_key = normalize_key(key)
|
|
436
|
+
result[normalized_key] = if value.is_a?(Hash)
|
|
437
|
+
deep_normalize_keys(value)
|
|
438
|
+
elsif value.is_a?(Array)
|
|
439
|
+
value.map { |item| item.is_a?(Hash) ? deep_normalize_keys(item) : item }
|
|
440
|
+
else
|
|
441
|
+
value
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def convert_hashes_to_parameters(key, value, assign_if_converted = true)
|
|
447
|
+
converted = convert_value_to_parameters(value)
|
|
448
|
+
self[key] = converted if assign_if_converted && !converted.equal?(value)
|
|
449
|
+
converted
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def convert_value_to_parameters(value)
|
|
453
|
+
if value.is_a?(Array)
|
|
454
|
+
value.map { |item| convert_value_to_parameters(item) }
|
|
455
|
+
elsif value.is_a?(Parameters) || !value.is_a?(Hash)
|
|
456
|
+
value
|
|
457
|
+
else
|
|
458
|
+
self.class.new(value)
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def wrap_array(value)
|
|
463
|
+
value.is_a?(Array) ? value : [value]
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
#
|
|
467
|
+
# --- Filtering ----------------------------------------------------------
|
|
468
|
+
#
|
|
469
|
+
|
|
470
|
+
# Whitelist of permitted scalar types for parameter filtering.
|
|
471
|
+
#
|
|
472
|
+
# These types are considered safe for mass assignment and include types
|
|
473
|
+
# commonly used in XML and JSON requests. String is first to optimize
|
|
474
|
+
# the common case through short-circuit evaluation.
|
|
475
|
+
#
|
|
476
|
+
# **Permitted Types:**
|
|
477
|
+
# - String, Symbol - text values
|
|
478
|
+
# - NilClass - null values
|
|
479
|
+
# - Numeric (Integer, Float, BigDecimal, etc.) - numeric values
|
|
480
|
+
# - TrueClass, FalseClass - boolean values
|
|
481
|
+
# - Date, Time, DateTime - temporal values
|
|
482
|
+
# - StringIO, IO - file-like objects
|
|
483
|
+
#
|
|
484
|
+
# @note DateTime inherits from Date, so it's implicitly included.
|
|
485
|
+
# @note If you modify this list, please update the README documentation.
|
|
486
|
+
PERMITTED_SCALAR_TYPES = [
|
|
487
|
+
String,
|
|
488
|
+
Symbol,
|
|
489
|
+
NilClass,
|
|
490
|
+
Numeric,
|
|
491
|
+
TrueClass,
|
|
492
|
+
FalseClass,
|
|
493
|
+
Date,
|
|
494
|
+
Time,
|
|
495
|
+
# DateTimes are Dates, we document the type but avoid the redundant check.
|
|
496
|
+
StringIO,
|
|
497
|
+
IO
|
|
498
|
+
].freeze
|
|
499
|
+
|
|
500
|
+
# Check if a value is a permitted scalar type.
|
|
501
|
+
#
|
|
502
|
+
# @param value [Object] the value to check
|
|
503
|
+
# @return [Boolean] true if value is a permitted scalar type
|
|
504
|
+
def permitted_scalar?(value)
|
|
505
|
+
PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Check if a value is an array containing only permitted scalars.
|
|
509
|
+
#
|
|
510
|
+
# @param value [Object] the value to check
|
|
511
|
+
# @return [Boolean, nil] true if array of permitted scalars, nil otherwise
|
|
512
|
+
def array_of_permitted_scalars?(value)
|
|
513
|
+
return unless value.is_a?(Array)
|
|
514
|
+
|
|
515
|
+
value.all? { |element| permitted_scalar?(element) }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def permitted_scalar_filter(params, key)
|
|
519
|
+
key = normalize_key(key)
|
|
520
|
+
if has_key?(key) && permitted_scalar?(self[key])
|
|
521
|
+
params[key] = self[key]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/).each do |matched_key|
|
|
525
|
+
if permitted_scalar?(self[matched_key])
|
|
526
|
+
params[matched_key] = self[matched_key]
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def array_of_permitted_scalars_filter(params, key, hash = self)
|
|
532
|
+
key = normalize_key(key)
|
|
533
|
+
if hash.has_key?(key) && array_of_permitted_scalars?(hash[key])
|
|
534
|
+
params[key] = hash[key]
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def hash_filter(params, filter)
|
|
539
|
+
# Normalize filter keys
|
|
540
|
+
normalized_filter = filter.each_with_object({}) do |(k, v), result|
|
|
541
|
+
result[normalize_key(k)] = v
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Slicing filters out non-declared keys.
|
|
545
|
+
slice(*normalized_filter.keys).each do |key, value|
|
|
546
|
+
next unless value
|
|
547
|
+
|
|
548
|
+
if normalized_filter[key] == []
|
|
549
|
+
# Declaration {:comment_ids => []}.
|
|
550
|
+
array_of_permitted_scalars_filter(params, key)
|
|
551
|
+
else
|
|
552
|
+
# Declaration {:user => :name} or {:user => [:name, :age, {:address => ...}]}.
|
|
553
|
+
params[key] = each_element(value) do |element, index|
|
|
554
|
+
if element.is_a?(Hash)
|
|
555
|
+
element = self.class.new(element) unless element.respond_to?(:permit)
|
|
556
|
+
element.permit(*wrap_array(normalized_filter[key]))
|
|
557
|
+
elsif normalized_filter[key].is_a?(Hash) && normalized_filter[key][index] == []
|
|
558
|
+
array_of_permitted_scalars_filter(params, index, value)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def each_element(value)
|
|
566
|
+
if value.is_a?(Array)
|
|
567
|
+
value.map { |el| yield el }.compact
|
|
568
|
+
# fields_for on an array of records uses numeric hash keys.
|
|
569
|
+
elsif fields_for_style?(value)
|
|
570
|
+
hash = value.class.new
|
|
571
|
+
value.each { |k, v| hash[k] = yield(v, k) }
|
|
572
|
+
hash
|
|
573
|
+
else
|
|
574
|
+
yield value
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def fields_for_style?(object)
|
|
579
|
+
object.is_a?(Hash) && !object.empty? && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def unpermitted_parameters!(params)
|
|
583
|
+
return unless self.class.action_on_unpermitted_parameters
|
|
584
|
+
|
|
585
|
+
unpermitted_keys = unpermitted_keys(params)
|
|
586
|
+
|
|
587
|
+
if unpermitted_keys.any?
|
|
588
|
+
case self.class.action_on_unpermitted_parameters
|
|
589
|
+
when :log
|
|
590
|
+
notify_unpermitted(unpermitted_keys)
|
|
591
|
+
when :raise
|
|
592
|
+
raise UnpermittedParameters.new(unpermitted_keys)
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def unpermitted_keys(params)
|
|
598
|
+
keys - params.keys - NEVER_UNPERMITTED_PARAMS
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def notify_unpermitted(keys)
|
|
602
|
+
if self.class.unpermitted_notification_handler
|
|
603
|
+
begin
|
|
604
|
+
self.class.unpermitted_notification_handler.call(keys)
|
|
605
|
+
rescue => e
|
|
606
|
+
# Log the error but don't prevent parameter processing
|
|
607
|
+
# In a real application, you might want to log this
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Validate that all provided metadata keys are allowed by the params class.
|
|
613
|
+
#
|
|
614
|
+
# @param params_class [Class] the params class to validate against
|
|
615
|
+
# @param metadata_keys [Array<Symbol>] the metadata keys to validate
|
|
616
|
+
# @raise [ArgumentError] if any metadata keys are not declared
|
|
617
|
+
# @return [void]
|
|
618
|
+
def validate_metadata_keys!(params_class, metadata_keys)
|
|
619
|
+
return if metadata_keys.empty?
|
|
620
|
+
|
|
621
|
+
disallowed_keys = metadata_keys.reject { |key| key == :current_user || params_class.metadata_allowed?(key) }
|
|
622
|
+
|
|
623
|
+
return unless disallowed_keys.any?
|
|
624
|
+
|
|
625
|
+
# Build a helpful error message
|
|
626
|
+
keys_list = disallowed_keys.map(&:inspect).join(', ')
|
|
627
|
+
class_name = params_class.name
|
|
628
|
+
|
|
629
|
+
raise ArgumentError, <<~ERROR.strip
|
|
630
|
+
Metadata key(s) #{keys_list} not allowed for #{class_name}.
|
|
631
|
+
|
|
632
|
+
To fix this, declare them in your params class:
|
|
633
|
+
|
|
634
|
+
class #{class_name} < ApplicationParams
|
|
635
|
+
metadata #{disallowed_keys.map(&:inspect).join(', ')}
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
Note: :current_user is always implicitly allowed and doesn't need to be declared.
|
|
639
|
+
ERROR
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|