actionpack 6.0.0

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 (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +311 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +58 -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 +267 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +150 -0
  10. data/lib/abstract_controller/callbacks.rb +224 -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 +32 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +67 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +271 -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 +81 -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 +280 -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 +151 -0
  32. data/lib/action_controller/metal/default_headers.rb +17 -0
  33. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  35. data/lib/action_controller/metal/exceptions.rb +74 -0
  36. data/lib/action_controller/metal/flash.rb +61 -0
  37. data/lib/action_controller/metal/force_ssl.rb +58 -0
  38. data/lib/action_controller/metal/head.rb +60 -0
  39. data/lib/action_controller/metal/helpers.rb +122 -0
  40. data/lib/action_controller/metal/http_authentication.rb +518 -0
  41. data/lib/action_controller/metal/implicit_render.rb +63 -0
  42. data/lib/action_controller/metal/instrumentation.rb +105 -0
  43. data/lib/action_controller/metal/live.rb +314 -0
  44. data/lib/action_controller/metal/mime_responds.rb +324 -0
  45. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  46. data/lib/action_controller/metal/params_wrapper.rb +297 -0
  47. data/lib/action_controller/metal/redirecting.rb +133 -0
  48. data/lib/action_controller/metal/renderers.rb +181 -0
  49. data/lib/action_controller/metal/rendering.rb +122 -0
  50. data/lib/action_controller/metal/request_forgery_protection.rb +456 -0
  51. data/lib/action_controller/metal/rescue.rb +28 -0
  52. data/lib/action_controller/metal/streaming.rb +223 -0
  53. data/lib/action_controller/metal/strong_parameters.rb +1105 -0
  54. data/lib/action_controller/metal/testing.rb +16 -0
  55. data/lib/action_controller/metal/url_for.rb +58 -0
  56. data/lib/action_controller/railtie.rb +89 -0
  57. data/lib/action_controller/railties/helpers.rb +24 -0
  58. data/lib/action_controller/renderer.rb +130 -0
  59. data/lib/action_controller/template_assertions.rb +11 -0
  60. data/lib/action_controller/test_case.rb +626 -0
  61. data/lib/action_dispatch.rb +114 -0
  62. data/lib/action_dispatch/http/cache.rb +226 -0
  63. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  64. data/lib/action_dispatch/http/content_security_policy.rb +284 -0
  65. data/lib/action_dispatch/http/filter_parameters.rb +86 -0
  66. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  67. data/lib/action_dispatch/http/headers.rb +132 -0
  68. data/lib/action_dispatch/http/mime_negotiation.rb +177 -0
  69. data/lib/action_dispatch/http/mime_type.rb +350 -0
  70. data/lib/action_dispatch/http/mime_types.rb +50 -0
  71. data/lib/action_dispatch/http/parameter_filter.rb +12 -0
  72. data/lib/action_dispatch/http/parameters.rb +136 -0
  73. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  74. data/lib/action_dispatch/http/request.rb +427 -0
  75. data/lib/action_dispatch/http/response.rb +534 -0
  76. data/lib/action_dispatch/http/upload.rb +92 -0
  77. data/lib/action_dispatch/http/url.rb +350 -0
  78. data/lib/action_dispatch/journey.rb +7 -0
  79. data/lib/action_dispatch/journey/formatter.rb +189 -0
  80. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  81. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  82. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  83. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  84. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  85. data/lib/action_dispatch/journey/nfa/simulator.rb +47 -0
  86. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  87. data/lib/action_dispatch/journey/nodes/node.rb +141 -0
  88. data/lib/action_dispatch/journey/parser.rb +199 -0
  89. data/lib/action_dispatch/journey/parser.y +50 -0
  90. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  91. data/lib/action_dispatch/journey/path/pattern.rb +203 -0
  92. data/lib/action_dispatch/journey/route.rb +204 -0
  93. data/lib/action_dispatch/journey/router.rb +153 -0
  94. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  95. data/lib/action_dispatch/journey/routes.rb +81 -0
  96. data/lib/action_dispatch/journey/scanner.rb +71 -0
  97. data/lib/action_dispatch/journey/visitors.rb +268 -0
  98. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  99. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  100. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  102. data/lib/action_dispatch/middleware/callbacks.rb +34 -0
  103. data/lib/action_dispatch/middleware/cookies.rb +663 -0
  104. data/lib/action_dispatch/middleware/debug_exceptions.rb +185 -0
  105. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  106. data/lib/action_dispatch/middleware/debug_view.rb +68 -0
  107. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -0
  108. data/lib/action_dispatch/middleware/executor.rb +21 -0
  109. data/lib/action_dispatch/middleware/flash.rb +300 -0
  110. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  111. data/lib/action_dispatch/middleware/public_exceptions.rb +61 -0
  112. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  113. data/lib/action_dispatch/middleware/remote_ip.rb +181 -0
  114. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  115. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  116. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  117. data/lib/action_dispatch/middleware/session/cookie_store.rb +113 -0
  118. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  119. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  120. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  121. data/lib/action_dispatch/middleware/stack.rb +148 -0
  122. data/lib/action_dispatch/middleware/static.rb +129 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +24 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +29 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +38 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +165 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  143. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  146. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  147. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  148. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  149. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +203 -0
  150. data/lib/action_dispatch/railtie.rb +58 -0
  151. data/lib/action_dispatch/request/session.rb +242 -0
  152. data/lib/action_dispatch/request/utils.rb +78 -0
  153. data/lib/action_dispatch/routing.rb +261 -0
  154. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  155. data/lib/action_dispatch/routing/inspector.rb +274 -0
  156. data/lib/action_dispatch/routing/mapper.rb +2289 -0
  157. data/lib/action_dispatch/routing/polymorphic_routes.rb +351 -0
  158. data/lib/action_dispatch/routing/redirection.rb +201 -0
  159. data/lib/action_dispatch/routing/route_set.rb +887 -0
  160. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  161. data/lib/action_dispatch/routing/url_for.rb +237 -0
  162. data/lib/action_dispatch/system_test_case.rb +168 -0
  163. data/lib/action_dispatch/system_testing/browser.rb +80 -0
  164. data/lib/action_dispatch/system_testing/driver.rb +68 -0
  165. data/lib/action_dispatch/system_testing/server.rb +31 -0
  166. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +97 -0
  167. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +33 -0
  168. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  169. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  170. data/lib/action_dispatch/testing/assertions.rb +24 -0
  171. data/lib/action_dispatch/testing/assertions/response.rb +106 -0
  172. data/lib/action_dispatch/testing/assertions/routing.rb +234 -0
  173. data/lib/action_dispatch/testing/integration.rb +659 -0
  174. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  175. data/lib/action_dispatch/testing/test_process.rb +50 -0
  176. data/lib/action_dispatch/testing/test_request.rb +71 -0
  177. data/lib/action_dispatch/testing/test_response.rb +25 -0
  178. data/lib/action_pack.rb +26 -0
  179. data/lib/action_pack/gem_version.rb +17 -0
  180. data/lib/action_pack/version.rb +10 -0
  181. metadata +329 -0
@@ -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
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ class LogSubscriber < ActiveSupport::LogSubscriber
5
+ INTERNAL_PARAMS = %w(controller action format _method only_path)
6
+
7
+ def start_processing(event)
8
+ return unless logger.info?
9
+
10
+ payload = event.payload
11
+ params = payload[:params].except(*INTERNAL_PARAMS)
12
+ format = payload[:format]
13
+ format = format.to_s.upcase if format.is_a?(Symbol)
14
+
15
+ info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
16
+ info " Parameters: #{params.inspect}" unless params.empty?
17
+ end
18
+
19
+ def process_action(event)
20
+ info do
21
+ payload = event.payload
22
+ additions = ActionController::Base.log_process_action(payload)
23
+ status = payload[:status]
24
+
25
+ if status.nil? && payload[:exception].present?
26
+ exception_class_name = payload[:exception].first
27
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
28
+ end
29
+
30
+ additions << "Allocations: #{event.allocations}"
31
+
32
+ message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
33
+ message << " (#{additions.join(" | ")})" unless additions.empty?
34
+ message << "\n\n" if defined?(Rails.env) && Rails.env.development?
35
+
36
+ message
37
+ end
38
+ end
39
+
40
+ def halted_callback(event)
41
+ info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
42
+ end
43
+
44
+ def send_file(event)
45
+ info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
46
+ end
47
+
48
+ def redirect_to(event)
49
+ info { "Redirected to #{event.payload[:location]}" }
50
+ end
51
+
52
+ def send_data(event)
53
+ info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
54
+ end
55
+
56
+ def unpermitted_parameters(event)
57
+ debug do
58
+ unpermitted_keys = event.payload[:keys]
59
+ color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED)
60
+ end
61
+ end
62
+
63
+ %w(write_fragment read_fragment exist_fragment?
64
+ expire_fragment expire_page write_page).each do |method|
65
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
66
+ def #{method}(event)
67
+ return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
68
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
69
+ human_name = #{method.to_s.humanize.inspect}
70
+ info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
71
+ end
72
+ METHOD
73
+ end
74
+
75
+ def logger
76
+ ActionController::Base.logger
77
+ end
78
+ end
79
+ end
80
+
81
+ ActionController::LogSubscriber.attach_to :action_controller
@@ -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 = nil, &block)
30
+ action = action.to_s
31
+
32
+ middlewares.reverse.inject(app || block) 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, :media_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
10
+ head :no_content
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ module ConditionalGet
5
+ extend ActiveSupport::Concern
6
+
7
+ include Head
8
+
9
+ included do
10
+ class_attribute :etaggers, default: []
11
+ end
12
+
13
+ module ClassMethods
14
+ # Allows you to consider additional controller-wide information when generating an ETag.
15
+ # For example, if you serve pages tailored depending on who's logged in at the moment, you
16
+ # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
17
+ # of cached pages.
18
+ #
19
+ # class InvoicesController < ApplicationController
20
+ # etag { current_user.try :id }
21
+ #
22
+ # def show
23
+ # # Etag will differ even for the same invoice when it's viewed by a different current_user
24
+ # @invoice = Invoice.find(params[:id])
25
+ # fresh_when(@invoice)
26
+ # end
27
+ # end
28
+ def etag(&etagger)
29
+ self.etaggers += [etagger]
30
+ end
31
+ end
32
+
33
+ # Sets the +etag+, +last_modified+, or both on the response and renders a
34
+ # <tt>304 Not Modified</tt> response if the request is already fresh.
35
+ #
36
+ # === Parameters:
37
+ #
38
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
39
+ # +:weak_etag+ option.
40
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
41
+ # Requests that set If-None-Match header may return a 304 Not Modified
42
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
43
+ # equivalence, not byte-for-byte equality, so they're good for caching
44
+ # HTML pages in browser caches. They can't be used for responses that
45
+ # must be byte-identical, like serving Range requests within a PDF file.
46
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
47
+ # Requests that set If-None-Match header may return a 304 Not Modified
48
+ # response if it matches the ETag exactly. A strong ETag implies exact
49
+ # equality: the response must match byte for byte. This is necessary for
50
+ # doing Range requests within a large video or PDF file, for example, or
51
+ # for compatibility with some CDNs that don't support weak ETags.
52
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
53
+ # response. Subsequent requests that set If-Modified-Since may return a
54
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
55
+ # * <tt>:public</tt> By default the Cache-Control header is private, set this to
56
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
57
+ # * <tt>:template</tt> By default, the template digest for the current
58
+ # controller/action is included in ETags. If the action renders a
59
+ # different template, you can include its digest instead. If the action
60
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
61
+ # to skip any attempt to check for a template digest.
62
+ #
63
+ # === Example:
64
+ #
65
+ # def show
66
+ # @article = Article.find(params[:id])
67
+ # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
68
+ # end
69
+ #
70
+ # This will render the show template if the request isn't sending a matching ETag or
71
+ # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
72
+ #
73
+ # You can also just pass a record. In this case +last_modified+ will be set
74
+ # by calling +updated_at+ and +etag+ by passing the object itself.
75
+ #
76
+ # def show
77
+ # @article = Article.find(params[:id])
78
+ # fresh_when(@article)
79
+ # end
80
+ #
81
+ # You can also pass an object that responds to +maximum+, such as a
82
+ # collection of active records. In this case +last_modified+ will be set by
83
+ # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
84
+ # most recently updated record) and the +etag+ by passing the object itself.
85
+ #
86
+ # def index
87
+ # @articles = Article.all
88
+ # fresh_when(@articles)
89
+ # end
90
+ #
91
+ # When passing a record or a collection, you can still set the public header:
92
+ #
93
+ # def show
94
+ # @article = Article.find(params[:id])
95
+ # fresh_when(@article, public: true)
96
+ # end
97
+ #
98
+ # When rendering a different template than the default controller/action
99
+ # style, you can indicate which digest to include in the ETag:
100
+ #
101
+ # before_action { fresh_when @article, template: 'widgets/show' }
102
+ #
103
+ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
104
+ weak_etag ||= etag || object unless strong_etag
105
+ last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
106
+
107
+ if strong_etag
108
+ response.strong_etag = combine_etags strong_etag,
109
+ last_modified: last_modified, public: public, template: template
110
+ elsif weak_etag || template
111
+ response.weak_etag = combine_etags weak_etag,
112
+ last_modified: last_modified, public: public, template: template
113
+ end
114
+
115
+ response.last_modified = last_modified if last_modified
116
+ response.cache_control[:public] = true if public
117
+
118
+ head :not_modified if request.fresh?(response)
119
+ end
120
+
121
+ # Sets the +etag+ and/or +last_modified+ on the response and checks it against
122
+ # the client request. If the request doesn't match the options provided, the
123
+ # request is considered stale and should be generated from scratch. Otherwise,
124
+ # it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
125
+ #
126
+ # === Parameters:
127
+ #
128
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
129
+ # +:weak_etag+ option.
130
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
131
+ # Requests that set If-None-Match header may return a 304 Not Modified
132
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
133
+ # equivalence, not byte-for-byte equality, so they're good for caching
134
+ # HTML pages in browser caches. They can't be used for responses that
135
+ # must be byte-identical, like serving Range requests within a PDF file.
136
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
137
+ # Requests that set If-None-Match header may return a 304 Not Modified
138
+ # response if it matches the ETag exactly. A strong ETag implies exact
139
+ # equality: the response must match byte for byte. This is necessary for
140
+ # doing Range requests within a large video or PDF file, for example, or
141
+ # for compatibility with some CDNs that don't support weak ETags.
142
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
143
+ # response. Subsequent requests that set If-Modified-Since may return a
144
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
145
+ # * <tt>:public</tt> By default the Cache-Control header is private, set this to
146
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
147
+ # * <tt>:template</tt> By default, the template digest for the current
148
+ # controller/action is included in ETags. If the action renders a
149
+ # different template, you can include its digest instead. If the action
150
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
151
+ # to skip any attempt to check for a template digest.
152
+ #
153
+ # === Example:
154
+ #
155
+ # def show
156
+ # @article = Article.find(params[:id])
157
+ #
158
+ # if stale?(etag: @article, last_modified: @article.updated_at)
159
+ # @statistics = @article.really_expensive_call
160
+ # respond_to do |format|
161
+ # # all the supported formats
162
+ # end
163
+ # end
164
+ # end
165
+ #
166
+ # You can also just pass a record. In this case +last_modified+ will be set
167
+ # by calling +updated_at+ and +etag+ by passing the object itself.
168
+ #
169
+ # def show
170
+ # @article = Article.find(params[:id])
171
+ #
172
+ # if stale?(@article)
173
+ # @statistics = @article.really_expensive_call
174
+ # respond_to do |format|
175
+ # # all the supported formats
176
+ # end
177
+ # end
178
+ # end
179
+ #
180
+ # You can also pass an object that responds to +maximum+, such as a
181
+ # collection of active records. In this case +last_modified+ will be set by
182
+ # calling +maximum(:updated_at)+ on the collection (the timestamp of the
183
+ # most recently updated record) and the +etag+ by passing the object itself.
184
+ #
185
+ # def index
186
+ # @articles = Article.all
187
+ #
188
+ # if stale?(@articles)
189
+ # @statistics = @articles.really_expensive_call
190
+ # respond_to do |format|
191
+ # # all the supported formats
192
+ # end
193
+ # end
194
+ # end
195
+ #
196
+ # When passing a record or a collection, you can still set the public header:
197
+ #
198
+ # def show
199
+ # @article = Article.find(params[:id])
200
+ #
201
+ # if stale?(@article, public: true)
202
+ # @statistics = @article.really_expensive_call
203
+ # respond_to do |format|
204
+ # # all the supported formats
205
+ # end
206
+ # end
207
+ # end
208
+ #
209
+ # When rendering a different template than the default controller/action
210
+ # style, you can indicate which digest to include in the ETag:
211
+ #
212
+ # def show
213
+ # super if stale? @article, template: 'widgets/show'
214
+ # end
215
+ #
216
+ def stale?(object = nil, **freshness_kwargs)
217
+ fresh_when(object, **freshness_kwargs)
218
+ !request.fresh?(response)
219
+ end
220
+
221
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
222
+ # instruction, so that intermediate caches must not cache the response.
223
+ #
224
+ # expires_in 20.minutes
225
+ # expires_in 3.hours, public: true
226
+ # expires_in 3.hours, public: true, must_revalidate: true
227
+ #
228
+ # This method will overwrite an existing Cache-Control header.
229
+ # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
230
+ #
231
+ # HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861
232
+ # It helps to cache an asset and serve it while is being revalidated and/or returning with an error.
233
+ #
234
+ # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds
235
+ # expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes
236
+ #
237
+ # The method will also ensure an HTTP Date header for client compatibility.
238
+ def expires_in(seconds, options = {})
239
+ response.cache_control.merge!(
240
+ max_age: seconds,
241
+ public: options.delete(:public),
242
+ must_revalidate: options.delete(:must_revalidate),
243
+ stale_while_revalidate: options.delete(:stale_while_revalidate),
244
+ stale_if_error: options.delete(:stale_if_error),
245
+ )
246
+ options.delete(:private)
247
+
248
+ response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
249
+ response.date = Time.now unless response.date?
250
+ end
251
+
252
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
253
+ # resource will be marked as stale, so clients must always revalidate.
254
+ # Intermediate/browser caches may still store the asset.
255
+ def expires_now
256
+ response.cache_control.replace(no_cache: true)
257
+ end
258
+
259
+ # Cache or yield the block. The cache is supposed to never expire.
260
+ #
261
+ # You can use this method when you have an HTTP response that never changes,
262
+ # and the browser and proxies should cache it indefinitely.
263
+ #
264
+ # * +public+: By default, HTTP responses are private, cached only on the
265
+ # user's web browser. To allow proxies to cache the response, set +true+ to
266
+ # indicate that they can serve the cached response to all users.
267
+ def http_cache_forever(public: false)
268
+ expires_in 100.years, public: public
269
+
270
+ yield if stale?(etag: request.fullpath,
271
+ last_modified: Time.new(2011, 1, 1).utc,
272
+ public: public)
273
+ end
274
+
275
+ private
276
+ def combine_etags(validator, options)
277
+ [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
278
+ end
279
+ end
280
+ end