actionpack 4.2.10 → 7.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,34 +1,55 @@
1
- require 'active_support/rails'
2
- require 'abstract_controller'
3
- require 'action_dispatch'
4
- require 'action_controller/metal/live'
5
- require 'action_controller/metal/strong_parameters'
1
+ # frozen_string_literal: true
6
2
 
3
+ # :markup: markdown
4
+
5
+ require "abstract_controller"
6
+ require "action_dispatch"
7
+ require "action_controller/deprecator"
8
+ require "action_controller/metal/strong_parameters"
9
+ require "action_controller/metal/exceptions"
10
+
11
+ # # Action Controller
12
+ #
13
+ # Action Controller is a module of Action Pack.
14
+ #
15
+ # Action Controller provides a base controller class that can be subclassed to
16
+ # implement filters and actions to handle requests. The result of an action is
17
+ # typically content generated from views.
7
18
  module ActionController
8
19
  extend ActiveSupport::Autoload
9
20
 
21
+ autoload :API
10
22
  autoload :Base
11
- autoload :Caching
12
23
  autoload :Metal
13
- autoload :Middleware
24
+ autoload :Renderer
25
+ autoload :FormBuilder
26
+
27
+ eager_autoload do
28
+ autoload :Caching
29
+ end
14
30
 
15
31
  autoload_under "metal" do
16
- autoload :Compatibility
32
+ autoload :AllowBrowser
17
33
  autoload :ConditionalGet
34
+ autoload :ContentSecurityPolicy
18
35
  autoload :Cookies
19
36
  autoload :DataStreaming
37
+ autoload :DefaultHeaders
20
38
  autoload :EtagWithTemplateDigest
39
+ autoload :EtagWithFlash
40
+ autoload :PermissionsPolicy
21
41
  autoload :Flash
22
- autoload :ForceSSL
23
42
  autoload :Head
24
43
  autoload :Helpers
25
- autoload :HideActions
26
44
  autoload :HttpAuthentication
45
+ autoload :BasicImplicitRender
27
46
  autoload :ImplicitRender
28
47
  autoload :Instrumentation
48
+ autoload :Live
49
+ autoload :Logging
29
50
  autoload :MimeResponds
30
51
  autoload :ParamsWrapper
31
- autoload :RackDelegation
52
+ autoload :RateLimiting
32
53
  autoload :Redirecting
33
54
  autoload :Renderers
34
55
  autoload :Rendering
@@ -36,23 +57,24 @@ module ActionController
36
57
  autoload :Rescue
37
58
  autoload :Streaming
38
59
  autoload :StrongParameters
60
+ autoload :ParameterEncoding
39
61
  autoload :Testing
40
62
  autoload :UrlFor
41
63
  end
42
64
 
43
- autoload :TestCase, 'action_controller/test_case'
44
- autoload :TemplateAssertions, 'action_controller/test_case'
65
+ autoload_under "api" do
66
+ autoload :ApiRendering
67
+ end
45
68
 
46
- def self.eager_load!
47
- super
48
- ActionController::Caching.eager_load!
69
+ autoload_at "action_controller/test_case" do
70
+ autoload :TestCase
71
+ autoload :TestRequest
72
+ autoload :TemplateAssertions
49
73
  end
50
74
  end
51
75
 
52
76
  # Common Active Support usage in Action Controller
