actionpack 7.0.4 → 7.1.5.1

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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +495 -257
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/abstract_controller/base.rb +20 -11
  6. data/lib/abstract_controller/caching/fragments.rb +2 -0
  7. data/lib/abstract_controller/callbacks.rb +31 -6
  8. data/lib/abstract_controller/deprecator.rb +7 -0
  9. data/lib/abstract_controller/helpers.rb +75 -28
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -16
  11. data/lib/abstract_controller/rendering.rb +12 -14
  12. data/lib/abstract_controller/translation.rb +11 -6
  13. data/lib/abstract_controller/url_for.rb +2 -0
  14. data/lib/abstract_controller.rb +6 -0
  15. data/lib/action_controller/api.rb +6 -4
  16. data/lib/action_controller/base.rb +3 -17
  17. data/lib/action_controller/caching.rb +2 -0
  18. data/lib/action_controller/deprecator.rb +7 -0
  19. data/lib/action_controller/form_builder.rb +2 -0
  20. data/lib/action_controller/log_subscriber.rb +16 -4
  21. data/lib/action_controller/metal/basic_implicit_render.rb +3 -1
  22. data/lib/action_controller/metal/conditional_get.rb +121 -123
  23. data/lib/action_controller/metal/content_security_policy.rb +5 -5
  24. data/lib/action_controller/metal/data_streaming.rb +20 -18
  25. data/lib/action_controller/metal/default_headers.rb +2 -0
  26. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  27. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -0
  28. data/lib/action_controller/metal/exceptions.rb +8 -0
  29. data/lib/action_controller/metal/head.rb +9 -7
  30. data/lib/action_controller/metal/helpers.rb +3 -14
  31. data/lib/action_controller/metal/http_authentication.rb +15 -9
  32. data/lib/action_controller/metal/implicit_render.rb +5 -3
  33. data/lib/action_controller/metal/instrumentation.rb +8 -1
  34. data/lib/action_controller/metal/live.rb +25 -1
  35. data/lib/action_controller/metal/mime_responds.rb +2 -2
  36. data/lib/action_controller/metal/params_wrapper.rb +4 -2
  37. data/lib/action_controller/metal/permissions_policy.rb +2 -2
  38. data/lib/action_controller/metal/redirecting.rb +29 -8
  39. data/lib/action_controller/metal/renderers.rb +4 -4
  40. data/lib/action_controller/metal/rendering.rb +114 -9
  41. data/lib/action_controller/metal/request_forgery_protection.rb +144 -53
  42. data/lib/action_controller/metal/rescue.rb +6 -3
  43. data/lib/action_controller/metal/streaming.rb +71 -31
  44. data/lib/action_controller/metal/strong_parameters.rb +200 -103
  45. data/lib/action_controller/metal/url_for.rb +9 -4
  46. data/lib/action_controller/metal.rb +79 -21
  47. data/lib/action_controller/railtie.rb +24 -10
  48. data/lib/action_controller/renderer.rb +99 -85
  49. data/lib/action_controller/test_case.rb +18 -8
  50. data/lib/action_controller.rb +13 -3
  51. data/lib/action_dispatch/constants.rb +32 -0
  52. data/lib/action_dispatch/deprecator.rb +7 -0
  53. data/lib/action_dispatch/http/cache.rb +9 -11
  54. data/lib/action_dispatch/http/content_security_policy.rb +35 -13
  55. data/lib/action_dispatch/http/filter_parameters.rb +23 -32
  56. data/lib/action_dispatch/http/headers.rb +3 -1
  57. data/lib/action_dispatch/http/mime_negotiation.rb +22 -22
  58. data/lib/action_dispatch/http/mime_type.rb +37 -11
  59. data/lib/action_dispatch/http/mime_types.rb +3 -1
  60. data/lib/action_dispatch/http/parameters.rb +1 -1
  61. data/lib/action_dispatch/http/permissions_policy.rb +38 -23
  62. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  63. data/lib/action_dispatch/http/request.rb +85 -32
  64. data/lib/action_dispatch/http/response.rb +80 -63
  65. data/lib/action_dispatch/http/upload.rb +15 -2
  66. data/lib/action_dispatch/journey/formatter.rb +8 -2
  67. data/lib/action_dispatch/journey/path/pattern.rb +14 -14
  68. data/lib/action_dispatch/journey/route.rb +3 -2
  69. data/lib/action_dispatch/journey/router.rb +9 -8
  70. data/lib/action_dispatch/journey/routes.rb +2 -2
  71. data/lib/action_dispatch/log_subscriber.rb +23 -0
  72. data/lib/action_dispatch/middleware/actionable_exceptions.rb +5 -6
  73. data/lib/action_dispatch/middleware/assume_ssl.rb +24 -0
  74. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  75. data/lib/action_dispatch/middleware/cookies.rb +108 -117
  76. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -25
  77. data/lib/action_dispatch/middleware/debug_locks.rb +4 -1
  78. data/lib/action_dispatch/middleware/debug_view.rb +7 -2
  79. data/lib/action_dispatch/middleware/exception_wrapper.rb +186 -27
  80. data/lib/action_dispatch/middleware/executor.rb +7 -1
  81. data/lib/action_dispatch/middleware/flash.rb +7 -0
  82. data/lib/action_dispatch/middleware/host_authorization.rb +18 -8
  83. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -3
  84. data/lib/action_dispatch/middleware/reloader.rb +7 -5
  85. data/lib/action_dispatch/middleware/remote_ip.rb +21 -20
  86. data/lib/action_dispatch/middleware/request_id.rb +4 -2
  87. data/lib/action_dispatch/middleware/server_timing.rb +4 -4
  88. data/lib/action_dispatch/middleware/session/abstract_store.rb +5 -0
  89. data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
  90. data/lib/action_dispatch/middleware/session/cookie_store.rb +11 -5
  91. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +3 -1
  92. data/lib/action_dispatch/middleware/show_exceptions.rb +39 -22
  93. data/lib/action_dispatch/middleware/ssl.rb +18 -6
  94. data/lib/action_dispatch/middleware/stack.rb +7 -2
  95. data/lib/action_dispatch/middleware/static.rb +14 -10
  96. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  97. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  98. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  99. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -3
  100. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -3
  101. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  102. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  103. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  104. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  105. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  106. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  107. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  108. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  109. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  110. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  111. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +59 -41
  112. data/lib/action_dispatch/railtie.rb +13 -4
  113. data/lib/action_dispatch/request/session.rb +16 -6
  114. data/lib/action_dispatch/request/utils.rb +8 -3
  115. data/lib/action_dispatch/routing/inspector.rb +54 -6
  116. data/lib/action_dispatch/routing/mapper.rb +97 -26
  117. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  118. data/lib/action_dispatch/routing/redirection.rb +15 -6
  119. data/lib/action_dispatch/routing/route_set.rb +53 -23
  120. data/lib/action_dispatch/routing/routes_proxy.rb +10 -15
  121. data/lib/action_dispatch/routing/url_for.rb +26 -22
  122. data/lib/action_dispatch/routing.rb +7 -7
  123. data/lib/action_dispatch/system_test_case.rb +3 -3
  124. data/lib/action_dispatch/system_testing/browser.rb +25 -19
  125. data/lib/action_dispatch/system_testing/driver.rb +15 -23
  126. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +27 -16
  127. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  128. data/lib/action_dispatch/testing/assertions/response.rb +14 -7
  129. data/lib/action_dispatch/testing/assertions/routing.rb +67 -28
  130. data/lib/action_dispatch/testing/assertions.rb +3 -1
  131. data/lib/action_dispatch/testing/integration.rb +27 -17
  132. data/lib/action_dispatch/testing/request_encoder.rb +4 -1
  133. data/lib/action_dispatch/testing/test_process.rb +4 -3
  134. data/lib/action_dispatch/testing/test_request.rb +1 -1
  135. data/lib/action_dispatch/testing/test_response.rb +23 -9
  136. data/lib/action_dispatch.rb +41 -4
  137. data/lib/action_pack/gem_version.rb +4 -4
  138. data/lib/action_pack/version.rb +1 -1
  139. data/lib/action_pack.rb +1 -1
  140. metadata +68 -32
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/version"
4
+
5
+ module ActionDispatch
6
+ module Constants
7
+ # Response Header keys for Rack 2.x and 3.x
8
+ if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
9
+ VARY = "Vary"
10
+ CONTENT_ENCODING = "Content-Encoding"
11
+ CONTENT_SECURITY_POLICY = "Content-Security-Policy"
12
+ CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
13
+ LOCATION = "Location"
14
+ FEATURE_POLICY = "Feature-Policy"
15
+ X_REQUEST_ID = "X-Request-Id"
16
+ X_CASCADE = "X-Cascade"
17
+ SERVER_TIMING = "Server-Timing"
18
+ STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security"
19
+ else
20
+ VARY = "vary"
21
+ CONTENT_ENCODING = "content-encoding"
22
+ CONTENT_SECURITY_POLICY = "content-security-policy"
23
+ CONTENT_SECURITY_POLICY_REPORT_ONLY = "content-security-policy-report-only"
24
+ LOCATION = "location"
25
+ FEATURE_POLICY = "feature-policy"
26
+ X_REQUEST_ID = "x-request-id"
27
+ X_CASCADE = "x-cascade"
28
+ SERVER_TIMING = "server-timing"
29
+ STRICT_TRANSPORT_SECURITY = "strict-transport-security"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -18,7 +18,7 @@ module ActionDispatch
18
18
  end
