actionpack 6.1.7.5 → 7.1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +355 -435
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +33 -37
  7. data/lib/abstract_controller/caching/fragments.rb +4 -2
  8. data/lib/abstract_controller/caching.rb +1 -1
  9. data/lib/abstract_controller/callbacks.rb +50 -11
  10. data/lib/abstract_controller/collector.rb +2 -2
  11. data/lib/abstract_controller/deprecator.rb +7 -0
  12. data/lib/abstract_controller/error.rb +1 -1
  13. data/lib/abstract_controller/helpers.rb +78 -30
  14. data/lib/abstract_controller/logger.rb +1 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +3 -16
  16. data/lib/abstract_controller/rendering.rb +12 -14
  17. data/lib/abstract_controller/translation.rb +26 -7
  18. data/lib/abstract_controller/url_for.rb +6 -6
  19. data/lib/abstract_controller.rb +6 -0
  20. data/lib/action_controller/api.rb +12 -10
  21. data/lib/action_controller/base.rb +8 -21
  22. data/lib/action_controller/caching.rb +2 -0
  23. data/lib/action_controller/deprecator.rb +7 -0
  24. data/lib/action_controller/form_builder.rb +4 -2
  25. data/lib/action_controller/log_subscriber.rb +20 -7
  26. data/lib/action_controller/metal/basic_implicit_render.rb +3 -1
  27. data/lib/action_controller/metal/conditional_get.rb +137 -102
  28. data/lib/action_controller/metal/content_security_policy.rb +37 -3
  29. data/lib/action_controller/metal/cookies.rb +1 -1
  30. data/lib/action_controller/metal/data_streaming.rb +25 -31
  31. data/lib/action_controller/metal/default_headers.rb +2 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -0
  34. data/lib/action_controller/metal/exceptions.rb +27 -30
  35. data/lib/action_controller/metal/flash.rb +6 -2
  36. data/lib/action_controller/metal/head.rb +9 -7
  37. data/lib/action_controller/metal/helpers.rb +5 -16
  38. data/lib/action_controller/metal/http_authentication.rb +78 -42
  39. data/lib/action_controller/metal/implicit_render.rb +5 -3
  40. data/lib/action_controller/metal/instrumentation.rb +62 -50
  41. data/lib/action_controller/metal/live.rb +67 -2
  42. data/lib/action_controller/metal/mime_responds.rb +5 -5
  43. data/lib/action_controller/metal/params_wrapper.rb +24 -13
  44. data/lib/action_controller/metal/permissions_policy.rb +20 -29
  45. data/lib/action_controller/metal/redirecting.rb +96 -23
  46. data/lib/action_controller/metal/renderers.rb +14 -15
  47. data/lib/action_controller/metal/rendering.rb +121 -16
  48. data/lib/action_controller/metal/request_forgery_protection.rb +208 -68
  49. data/lib/action_controller/metal/rescue.rb +7 -4
  50. data/lib/action_controller/metal/streaming.rb +74 -36
  51. data/lib/action_controller/metal/strong_parameters.rb +254 -151
  52. data/lib/action_controller/metal/testing.rb +9 -2
  53. data/lib/action_controller/metal/url_for.rb +10 -5
  54. data/lib/action_controller/metal.rb +89 -34
  55. data/lib/action_controller/railtie.rb +66 -9
  56. data/lib/action_controller/renderer.rb +99 -85
  57. data/lib/action_controller/test_case.rb +42 -11
  58. data/lib/action_controller.rb +10 -6
  59. data/lib/action_dispatch/constants.rb +32 -0
  60. data/lib/action_dispatch/deprecator.rb +7 -0
  61. data/lib/action_dispatch/http/cache.rb +21 -16
  62. data/lib/action_dispatch/http/content_security_policy.rb +122 -44
  63. data/lib/action_dispatch/http/filter_parameters.rb +14 -23
  64. data/lib/action_dispatch/http/headers.rb +3 -1
  65. data/lib/action_dispatch/http/mime_negotiation.rb +25 -15
  66. data/lib/action_dispatch/http/mime_type.rb +43 -22
  67. data/lib/action_dispatch/http/mime_types.rb +3 -1
  68. data/lib/action_dispatch/http/parameters.rb +6 -6
  69. data/lib/action_dispatch/http/permissions_policy.rb +57 -19
  70. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  71. data/lib/action_dispatch/http/request.rb +75 -51
  72. data/lib/action_dispatch/http/response.rb +81 -77
  73. data/lib/action_dispatch/http/upload.rb +15 -2
  74. data/lib/action_dispatch/http/url.rb +11 -19
  75. data/lib/action_dispatch/journey/formatter.rb +8 -2
  76. data/lib/action_dispatch/journey/gtg/builder.rb +11 -12
  77. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -4
  78. data/lib/action_dispatch/journey/gtg/transition_table.rb +77 -21
  79. data/lib/action_dispatch/journey/nodes/node.rb +70 -5
  80. data/lib/action_dispatch/journey/path/pattern.rb +36 -27
  81. data/lib/action_dispatch/journey/route.rb +8 -14
  82. data/lib/action_dispatch/journey/router/utils.rb +2 -2
  83. data/lib/action_dispatch/journey/router.rb +10 -9
  84. data/lib/action_dispatch/journey/routes.rb +5 -5
  85. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  86. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  87. data/lib/action_dispatch/log_subscriber.rb +23 -0
  88. data/lib/action_dispatch/middleware/actionable_exceptions.rb +5 -7
  89. data/lib/action_dispatch/middleware/assume_ssl.rb +24 -0
  90. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  91. data/lib/action_dispatch/middleware/cookies.rb +97 -107
  92. data/lib/action_dispatch/middleware/debug_exceptions.rb +31 -28
  93. data/lib/action_dispatch/middleware/debug_locks.rb +7 -4
  94. data/lib/action_dispatch/middleware/debug_view.rb +7 -2
  95. data/lib/action_dispatch/middleware/exception_wrapper.rb +190 -27
  96. data/lib/action_dispatch/middleware/executor.rb +3 -0
  97. data/lib/action_dispatch/middleware/flash.rb +24 -18
  98. data/lib/action_dispatch/middleware/host_authorization.rb +19 -20
  99. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -3
  100. data/lib/action_dispatch/middleware/reloader.rb +7 -5
  101. data/lib/action_dispatch/middleware/remote_ip.rb +32 -19
  102. data/lib/action_dispatch/middleware/request_id.rb +5 -3
  103. data/lib/action_dispatch/middleware/server_timing.rb +76 -0
  104. data/lib/action_dispatch/middleware/session/abstract_store.rb +6 -1
  105. data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
  106. data/lib/action_dispatch/middleware/session/cookie_store.rb +19 -13
  107. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +3 -1
  108. data/lib/action_dispatch/middleware/show_exceptions.rb +30 -25
  109. data/lib/action_dispatch/middleware/ssl.rb +18 -6
  110. data/lib/action_dispatch/middleware/stack.rb +34 -11
  111. data/lib/action_dispatch/middleware/static.rb +16 -16
  112. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  113. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +5 -5
  114. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -11
  115. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  116. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +2 -2
  117. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +10 -5
  118. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -3
  119. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +9 -9
  120. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  121. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -3
  122. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +45 -18
  123. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -15
  124. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +4 -4
  125. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +6 -6
  126. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +7 -7
  127. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  128. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  129. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  130. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -55
  131. data/lib/action_dispatch/railtie.rb +20 -4
  132. data/lib/action_dispatch/request/session.rb +59 -19
  133. data/lib/action_dispatch/request/utils.rb +8 -3
  134. data/lib/action_dispatch/routing/inspector.rb +55 -7
  135. data/lib/action_dispatch/routing/mapper.rb +117 -107
  136. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  137. data/lib/action_dispatch/routing/redirection.rb +20 -8
  138. data/lib/action_dispatch/routing/route_set.rb +67 -27
  139. data/lib/action_dispatch/routing/routes_proxy.rb +11 -16
  140. data/lib/action_dispatch/routing/url_for.rb +29 -26
  141. data/lib/action_dispatch/routing.rb +12 -13
  142. data/lib/action_dispatch/system_test_case.rb +8 -8
  143. data/lib/action_dispatch/system_testing/browser.rb +20 -29
  144. data/lib/action_dispatch/system_testing/driver.rb +34 -18
  145. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +35 -20
  146. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +0 -8
  147. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  148. data/lib/action_dispatch/testing/assertions/response.rb +14 -7
  149. data/lib/action_dispatch/testing/assertions/routing.rb +70 -30
  150. data/lib/action_dispatch/testing/assertions.rb +3 -4
  151. data/lib/action_dispatch/testing/integration.rb +33 -25
  152. data/lib/action_dispatch/testing/request_encoder.rb +4 -1
  153. data/lib/action_dispatch/testing/test_process.rb +5 -30
  154. data/lib/action_dispatch/testing/test_request.rb +1 -1
  155. data/lib/action_dispatch/testing/test_response.rb +34 -2
  156. data/lib/action_dispatch.rb +38 -4
  157. data/lib/action_pack/gem_version.rb +4 -4
  158. data/lib/action_pack/version.rb +1 -1
  159. data/lib/action_pack.rb +1 -1
  160. metadata +67 -30
