actionpack 8.0.3 → 8.1.0.beta1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +243 -168
  3. data/lib/abstract_controller/asset_paths.rb +4 -2
  4. data/lib/abstract_controller/base.rb +10 -2
  5. data/lib/abstract_controller/caching.rb +6 -3
  6. data/lib/abstract_controller/logger.rb +2 -1
  7. data/lib/action_controller/base.rb +1 -1
  8. data/lib/action_controller/caching.rb +1 -2
  9. data/lib/action_controller/form_builder.rb +1 -1
  10. data/lib/action_controller/log_subscriber.rb +7 -0
  11. data/lib/action_controller/metal/allow_browser.rb +1 -1
  12. data/lib/action_controller/metal/conditional_get.rb +25 -0
  13. data/lib/action_controller/metal/data_streaming.rb +1 -3
  14. data/lib/action_controller/metal/exceptions.rb +5 -0
  15. data/lib/action_controller/metal/flash.rb +1 -4
  16. data/lib/action_controller/metal/head.rb +3 -1
  17. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  18. data/lib/action_controller/metal/rate_limiting.rb +22 -7
  19. data/lib/action_controller/metal/redirecting.rb +61 -5
  20. data/lib/action_controller/metal/renderers.rb +27 -6
  21. data/lib/action_controller/metal/rendering.rb +7 -1
  22. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  23. data/lib/action_controller/metal/rescue.rb +9 -0
  24. data/lib/action_controller/railtie.rb +2 -6
  25. data/lib/action_dispatch/http/cache.rb +111 -1
  26. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  27. data/lib/action_dispatch/http/mime_types.rb +1 -0
  28. data/lib/action_dispatch/http/param_builder.rb +28 -27
  29. data/lib/action_dispatch/http/parameters.rb +3 -3
  30. data/lib/action_dispatch/http/permissions_policy.rb +4 -0
  31. data/lib/action_dispatch/http/query_parser.rb +12 -10
  32. data/lib/action_dispatch/http/request.rb +10 -5
  33. data/lib/action_dispatch/http/response.rb +16 -3
  34. data/lib/action_dispatch/http/url.rb +99 -3
  35. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  36. data/lib/action_dispatch/journey/gtg/transition_table.rb +33 -43
  37. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  38. data/lib/action_dispatch/journey/route.rb +45 -31
  39. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  40. data/lib/action_dispatch/journey/router.rb +59 -81
  41. data/lib/action_dispatch/journey/routes.rb +7 -0
  42. data/lib/action_dispatch/journey/visitors.rb +55 -23
  43. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  44. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  45. data/lib/action_dispatch/middleware/debug_exceptions.rb +7 -1
  46. data/lib/action_dispatch/middleware/debug_view.rb +11 -0
  47. data/lib/action_dispatch/middleware/exception_wrapper.rb +11 -5
  48. data/lib/action_dispatch/middleware/executor.rb +12 -2
  49. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -5
  50. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  51. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  52. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -2
  53. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  54. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  55. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  56. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +1 -0
  57. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  58. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  59. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  60. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  61. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  62. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  63. data/lib/action_dispatch/railtie.rb +10 -2
  64. data/lib/action_dispatch/routing/inspector.rb +4 -1
  65. data/lib/action_dispatch/routing/mapper.rb +323 -173
  66. data/lib/action_dispatch/routing/route_set.rb +2 -4
  67. data/lib/action_dispatch/routing/routes_proxy.rb +0 -1
  68. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  69. data/lib/action_dispatch/testing/assertions/response.rb +14 -0
  70. data/lib/action_dispatch/testing/assertions/routing.rb +11 -3
  71. data/lib/action_dispatch/testing/integration.rb +3 -2
  72. data/lib/action_pack/gem_version.rb +3 -3
  73. metadata +11 -10
@@ -40,7 +40,7 @@ module ActionController
40
40
  # rendered by this controller and its subclasses.
41
41
  #
42
42
  # #### Parameters
43
- # * `builder` - Default form builder, an instance of
43
+ # * `builder` - Default form builder. Accepts a subclass of
44
44
  # ActionView::Helpers::FormBuilder
