actionpack 7.2.2.1 → 8.0.1

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +123 -109
  3. data/lib/abstract_controller/rendering.rb +0 -1
  4. data/lib/action_controller/base.rb +1 -1
  5. data/lib/action_controller/form_builder.rb +3 -3
  6. data/lib/action_controller/metal/allow_browser.rb +11 -1
  7. data/lib/action_controller/metal/conditional_get.rb +5 -1
  8. data/lib/action_controller/metal/data_streaming.rb +4 -2
  9. data/lib/action_controller/metal/instrumentation.rb +1 -2
  10. data/lib/action_controller/metal/live.rb +13 -4
  11. data/lib/action_controller/metal/rate_limiting.rb +13 -4
  12. data/lib/action_controller/metal/redirecting.rb +2 -1
  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 +277 -89
  16. data/lib/action_controller/railtie.rb +1 -7
  17. data/lib/action_controller/test_case.rb +4 -2
  18. data/lib/action_dispatch/http/cache.rb +27 -10
  19. data/lib/action_dispatch/http/content_security_policy.rb +1 -0
  20. data/lib/action_dispatch/http/param_builder.rb +186 -0
  21. data/lib/action_dispatch/http/param_error.rb +26 -0
  22. data/lib/action_dispatch/http/permissions_policy.rb +2 -0
  23. data/lib/action_dispatch/http/query_parser.rb +53 -0
  24. data/lib/action_dispatch/http/request.rb +60 -16
  25. data/lib/action_dispatch/journey/parser.rb +99 -196
  26. data/lib/action_dispatch/journey/scanner.rb +44 -42
  27. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  28. data/lib/action_dispatch/middleware/debug_exceptions.rb +16 -3
  29. data/lib/action_dispatch/middleware/debug_view.rb +0 -5
  30. data/lib/action_dispatch/middleware/exception_wrapper.rb +0 -6
  31. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  32. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  33. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +0 -3
  34. data/lib/action_dispatch/railtie.rb +8 -0
  35. data/lib/action_dispatch/request/session.rb +1 -0
  36. data/lib/action_dispatch/request/utils.rb +9 -3
  37. data/lib/action_dispatch/routing/inspector.rb +1 -1
  38. data/lib/action_dispatch/routing/mapper.rb +90 -62
  39. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  40. data/lib/action_dispatch/routing/route_set.rb +20 -8
  41. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  42. data/lib/action_dispatch/testing/assertions/response.rb +12 -2
  43. data/lib/action_dispatch/testing/assertions/routing.rb +4 -4
  44. data/lib/action_dispatch/testing/integration.rb +11 -1
  45. data/lib/action_dispatch/testing/test_process.rb +1 -2
  46. data/lib/action_dispatch.rb +6 -4
  47. data/lib/action_pack/gem_version.rb +4 -4
  48. metadata +15 -34
  49. data/lib/action_dispatch/journey/parser.y +0 -50
  50. 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 shouldn'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,14 @@ 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.
97
+ #
98
+ # Examples:
84
99
  #
85
100
  # params = ActionController::Parameters.new({
86
101
  # person: {
@@ -90,14 +105,14 @@ module ActionController
90
105
  # }
91
106
  # })
92
107
  #
93
- # permitted = params.require(:person).permit(:name, :age)
94
- # permitted # => #<ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
95
- # permitted.permitted? # => true
108
+ # permitted = params.expect(person: [:name, :age])
109
+ # permitted # => #<ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
96
110
  #
97
111
  # Person.first.update!(permitted)
98
112
  # # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
99
113
  #
100
- # It provides two options that controls the top-level behavior of new instances:
114
+ # Parameters provides two options that control the top-level behavior of new
115
+ # instances:
101
116
  #
102
117
  # * `permit_all_parameters` - If it's `true`, all the parameters will be
103
118
  # permitted by default. The default is `false`.
@@ -112,8 +127,6 @@ module ActionController
112
127
  # * `:raise` to raise an ActionController::UnpermittedParameters
113
128
  # exception.
114
129
  #
115
- #
116
- #
117
130
  # Examples:
118
131
  #
119
132
  # params = ActionController::Parameters.new
