actionpack 6.1.3.2 → 7.0.0.alpha2

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -387
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -3
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +7 -21
  7. data/lib/abstract_controller/caching/fragments.rb +2 -2
  8. data/lib/abstract_controller/caching.rb +1 -1
  9. data/lib/abstract_controller/callbacks.rb +9 -8
  10. data/lib/abstract_controller/collector.rb +4 -2
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +3 -2
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/translation.rb +0 -2
  16. data/lib/abstract_controller/url_for.rb +4 -6
  17. data/lib/action_controller/api.rb +1 -1
  18. data/lib/action_controller/log_subscriber.rb +3 -1
  19. data/lib/action_controller/metal/conditional_get.rb +38 -1
  20. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  21. data/lib/action_controller/metal/cookies.rb +1 -1
  22. data/lib/action_controller/metal/data_streaming.rb +5 -13
  23. data/lib/action_controller/metal/etag_with_template_digest.rb +1 -1
  24. data/lib/action_controller/metal/exceptions.rb +19 -30
  25. data/lib/action_controller/metal/flash.rb +6 -2
  26. data/lib/action_controller/metal/http_authentication.rb +15 -15
  27. data/lib/action_controller/metal/instrumentation.rb +55 -52
  28. data/lib/action_controller/metal/live.rb +52 -3
  29. data/lib/action_controller/metal/mime_responds.rb +3 -3
  30. data/lib/action_controller/metal/params_wrapper.rb +10 -9
  31. data/lib/action_controller/metal/permissions_policy.rb +1 -1
  32. data/lib/action_controller/metal/query_tags.rb +16 -0
  33. data/lib/action_controller/metal/redirecting.rb +50 -16
  34. data/lib/action_controller/metal/rendering.rb +7 -7
  35. data/lib/action_controller/metal/request_forgery_protection.rb +64 -20
  36. data/lib/action_controller/metal/rescue.rb +1 -1
  37. data/lib/action_controller/metal/streaming.rb +1 -3
  38. data/lib/action_controller/metal/strong_parameters.rb +24 -28
  39. data/lib/action_controller/metal/testing.rb +0 -2
  40. data/lib/action_controller/metal.rb +7 -10
  41. data/lib/action_controller/railtie.rb +42 -5
  42. data/lib/action_controller/test_case.rb +9 -2
  43. data/lib/action_controller.rb +2 -5
  44. data/lib/action_dispatch/http/cache.rb +18 -12
  45. data/lib/action_dispatch/http/content_security_policy.rb +39 -35
  46. data/lib/action_dispatch/http/filter_parameters.rb +5 -0
  47. data/lib/action_dispatch/http/mime_negotiation.rb +13 -3
  48. data/lib/action_dispatch/http/mime_type.rb +9 -11
  49. data/lib/action_dispatch/http/parameters.rb +4 -4
  50. data/lib/action_dispatch/http/permissions_policy.rb +1 -1
  51. data/lib/action_dispatch/http/request.rb +10 -19
  52. data/lib/action_dispatch/http/response.rb +3 -3
  53. data/lib/action_dispatch/http/url.rb +9 -10
  54. data/lib/action_dispatch/journey/formatter.rb +2 -2
  55. data/lib/action_dispatch/journey/gtg/builder.rb +11 -12
  56. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -4
  57. data/lib/action_dispatch/journey/gtg/transition_table.rb +77 -21
  58. data/lib/action_dispatch/journey/nodes/node.rb +70 -5
  59. data/lib/action_dispatch/journey/path/pattern.rb +22 -13
  60. data/lib/action_dispatch/journey/route.rb +5 -12
  61. data/lib/action_dispatch/journey/router/utils.rb +2 -2
  62. data/lib/action_dispatch/journey/router.rb +1 -1
  63. data/lib/action_dispatch/journey/routes.rb +3 -3
  64. data/lib/action_dispatch/journey/visitors.rb +1 -1
  65. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  66. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  67. data/lib/action_dispatch/middleware/actionable_exceptions.rb +0 -1
  68. data/lib/action_dispatch/middleware/cookies.rb +7 -3
  69. data/lib/action_dispatch/middleware/debug_exceptions.rb +6 -4
  70. data/lib/action_dispatch/middleware/debug_locks.rb +3 -3
  71. data/lib/action_dispatch/middleware/exception_wrapper.rb +4 -0
  72. data/lib/action_dispatch/middleware/flash.rb +9 -11
  73. data/lib/action_dispatch/middleware/host_authorization.rb +9 -17
  74. data/lib/action_dispatch/middleware/remote_ip.rb +16 -4
  75. data/lib/action_dispatch/middleware/session/abstract_store.rb +1 -1
  76. data/lib/action_dispatch/middleware/show_exceptions.rb +7 -9
  77. data/lib/action_dispatch/middleware/stack.rb +27 -9
  78. data/lib/action_dispatch/middleware/static.rb +2 -5
  79. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +1 -1
  80. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -11
  81. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +2 -2
  82. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +3 -3
  83. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +1 -1
  84. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +4 -4
  85. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -3
  86. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +1 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +28 -18
  88. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +3 -3
  89. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +3 -3
  90. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  91. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  92. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +3 -3
  93. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +5 -14
  94. data/lib/action_dispatch/railtie.rb +8 -2
  95. data/lib/action_dispatch/request/session.rb +43 -13
  96. data/lib/action_dispatch/routing/mapper.rb +44 -72
  97. data/lib/action_dispatch/routing/redirection.rb +0 -2
  98. data/lib/action_dispatch/routing/route_set.rb +9 -6
  99. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  100. data/lib/action_dispatch/routing/url_for.rb +1 -2
  101. data/lib/action_dispatch/routing.rb +2 -2
  102. data/lib/action_dispatch/system_test_case.rb +5 -5
  103. data/lib/action_dispatch/system_testing/driver.rb +24 -4
  104. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +10 -6
  105. data/lib/action_dispatch/testing/assertions.rb +2 -5
  106. data/lib/action_dispatch/testing/integration.rb +6 -8
  107. data/lib/action_dispatch/testing/test_process.rb +12 -9
  108. data/lib/action_dispatch.rb +1 -1
  109. data/lib/action_pack/gem_version.rb +4 -4
  110. data/lib/action_pack.rb +1 -1
  111. metadata +21 -20
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionController #:nodoc:
3
+ module ActionController # :nodoc:
4
4
  module Flash
