actionpack 7.1.5.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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +308 -523
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +6 -2
  5. data/lib/abstract_controller/base.rb +104 -105
  6. data/lib/abstract_controller/caching/fragments.rb +50 -53
  7. data/lib/abstract_controller/caching.rb +8 -3
  8. data/lib/abstract_controller/callbacks.rb +70 -62
  9. data/lib/abstract_controller/collector.rb +7 -7
  10. data/lib/abstract_controller/deprecator.rb +2 -0
  11. data/lib/abstract_controller/error.rb +2 -0
  12. data/lib/abstract_controller/helpers.rb +71 -84
  13. data/lib/abstract_controller/logger.rb +4 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +13 -13
  16. data/lib/abstract_controller/translation.rb +12 -13
  17. data/lib/abstract_controller/url_for.rb +8 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api/api_rendering.rb +2 -0
  20. data/lib/action_controller/api.rb +76 -72
  21. data/lib/action_controller/base.rb +199 -126
  22. data/lib/action_controller/caching.rb +16 -14
  23. data/lib/action_controller/deprecator.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +21 -18
  25. data/lib/action_controller/log_subscriber.rb +23 -2
  26. data/lib/action_controller/metal/allow_browser.rb +133 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +217 -175
  29. data/lib/action_controller/metal/content_security_policy.rb +25 -24
  30. data/lib/action_controller/metal/cookies.rb +4 -2
  31. data/lib/action_controller/metal/data_streaming.rb +72 -63
  32. data/lib/action_controller/metal/default_headers.rb +5 -3
  33. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
  35. data/lib/action_controller/metal/exceptions.rb +16 -9
  36. data/lib/action_controller/metal/flash.rb +13 -14
  37. data/lib/action_controller/metal/head.rb +15 -11
  38. data/lib/action_controller/metal/helpers.rb +63 -55
  39. data/lib/action_controller/metal/http_authentication.rb +209 -201
  40. data/lib/action_controller/metal/implicit_render.rb +17 -15
  41. data/lib/action_controller/metal/instrumentation.rb +16 -14
  42. data/lib/action_controller/metal/live.rb +177 -128
  43. data/lib/action_controller/metal/logging.rb +6 -4
  44. data/lib/action_controller/metal/mime_responds.rb +151 -142
  45. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  46. data/lib/action_controller/metal/params_wrapper.rb +57 -59
  47. data/lib/action_controller/metal/permissions_policy.rb +22 -12
  48. data/lib/action_controller/metal/rate_limiting.rb +92 -0
  49. data/lib/action_controller/metal/redirecting.rb +213 -94
  50. data/lib/action_controller/metal/renderers.rb +78 -57
  51. data/lib/action_controller/metal/rendering.rb +111 -77
  52. data/lib/action_controller/metal/request_forgery_protection.rb +182 -143
  53. data/lib/action_controller/metal/rescue.rb +20 -9
  54. data/lib/action_controller/metal/streaming.rb +118 -195
  55. data/lib/action_controller/metal/strong_parameters.rb +720 -530
  56. data/lib/action_controller/metal/testing.rb +2 -0
  57. data/lib/action_controller/metal/url_for.rb +17 -15
  58. data/lib/action_controller/metal.rb +86 -60
  59. data/lib/action_controller/railtie.rb +36 -15
  60. data/lib/action_controller/railties/helpers.rb +2 -0
  61. data/lib/action_controller/renderer.rb +41 -36
  62. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +160 -131
  65. data/lib/action_controller.rb +5 -1
  66. data/lib/action_dispatch/constants.rb +8 -0
  67. data/lib/action_dispatch/deprecator.rb +2 -0
  68. data/lib/action_dispatch/http/cache.rb +163 -35
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +54 -39
  71. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +22 -22
  74. data/lib/action_dispatch/http/mime_negotiation.rb +89 -41
  75. data/lib/action_dispatch/http/mime_type.rb +25 -21
  76. data/lib/action_dispatch/http/mime_types.rb +3 -0
  77. data/lib/action_dispatch/http/param_builder.rb +187 -0
  78. data/lib/action_dispatch/http/param_error.rb +26 -0
  79. data/lib/action_dispatch/http/parameters.rb +14 -12
  80. data/lib/action_dispatch/http/permissions_policy.rb +25 -36
  81. data/lib/action_dispatch/http/query_parser.rb +55 -0
  82. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  83. data/lib/action_dispatch/http/request.rb +141 -92
  84. data/lib/action_dispatch/http/response.rb +137 -77
  85. data/lib/action_dispatch/http/upload.rb +18 -16
  86. data/lib/action_dispatch/http/url.rb +187 -89
  87. data/lib/action_dispatch/journey/formatter.rb +21 -9
  88. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  89. data/lib/action_dispatch/journey/gtg/simulator.rb +34 -11
  90. data/lib/action_dispatch/journey/gtg/transition_table.rb +47 -53
  91. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  92. data/lib/action_dispatch/journey/nodes/node.rb +8 -6
  93. data/lib/action_dispatch/journey/parser.rb +99 -195
  94. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  95. data/lib/action_dispatch/journey/route.rb +54 -38
  96. data/lib/action_dispatch/journey/router/utils.rb +22 -27
  97. data/lib/action_dispatch/journey/router.rb +63 -83
  98. data/lib/action_dispatch/journey/routes.rb +11 -2
  99. data/lib/action_dispatch/journey/scanner.rb +46 -42
  100. data/lib/action_dispatch/journey/visitors.rb +57 -23
  101. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  102. data/lib/action_dispatch/journey.rb +2 -0
  103. data/lib/action_dispatch/log_subscriber.rb +7 -1
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
  106. data/lib/action_dispatch/middleware/callbacks.rb +3 -1
  107. data/lib/action_dispatch/middleware/cookies.rb +125 -106
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +37 -8
  109. data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
  110. data/lib/action_dispatch/middleware/debug_view.rb +13 -5
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +18 -23
  112. data/lib/action_dispatch/middleware/executor.rb +19 -4
  113. data/lib/action_dispatch/middleware/flash.rb +63 -51
  114. data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +14 -12
  116. data/lib/action_dispatch/middleware/reloader.rb +5 -3
  117. data/lib/action_dispatch/middleware/remote_ip.rb +87 -77
  118. data/lib/action_dispatch/middleware/request_id.rb +16 -10
  119. data/lib/action_dispatch/middleware/server_timing.rb +4 -2
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +30 -8
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +16 -16
  125. data/lib/action_dispatch/middleware/ssl.rb +53 -40
  126. data/lib/action_dispatch/middleware/stack.rb +11 -10
  127. data/lib/action_dispatch/middleware/static.rb +33 -31
  128. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  141. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
  142. data/lib/action_dispatch/railtie.rb +23 -3
  143. data/lib/action_dispatch/request/session.rb +24 -21
  144. data/lib/action_dispatch/request/utils.rb +11 -3
  145. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  146. data/lib/action_dispatch/routing/inspector.rb +85 -60
  147. data/lib/action_dispatch/routing/mapper.rb +1031 -851
  148. data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
  149. data/lib/action_dispatch/routing/redirection.rb +47 -39
  150. data/lib/action_dispatch/routing/route_set.rb +79 -56
  151. data/lib/action_dispatch/routing/routes_proxy.rb +7 -4
  152. data/lib/action_dispatch/routing/url_for.rb +130 -125
  153. data/lib/action_dispatch/routing.rb +150 -148
  154. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  155. data/lib/action_dispatch/system_test_case.rb +91 -81
  156. data/lib/action_dispatch/system_testing/browser.rb +16 -23
  157. data/lib/action_dispatch/system_testing/driver.rb +2 -0
  158. data/lib/action_dispatch/system_testing/server.rb +2 -0
  159. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +34 -23
  160. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  161. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  162. data/lib/action_dispatch/testing/assertions/response.rb +52 -25
  163. data/lib/action_dispatch/testing/assertions/routing.rb +168 -87
  164. data/lib/action_dispatch/testing/assertions.rb +2 -0
  165. data/lib/action_dispatch/testing/integration.rb +233 -223
  166. data/lib/action_dispatch/testing/request_encoder.rb +11 -9
  167. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  168. data/lib/action_dispatch/testing/test_process.rb +11 -8
  169. data/lib/action_dispatch/testing/test_request.rb +3 -1
  170. data/lib/action_dispatch/testing/test_response.rb +27 -26
  171. data/lib/action_dispatch.rb +36 -32
  172. data/lib/action_pack/gem_version.rb +6 -4
  173. data/lib/action_pack/version.rb +3 -1
  174. data/lib/action_pack.rb +17 -16
  175. metadata +36 -32
  176. data/lib/action_dispatch/journey/parser.y +0 -50
  177. data/lib/action_dispatch/journey/parser_extras.rb +0 -31
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController # :nodoc:
6
+ module AllowBrowser
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Specify the browser versions that will be allowed to access all actions (or
11
+ # some, as limited by `only:` or `except:`). Only browsers matched in the hash
12
+ # or named set passed to `versions:` will be blocked if they're below the
13
+ # versions specified. This means that all other browsers, as well as agents that
14
+ # aren't reporting a user-agent header, will be allowed access.
15
+ #
16
+ # A browser that's blocked will by default be served the file in
17
+ # public/406-unsupported-browser.html with an HTTP status code of "406 Not
18
+ # Acceptable".
19
+ #
20
+ # In addition to specifically named browser versions, you can also pass
21
+ # `:modern` as the set to restrict support to browsers natively supporting webp
22
+ # images, web push, badges, import maps, CSS nesting, and CSS :has. This
23
+ # includes Safari 17.2+, Chrome 120+, Firefox 121+, Opera 106+.
24
+ #
25
+ # You can use https://caniuse.com to check for browser versions supporting the
26
+ # features you use.
27
+ #
28
+ # You can use `ActiveSupport::Notifications` to subscribe to events of browsers
29
+ # being blocked using the `browser_block.action_controller` event name.
30
+ #
31
+ # Examples:
32
+ #
33
+ # class ApplicationController < ActionController::Base
34
+ # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has
35
+ # allow_browser versions: :modern
36
+ # end
37
+ #
38
+ # class ApplicationController < ActionController::Base
39
+ # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has
40
+ # allow_browser versions: :modern, block: :handle_outdated_browser
41
+ #
42
+ # private
43
+ # def handle_outdated_browser
44
+ # render file: Rails.root.join("public/custom-error.html"), status: :not_acceptable
45
+ # end
46
+ # end
47
+ #
48
+ # class ApplicationController < ActionController::Base
49
+ # # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
50
+ # allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
51
+ # end
52
+ #
53
+ # class MessagesController < ApplicationController
54
+ # # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action.
55
+ # allow_browser versions: { opera: 104, chrome: 119 }, only: :show
56
+ # end
57
+ def allow_browser(versions:, block: -> { render file: Rails.root.join("public/406-unsupported-browser.html"), layout: false, status: :not_acceptable }, **options)
58
+ before_action -> { allow_browser(versions: versions, block: block) }, **options
59
+ end
60
+ end
61
+
62
+ private
63
+ def allow_browser(versions:, block:)
64
+ require "useragent"
65
+
66
+ if BrowserBlocker.new(request, versions: versions).blocked?
67
+ ActiveSupport::Notifications.instrument("browser_block.action_controller", request: request, versions: versions) do
68
+ block.is_a?(Symbol) ? send(block) : instance_exec(&block)
69
+ end
70
+ end
71
+ end
72
+
73
+ class BrowserBlocker # :nodoc:
74
+ SETS = {
75
+ modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false }
76
+ }
77
+
78
+ attr_reader :request, :versions
79
+
80
+ def initialize(request, versions:)
81
+ @request, @versions = request, versions
82
+ end
83
+
84
+ def blocked?
85
+ user_agent_version_reported? && unsupported_browser?
86
+ end
87
+
88
+ private
89
+ def parsed_user_agent
90
+ @parsed_user_agent ||= UserAgent.parse(request.user_agent)
91
+ end
92
+
93
+ def user_agent_version_reported?
94
+ request.user_agent.present? && parsed_user_agent.version.to_s.present?
95
+ end
96
+
97
+ def unsupported_browser?
98
+ version_guarded_browser? && version_below_minimum_required? && !bot?
99
+ end
100
+
101
+ def version_guarded_browser?
102
+ minimum_browser_version_for_browser != nil
103
+ end
104
+
105
+ def bot?
106
+ parsed_user_agent.bot?
107
+ end
108
+
109
+ def version_below_minimum_required?
110
+ if minimum_browser_version_for_browser
111
+ parsed_user_agent.version < UserAgent::Version.new(minimum_browser_version_for_browser.to_s)
112
+ else
113
+ true
114
+ end
115
+ end
116
+
117
+ def minimum_browser_version_for_browser
118
+ expanded_versions[normalized_browser_name]
119
+ end
120
+
121
+ def expanded_versions
122
+ @expanded_versions ||= (SETS[versions] || versions).with_indifferent_access
123
+ end
124
+
125
+ def normalized_browser_name
126
+ case name = parsed_user_agent.browser.downcase
127
+ when "internet explorer" then "ie"
128
+ else name
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionController
4
6
  module BasicImplicitRender # :nodoc:
