actionpack 7.2.1.1 → 8.0.0.rc1

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.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -100
  3. data/lib/abstract_controller/helpers.rb +2 -0
  4. data/lib/abstract_controller/rendering.rb +0 -1
  5. data/lib/action_controller/api.rb +1 -0
  6. data/lib/action_controller/form_builder.rb +3 -3
  7. data/lib/action_controller/metal/allow_browser.rb +12 -2
  8. data/lib/action_controller/metal/conditional_get.rb +6 -3
  9. data/lib/action_controller/metal/http_authentication.rb +6 -3
  10. data/lib/action_controller/metal/instrumentation.rb +1 -2
  11. data/lib/action_controller/metal/live.rb +19 -8
  12. data/lib/action_controller/metal/rate_limiting.rb +13 -4
  13. data/lib/action_controller/metal/renderers.rb +2 -3
  14. data/lib/action_controller/metal/streaming.rb +5 -84
  15. data/lib/action_controller/metal/strong_parameters.rb +274 -88
  16. data/lib/action_controller/railtie.rb +1 -7
  17. data/lib/action_controller/test_case.rb +6 -5
  18. data/lib/action_dispatch/http/cache.rb +27 -10
  19. data/lib/action_dispatch/http/content_security_policy.rb +5 -8
  20. data/lib/action_dispatch/http/filter_parameters.rb +4 -9
  21. data/lib/action_dispatch/http/filter_redirect.rb +2 -9
  22. data/lib/action_dispatch/http/param_builder.rb +163 -0
  23. data/lib/action_dispatch/http/param_error.rb +26 -0
  24. data/lib/action_dispatch/http/permissions_policy.rb +2 -0
  25. data/lib/action_dispatch/http/query_parser.rb +31 -0
  26. data/lib/action_dispatch/http/request.rb +60 -16
  27. data/lib/action_dispatch/journey/parser.rb +99 -196
  28. data/lib/action_dispatch/journey/scanner.rb +40 -42
  29. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  30. data/lib/action_dispatch/middleware/debug_exceptions.rb +16 -3
  31. data/lib/action_dispatch/middleware/debug_view.rb +0 -5
  32. data/lib/action_dispatch/middleware/exception_wrapper.rb +0 -6
  33. data/lib/action_dispatch/middleware/remote_ip.rb +5 -6
  34. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  35. data/lib/action_dispatch/middleware/ssl.rb +14 -4
  36. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +0 -3
  37. data/lib/action_dispatch/railtie.rb +2 -0
  38. data/lib/action_dispatch/request/utils.rb +9 -3
  39. data/lib/action_dispatch/routing/inspector.rb +1 -1
  40. data/lib/action_dispatch/routing/mapper.rb +91 -62
  41. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  42. data/lib/action_dispatch/routing/route_set.rb +20 -8
  43. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  44. data/lib/action_dispatch/testing/assertions/response.rb +12 -2
  45. data/lib/action_dispatch/testing/assertions/routing.rb +4 -4
  46. data/lib/action_dispatch/testing/integration.rb +11 -1
  47. data/lib/action_dispatch.rb +6 -0
  48. data/lib/action_pack/gem_version.rb +4 -4
  49. metadata +15 -34
  50. data/lib/action_dispatch/journey/parser.y +0 -50
  51. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -165,95 +165,16 @@ module ActionController # :nodoc:
165
165
  #
166
166
  # ## Web server support
167
167
  #