5
5
  extend ActiveSupport::Concern
6
6
 
@@ -41,10 +41,14 @@ module ActionController #:nodoc:
41
41
  self._flash_types += [type]
42
42
  end
43
43
  end
44
+
45
+ def action_methods # :nodoc:
46
+ @action_methods ||= super - _flash_types.map(&:to_s).to_set
47
+ end
44
48
  end
45
49
 
46
50
  private
47
- def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
51
+ def redirect_to(options = {}, response_options_and_flash = {}) # :doc:
48
52
  self.class._flash_types.each do |flash_type|
49
53
  if type = response_options_and_flash.delete(flash_type)
50
54
  flash[flash_type] = type
@@ -4,9 +4,9 @@ require "base64"
4
4
  require "active_support/security_utils"
5
5
 
6
6
  module ActionController
7
- # Makes it dead easy to do HTTP Basic, Digest and Token authentication.
7
+ # HTTP Basic, Digest and Token authentication.
8
8
  module HttpAuthentication
9
- # Makes it dead easy to do HTTP \Basic authentication.
9
+ # HTTP \Basic authentication.
10
10
  #
11
11
  # === Simple \Basic example
12
12
  #
@@ -24,8 +24,8 @@ module ActionController
24
24
  #
25
25
  # === Advanced \Basic example
26
26
  #
27
- # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
28
- # the regular HTML interface is protected by a session approach:
27
+ # Here is a more advanced \Basic example where only Atom feeds and the XML API are protected by HTTP authentication.
28
+ # The regular HTML interface is protected by a session approach:
29
29
  #
30
30
  # class ApplicationController < ActionController::Base
31
31
  # before_action :set_account, :authenticate
@@ -134,15 +134,15 @@ module ActionController
134
134
  end
135
135
  end
136
136
 
137
- # Makes it dead easy to do HTTP \Digest authentication.
137
+ # HTTP \Digest authentication.
138
138
  #
139
139
  # === Simple \Digest example
140
140
  #
141
- # require "digest/md5"
141
+ # require "openssl"
142
142
  # class PostsController < ApplicationController
143
143
  # REALM = "SuperSecret"
144
144
  # USERS = {"dhh" => "secret", #plain text password
145
- # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
145
+ # "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
146
146
  #
147
147
  # before_action :authenticate, except: [:index]
148
148
  #
@@ -230,12 +230,12 @@ module ActionController
230
230
  # of a plain-text password.
231
231
  def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
232
232
  ha1 = password_is_ha1 ? password : ha1(credentials, password)
233
- ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
234
- ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
233
+ ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
234
+ OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
235
235
  end
236
236
 
237
237
  def ha1(credentials, password)
238
- ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
238
+ OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
239
239
  end
240
240
 
241
241
  def encode_credentials(http_method, credentials, password, password_is_ha1)