@@ -4,6 +4,8 @@ require "benchmark"
4
4
  require "abstract_controller/logger"
5
5
 
6
6
  module ActionController
7
+ # = Action Controller \Instrumentation
8
+ #
7
9
  # Adds instrumentation to several ends in ActionController::Base. It also provides
8
10
  # some hooks related with process_action. This allows an ORM like Active Record
9
11
  # and/or DataMapper to plug in ActionController and show related information.
@@ -16,28 +18,9 @@ module ActionController
16
18
 
17
19
  attr_internal :view_runtime
18
20
 
19
- def process_action(*)
20
- raw_payload = {
21
- controller: self.class.name,
22
- action: action_name,
23
- request: request,
24
- params: request.filtered_parameters,
25
- headers: request.headers,
26
- format: request.format.ref,
27
- method: request.request_method,
28
- path: request.fullpath
29
- }
30
-
31
- ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
32
-
33
- ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
34
- result = super
35
- payload[:response] = response
36
- payload[:status] = response.status
37
- result
38
- ensure
39
- append_info_to_payload(payload)
40
- end
21
+ def initialize(...) # :nodoc:
22
+ super
23
+ self.view_runtime = nil
41
24
  end
42
25
 
43
26
  def render(*)
@@ -70,37 +53,66 @@ module ActionController
70
53
  end