168
- # Not all web servers support streaming out-of-the-box. You need to check the
169
- # instructions for each of them.
170
- #
171
- # #### Unicorn
172
- #
173
- # Unicorn supports streaming but it needs to be configured. For this, you need
174
- # to create a config file as follow:
175
- #
176
- # # unicorn.config.rb
177
- # listen 3000, tcp_nopush: false
178
- #
179
- # And use it on initialization:
180
- #
181
- # unicorn_rails --config-file unicorn.config.rb
182
- #
183
- # You may also want to configure other parameters like `:tcp_nodelay`.
184
- #
185
- # For more information, please check the
186
- # [documentation](https://bogomips.org/unicorn/Unicorn/Configurator.html#method-
187
- # i-listen).
188
- #
189
- # If you are using Unicorn with NGINX, you may need to tweak NGINX. Streaming
190
- # should work out of the box on Rainbows.
191
- #
192
- # #### Passenger
193
- #
194
- # Phusion Passenger with NGINX, offers two streaming mechanisms out of the box.
195
- #
196
- # 1. NGINX response buffering mechanism which is dependent on the value of
197
- # `passenger_buffer_response` option (default is "off").
198
- # 2. Passenger buffering system which is always 'on' irrespective of the value
199
- # of `passenger_buffer_response`.
200
- #
201
- #
202
- # When `passenger_buffer_response` is turned "on", then streaming would be done
203
- # at the NGINX level which waits until the application is done sending the
204
- # response back to the client.
205
- #
206
- # For more information, please check the [documentation]
207
- # (https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response).
168
+ # Rack 3+ compatible servers all support streaming.
208
169
  module Streaming
209
- class Body # :nodoc:
210
- TERM = "\r\n"
211
- TAIL = "0#{TERM}"
212
-
213
- # Store the response body to be chunked.
214
- def initialize(body)
215
- @body = body
216
- end
217
-
218
- # For each element yielded by the response body, yield the element in chunked
219
- # encoding.
220
- def each(&block)
221
- term = TERM
222
- @body.each do |chunk|
223
- size = chunk.bytesize
224
- next if size == 0
225
-
226
- yield [size.to_s(16), term, chunk.b, term].join
227
- end
228
- yield TAIL
229
- yield term
230
- end
231
-
232
- # Close the response body if the response body supports it.
233
- def close
234
- @body.close if @body.respond_to?(:close)
235
- end
236
- end
237
-
238
170
  private
239
- # Set proper cache control and transfer encoding when streaming
240
- def _process_options(options)
241
- super
242
- if options[:stream]
243
- if request.version == "HTTP/1.0"
244
- options.delete(:stream)
245
- else
246
- headers["Cache-Control"] ||= "no-cache"
247
- headers["Transfer-Encoding"] = "chunked"
248
- headers.delete("Content-Length")
249
- end
250
- end
251
- end
252
-
253
171
  # Call render_body if we are streaming instead of usual `render`.
254
172
  def _render_template(options)
255
173
  if options.delete(:stream)
256
- Body.new view_renderer.render_body(view_context, options)
174
+ # It shoudn't be necessary to set this.
175
+ headers["cache-control"] ||= "no-cache"
176
+
177
+ view_renderer.render_body(view_context, options)
257
178
  else
258
179
  super
259
180
  end
@@ -10,7 +10,6 @@ require "active_support/deep_mergeable"
10
10
  require "action_dispatch/http/upload"
11
11
  require "rack/test"
12
12
  require "stringio"
13
- require "set"
14
13
  require "yaml"
15
14
 
16
15
  module ActionController
@@ -18,16 +17,18 @@ module ActionController
18
17
  #
19
18
  # params = ActionController::Parameters.new(a: {})
20
19
  # params.fetch(:b)
21
- # # => ActionController::ParameterMissing: param is missing or the value is empty: b
20
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: b
22
21
  # params.require(:a)
23
- # # => ActionController::ParameterMissing: param is missing or the value is empty: a
22
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: a
23
+ # params.expect(a: [])
24
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: a
24
25
  class ParameterMissing < KeyError
25
26
  attr_reader :param, :keys # :nodoc:
26
27
 
27
28
  def initialize(param, keys = nil) # :nodoc:
28
29
  @param = param
29
30
  @keys = keys
30
- super("param is missing or the value is empty: #{param}")
31
+ super("param is missing or the value is empty or invalid: #{param}")
31
32
  end
32
33
 
33
34
  if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
@@ -39,6 +40,15 @@ module ActionController
39
40
  end
40
41
  end
41
42
 