5
7
  def send_action(method, *args)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/core_ext/object/try"
4
6
  require "active_support/core_ext/integer/time"
5
7
 
@@ -14,116 +16,123 @@ module ActionController
14
16
  end
15
17
 
16
18
  module ClassMethods
17
- # Allows you to consider additional controller-wide information when generating an ETag.
18
- # For example, if you serve pages tailored depending on who's logged in at the moment, you
19
- # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
20
- # of cached pages.
19
+ # Allows you to consider additional controller-wide information when generating
20
+ # an ETag. For example, if you serve pages tailored depending on who's logged in
21
+ # at the moment, you may want to add the current user id to be part of the ETag
22
+ # to prevent unauthorized displaying of cached pages.
21
23
  #
22
- # class InvoicesController < ApplicationController
23
- # etag { current_user&.id }
24
+ # class InvoicesController < ApplicationController
25
+ # etag { current_user&.id }
24
26
  #
25
- # def show
26
- # # Etag will differ even for the same invoice when it's viewed by a different current_user
27
- # @invoice = Invoice.find(params[:id])
28
- # fresh_when etag: @invoice
27
+ # def show
28
+ # # Etag will differ even for the same invoice when it's viewed by a different current_user
29
+ # @invoice = Invoice.find(params[:id])
30
+ # fresh_when etag: @invoice
31
+ # end
29
32
  # end