@@ -309,7 +309,7 @@ module ActionController
309
309
  def nonce(secret_key, time = Time.now)
310
310
  t = time.to_i
311
311
  hashed = [t, secret_key]
312
- digest = ::Digest::MD5.hexdigest(hashed.join(":"))
312
+ digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
313
313
  ::Base64.strict_encode64("#{t}:#{digest}")
314
314
  end
315
315
 
@@ -326,11 +326,11 @@ module ActionController
326
326
 
327
327
  # Opaque based on digest of secret key
328
328
  def opaque(secret_key)
329
- ::Digest::MD5.hexdigest(secret_key)
329
+ OpenSSL::Digest::MD5.hexdigest(secret_key)
330
330
  end
331
331
  end
332
332
 
333
- # Makes it dead easy to do HTTP Token authentication.
333
+ # HTTP Token authentication.
334
334
  #
335
335
  # Simple Token example:
336
336
  #
@@ -358,8 +358,8 @@ module ActionController
358
358
  # end
359
359
  #
360
360
  #
361
- # Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
362
- # the regular HTML interface is protected by a session approach:
361
+ # Here is a more advanced Token example where only Atom feeds and the XML API are protected by HTTP token authentication.
362
+ # The regular HTML interface is protected by a session approach:
363
363
  #
364
364
  # class ApplicationController < ActionController::Base
365
365
  # before_action :set_account, :authenticate
@@ -16,30 +16,6 @@ module ActionController
16
16
 
17
17
  attr_internal :view_runtime
18
18
 
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
41
- end
42
-
43
19
  def render(*)
44
20
  render_output = nil
45
21
  self.view_runtime = cleanup_view_runtime do
@@ -70,37 +46,64 @@ module ActionController
70
46
  end
71
47
  end
72
48
 
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
49
+ private
50
+ def process_action(*)
51
+ raw_payload = {
52
+ controller: self.class.name,
53
+ action: action_name,
54
+ request: request,
55
+ params: request.filtered_parameters,
56
+ headers: request.headers,
57
+ format: request.format.ref,
58
+ method: request.request_method,
59
+ path: request.fullpath
60
+ }
78
61
 
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
62
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
88
63
 
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
64
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
65
+ result = super
66
+ payload[:response] = response
67
+ payload[:status] = response.status
68
+ result
69
+ rescue => error
70
+ payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
71
+ raise
72
+ ensure
73
+ append_info_to_payload(payload)
74
+ end
75
+ end
94
76
 
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
77
+ # A hook invoked every time a before callback is halted.
78
+ def halted_callback_hook(filter, _)
79
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
80
+ end
81
+
82
+ # A hook which allows you to clean up any time, wrongly taken into account in
83
+ # views, like database querying time.
84
+ #
85
+ # def cleanup_view_runtime
86
+ # super - time_taken_in_something_expensive
87
+ # end
88
+ def cleanup_view_runtime # :doc:
89
+ yield
90
+ end
91
+
92
+ # Every time after an action is processed, this method is invoked
93
+ # with the payload, so you can add more information.
94
+ def append_info_to_payload(payload) # :doc:
95
+ payload[:view_runtime] = view_runtime
96
+ end
97
+
98
+ module ClassMethods
99
+ # A hook which allows other frameworks to log what happened during
100
+ # controller process action. This method should return an array
101
+ # with the messages to be added.
102
+ def log_process_action(payload) # :nodoc:
103
+ messages, view_runtime = [], payload[:view_runtime]
104
+ messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
105
+ messages
106
+ end
103
107
  end
104
- end
105
108
  end
106
109
  end
@@ -124,9 +124,14 @@ module ActionController
124
124
  class ClientDisconnected < RuntimeError
125
125
  end
126
126
 
127
- class Buffer < ActionDispatch::Response::Buffer #:nodoc:
127
+ class Buffer < ActionDispatch::Response::Buffer # :nodoc:
128
128
  include MonitorMixin
129
129
 
130
+ class << self
131
+ attr_accessor :queue_size
132
+ end
133
+ @queue_size = 10
134
+
130
135
  # Ignore that the client has disconnected.
131
136
  #
132
137
  # If this value is `true`, calling `write` after the client
@@ -136,7 +141,7 @@ module ActionController
136
141
  attr_accessor :ignore_disconnect
137
142
 
138
143
  def initialize(response)
139
- super(response, SizedQueue.new(10))
144
+ super(response, build_queue(self.class.queue_size))
140
145
  @error_callback = lambda { true }
141
146
  @cv = new_cond
142
147
  @aborted = false