43
+ # Raised from `expect!` when an expected parameter is missing or is of an
44
+ # incompatible type.
45
+ #
46
+ # params = ActionController::Parameters.new(a: {})
47
+ # params.expect!(:a)
48
+ # # => ActionController::ExpectedParameterMissing: param is missing or the value is empty or invalid: a
49
+ class ExpectedParameterMissing < ParameterMissing
50
+ end
51
+
42
52
  # Raised when a supplied parameter is not expected and
43
53
  # ActionController::Parameters.action_on_unpermitted_parameters is set to
44
54
  # `:raise`.
@@ -78,9 +88,12 @@ module ActionController
78
88
  #
79
89
  # Allows you to choose which attributes should be permitted for mass updating
80
90
  # and thus prevent accidentally exposing that which shouldn't be exposed.
81
- # Provides two methods for this purpose: #require and #permit. The former is
82
- # used to mark parameters as required. The latter is used to set the parameter
83
- # as permitted and limit which attributes should be allowed for mass updating.
91
+ #
92
+ # Provides methods for filtering and requiring params:
93
+ #
94
+ # * `expect` to safely permit and require parameters in one step.
95
+ # * `permit` to filter params for mass assignment.
96
+ # * `require` to require a parameter or raise an error.
84
97
  #
85
98
  # params = ActionController::Parameters.new({
86
99
  # person: {
@@ -90,14 +103,14 @@ module ActionController
90
103
  # }
91
104
  # })
92
105
  #
93
- # permitted = params.require(:person).permit(:name, :age)
94
- # permitted # => #<ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
95
- # permitted.permitted? # => true
106
+ # permitted = params.expect(person: [:name, :age])
107
+ # permitted # => #<ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
96
108
  #
97
109
  # Person.first.update!(permitted)
98
110
  # # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
99
111
  #
100
- # It provides two options that controls the top-level behavior of new instances:
112
+ # Paramaters provides two options that control the top-level behavior of new
113
+ # instances:
101
114
  #
102
115
  # * `permit_all_parameters` - If it's `true`, all the parameters will be
103
116
  # permitted by default. The default is `false`.
@@ -112,8 +125,6 @@ module ActionController
112
125
  # * `:raise` to raise an ActionController::UnpermittedParameters
113
126
  # exception.
114
127
  #
115
- #
116
- #
117
128
  # Examples:
118
129
  #
119
130
  # params = ActionController::Parameters.new
@@ -250,20 +261,6 @@ module ActionController
250
261
  cattr_accessor :always_permitted_parameters, default: %w( controller action )
251
262
 
252
263
  class << self
253
- def allow_deprecated_parameters_hash_equality
254
- ActionController.deprecator.warn <<-WARNING.squish
255
- `Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality` is
256
- deprecated and will be removed in Rails 8.0.
257
- WARNING
258
- end
259
-
260
- def allow_deprecated_parameters_hash_equality=(value)
261
- ActionController.deprecator.warn <<-WARNING.squish
262
- `Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality`
263
- is deprecated and will be removed in Rails 8.0.
264
- WARNING
265
- end
266
-
267
264
  def nested_attribute?(key, value) # :nodoc:
268
265
  /\A-?\d+\z/.match?(key) && (value.is_a?(Hash) || value.is_a?(Parameters))
269
266
  end
@@ -481,16 +478,16 @@ module ActionController
481
478
  # Otherwise raises ActionController::ParameterMissing:
482
479
  #
483
480
  # ActionController::Parameters.new.require(:person)
484
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
481
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
485
482
  #
486
483
  # ActionController::Parameters.new(person: nil).require(:person)
487
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
484
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
488
485
  #
489
486
  # ActionController::Parameters.new(person: "\t").require(:person)
490
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
487
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
491
488
  #
492
489
  # ActionController::Parameters.new(person: {}).require(:person)
493
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
490
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
494
491
  #
495
492
  # When given an array of keys, the method tries to require each one of them in
496
493
  # order. If it succeeds, an array with the respective return values is returned:
@@ -502,23 +499,21 @@ module ActionController
502
499
  #
503
500
  # params = ActionController::Parameters.new(user: {}, profile: {})