30
- # end
31
33
  def etag(&etagger)
32
34
  self.etaggers += [etagger]
33
35
  end
34
36
  end
35
37
 
36
- # Sets the +etag+, +last_modified+, or both on the response, and renders a
37
- # <tt>304 Not Modified</tt> response if the request is already fresh.
38
- #
39
- # ==== Options
40
- #
41
- # [+:etag+]
42
- # Sets a "weak" ETag validator on the response. See the +:weak_etag+ option.
43
- # [+:weak_etag+]
44
- # Sets a "weak" ETag validator on the response. Requests that specify an
45
- # +If-None-Match+ header may receive a <tt>304 Not Modified</tt> response
46
- # if the ETag matches exactly.
47
- #
48
- # A weak ETag indicates semantic equivalence, not byte-for-byte equality,
49
- # so they're good for caching HTML pages in browser caches. They can't be
50
- # used for responses that must be byte-identical, like serving +Range+
51
- # requests within a PDF file.
52
- # [+:strong_etag+]
53
- # Sets a "strong" ETag validator on the response. Requests that specify an
54
- # +If-None-Match+ header may receive a <tt>304 Not Modified</tt> response
55
- # if the ETag matches exactly.
56
- #
57
- # A strong ETag implies exact equality -- the response must match byte for
58
- # byte. This is necessary for serving +Range+ requests within a large
59
- # video or PDF file, for example, or for compatibility with some CDNs that
60
- # don't support weak ETags.
61
- # [+:last_modified+]
62
- # Sets a "weak" last-update validator on the response. Subsequent requests
63
- # that specify an +If-Modified-Since+ header may receive a <tt>304 Not Modified</tt>
64
- # response if +last_modified+ <= +If-Modified-Since+.
65
- # [+:public+]
66
- # By default the +Cache-Control+ header is private. Set this option to
67
- # +true+ if you want your application to be cacheable by other devices,
68
- # such as proxy caches.
69
- # [+:cache_control+]
70
- # When given, will overwrite an existing +Cache-Control+ header. For a
71
- # list of +Cache-Control+ directives, see the {article on
72
- # MDN}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control].
73
- # [+:template+]
74
- # By default, the template digest for the current controller/action is
75
- # included in ETags. If the action renders a different template, you can
76
- # include its digest instead. If the action doesn't render a template at
77
- # all, you can pass <tt>template: false</tt> to skip any attempt to check
78
- # for a template digest.
79
- #
80
- # ==== Examples
81
- #
82
- # def show
83
- # @article = Article.find(params[:id])
84
- # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
85
- # end
86
- #
87
- # This will send a <tt>304 Not Modified</tt> response if the request
88
- # specifies a matching ETag and +If-Modified-Since+ header. Otherwise, it
89
- # will render the +show+ template.
38
+ # Sets the `etag`, `last_modified`, or both on the response, and renders a `304
39
+ # Not Modified` response if the request is already fresh.
40
+ #
41
+ # #### Options
42
+ #
43
+ # `:etag`
44
+ # : Sets a "weak" ETag validator on the response. See the `:weak_etag` option.
45
+ #
46
+ # `:weak_etag`
47
+ # : Sets a "weak" ETag validator on the response. Requests that specify an
48
+ # `If-None-Match` header may receive a `304 Not Modified` response if the
49
+ # ETag matches exactly.
50
+ #
51
+ # : A weak ETag indicates semantic equivalence, not byte-for-byte equality, so
52
+ # they're good for caching HTML pages in browser caches. They can't be used
53
+ # for responses that must be byte-identical, like serving `Range` requests
54
+ # within a PDF file.
55
+ #
56
+ # `:strong_etag`
57
+ # : Sets a "strong" ETag validator on the response. Requests that specify an
58
+ # `If-None-Match` header may receive a `304 Not Modified` response if the
59
+ # ETag matches exactly.
60
+ #
61
+ # : A strong ETag implies exact equality -- the response must match byte for
62
+ # byte. This is necessary for serving `Range` requests within a large video
63
+ # or PDF file, for example, or for compatibility with some CDNs that don't
64
+ # support weak ETags.
65
+ #
66
+ # `:last_modified`
67
+ # : Sets a "weak" last-update validator on the response. Subsequent requests
68
+ # that specify an `If-Modified-Since` header may receive a `304 Not
69
+ # Modified` response if `last_modified` <= `If-Modified-Since`.
70
+ #
71
+ # `:public`
72
+ # : By default the `Cache-Control` header is private. Set this option to
73
+ # `true` if you want your application to be cacheable by other devices, such
74
+ # as proxy caches.
75
+ #
76
+ # `:cache_control`
77
+ # : When given, will overwrite an existing `Cache-Control` header. For a list
78
+ # of `Cache-Control` directives, see the [article on
79
+ # MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
80
+ #
81
+ # `:template`
82
+ # : By default, the template digest for the current controller/action is
83
+ # included in ETags. If the action renders a different template, you can
84
+ # include its digest instead. If the action doesn't render a template at
85
+ # all, you can pass `template: false` to skip any attempt to check for a
86
+ # template digest.
87
+ #
88
+ #
89
+ # #### Examples
90
+ #
91
+ # def show
92
+ # @article = Article.find(params[:id])
93
+ # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
94
+ # end
95
+ #
96
+ # This will send a `304 Not Modified` response if the request specifies a
97
+ # matching ETag and `If-Modified-Since` header. Otherwise, it will render the
98
+ # `show` template.
90
99
  #
