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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +408 -95
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +4 -2
  5. data/lib/abstract_controller/base.rb +12 -17
  6. data/lib/abstract_controller/caching.rb +6 -3
  7. data/lib/abstract_controller/callbacks.rb +6 -0
  8. data/lib/abstract_controller/collector.rb +1 -1
  9. data/lib/abstract_controller/helpers.rb +1 -1
  10. data/lib/abstract_controller/logger.rb +2 -1
  11. data/lib/abstract_controller/rendering.rb +0 -1
  12. data/lib/action_controller/api.rb +1 -0
  13. data/lib/action_controller/base.rb +3 -2
  14. data/lib/action_controller/caching.rb +1 -2
  15. data/lib/action_controller/form_builder.rb +4 -4
  16. data/lib/action_controller/log_subscriber.rb +22 -3
  17. data/lib/action_controller/metal/allow_browser.rb +12 -2
  18. data/lib/action_controller/metal/conditional_get.rb +30 -1
  19. data/lib/action_controller/metal/data_streaming.rb +5 -5
  20. data/lib/action_controller/metal/exceptions.rb +5 -0
  21. data/lib/action_controller/metal/flash.rb +1 -4
  22. data/lib/action_controller/metal/head.rb +3 -1
  23. data/lib/action_controller/metal/instrumentation.rb +1 -2
  24. data/lib/action_controller/metal/live.rb +66 -26
  25. data/lib/action_controller/metal/params_wrapper.rb +3 -3
  26. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  27. data/lib/action_controller/metal/rate_limiting.rb +39 -9
  28. data/lib/action_controller/metal/redirecting.rb +109 -16
  29. data/lib/action_controller/metal/renderers.rb +29 -9
  30. data/lib/action_controller/metal/rendering.rb +8 -2
  31. data/lib/action_controller/metal/request_forgery_protection.rb +21 -11
  32. data/lib/action_controller/metal/rescue.rb +9 -0
  33. data/lib/action_controller/metal/streaming.rb +5 -84
  34. data/lib/action_controller/metal/strong_parameters.rb +277 -92
  35. data/lib/action_controller/railtie.rb +33 -15
  36. data/lib/action_controller/renderer.rb +0 -1
  37. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  38. data/lib/action_controller/test_case.rb +12 -2
  39. data/lib/action_dispatch/constants.rb +6 -0
  40. data/lib/action_dispatch/http/cache.rb +138 -11
  41. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  42. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  43. data/lib/action_dispatch/http/mime_negotiation.rb +63 -4
  44. data/lib/action_dispatch/http/mime_types.rb +1 -0
  45. data/lib/action_dispatch/http/param_builder.rb +187 -0
  46. data/lib/action_dispatch/http/param_error.rb +26 -0
  47. data/lib/action_dispatch/http/parameters.rb +3 -3
  48. data/lib/action_dispatch/http/permissions_policy.rb +6 -0
  49. data/lib/action_dispatch/http/query_parser.rb +55 -0
  50. data/lib/action_dispatch/http/request.rb +73 -23
  51. data/lib/action_dispatch/http/response.rb +65 -17
  52. data/lib/action_dispatch/http/url.rb +112 -16
  53. data/lib/action_dispatch/journey/formatter.rb +8 -3
  54. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  55. data/lib/action_dispatch/journey/gtg/transition_table.rb +37 -45
  56. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  57. data/lib/action_dispatch/journey/parser.rb +99 -196
  58. data/lib/action_dispatch/journey/route.rb +45 -31
  59. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  60. data/lib/action_dispatch/journey/router.rb +59 -81
  61. data/lib/action_dispatch/journey/routes.rb +7 -0
  62. data/lib/action_dispatch/journey/scanner.rb +44 -42
  63. data/lib/action_dispatch/journey/visitors.rb +55 -23
  64. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  65. data/lib/action_dispatch/log_subscriber.rb +7 -3
  66. data/lib/action_dispatch/middleware/cookies.rb +8 -4
  67. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -5
  68. data/lib/action_dispatch/middleware/debug_view.rb +11 -5
  69. data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -14
  70. data/lib/action_dispatch/middleware/executor.rb +17 -4
  71. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
  72. data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
  73. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  74. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  75. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  76. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  77. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  78. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  79. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  80. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  84. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  86. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  88. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  89. data/lib/action_dispatch/railtie.rb +21 -0
  90. data/lib/action_dispatch/request/session.rb +1 -0
  91. data/lib/action_dispatch/request/utils.rb +9 -3
  92. data/lib/action_dispatch/routing/inspector.rb +80 -57
  93. data/lib/action_dispatch/routing/mapper.rb +409 -228
  94. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  95. data/lib/action_dispatch/routing/redirection.rb +10 -7
  96. data/lib/action_dispatch/routing/route_set.rb +21 -12
  97. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  98. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  99. data/lib/action_dispatch/system_test_case.rb +3 -3
  100. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  101. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  102. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  103. data/lib/action_dispatch/testing/assertions/response.rb +26 -2
  104. data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
  105. data/lib/action_dispatch/testing/integration.rb +16 -7
  106. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  107. data/lib/action_dispatch/testing/test_process.rb +1 -2
  108. data/lib/action_dispatch.rb +14 -4
  109. data/lib/action_pack/gem_version.rb +3 -3
  110. metadata +19 -38
  111. data/lib/action_dispatch/journey/parser.y +0 -50
  112. 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
