actionpack 7.2.2.1 → 8.0.5

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -101
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/base.rb +1 -12
  5. data/lib/abstract_controller/collector.rb +1 -1
  6. data/lib/abstract_controller/helpers.rb +1 -1
  7. data/lib/abstract_controller/rendering.rb +0 -1
  8. data/lib/action_controller/base.rb +1 -1
  9. data/lib/action_controller/form_builder.rb +3 -3
  10. data/lib/action_controller/metal/allow_browser.rb +11 -1
  11. data/lib/action_controller/metal/conditional_get.rb +5 -1
  12. data/lib/action_controller/metal/data_streaming.rb +4 -2
  13. data/lib/action_controller/metal/instrumentation.rb +1 -2
  14. data/lib/action_controller/metal/live.rb +59 -11
  15. data/lib/action_controller/metal/params_wrapper.rb +3 -3
  16. data/lib/action_controller/metal/rate_limiting.rb +13 -4
  17. data/lib/action_controller/metal/redirecting.rb +4 -3
  18. data/lib/action_controller/metal/renderers.rb +2 -3
  19. data/lib/action_controller/metal/rendering.rb +1 -1
  20. data/lib/action_controller/metal/request_forgery_protection.rb +3 -1
  21. data/lib/action_controller/metal/streaming.rb +5 -84
  22. data/lib/action_controller/metal/strong_parameters.rb +277 -92
  23. data/lib/action_controller/railtie.rb +6 -7
  24. data/lib/action_controller/renderer.rb +0 -1
  25. data/lib/action_controller/test_case.rb +12 -2
  26. data/lib/action_dispatch/constants.rb +6 -0
  27. data/lib/action_dispatch/http/cache.rb +27 -10
  28. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  29. data/lib/action_dispatch/http/mime_negotiation.rb +8 -3
  30. data/lib/action_dispatch/http/param_builder.rb +186 -0
  31. data/lib/action_dispatch/http/param_error.rb +26 -0
  32. data/lib/action_dispatch/http/permissions_policy.rb +2 -0
  33. data/lib/action_dispatch/http/query_parser.rb +53 -0
  34. data/lib/action_dispatch/http/request.rb +64 -19
  35. data/lib/action_dispatch/http/response.rb +49 -14
  36. data/lib/action_dispatch/http/url.rb +2 -2
  37. data/lib/action_dispatch/journey/formatter.rb +8 -3
  38. data/lib/action_dispatch/journey/gtg/transition_table.rb +4 -4
  39. data/lib/action_dispatch/journey/parser.rb +99 -196
  40. data/lib/action_dispatch/journey/scanner.rb +44 -42
  41. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  42. data/lib/action_dispatch/middleware/debug_exceptions.rb +19 -4
  43. data/lib/action_dispatch/middleware/debug_view.rb +0 -5
  44. data/lib/action_dispatch/middleware/exception_wrapper.rb +3 -9
  45. data/lib/action_dispatch/middleware/executor.rb +5 -2
  46. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -1
  47. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  48. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  49. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +0 -3
  50. data/lib/action_dispatch/railtie.rb +8 -0
  51. data/lib/action_dispatch/request/session.rb +1 -0
  52. data/lib/action_dispatch/request/utils.rb +9 -3
  53. data/lib/action_dispatch/routing/inspector.rb +1 -1
  54. data/lib/action_dispatch/routing/mapper.rb +96 -67
  55. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  56. data/lib/action_dispatch/routing/route_set.rb +21 -10
  57. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  58. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  59. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  60. data/lib/action_dispatch/testing/assertions/response.rb +12 -2
  61. data/lib/action_dispatch/testing/assertions/routing.rb +16 -12
  62. data/lib/action_dispatch/testing/integration.rb +20 -10
  63. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  64. data/lib/action_dispatch/testing/test_process.rb +1 -2
  65. data/lib/action_dispatch.rb +6 -4
  66. data/lib/action_pack/gem_version.rb +4 -4
  67. metadata +16 -38
  68. data/lib/action_dispatch/journey/parser.y +0 -50
  69. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -9,6 +9,8 @@ module ActionDispatch
9
9
  HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
10
10
  HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
11
11
 
12
+ mattr_accessor :strict_freshness, default: false
13
+
12
14
  def if_modified_since