@@ -250,20 +263,6 @@ module ActionController
250
263
  cattr_accessor :always_permitted_parameters, default: %w( controller action )
251
264
 
252
265
  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
266
  def nested_attribute?(key, value) # :nodoc:
268
267
  /\A-?\d+\z/.match?(key) && (value.is_a?(Hash) || value.is_a?(Parameters))
269
268
  end
@@ -481,16 +480,16 @@ module ActionController
481
480
  # Otherwise raises ActionController::ParameterMissing:
482
481
  #
483
482
  # ActionController::Parameters.new.require(:person)
484
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
483
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
485
484
  #
486
485
  # ActionController::Parameters.new(person: nil).require(:person)
487
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
486
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
488
487
  #
489
488
  # ActionController::Parameters.new(person: "\t").require(:person)
490
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
489
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
491
490
  #
492
491
  # ActionController::Parameters.new(person: {}).require(:person)
493
- # # ActionController::ParameterMissing: param is missing or the value is empty: person
492
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
494
493
  #
495
494
  # When given an array of keys, the method tries to require each one of them in
496
495
  # order. If it succeeds, an array with the respective return values is returned:
@@ -502,23 +501,21 @@ module ActionController
502
501
  #
503
502
  # params = ActionController::Parameters.new(user: {}, profile: {})
504
503
  # user_params, profile_params = params.require([:user, :profile])
505
- # # ActionController::ParameterMissing: param is missing or the value is empty: user
504
+ # # ActionController::ParameterMissing: param is missing or the value is empty or invalid: user
506
505
  #
507
- # Technically this method can be used to fetch terminal values:
506
+ # This method is not recommended for fetching terminal values because it does
507
+ # not permit the values. For example, this can cause problems:
508
508
  #
509
509
  # # CAREFUL
510
510
  # params = ActionController::Parameters.new(person: { name: "Finn" })
511
511
  # name = params.require(:person).require(:name) # CAREFUL
512
512
  #
513
- # but take into account that at some point those ones have to be permitted:
513
+ # It is recommended to use `expect` instead:
514
514
  #
515
515
  # def person_params
516
- # params.require(:person).permit(:name).tap do |person_params|
517
- # person_params.require(:name) # SAFER
518
- # end
516
+ # params.expect(person: :name).require(:name)
519
517
  # end
520
518
  #
521
- # for example.
522
519
  def require(key)
523
520
  return key.map { |k| require(k) } if key.is_a?(Array)
524
521
  value = self[key]
@@ -536,8 +533,8 @@ module ActionController
536
533
  # This is useful for limiting which attributes should be allowed for mass
537
534
  # updating.
538
535
  #
539
- # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" })
540
- # permitted = params.require(:user).permit(:name, :age)
536
+ # params = ActionController::Parameters.new(name: "Francesco", age: 22, role: "admin")
537
+ # permitted = params.permit(:name, :age)
541
538
  # permitted.permitted? # => true
542
539
  # permitted.has_key?(:name) # => true
543
540
  # permitted.has_key?(:age) # => true
@@ -567,7 +564,7 @@ module ActionController
567
564
  # `permit` ensures values in the returned structure are permitted scalars and
568
565
  # filters out anything else.
569
566
  #
570
- # You can also use `permit` on nested parameters, like:
567
+ # You can also use `permit` on nested parameters:
571
568
  #
572
569
  # params = ActionController::Parameters.new({
573
570
  # person: {
@@ -587,6 +584,29 @@ module ActionController
587
584
  # permitted[:person][:pets][0][:name] # => "Purplish"
588
585
  # permitted[:person][:pets][0][:category] # => nil
589
586
  #