45
45
  def default_form_builder(builder)
46
46
  self._default_form_builder = builder
@@ -49,6 +49,13 @@ module ActionController
49
49
  end
50
50
  subscribe_log_level :halted_callback, :info
51
51
 
52
+ # Manually subscribed below
53
+ def rescue_from_callback(event)
54
+ exception = event.payload[:exception]
55
+ info { "rescue_from handled #{exception.class} (#{exception.message}) - #{exception.backtrace.first.delete_prefix("#{Rails.root}/")}" }
56
+ end
57
+ subscribe_log_level :rescue_from_callback, :info
58
+
52
59
  def send_file(event)
53
60
  info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
54
61
  end
@@ -14,7 +14,7 @@ module ActionController # :nodoc:
14
14
  # aren't reporting a user-agent header, will be allowed access.
15
15
  #
16
16
  # A browser that's blocked will by default be served the file in
17
- # public/406-unsupported-browser.html with a HTTP status code of "406 Not
17
+ # public/406-unsupported-browser.html with an HTTP status code of "406 Not
18
18
  # Acceptable".
19
19
  #
20
20
  # In addition to specifically named browser versions, you can also pass
@@ -332,6 +332,31 @@ module ActionController
332
332
  response.cache_control.replace(no_store: true)
333
333
  end
334
334
 
335
+ # Adds the `must-understand` directive to the `Cache-Control` header, which indicates
336
+ # that a cache MUST understand the semantics of the response status code that has been
337
+ # received, or discard the response.
338
+ #
339
+ # This is particularly useful when returning responses with new or uncommon
340
+ # status codes that might not be properly interpreted by older caches.
341
+ #
342
+ # #### Example
343
+ #
344
+ # def show
345
+ # @article = Article.find(params[:id])
346
+ #
347
+ # if @article.early_access?
348
+ # must_understand
349
+ # render status: 203 # Non-Authoritative Information
350
+ # else
351
+ # fresh_when @article
352
+ # end
353
+ # end
354
+ #
355
+ def must_understand
356
+ response.cache_control[:must_understand] = true
357
+ response.cache_control[:no_store] = true
358
+ end
359
+
335
360
  private
336
361
  def combine_etags(validator, options)
337
362
  [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
@@ -134,9 +134,7 @@ module ActionController # :nodoc:
134
134
  raise ArgumentError, ":type option required" if content_type.nil?
135
135
 
136
136
  if content_type.is_a?(Symbol)
137
- extension = Mime[content_type]
138
- raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
139
- self.content_type = extension
137
+ self.content_type = content_type
140
138
  else
141
139
  if !type_provided && options[:filename]
142
140
  # If type wasn't provided, try guessing from file extension.
@@ -103,4 +103,9 @@ module ActionController
103
103
  super(message)
104
104
  end
105
105
  end
106
+
107
+ # Raised when a Rate Limit is exceeded by too many requests within a period of
108
+ # time.
109
+ class TooManyRequests < ActionControllerError
110
+ end
106
111
  end
@@ -38,15 +38,12 @@ module ActionController # :nodoc:
38
38
  define_method(type) do
39
39
  request.flash[type]
40
40
  end
41
+ private type
41
42
  helper_method(type) if respond_to?(:helper_method)
42
43
 
43
44
  self._flash_types += [type]
44
45
  end
45
46
  end
46
-
47
- def action_methods # :nodoc:
48
- @action_methods ||= super - _flash_types.map(&:to_s).to_set
49
- end
50
47
  end
51
48
 
52
49
  private
@@ -25,6 +25,8 @@ module ActionController
25
25
  raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
26
26
  end
27
27
 
28
+ raise ::AbstractController::DoubleRenderError if response_body
29
+
28
30
  status ||= :ok
29
31
 
30
32
  if options
@@ -41,7 +43,7 @@ module ActionController
41
43
 
42
44
  if include_content?(response_code)
43
45
  unless self.media_type
44
- self.content_type = content_type || ((f = formats) && Mime[f.first]) || Mime[:html]
46
+ self.content_type = content_type || ((f = formats) && Mime[f.first]) || :html
45
47
  end
46
48
 
47
49
  response.charset = false
@@ -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
  #
@@ -46,23 +51,33 @@ module ActionController # :nodoc:
46
51
  # class APIController < ApplicationController
47
52
  # RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
48
53
  # rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
54
+ # rate_limit to: 100, within: 5.minutes, scope: :api_global
49
55
  # end
50
56
  #
51
57
  # class SessionsController < ApplicationController
52
58
  # rate_limit to: 3, within: 2.seconds, name: "short-term"
53
59
  # rate_limit to: 10, within: 5.minutes, name: "long-term"
54
60
  # end
55
- def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, name: nil, **options)
56
- before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store, name: name) }, **options
61
+ def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { raise TooManyRequests }, store: cache_store, name: nil, scope: nil, **options)
62
+ before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store, name: name, scope: scope || controller_path) }, **options
57
63
  end