13
15
  if since = get_header(HTTP_IF_MODIFIED_SINCE)
14
16
  Time.rfc2822(since) rescue nil
@@ -34,19 +36,32 @@ module ActionDispatch
34
36
  end
35
37
  end
36
38
 
37
- # Check response freshness (`Last-Modified` and ETag) against request
38
- # `If-Modified-Since` and `If-None-Match` conditions. If both headers are
39
- # supplied, both must match, or the request is not considered fresh.
39
+ # Check response freshness (`Last-Modified` and `ETag`) against request
40
+ # `If-Modified-Since` and `If-None-Match` conditions.
41
+ # If both headers are supplied, based on configuration, either `ETag` is preferred over `Last-Modified`
42
+ # or both are considered equally. You can adjust the preference with
43
+ # `config.action_dispatch.strict_freshness`.
44
+ # Reference: http://tools.ietf.org/html/rfc7232#section-6
40
45
  def fresh?(response)
41
- last_modified = if_modified_since
42
- etag = if_none_match
46
+ if Request.strict_freshness
47
+ if if_none_match
48
+ etag_matches?(response.etag)
49
+ elsif if_modified_since
50
+ not_modified?(response.last_modified)
51
+ else
52
+ false
53
+ end
54
+ else
55
+ last_modified = if_modified_since
56
+ etag = if_none_match
43
57
 
44
- return false unless last_modified || etag
58
+ return false unless last_modified || etag
45
59
 
46
- success = true
47
- success &&= not_modified?(response.last_modified) if last_modified
48
- success &&= etag_matches?(response.etag) if etag
49
- success
60
+ success = true
61
+ success &&= not_modified?(response.last_modified) if last_modified
62
+ success &&= etag_matches?(response.etag) if etag
63
+ success
64
+ end
50
65
  end
51
66
  end
52
67
 
@@ -171,6 +186,7 @@ module ActionDispatch
171
186
  PUBLIC = "public"
172
187
  PRIVATE = "private"
173
188
  MUST_REVALIDATE = "must-revalidate"
189
+ IMMUTABLE = "immutable"
174
190
 
175
191
  def handle_conditional_get!
176
192
  # Normally default cache control setting is handled by ETag middleware. But, if
@@ -221,6 +237,7 @@ module ActionDispatch
221
237
  options << MUST_REVALIDATE if control[:must_revalidate]
222
238
  options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
223
239
  options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
240
+ options << IMMUTABLE if control[:immutable]
224
241
  options.concat(extras) if extras
225
242
  end
226
243
 
@@ -128,6 +128,7 @@ module ActionDispatch # :nodoc:
128
128
  MAPPINGS = {
129
129
  self: "'self'",
130
130
  unsafe_eval: "'unsafe-eval'",
131
+ wasm_unsafe_eval: "'wasm-unsafe-eval'",
131
132
  unsafe_hashes: "'unsafe-hashes'",
132
133
  unsafe_inline: "'unsafe-inline'",
133
134
  none: "'none'",
@@ -170,6 +171,8 @@ module ActionDispatch # :nodoc:
170
171
  worker_src: "worker-src"
171
172
  }.freeze
172
173
 
174
+ HASH_SOURCE_ALGORITHM_PREFIXES = ["sha256-", "sha384-", "sha512-"].freeze
175
+
173
176
  DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
174
177
 
175
178
  private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
@@ -304,7 +307,13 @@ module ActionDispatch # :nodoc:
304
307
  case source
305
308
  when Symbol
306
309
  apply_mapping(source)
307
- when String, Proc
310
+ when String
311
+ if hash_source?(source)
312
+ "'#{source}'"
313
+ else
314
+ source
315
+ end
316
+ when Proc
308
317
  source
309
318
  else
310
319
  raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
@@ -373,5 +382,9 @@ module ActionDispatch # :nodoc:
373
382
  def nonce_directive?(directive, nonce_directives)
374
383
  nonce_directives.include?(directive)
375
384
  end
385
+
386
+ def hash_source?(source)
387
+ source.start_with?(*HASH_SOURCE_ALGORITHM_PREFIXES)
388
+ end
376
389
  end
377
390
  end
@@ -56,9 +56,14 @@ module ActionDispatch
56
56
 
57
57
  # Returns the MIME type for the format used in the request.