504
501
  # user_params, profile_params = params.require([:user, :profile])
505
- # # ActionController::ParameterMissing: param is missing or the value is empty: user
502
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: user
506
503
  #
507
- # Technically this method can be used to fetch terminal values:
504
+ # This method is not recommended for fetching terminal values because it does
505
+ # not permit the values. For example, this can cause problems:
508
506
  #
509
507
  # # CAREFUL
510
508
  # params = ActionController::Parameters.new(person: { name: "Finn" })
511
509
  # name = params.require(:person).require(:name) # CAREFUL
512
510
  #
513
- # but take into account that at some point those ones have to be permitted:
511
+ # It is recommended to use `expect` instead:
514
512
  #
515
513
  # def person_params
516
- # params.require(:person).permit(:name).tap do |person_params|
517
- # person_params.require(:name) # SAFER
518
- # end
514
+ # # params.expect(person: :name).require(:name)
519
515
  # end
520
516
  #
521
- # for example.
522
517
  def require(key)
523
518
  return key.map { |k| require(k) } if key.is_a?(Array)
524
519
  value = self[key]
@@ -536,8 +531,8 @@ module ActionController
536
531
  # This is useful for limiting which attributes should be allowed for mass
537
532
  # updating.
538
533
  #
539
- # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" })
540
- # permitted = params.require(:user).permit(:name, :age)
534
+ # params = ActionController::Parameters.new(name: "Francesco", age: 22, role: "admin")
535
+ # permitted = params.permit(:name, :age)
541
536
  # permitted.permitted? # => true
542
537
  # permitted.has_key?(:name) # => true
543
538
  # permitted.has_key?(:age) # => true
@@ -567,7 +562,7 @@ module ActionController
567
562
  # `permit` ensures values in the returned structure are permitted scalars and
568
563
  # filters out anything else.
569
564
  #
570
- # You can also use `permit` on nested parameters, like:
565
+ # You can also use `permit` on nested parameters:
571
566
  #
572
567
  # params = ActionController::Parameters.new({
573
568
  # person: {
@@ -587,6 +582,29 @@ module ActionController
587
582
  # permitted[:person][:pets][0][:name] # => "Purplish"
588
583
  # permitted[:person][:pets][0][:category] # => nil
589
584
  #
585
+ # This has the added benefit of rejecting user-modified inputs that send a
586
+ # string when a hash is expected.
587
+ #
588
+ # When followed by `require`, you can both filter and require parameters
589
+ # following the typical pattern of a Rails form. The `expect` method was
590
+ # made specifically for this use case and is the recommended way to require
591
+ # and permit parameters.
592
+ #
593
+ # permitted = params.expect(person: [:name, :age])
594
+ #
595
+ # When using `permit` and `require` separately, pay careful attention to the
596
+ # order of the method calls.
597
+ #
598
+ # params = ActionController::Parameters.new(person: { name: "Martin", age: 40, role: "admin" })
599
+ # permitted = params.permit(person: [:name, :age]).require(:person) # correct
600
+ #
601
+ # When require is used first, it is possible for users of your application to
602
+ # trigger a NoMethodError when the user, for example, sends a string for :person.
603
+ #
604
+ # params = ActionController::Parameters.new(person: "tampered")
605
+ # permitted = params.require(:person).permit(:name, :age) # not recommended
606
+ # # => NoMethodError: undefined method `permit' for an instance of String
607
+ #
590
608
  # Note that if you use `permit` in a key that points to a hash, it won't allow
591
609
  # all the hash. You also need to specify which attributes inside the hash should
592
610
  # be permitted.
@@ -600,13 +618,13 @@ module ActionController
600
618
  # }
601
619
  # })
602
620
  #
603
- # params.require(:person).permit(:contact)
621
+ # params.permit(person: :contact).require(:person)
604
622
  # # => #<ActionController::Parameters {} permitted: true>
605
623
  #
606
- # params.require(:person).permit(contact: :phone)
624
+ # params.permit(person: { contact: :phone }).require(:person)
607
625
  # # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
608
626
  #
