actionpack 8.0.4 → 8.1.0.beta1

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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -173
  3. data/lib/abstract_controller/asset_paths.rb +4 -2
  4. data/lib/abstract_controller/base.rb +10 -2
  5. data/lib/abstract_controller/caching.rb +6 -3
  6. data/lib/abstract_controller/helpers.rb +1 -1
  7. data/lib/abstract_controller/logger.rb +2 -1
  8. data/lib/action_controller/base.rb +1 -1
  9. data/lib/action_controller/caching.rb +1 -2
  10. data/lib/action_controller/form_builder.rb +1 -1
  11. data/lib/action_controller/log_subscriber.rb +7 -0
  12. data/lib/action_controller/metal/allow_browser.rb +1 -1
  13. data/lib/action_controller/metal/conditional_get.rb +25 -0
  14. data/lib/action_controller/metal/data_streaming.rb +1 -3
  15. data/lib/action_controller/metal/exceptions.rb +5 -0
  16. data/lib/action_controller/metal/flash.rb +1 -4
  17. data/lib/action_controller/metal/head.rb +3 -1
  18. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  19. data/lib/action_controller/metal/rate_limiting.rb +22 -7
  20. data/lib/action_controller/metal/redirecting.rb +61 -5
  21. data/lib/action_controller/metal/renderers.rb +27 -6
  22. data/lib/action_controller/metal/rendering.rb +7 -1
  23. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  24. data/lib/action_controller/metal/rescue.rb +9 -0
  25. data/lib/action_controller/railtie.rb +2 -6
  26. data/lib/action_dispatch/http/cache.rb +111 -1
  27. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  28. data/lib/action_dispatch/http/mime_types.rb +1 -0
  29. data/lib/action_dispatch/http/param_builder.rb +28 -27
  30. data/lib/action_dispatch/http/parameters.rb +3 -3
  31. data/lib/action_dispatch/http/permissions_policy.rb +4 -0
  32. data/lib/action_dispatch/http/query_parser.rb +12 -10
  33. data/lib/action_dispatch/http/request.rb +10 -5
  34. data/lib/action_dispatch/http/response.rb +16 -3
  35. data/lib/action_dispatch/http/url.rb +99 -3
  36. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  37. data/lib/action_dispatch/journey/gtg/transition_table.rb +33 -43
  38. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  39. data/lib/action_dispatch/journey/route.rb +45 -31
  40. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  41. data/lib/action_dispatch/journey/router.rb +59 -81
  42. data/lib/action_dispatch/journey/routes.rb +7 -0
  43. data/lib/action_dispatch/journey/visitors.rb +55 -23
  44. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  45. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  46. data/lib/action_dispatch/middleware/debug_exceptions.rb +7 -1
  47. data/lib/action_dispatch/middleware/debug_view.rb +11 -0
  48. data/lib/action_dispatch/middleware/exception_wrapper.rb +11 -5
  49. data/lib/action_dispatch/middleware/executor.rb +12 -2
  50. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -5
  51. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  52. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  53. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -2
  54. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  55. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  56. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  57. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +1 -0
  58. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  59. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  60. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  61. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  62. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  63. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  64. data/lib/action_dispatch/railtie.rb +10 -2
  65. data/lib/action_dispatch/routing/inspector.rb +4 -1
  66. data/lib/action_dispatch/routing/mapper.rb +323 -173
  67. data/lib/action_dispatch/routing/route_set.rb +2 -4
  68. data/lib/action_dispatch/routing/routes_proxy.rb +0 -1
  69. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  70. data/lib/action_dispatch/testing/assertions/response.rb +14 -0
  71. data/lib/action_dispatch/testing/assertions/routing.rb +11 -3
  72. data/lib/action_dispatch/testing/integration.rb +4 -3
  73. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  74. data/lib/action_pack/gem_version.rb +3 -3
  75. metadata +11 -10
@@ -55,6 +55,17 @@ module ActionDispatch
55
55
  end
56
56
  end