@@ -163,6 +168,11 @@ module ActionController
163
168
  end
164
169
  end
165
170
 
171
+ # Same as +write+ but automatically include a newline at the end of the string.
172
+ def writeln(string)
173
+ write string.end_with?("\n") ? string : "#{string}\n"
174
+ end
175
+
166
176
  # Write a 'close' event to the buffer; the producer/writing thread
167
177
  # uses this to notify us that it's finished supplying content.
168
178
  #
@@ -214,9 +224,13 @@ module ActionController
214
224
  yield str
215
225
  end
216
226
  end
227
+
228
+ def build_queue(queue_size)
229
+ queue_size ? SizedQueue.new(queue_size) : Queue.new
230
+ end
217
231
  end
218
232
 
219
- class Response < ActionDispatch::Response #:nodoc: all
233
+ class Response < ActionDispatch::Response # :nodoc: all
220
234
  private
221
235
  def before_committed
222
236
  super
@@ -282,6 +296,41 @@ module ActionController
282
296
  response.close if response
283
297
  end
284
298
 
299
+ # Sends a stream to the browser, which is helpful when you're generating exports or other running data where you
300
+ # don't want the entire file buffered in memory first. Similar to send_data, but where the data is generated live.
301
+ #
302
+ # Options:
303
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
304
+ # * <tt>:type</tt> - specifies an HTTP content type.
305
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
306
+ # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
307
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
308
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
309
+ # Valid values are 'inline' and 'attachment' (default).
310
+ #
311
+ # Example of generating a csv export:
312
+ #
313
+ # send_stream(filename: "subscribers.csv") do |stream|
314
+ # stream.write "email_address,updated_at\n"
315
+ #
316
+ # @subscribers.find_each do |subscriber|
317
+ # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
318
+ # end
319
+ # end
320
+ def send_stream(filename:, disposition: "attachment", type: nil)
321
+ response.headers["Content-Type"] =
322
+ (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
323
+ Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete(".")) ||
324
+ "application/octet-stream"
325
+
326
+ response.headers["Content-Disposition"] =
327
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
328
+
329
+ yield response.stream
330
+ ensure
331
+ response.stream.close
332
+ end
333
+
285
334
  private
286
335
  # Spawn a new thread to serve up the controller in. This is to get
287
336
  # 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:
@@ -103,7 +103,7 @@ module ActionController #:nodoc:
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 = {}
@@ -242,14 +242,14 @@ module ActionController
242
242
  end
243
243
  end
244
244
 
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
245
  private
246
+ # Performs parameters wrapping upon the request. Called automatically
247
+ # by the metal call stack.
248
+ def process_action(*)
249
+ _perform_parameter_wrapping if _wrapper_enabled?
250
+ super
251
+ end
252
+
253
253
  # Returns the wrapper key which will be used to store wrapped parameters.
254
254
  def _wrapper_key
255
255
  _wrapper_options.name
@@ -281,7 +281,10 @@ module ActionController
281
281
  return false unless request.has_content_type?
282
282
 
283
283
  ref = request.content_mime_type.ref
284
+
284
285
  _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
286
+ rescue ActionDispatch::Http::Parameters::ParseError
287
+ false
285
288
  end
286
289
 
287
290
  def _perform_parameter_wrapping
@@ -295,8 +298,6 @@ module ActionController
295
298
 
296
299
  # This will display the wrapped hash in the log file.
297
300
  request.filtered_parameters.merge! wrapped_filtered_hash
298
- rescue ActionDispatch::Http::Parameters::ParseError
299
- # swallow parse error exception
300
301
  end
301
302
  end
302
303
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionController #:nodoc:
3
+ module ActionController # :nodoc:
4
4
  # HTTP Permissions Policy is a web standard for defining a mechanism to
5
5
  # allow and deny the use of browser permissions in its own context, and
6
6
  # in content within any <iframe> elements in the document.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ module QueryTags # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ around_action :expose_controller_to_query_logs
9
+ end
10
+
11
+ private
12
+ def expose_controller_to_query_logs(&block)
13
+ ActiveRecord::QueryLogs.set_context(controller: self, &block)
14
+ end
15
+ end
16
+ end
@@ -7,6 +7,10 @@ module ActionController
7
7
  include AbstractController::Logger
8
8
  include ActionController::UrlFor
9
9
 
10
+ included do
11
+ mattr_accessor :raise_on_open_redirects, default: false
12
+ end
13
+
10
14
  # Redirects the browser to the target specified in +options+. This parameter can be any one of:
11
15
  #
12
16
  # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
@@ -54,16 +58,28 @@ module ActionController
54
58
  #
55
59
  # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