609
- # params.require(:person).permit(contact: [ :email, :phone ])
627
+ # params.permit(person: { contact: [ :email, :phone ] }).require(:person)
610
628
  # # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
611
629
  #
612
630
  # If your parameters specify multiple parameters indexed by a number, you can
@@ -646,20 +664,127 @@ module ActionController
646
664
  # params.permit(person: { '0': [:email], '1': [:phone]}).to_h
647
665
  # # => {"person"=>{"0"=>{"email"=>"none@test.com"}, "1"=>{"phone"=>"555-6789"}}}
648
666
  def permit(*filters)
649
- params = self.class.new
650
-
651
- filters.flatten.each do |filter|
652
- case filter
653
- when Symbol, String
654
- permitted_scalar_filter(params, filter)
655
- when Hash
656
- hash_filter(params, filter)
657
- end
658
- end
667
+ permit_filters(filters, on_unpermitted: self.class.action_on_unpermitted_parameters, explicit_arrays: false)
668
+ end
659
669
 
660
- unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
670
+ # `expect` is the preferred way to require and permit parameters.
671
+ # It is safer than the previous recommendation to call `permit` and `require`
672
+ # in sequence, which could allow user triggered 500 errors.
673
+ #
674
+ # `expect` is more strict with types to avoid a number of potential pitfalls
675
+ # that may be encountered with the `.require.permit` pattern.
676
+ #
677
+ # For example:
678
+ #
679
+ # params = ActionController::Parameters.new(comment: { text: "hello" })
680
+ # params.expect(comment: [:text])
681
+ # # => #<ActionController::Parameters { text: "hello" } permitted: true>
682
+ #
683
+ # params = ActionController::Parameters.new(comment: [{ text: "hello" }, { text: "world" }])
684
+ # params.expect(comment: [:text])
685
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: comment
686
+ #
687
+ # In order to permit an array of parameters, the array must be defined
688
+ # explicitly. Use double array brackets, an array inside an array, to
689
+ # declare that an array of parameters is expected.
690
+ #
691
+ # params = ActionController::Parameters.new(comments: [{ text: "hello" }, { text: "world" }])
692
+ # params.expect(comments: [[:text]])
693
+ # # => [#<ActionController::Parameters { "text" => "hello" } permitted: true>,
694
+ # # #<ActionController::Parameters { "text" => "world" } permitted: true>]
695
+ #
696
+ # params = ActionController::Parameters.new(comments: { text: "hello" })
697
+ # params.expect(comments: [[:text]])
698
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: comments
699
+ #
700
+ # `expect` is intended to protect against array tampering.
701
+ #
702
+ # params = ActionController::Parameters.new(user: "hack")
703
+ # # The previous way of requiring and permitting parameters will error
704
+ # params.require(:user).permit(:name, pets: [:name]) # wrong
705
+ # # => NoMethodError: undefined method `permit' for an instance of String
706
+ #
707
+ # # similarly with nested parameters
708
+ # params = ActionController::Parameters.new(user: { name: "Martin", pets: { name: "hack" } })
709
+ # user_params = params.require(:user).permit(:name, pets: [:name]) # wrong
710
+ # # user_params[:pets] is expected to be an array but is a hash
711
+ #
712
+ # `expect` solves this by being more strict with types.
713
+ #
714
+ # params = ActionController::Parameters.new(user: "hack")
715
+ # params.expect(user: [ :name, pets: [[:name]] ])
716
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: user
717
+ #
718
+ # # with nested parameters
719
+ # params = ActionController::Parameters.new(user: { name: "Martin", pets: { name: "hack" } })
720
+ # user_params = params.expect(user: [:name, pets: [[:name]] ])
721
+ # user_params[:pets] # => nil
722
+ #
723
+ # As the examples show, `expect` requires the `:user` key, and any root keys
724
+ # similar to the `.require.permit` pattern. If multiple root keys are
725
+ # expected, they will all be required.
726
+ #
727
+ # params = ActionController::Parameters.new(name: "Martin", pies: [{ type: "dessert", flavor: "pumpkin"}])
728
+ # name, pies = params.expect(:name, pies: [[:type, :flavor]])
729
+ # name # => "Martin"
730
+ # pies # => [#<ActionController::Parameters {"type"=>"dessert", "flavor"=>"pumpkin"} permitted: true>]
731
+ #
732
+ # When called with a hash with multiple keys, `expect` will permit the
733
+ # parameters and require the keys in the order they are given in the hash,
734
+ # returning an array of the permitted parameters.
735
+ #
736
+ # params = ActionController::Parameters.new(subject: { name: "Martin" }, object: { pie: "pumpkin" })
737
+ # subject, object = params.expect(subject: [:name], object: [:pie])
738
+ # subject # => #<ActionController::Parameters {"name"=>"Martin"} permitted: true>
739
+ # object # => #<ActionController::Parameters {"pie"=>"pumpkin"} permitted: true>
740
+ #
741
+ # Besides being more strict about array vs hash params, `expect` uses permit
742
+ # internally, so it will behave similarly.
743
+ #
744
+ # params = ActionController::Parameters.new({
745
+ # person: {
746
+ # name: "Francesco",
747
+ # age: 22,
748
+ # pets: [{
749
+ # name: "Purplish",
750
+ # category: "dogs"
751
+ # }]
752
+ # }
753
+ # })
754
+ #
755
+ # permitted = params.expect(person: [ :name, { pets: [[:name]] } ])
756
+ # permitted.permitted? # => true
757
+ # permitted[:name] # => "Francesco"
758
+ # permitted[:age] # => nil
759
+ # permitted[:pets][0][:name] # => "Purplish"
760
+ # permitted[:pets][0][:category] # => nil
761
+ #
762
+ # An array of permitted scalars may be expected with the following:
763
+ #
764
+ # params = ActionController::Parameters.new(tags: ["rails", "parameters"])
765
+ # permitted = params.expect(tags: [])
766
+ # permitted.permitted? # => true
767
+ # permitted.is_a?(Array) # => true
768
+ # permitted.size # => 2
769
+ #
770
+ def expect(*filters)
771
+ params = permit_filters(filters)
772
+ keys = filters.flatten.flat_map { |f| f.is_a?(Hash) ? f.keys : f }
773
+ values = params.require(keys)
774
+ values.size == 1 ? values.first : values
775
+ end
661
776
 