91
100
  # You can also just pass a record:
92
101
  #
93
- # def show
94
- # @article = Article.find(params[:id])
95
- # fresh_when(@article)
96
- # end
102
+ # def show
103
+ # @article = Article.find(params[:id])
104
+ # fresh_when(@article)
105
+ # end
97
106
  #
98
- # +etag+ will be set to the record, and +last_modified+ will be set to the
99
- # record's +updated_at+.
107
+ # `etag` will be set to the record, and `last_modified` will be set to the
108
+ # record's `updated_at`.
100
109
  #
101
- # You can also pass an object that responds to +maximum+, such as a
102
- # collection of records:
110
+ # You can also pass an object that responds to `maximum`, such as a collection
111
+ # of records:
103
112
  #
104
- # def index
105
- # @articles = Article.all
106
- # fresh_when(@articles)
107
- # end
113
+ # def index
114
+ # @articles = Article.all
115
+ # fresh_when(@articles)
116
+ # end
108
117
  #
109
- # In this case, +etag+ will be set to the collection, and +last_modified+
110
- # will be set to <tt>maximum(:updated_at)</tt> (the timestamp of the most
111
- # recently updated record).
118
+ # In this case, `etag` will be set to the collection, and `last_modified` will
119
+ # be set to `maximum(:updated_at)` (the timestamp of the most recently updated
120
+ # record).
112
121
  #