53
- require 'active_support/core_ext/module/attribute_accessors'
54
- require 'active_support/core_ext/load_error'
55
- require 'active_support/core_ext/module/attr_internal'
56
- require 'active_support/core_ext/name_error'
57
- require 'active_support/core_ext/uri'
58
- require 'active_support/inflector'
77
+ require "active_support/core_ext/module/attribute_accessors"
78
+ require "active_support/core_ext/module/attr_internal"
79
+ require "active_support/core_ext/name_error"
80
+ require "active_support/inflector"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "rack/version"
6
+
7
+ module ActionDispatch
8
+ module Constants
9
+ # Response Header keys for Rack 2.x and 3.x
10
+ if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
11
+ VARY = "Vary"
12
+ CONTENT_ENCODING = "Content-Encoding"
13
+ CONTENT_SECURITY_POLICY = "Content-Security-Policy"
14
+ CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
15
+ LOCATION = "Location"
16
+ FEATURE_POLICY = "Feature-Policy"
17
+ X_REQUEST_ID = "X-Request-Id"
18
+ X_CASCADE = "X-Cascade"
19
+ SERVER_TIMING = "Server-Timing"
20
+ STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security"
21
+ else
22
+ VARY = "vary"
23
+ CONTENT_ENCODING = "content-encoding"
24
+ CONTENT_SECURITY_POLICY = "content-security-policy"
25
+ CONTENT_SECURITY_POLICY_REPORT_ONLY = "content-security-policy-report-only"
26
+ LOCATION = "location"
27
+ FEATURE_POLICY = "feature-policy"
28
+ X_REQUEST_ID = "x-request-id"
29
+ X_CASCADE = "x-cascade"
30
+ SERVER_TIMING = "server-timing"
31
+ STRICT_TRANSPORT_SECURITY = "strict-transport-security"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ def self.deprecator # :nodoc:
7
+ @deprecator ||= ActiveSupport::Deprecation.new
8
+ end
9
+ end
@@ -1,26 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
1
4
 
2
5
  module ActionDispatch
3
6
  module Http
4
7
  module Cache
5
8
  module Request
6
-
7
- HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
8
- HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
9
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
10
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
9
11
 
10
12
  def if_modified_since
11
- if since = env[HTTP_IF_MODIFIED_SINCE]
13
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
12
14
  Time.rfc2822(since) rescue nil
13
15
  end
14
16
  end
15
17
 
16
18
  def if_none_match
17
- env[HTTP_IF_NONE_MATCH]
19
+ get_header HTTP_IF_NONE_MATCH
18
20
  end
19
21
 
20
22
  def if_none_match_etags