19
19
 
20
20
  def if_none_match_etags
21
- if_none_match ? if_none_match.split(/\s*,\s*/) : []
21
+ if_none_match ? if_none_match.split(",").each(&:strip!) : []
22
22
  end
23
23
 
24
24
  def not_modified?(modified_at)
@@ -32,8 +32,8 @@ module ActionDispatch
32
32
  end
33
33
  end
34
34
 
35
- # Check response freshness (Last-Modified and ETag) against request
36
- # If-Modified-Since and If-None-Match conditions. If both headers are
35
+ # Check response freshness (+Last-Modified+ and ETag) against request
36
+ # +If-Modified-Since+ and +If-None-Match+ conditions. If both headers are
37
37
  # supplied, both must match, or the request is not considered fresh.
38
38
  def fresh?(response)
39
39
  last_modified = if_modified_since
@@ -81,8 +81,8 @@ module ActionDispatch
81
81
 
82
82
  # This method sets a weak ETag validator on the response so browsers
83
83
  # and proxies may cache the response, keyed on the ETag. On subsequent
84
- # requests, the If-None-Match header is set to the cached ETag. If it
85
- # matches the current ETag, we can return a 304 Not Modified response
84
+ # requests, the +If-None-Match+ header is set to the cached ETag. If it
85
+ # matches the current ETag, we can return a <tt>304 Not Modified</tt> response
86
86
  # with no body, letting the browser or proxy know that their cache is
