actionpack 8.0.3 → 8.1.0

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +337 -158
  3. data/lib/abstract_controller/asset_paths.rb +4 -2
  4. data/lib/abstract_controller/base.rb +12 -3
  5. data/lib/abstract_controller/caching.rb +6 -3
  6. data/lib/abstract_controller/helpers.rb +1 -1
  7. data/lib/abstract_controller/logger.rb +2 -1
  8. data/lib/action_controller/api.rb +1 -0
  9. data/lib/action_controller/base.rb +2 -1
  10. data/lib/action_controller/caching.rb +1 -2
  11. data/lib/action_controller/form_builder.rb +1 -1
  12. data/lib/action_controller/log_subscriber.rb +18 -3
  13. data/lib/action_controller/metal/allow_browser.rb +1 -1
  14. data/lib/action_controller/metal/conditional_get.rb +25 -0
  15. data/lib/action_controller/metal/data_streaming.rb +1 -3
  16. data/lib/action_controller/metal/exceptions.rb +5 -0
  17. data/lib/action_controller/metal/flash.rb +1 -4
  18. data/lib/action_controller/metal/head.rb +3 -1
  19. data/lib/action_controller/metal/live.rb +9 -18
  20. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  21. data/lib/action_controller/metal/rate_limiting.rb +30 -9
  22. data/lib/action_controller/metal/redirecting.rb +105 -13
  23. data/lib/action_controller/metal/renderers.rb +27 -6
  24. data/lib/action_controller/metal/rendering.rb +7 -1
  25. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  26. data/lib/action_controller/metal/rescue.rb +9 -0
  27. data/lib/action_controller/railtie.rb +27 -8
  28. data/lib/action_controller/structured_event_subscriber.rb +112 -0
  29. data/lib/action_dispatch/http/cache.rb +111 -1
  30. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  31. data/lib/action_dispatch/http/mime_negotiation.rb +55 -1
  32. data/lib/action_dispatch/http/mime_types.rb +1 -0
  33. data/lib/action_dispatch/http/param_builder.rb +28 -27
  34. data/lib/action_dispatch/http/parameters.rb +3 -3
  35. data/lib/action_dispatch/http/permissions_policy.rb +4 -0
  36. data/lib/action_dispatch/http/query_parser.rb +12 -10
  37. data/lib/action_dispatch/http/request.rb +10 -5
  38. data/lib/action_dispatch/http/response.rb +16 -3
  39. data/lib/action_dispatch/http/url.rb +110 -14
  40. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  41. data/lib/action_dispatch/journey/gtg/transition_table.rb +33 -41
  42. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  43. data/lib/action_dispatch/journey/route.rb +45 -31
  44. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  45. data/lib/action_dispatch/journey/router.rb +59 -81
  46. data/lib/action_dispatch/journey/routes.rb +7 -0
  47. data/lib/action_dispatch/journey/visitors.rb +55 -23
  48. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  49. data/lib/action_dispatch/log_subscriber.rb +7 -3
  50. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  51. data/lib/action_dispatch/middleware/debug_exceptions.rb +7 -1
  52. data/lib/action_dispatch/middleware/debug_view.rb +11 -0
  53. data/lib/action_dispatch/middleware/exception_wrapper.rb +11 -5
  54. data/lib/action_dispatch/middleware/executor.rb +12 -2
  55. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -5
  56. data/lib/action_dispatch/middleware/remote_ip.rb +9 -4
  57. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  58. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  59. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -2
  60. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  61. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  62. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  63. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  64. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  65. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  66. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  67. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  68. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  69. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  70. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  71. data/lib/action_dispatch/railtie.rb +14 -2
  72. data/lib/action_dispatch/routing/inspector.rb +79 -56
  73. data/lib/action_dispatch/routing/mapper.rb +324 -172
  74. data/lib/action_dispatch/routing/redirection.rb +10 -7
  75. data/lib/action_dispatch/routing/route_set.rb +2 -4
  76. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  77. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  78. data/lib/action_dispatch/testing/assertions/response.rb +14 -0
  79. data/lib/action_dispatch/testing/assertions/routing.rb +11 -3
  80. data/lib/action_dispatch/testing/integration.rb +1 -1
  81. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  82. data/lib/action_dispatch.rb +8 -0
  83. data/lib/action_pack/gem_version.rb +2 -2
  84. metadata +13 -10
@@ -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
@@ -12,9 +12,11 @@ require "action_view/railtie"
12
12
  module ActionController
13
13
  class Railtie < Rails::Railtie # :nodoc:
14
14
  config.action_controller = ActiveSupport::OrderedOptions.new
15
- config.action_controller.raise_on_open_redirects = false
15
+ config.action_controller.action_on_open_redirect = :log
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
@@ -55,7 +57,8 @@ module ActionController
55
57
  paths = app.config.paths