- # Requests that exceed the rate limit are refused with a `429 Too Many Requests`
22
- # response. You can specialize this by passing a callable in the `with:`
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: -> { redirect_to busy_controller_url, alert: "Too many signups on domain!" }, only: :new
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
- def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, **options)
48
- before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store) }, **options
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
- count = store.increment("rate-limit:#{controller_path}:#{instance_exec(&by)}", 1, expires_in: within)
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", request: request) do
57
- instance_exec(&with)
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. Note: this was a new default in Rails
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
- # Raises UnsafeRedirectError in the case of an unsafe redirect.
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) { _allow_other_host }
154
+ allow_other_host = response_options.delete(:allow_other_host)
108
155
 
109
- self.status = _extract_redirect_to_status(options, response_options)
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
- !raise_on_open_redirects
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
- Rack::Utils.status_code(options.delete(:status))
268
+ ActionDispatch::Response.rack_status_code(options.delete(:status))
215
269
  elsif response_options.key?(:status)
216
- Rack::Utils.status_code(response_options[:status])
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 UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
300
+ raise OpenRedirectError.new(location)
227
301
  end
228
302
  end
229
303
 
230
304
  def _url_host_allowed?(url)
231
- host = URI(url.to_s).host
305
+ url_to_s = url.to_s
306
+ host = URI(url_to_s).host
232
307
 
233
- return true if host == request.host
234
- return false unless host.nil?
235
- return false unless url.to_s.start_with?("/")
236
- !url.to_s.start_with?("//")
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
- json = json.to_json(options) unless json.kind_of?(String)
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 = Mime[:js]
176
+ self.content_type = :js
162
177
  end
163
178
 
164
179
  "/**/#{options[:callback]}(#{json})"
165
180
  else
166
- self.content_type = Mime[:json] if media_type.nil?
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 = Mime[:js] if media_type.nil?
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 = Mime[:xml] if media_type.nil?
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 = Mime[:html].to_s
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] = Rack::Utils.status_code(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
- config_accessor :request_forgery_protection_token
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
- config_accessor :forgery_protection_strategy
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
- config_accessor :allow_forgery_protection
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
- config_accessor :log_warning_on_csrf_failure
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
- config_accessor :forgery_protection_origin_check
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
- config_accessor :per_form_csrf_tokens
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
- config_accessor :csrf_token_storage_strategy
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
- !protect_against_forgery? || request.get? || request.head? ||
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.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
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) # :doc:
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) # :doc:
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
- # Not all web servers support streaming out-of-the-box. You need to check the
169
- # instructions for each of them.
170
- #
171
- # #### Unicorn
172
- #
173
- # Unicorn supports streaming but it needs to be configured. For this, you need
174
- # to create a config file as follow:
175
- #
176
- # # unicorn.config.rb
177
- # listen 3000, tcp_nopush: false
178
- #
179
- # And use it on initialization:
180
- #
181
- # unicorn_rails --config-file unicorn.config.rb
182
- #
183
- # You may also want to configure other parameters like `:tcp_nodelay`.
184
- #
185
- # For more information, please check the
186
- # [documentation](https://bogomips.org/unicorn/Unicorn/Configurator.html#method-
187
- # i-listen).
188
- #
189
- # If you are using Unicorn with NGINX, you may need to tweak NGINX. Streaming
190
- # should work out of the box on Rainbows.
191
- #
192
- # #### Passenger
193
- #
194
- # Phusion Passenger with NGINX, offers two streaming mechanisms out of the box.
195
- #
196
- # 1. NGINX response buffering mechanism which is dependent on the value of
197
- # `passenger_buffer_response` option (default is "off").
198
- # 2. Passenger buffering system which is always 'on' irrespective of the value
199
- # of `passenger_buffer_response`.
200
- #
201
- #
202
- # When `passenger_buffer_response` is turned "on", then streaming would be done
203
- # at the NGINX level which waits until the application is done sending the
204
- # response back to the client.
205
- #
206
- # For more information, please check the [documentation]
207
- # (https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response).
168
+ # Rack 3+ compatible servers all support streaming.
208
169
  module Streaming
209
- class Body # :nodoc:
210
- TERM = "\r\n"
211
- TAIL = "0#{TERM}"
212
-
213
- # Store the response body to be chunked.
214
- def initialize(body)
215
- @body = body
216
- end
217
-
218
- # For each element yielded by the response body, yield the element in chunked
219
- # encoding.
220
- def each(&block)
221
- term = TERM
222
- @body.each do |chunk|
223
- size = chunk.bytesize
224
- next if size == 0
225
-
226
- yield [size.to_s(16), term, chunk.b, term].join
227
- end
228
- yield TAIL
229
- yield term
230
- end
231
-
232
- # Close the response body if the response body supports it.
233
- def close
234
- @body.close if @body.respond_to?(:close)
235
- end
236
- end
237
-
238
170
  private
239
- # Set proper cache control and transfer encoding when streaming
240
- def _process_options(options)
241
- super
242
- if options[:stream]
243
- if request.version == "HTTP/1.0"
244
- options.delete(:stream)
245
- else
246
- headers["Cache-Control"] ||= "no-cache"
247
- headers["Transfer-Encoding"] = "chunked"
248
- headers.delete("Content-Length")
249
- end
250
- end
251
- end
252
-
253
171
  # Call render_body if we are streaming instead of usual `render`.
254
172
  def _render_template(options)
255
173
  if options.delete(:stream)
256
- Body.new view_renderer.render_body(view_context, options)
174
+ # It shouldn't be necessary to set this.
175
+ headers["cache-control"] ||= "no-cache"
176
+
177
+ view_renderer.render_body(view_context, options)
257
178
  else
258
179
  super
259
180
  end