587
+ # This has the added benefit of rejecting user-modified inputs that send a
588
+ # string when a hash is expected.
589
+ #
590
+ # When followed by `require`, you can both filter and require parameters
591
+ # following the typical pattern of a Rails form. The `expect` method was
592
+ # made specifically for this use case and is the recommended way to require
593
+ # and permit parameters.
594
+ #
595
+ # permitted = params.expect(person: [:name, :age])
596
+ #
597
+ # When using `permit` and `require` separately, pay careful attention to the
598
+ # order of the method calls.
599
+ #
600
+ # params = ActionController::Parameters.new(person: { name: "Martin", age: 40, role: "admin" })
601
+ # permitted = params.permit(person: [:name, :age]).require(:person) # correct
602
+ #
603
+ # When require is used first, it is possible for users of your application to
604
+ # trigger a NoMethodError when the user, for example, sends a string for :person.
605
+ #
606
+ # params = ActionController::Parameters.new(person: "tampered")
607
+ # permitted = params.require(:person).permit(:name, :age) # not recommended
608
+ # # => NoMethodError: undefined method `permit' for an instance of String
609
+ #
590
610
  # Note that if you use `permit` in a key that points to a hash, it won't allow
591
611
  # all the hash. You also need to specify which attributes inside the hash should
592
612
  # be permitted.
@@ -600,13 +620,13 @@ module ActionController
600
620
  # }
601
621
  # })
602
622
  #
603
- # params.require(:person).permit(:contact)
604
- # # => #<ActionController::Parameters {} permitted: true>
623
+ # params.permit(person: :contact).require(:person)
624
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: person
605
625
  #
606
- # params.require(:person).permit(contact: :phone)
626
+ # params.permit(person: { contact: :phone }).require(:person)
607
627
  # # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
608
628
  #
609
- # params.require(:person).permit(contact: [ :email, :phone ])
629
+ # params.permit(person: { contact: [ :email, :phone ] }).require(:person)
610
630
  # # => #<ActionController::Parameters {"contact"=>#<ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
611
631
  #
612
632
  # If your parameters specify multiple parameters indexed by a number, you can
@@ -646,20 +666,127 @@ module ActionController
646
666
  # params.permit(person: { '0': [:email], '1': [:phone]}).to_h
647
667
  # # => {"person"=>{"0"=>{"email"=>"none@test.com"}, "1"=>{"phone"=>"555-6789"}}}
648
668
  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
669
+ permit_filters(filters, on_unpermitted: self.class.action_on_unpermitted_parameters, explicit_arrays: false)
670
+ end
659
671
 