113
- # When passing a record or a collection, you can still specify other
114
- # options, such as +:public+ and +:cache_control+:
122
+ # When passing a record or a collection, you can still specify other options,
123
+ # such as `:public` and `:cache_control`:
115
124
  #
116
- # def show
117
- # @article = Article.find(params[:id])
118
- # fresh_when(@article, public: true, cache_control: { no_cache: true })
119
- # end
125
+ # def show
126
+ # @article = Article.find(params[:id])
127
+ # fresh_when(@article, public: true, cache_control: { no_cache: true })
128
+ # end
120
129
  #
121
- # The above will set <tt>Cache-Control: public, no-cache</tt> in the response.
130
+ # The above will set `Cache-Control: public, no-cache` in the response.
122
131
  #
123
132
  # When rendering a different template than the controller/action's default
124
133
  # template, you can indicate which digest to include in the ETag:
125
134
  #
126
- # before_action { fresh_when @article, template: "widgets/show" }
135
+ # before_action { fresh_when @article, template: "widgets/show" }
127
136
  #
128
137
  def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, cache_control: {}, template: nil)
129
138
  response.cache_control.delete(:no_store)
@@ -145,131 +154,138 @@ module ActionController
145
154
  head :not_modified if request.fresh?(response)
146
155
  end
147
156
 