662
- params.permit!
777
+ # Same as `expect`, but raises an `ActionController::ExpectedParameterMissing`
778
+ # instead of `ActionController::ParameterMissing`. Unlike `expect` which
779
+ # will render a 400 response, `expect!` will raise an exception that is
780
+ # not handled. This is intended for debugging invalid params for an
781
+ # internal API where incorrectly formatted params would indicate a bug
782
+ # in a client library that should be fixed.
783
+ #
784
+ def expect!(*filters)
785
+ expect(*filters)
786
+ rescue ParameterMissing => e
787
+ raise ExpectedParameterMissing.new(e.param, e.keys)
663
788
  end
664
789
 
665
790
  # Returns a parameter for the given `key`. If not found, returns `nil`.
@@ -686,7 +811,7 @@ module ActionController
686
811
  #
687
812
  # params = ActionController::Parameters.new(person: { name: "Francesco" })
688
813
  # params.fetch(:person) # => #<ActionController::Parameters {"name"=>"Francesco"} permitted: false>
689
- # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
814
+ # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: none
690
815
  # params.fetch(:none, {}) # => #<ActionController::Parameters {} permitted: false>
691
816
  # params.fetch(:none, "Francesco") # => "Francesco"
692
817
  # params.fetch(:none) { "Francesco" } # => "Francesco"
@@ -999,6 +1124,26 @@ module ActionController
999
1124
  hash
1000
1125
  end
1001
1126
 