58
58
  #
59
- # GET /posts/5.xml | request.format => Mime[:xml]
60
- # GET /posts/5.xhtml | request.format => Mime[:html]
61
- # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
59
+ # # GET /posts/5.xml
60
+ # request.format # => Mime[:xml]
61
+ #
62
+ # # GET /posts/5.xhtml
63
+ # request.format # => Mime[:html]
64
+ #
65
+ # # GET /posts/5
66
+ # request.format # => Mime[:html] or Mime[:js], or request.accepts.first
62
67
  #
63
68
  def format(_view_path = nil)
64
69
  formats.first || Mime::NullType.instance
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class ParamBuilder
5
+ # --
6
+ # This implementation is based on Rack::QueryParser,
7
+ # Copyright (C) 2007-2021 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
8
+
9
+ def self.make_default(param_depth_limit)
10
+ new param_depth_limit
11
+ end
12
+
13
+ attr_reader :param_depth_limit
14
+
15
+ def initialize(param_depth_limit)
16
+ @param_depth_limit = param_depth_limit
17
+ end
18
+
19
+ cattr_accessor :ignore_leading_brackets
20
+
21
+ LEADING_BRACKETS_COMPAT = defined?(::Rack::RELEASE) && ::Rack::RELEASE.to_s.start_with?("2.")
22
+
23
+ cattr_accessor :default
24
+ self.default = make_default(100)
25
+
26
+ class << self
27
+ delegate :from_query_string, :from_pairs, :from_hash, to: :default
28
+ end
29
+
30
+ def from_query_string(qs, separator: nil, encoding_template: nil)
31
+ from_pairs QueryParser.each_pair(qs, separator), encoding_template: encoding_template
32
+ end
33
+
34
+ def from_pairs(pairs, encoding_template: nil)
35
+ params = make_params
36
+
37
+ pairs.each do |k, v|
38
+ if Hash === v
39
+ v = ActionDispatch::Http::UploadedFile.new(v)
40
+ end
41
+
42
+ store_nested_param(params, k, v, 0, encoding_template)
43
+ end
44
+
45
+ params
46
+ rescue ArgumentError => e
47
+ raise InvalidParameterError, e.message, e.backtrace
48
+ end
49
+
50
+ def from_hash(hash, encoding_template: nil)
51
+ # Force encodings from encoding template
52
+ hash = Request::Utils::CustomParamEncoder.encode_for_template(hash, encoding_template)
53
+
54
+ # Assert valid encoding
55
+ Request::Utils.check_param_encoding(hash)
56
+
57
+ # Convert hashes to HWIA (or UploadedFile), and deep-munge nils
58
+ # out of arrays
59
+ hash = Request::Utils.normalize_encode_params(hash)
60
+
61
+ hash
62
+ end
63
+
64
+ private
65
+ def store_nested_param(params, name, v, depth, encoding_template = nil)
66
+ raise ParamsTooDeepError if depth >= param_depth_limit
67
+
68
+ if !name
69
+ # nil name, treat same as empty string (required by tests)
70
+ k = after = ""
71
+ elsif depth == 0
72
+ if ignore_leading_brackets || (ignore_leading_brackets.nil? && LEADING_BRACKETS_COMPAT)
73
+ # Rack 2 compatible behavior, ignore leading brackets
74
+ if name =~ /\A[\[\]]*([^\[\]]+)\]*/
75
+ k = $1
76
+ after = $' || ""
77
+
78
+ if !ignore_leading_brackets && (k != $& || !after.empty? && !after.start_with?("["))
79
+ ActionDispatch.deprecator.warn("Skipping over leading brackets in parameter name #{name.inspect} is deprecated and will parse differently in Rails 8.1 or Rack 3.0.")
80
+ end
81
+ else
82
+ k = name
83
+ after = ""
84
+ end
85
+ else
86
+ # Start of parsing, don't treat [] or [ at start of string specially
87
+ if start = name.index("[", 1)
88
+ # Start of parameter nesting, use part before brackets as key
89
+ k = name[0, start]
90
+ after = name[start, name.length]
91
+ else
92
+ # Plain parameter with no nesting
93
+ k = name
94
+ after = ""
95
+ end
96
+ end
97
+ elsif name.start_with?("[]")
98
+ # Array nesting
99
+ k = "[]"
100
+ after = name[2, name.length]
101
+ elsif name.start_with?("[") && (start = name.index("]", 1))
102
+ # Hash nesting, use the part inside brackets as the key
103
+ k = name[1, start - 1]
104
+ after = name[start + 1, name.length]
105
+ else
106
+ # Probably malformed input, nested but not starting with [
107
+ # treat full name as key for backwards compatibility.
108
+ k = name
109
+ after = ""
110
+ end
111
+
112
+ return if k.empty?
113
+
114
+ if depth == 0 && String === v
115
+ # We have to wait until we've found the top part of the name,
116
+ # because that's what the encoding template is configured with
117
+ if encoding_template && (designated_encoding = encoding_template[k]) && !v.frozen?
118
+ v.force_encoding(designated_encoding)
119
+ end
120
+
121
+ # ... and we can't validate the encoding until after we've
122
+ # applied any template override
123
+ unless v.valid_encoding?
124
+ raise InvalidParameterError, "Invalid encoding for parameter: #{v.scrub}"
125
+ end
126
+ end
127
+
128
+ if after == ""
129
+ if k == "[]" && depth != 0
130
+ return (v || !ActionDispatch::Request::Utils.perform_deep_munge) ? [v] : []
131
+ else
132
+ params[k] = v
133
+ end
134
+ elsif after == "["
135
+ params[name] = v
136
+ elsif after == "[]"
137
+ params[k] ||= []
138
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
139
+ params[k] << v if v || !ActionDispatch::Request::Utils.perform_deep_munge
140
+ elsif after.start_with?("[]")
141
+ # Recognize x[][y] (hash inside array) parameters
142
+ unless after[2] == "[" && after.end_with?("]") && (child_key = after[3, after.length - 4]) && !child_key.empty? && !child_key.index("[") && !child_key.index("]")
143
+ # Handle other nested array parameters
144
+ child_key = after[2, after.length]
145
+ end
146
+ params[k] ||= []
147
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
148
+ if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
149
+ store_nested_param(params[k].last, child_key, v, depth + 1)
150
+ else
151
+ params[k] << store_nested_param(make_params, child_key, v, depth + 1)
152
+ end
153
+ else
154
+ params[k] ||= make_params
155
+ raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
156
+ params[k] = store_nested_param(params[k], after, v, depth + 1)
157
+ end
158
+
159
+ params
160
+ end
161
+
162
+ def make_params
163
+ ActiveSupport::HashWithIndifferentAccess.new
164
+ end
165
+
166
+ def new_depth_limit(param_depth_limit)
167
+ self.class.new @params_class, param_depth_limit
168
+ end
169
+
170
+ def params_hash_type?(obj)
171
+ Hash === obj
172
+ end
173
+
174
+ def params_hash_has_key?(hash, key)
175
+ return false if key.include?("[]")
176
+
177
+ key.split(/[\[\]]+/).inject(hash) do |h, part|
178
+ next h if part == ""
179
+ return false unless params_hash_type?(h) && h.key?(part)
180
+ h[part]
181
+ end
182
+
183
+ true
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class ParamError < ActionDispatch::Http::Parameters::ParseError
5
+ def initialize(message = nil)
6
+ super
7
+ end
8
+
9
+ def self.===(other)
10
+ super || (
11
+ defined?(Rack::Utils::ParameterTypeError) && Rack::Utils::ParameterTypeError === other ||
12
+ defined?(Rack::Utils::InvalidParameterError) && Rack::Utils::InvalidParameterError === other ||
13
+ defined?(Rack::QueryParser::ParamsTooDeepError) && Rack::QueryParser::ParamsTooDeepError === other
14
+ )
15
+ end
16
+ end
17
+
18
+ class ParameterTypeError < ParamError
19
+ end
20
+
21
+ class InvalidParameterError < ParamError
22
+ end
23
+
24
+ class ParamsTooDeepError < ParamError
25
+ end
26
+ end
@@ -86,12 +86,14 @@ module ActionDispatch # :nodoc:
86
86
  ambient_light_sensor: "ambient-light-sensor",