87
87
  # current. Big savings in request time and network bandwidth.
88
88
  #
@@ -92,7 +92,7 @@ module ActionDispatch
92
92
  # is viewing.
93
93
  #
94
94
  # Strong ETags are considered byte-for-byte identical. They allow a
95
- # browser or proxy cache to support Range requests, useful for paging
95
+ # browser or proxy cache to support +Range+ requests, useful for paging
96
96
  # through a PDF file or scrubbing through a video. Some CDNs only
97
97
  # support strong ETags and will ignore weak ETags entirely.
98
98
  #
@@ -112,12 +112,12 @@ module ActionDispatch
112
112
 
113
113
  def etag?; etag; end
114
114
 
115
- # True if an ETag is set and it's a weak validator (preceded with W/)
115
+ # True if an ETag is set, and it's a weak validator (preceded with <tt>W/</tt>).
116
116
  def weak_etag?
117
117
  etag? && etag.start_with?('W/"')
118
118
  end
119
119
 
120
- # True if an ETag is set and it isn't a weak validator (not preceded with W/)
120
+ # True if an ETag is set, and it isn't a weak validator (not preceded with <tt>W/</tt>).
121
121
  def strong_etag?
122
122
  etag? && !weak_etag?
123
123
  end
@@ -138,15 +138,13 @@ module ActionDispatch
138
138
  def cache_control_segments