148
- # Sets the +etag+ and/or +last_modified+ on the response and checks them
149
- # against the request. If the request doesn't match the provided options, it
150
- # is considered stale, and the response should be rendered from scratch.
151
- # Otherwise, it is fresh, and a <tt>304 Not Modified</tt> is sent.
157
+ # Sets the `etag` and/or `last_modified` on the response and checks them against
158
+ # the request. If the request doesn't match the provided options, it is
159
+ # considered stale, and the response should be rendered from scratch. Otherwise,
160
+ # it is fresh, and a `304 Not Modified` is sent.
152
161
  #
153
- # ==== Options
162
+ # #### Options
154
163
  #
155
164
  # See #fresh_when for supported options.
156
165
  #
157
- # ==== Examples
166
+ # #### Examples
158
167
  #
159
- # def show
160
- # @article = Article.find(params[:id])
168
+ # def show
169
+ # @article = Article.find(params[:id])
161
170
  #
162
- # if stale?(etag: @article, last_modified: @article.updated_at)
163
- # @statistics = @article.really_expensive_call
164
- # respond_to do |format|
165
- # # all the supported formats
171
+ # if stale?(etag: @article, last_modified: @article.updated_at)
172
+ # @statistics = @article.really_expensive_call
173
+ # respond_to do |format|
174
+ # # all the supported formats
175
+ # end
166
176
  # end
167
177
  # end
168
- # end
169
178
  #
170
179
  # You can also just pass a record:
171
180
  #
172
- # def show
173
- # @article = Article.find(params[:id])
181
+ # def show
182
+ # @article = Article.find(params[:id])
174
183
  #
175
- # if stale?(@article)
176
- # @statistics = @article.really_expensive_call
177
- # respond_to do |format|
178
- # # all the supported formats
184
+ # if stale?(@article)
185
+ # @statistics = @article.really_expensive_call
186
+ # respond_to do |format|
187
+ # # all the supported formats
188
+ # end
179
189
  # end
180
190
  # end
181
- # end
182
191
  #
183
- # +etag+ will be set to the record, and +last_modified+ will be set to the
184
- # record's +updated_at+.
192
+ # `etag` will be set to the record, and `last_modified` will be set to the
193
+ # record's `updated_at`.
185
194
  #
186
- # You can also pass an object that responds to +maximum+, such as a
187
- # collection of records:
195
+ # You can also pass an object that responds to `maximum`, such as a collection
196
+ # of records:
188
197
  #
189
- # def index
190
- # @articles = Article.all
198
+ # def index
199
+ # @articles = Article.all
191
200
  #
192
- # if stale?(@articles)
193
- # @statistics = @articles.really_expensive_call
194
- # respond_to do |format|
195
- # # all the supported formats
201
+ # if stale?(@articles)
202
+ # @statistics = @articles.really_expensive_call
203
+ # respond_to do |format|
204
+ # # all the supported formats
205
+ # end
196
206
  # end
197
207
  # end
198
- # end
199
208
  #
200
- # In this case, +etag+ will be set to the collection, and +last_modified+
201
- # will be set to <tt>maximum(:updated_at)</tt> (the timestamp of the most
202
- # recently updated record).
209
+ # In this case, `etag` will be set to the collection, and `last_modified` will
210
+ # be set to `maximum(:updated_at)` (the timestamp of the most recently updated
211
+ # record).
203
212
  #
204
- # When passing a record or a collection, you can still specify other
205
- # options, such as +:public+ and +:cache_control+:
213
+ # When passing a record or a collection, you can still specify other options,
214
+ # such as `:public` and `:cache_control`:
206
215
  #
