actionpack 7.2.1.1 → 8.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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