87
87
  autoplay: "autoplay",
88
88
  camera: "camera",
89
+ display_capture: "display-capture",
89
90
  encrypted_media: "encrypted-media",
90
91
  fullscreen: "fullscreen",
91
92
  geolocation: "geolocation",
92
93
  gyroscope: "gyroscope",
93
94
  hid: "hid",
94
95
  idle_detection: "idle-detection",
96
+ keyboard_map: "keyboard-map",
95
97
  magnetometer: "magnetometer",
96
98
  microphone: "microphone",
97
99
  midi: "midi",
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "rack"
5
+
6
+ module ActionDispatch
7
+ class QueryParser
8
+ DEFAULT_SEP = /& */n
9
+ COMPAT_SEP = /[&;] */n
10
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n, "&;" => /[&;] */n }
11
+
12
+ cattr_accessor :strict_query_string_separator
13
+
14
+ SEMICOLON_COMPAT = defined?(::Rack::QueryParser::DEFAULT_SEP) && ::Rack::QueryParser::DEFAULT_SEP.to_s.include?(";")
15
+
16
+ #--
17
+ # Note this departs from WHATWG's specified parsing algorithm by
18
+ # giving a nil value for keys that do not use '='. Callers that need
19
+ # the standard's interpretation can use `v.to_s`.
20
+ def self.each_pair(s, separator = nil)
21
+ return enum_for(:each_pair, s, separator) unless block_given?
22
+
23
+ s ||= ""
24
+
25
+ splitter =
26
+ if separator
27
+ COMMON_SEP[separator] || /[#{separator}] */n
28
+ elsif strict_query_string_separator
29
+ DEFAULT_SEP
30
+ elsif SEMICOLON_COMPAT && s.include?(";")
31
+ if strict_query_string_separator.nil?
32
+ ActionDispatch.deprecator.warn("Using semicolon as a query string separator is deprecated and will not be supported in Rails 8.1 or Rack 3.0. Use `&` instead.")
33
+ end
34
+ COMPAT_SEP
35
+ else
36
+ DEFAULT_SEP
37
+ end
38
+
39
+ s.split(splitter).each do |part|
40
+ next if part.empty?
41
+
42
+ k, v = part.split("=", 2)
43
+
44
+ k = URI.decode_www_form_component(k)
45
+ v &&= URI.decode_www_form_component(v)
46
+
47
+ yield k, v
48
+ end
49
+
50
+ nil
51
+ end
52
+ end
53
+ end
@@ -55,12 +55,17 @@ module ActionDispatch
55
55
  METHOD
56
56
  end
57
57
 
58
+ TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING" # :nodoc:
59
+
58
60
  def self.empty
59
61
  new({})
60
62
  end
61
63
 
62
64
  def initialize(env)
63
65
  super
66
+
67
+ @rack_request = Rack::Request.new(env)
68
+
64
69
  @method = nil
65
70
  @request_method = nil
66
71
  @remote_ip = nil
@@ -69,6 +74,8 @@ module ActionDispatch
69
74
  @ip = nil
70
75
  end
71
76
 
77
+ attr_reader :rack_request
78
+
72
79
  def commit_cookie_jar! # :nodoc:
73
80
  end
74
81
 
@@ -132,7 +139,7 @@ module ActionDispatch
132
139
 
133
140
  # Populate the HTTP method lookup cache.
134
141
  HTTP_METHODS.each { |method|
135
- HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
142
+ HTTP_METHOD_LOOKUP[method] = method.downcase.underscore.to_sym
136
143
  }
137
144
 
138
145
  alias raw_request_method request_method # :nodoc:
@@ -236,8 +243,9 @@ module ActionDispatch
236
243
  #
237
244
  # send_early_hints("link" => "</style.css>; rel=preload; as=style,</script.js>; rel=preload")
238
245
  #
239
- # If you are using `javascript_include_tag` or `stylesheet_link_tag` the Early
240
- # Hints headers are included by default if supported.
246
+ # If you are using {javascript_include_tag}[rdoc-ref:ActionView::Helpers::AssetTagHelper#javascript_include_tag]
247
+ # or {stylesheet_link_tag}[rdoc-ref:ActionView::Helpers::AssetTagHelper#stylesheet_link_tag]
248
+ # the Early Hints headers are included by default if supported.
241
249
  def send_early_hints(links)
242
250
  env["rack.early_hints"]&.call(links)
243
251
  end
@@ -282,7 +290,7 @@ module ActionDispatch
282
290
 
283
291
  # Returns the content length of the request as an integer.
284
292
  def content_length
285
- return raw_post.bytesize if headers.key?("Transfer-Encoding")
293
+ return raw_post.bytesize if has_header?(TRANSFER_ENCODING)
286
294
  super.to_i
287
295
  end
288
296
 
@@ -386,15 +394,12 @@ module ActionDispatch
386
394
  # Override Rack's GET method to support indifferent access.
387
395
  def GET
388
396
  fetch_header("action_dispatch.request.query_parameters") do |k|
389
- rack_query_params = super || {}
390
- controller = path_parameters[:controller]
391
- action = path_parameters[:action]
392
- rack_query_params = Request::Utils.set_binary_encoding(self, rack_query_params, controller, action)
393
- # Check for non UTF-8 parameter values, which would cause errors later
394
- Request::Utils.check_param_encoding(rack_query_params)
395
- set_header k, Request::Utils.normalize_encode_params(rack_query_params)
397
+ encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
398
+ rack_query_params = ActionDispatch::ParamBuilder.from_query_string(rack_request.query_string, encoding_template: encoding_template)
399
+
400
+ set_header k, rack_query_params
396
401
  end
397
- rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError => e
402
+ rescue ActionDispatch::ParamError => e
398
403
  raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
399
404
  end
400
405
  alias :query_parameters :GET
@@ -402,18 +407,54 @@ module ActionDispatch
402
407
  # Override Rack's POST method to support indifferent access.
403
408
  def POST
404
409
  fetch_header("action_dispatch.request.request_parameters") do
405
- pr = parse_formatted_parameters(params_parsers) do |params|
406
- super || {}
410
+ encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
411
+
412
+ param_list = nil
413
+ pr = parse_formatted_parameters(params_parsers) do
414
+ if param_list = request_parameters_list
415
+ ActionDispatch::ParamBuilder.from_pairs(param_list, encoding_template: encoding_template)
416
+ else
417
+ # We're not using a version of Rack that provides raw form
418
+ # pairs; we must use its hash (and thus post-process it below).
419
+ fallback_request_parameters
420
+ end
407
421
  end
408
- pr = Request::Utils.set_binary_encoding(self, pr, path_parameters[:controller], path_parameters[:action])
409
- Request::Utils.check_param_encoding(pr)
410
- self.request_parameters = Request::Utils.normalize_encode_params(pr)
422
+
423
+ # If the request body was parsed by a custom parser like JSON
424
+ # (and thus the above block was not run), we need to
425
+ # post-process the result hash.
426
+ if param_list.nil?
427
+ pr = ActionDispatch::ParamBuilder.from_hash(pr, encoding_template: encoding_template)
428
+ end
429
+
430
+ self.request_parameters = pr
411
431
  end
412
- rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError, EOFError => e
432
+ rescue ActionDispatch::ParamError, EOFError => e
413
433
  raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
414
434
  end
415
435
  alias :request_parameters :POST
416
436
 
437
+ def request_parameters_list
438
+ # We don't use Rack's parse result, but we must call it so Rack
439
+ # can populate the rack.request.* keys we need.
440
+ rack_post = rack_request.POST
441
+
442
+ if form_pairs = get_header("rack.request.form_pairs")
443
+ # Multipart
444
+ form_pairs
445
+ elsif form_vars = get_header("rack.request.form_vars")
446
+ # URL-encoded
447
+ ActionDispatch::QueryParser.each_pair(form_vars)
448
+ elsif rack_post && !rack_post.empty?
449
+ # It was multipart, but Rack did not preserve a pair list
450
+ # (probably too old). Flat parameter list is not available.
451
+ nil
452
+ else
453
+ # No request body, or not a format Rack knows
454
+ []
455
+ end
456
+ end
457
+
417
458
  # Returns the authorization header regardless of whether it was specified
418
459
  # directly or through one of the proxy alternatives.
419
460
  def authorization
@@ -468,7 +509,7 @@ module ActionDispatch
468
509
  def read_body_stream
469
510
  if body_stream
470
511
  reset_stream(body_stream) do
471
- if headers.key?("Transfer-Encoding")
512
+ if has_header?(TRANSFER_ENCODING)
472
513
  body_stream.read # Read body stream until EOF if "Transfer-Encoding" is present
473
514
  else
474
515
  body_stream.read(content_length)
@@ -490,6 +531,10 @@ module ActionDispatch
490
531
  yield
491
532
  end
492
533
  end
534
+
535
+ def fallback_request_parameters
536
+ rack_request.POST
537
+ end
493
538
  end
494
539
  end
495
540
 
@@ -46,6 +46,20 @@ module ActionDispatch # :nodoc:
46
46
  Headers = ::Rack::Utils::HeaderHash
47
47
  end
48
48
 
49
+ class << self
50
+ if ActionDispatch::Constants::UNPROCESSABLE_CONTENT == :unprocessable_content
51
+ def rack_status_code(status) # :nodoc:
52
+ status = :unprocessable_content if status == :unprocessable_entity
53
+ Rack::Utils.status_code(status)
54
+ end
55
+ else
56
+ def rack_status_code(status) # :nodoc:
57
+ status = :unprocessable_entity if status == :unprocessable_content
58
+ Rack::Utils.status_code(status)
59
+ end
60
+ end
61
+ end
62
+
49
63
  # To be deprecated:
50
64
  Header = Headers
51
65
 
@@ -105,10 +119,22 @@ module ActionDispatch # :nodoc:
105
119
  @str_body = nil
106
120
  end
107
121
 
122
+ BODY_METHODS = { to_ary: true }
123
+
124
+ def respond_to?(method, include_private = false)
125
+ if BODY_METHODS.key?(method)
126
+ @buf.respond_to?(method)
127
+ else
128
+ super
129
+ end
130
+ end
131
+
108
132
  def to_ary
109
- @buf.respond_to?(:to_ary) ?
110
- @buf.to_ary :
111
- @buf.each
133
+ if @str_body
134
+ [body]
135
+ else
136
+ @buf = @buf.to_ary
137
+ end
112
138
  end
113
139
 
114
140
  def body
@@ -245,7 +271,7 @@ module ActionDispatch # :nodoc:
245
271
 
246
272
  # Sets the HTTP status code.
247
273
  def status=(status)
248
- @status = Rack::Utils.status_code(status)
274
+ @status = Response.rack_status_code(status)
249
275
  end
250
276
 
251
277
  # Sets the HTTP response's content MIME type. For example, in the controller you
@@ -328,7 +354,13 @@ module ActionDispatch # :nodoc:
328
354
  # Returns the content of the response as a string. This contains the contents of
329
355
  # any calls to `render`.
330
356
  def body
331
- @stream.body
357
+ if @stream.respond_to?(:to_ary)
358
+ @stream.to_ary.join
359
+ elsif @stream.respond_to?(:body)
360
+ @stream.body
361
+ else
362
+ @stream
363
+ end
332
364
  end
333
365
 
334
366
  def write(string)
@@ -337,11 +369,16 @@ module ActionDispatch # :nodoc:
337
369
 
338
370
  # Allows you to manually set or override the response body.
339
371
  def body=(body)
340
- if body.respond_to?(:to_path)
341
- @stream = body
342
- else
343
- synchronize do
344
- @stream = build_buffer self, munge_body_object(body)
372
+ # Prevent ActionController::Metal::Live::Response from committing the response prematurely.
373
+ synchronize do
374
+ if body.respond_to?(:to_str)
375
+ @stream = build_buffer(self, [body])
376
+ elsif body.respond_to?(:to_path)
377
+ @stream = body
378
+ elsif body.respond_to?(:to_ary)
379
+ @stream = build_buffer(self, body)
380
+ else
381
+ @stream = body
345
382
  end
346
383
  end
347
384
  end
@@ -482,10 +519,6 @@ module ActionDispatch # :nodoc:
482
519
  Buffer.new response, body
483
520
  end
484
521
 
485
- def munge_body_object(body)
486
- body.respond_to?(:each) ? body : [body]
487
- end
488
-
489
522
  def assign_default_content_type_and_charset!
490
523
  return if media_type
491
524
 
@@ -499,6 +532,8 @@ module ActionDispatch # :nodoc:
499
532
  @response = response
500
533
  end
501
534
 
535
+ attr :response
536
+
502
537
  def close
503
538
  # Rack "close" maps to Response#abort, and **not** Response#close (which is used
504
539
  # when the controller's finished writing)