56
58
  options = app.config.action_controller
57
59
 
58
- options.logger ||= Rails.logger
60
+ options.logger = options.fetch(:logger, Rails.logger)
61
+
59
62
  options.cache_store ||= Rails.cache
60
63
 
61
64
  options.javascripts_dir ||= paths["public/javascripts"].first
@@ -93,12 +96,6 @@ module ActionController
93
96
  end
94
97
  end
95
98
 
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
99
  initializer "action_controller.request_forgery_protection" do |app|
103
100
  ActiveSupport.on_load(:action_controller_base) do
104
101
  if app.config.action_controller.default_protect_from_forgery
@@ -107,6 +104,22 @@ module ActionController
107
104
  end
108
105
  end
109
106
 
107
+ initializer "action_controller.open_redirects" do |app|
108
+ ActiveSupport.on_load(:action_controller, run_once: true) do
109
+ if app.config.action_controller.has_key?(:raise_on_open_redirects)
110
+ ActiveSupport.deprecator.warn(<<~MSG.squish)
111
+ `raise_on_open_redirects` is deprecated and will be removed in a future Rails version.
112
+ Use `config.action_controller.action_on_open_redirect = :raise` instead.
113
+ MSG
114
+
115
+ # Fallback to the default behavior in case of `load_default` set `action_on_open_redirect`, but apps set `raise_on_open_redirects`.
116
+ if app.config.action_controller.raise_on_open_redirects == false && app.config.action_controller.action_on_open_redirect == :raise
117
+ self.action_on_open_redirect = :log
118
+ end
119
+ end
120
+ end
121
+ end
122
+
110
123
  initializer "action_controller.query_log_tags" do |app|
111
124
  query_logs_tags_enabled = app.config.respond_to?(:active_record) &&
112
125
  app.config.active_record.query_log_tags_enabled &&
@@ -139,5 +152,11 @@ module ActionController
139
152
  ActionController::TestCase.executor_around_each_request = app.config.active_support.executor_around_test_case
140
153
  end
141
154
  end
155
+
156
+ initializer "action_controller.backtrace_cleaner" do
157
+ ActiveSupport.on_load(:action_controller) do
158
+ ActionController::LogSubscriber.backtrace_cleaner = Rails.backtrace_cleaner
159
+ end
160
+ end
142
161
  end
143
162
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
5
+ INTERNAL_PARAMS = %w(controller action format _method only_path)
6
+
7
+ def start_processing(event)
8
+ payload = event.payload
9
+ params = {}
10
+ payload[:params].each_pair do |k, v|
11
+ params[k] = v unless INTERNAL_PARAMS.include?(k)
12
+ end
13
+ format = payload[:format]
14
+ format = format.to_s.upcase if format.is_a?(Symbol)
15
+ format = "*/*" if format.nil?
16
+
17
+ emit_event("action_controller.request_started",
18
+ controller: payload[:controller],
19
+ action: payload[:action],
20
+ format:,
21
+ params:,
22
+ )
23
+ end
24
+
25
+ def process_action(event)
26
+ payload = event.payload
27
+ status = payload[:status]
28
+
29
+ if status.nil? && (exception_class_name = payload[:exception]&.first)
30
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
31
+ end
32
+
33
+ emit_event("action_controller.request_completed", {
34
+ controller: payload[:controller],
35
+ action: payload[:action],
36
+ status: status,
37
+ **additions_for(payload),
38
+ duration_ms: event.duration.round(2),
39
+ gc_time_ms: event.gc_time.round(1),
40
+ }.compact)
41
+ end
42
+
43
+ def halted_callback(event)
44
+ emit_event("action_controller.callback_halted", filter: event.payload[:filter])
45
+ end
46
+
47
+ def rescue_from_callback(event)
48
+ exception = event.payload[:exception]
49
+ emit_event("action_controller.rescue_from_handled",
50
+ exception_class: exception.class.name,
51
+ exception_message: exception.message,
52
+ exception_backtrace: exception.backtrace&.first&.delete_prefix("#{Rails.root}/")
53
+ )
54
+ end
55
+
56
+ def send_file(event)
57
+ emit_event("action_controller.file_sent", path: event.payload[:path], duration_ms: event.duration.round(1))
58
+ end
59
+
60
+ def redirect_to(event)
61
+ emit_event("action_controller.redirected", location: event.payload[:location])
62
+ end
63
+
64
+ def send_data(event)
65
+ emit_event("action_controller.data_sent", filename: event.payload[:filename], duration_ms: event.duration.round(1))
66
+ end
67
+
68
+ def unpermitted_parameters(event)
69
+ unpermitted_keys = event.payload[:keys]
70
+ context = event.payload[:context]
71
+
72
+ emit_debug_event("action_controller.unpermitted_parameters",
73
+ unpermitted_keys:,
74
+ context: context.except(:request)
75
+ )
76
+ end
77
+ debug_only :unpermitted_parameters
78
+
79
+ def write_fragment(event)
80
+ fragment_cache(__method__, event)
81
+ end
82
+
83
+ def read_fragment(event)
84
+ fragment_cache(__method__, event)
85
+ end
86
+
87
+ def exist_fragment?(event)
88
+ fragment_cache(__method__, event)
89
+ end
90
+
91
+ def expire_fragment(event)
92
+ fragment_cache(__method__, event)
93
+ end
94
+
95
+ private
96
+ def fragment_cache(method_name, event)
97
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
98
+
99
+ emit_event("action_controller.fragment_cache",
100
+ method: "#{method_name}",
101
+ key: key,
102
+ duration_ms: event.duration.round(1)
103
+ )
104
+ end
105
+
106
+ def additions_for(payload)
107
+ payload.slice(:view_runtime, :db_runtime, :queries_count, :cached_queries_count)
108
+ end
109
+ end
110
+ end
111
+
112
+ ActionController::StructuredEventSubscriber.attach_to :action_controller
@@ -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]
@@ -17,9 +17,11 @@ module ActionDispatch
17
17
  # For more information about filter behavior, see
