actionpack 4.2.8 → 5.2.4.2

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 (166) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +285 -444
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller.rb +12 -5
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +45 -49
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  10. data/lib/abstract_controller/callbacks.rb +47 -31
  11. data/lib/abstract_controller/collector.rb +8 -11
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +25 -25
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  16. data/lib/abstract_controller/rendering.rb +42 -41
  17. data/lib/abstract_controller/translation.rb +10 -7
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +29 -21
  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 +27 -19
  23. data/lib/action_controller/caching.rb +14 -57
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +10 -15
  26. data/lib/action_controller/metal.rb +98 -83
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +118 -44
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +3 -3
  31. data/lib/action_controller/metal/data_streaming.rb +27 -46
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  34. data/lib/action_controller/metal/exceptions.rb +8 -14
  35. data/lib/action_controller/metal/flash.rb +4 -3
  36. data/lib/action_controller/metal/force_ssl.rb +23 -21
  37. data/lib/action_controller/metal/head.rb +21 -19
  38. data/lib/action_controller/metal/helpers.rb +24 -14
  39. data/lib/action_controller/metal/http_authentication.rb +64 -57
  40. data/lib/action_controller/metal/implicit_render.rb +62 -8
  41. data/lib/action_controller/metal/instrumentation.rb +19 -21
  42. data/lib/action_controller/metal/live.rb +90 -106
  43. data/lib/action_controller/metal/mime_responds.rb +33 -46
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  46. data/lib/action_controller/metal/redirecting.rb +49 -28
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +72 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +203 -92
  50. data/lib/action_controller/metal/rescue.rb +9 -16
  51. data/lib/action_controller/metal/streaming.rb +12 -10
  52. data/lib/action_controller/metal/strong_parameters.rb +582 -165
  53. data/lib/action_controller/metal/testing.rb +2 -17
  54. data/lib/action_controller/metal/url_for.rb +19 -10
  55. data/lib/action_controller/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +2 -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 +280 -411
  60. data/lib/action_dispatch.rb +27 -19
  61. data/lib/action_dispatch/http/cache.rb +93 -47
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  64. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  65. data/lib/action_dispatch/http/headers.rb +55 -22
  66. data/lib/action_dispatch/http/mime_negotiation.rb +60 -41
  67. data/lib/action_dispatch/http/mime_type.rb +134 -121
  68. data/lib/action_dispatch/http/mime_types.rb +20 -6
  69. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  70. data/lib/action_dispatch/http/parameters.rb +98 -39
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +200 -118
  73. data/lib/action_dispatch/http/response.rb +225 -110
  74. data/lib/action_dispatch/http/upload.rb +12 -6
  75. data/lib/action_dispatch/http/url.rb +110 -28
  76. data/lib/action_dispatch/journey.rb +7 -5
  77. data/lib/action_dispatch/journey/formatter.rb +55 -32
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  81. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  82. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  85. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  86. data/lib/action_dispatch/journey/parser.rb +23 -22
  87. data/lib/action_dispatch/journey/parser.y +3 -2
  88. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  89. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  90. data/lib/action_dispatch/journey/route.rb +106 -28
  91. data/lib/action_dispatch/journey/router.rb +35 -23
  92. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  93. data/lib/action_dispatch/journey/routes.rb +18 -16
  94. data/lib/action_dispatch/journey/scanner.rb +18 -15
  95. data/lib/action_dispatch/journey/visitors.rb +99 -52
  96. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  97. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  101. data/lib/action_dispatch/middleware/executor.rb +21 -0
  102. data/lib/action_dispatch/middleware/flash.rb +78 -54
  103. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  104. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  105. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  106. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  107. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  108. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  109. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  110. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  111. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  112. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  113. data/lib/action_dispatch/middleware/stack.rb +31 -44
  114. data/lib/action_dispatch/middleware/static.rb +57 -50
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  116. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  124. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  125. data/lib/action_dispatch/railtie.rb +19 -11
  126. data/lib/action_dispatch/request/session.rb +106 -59
  127. data/lib/action_dispatch/request/utils.rb +67 -24
  128. data/lib/action_dispatch/routing.rb +17 -18
  129. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  130. data/lib/action_dispatch/routing/inspector.rb +58 -67
  131. data/lib/action_dispatch/routing/mapper.rb +734 -447
  132. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  133. data/lib/action_dispatch/routing/redirection.rb +36 -26
  134. data/lib/action_dispatch/routing/route_set.rb +321 -291
  135. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  136. data/lib/action_dispatch/routing/url_for.rb +65 -25
  137. data/lib/action_dispatch/system_test_case.rb +147 -0
  138. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  139. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  140. data/lib/action_dispatch/system_testing/server.rb +31 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  143. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  144. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  145. data/lib/action_dispatch/testing/assertions.rb +6 -4
  146. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  147. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  148. data/lib/action_dispatch/testing/integration.rb +347 -209
  149. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  150. data/lib/action_dispatch/testing/test_process.rb +28 -22
  151. data/lib/action_dispatch/testing/test_request.rb +27 -34
  152. data/lib/action_dispatch/testing/test_response.rb +35 -7
  153. data/lib/action_pack.rb +4 -2
  154. data/lib/action_pack/gem_version.rb +5 -3
  155. data/lib/action_pack/version.rb +3 -1
  156. metadata +56 -39
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ # Override the default form builder for all views rendered by this
5
+ # controller and any of its descendants. Accepts a subclass of
6
+ # +ActionView::Helpers::FormBuilder+.
7
+ #
8
+ # For example, given a form builder:
9
+ #
10
+ # class AdminFormBuilder < ActionView::Helpers::FormBuilder
11
+ # def special_field(name)
12
+ # end
13
+ # end
14
+ #
15
+ # The controller specifies a form builder as its default:
16
+ #
17
+ # class AdminAreaController < ApplicationController
18
+ # default_form_builder AdminFormBuilder
19
+ # end
20
+ #
21
+ # Then in the view any form using +form_for+ will be an instance of the
22
+ # specified form builder:
23
+ #
24
+ # <%= form_for(@instance) do |builder| %>
25
+ # <%= builder.special_field(:name) %>
26
+ # <% end %>
27
+ module FormBuilder
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ class_attribute :_default_form_builder, instance_accessor: false
32
+ end
33
+
34
+ module ClassMethods
35
+ # Set the form builder to be used as the default for all forms
36
+ # in the views rendered by this controller and its subclasses.
37
+ #
38
+ # ==== Parameters
39
+ # * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+
40
+ def default_form_builder(builder)
41
+ self._default_form_builder = builder
42
+ end
43
+ end
44
+
45
+ # Default form builder for the controller
46
+ def default_form_builder
47
+ self.class._default_form_builder
48
+ end
49
+ end
50
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController
2
4
  class LogSubscriber < ActiveSupport::LogSubscriber
3
5
  INTERNAL_PARAMS = %w(controller action format _method only_path)
@@ -24,8 +26,10 @@ module ActionController
24
26
  exception_class_name = payload[:exception].first
25
27
  status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
26
28
  end
27
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
28
- message << " (#{additions.join(" | ")})" unless additions.blank?
29
+ message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms".dup
30
+ message << " (#{additions.join(" | ".freeze)})" unless additions.empty?
31
+ message << "\n\n" if defined?(Rails.env) && Rails.env.development?
32
+
29
33
  message
30
34
  end
31
35
  end
@@ -49,16 +53,7 @@ module ActionController
49
53
  def unpermitted_parameters(event)
50
54
  debug do
51
55
  unpermitted_keys = event.payload[:keys]
52
- "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
53
- end
54
- end
55
-
56
- def deep_munge(event)
57
- debug do
58
- "Value for params[:#{event.payload[:keys].join('][:')}] was set "\
59
- "to nil, because it was one of [], [null] or [null, null, ...]. "\
60
- "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\
61
- "for more information."\
56
+ "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}"
62
57
  end
63
58
  end
64
59
 
@@ -66,10 +61,10 @@ module ActionController
66
61
  expire_fragment expire_page write_page).each do |method|
67
62
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
68
63
  def #{method}(event)
69
- return unless logger.info?
70
- key_or_path = event.payload[:key] || event.payload[:path]
64
+ return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
65
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
71
66
  human_name = #{method.to_s.humanize.inspect}
72
- info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)")
67
+ info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
73
68
  end
74
69
  METHOD
75
70
  end
@@ -1,5 +1,9 @@
1
- require 'active_support/core_ext/array/extract_options'
2
- require 'action_dispatch/middleware/stack'
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"
3
7
 
4
8
  module ActionController
5
9
  # Extend ActionDispatch middleware stack to make it aware of options
@@ -11,32 +15,50 @@ module ActionController
11
15
  #
12
16
  class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
13
17
  class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
14
- def initialize(klass, *args, &block)
15
- options = args.extract_options!
16
- @only = Array(options.delete(:only)).map(&:to_s)
17
- @except = Array(options.delete(:except)).map(&:to_s)
18
- args << options unless options.empty?
19
- super
18
+ def initialize(klass, args, actions, strategy, block)
19
+ @actions = actions
20
+ @strategy = strategy
21
+ super(klass, args, block)
20
22
  end
21
23
 
22
24
  def valid?(action)
23
- if @only.present?
24
- @only.include?(action)
25
- elsif @except.present?
26
- !@except.include?(action)
27
- else
28
- true
29
- end
25
+ @strategy.call @actions, action
30
26
  end
31
27
  end
32
28
 
33
- def build(action, app = Proc.new)
29
+ def build(action, app = nil, &block)
34
30
  action = action.to_s
35
31
 
36
- middlewares.reverse.inject(app) do |a, middleware|
32
+ middlewares.reverse.inject(app || block) do |a, middleware|
37
33
  middleware.valid?(action) ? middleware.build(a) : a
38
34
  end
39
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
40
62
  end
41
63
 
42
64
  # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
@@ -98,12 +120,6 @@ module ActionController
98
120
  class Metal < AbstractController::Base
99
121
  abstract!
100
122
 
101
- attr_internal_writer :env
102
-
103
- def env
104
- @_env ||= {}
105
- end
106
-
107
123
  # Returns the last part of the controller's name, underscored, without the ending
108
124
  # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
109
125
  # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
@@ -111,26 +127,30 @@ module ActionController
111
127
  # ==== Returns
112
128
  # * <tt>string</tt>
113
129
  def self.controller_name
114
- @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore
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
115
137
  end
116
138
 
117
- # Delegates to the class' <tt>controller_name</tt>
139
+ def self.binary_params_for?(action) # :nodoc:
140
+ false
141
+ end
142
+
143
+ # Delegates to the class' <tt>controller_name</tt>.
118
144
  def controller_name
119
145
  self.class.controller_name
120
146
  end
121
147
 
122
- # The details below can be overridden to support a specific
123
- # Request and Response object. The default ActionController::Base
124
- # implementation includes RackDelegation, which makes a request
125
- # and response object available. You might wish to control the
126
- # environment and response manually for performance reasons.
127
-
128
- attr_internal :headers, :response, :request
129
- delegate :session, :to => "@_request"
148
+ attr_internal :response, :request
149
+ delegate :session, to: "@_request"
150
+ delegate :headers, :status=, :location=, :content_type=,
151
+ :status, :location, :content_type, to: "@_response"
130
152
 
131
153
  def initialize
132
- @_headers = {"Content-Type" => "text/html"}
133
- @_status = 200
134
154
  @_request = nil
135
155
  @_response = nil
136
156
  @_routes = nil
@@ -145,64 +165,52 @@ module ActionController
145
165
  @_params = val
146
166
  end
147
167
 
148
- # Basic implementations for content_type=, location=, and headers are
149
- # provided to reduce the dependency on the RackDelegation module
150
- # in Renderer and Redirector.
151
-
152
- def content_type=(type)
153
- headers["Content-Type"] = type.to_s
154
- end
155
-
156
- def content_type
157
- headers["Content-Type"]
158
- end
159
-
160
- def location
161
- headers["Location"]
162
- end
163
-
164
- def location=(url)
165
- headers["Location"] = url
166
- end
168
+ alias :response_code :status # :nodoc:
167
169
 
168
- # Basic url_for that can be overridden for more robust functionality
170
+ # Basic url_for that can be overridden for more robust functionality.
169
171
  def url_for(string)
170
172
  string
171
173
  end
172
174
 
173
- def status
174
- @_status
175
- end
176
- alias :response_code :status # :nodoc:
177
-
178
- def status=(status)
179
- @_status = Rack::Utils.status_code(status)
180
- end
181
-
182
175
  def response_body=(body)
183
176
  body = [body] unless body.nil? || body.respond_to?(:each)
177
+ response.reset_body!
178
+ return unless body
179
+ response.body = body
184
180
  super
185
181
  end
186
182
 
187
183
  # Tests if render or redirect has already happened.
188
184
  def performed?
189
- response_body || (response && response.committed?)
185
+ response_body || response.committed?
190
186
  end
191
187
 
192
- def dispatch(name, request) #:nodoc:
193
- @_request = request
194
- @_env = request.env
195
- @_env['action_controller.instance'] = self
188
+ def dispatch(name, request, response) #:nodoc:
189
+ set_request!(request)
190
+ set_response!(response)
196
191
  process(name)
192
+ request.commit_flash
197
193
  to_a
198
194
  end
199
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
+
200
205
  def to_a #:nodoc:
201
- response ? response.to_a : [status, headers, response_body]
206
+ response.to_a
202
207
  end
203
208
 
204
- class_attribute :middleware_stack
205
- self.middleware_stack = ActionController::MiddlewareStack.new
209
+ def reset_session
210
+ @_request.reset_session
211
+ end
212
+
213
+ class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
206
214
 
207
215
  def self.inherited(base) # :nodoc:
208
216
  base.middleware_stack = middleware_stack.dup
@@ -220,21 +228,28 @@ module ActionController
220
228
  middleware_stack
221
229
  end
222
230
 
223
- # Makes the controller a Rack endpoint that runs the action in the given
224
- # +env+'s +action_dispatch.request.path_parameters+ key.
225
- def self.call(env)
226
- req = ActionDispatch::Request.new env
227
- action(req.path_parameters[:action]).call(env)
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
228
244
  end
229
245
 
230
- # Returns a Rack endpoint for the given action name.
231
- def self.action(name, klass = ActionDispatch::Request)
246
+ # Direct dispatch to the controller. Instantiates the controller, then
247
+ # executes the action named +name+.
248
+ def self.dispatch(name, req, res)
232
249
  if middleware_stack.any?
233
- middleware_stack.build(name) do |env|
234
- new.dispatch(name, klass.new(env))
235
- end
250
+ middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
236
251
  else
237
- lambda { |env| new.dispatch(name, klass.new(env)) }
252
+ new.dispatch(name, req, res)
238
253
  end
239
254
  end
240
255
  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
@@ -1,21 +1,21 @@
1
- require 'active_support/core_ext/hash/keys'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
2
4
 
3
5
  module ActionController
4
6
  module ConditionalGet
5
7
  extend ActiveSupport::Concern
6
8
 
7
- include RackDelegation
8
9
  include Head
9
10
 
10
11
  included do
11
- class_attribute :etaggers
12
- self.etaggers = []
12
+ class_attribute :etaggers, default: []
13
13
  end
14
14
 
15
15
  module ClassMethods
16
16
  # Allows you to consider additional controller-wide information when generating an ETag.
17
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 authorized displaying
18
+ # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
19
19
  # of cached pages.
20
20
  #
21
21
  # class InvoicesController < ApplicationController
@@ -37,10 +37,25 @@ module ActionController
37
37
  #
38
38
  # === Parameters:
39
39
  #
40
- # * <tt>:etag</tt>.
41
- # * <tt>:last_modified</tt>.
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.
42
57
  # * <tt>:public</tt> By default the Cache-Control header is private, set this to
43
- # +true+ if you want your application to be cachable by other devices (proxy caches).
58
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
44
59
  # * <tt>:template</tt> By default, the template digest for the current
45
60
  # controller/action is included in ETags. If the action renders a
46
61
  # different template, you can include its digest instead. If the action
@@ -51,21 +66,31 @@ module ActionController
51
66
  #
52
67
  # def show
53
68
  # @article = Article.find(params[:id])
54
- # fresh_when(etag: @article, last_modified: @article.created_at, public: true)
69
+ # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
55
70
  # end
56
71
  #
57
72
  # This will render the show template if the request isn't sending a matching ETag or
58
73
  # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
59
74
  #
60
- # You can also just pass a record where +last_modified+ will be set by calling
61
- # +updated_at+ and the +etag+ by passing the object itself.
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.
62
77
  #
63
78
  # def show
64
79
  # @article = Article.find(params[:id])
65
80
  # fresh_when(@article)
66
81
  # end
67
82
  #
68
- # When passing a record, you can still set whether the public header:
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:
69
94
  #
70
95
  # def show
71
96
  # @article = Article.find(params[:id])
@@ -77,18 +102,20 @@ module ActionController
77
102
  #
78
103
  # before_action { fresh_when @article, template: 'widgets/show' }
79
104
  #
80
- def fresh_when(record_or_options, additional_options = {})
81
- if record_or_options.is_a? Hash
82
- options = record_or_options
83
- options.assert_valid_keys(:etag, :last_modified, :public, :template)
84
- else
85
- record = record_or_options
86
- options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options)
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
87
115
  end
88
116
 
89
- response.etag = combine_etags(options) if options[:etag] || options[:template]
90
- response.last_modified = options[:last_modified] if options[:last_modified]
91
- response.cache_control[:public] = true if options[:public]
117
+ response.last_modified = last_modified if last_modified
118
+ response.cache_control[:public] = true if public
92
119
 
93
120
  head :not_modified if request.fresh?(response)
94
121
  end
@@ -100,10 +127,25 @@ module ActionController
100
127
  #
101
128
  # === Parameters:
102
129
  #
103
- # * <tt>:etag</tt>.
104
- # * <tt>:last_modified</tt>.
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.
105
147
  # * <tt>:public</tt> By default the Cache-Control header is private, set this to
106
- # +true+ if you want your application to be cachable by other devices (proxy caches).
148
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
107
149
  # * <tt>:template</tt> By default, the template digest for the current
108
150
  # controller/action is included in ETags. If the action renders a
109
151
  # different template, you can include its digest instead. If the action
@@ -115,7 +157,7 @@ module ActionController
115
157
  # def show
116
158
  # @article = Article.find(params[:id])
117
159
  #
118
- # if stale?(etag: @article, last_modified: @article.created_at)
160
+ # if stale?(etag: @article, last_modified: @article.updated_at)
119
161
  # @statistics = @article.really_expensive_call
120
162
  # respond_to do |format|
121
163
  # # all the supported formats
@@ -123,8 +165,8 @@ module ActionController
123
165
  # end
124
166
  # end
125
167
  #
126
- # You can also just pass a record where +last_modified+ will be set by calling
127
- # +updated_at+ and the +etag+ by passing the object itself.
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.
128
170
  #
129
171
  # def show
130
172
  # @article = Article.find(params[:id])
@@ -137,7 +179,23 @@ module ActionController
137
179
  # end
138
180
  # end
139
181
  #
140
- # When passing a record, you can still set whether the public header:
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:
141
199
  #
142
200
  # def show
143
201
  # @article = Article.find(params[:id])
@@ -157,12 +215,12 @@ module ActionController
157
215
  # super if stale? @article, template: 'widgets/show'
158
216
  # end
159
217
  #
160
- def stale?(record_or_options, additional_options = {})
161
- fresh_when(record_or_options, additional_options)
218
+ def stale?(object = nil, **freshness_kwargs)
219
+ fresh_when(object, **freshness_kwargs)
162
220
  !request.fresh?(response)
163
221
  end
164
222
 
165
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
223
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
166
224
  # instruction, so that intermediate caches must not cache the response.
167
225
  #
168
226
  # expires_in 20.minutes
@@ -170,31 +228,47 @@ module ActionController
170
228
  # expires_in 3.hours, public: true, must_revalidate: true
171
229
  #
172
230
  # This method will overwrite an existing Cache-Control header.
173
- # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
231
+ # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
174
232
  #
175
- # The method will also ensure a HTTP Date header for client compatibility.
233
+ # The method will also ensure an HTTP Date header for client compatibility.
176
234
  def expires_in(seconds, options = {})
177
235
  response.cache_control.merge!(
178
- :max_age => seconds,
179
- :public => options.delete(:public),
180
- :must_revalidate => options.delete(:must_revalidate)
236
+ max_age: seconds,
237
+ public: options.delete(:public),
238
+ must_revalidate: options.delete(:must_revalidate)
181
239
  )
182
240
  options.delete(:private)
183
241
 
184
- response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"}
242
+ response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
185
243
  response.date = Time.now unless response.date?
186
244
  end
187
245
 
188
- # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
189
- # occur by the browser or intermediate caches (like caching proxy servers).
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.
190
249
  def expires_now
191
- response.cache_control.replace(:no_cache => true)
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)
192
267
  end
193
268
 
194
269
  private
195
- def combine_etags(options)
196
- etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
197
- etags.unshift options[:etag]
270
+ def combine_etags(validator, options)
271
+ [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
198
272
  end
199
273
  end
200
274
  end