139
139
  if cache_control = _cache_control
140
140
  cache_control.delete(" ").split(",")
141
- else
142
- []
143
141
  end
144
142
  end
145
143
 
146
144
  def cache_control_headers
147
145
  cache_control = {}
148
146
 
149
- cache_control_segments.each do |segment|
147
+ cache_control_segments&.each do |segment|
150
148
  directive, argument = segment.split("=", 2)
151
149
 
152
150
  if SPECIAL_KEYS.include? directive
@@ -4,6 +4,8 @@ require "active_support/core_ext/object/deep_dup"
4
4
  require "active_support/core_ext/array/wrap"
5
5
 
6
6
  module ActionDispatch # :nodoc:
7
+ # = Action Dispatch Content Security Policy
8
+ #
7
9
  # Configures the HTTP
8
10
  # {Content-Security-Policy}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy]
9
11
  # response header to help protect against XSS and injection attacks.
@@ -22,21 +24,25 @@ module ActionDispatch # :nodoc:
22
24
  # policy.report_uri "/csp-violation-report-endpoint"
23
25
  # end
24
26
  class ContentSecurityPolicy
25
- class Middleware
26
- CONTENT_TYPE = "Content-Type"
27
- POLICY = "Content-Security-Policy"
28
- POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
27
+ class InvalidDirectiveError < StandardError
28
+ end
29
29
 
30
+ class Middleware
30
31
  def initialize(app)
31
32
  @app = app
32
33
  end
33
34
 
34
35
  def call(env)
35
- request = ActionDispatch::Request.new env
36
- _, headers, _ = response = @app.call(env)
36
+ status, headers, _ = response = @app.call(env)
37
+
38
+ # Returning CSP headers with a 304 Not Modified is harmful, since nonces in the new
39
+ # CSP headers might not match nonces in the cached HTML.
40
+ return response if status == 304
37
41
 
38
42
  return response if policy_present?(headers)
39
43
 
44
+ request = ActionDispatch::Request.new env
45
+
40
46
  if policy = request.content_security_policy
41
47
  nonce = request.content_security_policy_nonce
42
48
  nonce_directives = request.content_security_policy_nonce_directives
@@ -50,14 +56,15 @@ module ActionDispatch # :nodoc:
50
56
  private
51
57
  def header_name(request)
52
58
  if request.content_security_policy_report_only
53
- POLICY_REPORT_ONLY
59
+ ActionDispatch::Constants::CONTENT_SECURITY_POLICY_REPORT_ONLY
54
60
  else
55
- POLICY
61
+ ActionDispatch::Constants::CONTENT_SECURITY_POLICY
56
62
  end
57
63
  end
58
64
 
59
65
  def policy_present?(headers)
60
- headers[POLICY] || headers[POLICY_REPORT_ONLY]
66
+ headers[ActionDispatch::Constants::CONTENT_SECURITY_POLICY] ||
67
+ headers[ActionDispatch::Constants::CONTENT_SECURITY_POLICY_REPORT_ONLY]
61
68
  end
62
69
  end
63
70
 