71
54
  end
72
55
 
73
- private
74
- # A hook invoked every time a before callback is halted.
75
- def halted_callback_hook(filter, _)
76
- ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
77
- end
56
+ private
57
+ def process_action(*)
58
+ ActiveSupport::ExecutionContext[:controller] = self
78
59
 
79
- # A hook which allows you to clean up any time, wrongly taken into account in
80
- # views, like database querying time.
81
- #
82
- # def cleanup_view_runtime
83
- # super - time_taken_in_something_expensive
84
- # end
85
- def cleanup_view_runtime # :doc:
86
- yield
87
- end
60
+ raw_payload = {
61
+ controller: self.class.name,
62
+ action: action_name,
63
+ request: request,
64
+ params: request.filtered_parameters,
65
+ headers: request.headers,
66
+ format: request.format.ref,
67
+ method: request.request_method,
68
+ path: request.filtered_path
69
+ }
88
70
 
89
- # Every time after an action is processed, this method is invoked
90
- # with the payload, so you can add more information.
91
- def append_info_to_payload(payload) # :doc:
92
- payload[:view_runtime] = view_runtime
93
- end
71
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
94
72
 
95
- module ClassMethods
96
- # A hook which allows other frameworks to log what happened during
97
- # controller process action. This method should return an array
98
- # with the messages to be added.
99
- def log_process_action(payload) #:nodoc:
100
- messages, view_runtime = [], payload[:view_runtime]
101
- messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
102
- messages
73
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
74
+ result = super
75
+ payload[:response] = response
76
+ payload[:status] = response.status
77
+ result
78
+ rescue => error
79
+ payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
80
+ raise
81
+ ensure
82
+ append_info_to_payload(payload)
83
+ end
84
+ end
85
+
86
+ # A hook invoked every time a before callback is halted.
87
+ def halted_callback_hook(filter, _)
88
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
89
+ end
90
+
91
+ # A hook which allows you to clean up any time, wrongly taken into account in
92
+ # views, like database querying time.
93
+ #
94
+ # def cleanup_view_runtime
95
+ # super - time_taken_in_something_expensive
96
+ # end
97
+ def cleanup_view_runtime # :doc:
98
+ yield
99
+ end
100
+
101
+ # Every time after an action is processed, this method is invoked
102
+ # with the payload, so you can add more information.
103
+ def append_info_to_payload(payload) # :doc:
104
+ payload[:view_runtime] = view_runtime
105
+ end
106
+
107
+ module ClassMethods
108
+ # A hook which allows other frameworks to log what happened during
109
+ # controller process action. This method should return an array
110
+ # with the messages to be added.
111
+ def log_process_action(payload) # :nodoc:
112
+ messages, view_runtime = [], payload[:view_runtime]
113
+ messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
114
+ messages
115
+ end
103
116
  end