56
60
  # To terminate the execution of the function immediately after the +redirect_to+, use return.
61
+ #
57
62
  # redirect_to post_url(@post) and return
63
+ #
64
+ # Passing user input directly into +redirect_to+ is considered dangerous (e.g. `redirect_to(params[:location])`).
65
+ # Always use regular expressions or a permitted list when redirecting to a user specified location.
58
66
  def redirect_to(options = {}, response_options = {})
67
+ response_options[:allow_other_host] ||= _allow_other_host unless response_options.key?(:allow_other_host)
68
+
59
69
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
60
70
  raise AbstractController::DoubleRenderError if response_body
61
71
 
62
72
  self.status = _extract_redirect_to_status(options, response_options)
63
- self.location = _compute_redirect_to_location(request, options)
73
+ self.location = _compute_safe_redirect_to_location(request, options, response_options)
64
74
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
65
75
  end
66
76
 
77
+ # Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
78
+ # of the first positional argument.
79
+ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
80
+ redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
81
+ end
82
+
67
83
  # Redirects the browser to the page that issued the request (the referrer)
68
84
  # if possible, otherwise redirects to the provided default fallback
69
85
  # location.
@@ -73,35 +89,49 @@ module ActionController
73
89
  # subject to browser security settings and user preferences. If the request
74
90
  # is missing this header, the <tt>fallback_location</tt> will be used.
75
91
  #
76
- # redirect_back fallback_location: { action: "show", id: 5 }
77
- # redirect_back fallback_location: @post
78
- # redirect_back fallback_location: "http://www.rubyonrails.org"
79
- # redirect_back fallback_location: "/images/screenshot.jpg"
80
- # redirect_back fallback_location: posts_url
81
- # redirect_back fallback_location: proc { edit_post_url(@post) }
82
- # redirect_back fallback_location: '/', allow_other_host: false
92
+ # redirect_back_or_to({ action: "show", id: 5 })
93
+ # redirect_back_or_to @post
94
+ # redirect_back_or_to "http://www.rubyonrails.org"
95
+ # redirect_back_or_to "/images/screenshot.jpg"
96
+ # redirect_back_or_to posts_url
97
+ # redirect_back_or_to proc { edit_post_url(@post) }
98
+ # redirect_back_or_to '/', allow_other_host: false
83
99
  #
84
100
  # ==== Options
85
- # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
86
101
  # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
87
102
  #
88
103
  # All other options that can be passed to #redirect_to are accepted as
89
104
  # options and the behavior is identical.
90
- def redirect_back(fallback_location:, allow_other_host: true, **args)
91
- referer = request.headers["Referer"]
92
- redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
93
- redirect_to redirect_to_referer ? referer : fallback_location, **args
105
+ def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
106
+ location = request.referer || fallback_location
107
+ location = fallback_location unless allow_other_host || _url_host_allowed?(request.referer)
108
+ allow_other_host = true if _allow_other_host && !allow_other_host # if the fallback is an open redirect
109
+
110
+ redirect_to location, allow_other_host: allow_other_host, **options
94
111
  end
95
112
 
96
- def _compute_redirect_to_location(request, options) #:nodoc:
113
+ def _compute_safe_redirect_to_location(request, options, response_options)
114
+ location = _compute_redirect_to_location(request, options)
115
+
116
+ if response_options[:allow_other_host] || _url_host_allowed?(location)
117
+ location
118
+ else
119
+ raise(ArgumentError, <<~MSG.squish)
120
+ Unsafe redirect #{location.truncate(100).inspect},
121
+ use :allow_other_host to redirect anyway.
122
+ MSG
123
+ end
124
+ end
125
+
126
+ def _compute_redirect_to_location(request, options) # :nodoc:
97
127
  case options
98
128
  # The scheme name consist of a letter followed by any combination of
99
129
  # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
100
130
  # characters; and is terminated by a colon (":").
101
131
  # See https://tools.ietf.org/html/rfc3986#section-3.1
102
132
  # The protocol relative scheme starts with a double slash "//".
103
- when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
104
- options
133
+ when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
134
+ options.to_str
105
135
  when String
106
136
  request.protocol + request.host_with_port + options
107
137
  when Proc
@@ -114,6 +144,10 @@ module ActionController
114
144
  public :_compute_redirect_to_location
115
145
 
116
146
  private
147
+ def _allow_other_host
148
+ !raise_on_open_redirects
149
+ end
150
+
117
151
  def _extract_redirect_to_status(options, response_options)
118
152
  if options.is_a?(Hash) && options.key?(:status)
119
153
  Rack::Utils.status_code(options.delete(:status))