58
64
  end
59
65
 
60
66
  private
61
- def rate_limiting(to:, within:, by:, with:, store:, name:)
62
- cache_key = ["rate-limit", controller_path, name, instance_exec(&by)].compact.join(":")
67
+ def rate_limiting(to:, within:, by:, with:, store:, name:, scope:)
68
+ by = instance_exec(&by)
69
+ cache_key = ["rate-limit", scope, name, by].compact.join(":")
63
70
  count = store.increment(cache_key, 1, expires_in: within)
64
71
  if count && count > to
65
- ActiveSupport::Notifications.instrument("rate_limit.action_controller", request: request) do
72
+ ActiveSupport::Notifications.instrument("rate_limit.action_controller",
73
+ request: request,
74
+ count: count,
75
+ to: to,
76
+ within: within,
77
+ by: by,
78
+ name: name,
79
+ scope: scope,
80
+ cache_key: cache_key) do
66
81
  instance_exec(&with)
67
82
  end
68
83
  end
@@ -15,6 +15,19 @@ module ActionController
15
15
 
16
16
  included do
17
17
  mattr_accessor :raise_on_open_redirects, default: false
18
+ mattr_accessor :action_on_path_relative_redirect, default: :log
19
+ class_attribute :_allowed_redirect_hosts, :allowed_redirect_hosts_permissions, instance_accessor: false, instance_predicate: false
20
+ singleton_class.alias_method :allowed_redirect_hosts, :_allowed_redirect_hosts
21
+ end
22
+
23
+ module ClassMethods # :nodoc:
24
+ def allowed_redirect_hosts=(hosts)
25
+ hosts = hosts.dup.freeze
26
+ self._allowed_redirect_hosts = hosts
27
+ self.allowed_redirect_hosts_permissions = if hosts.present?
28
+ ActionDispatch::HostAuthorization::Permissions.new(hosts)
29
+ end
30
+ end
18
31
  end
19
32
 
20
33
  # Redirects the browser to the target specified in `options`. This parameter can
@@ -100,6 +113,26 @@ module ActionController
100
113
  #
101
114
  # See #url_from for more information on what an internal and safe URL is, or how
102
115
  # to fall back to an alternate redirect URL in the unsafe case.
116
+ #
117
+ # ### Path Relative URL Redirect Protection
118
+ #
119
+ # Rails also protects against potentially unsafe path relative URL redirects that don't
120
+ # start with a leading slash. These can create security vulnerabilities:
121
+ #
122
+ # redirect_to "example.com" # Creates http://yourdomain.comexample.com
123
+ # redirect_to "@attacker.com" # Creates http://yourdomain.com@attacker.com
124
+ # # which browsers interpret as user@host
125
+ #
126
+ # You can configure how Rails handles these cases using:
127
+ #
128
+ # config.action_controller.action_on_path_relative_redirect = :log # default
129
+ # config.action_controller.action_on_path_relative_redirect = :notify
130
+ # config.action_controller.action_on_path_relative_redirect = :raise
131
+ #
132
+ # * `:log` - Logs a warning but allows the redirect
133
+ # * `:notify` - Sends an ActiveSupport notification but allows the redirect
134
+ # (includes stack trace to help identify the source)
135
+ # * `:raise` - Raises an UnsafeRedirectError
103
136
  def redirect_to(options = {}, response_options = {})