104
- end
105
117
  end
106
118
  end
@@ -5,6 +5,8 @@ require "delegate"
5
5
  require "active_support/json"
6
6
 
7
7
  module ActionController
8
+ # = Action Controller \Live
9
+ #
8
10
  # Mix this module into your controller, and all actions in that controller
9
11
  # will be able to stream data to the client as it's written.
10
12
  #
@@ -34,6 +36,21 @@ module ActionController
34
36
  # The final caveat is that your actions are executed in a separate thread than
35
37
  # the main thread. Make sure your actions are thread safe, and this shouldn't
36
38
  # be a problem (don't share state across threads, etc).
39
+ #
40
+ # Note that \Rails includes +Rack::ETag+ by default, which will buffer your
41
+ # response. As a result, streaming responses may not work properly with Rack
42
+ # 2.2.x, and you may need to implement workarounds in your application.
43
+ # You can either set the +ETag+ or +Last-Modified+ response headers or remove
44
+ # +Rack::ETag+ from the middleware stack to address this issue.
45
+ #
46
+ # Here's an example of how you can set the +Last-Modified+ header if your Rack
47
+ # version is 2.2.x:
48
+ #
49
+ # def stream
50
+ # response.headers["Content-Type"] = "text/event-stream"
51
+ # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
52
+ # ...
53
+ # end
37
54
  module Live
38
55
  extend ActiveSupport::Concern
39
56
 
@@ -49,6 +66,8 @@ module ActionController
49
66
  end
50
67
  end
51
68
 
69
+ # = Action Controller \Live Server Sent Events
70
+ #
52
71
  # This class provides the ability to write an SSE (Server Sent Event)
53
72
  # to an IO stream. The class is initialized with a stream and can be used
54
73
  # to either write a JSON string or an object which can be converted to JSON.
@@ -124,7 +143,7 @@ module ActionController
124
143
  class ClientDisconnected < RuntimeError
125
144
  end
126
145
 
127
- class Buffer < ActionDispatch::Response::Buffer #:nodoc:
146
+ class Buffer < ActionDispatch::Response::Buffer # :nodoc:
128
147
  include MonitorMixin
129
148
 
130
149
  class << self
@@ -148,6 +167,11 @@ module ActionController
148
167
  @ignore_disconnect = false
149
168
  end
150
169
 
170
+ # ActionDispatch::Response delegates #to_ary to the internal ActionDispatch::Response::Buffer,
171
+ # defining #to_ary is an indicator that the response body can be buffered and/or cached by
172
+ # Rack middlewares, this is not the case for Live responses so we undefine it for this Buffer subclass.
173
+ undef_method :to_ary
174
+
151
175
  def write(string)
152
176
  unless @response.committed?
153
177
  @response.headers["Cache-Control"] ||= "no-cache"
@@ -168,6 +192,11 @@ module ActionController
168
192
  end
169
193
  end
170
194
 