57
57
 
58
+ def editor_url(location, line: nil)
59
+ if editor = ActiveSupport::Editor.current
60
+ line ||= location&.lineno
61
+ absolute_path = location&.absolute_path
62
+
63
+ if absolute_path && line && File.exist?(absolute_path)
64
+ editor.url_for(absolute_path, line)
65
+ end
66
+ end
67
+ end
68
+
58
69
  def protect_against_forgery?
59
70
  false
60
71
  end
@@ -23,6 +23,7 @@ module ActionDispatch
23
23
  "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
24
24
  "ActionController::BadRequest" => :bad_request,
25
25
  "ActionController::ParameterMissing" => :bad_request,
26
+ "ActionController::TooManyRequests" => :too_many_requests,
26
27
  "Rack::QueryParser::ParameterTypeError" => :bad_request,
27
28
  "Rack::QueryParser::InvalidParameterError" => :bad_request
28
29
  )
@@ -148,15 +149,20 @@ module ActionDispatch
148
149
  application_trace_with_ids = []
149
150
  framework_trace_with_ids = []
150
151
  full_trace_with_ids = []
152
+ application_traces = application_trace.map(&:to_s)
151
153
 
154
+ full_trace = backtrace_cleaner&.clean_locations(backtrace, :all).presence || backtrace
152
155
  full_trace.each_with_index do |trace, idx|
156
+ filtered_trace = backtrace_cleaner&.clean_frame(trace, :all) || trace
157
+
153
158
  trace_with_id = {
154
159
  exception_object_id: @exception.object_id,
155
160
  id: idx,
156
- trace: trace
161
+ trace: trace,
162
+ filtered_trace: filtered_trace,
157
163
  }
158
164
 
159
- if application_trace.include?(trace)
165
+ if application_traces.include?(filtered_trace.to_s)
160
166
  application_trace_with_ids << trace_with_id
161
167
  else
162
168
  framework_trace_with_ids << trace_with_id
@@ -197,7 +203,7 @@ module ActionDispatch
197
203
 
198
204
  def source_extracts
199
205
  backtrace.map do |trace|
200
- extract_source(trace)
206
+ extract_source(trace).merge(trace: trace)
201
207
  end
202
208
  end
203
209
 
@@ -261,13 +267,13 @@ module ActionDispatch
261
267
  end
262
268
 
263
269
  (@exception.backtrace_locations || []).map do |loc|
264
- if built_methods.key?(loc.label.to_s)
270
+ if built_methods.key?(loc.base_label)
265
271
  thread_backtrace_location = if loc.respond_to?(:__getobj__)
266
272
  loc.__getobj__
267
273
  else
268
274
  loc
269
275
  end
270
- SourceMapLocation.new(thread_backtrace_location, built_methods[loc.label.to_s])
276
+ SourceMapLocation.new(thread_backtrace_location, built_methods[loc.base_label])
271
277
  else
272
278
  loc
273
279
  end
@@ -12,6 +12,10 @@ module ActionDispatch
12
12
 
13
13
  def call(env)
14
14
  state = @executor.run!(reset: true)
15
+ if response_finished = env["rack.response_finished"]
16
+ response_finished << proc { state.complete! }
17
+ end
18
+
15
19
  begin
16
20
  response = @app.call(env)
17
21
 
@@ -20,7 +24,11 @@ module ActionDispatch
20
24
  @executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
21
25
  end
22
26
 
23
- returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
27
+ unless response_finished
28
+ response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
29
+ end
30
+ returned = true
31
+ response
24
32
  rescue Exception => error
25
33
  request = ActionDispatch::Request.new env
26
34
  backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
@@ -28,7 +36,9 @@ module ActionDispatch
28
36
  @executor.error_reporter.report(wrapper.unwrapped_exception, handled: false, source: "application.action_dispatch")
29
37
  raise
30
38
  ensure
31
- state.complete! unless returned
39
+ if !returned && !response_finished
40
+ state.complete!
41
+ end
32
42
  end