@@ -119,6 +126,7 @@ module ActionDispatch # :nodoc:
119
126
  MAPPINGS = {
120
127
  self: "'self'",
121
128
  unsafe_eval: "'unsafe-eval'",
129
+ unsafe_hashes: "'unsafe-hashes'",
122
130
  unsafe_inline: "'unsafe-inline'",
123
131
  none: "'none'",
124
132
  http: "http:",
@@ -312,9 +320,9 @@ module ActionDispatch # :nodoc:
312
320
  @directives.map do |directive, sources|
313
321
  if sources.is_a?(Array)
314
322
  if nonce && nonce_directive?(directive, nonce_directives)
315
- "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
323
+ "#{directive} #{build_directive(directive, sources, context).join(' ')} 'nonce-#{nonce}'"
316
324
  else
317
- "#{directive} #{build_directive(sources, context).join(' ')}"
325
+ "#{directive} #{build_directive(directive, sources, context).join(' ')}"
318
326
  end
319
327
  elsif sources
320
328
  directive
@@ -324,8 +332,22 @@ module ActionDispatch # :nodoc:
324
332
  end
325
333
  end
326
334
 
327
- def build_directive(sources, context)
328
- sources.map { |source| resolve_source(source, context) }
335
+ def validate(directive, sources)
336
+ sources.flatten.each do |source|
337
+ if source.include?(";") || source != source.gsub(/[[:space:]]/, "")
338
+ raise InvalidDirectiveError, <<~MSG.squish
339
+ Invalid Content Security Policy #{directive}: "#{source}".
340
+ Directive values must not contain whitespace or semicolons.
341
+ Please use multiple arguments or other directive methods instead.
342
+ MSG
343
+ end
344
+ end
345
+ end
346
+
347
+ def build_directive(directive, sources, context)
348
+ resolved_sources = sources.map { |source| resolve_source(source, context) }
349
+
350
+ validate(directive, resolved_sources)
329
351
  end
330
352
 
331
353
  def resolve_source(source, context)
@@ -4,33 +4,15 @@ require "active_support/parameter_filter"
4
4
 
5
5
  module ActionDispatch
6
6
  module Http
7
- # Allows you to specify sensitive parameters which will be replaced from
8
- # the request log by looking in the query string of the request and all
9
- # sub-hashes of the params hash to filter. Filtering only certain sub-keys
10
- # from a hash is possible by using the dot notation: 'credit_card.number'.
11
- # If a block is given, each key and value of the params hash and all
12
- # sub-hashes are passed to it, where the value or the key can be replaced using
13
- # String#replace or similar methods.
7
+ # = Action Dispatch HTTP Filter Parameters
14
8
  #
15
- # env["action_dispatch.parameter_filter"] = [:password]
16
- # => replaces the value to all keys matching /password/i with "[FILTERED]"
9
+ # Allows you to specify sensitive query string and POST parameters to filter
10
+ # from the request log.
17
11
  #
12
+ # # Replaces values with "[FILTERED]" for keys that match /foo|bar/i.
18
13
  # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
19
- # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
20
14
  #
21
- # env["action_dispatch.parameter_filter"] = [ /\Apin\z/i, /\Apin_/i ]
22
- # => replaces the value for the exact (case-insensitive) key 'pin' and all
23
- # (case-insensitive) keys beginning with 'pin_', with "[FILTERED]"
24
- # Does not match keys with 'pin' as a substring, such as 'shipping_id'.
25
- #
26
- # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
27
- # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
28
- # change { file: { code: "xxxx"} }
29
- #
30
- # env["action_dispatch.parameter_filter"] = -> (k, v) do
31
- # v.reverse! if k.match?(/secret/i)
32
- # end
33
- # => reverses the value to all keys matching /secret/i
15
+ # For more information about filter behavior, see ActiveSupport::ParameterFilter.
34
16
  module FilterParameters
35
17
  ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
36
18
  NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
@@ -41,6 +23,7 @@ module ActionDispatch
41
23
  @filtered_parameters = nil
42
24
  @filtered_env = nil
43
25
  @filtered_path = nil
26
+ @parameter_filter = nil
44
27
  end
45
28
 
46
29
  # Returns a hash of parameters with all sensitive data replaced.
@@ -60,13 +43,16 @@ module ActionDispatch
60
43
  @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
61
44
  end
62
45
 
63
- private
64
- def parameter_filter # :doc:
65
- parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
66
- return NULL_PARAM_FILTER
67
- }
46
+ # Returns the +ActiveSupport::ParameterFilter+ object used to filter in this request.
47
+ def parameter_filter
48
+ @parameter_filter ||= if has_header?("action_dispatch.parameter_filter")
49
+ parameter_filter_for get_header("action_dispatch.parameter_filter")
50
+ else
51
+ NULL_PARAM_FILTER
52
+ end
68
53
  end