195
+ # Same as +write+ but automatically include a newline at the end of the string.
196
+ def writeln(string)
197
+ write string.end_with?("\n") ? string : "#{string}\n"
198
+ end
199
+
171
200
  # Write a 'close' event to the buffer; the producer/writing thread
172
201
  # uses this to notify us that it's finished supplying content.
173
202
  #
@@ -225,7 +254,7 @@ module ActionController
225
254
  end
226
255
  end
227
256
 
228
- class Response < ActionDispatch::Response #:nodoc: all
257
+ class Response < ActionDispatch::Response # :nodoc: all
229
258
  private
230
259
  def before_committed
231
260
  super
@@ -256,6 +285,7 @@ module ActionController
256
285
  # Since we're processing the view in a different thread, copy the
257
286
  # thread locals from the main thread to the child thread. :'(
258
287
  locals.each { |k, v| t2[k] = v }
288
+ ActiveSupport::IsolatedExecutionState.share_with(t1)
259
289
 
260
290
  begin
261
291
  super(name)
@@ -291,6 +321,41 @@ module ActionController
291
321
  response.close if response
292
322
  end
293
323
 
324
+ # Sends a stream to the browser, which is helpful when you're generating exports or other running data where you
325
+ # don't want the entire file buffered in memory first. Similar to send_data, but where the data is generated live.
326
+ #
327
+ # Options:
328
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
329
+ # * <tt>:type</tt> - specifies an HTTP content type.
330
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
331
+ # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
332
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
333
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
334
+ # Valid values are 'inline' and 'attachment' (default).
335
+ #
336
+ # Example of generating a csv export:
337
+ #
338
+ # send_stream(filename: "subscribers.csv") do |stream|
339
+ # stream.write "email_address,updated_at\n"
340
+ #
341
+ # @subscribers.find_each do |subscriber|
342
+ # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
343
+ # end
344
+ # end
345
+ def send_stream(filename:, disposition: "attachment", type: nil)
346
+ response.headers["Content-Type"] =
347
+ (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
348
+ Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
349
+ "application/octet-stream"
350
+
351
+ response.headers["Content-Disposition"] =
352
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
353
+
354
+ yield response.stream
355
+ ensure
356
+ response.stream.close
357
+ end
358
+
294
359
  private
295
360
  # Spawn a new thread to serve up the controller in. This is to get
296
361
  # around the fact that Rack isn't based around IOs and we need to use
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "abstract_controller/collector"
4
4
 
5
- module ActionController #:nodoc:
5
+ module ActionController # :nodoc:
6
6
  module MimeResponds
7
7
  # Without web-service support, an action which collects the data for displaying a list of people
8
8
  # might look something like this:
@@ -32,7 +32,7 @@ module ActionController #:nodoc:
32
32
  #
33
33
  # What that says is, "if the client wants HTML or JS in response to this action, just respond as we
34
34
  # would have before, but if the client wants XML, return them the list of people in XML format."
35
- # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
35
+ # (\Rails determines the desired response format from the HTTP Accept header submitted by the client.)
36
36
  #
37
37
  # Supposing you have an action that adds a new person, optionally creating their company
38
38
  # (by name) if it does not already exist, without web-services, it might look like this:
@@ -98,12 +98,12 @@ module ActionController #:nodoc:
98
98
  #
99
99
  # Note that you can define your own XML parameter parser which would allow you to describe multiple entities
100
100
  # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
101
- # and accept Rails' defaults, life will be much easier.
101
+ # and accept \Rails' defaults, life will be much easier.
102
102
  #
103
103
  # If you need to use a MIME type which isn't supported by default, you can register your own handlers in
104
104
  # +config/initializers/mime_types.rb+ as follows.
105
105
  #
106
- # Mime::Type.register "image/jpg", :jpg
106
+ # Mime::Type.register "image/jpeg", :jpg
107
107
  #
108
108
  # +respond_to+ also allows you to specify a common block for different formats by using +any+:
109
109
  #
@@ -289,7 +289,7 @@ module ActionController #:nodoc:
289
289
  @format = request.negotiate_mime(@responses.keys)
290
290
  end
291
291
 
292
- class VariantCollector #:nodoc:
292
+ class VariantCollector # :nodoc:
293
293
  def initialize(variant = nil)
294
294
  @variant = variant
295
295
  @variants = {}
@@ -6,14 +6,19 @@ require "active_support/core_ext/module/anonymous"
6
6
  require "action_dispatch/http/mime_type"
7
7
 
8
8
  module ActionController
9
+ # = Action Controller Params Wrapper
10
+ #
9
11
  # Wraps the parameters hash into a nested hash. This will allow clients to
10
12
  # submit requests without having to specify any root elements.
11
13
  #
12
- # This functionality is enabled in +config/initializers/wrap_parameters.rb+
13
- # and can be customized.
14
+ # This functionality is enabled by default for JSON, and can be customized by
15
+ # setting the format array:
16
+ #
17
+ # class ApplicationController < ActionController::Base
18
+ # wrap_parameters format: [:json, :xml]
19
+ # end
14
20
  #
15
- # You could also turn it on per controller by setting the format array to
16
- # a non-empty array:
21
+ # You could also turn it on per controller:
17
22
  #
18
23
  # class UsersController < ApplicationController
19
24
  # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
@@ -65,9 +70,15 @@ module ActionController
65
70
  # class Admin::UsersController < ApplicationController
66
71
  # end
67
72
  #
68
- # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
73
+ # will try to check if +Admin::User+ or +User+ model exists, and use it to
69
74
  # determine the wrapper key respectively. If both models don't exist,
70
- # it will then fallback to use +user+ as the key.
75
+ # it will then fall back to use +user+ as the key.
76
+ #
77
+ # To disable this functionality for a controller:
78
+ #
79
+ # class UsersController < ApplicationController
80
+ # wrap_parameters false
81
+ # end
71
82
  module ParamsWrapper
72
83
  extend ActiveSupport::Concern
73
84
 
@@ -242,14 +253,14 @@ module ActionController
242
253
  end
243
254
  end
244
255
 
245
- # Performs parameters wrapping upon the request. Called automatically
246
- # by the metal call stack.
247
- def process_action(*)
248
- _perform_parameter_wrapping if _wrapper_enabled?
249
- super
250
- end
251
-
252
256
  private
257
+ # Performs parameters wrapping upon the request. Called automatically
258
+ # by the metal call stack.
259
+ def process_action(*)
260
+ _perform_parameter_wrapping if _wrapper_enabled?
261
+ super
262
+ end
263
+
253
264
  # Returns the wrapper key which will be used to store wrapped parameters.
254
265
  def _wrapper_key
255
266
  _wrapper_options.name
@@ -1,42 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionController #:nodoc:
4
- # HTTP Permissions Policy is a web standard for defining a mechanism to
5
- # allow and deny the use of browser permissions in its own context, and
6
- # in content within any <iframe> elements in the document.
7
- #
8
- # Full details of HTTP Permissions Policy specification and guidelines can
9
- # be found at MDN:
10
- #
11
- # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
12
- #
13
- # Examples of usage:
14
- #
15
- # # Global policy
16
- # Rails.application.config.permissions_policy do |f|
17
- # f.camera :none
18
- # f.gyroscope :none
19
- # f.microphone :none
20
- # f.usb :none
21
- # f.fullscreen :self
22
- # f.payment :self, "https://secure.example.com"
23
- # end
24
- #
25
- # # Controller level policy
26
- # class PagesController < ApplicationController
27
- # permissions_policy do |p|
28
- # p.geolocation "https://example.com"
29
- # end
30
- # end
3
+ module ActionController # :nodoc:
31
4
  module PermissionsPolicy
32
5
  extend ActiveSupport::Concern
33
6
 
34
7
  module ClassMethods
8
+ # Overrides parts of the globally configured +Feature-Policy+
9
+ # header:
10
+ #
11
+ # class PagesController < ApplicationController
12
+ # permissions_policy do |policy|
13
+ # policy.geolocation "https://example.com"
14
+ # end
15
+ # end
16
+ #
17
+ # Options can be passed similar to +before_action+. For example, pass
18
+ # <tt>only: :index</tt> to override the header on the index action only:
19
+ #
20
+ # class PagesController < ApplicationController
21
+ # permissions_policy(only: :index) do |policy|
22
+ # policy.camera :self
23
+ # end
24
+ # end
25
+ #
35
26
  def permissions_policy(**options, &block)
36
27
  before_action(options) do
37
28
  if block_given?
38
29
  policy = request.permissions_policy.clone
39
- yield policy
30
+ instance_exec(policy, &block)
40
31
  request.permissions_policy = policy
41
32
  end
42
33
  end
@@ -7,9 +7,13 @@ module ActionController
7
7
  include AbstractController::Logger
8
8
  include ActionController::UrlFor
9
9
 
10
+ class UnsafeRedirectError < StandardError; end
11
+
10
12
  ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
11
13
 
12
- class UnsafeRedirectError < StandardError; end
14
+ included do
15
+ mattr_accessor :raise_on_open_redirects, default: false
16
+ end
13
17
 
14
18
  # Redirects the browser to the target specified in +options+. This parameter can be any one of:
15
19
  #
@@ -19,7 +23,7 @@ module ActionController
19
23
  # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
20
24
  # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
21
25
  #
22
- # === Examples:
26
+ # === Examples
23
27
  #
24
28
  # redirect_to action: "show", id: 5
25
29
  # redirect_to @post
@@ -58,18 +62,44 @@ module ActionController
58
62
  #
59
63
  # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
60
64
  # To terminate the execution of the function immediately after the +redirect_to+, use return.
65
+ #
61
66
  # redirect_to post_url(@post) and return
67
+ #
68
+ # === Open Redirect protection
69
+ #
70
+ # By default, \Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
71
+ # Note: this was a new default in \Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt>
72
+ #
73
+ # Here #redirect_to automatically validates the potentially-unsafe URL:
74
+ #
75
+ # redirect_to params[:redirect_url]
76
+ #
77
+ # Raises UnsafeRedirectError in the case of an unsafe redirect.
78
+ #
79
+ # To allow any external redirects pass <tt>allow_other_host: true</tt>, though using a user-provided param in that case is unsafe.
80
+ #
81
+ # redirect_to "https://rubyonrails.org", allow_other_host: true
82
+ #
83
+ # See #url_from for more information on what an internal and safe URL is, or how to fall back to an alternate redirect URL in the unsafe case.
62
84
  def redirect_to(options = {}, response_options = {})
63
85
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
64
86
  raise AbstractController::DoubleRenderError if response_body
65
87
 
66
- self.status = _extract_redirect_to_status(options, response_options)
88
+ allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
89
+
90
+ self.status = _extract_redirect_to_status(options, response_options)
67
91
 
68
92
  redirect_to_location = _compute_redirect_to_location(request, options)
69
93
  _ensure_url_is_http_header_safe(redirect_to_location)
70
94
 
71
- self.location = redirect_to_location
72
- self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
95
+ self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
96
+ self.response_body = ""
97
+ end
98
+
99
+ # Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
100
+ # of the first positional argument.
101
+ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
102
+ redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
73
103
  end
74
104
 
75
105
  # Redirects the browser to the page that issued the request (the referrer)
@@ -81,35 +111,37 @@ module ActionController
81
111
  # subject to browser security settings and user preferences. If the request
82
112
  # is missing this header, the <tt>fallback_location</tt> will be used.
83
113
  #
84
- # redirect_back fallback_location: { action: "show", id: 5 }
85
- # redirect_back fallback_location: @post
86
- # redirect_back fallback_location: "http://www.rubyonrails.org"
87
- # redirect_back fallback_location: "/images/screenshot.jpg"
88
- # redirect_back fallback_location: posts_url
89
- # redirect_back fallback_location: proc { edit_post_url(@post) }
90
- # redirect_back fallback_location: '/', allow_other_host: false
114
+ # redirect_back_or_to({ action: "show", id: 5 })
115
+ # redirect_back_or_to @post
116
+ # redirect_back_or_to "http://www.rubyonrails.org"
117
+ # redirect_back_or_to "/images/screenshot.jpg"
118
+ # redirect_back_or_to posts_url
119
+ # redirect_back_or_to proc { edit_post_url(@post) }
120
+ # redirect_back_or_to '/', allow_other_host: false
91
121
  #
92
122
  # ==== Options
93
- # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
94
123
  # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
95
124
  #
96
125
  # All other options that can be passed to #redirect_to are accepted as
97
- # options and the behavior is identical.
98
- def redirect_back(fallback_location:, allow_other_host: true, **args)
99
- referer = request.headers["Referer"]
100
- redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
101
- redirect_to redirect_to_referer ? referer : fallback_location, **args
126
+ # options, and the behavior is identical.
127
+ def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
128
+ if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
129
+ redirect_to request.referer, allow_other_host: allow_other_host, **options
130
+ else
131
+ # The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over.
132
+ redirect_to fallback_location, **options
133
+ end
102
134
  end
103
135
 
104
- def _compute_redirect_to_location(request, options) #:nodoc:
136
+ def _compute_redirect_to_location(request, options) # :nodoc:
105
137
  case options
106
138
  # The scheme name consist of a letter followed by any combination of
107
139
  # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
108
140
  # characters; and is terminated by a colon (":").
109
141
  # See https://tools.ietf.org/html/rfc3986#section-3.1
110
142
  # The protocol relative scheme starts with a double slash "//".
111
- when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
112
- options
143
+ when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
144
+ options.to_str
113
145
  when String
114
146
  request.protocol + request.host_with_port + options
115
147
  when Proc
@@ -121,7 +153,35 @@ module ActionController
121
153
  module_function :_compute_redirect_to_location
122
154
  public :_compute_redirect_to_location
123
155
 
156
+ # Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not.
157
+ # Useful to wrap a params provided redirect URL and fall back to an alternate URL to redirect to:
158
+ #
159
+ # redirect_to url_from(params[:redirect_url]) || root_url
160
+ #
161
+ # The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>:
162
+ #
163
+ # # If request.host is example.com:
164
+ # url_from("https://example.com/profile") # => "https://example.com/profile"
165
+ # url_from("http://example.com/profile") # => "http://example.com/profile"
166
+ # url_from("http://evil.com/profile") # => nil
167
+ #
168
+ # Subdomains are considered part of the host:
169
+ #
170
+ # # If request.host is on https://example.com or https://app.example.com, you'd get:
171
+ # url_from("https://dev.example.com/profile") # => nil
172
+ #
173
+ # NOTE: there's a similarity with {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor#url_for], which generates an internal URL from various options from within the app, e.g. <tt>url_for(@post)</tt>.
174
+ # However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>.
175
+ def url_from(location)
176
+ location = location.presence
177
+ location if location && _url_host_allowed?(location)
178
+ end
179
+
124
180
  private
181
+ def _allow_other_host
182
+ !raise_on_open_redirects
183
+ end
184
+
125
185
  def _extract_redirect_to_status(options, response_options)
126
186
  if options.is_a?(Hash) && options.key?(:status)
127
187
  Rack::Utils.status_code(options.delete(:status))
@@ -132,8 +192,21 @@ module ActionController
132
192
  end
133
193
  end
134
194
 
195
+ def _enforce_open_redirect_protection(location, allow_other_host:)
196
+ if allow_other_host || _url_host_allowed?(location)
197
+ location
198
+ else
199
+ raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
200
+ end
201
+ end
202
+
135
203
  def _url_host_allowed?(url)
136
- URI(url.to_s).host == request.host
204
+ host = URI(url.to_s).host
205
+
206
+ return true if host == request.host
207
+ return false unless host.nil?
208
+ return false unless url.to_s.start_with?("/")
209
+ !url.to_s.start_with?("//")
137
210
  rescue ArgumentError, URI::Error
138
211
  false
139
212
  end
@@ -142,7 +215,7 @@ module ActionController
142
215
  # Attempt to comply with the set of valid token characters
143
216
  # defined for an HTTP header value in
144
217
  # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
145
- if url.match(ILLEGAL_HEADER_VALUE_REGEX)
218
+ if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
146
219
  msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
147
220
  "Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"
148
221
  raise UnsafeRedirectError, msg