18
18
  # ActiveSupport::ParameterFilter.
19
19
  module FilterParameters
20
- ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
21
- NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
22
- NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
20
+ # :stopdoc:
21
+ ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"]
22
+ NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new
23
+ NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH
24
+ # :startdoc:
23
25
 
24
26
  def initialize
25
27
  super
@@ -91,7 +91,49 @@ module ActionDispatch
91
91
  end
92
92
  end
93
93
 
94
- # Sets the variant for template.
94
+ # Sets the \variant for the response template.
95
+ #
96
+ # When determining which template to render, Action View will incorporate
97
+ # all variants from the request. For example, if an
98
+ # `ArticlesController#index` action needs to respond to
99
+ # `request.variant = [:ios, :turbo_native]`, it will render the
100
+ # first template file it can find in the following list:
101
+ #
102
+ # - `app/views/articles/index.html+ios.erb`
103
+ # - `app/views/articles/index.html+turbo_native.erb`
104
+ # - `app/views/articles/index.html.erb`
105
+ #
106
+ # Variants add context to the requests that views render appropriately.
107
+ # Variant names are arbitrary, and can communicate anything from the
108
+ # request's platform (`:android`, `:ios`, `:linux`, `:macos`, `:windows`)
109
+ # to its browser (`:chrome`, `:edge`, `:firefox`, `:safari`), to the type
110
+ # of user (`:admin`, `:guest`, `:user`).
111
+ #
112
+ # Note: Adding many new variant templates with similarities to existing
113
+ # template files can make maintaining your view code more difficult.
114
+ #
115
+ # #### Parameters
116
+ #
117
+ # * `variant` - a symbol name or an array of symbol names for variants
118
+ # used to render the response template
119
+ #
120
+ # #### Examples
121
+ #
122
+ # class ApplicationController < ActionController::Base
123
+ # before_action :determine_variants
124
+ #
125
+ # private
126
+ # def determine_variants
127
+ # variants = []
128
+ #
129
+ # # some code to determine the variant(s) to use
130
+ #
131
+ # variants << :ios if request.user_agent.include?("iOS")
132
+ # variants << :turbo_native if request.user_agent.include?("Turbo Native")
133
+ #
134
+ # request.variant = variants
135
+ # end
136
+ # end
95
137
  def variant=(variant)
96
138
  variant = Array(variant)
97
139
 
@@ -102,6 +144,18 @@ module ActionDispatch
102
144
  end
103
145
  end
104
146
 
147
+ # Returns the \variant for the response template as an instance of
148
+ # ActiveSupport::ArrayInquirer.
149
+ #
150
+ # request.variant = :phone
151
+ # request.variant.phone? # => true
152
+ # request.variant.tablet? # => false
153
+ #
154
+ # request.variant = [:phone, :tablet]
155
+ # request.variant.phone? # => true
156
+ # request.variant.desktop? # => false
157
+ # request.variant.any?(:phone, :desktop) # => true
158
+ # request.variant.any?(:desktop, :watch) # => false
105
159
  def variant
106
160
  @variant ||= ActiveSupport::ArrayInquirer.new
107
161
  end
@@ -13,6 +13,7 @@ Mime::Type.register "text/calendar", :ics
13
13
  Mime::Type.register "text/csv", :csv
14
14
  Mime::Type.register "text/vcard", :vcf
15
15
  Mime::Type.register "text/vtt", :vtt, %w(vtt)
16
+ Mime::Type.register "text/markdown", :md, [], %w(md markdown)
16
17
 
17
18
  Mime::Type.register "image/png", :png, [], %w(png)
18
19
  Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)