104
137
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
105
138
  raise AbstractController::DoubleRenderError if response_body
@@ -166,6 +199,10 @@ module ActionController
166
199
  when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
167
200
  options.to_str
168
201
  when String
202
+ if !options.start_with?("/", "?") && !options.empty?
203
+ _handle_path_relative_redirect(options)
204
+ end
205
+
169
206
  request.protocol + request.host_with_port + options
170
207
  when Proc
171
208
  _compute_redirect_to_location request, instance_eval(&options)
@@ -229,12 +266,14 @@ module ActionController
229
266
  end
230
267
 
231
268
  def _url_host_allowed?(url)
232
- host = URI(url.to_s).host
269
+ url_to_s = url.to_s
270
+ host = URI(url_to_s).host
233
271
 
234
- return true if host == request.host
235
- return false unless host.nil?
236
- return false unless url.to_s.start_with?("/")
237
- !url.to_s.start_with?("//")
272
+ if host.nil?
273
+ url_to_s.start_with?("/") && !url_to_s.start_with?("//")
274
+ else
275
+ host == request.host || self.class.allowed_redirect_hosts_permissions&.allows?(host)
276
+ end
238
277
  rescue ArgumentError, URI::Error
239
278
  false
240
279
  end
@@ -248,5 +287,22 @@ module ActionController
248
287
  raise UnsafeRedirectError, msg
249
288
  end
250
289
  end
290
+
291
+ def _handle_path_relative_redirect(url)
292
+ message = "Path relative URL redirect detected: #{url.inspect}"
293
+
294
+ case action_on_path_relative_redirect
295
+ when :log
296
+ logger&.warn message
297
+ when :notify
298
+ ActiveSupport::Notifications.instrument("unsafe_redirect.action_controller",
299
+ url: url,
300
+ message: message,
301
+ stack_trace: caller
302
+ )
303
+ when :raise
304
+ raise UnsafeRedirectError, message
305
+ end
306
+ end
251
307
  end
252
308
  end
@@ -27,8 +27,23 @@ module ActionController
27
27
  # Default values are `:json`, `:js`, `:xml`.
28
28
  RENDERERS = Set.new
29
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
+
30
42
  included do
31
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
32
47
  end
33
48
 
34
49
  # Used in ActionController::Base and ActionController::API to include all
@@ -86,7 +101,7 @@ module ActionController
86
101
  remove_possible_method(method_name)
87
102
  end
88
103
 
89
- def self._render_with_renderer_method_name(key)
104
+ def self._render_with_renderer_method_name(key) # :nodoc:
90
105
  "_render_with_renderer_#{key}"
91
106
  end
92
107
 
@@ -140,7 +155,7 @@ module ActionController
140
155
  _render_to_body_with_renderer(options) || super
141
156
  end
142
157
 
143
- def _render_to_body_with_renderer(options)
158
+ def _render_to_body_with_renderer(options) # :nodoc:
144
159
  _renderers.each do |name|
145
160
  if options.key?(name)
146
161
  _process_options(options)
@@ -153,28 +168,34 @@ module ActionController
153
168
 
154
169
  add :json do |json, options|
155
170
  json_options = options.except(:callback, :content_type, :status)
171
+ json_options[:escape] ||= false if !self.class.escape_json_responses? && options[:callback].blank?
156
172
  json = json.to_json(json_options) unless json.kind_of?(String)
157
173
 
158
174
  if options[:callback].present?
159
175
  if media_type.nil? || media_type == Mime[:json]
160
- self.content_type = Mime[:js]
176
+ self.content_type = :js
161
177
  end
162
178
 
163
179
  "/**/#{options[:callback]}(#{json})"
164
180
  else
165
- self.content_type = Mime[:json] if media_type.nil?
181
+ self.content_type = :json if media_type.nil?
166
182
  json
167
183
  end
168
184
  end
169
185
 
170
186
  add :js do |js, options|
171
- self.content_type = Mime[:js] if media_type.nil?
187
+ self.content_type = :js if media_type.nil?
172
188
  js.respond_to?(:to_js) ? js.to_js(options) : js
