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
@@ -96,7 +96,6 @@ module ActionController
96
96
  # * `:script_name` - The portion of the incoming request's URL path that
97
97
  # corresponds to the application. Converts to Rack's `SCRIPT_NAME`.
98
98
  # * `:input` - The input stream. Converts to Rack's `rack.input`.
99
- #
100
99
  # * `defaults` - Default values for the Rack env. Entries are specified in the
101
100
  # same format as `env`. `env` will be merged on top of these values.
102
101
  # `defaults` will be retained when calling #new on a renderer instance.
@@ -0,0 +1,116 @@
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
+
50
+ exception_backtrace = exception.backtrace&.first
51
+ exception_backtrace = exception_backtrace&.delete_prefix("#{Rails.root}/") if defined?(Rails.root) && Rails.root
52
+
53
+ emit_event("action_controller.rescue_from_handled",
54
+ exception_class: exception.class.name,
55
+ exception_message: exception.message,
56
+ exception_backtrace:
57
+ )
58
+ end
59
+
60
+ def send_file(event)
61
+ emit_event("action_controller.file_sent", path: event.payload[:path], duration_ms: event.duration.round(1))
62
+ end
63
+
64
+ def redirect_to(event)
65
+ emit_event("action_controller.redirected", location: event.payload[:location])
66
+ end
67
+
68
+ def send_data(event)
69
+ emit_event("action_controller.data_sent", filename: event.payload[:filename], duration_ms: event.duration.round(1))
70
+ end
71
+
72
+ def unpermitted_parameters(event)
73
+ unpermitted_keys = event.payload[:keys]
74
+ context = event.payload[:context]
75
+
76
+ emit_debug_event("action_controller.unpermitted_parameters",
77
+ unpermitted_keys:,
78
+ context: context.except(:request)
79
+ )
80
+ end
81
+ debug_only :unpermitted_parameters
82
+
83
+ def write_fragment(event)
84
+ fragment_cache(__method__, event)
85
+ end
86
+
87
+ def read_fragment(event)
88
+ fragment_cache(__method__, event)
89
+ end
90
+
91
+ def exist_fragment?(event)
92
+ fragment_cache(__method__, event)
93
+ end
94
+
95
+ def expire_fragment(event)
96
+ fragment_cache(__method__, event)
97
+ end
98
+
99
+ private
100
+ def fragment_cache(method_name, event)
101
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
102
+
103
+ emit_event("action_controller.fragment_cache",
104
+ method: "#{method_name}",
105
+ key: key,
106
+ duration_ms: event.duration.round(1)
107
+ )
108
+ end
109
+
110
+ def additions_for(payload)
111
+ payload.slice(:view_runtime, :db_runtime, :queries_count, :cached_queries_count)
112
+ end
113
+ end
114
+ end
115
+
116
+ ActionController::StructuredEventSubscriber.attach_to :action_controller
@@ -22,11 +22,21 @@ module ActionController
22
22
  # database on the main thread, so they could open a txn, then the controller
23
23
  # thread will open a new connection and try to access data that's only visible
24
24
  # to the main thread's txn. This is the problem in #23483.
25
+ alias_method :original_new_controller_thread, :new_controller_thread
26
+
25
27
  silence_redefinition_of_method :new_controller_thread
26
28
  def new_controller_thread # :nodoc:
27
29
  yield
28
30
  end
29
31
 
32
+ # Because of the above, we need to prevent the clearing of thread locals, since
33
+ # no new thread is actually spawned in the test environment.
34
+ alias_method :original_clean_up_thread_locals, :clean_up_thread_locals
35
+
36
+ silence_redefinition_of_method :clean_up_thread_locals
37
+ def clean_up_thread_locals(*args) # :nodoc:
38
+ end
39
+
30
40
  # Avoid a deadlock from the queue filling up
31
41
  Buffer.queue_size = nil
32
42
  end
@@ -106,7 +116,7 @@ module ActionController
106
116
  set_header k, "application/x-www-form-urlencoded"
107
117
  end
108
118
 
109
- case content_mime_type.to_sym
119
+ case content_mime_type&.to_sym
110
120
  when nil
111
121
  raise "Unknown Content-Type: #{content_type}"
112
122
  when :json
@@ -121,7 +131,7 @@ module ActionController
121
131
  end
122
132
  end
123
133
 
124
- data_stream = StringIO.new(data)
134
+ data_stream = StringIO.new(data.b)
125
135
  set_header "CONTENT_LENGTH", data_stream.length.to_s
126
136
  set_header "rack.input", data_stream
127
137
  end
@@ -30,5 +30,11 @@ module ActionDispatch
30
30
  SERVER_TIMING = "server-timing"
31
31
  STRICT_TRANSPORT_SECURITY = "strict-transport-security"
32
32
  end
33
+
34
+ if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3.1")
35
+ UNPROCESSABLE_CONTENT = :unprocessable_entity
36
+ else
37
+ UNPROCESSABLE_CONTENT = :unprocessable_content
38
+ end
33
39
  end
34
40
  end
@@ -9,6 +9,8 @@ module ActionDispatch
9
9
  HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
10
10
  HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
11
11
 
12
+ mattr_accessor :strict_freshness, default: false
13
+
12
14
  def if_modified_since
13
15
  if since = get_header(HTTP_IF_MODIFIED_SINCE)
14
16
  Time.rfc2822(since) rescue nil
@@ -34,19 +36,140 @@ module ActionDispatch
34
36
  end
35
37
  end
36
38
 