207
- # def show
208
- # @article = Article.find(params[:id])
216
+ # def show
217
+ # @article = Article.find(params[:id])
209
218
  #
210
- # if stale?(@article, public: true, cache_control: { no_cache: true })
211
- # @statistics = @articles.really_expensive_call
212
- # respond_to do |format|
213
- # # all the supported formats
219
+ # if stale?(@article, public: true, cache_control: { no_cache: true })
220
+ # @statistics = @articles.really_expensive_call
221
+ # respond_to do |format|
222
+ # # all the supported formats
223
+ # end
214
224
  # end
215
225
  # end
216
- # end
217
226
  #
218
- # The above will set <tt>Cache-Control: public, no-cache</tt> in the response.
227
+ # The above will set `Cache-Control: public, no-cache` in the response.
219
228
  #
220
229
  # When rendering a different template than the controller/action's default
221
230
  # template, you can indicate which digest to include in the ETag:
222
231
  #
223
- # def show
224
- # super if stale?(@article, template: "widgets/show")
225
- # end
232
+ # def show
233
+ # super if stale?(@article, template: "widgets/show")
234
+ # end
226
235
  #
227
236
  def stale?(object = nil, **freshness_kwargs)
228
237
  fresh_when(object, **freshness_kwargs)
229
238
  !request.fresh?(response)
230
239
  end
231
240
 
232
- # Sets the +Cache-Control+ header, overwriting existing directives. This
233
- # method will also ensure an HTTP +Date+ header for client compatibility.
241
+ # Sets the `Cache-Control` header, overwriting existing directives. This method
242
+ # will also ensure an HTTP `Date` header for client compatibility.
243
+ #
244
+ # Defaults to issuing the `private` directive, so that intermediate caches must
245
+ # not cache the response.
246
+ #
247
+ # #### Options
248
+ #
249
+ # `:public`
250
+ # : If true, replaces the default `private` directive with the `public`
251
+ # directive.
252
+ #
253
+ # `:must_revalidate`
254
+ # : If true, adds the `must-revalidate` directive.
234
255
  #
235
- # Defaults to issuing the +private+ directive, so that intermediate caches
236
- # must not cache the response.
256
+ # `:stale_while_revalidate`
257
+ # : Sets the value of the `stale-while-revalidate` directive.
237
258
  #
238
- # ==== Options
259
+ # `:stale_if_error`
260
+ # : Sets the value of the `stale-if-error` directive.
239
261
  #
240
- # [+:public+]
241
- # If true, replaces the default +private+ directive with the +public+
242
- # directive.
243
- # [+:must_revalidate+]
244
- # If true, adds the +must-revalidate+ directive.
245
- # [+:stale_while_revalidate+]
246
- # Sets the value of the +stale-while-revalidate+ directive.
247
- # [+:stale_if_error+]
248
- # Sets the value of the +stale-if-error+ directive.
262
+ # `:immutable`
263
+ # : If true, adds the `immutable` directive.
249
264
  #
250
- # Any additional key-value pairs are concatenated as directives. For a list
251
- # of supported +Cache-Control+ directives, see the {article on
252
- # MDN}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control].
253
265
  #
254
- # ==== Examples
266
+ # Any additional key-value pairs are concatenated as directives. For a list of
267
+ # supported `Cache-Control` directives, see the [article on
268
+ # MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
255
269
  #
256
- # expires_in 10.minutes
257
- # # => Cache-Control: max-age=600, private
270
+ # #### Examples
258
271
  #
259
- # expires_in 10.minutes, public: true
260
- # # => Cache-Control: max-age=600, public
272
+ # expires_in 10.minutes
273
+ # # => Cache-Control: max-age=600, private
261
274
  #
262
- # expires_in 10.minutes, public: true, must_revalidate: true
263
- # # => Cache-Control: max-age=600, public, must-revalidate
275
+ # expires_in 10.minutes, public: true
276
+ # # => Cache-Control: max-age=600, public
264
277
  #
265
- # expires_in 1.hour, stale_while_revalidate: 60.seconds
266
- # # => Cache-Control: max-age=3600, private, stale-while-revalidate=60
278
+ # expires_in 10.minutes, public: true, must_revalidate: true
279
+ # # => Cache-Control: max-age=600, public, must-revalidate
267
280
  #