21
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
22
- etag.gsub(/^\"|\"$/, "")
23
- end
23
+ if_none_match ? if_none_match.split(",").each(&:strip!) : []
24
24
  end
25
25
 
26
26
  def not_modified?(modified_at)
@@ -29,13 +29,13 @@ module ActionDispatch
29
29
 
30
30
  def etag_matches?(etag)
31
31
  if etag
32
- etag = etag.gsub(/^\"|\"$/, "")
33
- if_none_match_etags.include?(etag)
32
+ validators = if_none_match_etags
33
+ validators.include?(etag) || validators.include?("*")
34
34
  end
35
35
  end
36
36
 
37
- # Check response freshness (Last-Modified and ETag) against request
38
- # If-Modified-Since and If-None-Match conditions. If both headers are
37
+ # Check response freshness (`Last-Modified` and ETag) against request
38
+ # `If-Modified-Since` and `If-None-Match` conditions. If both headers are
39
39
  # supplied, both must match, or the request is not considered fresh.
40
40
  def fresh?(response)
41
41
  last_modified = if_modified_since
@@ -51,67 +51,107 @@ module ActionDispatch
51
51
  end
52
52
 
53
53
  module Response
54
- attr_reader :cache_control, :etag
55
- alias :etag? :etag
54
+ attr_reader :cache_control
56
55
 
57
56
  def last_modified
58
- if last = headers[LAST_MODIFIED]
57
+ if last = get_header(LAST_MODIFIED)
59
58
  Time.httpdate(last)
60
59
  end
61
60
  end
62
61
 
63
62
  def last_modified?
64
- headers.include?(LAST_MODIFIED)
63
+ has_header? LAST_MODIFIED
65
64
  end
66
65
 
67
66
  def last_modified=(utc_time)
68
- headers[LAST_MODIFIED] = utc_time.httpdate
67
+ set_header LAST_MODIFIED, utc_time.httpdate
69
68
  end
70
69
 
71
70
  def date
72
- if date_header = headers[DATE]
71
+ if date_header = get_header(DATE)
73
72
  Time.httpdate(date_header)
74
73
  end
75
74
  end
76
75
 
77
76
  def date?
78
- headers.include?(DATE)
77
+ has_header? DATE
79
78
  end
80
79
 
81
80
  def date=(utc_time)
82
- headers[DATE] = utc_time.httpdate
81
+ set_header DATE, utc_time.httpdate
82
+ end
83
+
84
+ # This method sets a weak ETag validator on the response so browsers and proxies
85
+ # may cache the response, keyed on the ETag. On subsequent requests, the
86
+ # `If-None-Match` header is set to the cached ETag. If it matches the current
87
+ # ETag, we can return a `304 Not Modified` response with no body, letting the
88
+ # browser or proxy know that their cache is current. Big savings in request time
89
+ # and network bandwidth.
90
+ #
91
+ # Weak ETags are considered to be semantically equivalent but not byte-for-byte
92
+ # identical. This is perfect for browser caching of HTML pages where we don't
93
+ # care about exact equality, just what the user is viewing.
94
+ #
95
+ # Strong ETags are considered byte-for-byte identical. They allow a browser or
96
+ # proxy cache to support `Range` requests, useful for paging through a PDF file
97
+ # or scrubbing through a video. Some CDNs only support strong ETags and will
98
+ # ignore weak ETags entirely.
99
+ #
100
+ # Weak ETags are what we almost always need, so they're the default. Check out
101
+ # #strong_etag= to provide a strong ETag validator.
102
+ def etag=(weak_validators)
103
+ self.weak_etag = weak_validators
104
+ end
105
+
106
+ def weak_etag=(weak_validators)
107
+ set_header "ETag", generate_weak_etag(weak_validators)
83
108
  end
84
109
 
85
- def etag=(etag)
86
- key = ActiveSupport::Cache.expand_cache_key(etag)
87
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
110
+ def strong_etag=(strong_validators)
111
+ set_header "ETag", generate_strong_etag(strong_validators)
112
+ end
113
+
114
+ def etag?; etag; end
115
+
116
+ # True if an ETag is set, and it's a weak validator (preceded with `W/`).
117
+ def weak_etag?
118
+ etag? && etag.start_with?('W/"')
119
+ end
120
+
121
+ # True if an ETag is set, and it isn't a weak validator (not preceded with
122
+ # `W/`).
123
+ def strong_etag?
124
+ etag? && !weak_etag?
88
125
  end
89
126
 
90
127
  private
128
+ DATE = "Date"
129
+ LAST_MODIFIED = "Last-Modified"
130
+ SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
91
131
 
92
- DATE = 'Date'.freeze
93
- LAST_MODIFIED = "Last-Modified".freeze
94
- ETAG = "ETag".freeze
95
- CACHE_CONTROL = "Cache-Control".freeze
96
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
132
+ def generate_weak_etag(validators)
133
+ "W/#{generate_strong_etag(validators)}"
134
+ end
135
+
136
+ def generate_strong_etag(validators)
137
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
138
+ end
97
139
 
98
140
  def cache_control_segments
99
- if cache_control = self[CACHE_CONTROL]
100
- cache_control.delete(' ').split(',')
101
- else
102
- []
141
+ if cache_control = _cache_control
142
+ cache_control.delete(" ").split(",")
103
143
  end
104
144
  end
105
145
 
106
146
  def cache_control_headers
107
147
  cache_control = {}
108
148
 
109
- cache_control_segments.each do |segment|
110
- directive, argument = segment.split('=', 2)
149
+ cache_control_segments&.each do |segment|
150
+ directive, argument = segment.split("=", 2)
111
151
 
112
152
  if SPECIAL_KEYS.include? directive
113
- key = directive.tr('-', '_')
114
- cache_control[key.to_sym] = argument || true
153
+ directive.tr!("-", "_")
154
+ cache_control[directive.to_sym] = argument || true
115
155
  else
116
156
  cache_control[:extras] ||= []
117
157
  cache_control[:extras] << segment
@@ -123,52 +163,68 @@ module ActionDispatch
123
163
 
124
164
  def prepare_cache_control!
125
165
  @cache_control = cache_control_headers
126
- @etag = self[ETAG]
127
166
  end
128
167
 
168
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
169
+ NO_STORE = "no-store"
170
+ NO_CACHE = "no-cache"
171
+ PUBLIC = "public"
172
+ PRIVATE = "private"
173
+ MUST_REVALIDATE = "must-revalidate"
174
+
129
175
  def handle_conditional_get!
130
- if etag? || last_modified? || !@cache_control.empty?
131
- set_conditional_cache_control!
176
+ # Normally default cache control setting is handled by ETag middleware. But, if
177
+ # an etag is already set, the middleware defaults to `no-cache` unless a default
178
+ # `Cache-Control` value is previously set. So, set a default one here.
179
+ if (etag? || last_modified?) && !self._cache_control
180
+ self._cache_control = DEFAULT_CACHE_CONTROL
132
181
  end
133
182
  end
134
183
 
135
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
136
- NO_CACHE = "no-cache".freeze
137
- PUBLIC = "public".freeze
138
- PRIVATE = "private".freeze
139
- MUST_REVALIDATE = "must-revalidate".freeze
184
+ def merge_and_normalize_cache_control!(cache_control)
185
+ control = cache_control_headers
186
+
187
+ return if control.empty? && cache_control.empty? # Let middleware handle default behavior
188
+
189
+ if cache_control.any?
190
+ # Any caching directive coming from a controller overrides no-cache/no-store in
191
+ # the default Cache-Control header.
192
+ control.delete(:no_cache)
193
+ control.delete(:no_store)
140
194
 
141
- def set_conditional_cache_control!
142
- control = {}
143
- cc_headers = cache_control_headers
144
- if extras = cc_headers.delete(:extras)
145
- @cache_control[:extras] ||= []
146
- @cache_control[:extras] += extras
147
- @cache_control[:extras].uniq!
195
+ if extras = control.delete(:extras)
196
+ cache_control[:extras] ||= []
197
+ cache_control[:extras] += extras
198
+ cache_control[:extras].uniq!
199
+ end
200
+
201
+ control.merge! cache_control
148
202
  end
149
203
 
150
- control.merge! cc_headers
151
- control.merge! @cache_control
204
+ options = []
152
205
 
153
- if control.empty?
154
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
206
+ if control[:no_store]
207
+ options << PRIVATE if control[:private]
208
+ options << NO_STORE
155
209
  elsif control[:no_cache]
156
- headers[CACHE_CONTROL] = NO_CACHE
157
- if control[:extras]
158
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
159
- end
210
+ options << PUBLIC if control[:public]
211
+ options << NO_CACHE
212
+ options.concat(control[:extras]) if control[:extras]
160
213
  else
161
- extras = control[:extras]
214
+ extras = control[:extras]
162
215
  max_age = control[:max_age]
216
+ stale_while_revalidate = control[:stale_while_revalidate]
217
+ stale_if_error = control[:stale_if_error]
163
218
 
164
- options = []
165
219
  options << "max-age=#{max_age.to_i}" if max_age
166
220
  options << (control[:public] ? PUBLIC : PRIVATE)
167
221
  options << MUST_REVALIDATE if control[:must_revalidate]
222
+ options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
223
+ options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
168
224
  options.concat(extras) if extras
169
-
170
- headers[CACHE_CONTROL] = options.join(", ")
171
225
  end
226
+
227
+ self._cache_control = options.join(", ")
172
228
  end
173
229
  end
174
230
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ module Http
7
+ class ContentDisposition # :nodoc:
8
+ def self.format(disposition:, filename:)
9
+ new(disposition: disposition, filename: filename).to_s
10
+ end
11
+
12
+ attr_reader :disposition, :filename
13
+
14
+ def initialize(disposition:, filename:)
15
+ @disposition = disposition
16
+ @filename = filename
17
+ end
18
+
19
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!\#$+.^_`|~-]/
20
+
21
+ def ascii_filename
22
+ 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
23
+ end
24
+
25
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!\#$&+.^_`|~-]/
26
+
27
+ def utf8_filename
28
+ "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
29
+ end
30
+
31
+ def to_s
32
+ if filename
33
+ "#{disposition}; #{ascii_filename}; #{utf8_filename}"
34
+ else
35
+ "#{disposition}"
36
+ end
37
+ end
38
+
39
+ private
40
+ def percent_escape(string, pattern)
41
+ string.gsub(pattern) do |char|
42
+ char.bytes.map { |byte| "%%%02X" % byte }.join
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end