173
189
  end
174
190
 
175
191
  add :xml do |xml, options|
176
- self.content_type = Mime[:xml] if media_type.nil?
192
+ self.content_type = :xml if media_type.nil?
177
193
  xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
178
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
179
200
  end
180
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)
@@ -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
@@ -461,7 +468,7 @@ module ActionController # :nodoc:
461
468
  # * Does the `X-CSRF-Token` header match the form_authenticity_token?
462
469
  #
463
470
  def verified_request? # :doc:
464
- !protect_against_forgery? || request.get? || request.head? ||
471
+ request.get? || request.head? || !protect_against_forgery? ||
465
472
  (valid_request_origin? && any_authenticity_token_valid?)
466
473
  end
467
474
 
@@ -621,6 +628,7 @@ module ActionController # :nodoc:
621
628
  If you cannot change the referrer policy, you can disable origin checking with the
622
629
  Rails.application.config.action_controller.forgery_protection_origin_check setting.
623
630
  MSG
631
+ private_constant :NULL_ORIGIN_MESSAGE
624
632
 
625
633
  # Checks if the request originated from the same origin by looking at the Origin
626
634
  # header.
@@ -634,7 +642,7 @@ module ActionController # :nodoc:
634
642
  end
635
643
  end
636
644
 
637
- def normalize_action_path(action_path) # :doc:
645
+ def normalize_action_path(action_path)
638
646
  uri = URI.parse(action_path)
639
647
 
640
648
  if uri.relative? && (action_path.blank? || !action_path.start_with?("/"))
@@ -644,7 +652,7 @@ module ActionController # :nodoc:
644
652
  end
645
653
  end
646
654
 
647
- def normalize_relative_action_path(rel_action_path) # :doc:
655
+ def normalize_relative_action_path(rel_action_path)
648
656
  uri = URI.parse(request.path)
649
657
  # add the action path to the request.path
650
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
@@ -13,8 +13,10 @@ module ActionController
13
13
  class Railtie < Rails::Railtie # :nodoc:
14
14
  config.action_controller = ActiveSupport::OrderedOptions.new
15
15
  config.action_controller.raise_on_open_redirects = false
16
+ config.action_controller.action_on_path_relative_redirect = :log
16
17
  config.action_controller.log_query_tags_around_actions = true
17
18
  config.action_controller.wrap_parameters_by_default = false
19
+ config.action_controller.allowed_redirect_hosts = []
18
20
 
19
21
  config.eager_load_namespaces << AbstractController
20
22
  config.eager_load_namespaces << ActionController
@@ -93,12 +95,6 @@ module ActionController
93
95
  end
94
96
  end
95
97
 
96
- initializer "action_controller.compile_config_methods" do
97
- ActiveSupport.on_load(:action_controller) do
98
- config.compile_methods! if config.respond_to?(:compile_methods!)
99
- end
100
- end
101
-
102
98
  initializer "action_controller.request_forgery_protection" do |app|
103
99
  ActiveSupport.on_load(:action_controller_base) do
104
100
  if app.config.action_controller.default_protect_from_forgery
@@ -63,6 +63,114 @@ module ActionDispatch
63
63
  success
64
64
  end
65
65
  end