37
- # Check response freshness (`Last-Modified` and ETag) against request
38
- # `If-Modified-Since` and `If-None-Match` conditions. If both headers are
39
- # supplied, both must match, or the request is not considered fresh.
39
+ # Check response freshness (`Last-Modified` and `ETag`) against request
40
+ # `If-Modified-Since` and `If-None-Match` conditions.
41
+ # If both headers are supplied, based on configuration, either `ETag` is preferred over `Last-Modified`
42
+ # or both are considered equally. You can adjust the preference with
43
+ # `config.action_dispatch.strict_freshness`.
44
+ # Reference: http://tools.ietf.org/html/rfc7232#section-6
40
45
  def fresh?(response)
41
- last_modified = if_modified_since
42
- etag = if_none_match
46
+ if Request.strict_freshness
47
+ if if_none_match
48
+ etag_matches?(response.etag)
49
+ elsif if_modified_since
50
+ not_modified?(response.last_modified)
51
+ else
52
+ false
53
+ end
54
+ else
55
+ last_modified = if_modified_since
56
+ etag = if_none_match
57
+
58
+ return false unless last_modified || etag
59
+
60
+ success = true
61
+ success &&= not_modified?(response.last_modified) if last_modified
62
+ success &&= etag_matches?(response.etag) if etag
63
+ success
64
+ end
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
43
114
 
44
- return false unless last_modified || etag
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
45
119
 
46
- success = true
47
- success &&= not_modified?(response.last_modified) if last_modified
48
- success &&= etag_matches?(response.etag) if etag
49
- success
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
50
173
  end
51
174
  end
52
175
 
@@ -127,7 +250,7 @@ module ActionDispatch
127
250
  private
128
251
  DATE = "Date"
129
252
  LAST_MODIFIED = "Last-Modified"
130
- 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])
131
254
 
132
255
  def generate_weak_etag(validators)
133
256
  "W/#{generate_strong_etag(validators)}"
@@ -171,6 +294,8 @@ module ActionDispatch
171
294
  PUBLIC = "public"
172
295
  PRIVATE = "private"
173
296
  MUST_REVALIDATE = "must-revalidate"
297
+ IMMUTABLE = "immutable"
298
+ MUST_UNDERSTAND = "must-understand"
174
299
 
175
300
  def handle_conditional_get!
176
301
  # Normally default cache control setting is handled by ETag middleware. But, if
@@ -205,6 +330,7 @@ module ActionDispatch
205
330
 
206
331
  if control[:no_store]
207
332
  options << PRIVATE if control[:private]
333
+ options << MUST_UNDERSTAND if control[:must_understand]
208
334
  options << NO_STORE
209
335
  elsif control[:no_cache]
210
336
  options << PUBLIC if control[:public]
@@ -221,6 +347,7 @@ module ActionDispatch
221
347
  options << MUST_REVALIDATE if control[:must_revalidate]
222
348
  options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
223
349
  options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
350
+ options << IMMUTABLE if control[:immutable]
224
351
  options.concat(extras) if extras
225
352
  end
226
353
 
@@ -128,6 +128,7 @@ module ActionDispatch # :nodoc:
128
128
  MAPPINGS = {
129
129
  self: "'self'",
130
130
  unsafe_eval: "'unsafe-eval'",
131
+ wasm_unsafe_eval: "'wasm-unsafe-eval'",
131
132
  unsafe_hashes: "'unsafe-hashes'",
132
133
  unsafe_inline: "'unsafe-inline'",
133
134
  none: "'none'",
@@ -170,6 +171,8 @@ module ActionDispatch # :nodoc:
170
171
  worker_src: "worker-src"
171
172
  }.freeze
172
173
 
174
+ HASH_SOURCE_ALGORITHM_PREFIXES = ["sha256-", "sha384-", "sha512-"].freeze
175
+
173
176
  DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
174
177
 
175
178
  private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
@@ -304,7 +307,13 @@ module ActionDispatch # :nodoc:
304
307
  case source
305
308
  when Symbol
306
309
  apply_mapping(source)
307
- when String, Proc
310
+ when String
311
+ if hash_source?(source)
312
+ "'#{source}'"
313
+ else
314
+ source
315
+ end
316
+ when Proc
308
317
  source
309
318
  else
310
319
  raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
@@ -373,5 +382,9 @@ module ActionDispatch # :nodoc:
373
382
  def nonce_directive?(directive, nonce_directives)
374
383
  nonce_directives.include?(directive)
375
384
  end
385
+
386
+ def hash_source?(source)
387
+ source.start_with?(*HASH_SOURCE_ALGORITHM_PREFIXES)
388
+ end
376
389
  end
377
390
  end
@@ -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
@@ -56,9 +56,14 @@ module ActionDispatch
56
56
 
57
57
  # Returns the MIME type for the format used in the request.
58
58
  #
59
- # GET /posts/5.xml | request.format => Mime[:xml]
60
- # GET /posts/5.xhtml | request.format => Mime[:html]
61
- # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
59
+ # # GET /posts/5.xml
60
+ # request.format # => Mime[:xml]
61
+ #
62
+ # # GET /posts/5.xhtml
63
+ # request.format # => Mime[:html]
64
+ #
65
+ # # GET /posts/5
66
+ # request.format # => Mime[:html] or Mime[:js], or request.accepts.first
62
67
  #
63
68
  def format(_view_path = nil)
64
69
  formats.first || Mime::NullType.instance
@@ -86,7 +91,49 @@ module ActionDispatch
86
91
  end
87
92
  end
88
93
 
89
- # 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
90
137
  def variant=(variant)
91
138
  variant = Array(variant)
92
139
 
@@ -97,6 +144,18 @@ module ActionDispatch
97
144
  end
98
145
  end
99
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
100
159
  def variant
101
160
  @variant ||= ActiveSupport::ArrayInquirer.new
102
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)