actionpack 5.2.3

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 (170) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +429 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +265 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +166 -0
  10. data/lib/abstract_controller/callbacks.rb +212 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +31 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +66 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +276 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +78 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +274 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +152 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  34. data/lib/action_controller/metal/exceptions.rb +53 -0
  35. data/lib/action_controller/metal/flash.rb +61 -0
  36. data/lib/action_controller/metal/force_ssl.rb +99 -0
  37. data/lib/action_controller/metal/head.rb +60 -0
  38. data/lib/action_controller/metal/helpers.rb +123 -0
  39. data/lib/action_controller/metal/http_authentication.rb +519 -0
  40. data/lib/action_controller/metal/implicit_render.rb +73 -0
  41. data/lib/action_controller/metal/instrumentation.rb +107 -0
  42. data/lib/action_controller/metal/live.rb +312 -0
  43. data/lib/action_controller/metal/mime_responds.rb +313 -0
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +293 -0
  46. data/lib/action_controller/metal/redirecting.rb +133 -0
  47. data/lib/action_controller/metal/renderers.rb +181 -0
  48. data/lib/action_controller/metal/rendering.rb +122 -0
  49. data/lib/action_controller/metal/request_forgery_protection.rb +445 -0
  50. data/lib/action_controller/metal/rescue.rb +28 -0
  51. data/lib/action_controller/metal/streaming.rb +223 -0
  52. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  53. data/lib/action_controller/metal/testing.rb +16 -0
  54. data/lib/action_controller/metal/url_for.rb +58 -0
  55. data/lib/action_controller/railtie.rb +89 -0
  56. data/lib/action_controller/railties/helpers.rb +24 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +629 -0
  60. data/lib/action_dispatch.rb +112 -0
  61. data/lib/action_dispatch/http/cache.rb +222 -0
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +84 -0
  64. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  65. data/lib/action_dispatch/http/headers.rb +132 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +175 -0
  67. data/lib/action_dispatch/http/mime_type.rb +342 -0
  68. data/lib/action_dispatch/http/mime_types.rb +50 -0
  69. data/lib/action_dispatch/http/parameter_filter.rb +86 -0
  70. data/lib/action_dispatch/http/parameters.rb +126 -0
  71. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  72. data/lib/action_dispatch/http/request.rb +430 -0
  73. data/lib/action_dispatch/http/response.rb +519 -0
  74. data/lib/action_dispatch/http/upload.rb +84 -0
  75. data/lib/action_dispatch/http/url.rb +350 -0
  76. data/lib/action_dispatch/journey.rb +7 -0
  77. data/lib/action_dispatch/journey/formatter.rb +189 -0
  78. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  81. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  86. data/lib/action_dispatch/journey/parser.rb +199 -0
  87. data/lib/action_dispatch/journey/parser.y +50 -0
  88. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  89. data/lib/action_dispatch/journey/path/pattern.rb +198 -0
  90. data/lib/action_dispatch/journey/route.rb +203 -0
  91. data/lib/action_dispatch/journey/router.rb +156 -0
  92. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  93. data/lib/action_dispatch/journey/routes.rb +82 -0
  94. data/lib/action_dispatch/journey/scanner.rb +64 -0
  95. data/lib/action_dispatch/journey/visitors.rb +268 -0
  96. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  97. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  98. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  99. data/lib/action_dispatch/middleware/callbacks.rb +36 -0
  100. data/lib/action_dispatch/middleware/cookies.rb +685 -0
  101. data/lib/action_dispatch/middleware/debug_exceptions.rb +205 -0
  102. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +147 -0
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +300 -0
  106. data/lib/action_dispatch/middleware/public_exceptions.rb +57 -0
  107. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  108. data/lib/action_dispatch/middleware/remote_ip.rb +183 -0
  109. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  110. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  111. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  112. data/lib/action_dispatch/middleware/session/cookie_store.rb +118 -0
  113. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  114. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  115. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  116. data/lib/action_dispatch/middleware/stack.rb +116 -0
  117. data/lib/action_dispatch/middleware/static.rb +130 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +161 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  139. data/lib/action_dispatch/railtie.rb +55 -0
  140. data/lib/action_dispatch/request/session.rb +234 -0
  141. data/lib/action_dispatch/request/utils.rb +78 -0
  142. data/lib/action_dispatch/routing.rb +260 -0
  143. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  144. data/lib/action_dispatch/routing/inspector.rb +225 -0
  145. data/lib/action_dispatch/routing/mapper.rb +2267 -0
  146. data/lib/action_dispatch/routing/polymorphic_routes.rb +352 -0
  147. data/lib/action_dispatch/routing/redirection.rb +201 -0
  148. data/lib/action_dispatch/routing/route_set.rb +890 -0
  149. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  150. data/lib/action_dispatch/routing/url_for.rb +236 -0
  151. data/lib/action_dispatch/system_test_case.rb +147 -0
  152. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  153. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  154. data/lib/action_dispatch/system_testing/server.rb +31 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  158. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  159. data/lib/action_dispatch/testing/assertions.rb +24 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +107 -0
  161. data/lib/action_dispatch/testing/assertions/routing.rb +222 -0
  162. data/lib/action_dispatch/testing/integration.rb +652 -0
  163. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  164. data/lib/action_dispatch/testing/test_process.rb +50 -0
  165. data/lib/action_dispatch/testing/test_request.rb +71 -0
  166. data/lib/action_dispatch/testing/test_response.rb +53 -0
  167. data/lib/action_pack.rb +26 -0
  168. data/lib/action_pack/gem_version.rb +17 -0
  169. data/lib/action_pack/version.rb +10 -0
  170. metadata +318 -0
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract_options"
4
+ require "action_dispatch/middleware/stack"
5
+ require "action_dispatch/http/request"
6
+ require "action_dispatch/http/response"
7
+
8
+ module ActionController
9
+ # Extend ActionDispatch middleware stack to make it aware of options
10
+ # allowing the following syntax in controllers:
11
+ #
12
+ # class PostsController < ApplicationController
13
+ # use AuthenticationMiddleware, except: [:index, :show]
14
+ # end
15
+ #
16
+ class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
17
+ class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
18
+ def initialize(klass, args, actions, strategy, block)
19
+ @actions = actions
20
+ @strategy = strategy
21
+ super(klass, args, block)
22
+ end
23
+
24
+ def valid?(action)
25
+ @strategy.call @actions, action
26
+ end
27
+ end
28
+
29
+ def build(action, app = Proc.new)
30
+ action = action.to_s
31
+
32
+ middlewares.reverse.inject(app) do |a, middleware|
33
+ middleware.valid?(action) ? middleware.build(a) : a
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ INCLUDE = ->(list, action) { list.include? action }
40
+ EXCLUDE = ->(list, action) { !list.include? action }
41
+ NULL = ->(list, action) { true }
42
+
43
+ def build_middleware(klass, args, block)
44
+ options = args.extract_options!
45
+ only = Array(options.delete(:only)).map(&:to_s)
46
+ except = Array(options.delete(:except)).map(&:to_s)
47
+ args << options unless options.empty?
48
+
49
+ strategy = NULL
50
+ list = nil
51
+
52
+ if only.any?
53
+ strategy = INCLUDE
54
+ list = only
55
+ elsif except.any?
56
+ strategy = EXCLUDE
57
+ list = except
58
+ end
59
+
60
+ Middleware.new(klass, args, list, strategy, block)
61
+ end
62
+ end
63
+
64
+ # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
65
+ # valid Rack interface without the additional niceties provided by
66
+ # <tt>ActionController::Base</tt>.
67
+ #
68
+ # A sample metal controller might look like this:
69
+ #
70
+ # class HelloController < ActionController::Metal
71
+ # def index
72
+ # self.response_body = "Hello World!"
73
+ # end
74
+ # end
75
+ #
76
+ # And then to route requests to your metal controller, you would add
77
+ # something like this to <tt>config/routes.rb</tt>:
78
+ #
79
+ # get 'hello', to: HelloController.action(:index)
80
+ #
81
+ # The +action+ method returns a valid Rack application for the \Rails
82
+ # router to dispatch to.
83
+ #
84
+ # == Rendering Helpers
85
+ #
86
+ # <tt>ActionController::Metal</tt> by default provides no utilities for rendering
87
+ # views, partials, or other responses aside from explicitly calling of
88
+ # <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To
89
+ # add the render helpers you're used to having in a normal controller, you
90
+ # can do the following:
91
+ #
92
+ # class HelloController < ActionController::Metal
93
+ # include AbstractController::Rendering
94
+ # include ActionView::Layouts
95
+ # append_view_path "#{Rails.root}/app/views"
96
+ #
97
+ # def index
98
+ # render "hello/index"
99
+ # end
100
+ # end
101
+ #
102
+ # == Redirection Helpers
103
+ #
104
+ # To add redirection helpers to your metal controller, do the following:
105
+ #
106
+ # class HelloController < ActionController::Metal
107
+ # include ActionController::Redirecting
108
+ # include Rails.application.routes.url_helpers
109
+ #
110
+ # def index
111
+ # redirect_to root_url
112
+ # end
113
+ # end
114
+ #
115
+ # == Other Helpers
116
+ #
117
+ # You can refer to the modules included in <tt>ActionController::Base</tt> to see
118
+ # other features you can bring into your metal controller.
119
+ #
120
+ class Metal < AbstractController::Base
121
+ abstract!
122
+
123
+ # Returns the last part of the controller's name, underscored, without the ending
124
+ # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
125
+ # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
126
+ #
127
+ # ==== Returns
128
+ # * <tt>string</tt>
129
+ def self.controller_name
130
+ @controller_name ||= name.demodulize.sub(/Controller$/, "").underscore
131
+ end
132
+
133
+ def self.make_response!(request)
134
+ ActionDispatch::Response.new.tap do |res|
135
+ res.request = request
136
+ end
137
+ end
138
+
139
+ def self.binary_params_for?(action) # :nodoc:
140
+ false
141
+ end
142
+
143
+ # Delegates to the class' <tt>controller_name</tt>.
144
+ def controller_name
145
+ self.class.controller_name
146
+ end
147
+
148
+ attr_internal :response, :request
149
+ delegate :session, to: "@_request"
150
+ delegate :headers, :status=, :location=, :content_type=,
151
+ :status, :location, :content_type, to: "@_response"
152
+
153
+ def initialize
154
+ @_request = nil
155
+ @_response = nil
156
+ @_routes = nil
157
+ super
158
+ end
159
+
160
+ def params
161
+ @_params ||= request.parameters
162
+ end
163
+
164
+ def params=(val)
165
+ @_params = val
166
+ end
167
+
168
+ alias :response_code :status # :nodoc:
169
+
170
+ # Basic url_for that can be overridden for more robust functionality.
171
+ def url_for(string)
172
+ string
173
+ end
174
+
175
+ def response_body=(body)
176
+ body = [body] unless body.nil? || body.respond_to?(:each)
177
+ response.reset_body!
178
+ return unless body
179
+ response.body = body
180
+ super
181
+ end
182
+
183
+ # Tests if render or redirect has already happened.
184
+ def performed?
185
+ response_body || response.committed?
186
+ end
187
+
188
+ def dispatch(name, request, response) #:nodoc:
189
+ set_request!(request)
190
+ set_response!(response)
191
+ process(name)
192
+ request.commit_flash
193
+ to_a
194
+ end
195
+
196
+ def set_response!(response) # :nodoc:
197
+ @_response = response
198
+ end
199
+
200
+ def set_request!(request) #:nodoc:
201
+ @_request = request
202
+ @_request.controller_instance = self
203
+ end
204
+
205
+ def to_a #:nodoc:
206
+ response.to_a
207
+ end
208
+
209
+ def reset_session
210
+ @_request.reset_session
211
+ end
212
+
213
+ class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
214
+
215
+ def self.inherited(base) # :nodoc:
216
+ base.middleware_stack = middleware_stack.dup
217
+ super
218
+ end
219
+
220
+ # Pushes the given Rack middleware and its arguments to the bottom of the
221
+ # middleware stack.
222
+ def self.use(*args, &block)
223
+ middleware_stack.use(*args, &block)
224
+ end
225
+
226
+ # Alias for +middleware_stack+.
227
+ def self.middleware
228
+ middleware_stack
229
+ end
230
+
231
+ # Returns a Rack endpoint for the given action name.
232
+ def self.action(name)
233
+ app = lambda { |env|
234
+ req = ActionDispatch::Request.new(env)
235
+ res = make_response! req
236
+ new.dispatch(name, req, res)
237
+ }
238
+
239
+ if middleware_stack.any?
240
+ middleware_stack.build(name, app)
241
+ else
242
+ app
243
+ end
244
+ end
245
+
246
+ # Direct dispatch to the controller. Instantiates the controller, then
247
+ # executes the action named +name+.
248
+ def self.dispatch(name, req, res)
249
+ if middleware_stack.any?
250
+ middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
251
+ else
252
+ new.dispatch(name, req, res)
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ module BasicImplicitRender # :nodoc:
5
+ def send_action(method, *args)
6
+ super.tap { default_render unless performed? }
7
+ end
8
+
9
+ def default_render(*args)
10
+ head :no_content
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+
5
+ module ActionController
6
+ module ConditionalGet
7
+ extend ActiveSupport::Concern
8
+
9
+ include Head
10
+
11
+ included do
12
+ class_attribute :etaggers, default: []
13
+ end
14
+
15
+ module ClassMethods
16
+ # Allows you to consider additional controller-wide information when generating an ETag.
17
+ # For example, if you serve pages tailored depending on who's logged in at the moment, you
18
+ # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
19
+ # of cached pages.
20
+ #
21
+ # class InvoicesController < ApplicationController
22
+ # etag { current_user.try :id }
23
+ #
24
+ # def show
25
+ # # Etag will differ even for the same invoice when it's viewed by a different current_user
26
+ # @invoice = Invoice.find(params[:id])
27
+ # fresh_when(@invoice)
28
+ # end
29
+ # end
30
+ def etag(&etagger)
31
+ self.etaggers += [etagger]
32
+ end
33
+ end
34
+
35
+ # Sets the +etag+, +last_modified+, or both on the response and renders a
36
+ # <tt>304 Not Modified</tt> response if the request is already fresh.
37
+ #
38
+ # === Parameters:
39
+ #
40
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
41
+ # +:weak_etag+ option.
42
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
43
+ # Requests that set If-None-Match header may return a 304 Not Modified
44
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
45
+ # equivalence, not byte-for-byte equality, so they're good for caching
46
+ # HTML pages in browser caches. They can't be used for responses that
47
+ # must be byte-identical, like serving Range requests within a PDF file.
48
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
49
+ # Requests that set If-None-Match header may return a 304 Not Modified
50
+ # response if it matches the ETag exactly. A strong ETag implies exact
51
+ # equality: the response must match byte for byte. This is necessary for
52
+ # doing Range requests within a large video or PDF file, for example, or
53
+ # for compatibility with some CDNs that don't support weak ETags.
54
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
55
+ # response. Subsequent requests that set If-Modified-Since may return a
56
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
57
+ # * <tt>:public</tt> By default the Cache-Control header is private, set this to
58
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
59
+ # * <tt>:template</tt> By default, the template digest for the current
60
+ # controller/action is included in ETags. If the action renders a
61
+ # different template, you can include its digest instead. If the action
62
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
63
+ # to skip any attempt to check for a template digest.
64
+ #
65
+ # === Example:
66
+ #
67
+ # def show
68
+ # @article = Article.find(params[:id])
69
+ # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
70
+ # end
71
+ #
72
+ # This will render the show template if the request isn't sending a matching ETag or
73
+ # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
74
+ #
75
+ # You can also just pass a record. In this case +last_modified+ will be set
76
+ # by calling +updated_at+ and +etag+ by passing the object itself.
77
+ #
78
+ # def show
79
+ # @article = Article.find(params[:id])
80
+ # fresh_when(@article)
81
+ # end
82
+ #
83
+ # You can also pass an object that responds to +maximum+, such as a
84
+ # collection of active records. In this case +last_modified+ will be set by
85
+ # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
86
+ # most recently updated record) and the +etag+ by passing the object itself.
87
+ #
88
+ # def index
89
+ # @articles = Article.all
90
+ # fresh_when(@articles)
91
+ # end
92
+ #
93
+ # When passing a record or a collection, you can still set the public header:
94
+ #
95
+ # def show
96
+ # @article = Article.find(params[:id])
97
+ # fresh_when(@article, public: true)
98
+ # end
99
+ #
100
+ # When rendering a different template than the default controller/action
101
+ # style, you can indicate which digest to include in the ETag:
102
+ #
103
+ # before_action { fresh_when @article, template: 'widgets/show' }
104
+ #
105
+ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
106
+ weak_etag ||= etag || object unless strong_etag
107
+ last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
108
+
109
+ if strong_etag
110
+ response.strong_etag = combine_etags strong_etag,
111
+ last_modified: last_modified, public: public, template: template
112
+ elsif weak_etag || template
113
+ response.weak_etag = combine_etags weak_etag,
114
+ last_modified: last_modified, public: public, template: template
115
+ end
116
+
117
+ response.last_modified = last_modified if last_modified
118
+ response.cache_control[:public] = true if public
119
+
120
+ head :not_modified if request.fresh?(response)
121
+ end
122
+
123
+ # Sets the +etag+ and/or +last_modified+ on the response and checks it against
124
+ # the client request. If the request doesn't match the options provided, the
125
+ # request is considered stale and should be generated from scratch. Otherwise,
126
+ # it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
127
+ #
128
+ # === Parameters:
129
+ #
130
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
131
+ # +:weak_etag+ option.
132
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
133
+ # Requests that set If-None-Match header may return a 304 Not Modified
134
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
135
+ # equivalence, not byte-for-byte equality, so they're good for caching
136
+ # HTML pages in browser caches. They can't be used for responses that
137
+ # must be byte-identical, like serving Range requests within a PDF file.
138
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
139
+ # Requests that set If-None-Match header may return a 304 Not Modified
140
+ # response if it matches the ETag exactly. A strong ETag implies exact
141
+ # equality: the response must match byte for byte. This is necessary for
142
+ # doing Range requests within a large video or PDF file, for example, or
143
+ # for compatibility with some CDNs that don't support weak ETags.
144
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
145
+ # response. Subsequent requests that set If-Modified-Since may return a
146
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
147
+ # * <tt>:public</tt> By default the Cache-Control header is private, set this to
148
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
149
+ # * <tt>:template</tt> By default, the template digest for the current
150
+ # controller/action is included in ETags. If the action renders a
151
+ # different template, you can include its digest instead. If the action
152
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
153
+ # to skip any attempt to check for a template digest.
154
+ #
155
+ # === Example:
156
+ #
157
+ # def show
158
+ # @article = Article.find(params[:id])
159
+ #
160
+ # if stale?(etag: @article, last_modified: @article.updated_at)
161
+ # @statistics = @article.really_expensive_call
162
+ # respond_to do |format|
163
+ # # all the supported formats
164
+ # end
165
+ # end
166
+ # end
167
+ #
168
+ # You can also just pass a record. In this case +last_modified+ will be set
169
+ # by calling +updated_at+ and +etag+ by passing the object itself.
170
+ #
171
+ # def show
172
+ # @article = Article.find(params[:id])
173
+ #
174
+ # if stale?(@article)
175
+ # @statistics = @article.really_expensive_call
176
+ # respond_to do |format|
177
+ # # all the supported formats
178
+ # end
179
+ # end
180
+ # end
181
+ #
182
+ # You can also pass an object that responds to +maximum+, such as a
183
+ # collection of active records. In this case +last_modified+ will be set by
184
+ # calling +maximum(:updated_at)+ on the collection (the timestamp of the
185
+ # most recently updated record) and the +etag+ by passing the object itself.
186
+ #
187
+ # def index
188
+ # @articles = Article.all
189
+ #
190
+ # if stale?(@articles)
191
+ # @statistics = @articles.really_expensive_call
192
+ # respond_to do |format|
193
+ # # all the supported formats
194
+ # end
195
+ # end
196
+ # end
197
+ #
198
+ # When passing a record or a collection, you can still set the public header:
199
+ #
200
+ # def show
201
+ # @article = Article.find(params[:id])
202
+ #
203
+ # if stale?(@article, public: true)
204
+ # @statistics = @article.really_expensive_call
205
+ # respond_to do |format|
206
+ # # all the supported formats
207
+ # end
208
+ # end
209
+ # end
210
+ #
211
+ # When rendering a different template than the default controller/action
212
+ # style, you can indicate which digest to include in the ETag:
213
+ #
214
+ # def show
215
+ # super if stale? @article, template: 'widgets/show'
216
+ # end
217
+ #
218
+ def stale?(object = nil, **freshness_kwargs)
219
+ fresh_when(object, **freshness_kwargs)
220
+ !request.fresh?(response)
221
+ end
222
+
223
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
224
+ # instruction, so that intermediate caches must not cache the response.
225
+ #
226
+ # expires_in 20.minutes
227
+ # expires_in 3.hours, public: true
228
+ # expires_in 3.hours, public: true, must_revalidate: true
229
+ #
230
+ # This method will overwrite an existing Cache-Control header.
231
+ # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
232
+ #
233
+ # The method will also ensure an HTTP Date header for client compatibility.
234
+ def expires_in(seconds, options = {})
235
+ response.cache_control.merge!(
236
+ max_age: seconds,
237
+ public: options.delete(:public),
238
+ must_revalidate: options.delete(:must_revalidate)
239
+ )
240
+ options.delete(:private)
241
+
242
+ response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
243
+ response.date = Time.now unless response.date?
244
+ end
245
+
246
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
247
+ # resource will be marked as stale, so clients must always revalidate.
248
+ # Intermediate/browser caches may still store the asset.
249
+ def expires_now
250
+ response.cache_control.replace(no_cache: true)
251
+ end
252
+
253
+ # Cache or yield the block. The cache is supposed to never expire.
254
+ #
255
+ # You can use this method when you have an HTTP response that never changes,
256
+ # and the browser and proxies should cache it indefinitely.
257
+ #
258
+ # * +public+: By default, HTTP responses are private, cached only on the
259
+ # user's web browser. To allow proxies to cache the response, set +true+ to
260
+ # indicate that they can serve the cached response to all users.
261
+ def http_cache_forever(public: false)
262
+ expires_in 100.years, public: public
263
+
264
+ yield if stale?(etag: request.fullpath,
265
+ last_modified: Time.new(2011, 1, 1).utc,
266
+ public: public)
267
+ end
268
+
269
+ private
270
+ def combine_etags(validator, options)
271
+ [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
272
+ end
273
+ end
274
+ end