66
+
67
+ def cache_control_directives
68
+ @cache_control_directives ||= CacheControlDirectives.new(get_header("HTTP_CACHE_CONTROL"))
69
+ end
70
+
71
+ # Represents the HTTP Cache-Control header for requests,
72
+ # providing methods to access various cache control directives
73
+ # Reference: https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives
74
+ class CacheControlDirectives
75
+ def initialize(cache_control_header)
76
+ @only_if_cached = false
77
+ @no_cache = false
78
+ @no_store = false
79
+ @no_transform = false
80
+ @max_age = nil
81
+ @max_stale = nil
82
+ @min_fresh = nil
83
+ @stale_if_error = false
84
+ parse_directives(cache_control_header)
85
+ end
86
+
87
+ # Returns true if the only-if-cached directive is present.
88
+ # This directive indicates that the client only wishes to obtain a
89
+ # stored response. If a valid stored response is not available,
90
+ # the server should respond with a 504 (Gateway Timeout) status.
91
+ def only_if_cached?
92
+ @only_if_cached
93
+ end
94
+
95
+ # Returns true if the no-cache directive is present.
96
+ # This directive indicates that a cache must not use the response
97
+ # to satisfy subsequent requests without successful validation on the origin server.
98
+ def no_cache?
99
+ @no_cache
100
+ end
101
+
102
+ # Returns true if the no-store directive is present.
103
+ # This directive indicates that a cache must not store any part of the
104
+ # request or response.
105
+ def no_store?
106
+ @no_store
107
+ end
108
+
109
+ # Returns true if the no-transform directive is present.
110
+ # This directive indicates that a cache or proxy must not transform the payload.
111
+ def no_transform?
112
+ @no_transform
113
+ end
114
+
115
+ # Returns the value of the max-age directive.
116
+ # This directive indicates that the client is willing to accept a response
117
+ # whose age is no greater than the specified number of seconds.
118
+ attr_reader :max_age
119
+
120
+ # Returns the value of the max-stale directive.
121
+ # When max-stale is present with a value, returns that integer value.
122
+ # When max-stale is present without a value, returns true (unlimited staleness).
123
+ # When max-stale is not present, returns nil.
124
+ attr_reader :max_stale
125
+
126
+ # Returns true if max-stale directive is present (with or without a value)
127
+ def max_stale?
128
+ !@max_stale.nil?
129
+ end
130
+
131
+ # Returns true if max-stale directive is present without a value (unlimited staleness)
132
+ def max_stale_unlimited?
133
+ @max_stale == true
134
+ end
135
+
136
+ # Returns the value of the min-fresh directive.
137
+ # This directive indicates that the client is willing to accept a response
138
+ # whose freshness lifetime is no less than its current age plus the specified time in seconds.
139
+ attr_reader :min_fresh
140
+
141
+ # Returns the value of the stale-if-error directive.
142
+ # This directive indicates that the client is willing to accept a stale response
143
+ # if the check for a fresh one fails with an error for the specified number of seconds.
144
+ attr_reader :stale_if_error
145
+
146
+ private
147
+ def parse_directives(header_value)
148
+ return unless header_value
149
+
150
+ header_value.delete(" ").downcase.split(",").each do |directive|
151
+ name, value = directive.split("=", 2)
152
+
153
+ case name
154
+ when "max-age"
155
+ @max_age = value.to_i
156
+ when "min-fresh"
157
+ @min_fresh = value.to_i
158
+ when "stale-if-error"
159
+ @stale_if_error = value.to_i
160
+ when "no-cache"
161
+ @no_cache = true
162
+ when "no-store"
163
+ @no_store = true
164
+ when "no-transform"
165
+ @no_transform = true
166
+ when "only-if-cached"
167
+ @only_if_cached = true
168
+ when "max-stale"
169
+ @max_stale = value ? value.to_i : true
170
+ end
171
+ end
172
+ end
173
+ end
66
174
  end
67
175
 
68
176
  module Response
@@ -142,7 +250,7 @@ module ActionDispatch
142
250
  private
143
251
  DATE = "Date"
144
252
  LAST_MODIFIED = "Last-Modified"
145
- SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
253
+ SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate must-understand])
146
254
 
147
255
  def generate_weak_etag(validators)
148
256
  "W/#{generate_strong_etag(validators)}"
@@ -187,6 +295,7 @@ module ActionDispatch
187
295
  PRIVATE = "private"
188
296
  MUST_REVALIDATE = "must-revalidate"
189
297
  IMMUTABLE = "immutable"
298
+ MUST_UNDERSTAND = "must-understand"
190
299
 
191
300
  def handle_conditional_get!
192
301
  # Normally default cache control setting is handled by ETag middleware. But, if
@@ -221,6 +330,7 @@ module ActionDispatch
221
330
 
222
331
  if control[:no_store]
223
332
  options << PRIVATE if control[:private]
333
+ options << MUST_UNDERSTAND if control[:must_understand]
224
334
  options << NO_STORE
225
335
  elsif control[:no_cache]
226
336
  options << PUBLIC if control[:public]