1127
+ # Filters self and optionally checks for unpermitted keys
1128
+ def permit_filters(filters, on_unpermitted: nil, explicit_arrays: true)
1129
+ params = self.class.new
1130
+
1131
+ filters.flatten.each do |filter|
1132
+ case filter
1133
+ when Symbol, String
1134
+ # Declaration [:name, "age"]
1135
+ permitted_scalar_filter(params, filter)
1136
+ when Hash
1137
+ # Declaration [{ person: ... }]
1138
+ hash_filter(params, filter, on_unpermitted:, explicit_arrays:)
1139
+ end
1140
+ end
1141
+
1142
+ unpermitted_parameters!(params, on_unpermitted:)
1143
+
1144
+ params.permit!
1145
+ end
1146
+
1002
1147
  private
1003
1148
  def new_instance_with_inherited_permitted_status(hash)
1004
1149
  self.class.new(hash, @logging_context).tap do |new_instance|
@@ -1088,23 +1233,45 @@ module ActionController
1088
1233
  end
1089
1234
  end
1090
1235
 
1091
- def each_element(object, filter, &block)
1236
+ # When an array is expected, you must specify an array explicitly
1237
+ # using the following format:
1238
+ #
1239
+ # params.expect(comments: [[:flavor]])
1240
+ #
1241
+ # Which will match only the following array formats:
1242
+ #
1243
+ # { pies: [{ flavor: "rhubarb" }, { flavor: "apple" }] }
1244
+ # { pies: { "0" => { flavor: "key lime" }, "1" => { flavor: "mince" } } }
1245
+ #
1246
+ # When using `permit`, arrays are specified the same way as hashes:
1247
+ #
1248
+ # params.expect(pies: [:flavor])
1249
+ #
1250
+ # In this case, `permit` would also allow matching with a hash (or vice versa):
1251
+ #
1252
+ # { pies: { flavor: "cherry" } }
1253
+ #
1254
+ def array_filter?(filter)
1255
+ filter.is_a?(Array) && filter.size == 1 && filter.first.is_a?(Array)
1256
+ end
1257
+
1258
+ # Called when an explicit array filter is encountered.
1259
+ def each_array_element(object, filter, &block)
1092
1260
  case object
1093
1261
  when Array
1094
1262
  object.grep(Parameters).filter_map(&block)
1095
1263
  when Parameters
1096
1264
  if object.nested_attributes? && !specify_numeric_keys?(filter)
1097
1265
  object.each_nested_attribute(&block)
1098
- else
1099
- yield object
1100
1266
  end
1101
1267
  end
1102
1268
  end
1103
1269
 
1104
- def unpermitted_parameters!(params)
1270
+ def unpermitted_parameters!(params, on_unpermitted: self.class.action_on_unpermitted_parameters)
1271
+ return unless on_unpermitted
1105
1272
  unpermitted_keys = unpermitted_keys(params)
1106
1273
  if unpermitted_keys.any?
1107
- case self.class.action_on_unpermitted_parameters
1274
+ case on_unpermitted
1108
1275
  when :log
1109
1276
  name = "unpermitted_parameters.action_controller"
1110
1277
  ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys, context: @logging_context)
@@ -1174,45 +1341,63 @@ module ActionController
1174
1341
  end
1175
1342
  end
1176
1343
 
1177
- def array_of_permitted_scalars?(value)
1178
- if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
1179
- yield value
1180
- end
1181
- end
1182
-
1183
1344
  def non_scalar?(value)
1184
1345
  value.is_a?(Array) || value.is_a?(Parameters)
1185
1346
  end
1186
1347
 
1187
1348
  EMPTY_ARRAY = [] # :nodoc:
1188
1349
  EMPTY_HASH = {} # :nodoc:
1189
- def hash_filter(params, filter)
1350
+ def hash_filter(params, filter, on_unpermitted: self.class.action_on_unpermitted_parameters, explicit_arrays: false)
1190
1351
  filter = filter.with_indifferent_access
1191
1352
 
1192
1353
  # Slicing filters out non-declared keys.
1193
1354
  slice(*filter.keys).each do |key, value|
1194
1355
  next unless value
1195
1356
  next unless has_key? key
1357
+ result = permit_value(value, filter[key], on_unpermitted:, explicit_arrays:)
1358
+ params[key] = result unless result.nil?
1359
+ end
1360
+ end
1196
1361
 
