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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +228 -101
- data/README.rdoc +1 -1
- data/lib/abstract_controller/base.rb +1 -12
- data/lib/abstract_controller/collector.rb +1 -1
- data/lib/abstract_controller/helpers.rb +1 -1
- data/lib/abstract_controller/rendering.rb +0 -1
- data/lib/action_controller/base.rb +1 -1
- data/lib/action_controller/form_builder.rb +3 -3
- data/lib/action_controller/metal/allow_browser.rb +11 -1
- data/lib/action_controller/metal/conditional_get.rb +5 -1
- data/lib/action_controller/metal/data_streaming.rb +4 -2
- data/lib/action_controller/metal/instrumentation.rb +1 -2
- data/lib/action_controller/metal/live.rb +59 -11
- data/lib/action_controller/metal/params_wrapper.rb +3 -3
- data/lib/action_controller/metal/rate_limiting.rb +13 -4
- data/lib/action_controller/metal/redirecting.rb +4 -3
- data/lib/action_controller/metal/renderers.rb +2 -3
- data/lib/action_controller/metal/rendering.rb +1 -1
- data/lib/action_controller/metal/request_forgery_protection.rb +3 -1
- data/lib/action_controller/metal/streaming.rb +5 -84
- data/lib/action_controller/metal/strong_parameters.rb +277 -92
- data/lib/action_controller/railtie.rb +6 -7
- data/lib/action_controller/renderer.rb +0 -1
- data/lib/action_controller/test_case.rb +12 -2
- data/lib/action_dispatch/constants.rb +6 -0
- data/lib/action_dispatch/http/cache.rb +27 -10
- data/lib/action_dispatch/http/content_security_policy.rb +14 -1
- data/lib/action_dispatch/http/mime_negotiation.rb +8 -3
- data/lib/action_dispatch/http/param_builder.rb +186 -0
- data/lib/action_dispatch/http/param_error.rb +26 -0
- data/lib/action_dispatch/http/permissions_policy.rb +2 -0
- data/lib/action_dispatch/http/query_parser.rb +53 -0
- data/lib/action_dispatch/http/request.rb +64 -19
- data/lib/action_dispatch/http/response.rb +49 -14
- data/lib/action_dispatch/http/url.rb +2 -2
- data/lib/action_dispatch/journey/formatter.rb +8 -3
- data/lib/action_dispatch/journey/gtg/transition_table.rb +4 -4
- data/lib/action_dispatch/journey/parser.rb +99 -196
- data/lib/action_dispatch/journey/scanner.rb +44 -42
- data/lib/action_dispatch/middleware/cookies.rb +4 -2
- data/lib/action_dispatch/middleware/debug_exceptions.rb +19 -4
- data/lib/action_dispatch/middleware/debug_view.rb +0 -5
- data/lib/action_dispatch/middleware/exception_wrapper.rb +3 -9
- data/lib/action_dispatch/middleware/executor.rb +5 -2
- data/lib/action_dispatch/middleware/public_exceptions.rb +5 -1
- data/lib/action_dispatch/middleware/request_id.rb +2 -1
- data/lib/action_dispatch/middleware/ssl.rb +13 -3
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +0 -3
- data/lib/action_dispatch/railtie.rb +8 -0
- data/lib/action_dispatch/request/session.rb +1 -0
- data/lib/action_dispatch/request/utils.rb +9 -3
- data/lib/action_dispatch/routing/inspector.rb +1 -1
- data/lib/action_dispatch/routing/mapper.rb +96 -67
- data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
- data/lib/action_dispatch/routing/route_set.rb +21 -10
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
- data/lib/action_dispatch/system_testing/browser.rb +12 -21
- data/lib/action_dispatch/testing/assertion_response.rb +1 -1
- data/lib/action_dispatch/testing/assertions/response.rb +12 -2
- data/lib/action_dispatch/testing/assertions/routing.rb +16 -12
- data/lib/action_dispatch/testing/integration.rb +20 -10
- data/lib/action_dispatch/testing/request_encoder.rb +9 -9
- data/lib/action_dispatch/testing/test_process.rb +1 -2
- data/lib/action_dispatch.rb +6 -4
- data/lib/action_pack/gem_version.rb +4 -4
- metadata +16 -38
- data/lib/action_dispatch/journey/parser.y +0 -50
- 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.
|
|
39
|
-
# supplied,
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
58
|
+
return false unless last_modified || etag
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
60
|
-
#
|
|
61
|
-
#
|
|
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
|
|
240
|
-
#
|
|
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
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
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
|
|
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
|
-
@
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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)
|