33
43
  end
34
44
  end
@@ -25,11 +25,7 @@ module ActionDispatch
25
25
  def call(env)
26
26
  request = ActionDispatch::Request.new(env)
27
27
  status = request.path_info[1..-1].to_i
28
- begin
29
- content_type = request.formats.first
30
- rescue ActionDispatch::Http::MimeNegotiation::InvalidType
31
- content_type = Mime[:text]
32
- end
28
+ content_type = request.formats.first
33
29
  body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
34
30
 
35
31
  if env["action_dispatch.original_request_method"] == "HEAD"
@@ -18,11 +18,16 @@ module ActionDispatch
18
18
  # * `expire_after` - The length of time a session will be stored before
19
19
  # automatically expiring. By default, the `:expires_in` option of the cache
20
20
  # is used.
21
+ # * `check_collisions` - Check if newly generated session ids aren't already in use.
22
+ # If for some reason 128 bits of randomness aren't considered secure enough to avoid
23
+ # collisions, this option can be enabled to ensure newly generated ids aren't in use.
24
+ # By default, it is set to `false` to avoid additional cache write operations.
21
25
  #
22
26
  class CacheStore < AbstractSecureStore
23
27
  def initialize(app, options = {})
24
28
  @cache = options[:cache] || Rails.cache
25
29
  options[:expire_after] ||= @cache.options[:expires_in]
30
+ @check_collisions = options[:check_collisions] || false
26
31
  super
27
32
  end
28
33
 
@@ -61,6 +66,18 @@ module ActionDispatch
61
66
  def get_session_with_fallback(sid)
62
67
  @cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
63
68
  end
69
+
70
+ def generate_sid
71
+ if @check_collisions
72
+ loop do
73
+ sid = super
74
+ key = cache_key(sid.private_id)
75
+ break sid if @cache.write(key, {}, unless_exist: true, expires_in: default_options[:expire_after])
76
+ end
77
+ else
78
+ super
79
+ end
80
+ end
64
81
  end
65
82
  end
66
83
  end
@@ -0,0 +1 @@
1
+ <button onclick="copyAsText.bind(this)()">Copy as text</button>
@@ -11,8 +11,9 @@
11
11
  <tr>
12
12
  <td>
13
13
  <pre class="line_numbers">
14
- <% source_extract[:code].each_key do |line_number| %>
15
- <span><%= line_number -%></span>
14
+ <% source_extract[:code].each_key do |line| %>
15
+ <% file_url = editor_url(source_extract[:trace], line: line) %>
16
+ <span><%= link_to_if file_url, line, file_url -%></span>
16
17
  <% end %>
17
18
  </pre>
18
19
  </td>
@@ -13,13 +13,17 @@
13
13
  <% end %>
14
14
 
15
15
  <% traces.each do |name, trace| %>
16
- <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
16
+ <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" class="trace-container" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
17
17
  <code class="traces">
18
18
  <% trace.each do |frame| %>
19
- <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
20
- <%= frame[:trace] %>
21
- </a>
22
- <br>
19
+ <div class="trace">
20
+ <% file_url = editor_url(frame[:trace]) %>
21
+ <%= file_url && link_to("✏️", file_url, class: "edit-icon") %>
22
+ <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
23
+ <%= frame[:trace] %>
24
+ </a>
25
+ <br>
26
+ </div>
23
27
  <% end %>
24
28
  </code>
25
29
  </div>
@@ -1,4 +1,5 @@
1
1
  <header>
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Blocked hosts: <%= @hosts.join(", ") %></h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -1,4 +1,5 @@
1
1
  <header>
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception_wrapper.exception_class_name %>
4
5
  <% if params_valid? && @request.parameters['controller'] %>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception.class.to_s %>
4
5
  <% if @request.parameters['controller'] %>
@@ -38,6 +38,22 @@
38
38
  padding: 0.5em 1.5em;
39
39
  }
40
40
 