660
- unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
672
+ # `expect` is the preferred way to require and permit parameters.
673
+ # It is safer than the previous recommendation to call `permit` and `require`
674
+ # in sequence, which could allow user triggered 500 errors.
675
+ #
676
+ # `expect` is more strict with types to avoid a number of potential pitfalls
677
+ # that may be encountered with the `.require.permit` pattern.
678
+ #
679
+ # For example:
680
+ #
681
+ # params = ActionController::Parameters.new(comment: { text: "hello" })
682
+ # params.expect(comment: [:text])
683
+ # # => #<ActionController::Parameters { text: "hello" } permitted: true>
684
+ #
685
+ # params = ActionController::Parameters.new(comment: [{ text: "hello" }, { text: "world" }])
686
+ # params.expect(comment: [:text])
687
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: comment
688
+ #
689
+ # In order to permit an array of parameters, the array must be defined
690
+ # explicitly. Use double array brackets, an array inside an array, to
691
+ # declare that an array of parameters is expected.
692
+ #
693
+ # params = ActionController::Parameters.new(comments: [{ text: "hello" }, { text: "world" }])
694
+ # params.expect(comments: [[:text]])
695
+ # # => [#<ActionController::Parameters { "text" => "hello" } permitted: true>,
696
+ # # #<ActionController::Parameters { "text" => "world" } permitted: true>]
697
+ #
698
+ # params = ActionController::Parameters.new(comments: { text: "hello" })
699
+ # params.expect(comments: [[:text]])
700
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: comments
701
+ #
702
+ # `expect` is intended to protect against array tampering.
703
+ #
704
+ # params = ActionController::Parameters.new(user: "hack")
705
+ # # The previous way of requiring and permitting parameters will error
706
+ # params.require(:user).permit(:name, pets: [:name]) # wrong
707
+ # # => NoMethodError: undefined method `permit' for an instance of String
708
+ #
709
+ # # similarly with nested parameters
710
+ # params = ActionController::Parameters.new(user: { name: "Martin", pets: { name: "hack" } })
711
+ # user_params = params.require(:user).permit(:name, pets: [:name]) # wrong
712
+ # # user_params[:pets] is expected to be an array but is a hash
713
+ #
714
+ # `expect` solves this by being more strict with types.
715
+ #
716
+ # params = ActionController::Parameters.new(user: "hack")
717
+ # params.expect(user: [ :name, pets: [[:name]] ])
718
+ # # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: user
719
+ #
720
+ # # with nested parameters
721
+ # params = ActionController::Parameters.new(user: { name: "Martin", pets: { name: "hack" } })
722
+ # user_params = params.expect(user: [:name, pets: [[:name]] ])
723
+ # user_params[:pets] # => nil
724
+ #
725
+ # As the examples show, `expect` requires the `:user` key, and any root keys
726
+ # similar to the `.require.permit` pattern. If multiple root keys are
727
+ # expected, they will all be required.
728
+ #
729
+ # params = ActionController::Parameters.new(name: "Martin", pies: [{ type: "dessert", flavor: "pumpkin"}])
730
+ # name, pies = params.expect(:name, pies: [[:type, :flavor]])
731
+ # name # => "Martin"
732
+ # pies # => [#<ActionController::Parameters {"type"=>"dessert", "flavor"=>"pumpkin"} permitted: true>]
733
+ #
734
+ # When called with a hash with multiple keys, `expect` will permit the
735
+ # parameters and require the keys in the order they are given in the hash,
736
+ # returning an array of the permitted parameters.
737
+ #
738
+ # params = ActionController::Parameters.new(subject: { name: "Martin" }, object: { pie: "pumpkin" })
739
+ # subject, object = params.expect(subject: [:name], object: [:pie])
740
+ # subject # => #<ActionController::Parameters {"name"=>"Martin"} permitted: true>
741
+ # object # => #<ActionController::Parameters {"pie"=>"pumpkin"} permitted: true>
742
+ #
743
+ # Besides being more strict about array vs hash params, `expect` uses permit
744
+ # internally, so it will behave similarly.
745
+ #
746
+ # params = ActionController::Parameters.new({
747
+ # person: {
748
+ # name: "Francesco",
749
+ # age: 22,
750
+ # pets: [{
751
+ # name: "Purplish",
752
+ # category: "dogs"
753
+ # }]
754
+ # }
755
+ # })
756
+ #
757
+ # permitted = params.expect(person: [ :name, { pets: [[:name]] } ])
758
+ # permitted.permitted? # => true
759
+ # permitted[:name] # => "Francesco"
760
+ # permitted[:age] # => nil
761
+ # permitted[:pets][0][:name] # => "Purplish"
762
+ # permitted[:pets][0][:category] # => nil
763
+ #
764
+ # An array of permitted scalars may be expected with the following:
765
+ #
766
+ # params = ActionController::Parameters.new(tags: ["rails", "parameters"])
767
+ # permitted = params.expect(tags: [])
768
+ # permitted.permitted? # => true
769
+ # permitted.is_a?(Array) # => true
770
+ # permitted.size # => 2
771
+ #
772
+ def expect(*filters)
773
+ params = permit_filters(filters)
774
+ keys = filters.flatten.flat_map { |f| f.is_a?(Hash) ? f.keys : f }
775
+ values = params.require(keys)
776
+ values.size == 1 ? values.first : values
777
+ end
661
778
 
662
- params.permit!
779
+ # Same as `expect`, but raises an `ActionController::ExpectedParameterMissing`
780
+ # instead of `ActionController::ParameterMissing`. Unlike `expect` which
781
+ # will render a 400 response, `expect!` will raise an exception that is
782
+ # not handled. This is intended for debugging invalid params for an
783
+ # internal API where incorrectly formatted params would indicate a bug
784
+ # in a client library that should be fixed.
785
+ #
786
+ def expect!(*filters)
787
+ expect(*filters)
788
+ rescue ParameterMissing => e
789
+ raise ExpectedParameterMissing.new(e.param, e.keys)
663
790
  end
664
791
 
665
792
  # Returns a parameter for the given `key`. If not found, returns `nil`.
@@ -686,7 +813,7 @@ module ActionController
686
813
  #
