Skip to content

Commit 14a3bd5

Browse files
committed
Make AC::Parameters not inherited from Hash
This is another take at rails#14384 as we decided to wait until `master` is targeting Rails 5.0. This commit is implementation-complete, as it guarantees that all the public methods on the hash-inherited Parameters are still working (based on test case). We can decide to follow-up later if we want to remove some methods out from Parameters.
1 parent a0b4dc2 commit 14a3bd5

File tree

10 files changed

+138
-35
lines changed

10 files changed

+138
-35
lines changed

actionpack/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
* `ActionController::Parameters` no longer inherits from
2+
`HashWithIndifferentAccess`
3+
4+
Inheriting from `HashWithIndifferentAccess` allowed users to call any
5+
enumerable methods on `Parameters` object, resulting in a risk of losing the
6+
`permitted?` status or even getting back a pure `Hash` object instead of
7+
a `Parameters` object with proper sanitization.
8+
9+
By stop inheriting from `HashWithIndifferentAccess`, we are able to make
10+
sure that all methods that are defined in `Parameters` object will return
11+
a proper `Parameters` object with a correct `permitted?` flag.
12+
13+
*Prem Sichanugrist*
14+
115
* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch`
216
from the concurrent-ruby gem.
317

actionpack/lib/action_controller/metal/strong_parameters.rb

Lines changed: 99 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,12 @@ def initialize(params) # :nodoc:
104104
# params = ActionController::Parameters.new(key: 'value')
105105
# params[:key] # => "value"
106106
# params["key"] # => "value"
107-
class Parameters < ActiveSupport::HashWithIndifferentAccess
107+
class Parameters
108108
cattr_accessor :permit_all_parameters, instance_accessor: false
109109
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
110110

111+
delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters
112+
111113
# By default, never raise an UnpermittedParameters exception if these
112114
# params are present. The default includes both 'controller' and 'action'
113115
# because they are added by Rails and should be of no concern. One way
@@ -144,11 +146,19 @@ def self.const_missing(const_name)
144146
# params = ActionController::Parameters.new(name: 'Francesco')
145147
# params.permitted? # => true
146148
# Person.new(params) # => #<Person id: nil, name: "Francesco">
147-
def initialize(attributes = nil)
148-
super(attributes)
149+
def initialize(parameters = {})
150+
@parameters = parameters.with_indifferent_access
149151
@permitted = self.class.permit_all_parameters
150152
end
151153

154+
def ==(other_hash)
155+
if other_hash.respond_to?(:permitted?)
156+
super
157+
else
158+
@parameters == other_hash
159+
end
160+
end
161+
152162
# Returns a safe +Hash+ representation of this parameter with all
153163
# unpermitted keys removed.
154164
#
@@ -162,28 +172,25 @@ def initialize(attributes = nil)
162172
# safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
163173
def to_h
164174
if permitted?
165-
to_hash
175+
@parameters.to_h
166176
else
167177
slice(*self.class.always_permitted_parameters).permit!.to_h
168178
end
169179
end
170180

171181
# Returns an unsafe, unfiltered +Hash+ representation of this parameter.
172182
def to_unsafe_h
173-
to_hash
183+
@parameters
174184
end
175185
alias_method :to_unsafe_hash, :to_unsafe_h
176186

177187
# Convert all hashes in values into parameters, then yield each pair like
178188
# the same way as <tt>Hash#each_pair</tt>
179189
def each_pair(&block)
180-
super do |key, value|
181-
convert_hashes_to_parameters(key, value)
190+
@parameters.each_pair do |key, value|
191+
yield key, convert_hashes_to_parameters(key, value)
182192
end
183-
184-
super
185193
end
186-
187194
alias_method :each, :each_pair
188195

189196
# Attribute that keeps track of converted arrays, if any, to avoid double
@@ -347,7 +354,11 @@ def permit(*filters)
347354
# params[:person] # => {"name"=>"Francesco"}
348355
# params[:none] # => nil
349356
def [](key)
350-
convert_hashes_to_parameters(key, super)
357+
convert_hashes_to_parameters(key, @parameters[key])
358+
end
359+
360+
def []=(key, value)
361+
@parameters[key] = value
351362
end
352363

353364
# Returns a parameter for the given +key+. If the +key+
@@ -361,8 +372,12 @@ def [](key)
361372
# params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
362373
# params.fetch(:none, 'Francesco') # => "Francesco"
363374
# params.fetch(:none) { 'Francesco' } # => "Francesco"
364-
def fetch(key, *args)
365-
convert_hashes_to_parameters(key, super, false)
375+
def fetch(key, *args, &block)
376+
convert_hashes_to_parameters(
377+
key,
378+
@parameters.fetch(key, *args, &block),
379+
false
380+
)
366381
rescue KeyError
367382
raise ActionController::ParameterMissing.new(key)
368383
end
@@ -375,7 +390,16 @@ def fetch(key, *args)
375390
# params.slice(:a, :b) # => {"a"=>1, "b"=>2}
376391
# params.slice(:d) # => {}
377392
def slice(*keys)
378-
new_instance_with_inherited_permitted_status(super)
393+
new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
394+
end
395+
396+
def slice!(*keys)
397+
@parameters.slice!(*keys)
398+
self
399+
end
400+
401+
def except(*keys)
402+
new_instance_with_inherited_permitted_status(@parameters.except(*keys))
379403
end
380404

381405
# Removes and returns the key/value pairs matching the given keys.
@@ -384,7 +408,7 @@ def slice(*keys)
384408
# params.extract!(:a, :b) # => {"a"=>1, "b"=>2}
385409
# params # => {"c"=>3}
386410
def extract!(*keys)
387-
new_instance_with_inherited_permitted_status(super)
411+
new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
388412
end
389413

390414
# Returns a new <tt>ActionController::Parameters</tt> with the results of
@@ -393,36 +417,70 @@ def extract!(*keys)
393417
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
394418
# params.transform_values { |x| x * 2 }
395419
# # => {"a"=>2, "b"=>4, "c"=>6}
396-
def transform_values
397-
if block_given?
398-
new_instance_with_inherited_permitted_status(super)
420+
def transform_values(&block)
421+
if block
422+
new_instance_with_inherited_permitted_status(
423+
@parameters.transform_values(&block)
424+
)
399425
else
400-
super
426+
@parameters.transform_values
401427
end
402428
end
403429

430+
def transform_values!(&block)
431+
@parameters.transform_values!(&block)
432+
self
433+
end
434+
404435
# This method is here only to make sure that the returned object has the
405436
# correct +permitted+ status. It should not matter since the parent of
406437
# this object is +HashWithIndifferentAccess+
407-
def transform_keys # :nodoc:
408-
if block_given?
409-
new_instance_with_inherited_permitted_status(super)
438+
def transform_keys(&block) # :nodoc:
439+
if block
440+
new_instance_with_inherited_permitted_status(
441+
@parameters.transform_keys(&block)
442+
)
410443
else
411-
super
444+
@parameters.transform_keys
412445
end
413446
end
414447

448+
def transform_keys!(&block)
449+
@parameters.transform_keys!(&block)
450+
self
451+
end
452+
415453
# Deletes and returns a key-value pair from +Parameters+ whose key is equal
416454
# to key. If the key is not found, returns the default value. If the
417455
# optional code block is given and the key is not found, pass in the key
418456
# and return the result of block.
419457
def delete(key, &block)
420-
convert_hashes_to_parameters(key, super, false)
458+
convert_hashes_to_parameters(key, @parameters.delete(key), false)
459+
end
460+
461+
def select(&block)
462+
new_instance_with_inherited_permitted_status(@parameters.select(&block))
421463
end
422464

423465
# Equivalent to Hash#keep_if, but returns nil if no changes were made.
424466
def select!(&block)
425-
convert_value_to_parameters(super)
467+
@parameters.select!(&block)
468+
self
469+
end
470+
alias_method :keep_if, :select!
471+
472+
def reject(&block)
473+
new_instance_with_inherited_permitted_status(@parameters.reject(&block))
474+
end
475+
476+
def reject!(&block)
477+
@parameters.reject!(&block)
478+
self
479+
end
480+
alias_method :delete_if, :reject!
481+
482+
def values_at(*keys)
483+
convert_value_to_parameters(@parameters.values_at(*keys))
426484
end
427485

428486
# Returns an exact copy of the <tt>ActionController::Parameters</tt>
@@ -439,6 +497,18 @@ def dup
439497
end
440498
end
441499

500+
def merge(other_hash)
501+
new_instance_with_inherited_permitted_status(
502+
@parameters.merge(other_hash)
503+
)
504+
end
505+
506+
# This is required by ActiveModel attribute assignment, so that user can
507+
# pass +Parameters+ to a mass assignment methods in a model.
508+
def stringify_keys
509+
dup
510+
end
511+
442512
protected
443513
def permitted=(new_permitted)
444514
@permitted = new_permitted
@@ -453,7 +523,7 @@ def new_instance_with_inherited_permitted_status(hash)
453523

454524
def convert_hashes_to_parameters(key, value, assign_if_converted=true)
455525
converted = convert_value_to_parameters(value)
456-
self[key] = converted if assign_if_converted && !converted.equal?(value)
526+
@parameters[key] = converted if assign_if_converted && !converted.equal?(value)
457527
converted
458528
end
459529

@@ -482,7 +552,8 @@ def each_element(object)
482552
end
483553

484554
def fields_for_style?(object)
485-
object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
555+
(object.is_a?(Hash) || object.is_a?(Parameters)) &&
556+
object.to_unsafe_h.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
486557
end
487558

488559
def unpermitted_parameters!(params)
@@ -571,7 +642,7 @@ def hash_filter(params, filter)
571642
else
572643
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
573644
params[key] = each_element(value) do |element|
574-
if element.is_a?(Hash)
645+
if element.is_a?(Hash) || element.is_a?(Parameters)
575646
element = self.class.new(element) unless element.respond_to?(:permit)
576647
element.permit(*Array.wrap(filter[key]))
577648
end

actionpack/lib/action_dispatch/routing/url_for.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ def url_for(options = nil)
171171
route_name = options.delete :use_route
172172
_routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
173173
route_name)
174+
when ActionController::Parameters
175+
route_name = options.delete :use_route
176+
_routes.url_for(options.to_unsafe_h.symbolize_keys.
177+
reverse_merge!(url_options), route_name)
174178
when String
175179
options
176180
when Symbol

actionpack/test/controller/api/params_wrapper_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class UsersController < ActionController::API
77
wrap_parameters :person, format: [:json]
88

99
def test
10-
self.last_parameters = params.except(:controller, :action)
10+
self.last_parameters = params.except(:controller, :action).to_unsafe_h
1111
head :ok
1212
end
1313
end

actionpack/test/controller/parameters/nested_parameters_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def assert_filtered_out(params, key)
136136
authors_attributes: {
137137
:'0' => { name: 'William Shakespeare', age_of_death: '52' },
138138
:'1' => { name: 'Unattributed Assistant' },
139-
:'2' => { name: %w(injected names)}
139+
:'2' => { name: %w(injected names) }
140140
}
141141
}
142142
})

actionpack/test/controller/parameters/parameters_permit_test.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ def assert_filtered_out(params, key)
253253

254254
assert @params.to_h.is_a? Hash
255255
assert_not @params.to_h.is_a? ActionController::Parameters
256-
assert_equal @params.to_hash, @params.to_h
257256
end
258257

259258
test "to_h returns converted hash when .permit_all_parameters is set" do
@@ -284,6 +283,5 @@ def assert_filtered_out(params, key)
284283
test "to_unsafe_h returns unfiltered params" do
285284
assert @params.to_h.is_a? Hash
286285
assert_not @params.to_h.is_a? ActionController::Parameters
287-
assert_equal @params.to_hash, @params.to_unsafe_h
288286
end
289287
end

actionpack/test/controller/test_case_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def render_body
4545
end
4646

4747
def test_params
48-
render text: ::JSON.dump(params)
48+
render text: ::JSON.dump(params.to_unsafe_h)
4949
end
5050

5151
def test_query_parameters

actionpack/test/controller/webservice_test.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ def assign_parameters
1414
def dump_params_keys(hash = params)
1515
hash.keys.sort.inject("") do |s, k|
1616
value = hash[k]
17-
value = Hash === value ? "(#{dump_params_keys(value)})" : ""
17+
18+
if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
19+
value = "(#{dump_params_keys(value)})"
20+
else
21+
value = ""
22+
end
23+
1824
s << ", " unless s.empty?
1925
s << "#{k}#{value}"
2026
end

actionpack/test/dispatch/request/json_params_parsing_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class << self
113113

114114
def parse
115115
self.class.last_request_parameters = request.request_parameters
116-
self.class.last_parameters = params
116+
self.class.last_parameters = params.to_unsafe_h
117117
head :ok
118118
end
119119
end

actionview/lib/action_view/routing_url_for.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ def url_for(options = nil)
9191
end
9292
end
9393

94+
super(options)
95+
when ActionController::Parameters
96+
unless options.key?(:only_path)
97+
if options[:host].nil?
98+
options[:only_path] = _generate_paths_by_default
99+
else
100+
options[:only_path] = false
101+
end
102+
end
103+
94104
super(options)
95105
when :back
96106
_back_url

0 commit comments

Comments
 (0)