1197
- if filter[key] == EMPTY_ARRAY
1198
- # Declaration { comment_ids: [] }.
1199
- array_of_permitted_scalars?(self[key]) do |val|
1200
- params[key] = val
1201
- end
1202
- elsif filter[key] == EMPTY_HASH
1203
- # Declaration { preferences: {} }.
1204
- if value.is_a?(Parameters)
1205
- params[key] = permit_any_in_parameters(value)
1206
- end
1207
- elsif non_scalar?(value)
1208
- # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
1209
- params[key] = each_element(value, filter[key]) do |element|
1210
- element.permit(*Array.wrap(filter[key]))
1211
- end
1212
- end
1362
+ def permit_value(value, filter, on_unpermitted:, explicit_arrays:)
1363
+ if filter == EMPTY_ARRAY # Declaration { comment_ids: [] }.
1364
+ permit_array_of_scalars(value)
1365
+ elsif filter == EMPTY_HASH # Declaration { preferences: {} }.
1366
+ permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1367
+ elsif array_filter?(filter) # Declaration { comments: [[:text]] }
1368
+ permit_array_of_hashes(value, filter.first, on_unpermitted:, explicit_arrays:)
1369
+ elsif explicit_arrays # Declaration { user: { address: ... } } or { user: [:name, ...] } (only allows hash value)
1370
+ permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1371
+ elsif non_scalar?(value) # Declaration { user: { address: ... } } or { user: [:name, ...] }
1372
+ permit_hash_or_array(value, filter, on_unpermitted:, explicit_arrays:)
1213
1373
  end
1214
1374
  end
1215
1375
 
1376
+ def permit_array_of_scalars(value)
1377
+ value if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
1378
+ end
1379
+
1380
+ def permit_array_of_hashes(value, filter, on_unpermitted:, explicit_arrays:)
1381
+ each_array_element(value, filter) do |element|
1382
+ element.permit_filters(Array.wrap(filter), on_unpermitted:, explicit_arrays:)
1383
+ end
1384
+ end
1385
+
1386
+ def permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1387
+ return unless value.is_a?(Parameters)
1388
+
1389
+ if filter == EMPTY_HASH
1390
+ permit_any_in_parameters(value)
1391
+ else
1392
+ value.permit_filters(Array.wrap(filter), on_unpermitted:, explicit_arrays:)
1393
+ end
1394
+ end
1395
+
1396
+ def permit_hash_or_array(value, filter, on_unpermitted:, explicit_arrays:)
1397
+ permit_array_of_hashes(value, filter, on_unpermitted:, explicit_arrays:) ||
1398
+ permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1399
+ end
1400
+
1216
1401
  def permit_any_in_parameters(params)
1217
1402
  self.class.new.tap do |sanitized|
1218
1403
  params.each do |key, value|
@@ -1287,7 +1472,7 @@ module ActionController
1287
1472
  # # list between create and update. Also, you can specialize this method
1288
1473
  # # with per-user checking of permissible attributes.
1289
1474
  # def person_params
1290
- # params.require(:person).permit(:name, :age)
1475
+ # params.expect(person: [:name, :age])
1291
1476
  # end
1292
1477
  # end
1293
1478
  #
@@ -1314,11 +1499,12 @@ module ActionController
1314
1499
  # # It's mandatory to specify the nested attributes that should be permitted.
1315
1500
  # # If you use `permit` with just the key that points to the nested attributes hash,
1316
1501
  # # it will return an empty hash.
1317
- # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
1502
+ # params.expect(person: [ :name, :age, pets_attributes: [ :id, :name, :category ] ])
1318
1503
  # end
1319
1504
  # end
1320
1505
  #
1321
- # See ActionController::Parameters.require and
1506
+ # See ActionController::Parameters.expect,
1507
+ # See ActionController::Parameters.require, and
1322
1508
  # ActionController::Parameters.permit for more information.
1323
1509
  module StrongParameters
1324
1510
  # Returns a new ActionController::Parameters object that has been instantiated