41
+ header button {
42
+ appearance: none;
43
+ background-color: hsl(0 0% 0% / 0.2);
44
+ border: 0;
45
+ border-radius: 14px;
46
+ color: white;
47
+ float: right;
48
+ font-weight: 500;
49
+ height: 28px;
50
+ padding-inline: 14px;
51
+ margin: 0.35em 0;
52
+ }
53
+ header button:active {
54
+ background-color: hsl(0 0% 0% / 0.25);
55
+ }
56
+
41
57
  h1 {
42
58
  overflow-wrap: break-word;
43
59
  margin: 0.2em 0;
@@ -54,6 +70,30 @@
54
70
  font-size: 11px;
55
71
  }
56
72
 
73
+ .trace-container {
74
+ margin-top: 10px;
75
+ }
76
+
77
+ code.traces .trace {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 2px;
81
+ }
82
+
83
+ .edit-icon {
84
+ width: 16px;
85
+ height: 16px;
86
+ display: flex;
87
+ font-size: 13px;
88
+ align-items: center;
89
+ justify-content: center;
90
+ text-decoration: none;
91
+ }
92
+
93
+ .edit-icon:hover {
94
+ scale: 1.05;
95
+ }
96
+
57
97
  .response-heading, .request-heading {
58
98
  margin-top: 30px;
59
99
  }
@@ -274,11 +314,21 @@
274
314
  var toggleEnvDump = function() {
275
315
  return toggle('env_dump');
276
316
  }
317
+ var copyAsText = function() {
318
+ const text = document.getElementById("exception-message-for-copy").textContent;
319
+
320
+ navigator.clipboard.writeText(text).then(() => {
321
+ const beforeText = this.innerText;
322
+ this.innerText = "Copied!"
323
+ setTimeout(() => this.innerText = beforeText, 1000)
324
+ })
325
+ }
277
326
  </script>
278
327
  </head>
279
328
  <body>
280
329
 
281
330
  <%= yield %>
331
+ <script type="text/plain" id="exception-message-for-copy"><%= raw @exception_message_for_copy %></script>
282
332
 
283
333
  </body>
284
334
  </html>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>No view template for interactive request</h1>
3
4
  </header>
4
5
 
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Template is missing</h1>
3
4
  </header>
4
5
 
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Routing Error</h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception_wrapper.exception_name %> in
4
5
  <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Unknown action</h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -55,8 +55,16 @@ module ActionDispatch
55
55
  ActionDispatch::Http::URL.secure_protocol = app.config.force_ssl
56
56
  ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
57
57
 
58
- ActionDispatch::ParamBuilder.ignore_leading_brackets = app.config.action_dispatch.ignore_leading_brackets
59
- ActionDispatch::QueryParser.strict_query_string_separator = app.config.action_dispatch.strict_query_string_separator
58
+ unless app.config.action_dispatch.domain_extractor.nil?
59
+ ActionDispatch::Http::URL.domain_extractor = app.config.action_dispatch.domain_extractor
60
+ end
61
+
62
+ unless app.config.action_dispatch.ignore_leading_brackets.nil?
63
+ ActionDispatch::ParamBuilder.ignore_leading_brackets = app.config.action_dispatch.ignore_leading_brackets
64
+ end
65
+ unless app.config.action_dispatch.strict_query_string_separator.nil?
66
+ ActionDispatch::QueryParser.strict_query_string_separator = app.config.action_dispatch.strict_query_string_separator
67
+ end
60
68
 
61
69
  ActiveSupport.on_load(:action_dispatch_request) do
62
70
  self.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
@@ -89,7 +89,10 @@ module ActionDispatch
89
89
 
90
90
  @engines.each do |name, engine_routes|
91
91
  formatter.section_title "Routes for #{name}"
92
- formatter.section engine_routes
92
+ if engine_routes.any?
93
+ formatter.header engine_routes
94
+ formatter.section engine_routes
95
+ end
93
96
  end
94
97
 
95
98
  formatter.result