69
54
 
55
+ private
70
56
  def env_filter # :doc:
71
57
  user_key = fetch_header("action_dispatch.parameter_filter") {
72
58
  return NULL_ENV_FILTER
@@ -78,12 +64,17 @@ module ActionDispatch
78
64
  ActiveSupport::ParameterFilter.new(filters)
79
65
  end
80
66
 
81
- KV_RE = "[^&;=]+"
82
- PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
83
67
  def filtered_query_string # :doc:
84
- query_string.gsub(PAIR_RE) do |_|
85
- parameter_filter.filter($1 => $2).first.join("=")
68
+ parts = query_string.split(/([&;])/)
69
+ filtered_parts = parts.map do |part|
70
+ if part.include?("=")
71
+ key, value = part.split("=", 2)
72
+ parameter_filter.filter(key => value).first.join("=")
73
+ else
74
+ part
75
+ end
86
76
  end
77
+ filtered_parts.join("")
87
78
  end
88
79
  end
89
80
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionDispatch
4
4
  module Http
5
+ # = Action Dispatch HTTP \Headers
6
+ #
5
7
  # Provides access to the request's HTTP headers from the environment.
6
8
  #
7
9
  # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
@@ -65,7 +67,7 @@ module ActionDispatch
65
67
  @req.set_header env_name(key), value
66
68
  end
67
69
 
68
- # Add a value to a multivalued header like Vary or Accept-Encoding.
70
+ # Add a value to a multivalued header like +Vary+ or +Accept-Encoding+.
69
71
  def add(key, value)
70
72
  @req.add_header env_name(key), value
71
73
  end
@@ -16,7 +16,20 @@ module ActionDispatch
16
16
 
17
17
  included do
18
18
  mattr_accessor :ignore_accept_header, default: false
19
- cattr_accessor :return_only_media_type_on_content_type, default: false
19
+
20
+ def return_only_media_type_on_content_type=(value)
21
+ ActionDispatch.deprecator.warn(
22
+ "`config.action_dispatch.return_only_request_media_type_on_content_type` is deprecated and will" \
23
+ " be removed in Rails 7.2."
24
+ )
25
+ end
26
+
27
+ def return_only_media_type_on_content_type
28
+ ActionDispatch.deprecator.warn(
29
+ "`config.action_dispatch.return_only_request_media_type_on_content_type` is deprecated and will" \
30
+ " be removed in Rails 7.2."
31
+ )
32
+ end
20
33
  end
21
34
 
22
35
  # The MIME type of the HTTP request, such as Mime[:xml].
@@ -33,19 +46,6 @@ module ActionDispatch
33
46
  end
34
47
  end
35
48
 
36
- def content_type
37
- if self.class.return_only_media_type_on_content_type
38
- ActiveSupport::Deprecation.warn(
39
- "Rails 7.1 will return Content-Type header without modification." \
40
- " If you want just the MIME type, please use `#media_type` instead."
41
- )
42
-
43
- content_mime_type&.to_s
44
- else
45
- super
46
- end
47
- end
48
-
49
49
  def has_content_type? # :nodoc:
50
50
  get_header "CONTENT_TYPE"
51
51
  end
@@ -72,7 +72,7 @@ module ActionDispatch
72
72
  # GET /posts/5.xhtml | request.format => Mime[:html]
73
73
  # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
74
74
  #
75
- def format(view_path = [])
75
+ def format(_view_path = nil)
76
76
  formats.first || Mime::NullType.instance
77
77
  end
78
78
 
@@ -81,7 +81,7 @@ module ActionDispatch
81
81
  v = if params_readable?
82
82
  Array(Mime[parameters[:format]])
83
83
  elsif use_accept_header && valid_accept_header
84
- accepts
84
+ accepts.dup
85
85
  elsif extension_format = format_from_path_extension
86
86
  [extension_format]
87
87
  elsif xhr?
@@ -90,7 +90,7 @@ module ActionDispatch
90
90
  [Mime[:html]]
91
91
  end
92
92
 
93
- v = v.select do |format|
93
+ v.select! do |format|
94
94
  format.symbol || format.ref == "*/*"
95
95
  end
96
96
 
@@ -132,7 +132,7 @@ module ActionDispatch
132
132
  # Sets the \formats by string extensions. This differs from #format= by allowing you
133
133
  # to set multiple, ordered formats, which is useful when you want to have a fallback.
134
134
  #
135
- # In this example, the +:iphone+ format will be used if it's available, otherwise it'll fallback
135
+ # In this example, the +:iphone+ format will be used if it's available, otherwise it'll fall back
136
136
  # to the +:html+ format.
137
137
  #
138
138
  # class ApplicationController < ActionController::Base
@@ -172,22 +172,22 @@ module ActionDispatch
172
172
  # in which case we assume you're a browser and send HTML.
173
173
  BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
174
174
 
175
- def params_readable? # :doc:
175
+ def params_readable?
176
176
  parameters[:format]
177
177
  rescue *RESCUABLE_MIME_FORMAT_ERRORS
178
178
  false
179
179
  end
180
180
 
181
- def valid_accept_header # :doc:
181
+ def valid_accept_header
182
182
  (xhr? && (accept.present? || content_mime_type)) ||
183
183
  (accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
184
184
  end
185
185
 
186
- def use_accept_header # :doc:
186
+ def use_accept_header
187
187
  !self.class.ignore_accept_header
188
188
  end
189
189
 
190
- def format_from_path_extension # :doc:
190
+ def format_from_path_extension
191
191
  path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
192
192
  if match = path && path.match(/\.(\w+)\z/)
193
193
  Mime[match.captures.first]
@@ -11,6 +11,7 @@ module Mime
11
11
  def initialize
12
12
  @mimes = []
13
13
  @symbols = []
14
+ @symbols_set = Set.new
14
15
  end
15
16
 
16
17
  def each(&block)
@@ -19,17 +20,25 @@ module Mime
19
20
 
20
21
  def <<(type)
21
22
  @mimes << type
22
- @symbols << type.to_sym
23
+ sym_type = type.to_sym
24
+ @symbols << sym_type
25
+ @symbols_set << sym_type
23
26
  end
24
27
 
25
28
  def delete_if
26
29
  @mimes.delete_if do |x|
27
30
  if yield x
28
- @symbols.delete(x.to_sym)
31
+ sym_type = x.to_sym
32
+ @symbols.delete(sym_type)
33
+ @symbols_set.delete(sym_type)
29
34
  true
30
35
  end
31
36
  end
32
37
  end
38
+
39
+ def valid_symbols?(symbols) # :nodoc
40
+ symbols.all? { |s| @symbols_set.include?(s) }
41
+ end
33
42
  end
34
43
 
35
44
  SET = Mimes.new
@@ -42,6 +51,14 @@ module Mime
42
51
  Type.lookup_by_extension(type)
43
52
  end
44
53
 
54
+ def symbols
55
+ SET.symbols
56
+ end
57
+
58
+ def valid_symbols?(symbols) # :nodoc:
59
+ SET.valid_symbols?(symbols)
60
+ end
61
+
45
62
  def fetch(type, &block)
46
63
  return type if type.is_a?(Type)
47
64
  EXTENSION_LOOKUP.fetch(type.to_s, &block)
@@ -135,13 +152,20 @@ module Mime
135
152
 
136
153
  class << self
137
154
  TRAILING_STAR_REGEXP = /^(text|application)\/\*/
138
- PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
155
+ # all media-type parameters need to be before the q-parameter
156
+ # https://www.rfc-editor.org/rfc/rfc7231#section-5.3.2
157
+ PARAMETER_SEPARATOR_REGEXP = /;\s*q="?/
158
+ ACCEPT_HEADER_REGEXP = /[^,\s"](?:[^,"]|"[^"]*")*/
139
159
 
140
160
  def register_callback(&block)
141
161
  @register_callbacks << block
142
162
  end
143
163
 
144
164
  def lookup(string)
165
+ return LOOKUP[string] if LOOKUP.key?(string)
166
+
167
+ # fallback to the media-type without parameters if it was not found
168
+ string = string.split(";", 2)[0]&.rstrip
145
169
  LOOKUP[string] || Type.new(string)
146
170
  end
147
171
 
@@ -171,12 +195,14 @@ module Mime
171
195
 
172
196
  def parse(accept_header)
173
197
  if !accept_header.include?(",")
174
- accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
175
- return [] unless accept_header
176
- parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
198
+ if (index = accept_header.index(PARAMETER_SEPARATOR_REGEXP))
199
+ accept_header = accept_header[0, index].strip
200
+ end
201
+ return [] if accept_header.blank?
202
+ parse_trailing_star(accept_header) || Array(Mime::Type.lookup(accept_header))
177
203
  else
178
204
  list, index = [], 0
179
- accept_header.split(",").each do |header|
205
+ accept_header.scan(ACCEPT_HEADER_REGEXP).each do |header|
180
206
  params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
181
207
 
182
208
  next unless params
@@ -199,10 +225,10 @@ module Mime
199
225
  end
200
226
 
201
227
  # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
202
- # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
228
+ # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]]</tt>.
203
229
  #
204
230
  # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
205
- # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
231
+ # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]]</tt>.
206
232
  def parse_data_with_trailing_star(type)
207
233
  Mime::SET.select { |m| m.match?(type) }
208
234
  end
@@ -225,7 +251,7 @@ module Mime
225
251
  attr_reader :hash
226
252
 
227
253
  MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
228
- MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?#{MIME_NAME}#{Regexp.escape('"')}?"
254
+ MIME_PARAMETER_VALUE = "(?:#{MIME_NAME}|\"[^\"\r\\\\]*\")"
229
255
  MIME_PARAMETER = "\s*;\s*#{MIME_NAME}(?:=#{MIME_PARAMETER_VALUE})?"
230
256
  MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?>#{MIME_PARAMETER})*\s*)\z/
231
257
 
@@ -291,7 +317,7 @@ module Mime
291
317
  end
292
318
 
293
319
  def html?
294
- (symbol == :html) || /html/.match?(@string)
320
+ (symbol == :html) || @string.include?("html")
295
321
  end
296
322
 
297
323
  def all?; false; end
@@ -18,6 +18,7 @@ Mime::Type.register "image/gif", :gif, [], %w(gif)
18
18
  Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
19
19
  Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
20
20
  Mime::Type.register "image/svg+xml", :svg
21
+ Mime::Type.register "image/webp", :webp, [], %w(webp)
21
22
 
22
23
  Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
23
24
 
@@ -43,7 +44,8 @@ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
43
44
 
44
45
  # https://www.ietf.org/rfc/rfc4627.txt
45
46
  # http://www.json.org/JSONRequest.html
46
- Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
47
+ # https://www.ietf.org/rfc/rfc7807.txt
48
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/problem+json )
47
49
 
48
50
  Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
49
51
  Mime::Type.register "application/zip", :zip, [], %w(zip)
@@ -76,7 +76,7 @@ module ActionDispatch
76
76
  end
77
77
 
78
78
  # Returns a hash with the \parameters used to form the \path of the request.
79
- # Returned hash keys are strings:
79
+ # Returned hash keys are symbols:
80
80
  #
81
81
  # { action: "my_action", controller: "my_controller" }
82
82
  def path_parameters