actionpack 7.2.2.1 → 8.1.2
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 +408 -95
- data/README.rdoc +1 -1
- data/lib/abstract_controller/asset_paths.rb +4 -2
- data/lib/abstract_controller/base.rb +12 -17
- data/lib/abstract_controller/caching.rb +6 -3
- data/lib/abstract_controller/callbacks.rb +6 -0
- data/lib/abstract_controller/collector.rb +1 -1
- data/lib/abstract_controller/helpers.rb +1 -1
- data/lib/abstract_controller/logger.rb +2 -1
- data/lib/abstract_controller/rendering.rb +0 -1
- data/lib/action_controller/api.rb +1 -0
- data/lib/action_controller/base.rb +3 -2
- data/lib/action_controller/caching.rb +1 -2
- data/lib/action_controller/form_builder.rb +4 -4
- data/lib/action_controller/log_subscriber.rb +22 -3
- data/lib/action_controller/metal/allow_browser.rb +12 -2
- data/lib/action_controller/metal/conditional_get.rb +30 -1
- data/lib/action_controller/metal/data_streaming.rb +5 -5
- data/lib/action_controller/metal/exceptions.rb +5 -0
- data/lib/action_controller/metal/flash.rb +1 -4
- data/lib/action_controller/metal/head.rb +3 -1
- data/lib/action_controller/metal/instrumentation.rb +1 -2
- data/lib/action_controller/metal/live.rb +66 -26
- data/lib/action_controller/metal/params_wrapper.rb +3 -3
- data/lib/action_controller/metal/permissions_policy.rb +9 -0
- data/lib/action_controller/metal/rate_limiting.rb +39 -9
- data/lib/action_controller/metal/redirecting.rb +109 -16
- data/lib/action_controller/metal/renderers.rb +29 -9
- data/lib/action_controller/metal/rendering.rb +8 -2
- data/lib/action_controller/metal/request_forgery_protection.rb +21 -11
- data/lib/action_controller/metal/rescue.rb +9 -0
- 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 +33 -15
- data/lib/action_controller/renderer.rb +0 -1
- data/lib/action_controller/structured_event_subscriber.rb +116 -0
- data/lib/action_controller/test_case.rb +12 -2
- data/lib/action_dispatch/constants.rb +6 -0
- data/lib/action_dispatch/http/cache.rb +138 -11
- data/lib/action_dispatch/http/content_security_policy.rb +14 -1
- data/lib/action_dispatch/http/filter_parameters.rb +5 -3
- data/lib/action_dispatch/http/mime_negotiation.rb +63 -4
- data/lib/action_dispatch/http/mime_types.rb +1 -0
- data/lib/action_dispatch/http/param_builder.rb +187 -0
- data/lib/action_dispatch/http/param_error.rb +26 -0
- data/lib/action_dispatch/http/parameters.rb +3 -3
- data/lib/action_dispatch/http/permissions_policy.rb +6 -0
- data/lib/action_dispatch/http/query_parser.rb +55 -0
- data/lib/action_dispatch/http/request.rb +73 -23
- data/lib/action_dispatch/http/response.rb +65 -17
- data/lib/action_dispatch/http/url.rb +112 -16
- data/lib/action_dispatch/journey/formatter.rb +8 -3
- data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
- data/lib/action_dispatch/journey/gtg/transition_table.rb +37 -45
- data/lib/action_dispatch/journey/nodes/node.rb +2 -1
- data/lib/action_dispatch/journey/parser.rb +99 -196
- data/lib/action_dispatch/journey/route.rb +45 -31
- data/lib/action_dispatch/journey/router/utils.rb +8 -14
- data/lib/action_dispatch/journey/router.rb +59 -81
- data/lib/action_dispatch/journey/routes.rb +7 -0
- data/lib/action_dispatch/journey/scanner.rb +44 -42
- data/lib/action_dispatch/journey/visitors.rb +55 -23
- data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
- data/lib/action_dispatch/log_subscriber.rb +7 -3
- data/lib/action_dispatch/middleware/cookies.rb +8 -4
- data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -5
- data/lib/action_dispatch/middleware/debug_view.rb +11 -5
- data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -14
- data/lib/action_dispatch/middleware/executor.rb +17 -4
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
- data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
- data/lib/action_dispatch/middleware/request_id.rb +2 -1
- data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
- data/lib/action_dispatch/middleware/ssl.rb +13 -3
- data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
- data/lib/action_dispatch/railtie.rb +21 -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 +80 -57
- data/lib/action_dispatch/routing/mapper.rb +409 -228
- data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
- data/lib/action_dispatch/routing/redirection.rb +10 -7
- data/lib/action_dispatch/routing/route_set.rb +21 -12
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
- data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
- data/lib/action_dispatch/system_test_case.rb +3 -3
- data/lib/action_dispatch/system_testing/browser.rb +12 -21
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
- data/lib/action_dispatch/testing/assertion_response.rb +1 -1
- data/lib/action_dispatch/testing/assertions/response.rb +26 -2
- data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
- data/lib/action_dispatch/testing/integration.rb +16 -7
- 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 +14 -4
- data/lib/action_pack/gem_version.rb +3 -3
- metadata +19 -38
- data/lib/action_dispatch/journey/parser.y +0 -50
- data/lib/action_dispatch/journey/parser_extras.rb +0 -33
|
@@ -24,8 +24,17 @@ module ActionController # :nodoc:
|
|
|
24
24
|
# end
|
|
25
25
|
# end
|
|
26
26
|
#
|
|
27
|
+
# Requires a global policy defined in an initializer, which can be
|
|
28
|
+
# empty:
|
|
29
|
+
#
|
|
30
|
+
# Rails.application.config.permissions_policy do |policy|
|
|
31
|
+
# # policy.gyroscope :none
|
|
32
|
+
# end
|
|
27
33
|
def permissions_policy(**options, &block)
|
|
28
34
|
before_action(options) do
|
|
35
|
+
unless request.respond_to?(:permissions_policy)
|
|
36
|
+
raise "Cannot override permissions_policy if no global permissions_policy configured."
|
|
37
|
+
end
|
|
29
38
|
if block_given?
|
|
30
39
|
policy = request.permissions_policy.clone
|
|
31
40
|
instance_exec(policy, &block)
|
|
@@ -18,8 +18,13 @@ module ActionController # :nodoc:
|
|
|
18
18
|
# parameter. It's evaluated within the context of the controller processing the
|
|
19
19
|
# request.
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
21
|
+
# By default, rate limits are scoped to the controller's path. If you want to
|
|
22
|
+
# share rate limits across multiple controllers, you can provide your own scope,
|
|
23
|
+
# by passing value in the `scope:` parameter.
|
|
24
|
+
#
|
|
25
|
+
# Requests that exceed the rate limit will raise an `ActionController::TooManyRequests`
|
|
26
|
+
# error. By default, Action Dispatch will rescue from the error and refuse the request
|
|
27
|
+
# with a `429 Too Many Requests` response. You can specialize this by passing a callable in the `with:`
|
|
23
28
|
# parameter. It's evaluated within the context of the controller processing the
|
|
24
29
|
# request.
|
|
25
30
|
#
|
|
@@ -29,6 +34,9 @@ module ActionController # :nodoc:
|
|
|
29
34
|
# datastore as your general caches, you can pass a custom store in the `store`
|
|
30
35
|
# parameter.
|
|
31
36
|
#
|
|
37
|
+
# If you want to use multiple rate limits per controller, you need to give each of
|
|
38
|
+
# them an explicit name via the `name:` option.
|
|
39
|
+
#
|
|
32
40
|
# Examples:
|
|
33
41
|
#
|
|
34
42
|
# class SessionsController < ApplicationController
|
|
@@ -37,24 +45,46 @@ module ActionController # :nodoc:
|
|
|
37
45
|
#
|
|
38
46
|
# class SignupsController < ApplicationController
|
|
39
47
|
# rate_limit to: 1000, within: 10.seconds,
|
|
40
|
-
# by: -> { request.domain }, with:
|
|
48
|
+
# by: -> { request.domain }, with: :redirect_to_busy, only: :new
|
|
49
|
+
#
|
|
50
|
+
# private
|
|
51
|
+
# def redirect_to_busy
|
|
52
|
+
# redirect_to busy_controller_url, alert: "Too many signups on domain!"
|
|
53
|
+
# end
|
|
41
54
|
# end
|
|
42
55
|
#
|
|
43
56
|
# class APIController < ApplicationController
|
|
44
57
|
# RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
|
|
45
58
|
# rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
|
|
59
|
+
# rate_limit to: 100, within: 5.minutes, scope: :api_global
|
|
46
60
|
# end
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
#
|
|
62
|
+
# class SessionsController < ApplicationController
|
|
63
|
+
# rate_limit to: 3, within: 2.seconds, name: "short-term"
|
|
64
|
+
# rate_limit to: 10, within: 5.minutes, name: "long-term"
|
|
65
|
+
# end
|
|
66
|
+
def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { raise TooManyRequests }, store: cache_store, name: nil, scope: nil, **options)
|
|
67
|
+
before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store, name: name, scope: scope || controller_path) }, **options
|
|
49
68
|
end
|
|
50
69
|
end
|
|
51
70
|
|
|
52
71
|
private
|
|
53
|
-
def rate_limiting(to:, within:, by:, with:, store:)
|
|
54
|
-
|
|
72
|
+
def rate_limiting(to:, within:, by:, with:, store:, name:, scope:)
|
|
73
|
+
by = by.is_a?(Symbol) ? send(by) : instance_exec(&by)
|
|
74
|
+
|
|
75
|
+
cache_key = ["rate-limit", scope, name, by].compact.join(":")
|
|
76
|
+
count = store.increment(cache_key, 1, expires_in: within)
|
|
55
77
|
if count && count > to
|
|
56
|
-
ActiveSupport::Notifications.instrument("rate_limit.action_controller",
|
|
57
|
-
|
|
78
|
+
ActiveSupport::Notifications.instrument("rate_limit.action_controller",
|
|
79
|
+
request: request,
|
|
80
|
+
count: count,
|
|
81
|
+
to: to,
|
|
82
|
+
within: within,
|
|
83
|
+
by: by,
|
|
84
|
+
name: name,
|
|
85
|
+
scope: scope,
|
|
86
|
+
cache_key: cache_key) do
|
|
87
|
+
with.is_a?(Symbol) ? send(with) : instance_exec(&with)
|
|
58
88
|
end
|
|
59
89
|
end
|
|
60
90
|
end
|
|
@@ -11,10 +11,36 @@ module ActionController
|
|
|
11
11
|
|
|
12
12
|
class UnsafeRedirectError < StandardError; end
|
|
13
13
|
|
|
14
|
+
class OpenRedirectError < UnsafeRedirectError
|
|
15
|
+
def initialize(location)
|
|
16
|
+
super("Unsafe redirect to #{location.to_s.truncate(100).inspect}, pass allow_other_host: true to redirect anyway.")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class PathRelativeRedirectError < UnsafeRedirectError
|
|
21
|
+
def initialize(url)
|
|
22
|
+
super("Path relative URL redirect detected: #{url.inspect}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
14
26
|
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
|
15
27
|
|
|
16
28
|
included do
|
|
17
29
|
mattr_accessor :raise_on_open_redirects, default: false
|
|
30
|
+
mattr_accessor :action_on_open_redirect, default: :log
|
|
31
|
+
mattr_accessor :action_on_path_relative_redirect, default: :log
|
|
32
|
+
class_attribute :_allowed_redirect_hosts, :allowed_redirect_hosts_permissions, instance_accessor: false, instance_predicate: false
|
|
33
|
+
singleton_class.alias_method :allowed_redirect_hosts, :_allowed_redirect_hosts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ClassMethods # :nodoc:
|
|
37
|
+
def allowed_redirect_hosts=(hosts)
|
|
38
|
+
hosts = hosts.dup.freeze
|
|
39
|
+
self._allowed_redirect_hosts = hosts
|
|
40
|
+
self.allowed_redirect_hosts_permissions = if hosts.present?
|
|
41
|
+
ActionDispatch::HostAuthorization::Permissions.new(hosts)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
18
44
|
end
|
|
19
45
|
|
|
20
46
|
# Redirects the browser to the target specified in `options`. This parameter can
|
|
@@ -82,16 +108,17 @@ module ActionController
|
|
|
82
108
|
# ### Open Redirect protection
|
|
83
109
|
#
|
|
84
110
|
# By default, Rails protects against redirecting to external hosts for your
|
|
85
|
-
# app's safety, so called open redirects.
|
|
86
|
-
# 7.0, after upgrading opt-in by uncommenting the line with
|
|
87
|
-
# `raise_on_open_redirects` in
|
|
88
|
-
# `config/initializers/new_framework_defaults_7_0.rb`
|
|
111
|
+
# app's safety, so called open redirects.
|
|
89
112
|
#
|
|
90
113
|
# Here #redirect_to automatically validates the potentially-unsafe URL:
|
|
91
114
|
#
|
|
92
115
|
# redirect_to params[:redirect_url]
|
|
93
116
|
#
|
|
94
|
-
#
|
|
117
|
+
# The `action_on_open_redirect` configuration option controls the behavior when an unsafe
|
|
118
|
+
# redirect is detected:
|
|
119
|
+
# * `:log` - Logs a warning but allows the redirect
|
|
120
|
+
# * `:notify` - Sends an Active Support notification for monitoring
|
|
121
|
+
# * `:raise` - Raises an UnsafeRedirectError
|
|
95
122
|
#
|
|
96
123
|
# To allow any external redirects pass `allow_other_host: true`, though using a
|
|
97
124
|
# user-provided param in that case is unsafe.
|
|
@@ -100,19 +127,40 @@ module ActionController
|
|
|
100
127
|
#
|
|
101
128
|
# See #url_from for more information on what an internal and safe URL is, or how
|
|
102
129
|
# to fall back to an alternate redirect URL in the unsafe case.
|
|
130
|
+
#
|
|
131
|
+
# ### Path Relative URL Redirect Protection
|
|
132
|
+
#
|
|
133
|
+
# Rails also protects against potentially unsafe path relative URL redirects that don't
|
|
134
|
+
# start with a leading slash. These can create security vulnerabilities:
|
|
135
|
+
#
|
|
136
|
+
# redirect_to "example.com" # Creates http://yourdomain.comexample.com
|
|
137
|
+
# redirect_to "@attacker.com" # Creates http://yourdomain.com@attacker.com
|
|
138
|
+
# # which browsers interpret as user@host
|
|
139
|
+
#
|
|
140
|
+
# You can configure how Rails handles these cases using:
|
|
141
|
+
#
|
|
142
|
+
# config.action_controller.action_on_path_relative_redirect = :log # default
|
|
143
|
+
# config.action_controller.action_on_path_relative_redirect = :notify
|
|
144
|
+
# config.action_controller.action_on_path_relative_redirect = :raise
|
|
145
|
+
#
|
|
146
|
+
# * `:log` - Logs a warning but allows the redirect
|
|
147
|
+
# * `:notify` - Sends an Active Support notification but allows the redirect
|
|
148
|
+
# (includes stack trace to help identify the source)
|
|
149
|
+
# * `:raise` - Raises an UnsafeRedirectError
|
|
103
150
|
def redirect_to(options = {}, response_options = {})
|
|
104
151
|
raise ActionControllerError.new("Cannot redirect to nil!") unless options
|
|
105
152
|
raise AbstractController::DoubleRenderError if response_body
|
|
106
153
|
|
|
107
|
-
allow_other_host = response_options.delete(:allow_other_host)
|
|
154
|
+
allow_other_host = response_options.delete(:allow_other_host)
|
|
108
155
|
|
|
109
|
-
|
|
156
|
+
proposed_status = _extract_redirect_to_status(options, response_options)
|
|
110
157
|
|
|
111
158
|
redirect_to_location = _compute_redirect_to_location(request, options)
|
|
112
159
|
_ensure_url_is_http_header_safe(redirect_to_location)
|
|
113
160
|
|
|
114
161
|
self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
|
|
115
162
|
self.response_body = ""
|
|
163
|
+
self.status = proposed_status
|
|
116
164
|
end
|
|
117
165
|
|
|
118
166
|
# Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
|
|
@@ -165,6 +213,10 @@ module ActionController
|
|
|
165
213
|
when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
|
|
166
214
|
options.to_str
|
|
167
215
|
when String
|
|
216
|
+
if !options.start_with?("/", "?") && !options.empty?
|
|
217
|
+
_handle_path_relative_redirect(options)
|
|
218
|
+
end
|
|
219
|
+
|
|
168
220
|
request.protocol + request.host_with_port + options
|
|
169
221
|
when Proc
|
|
170
222
|
_compute_redirect_to_location request, instance_eval(&options)
|
|
@@ -206,34 +258,58 @@ module ActionController
|
|
|
206
258
|
|
|
207
259
|
private
|
|
208
260
|
def _allow_other_host
|
|
209
|
-
|
|
261
|
+
return false if raise_on_open_redirects
|
|
262
|
+
|
|
263
|
+
action_on_open_redirect != :raise
|
|
210
264
|
end
|
|
211
265
|
|
|
212
266
|
def _extract_redirect_to_status(options, response_options)
|
|
213
267
|
if options.is_a?(Hash) && options.key?(:status)
|
|
214
|
-
|
|
268
|
+
ActionDispatch::Response.rack_status_code(options.delete(:status))
|
|
215
269
|
elsif response_options.key?(:status)
|
|
216
|
-
|
|
270
|
+
ActionDispatch::Response.rack_status_code(response_options[:status])
|
|
217
271
|
else
|
|
218
272
|
302
|
|
219
273
|
end
|
|
220
274
|
end
|
|
221
275
|
|
|
222
276
|
def _enforce_open_redirect_protection(location, allow_other_host:)
|
|
277
|
+
# Explictly allowed other host or host is in allow list allow redirect
|
|
223
278
|
if allow_other_host || _url_host_allowed?(location)
|
|
224
279
|
location
|
|
280
|
+
# Explicitly disallowed other host
|
|
281
|
+
elsif allow_other_host == false
|
|
282
|
+
raise OpenRedirectError.new(location)
|
|
283
|
+
# Configuration disallows other hosts
|
|
284
|
+
elsif !_allow_other_host
|
|
285
|
+
raise OpenRedirectError.new(location)
|
|
286
|
+
# Log but allow redirect
|
|
287
|
+
elsif action_on_open_redirect == :log
|
|
288
|
+
logger.warn "Open redirect to #{location.inspect} detected" if logger
|
|
289
|
+
location
|
|
290
|
+
# Notify but allow redirect
|
|
291
|
+
elsif action_on_open_redirect == :notify
|
|
292
|
+
ActiveSupport::Notifications.instrument("open_redirect.action_controller",
|
|
293
|
+
location: location,
|
|
294
|
+
request: request,
|
|
295
|
+
stack_trace: caller,
|
|
296
|
+
)
|
|
297
|
+
location
|
|
298
|
+
# Fall through, should not happen but raise for safety
|
|
225
299
|
else
|
|
226
|
-
raise
|
|
300
|
+
raise OpenRedirectError.new(location)
|
|
227
301
|
end
|
|
228
302
|
end
|
|
229
303
|
|
|
230
304
|
def _url_host_allowed?(url)
|
|
231
|
-
|
|
305
|
+
url_to_s = url.to_s
|
|
306
|
+
host = URI(url_to_s).host
|
|
232
307
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
308
|
+
if host.nil?
|
|
309
|
+
url_to_s.start_with?("/") && !url_to_s.start_with?("//")
|
|
310
|
+
else
|
|
311
|
+
host == request.host || self.class.allowed_redirect_hosts_permissions&.allows?(host)
|
|
312
|
+
end
|
|
237
313
|
rescue ArgumentError, URI::Error
|
|
238
314
|
false
|
|
239
315
|
end
|
|
@@ -247,5 +323,22 @@ module ActionController
|
|
|
247
323
|
raise UnsafeRedirectError, msg
|
|
248
324
|
end
|
|
249
325
|
end
|
|
326
|
+
|
|
327
|
+
def _handle_path_relative_redirect(url)
|
|
328
|
+
message = "Path relative URL redirect detected: #{url.inspect}"
|
|
329
|
+
|
|
330
|
+
case action_on_path_relative_redirect
|
|
331
|
+
when :log
|
|
332
|
+
logger&.warn message
|
|
333
|
+
when :notify
|
|
334
|
+
ActiveSupport::Notifications.instrument("unsafe_redirect.action_controller",
|
|
335
|
+
url: url,
|
|
336
|
+
message: message,
|
|
337
|
+
stack_trace: caller
|
|
338
|
+
)
|
|
339
|
+
when :raise
|
|
340
|
+
raise PathRelativeRedirectError.new(url)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
250
343
|
end
|
|
251
344
|
end
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# :markup: markdown
|
|
4
4
|
|
|
5
|
-
require "set"
|
|
6
|
-
|
|
7
5
|
module ActionController
|
|
8
6
|
# See Renderers.add
|
|
9
7
|
def self.add_renderer(key, &block)
|
|
@@ -29,8 +27,23 @@ module ActionController
|
|
|
29
27
|
# Default values are `:json`, `:js`, `:xml`.
|
|
30
28
|
RENDERERS = Set.new
|
|
31
29
|
|
|
30
|
+
module DeprecatedEscapeJsonResponses # :nodoc:
|
|
31
|
+
def escape_json_responses=(value)
|
|
32
|
+
if value
|
|
33
|
+
ActionController.deprecator.warn(<<~MSG.squish)
|
|
34
|
+
Setting action_controller.escape_json_responses = true is deprecated and will have no effect in Rails 8.2.
|
|
35
|
+
Set it to `false`, or remove the config.
|
|
36
|
+
MSG
|
|
37
|
+
end
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
included do
|
|
33
43
|
class_attribute :_renderers, default: Set.new.freeze
|
|
44
|
+
class_attribute :escape_json_responses, instance_writer: false, instance_accessor: false, default: true
|
|
45
|
+
|
|
46
|
+
singleton_class.prepend DeprecatedEscapeJsonResponses
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
# Used in ActionController::Base and ActionController::API to include all
|
|
@@ -88,7 +101,7 @@ module ActionController
|
|
|
88
101
|
remove_possible_method(method_name)
|
|
89
102
|
end
|
|
90
103
|
|
|
91
|
-
def self._render_with_renderer_method_name(key)
|
|
104
|
+
def self._render_with_renderer_method_name(key) # :nodoc:
|
|
92
105
|
"_render_with_renderer_#{key}"
|
|
93
106
|
end
|
|
94
107
|
|
|
@@ -142,7 +155,7 @@ module ActionController
|
|
|
142
155
|
_render_to_body_with_renderer(options) || super
|
|
143
156
|
end
|
|
144
157
|
|
|
145
|
-
def _render_to_body_with_renderer(options)
|
|
158
|
+
def _render_to_body_with_renderer(options) # :nodoc:
|
|
146
159
|
_renderers.each do |name|
|
|
147
160
|
if options.key?(name)
|
|
148
161
|
_process_options(options)
|
|
@@ -154,28 +167,35 @@ module ActionController
|
|
|
154
167
|
end
|
|
155
168
|
|
|
156
169
|
add :json do |json, options|
|
|
157
|
-
|
|
170
|
+
json_options = options.except(:callback, :content_type, :status)
|
|
171
|
+
json_options[:escape] ||= false if !self.class.escape_json_responses? && options[:callback].blank?
|
|
172
|
+
json = json.to_json(json_options) unless json.kind_of?(String)
|
|
158
173
|
|
|
159
174
|
if options[:callback].present?
|
|
160
175
|
if media_type.nil? || media_type == Mime[:json]
|
|
161
|
-
self.content_type =
|
|
176
|
+
self.content_type = :js
|
|
162
177
|
end
|
|
163
178
|
|
|
164
179
|
"/**/#{options[:callback]}(#{json})"
|
|
165
180
|
else
|
|
166
|
-
self.content_type =
|
|
181
|
+
self.content_type = :json if media_type.nil?
|
|
167
182
|
json
|
|
168
183
|
end
|
|
169
184
|
end
|
|
170
185
|
|
|
171
186
|
add :js do |js, options|
|
|
172
|
-
self.content_type =
|
|
187
|
+
self.content_type = :js if media_type.nil?
|
|
173
188
|
js.respond_to?(:to_js) ? js.to_js(options) : js
|
|
174
189
|
end
|
|
175
190
|
|
|
176
191
|
add :xml do |xml, options|
|
|
177
|
-
self.content_type =
|
|
192
|
+
self.content_type = :xml if media_type.nil?
|
|
178
193
|
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
|
|
179
194
|
end
|
|
195
|
+
|
|
196
|
+
add :markdown do |md, options|
|
|
197
|
+
self.content_type = :md if media_type.nil?
|
|
198
|
+
md.respond_to?(:to_markdown) ? md.to_markdown : md
|
|
199
|
+
end
|
|
180
200
|
end
|
|
181
201
|
end
|
|
@@ -160,6 +160,12 @@ module ActionController
|
|
|
160
160
|
# render "posts/new", status: :unprocessable_entity
|
|
161
161
|
# # => renders app/views/posts/new.html.erb with HTTP status code 422
|
|
162
162
|
#
|
|
163
|
+
# `:variants`
|
|
164
|
+
# : This tells Rails to look for the first template matching any of the variations.
|
|
165
|
+
#
|
|
166
|
+
# render "posts/index", variants: [:mobile]
|
|
167
|
+
# # => renders app/views/posts/index.html+mobile.erb
|
|
168
|
+
#
|
|
163
169
|
#--
|
|
164
170
|
# Check for double render errors and set the content_type after rendering.
|
|
165
171
|
def render(*args)
|
|
@@ -208,7 +214,7 @@ module ActionController
|
|
|
208
214
|
end
|
|
209
215
|
|
|
210
216
|
def _set_html_content_type
|
|
211
|
-
self.content_type =
|
|
217
|
+
self.content_type = :html
|
|
212
218
|
end
|
|
213
219
|
|
|
214
220
|
def _set_rendered_content_type(format)
|
|
@@ -232,7 +238,7 @@ module ActionController
|
|
|
232
238
|
end
|
|
233
239
|
|
|
234
240
|
if options[:status]
|
|
235
|
-
options[:status] =
|
|
241
|
+
options[:status] = ActionDispatch::Response.rack_status_code(options[:status])
|
|
236
242
|
end
|
|
237
243
|
|
|
238
244
|
super
|
|
@@ -71,32 +71,39 @@ module ActionController # :nodoc:
|
|
|
71
71
|
included do
|
|
72
72
|
# Sets the token parameter name for RequestForgery. Calling
|
|
73
73
|
# `protect_from_forgery` sets it to `:authenticity_token` by default.
|
|
74
|
-
|
|
74
|
+
singleton_class.delegate :request_forgery_protection_token, :request_forgery_protection_token=, to: :config
|
|
75
|
+
delegate :request_forgery_protection_token, :request_forgery_protection_token=, to: :config
|
|
75
76
|
self.request_forgery_protection_token ||= :authenticity_token
|
|
76
77
|
|
|
77
78
|
# Holds the class which implements the request forgery protection.
|
|
78
|
-
|
|
79
|
+
singleton_class.delegate :forgery_protection_strategy, :forgery_protection_strategy=, to: :config
|
|
80
|
+
delegate :forgery_protection_strategy, :forgery_protection_strategy=, to: :config
|
|
79
81
|
self.forgery_protection_strategy = nil
|
|
80
82
|
|
|
81
83
|
# Controls whether request forgery protection is turned on or not. Turned off by
|
|
82
84
|
# default only in test mode.
|
|
83
|
-
|
|
85
|
+
singleton_class.delegate :allow_forgery_protection, :allow_forgery_protection=, to: :config
|
|
86
|
+
delegate :allow_forgery_protection, :allow_forgery_protection=, to: :config
|
|
84
87
|
self.allow_forgery_protection = true if allow_forgery_protection.nil?
|
|
85
88
|
|
|
86
89
|
# Controls whether a CSRF failure logs a warning. On by default.
|
|
87
|
-
|
|
90
|
+
singleton_class.delegate :log_warning_on_csrf_failure, :log_warning_on_csrf_failure=, to: :config
|
|
91
|
+
delegate :log_warning_on_csrf_failure, :log_warning_on_csrf_failure=, to: :config
|
|
88
92
|
self.log_warning_on_csrf_failure = true
|
|
89
93
|
|
|
90
94
|
# Controls whether the Origin header is checked in addition to the CSRF token.
|
|
91
|
-
|
|
95
|
+
singleton_class.delegate :forgery_protection_origin_check, :forgery_protection_origin_check=, to: :config
|
|
96
|
+
delegate :forgery_protection_origin_check, :forgery_protection_origin_check=, to: :config
|
|
92
97
|
self.forgery_protection_origin_check = false
|
|
93
98
|
|
|
94
99
|
# Controls whether form-action/method specific CSRF tokens are used.
|
|
95
|
-
|
|
100
|
+
singleton_class.delegate :per_form_csrf_tokens, :per_form_csrf_tokens=, to: :config
|
|
101
|
+
delegate :per_form_csrf_tokens, :per_form_csrf_tokens=, to: :config
|
|
96
102
|
self.per_form_csrf_tokens = false
|
|
97
103
|
|
|
98
104
|
# The strategy to use for storing and retrieving CSRF tokens.
|
|
99
|
-
|
|
105
|
+
singleton_class.delegate :csrf_token_storage_strategy, :csrf_token_storage_strategy=, to: :config
|
|
106
|
+
delegate :csrf_token_storage_strategy, :csrf_token_storage_strategy=, to: :config
|
|
100
107
|
self.csrf_token_storage_strategy = SessionStore.new
|
|
101
108
|
|
|
102
109
|
helper_method :form_authenticity_token
|
|
@@ -142,6 +149,7 @@ module ActionController # :nodoc:
|
|
|
142
149
|
#
|
|
143
150
|
#
|
|
144
151
|
# Built-in unverified request handling methods are:
|
|
152
|
+
#
|
|
145
153
|
# * `:exception` - Raises ActionController::InvalidAuthenticityToken
|
|
146
154
|
# exception.
|
|
147
155
|
# * `:reset_session` - Resets the session.
|
|
@@ -170,6 +178,7 @@ module ActionController # :nodoc:
|
|
|
170
178
|
#
|
|
171
179
|
#
|
|
172
180
|
# Built-in session token strategies are:
|
|
181
|
+
#
|
|
173
182
|
# * `:session` - Store the CSRF token in the session. Used as default if
|
|
174
183
|
# `:store` option is not specified.
|
|
175
184
|
# * `:cookie` - Store the CSRF token in an encrypted cookie.
|
|
@@ -459,7 +468,7 @@ module ActionController # :nodoc:
|
|
|
459
468
|
# * Does the `X-CSRF-Token` header match the form_authenticity_token?
|
|
460
469
|
#
|
|
461
470
|
def verified_request? # :doc:
|
|
462
|
-
|
|
471
|
+
request.get? || request.head? || !protect_against_forgery? ||
|
|
463
472
|
(valid_request_origin? && any_authenticity_token_valid?)
|
|
464
473
|
end
|
|
465
474
|
|
|
@@ -498,7 +507,7 @@ module ActionController # :nodoc:
|
|
|
498
507
|
# Checks the client's masked token to see if it matches the session token.
|
|
499
508
|
# Essentially the inverse of `masked_authenticity_token`.
|
|
500
509
|
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
|
|
501
|
-
if encoded_masked_token.
|
|
510
|
+
if !encoded_masked_token.is_a?(String) || encoded_masked_token.empty?
|
|
502
511
|
return false
|
|
503
512
|
end
|
|
504
513
|
|
|
@@ -619,6 +628,7 @@ module ActionController # :nodoc:
|
|
|
619
628
|
If you cannot change the referrer policy, you can disable origin checking with the
|
|
620
629
|
Rails.application.config.action_controller.forgery_protection_origin_check setting.
|
|
621
630
|
MSG
|
|
631
|
+
private_constant :NULL_ORIGIN_MESSAGE
|
|
622
632
|
|
|
623
633
|
# Checks if the request originated from the same origin by looking at the Origin
|
|
624
634
|
# header.
|
|
@@ -632,7 +642,7 @@ module ActionController # :nodoc:
|
|
|
632
642
|
end
|
|
633
643
|
end
|
|
634
644
|
|
|
635
|
-
def normalize_action_path(action_path)
|
|
645
|
+
def normalize_action_path(action_path)
|
|
636
646
|
uri = URI.parse(action_path)
|
|
637
647
|
|
|
638
648
|
if uri.relative? && (action_path.blank? || !action_path.start_with?("/"))
|
|
@@ -642,7 +652,7 @@ module ActionController # :nodoc:
|
|
|
642
652
|
end
|
|
643
653
|
end
|
|
644
654
|
|
|
645
|
-
def normalize_relative_action_path(rel_action_path)
|
|
655
|
+
def normalize_relative_action_path(rel_action_path)
|
|
646
656
|
uri = URI.parse(request.path)
|
|
647
657
|
# add the action path to the request.path
|
|
648
658
|
uri.path += "/#{rel_action_path}"
|
|
@@ -13,6 +13,15 @@ module ActionController # :nodoc:
|
|
|
13
13
|
extend ActiveSupport::Concern
|
|
14
14
|
include ActiveSupport::Rescuable
|
|
15
15
|
|
|
16
|
+
module ClassMethods
|
|
17
|
+
def handler_for_rescue(exception, ...) # :nodoc:
|
|
18
|
+
if handler = super
|
|
19
|
+
ActiveSupport::Notifications.instrument("rescue_from_callback.action_controller", exception: exception)
|
|
20
|
+
handler
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
16
25
|
# Override this method if you want to customize when detailed exceptions must be
|
|
17
26
|
# shown. This method is only called when `consider_all_requests_local` is
|
|
18
27
|
# `false`. By default, it returns `false`, but someone may set it to
|
|
@@ -165,95 +165,16 @@ module ActionController # :nodoc:
|
|
|
165
165
|
#
|
|
166
166
|
# ## Web server support
|
|
167
167
|
#
|
|
168
|
-
#
|
|
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
|
-
|
|
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
|