687
814
  # params = ActionController::Parameters.new(person: { name: "Francesco" })
688
815
  # params.fetch(:person) # => #<ActionController::Parameters {"name"=>"Francesco"} permitted: false>
689
- # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
816
+ # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: none
690
817
  # params.fetch(:none, {}) # => #<ActionController::Parameters {} permitted: false>
691
818
  # params.fetch(:none, "Francesco") # => "Francesco"
692
819
  # params.fetch(:none) { "Francesco" } # => "Francesco"
@@ -999,6 +1126,26 @@ module ActionController
999
1126
  hash
1000
1127
  end
1001
1128
 
1129
+ # Filters self and optionally checks for unpermitted keys
1130
+ def permit_filters(filters, on_unpermitted: nil, explicit_arrays: true)
1131
+ params = self.class.new
1132
+
1133
+ filters.flatten.each do |filter|
1134
+ case filter
1135
+ when Symbol, String
1136
+ # Declaration [:name, "age"]
1137
+ permitted_scalar_filter(params, filter)
1138
+ when Hash
1139
+ # Declaration [{ person: ... }]
1140
+ hash_filter(params, filter, on_unpermitted:, explicit_arrays:)
1141
+ end
1142
+ end
1143
+
1144
+ unpermitted_parameters!(params, on_unpermitted:)
1145
+
1146
+ params.permit!
1147
+ end
1148
+
1002
1149
  private
1003
1150
  def new_instance_with_inherited_permitted_status(hash)
1004
1151
  self.class.new(hash, @logging_context).tap do |new_instance|
@@ -1088,23 +1235,45 @@ module ActionController
1088
1235
  end
1089
1236
  end
1090
1237
 
1091
- def each_element(object, filter, &block)
1238
+ # When an array is expected, you must specify an array explicitly
1239
+ # using the following format:
1240
+ #
1241
+ # params.expect(comments: [[:flavor]])
1242
+ #
1243
+ # Which will match only the following array formats:
1244
+ #
1245
+ # { pies: [{ flavor: "rhubarb" }, { flavor: "apple" }] }
1246
+ # { pies: { "0" => { flavor: "key lime" }, "1" => { flavor: "mince" } } }
1247
+ #
1248
+ # When using `permit`, arrays are specified the same way as hashes:
1249
+ #
1250
+ # params.expect(pies: [:flavor])
1251
+ #
1252
+ # In this case, `permit` would also allow matching with a hash (or vice versa):
1253
+ #
1254
+ # { pies: { flavor: "cherry" } }
1255
+ #
1256
+ def array_filter?(filter)
1257
+ filter.is_a?(Array) && filter.size == 1 && filter.first.is_a?(Array)
1258
+ end
1259
+
1260
+ # Called when an explicit array filter is encountered.
1261
+ def each_array_element(object, filter, &block)
1092
1262
  case object
1093
1263
  when Array
1094
1264
  object.grep(Parameters).filter_map(&block)
1095
1265
  when Parameters
1096
1266
  if object.nested_attributes? && !specify_numeric_keys?(filter)
1097
1267
  object.each_nested_attribute(&block)
1098
- else
1099
- yield object
1100
1268
  end
1101
1269
  end
1102
1270
  end
1103
1271
 
1104
- def unpermitted_parameters!(params)
1272
+ def unpermitted_parameters!(params, on_unpermitted: self.class.action_on_unpermitted_parameters)
1273
+ return unless on_unpermitted
1105
1274
  unpermitted_keys = unpermitted_keys(params)
1106
1275
  if unpermitted_keys.any?
1107
- case self.class.action_on_unpermitted_parameters
1276
+ case on_unpermitted
1108
1277
  when :log
1109
1278
  name = "unpermitted_parameters.action_controller"
1110
1279
  ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys, context: @logging_context)
@@ -1174,45 +1343,63 @@ module ActionController
1174
1343
  end
1175
1344
  end
1176
1345
 
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
1346
  def non_scalar?(value)
1184
1347
  value.is_a?(Array) || value.is_a?(Parameters)
1185
1348
  end
1186
1349
 
1187
1350
  EMPTY_ARRAY = [] # :nodoc:
1188
1351
  EMPTY_HASH = {} # :nodoc:
1189
- def hash_filter(params, filter)
1352
+ def hash_filter(params, filter, on_unpermitted: self.class.action_on_unpermitted_parameters, explicit_arrays: false)
1190
1353
  filter = filter.with_indifferent_access
1191
1354
 
1192
1355
  # Slicing filters out non-declared keys.
1193
1356
  slice(*filter.keys).each do |key, value|
1194
1357
  next unless value
1195
1358
  next unless has_key? key
1359
+ result = permit_value(value, filter[key], on_unpermitted:, explicit_arrays:)
1360
+ params[key] = result unless result.nil?
1361
+ end
1362
+ end
1196
1363
 
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
1364
+ def permit_value(value, filter, on_unpermitted:, explicit_arrays:)
1365
+ if filter == EMPTY_ARRAY # Declaration { comment_ids: [] }.
1366
+ permit_array_of_scalars(value)
1367
+ elsif filter == EMPTY_HASH # Declaration { preferences: {} }.
1368
+ permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1369
+ elsif array_filter?(filter) # Declaration { comments: [[:text]] }
1370
+ permit_array_of_hashes(value, filter.first, on_unpermitted:, explicit_arrays:)
1371
+ elsif explicit_arrays # Declaration { user: { address: ... } } or { user: [:name, ...] } (only allows hash value)
1372
+ permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1373
+ elsif non_scalar?(value) # Declaration { user: { address: ... } } or { user: [:name, ...] }
1374
+ permit_hash_or_array(value, filter, on_unpermitted:, explicit_arrays:)
1213
1375
  end
1214
1376
  end
1215
1377
 
1378
+ def permit_array_of_scalars(value)
1379
+ value if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
1380
+ end
1381
+
1382
+ def permit_array_of_hashes(value, filter, on_unpermitted:, explicit_arrays:)
1383
+ each_array_element(value, filter) do |element|
1384
+ element.permit_filters(Array.wrap(filter), on_unpermitted:, explicit_arrays:)
1385
+ end
1386
+ end
1387
+
1388
+ def permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1389
+ return unless value.is_a?(Parameters)
1390
+
1391
+ if filter == EMPTY_HASH
1392
+ permit_any_in_parameters(value)
1393
+ else
1394
+ value.permit_filters(Array.wrap(filter), on_unpermitted:, explicit_arrays:)
1395
+ end
1396
+ end
1397
+
1398
+ def permit_hash_or_array(value, filter, on_unpermitted:, explicit_arrays:)
1399
+ permit_array_of_hashes(value, filter, on_unpermitted:, explicit_arrays:) ||
1400
+ permit_hash(value, filter, on_unpermitted:, explicit_arrays:)
1401
+ end
1402
+
1216
1403
  def permit_any_in_parameters(params)
1217
1404
  self.class.new.tap do |sanitized|
1218
1405
  params.each do |key, value|
@@ -1287,7 +1474,7 @@ module ActionController
1287
1474
  # # list between create and update. Also, you can specialize this method
1288
1475
  # # with per-user checking of permissible attributes.
1289
1476
  # def person_params
1290
- # params.require(:person).permit(:name, :age)
1477
+ # params.expect(person: [:name, :age])
1291
1478
  # end
1292
1479
  # end
1293
1480
  #
@@ -1314,11 +1501,12 @@ module ActionController
1314
1501
  # # It's mandatory to specify the nested attributes that should be permitted.
1315
1502
  # # If you use `permit` with just the key that points to the nested attributes hash,
1316
1503
  # # it will return an empty hash.
1317
- # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
1504
+ # params.expect(person: [ :name, :age, pets_attributes: [ :id, :name, :category ] ])
1318
1505
  # end
1319
1506
  # end
1320
1507
  #
1321
- # See ActionController::Parameters.require and
1508
+ # See ActionController::Parameters.expect,
1509
+ # See ActionController::Parameters.require, and
1322
1510
  # ActionController::Parameters.permit for more information.
1323
1511
  module StrongParameters
1324
1512
  # Returns a new ActionController::Parameters object that has been instantiated