268
- # expires_in 1.hour, stale_if_error: 5.minutes
269
- # # => Cache-Control: max-age=3600, private, stale-if-error=300
281
+ # expires_in 1.hour, stale_while_revalidate: 60.seconds
282
+ # # => Cache-Control: max-age=3600, private, stale-while-revalidate=60
270
283
  #
271
- # expires_in 1.hour, public: true, "s-maxage": 3.hours, "no-transform": true
272
- # # => Cache-Control: max-age=3600, public, s-maxage=10800, no-transform=true
284
+ # expires_in 1.hour, stale_if_error: 5.minutes
285
+ # # => Cache-Control: max-age=3600, private, stale-if-error=300
286
+ #
287
+ # expires_in 1.hour, public: true, "s-maxage": 3.hours, "no-transform": true
288
+ # # => Cache-Control: max-age=3600, public, s-maxage=10800, no-transform=true
273
289
  #
274
290
  def expires_in(seconds, options = {})
275
291
  response.cache_control.delete(:no_store)
@@ -279,6 +295,7 @@ module ActionController
279
295
  must_revalidate: options.delete(:must_revalidate),
280
296
  stale_while_revalidate: options.delete(:stale_while_revalidate),
281
297
  stale_if_error: options.delete(:stale_if_error),
298
+ immutable: options.delete(:immutable),
282
299
  )
283
300
  options.delete(:private)
284
301
 
@@ -286,8 +303,8 @@ module ActionController
286
303
  response.date = Time.now unless response.date?
287
304
  end
288
305
 
289
- # Sets an HTTP 1.1 +Cache-Control+ header of <tt>no-cache</tt>. This means the
290
- # resource will be marked as stale, so clients must always revalidate.
306
+ # Sets an HTTP 1.1 `Cache-Control` header of `no-cache`. This means the resource
307
+ # will be marked as stale, so clients must always revalidate.
291
308
  # Intermediate/browser caches may still store the asset.
292
309
  def expires_now
293
310
  response.cache_control.replace(no_cache: true)
@@ -295,26 +312,51 @@ module ActionController
295
312
 
296
313
  # Cache or yield the block. The cache is supposed to never expire.
297
314
  #
298
- # You can use this method when you have an HTTP response that never changes,
299
- # and the browser and proxies should cache it indefinitely.
315
+ # You can use this method when you have an HTTP response that never changes, and
316
+ # the browser and proxies should cache it indefinitely.
300
317
  #
301
- # * +public+: By default, HTTP responses are private, cached only on the
302
- # user's web browser. To allow proxies to cache the response, set +true+ to
303
- # indicate that they can serve the cached response to all users.
318
+ # * `public`: By default, HTTP responses are private, cached only on the
319
+ # user's web browser. To allow proxies to cache the response, set `true` to
320
+ # indicate that they can serve the cached response to all users.
304
321
  def http_cache_forever(public: false)
305
- expires_in 100.years, public: public
322
+ expires_in 100.years, public: public, immutable: true
306
323
 
307
324
  yield if stale?(etag: request.fullpath,
308
325
  last_modified: Time.new(2011, 1, 1).utc,
309
326
  public: public)
310
327
  end
311
328
 
312
- # Sets an HTTP 1.1 +Cache-Control+ header of <tt>no-store</tt>. This means the
313
- # resource may not be stored in any cache.
329
+ # Sets an HTTP 1.1 `Cache-Control` header of `no-store`. This means the resource
330
+ # may not be stored in any cache.
314
331
  def no_store
315
332
  response.cache_control.replace(no_store: true)
316
333
  end
317
334
 
335
+ # Adds the `must-understand` directive to the `Cache-Control` header, which indicates
336
+ # that a cache MUST understand the semantics of the response status code that has been
337
+ # received, or discard the response.
338
+ #
339
+ # This is particularly useful when returning responses with new or uncommon
340
+ # status codes that might not be properly interpreted by older caches.
341
+ #
342
+ # #### Example
343
+ #
344
+ # def show
345
+ # @article = Article.find(params[:id])
346
+ #
347
+ # if @article.early_access?
348
+ # must_understand
349
+ # render status: 203 # Non-Authoritative Information
350
+ # else
351
+ # fresh_when @article
352
+ # end
353
+ # end
354
+ #
355
+ def must_understand
356
+ response.cache_control[:must_understand] = true
357
+ response.cache_control[:no_store] = true
358
+ end
359
+
318
360
  private
319
361
  def combine_etags(validator, options)
320
362
  [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact