actionpack 4.2.10 → 7.2.0.rc1

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

Potentially problematic release.


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

Files changed (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,77 +1,113 @@
1
- require 'action_dispatch/http/response'
2
- require 'delegate'
3
- require 'active_support/json'
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "action_dispatch/http/response"
6
+ require "delegate"
7
+ require "active_support/json"
4
8
 
5
9
  module ActionController
6
- # Mix this module in to your controller, and all actions in that controller
7
- # will be able to stream data to the client as it's written.
10
+ # # Action Controller Live
8
11
  #
9
- # class MyController < ActionController::Base
10
- # include ActionController::Live
12
+ # Mix this module into your controller, and all actions in that controller will
13
+ # be able to stream data to the client as it's written.
11
14
  #
12
- # def stream
13
- # response.headers['Content-Type'] = 'text/event-stream'
14
- # 100.times {
15
- # response.stream.write "hello world\n"
16
- # sleep 1
17
- # }
18
- # ensure
19
- # response.stream.close
15
+ # class MyController < ActionController::Base
16
+ # include ActionController::Live
17
+ #
18
+ # def stream
19
+ # response.headers['Content-Type'] = 'text/event-stream'
20
+ # 100.times {
21
+ # response.stream.write "hello world\n"
22
+ # sleep 1
23
+ # }
24
+ # ensure
25
+ # response.stream.close
26
+ # end
20
27
  # end
21
- # end
22
28
  #
23
- # There are a few caveats with this use. You *cannot* write headers after the
24
- # response has been committed (Response#committed? will return truthy).
25
- # Calling +write+ or +close+ on the response stream will cause the response
26
- # object to be committed. Make sure all headers are set before calling write
27
- # or close on your stream.
29
+ # There are a few caveats with this module. You **cannot** write headers after
30
+ # the response has been committed (Response#committed? will return truthy).
31
+ # Calling `write` or `close` on the response stream will cause the response
32
+ # object to be committed. Make sure all headers are set before calling write or
33
+ # close on your stream.
28
34
  #
29
- # You *must* call close on your stream when you're finished, otherwise the
35
+ # You **must** call close on your stream when you're finished, otherwise the
30
36
  # socket may be left open forever.
31
37
  #
32
38
  # The final caveat is that your actions are executed in a separate thread than
33
- # the main thread. Make sure your actions are thread safe, and this shouldn't
34
- # be a problem (don't share state across threads, etc).
39
+ # the main thread. Make sure your actions are thread safe, and this shouldn't be
40
+ # a problem (don't share state across threads, etc).
41
+ #
42
+ # Note that Rails includes `Rack::ETag` by default, which will buffer your
43
+ # response. As a result, streaming responses may not work properly with Rack
44
+ # 2.2.x, and you may need to implement workarounds in your application. You can
45
+ # either set the `ETag` or `Last-Modified` response headers or remove
46
+ # `Rack::ETag` from the middleware stack to address this issue.
47
+ #
48
+ # Here's an example of how you can set the `Last-Modified` header if your Rack
49
+ # version is 2.2.x:
50
+ #
51
+ # def stream
52
+ # response.headers["Content-Type"] = "text/event-stream"
53
+ # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
54
+ # ...
55
+ # end
35
56
  module Live
36
- # This class provides the ability to write an SSE (Server Sent Event)
37
- # to an IO stream. The class is initialized with a stream and can be used
38
- # to either write a JSON string or an object which can be converted to JSON.
57
+ extend ActiveSupport::Concern
58
+
59
+ module ClassMethods
60
+ def make_response!(request)
61
+ if request.get_header("HTTP_VERSION") == "HTTP/1.0"
62
+ super
63
+ else
64
+ Live::Response.new.tap do |res|
65
+ res.request = request
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # # Action Controller Live Server Sent Events
72
+ #
73
+ # This class provides the ability to write an SSE (Server Sent Event) to an IO
74
+ # stream. The class is initialized with a stream and can be used to either write
75
+ # a JSON string or an object which can be converted to JSON.
39
76
  #
40
77
  # Writing an object will convert it into standard SSE format with whatever
41
78
  # options you have configured. You may choose to set the following options:
42
79
  #
43
- # 1) Event. If specified, an event with this name will be dispatched on
44
- # the browser.
45
- # 2) Retry. The reconnection time in milliseconds used when attempting
46
- # to send the event.
47
- # 3) Id. If the connection dies while sending an SSE to the browser, then
48
- # the server will receive a +Last-Event-ID+ header with value equal to +id+.
80
+ # 1) Event. If specified, an event with this name will be dispatched on
81
+ # the browser.
82
+ # 2) Retry. The reconnection time in milliseconds used when attempting
83
+ # to send the event.
84
+ # 3) Id. If the connection dies while sending an SSE to the browser, then
85
+ # the server will receive a +Last-Event-ID+ header with value equal to +id+.
49
86
  #
50
- # After setting an option in the constructor of the SSE object, all future
51
- # SSEs sent across the stream will use those options unless overridden.
87
+ # After setting an option in the constructor of the SSE object, all future SSEs
88
+ # sent across the stream will use those options unless overridden.
52
89
  #
53
90
  # Example Usage:
54
91
  #
55
- # class MyController < ActionController::Base
56
- # include ActionController::Live
92
+ # class MyController < ActionController::Base
93
+ # include ActionController::Live
57
94
  #
58
- # def index
59
- # response.headers['Content-Type'] = 'text/event-stream'
60
- # sse = SSE.new(response.stream, retry: 300, event: "event-name")
61
- # sse.write({ name: 'John'})
62
- # sse.write({ name: 'John'}, id: 10)
63
- # sse.write({ name: 'John'}, id: 10, event: "other-event")
64
- # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
65
- # ensure
66
- # sse.close
95
+ # def index
96
+ # response.headers['Content-Type'] = 'text/event-stream'
97
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
98
+ # sse.write({ name: 'John'})
99
+ # sse.write({ name: 'John'}, id: 10)
100
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
101
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
102
+ # ensure
103
+ # sse.close
104
+ # end
67
105
  # end
68
- # end
69
106
  #
70
- # Note: SSEs are not currently supported by IE. However, they are supported
71
- # by Chrome, Firefox, Opera, and Safari.
107
+ # Note: SSEs are not currently supported by IE. However, they are supported by
108
+ # Chrome, Firefox, Opera, and Safari.
72
109
  class SSE
73
-
74
- WHITELISTED_OPTIONS = %w( retry event id )
110
+ PERMITTED_OPTIONS = %w( retry event id )
75
111
 
76
112
  def initialize(stream, options = {})
77
113
  @stream = stream
@@ -92,17 +128,16 @@ module ActionController
92
128
  end
93
129
 
94
130
  private
95
-
96
131
  def perform_write(json, options)
97
132
  current_options = @options.merge(options).stringify_keys
98
133
 
99
- WHITELISTED_OPTIONS.each do |option_name|
134
+ PERMITTED_OPTIONS.each do |option_name|
100
135
  if (option_value = current_options[option_name])
101
136
  @stream.write "#{option_name}: #{option_value}\n"
102
137
  end
103
138
  end
104
139
 
105
- message = json.gsub(/\n/, "\ndata: ")
140
+ message = json.gsub("\n", "\ndata: ")
106
141
  @stream.write "data: #{message}\n\n"
107
142
  end
108
143
  end
@@ -110,29 +145,39 @@ module ActionController
110
145
  class ClientDisconnected < RuntimeError
111
146
  end
112
147
 
113
- class Buffer < ActionDispatch::Response::Buffer #:nodoc:
148
+ class Buffer < ActionDispatch::Response::Buffer # :nodoc:
114
149
  include MonitorMixin
115
150
 
151
+ class << self
152
+ attr_accessor :queue_size
153
+ end
154
+ @queue_size = 10
155
+
116
156
  # Ignore that the client has disconnected.
117
157
  #
118
- # If this value is `true`, calling `write` after the client
119
- # disconnects will result in the written content being silently
120
- # discarded. If this value is `false` (the default), a
121
- # ClientDisconnected exception will be raised.
158
+ # If this value is `true`, calling `write` after the client disconnects will
159
+ # result in the written content being silently discarded. If this value is
160
+ # `false` (the default), a ClientDisconnected exception will be raised.
122
161
  attr_accessor :ignore_disconnect
123
162
 
124
163
  def initialize(response)
164
+ super(response, build_queue(self.class.queue_size))
125
165
  @error_callback = lambda { true }
126
166
  @cv = new_cond
127
167
  @aborted = false
128
168
  @ignore_disconnect = false
129
- super(response, SizedQueue.new(10))
130
169
  end
131
170
 
171
+ # ActionDispatch::Response delegates #to_ary to the internal
172
+ # ActionDispatch::Response::Buffer, defining #to_ary is an indicator that the
173
+ # response body can be buffered and/or cached by Rack middlewares, this is not
174
+ # the case for Live responses so we undefine it for this Buffer subclass.
175
+ undef_method :to_ary
176
+
132
177
  def write(string)
133
178
  unless @response.committed?
134
- @response.headers["Cache-Control"] = "no-cache"
135
- @response.headers.delete "Content-Length"
179
+ @response.headers["Cache-Control"] ||= "no-cache"
180
+ @response.delete_header "Content-Length"
136
181
  end
137
182
 
138
183
  super
@@ -141,24 +186,20 @@ module ActionController
141
186
  @buf.clear
142
187
 
143
188
  unless @ignore_disconnect
144
- # Raise ClientDisconnected, which is a RuntimeError (not an
145
- # IOError), because that's more appropriate for something beyond
146
- # the developer's control.
189
+ # Raise ClientDisconnected, which is a RuntimeError (not an IOError), because
190
+ # that's more appropriate for something beyond the developer's control.
147
191
  raise ClientDisconnected, "client disconnected"
148
192
  end
149
193
  end
150
194
  end
151
195
 
152
- def each
153
- @response.sending!
154
- while str = @buf.pop
155
- yield str
156
- end
157
- @response.sent!
196
+ # Same as `write` but automatically include a newline at the end of the string.
197
+ def writeln(string)
198
+ write string.end_with?("\n") ? string : "#{string}\n"
158
199
  end
159
200
 
160
- # Write a 'close' event to the buffer; the producer/writing thread
161
- # uses this to notify us that it's finished supplying content.
201
+ # Write a 'close' event to the buffer; the producer/writing thread uses this to
202
+ # notify us that it's finished supplying content.
162
203
  #
163
204
  # See also #abort.
164
205
  def close
@@ -169,9 +210,8 @@ module ActionController
169
210
  end
170
211
  end
171
212
 
172
- # Inform the producer/writing thread that the client has
173
- # disconnected; the reading thread is no longer interested in
174
- # anything that's being written.
213
+ # Inform the producer/writing thread that the client has disconnected; the
214
+ # reading thread is no longer interested in anything that's being written.
175
215
  #
176
216
  # See also #close.
177
217
  def abort
@@ -183,18 +223,12 @@ module ActionController
183
223
 
184
224
  # Is the client still connected and waiting for content?
185
225
  #
186
- # The result of calling `write` when this is `false` is determined
187
- # by `ignore_disconnect`.
226
+ # The result of calling `write` when this is `false` is determined by
227
+ # `ignore_disconnect`.
188
228
  def connected?
189
229
  !@aborted
190
230
  end
191
231
 
192
- def await_close
193
- synchronize do
194
- @cv.wait_until { @closed }
195
- end
196
- end
197
-
198
232
  def on_error(&block)
199
233
  @error_callback = block
200
234
  end
@@ -202,60 +236,38 @@ module ActionController
202
236
  def call_on_error
203
237
  @error_callback.call
204
238
  end
205
- end
206
239
 
207
- class Response < ActionDispatch::Response #:nodoc: all
208
- class Header < DelegateClass(Hash) # :nodoc:
209
- def initialize(response, header)
210
- @response = response
211
- super(header)
212
- end
213
-
214
- def []=(k,v)
215
- if @response.committed?
216
- raise ActionDispatch::IllegalStateError, 'header already sent'
240
+ private
241
+ def each_chunk(&block)
242
+ loop do
243
+ str = nil
244
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
245
+ str = @buf.pop
246
+ end
247
+ break unless str
248
+ yield str
217
249
  end
218
-
219
- super
220
- end
221
-
222
- def merge(other)
223
- self.class.new @response, __getobj__.merge(other)
224
250
  end
225
251
 
226
- def to_hash
227
- __getobj__.dup
252
+ def build_queue(queue_size)
253
+ queue_size ? SizedQueue.new(queue_size) : Queue.new
228
254
  end
229
- end
255
+ end
230
256
 
257
+ class Response < ActionDispatch::Response # :nodoc: all
231
258
  private
259
+ def before_committed
260
+ super
261
+ jar = request.cookie_jar
262
+ # The response can be committed multiple times
263
+ jar.write self unless committed?
264
+ end
232
265
 
233
- def before_committed
234
- super
235
- jar = request.cookie_jar
236
- # The response can be committed multiple times
237
- jar.write self unless committed?
238
- end
239
-
240
- def before_sending
241
- super
242
- request.cookie_jar.commit!
243
- headers.freeze
244
- end
245
-
246
- def build_buffer(response, body)
247
- buf = Live::Buffer.new response
248
- body.each { |part| buf.write part }
249
- buf
250
- end
251
-
252
- def merge_default_headers(original, default)
253
- Header.new self, super
254
- end
255
-
256
- def handle_conditional_get!
257
- super unless committed?
258
- end
266
+ def build_buffer(response, body)
267
+ buf = Live::Buffer.new response
268
+ body.each { |part| buf.write part }
269
+ buf
270
+ end
259
271
  end
260
272
 
261
273
  def process(name)
@@ -263,52 +275,45 @@ module ActionController
263
275
  locals = t1.keys.map { |key| [key, t1[key]] }
264
276
 
265
277
  error = nil
266
- # This processes the action in a child thread. It lets us return the
267
- # response code and headers back up the rack stack, and still process
268
- # the body in parallel with sending data to the client
269
- Thread.new {
270
- t2 = Thread.current
271
- t2.abort_on_exception = true
272
-
273
- # Since we're processing the view in a different thread, copy the
274
- # thread locals from the main thread to the child thread. :'(
275
- locals.each { |k,v| t2[k] = v }
276
-
277
- begin
278
- super(name)
279
- rescue => e
280
- if @_response.committed?
281
- begin
282
- @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
283
- @_response.stream.call_on_error
284
- rescue => exception
285
- log_error(exception)
286
- ensure
287
- log_error(e)
288
- @_response.stream.close
278
+ # This processes the action in a child thread. It lets us return the response
279
+ # code and headers back up the Rack stack, and still process the body in
280
+ # parallel with sending data to the client.
281
+ new_controller_thread {
282
+ ActiveSupport::Dependencies.interlock.running do
283
+ t2 = Thread.current
284
+
285
+ # Since we're processing the view in a different thread, copy the thread locals
286
+ # from the main thread to the child thread. :'(
287
+ locals.each { |k, v| t2[k] = v }
288
+ ActiveSupport::IsolatedExecutionState.share_with(t1)
289
+
290
+ begin
291
+ super(name)
292
+ rescue => e
293
+ if @_response.committed?
294
+ begin
295
+ @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
296
+ @_response.stream.call_on_error
297
+ rescue => exception
298
+ log_error(exception)
299
+ ensure
300
+ log_error(e)
301
+ @_response.stream.close
302
+ end
303
+ else
304
+ error = e
289
305
  end
290
- else
291
- error = e
306
+ ensure
307
+ @_response.commit!
292
308
  end
293
- ensure
294
- @_response.commit!
295
309
  end
296
310
  }
297
311
 
298
- @_response.await_commit
299
- raise error if error
300
- end
301
-
302
- def log_error(exception)
303
- logger = ActionController::Base.logger
304
- return unless logger
305
-
306
- logger.fatal do
307
- message = "\n#{exception.class} (#{exception.message}):\n"
308
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
309
- message << " " << exception.backtrace.join("\n ")
310
- "#{message}\n\n"
312
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
313
+ @_response.await_commit
311
314
  end
315
+
316
+ raise error if error
312
317
  end
313
318
 
314
319
  def response_body=(body)
@@ -316,13 +321,70 @@ module ActionController
316
321
  response.close if response
317
322
  end
318
323
 
319
- def set_response!(request)
320
- if request.env["HTTP_VERSION"] == "HTTP/1.0"
321
- super
322
- else
323
- @_response = Live::Response.new
324
- @_response.request = request
324
+ # Sends a stream to the browser, which is helpful when you're generating exports
325
+ # or other running data where you don't want the entire file buffered in memory
326
+ # first. Similar to send_data, but where the data is generated live.
327
+ #
328
+ # Options:
329
+ # * `:filename` - suggests a filename for the browser to use.
330
+ # * `:type` - specifies an HTTP content type. You can specify either a string
331
+ # or a symbol for a registered type with `Mime::Type.register`, for example
332
+ # :json. If omitted, type will be inferred from the file extension specified
333
+ # in `:filename`. If no content type is registered for the extension, the
334
+ # default type 'application/octet-stream' will be used.
335
+ # * `:disposition` - specifies whether the file will be shown inline or
336
+ # downloaded. Valid values are 'inline' and 'attachment' (default).
337
+ #
338
+ #
339
+ # Example of generating a csv export:
340
+ #
341
+ # send_stream(filename: "subscribers.csv") do |stream|
342
+ # stream.write "email_address,updated_at\n"
343
+ #
344
+ # @subscribers.find_each do |subscriber|
345
+ # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
346
+ # end
347
+ # end
348
+ def send_stream(filename:, disposition: "attachment", type: nil)
349
+ payload = { filename: filename, disposition: disposition, type: type }
350
+ ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do
351
+ response.headers["Content-Type"] =
352
+ (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
353
+ Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
354
+ "application/octet-stream"
355
+
356
+ response.headers["Content-Disposition"] =
357
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
358
+
359
+ yield response.stream
325
360
  end
361
+ ensure
362
+ response.stream.close
326
363
  end
364
+
365
+ private
366
+ # Spawn a new thread to serve up the controller in. This is to get around the
367
+ # fact that Rack isn't based around IOs and we need to use a thread to stream
368
+ # data from the response bodies. Nobody should call this method except in Rails
369
+ # internals. Seriously!
370
+ def new_controller_thread # :nodoc:
371
+ Thread.new {
372
+ t2 = Thread.current
373
+ t2.abort_on_exception = true
374
+ yield
375
+ }
376
+ end
377
+
378
+ def log_error(exception)
379
+ logger = ActionController::Base.logger
380
+ return unless logger
381
+
382
+ logger.fatal do
383
+ message = +"\n#{exception.class} (#{exception.message}):\n"
384
+ message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
385
+ message << " " << exception.backtrace.join("\n ")
386
+ "#{message}\n\n"
387
+ end
388
+ end
327
389
  end
328
390
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController
6
+ module Logging
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Set a different log level per request.
11
+ #
12
+ # # Use the debug log level if a particular cookie is set.
13
+ # class ApplicationController < ActionController::Base
14
+ # log_at :debug, if: -> { cookies[:debug] }
15
+ # end
16
+ #
17
+ def log_at(level, **options)
18
+ around_action ->(_, action) { logger.log_at(level, &action) }, **options
19
+ end
20
+ end
21
+ end
22
+ end